-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from hugehoge/customize-swiftui-introspect
Fix the timing of introspecting UIScrollView
- Loading branch information
Showing
11 changed files
with
202 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 0 additions & 16 deletions
16
Snappable.xcworkspace/xcshareddata/swiftpm/Package.resolved
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import SwiftUI | ||
|
||
typealias PlatformView = UIView | ||
|
||
/// Utility methods to inspect the UIKit view hierarchy. | ||
enum Introspect { | ||
/// Finds a previous sibling that is of the specified type. | ||
/// This method inspects siblings recursively. | ||
/// Returns nil if no sibling contains the specified type. | ||
static func previousSibling<AnyViewType: PlatformView>( | ||
ofType type: AnyViewType.Type, | ||
from entry: PlatformView | ||
) -> AnyViewType? { | ||
|
||
guard let superview = entry.superview, | ||
let entryIndex = superview.subviews.firstIndex(of: entry), | ||
entryIndex > 0 | ||
else { | ||
return nil | ||
} | ||
|
||
for subview in superview.subviews[0..<entryIndex].reversed() { | ||
if let typed = subview as? AnyViewType { | ||
return typed | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
/// Finds an ancestor of the specified type. | ||
/// If it reaches the top of the view without finding the specified view type, it returns nil. | ||
static func findAncestor<AnyViewType: PlatformView>(ofType type: AnyViewType.Type, from entry: PlatformView) -> AnyViewType? { | ||
var superview = entry.superview | ||
while let s = superview { | ||
if let typed = s as? AnyViewType { | ||
return typed | ||
} | ||
superview = s.superview | ||
} | ||
return nil | ||
} | ||
|
||
/// Finds the view host of a specific view. | ||
/// SwiftUI wraps each UIView within a ViewHost, then within a HostingView. | ||
/// Returns nil if it couldn't find a view host. This should never happen when called with an IntrospectionView. | ||
static func findViewHost(from entry: PlatformView) -> PlatformView? { | ||
var superview = entry.superview | ||
while let s = superview { | ||
if NSStringFromClass(type(of: s)).contains("ViewHost") { | ||
return s | ||
} | ||
superview = s.superview | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
enum TargetViewSelector { | ||
static func siblingOfType<TargetView: PlatformView>(from entry: PlatformView) -> TargetView? { | ||
guard let viewHost = Introspect.findViewHost(from: entry) else { | ||
return nil | ||
} | ||
return Introspect.previousSibling(ofType: TargetView.self, from: viewHost) | ||
} | ||
|
||
static func siblingOfTypeOrAncestor<TargetView: PlatformView>(from entry: PlatformView) -> TargetView? { | ||
if let sibling: TargetView = siblingOfType(from: entry) { | ||
return sibling | ||
} | ||
return Introspect.findAncestor(ofType: TargetView.self, from: entry) | ||
} | ||
} | ||
|
||
/// Allows to safely access an array element by index | ||
/// Usage: array[safe: 2] | ||
private extension Array { | ||
subscript(safe index: Int) -> Element? { | ||
guard index >= 0, index < endIndex else { | ||
return nil | ||
} | ||
|
||
return self[index] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import UIKit | ||
import SwiftUI | ||
|
||
/// Introspection UIView that is inserted alongside the target view. | ||
class IntrospectionUIView: UIView { | ||
var didMoveToWindowHandler: (() -> Void)? | ||
|
||
required init() { | ||
super.init(frame: .zero) | ||
isHidden = true | ||
isUserInteractionEnabled = false | ||
} | ||
|
||
@available(*, unavailable) | ||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
override func didMoveToWindow() { | ||
didMoveToWindowHandler?() | ||
} | ||
} | ||
|
||
/// Introspection View that is injected into the UIKit hierarchy alongside the target view. | ||
/// After `updateUIView` is called, it calls `selector` to find the target view, then `customize` when the target view is found. | ||
struct UIKitIntrospectionView<TargetViewType: UIView>: UIViewRepresentable { | ||
|
||
/// Method that introspects the view hierarchy to find the target view. | ||
/// First argument is the introspection view itself, which is contained in a view host alongside the target view. | ||
let selector: (IntrospectionUIView) -> TargetViewType? | ||
|
||
/// User-provided customization method for the target view. | ||
let customize: (TargetViewType) -> Void | ||
|
||
init( | ||
selector: @escaping (IntrospectionUIView) -> TargetViewType?, | ||
customize: @escaping (TargetViewType) -> Void | ||
) { | ||
self.selector = selector | ||
self.customize = customize | ||
} | ||
|
||
func makeUIView(context: UIViewRepresentableContext<UIKitIntrospectionView>) -> IntrospectionUIView { | ||
let view = IntrospectionUIView() | ||
view.accessibilityLabel = "IntrospectionUIView<\(TargetViewType.self)>" | ||
return view | ||
} | ||
|
||
/// When `updateUiView` is called after creating the Introspection view, it is not yet in the UIKit hierarchy. | ||
/// At this point, `introspectionView.superview.superview` is nil and we can't access the target UIKit view. | ||
/// To workaround this, we wait until the introspection view did attach to the window and the runloop is done | ||
/// inserting the introspection view in the hierarchy, then run the selector. | ||
/// Finding the target view fails silently if the selector yield no result. This happens when `updateUIView` | ||
/// gets called when the introspection view gets removed from the hierarchy. | ||
func updateUIView( | ||
_ uiView: IntrospectionUIView, | ||
context: UIViewRepresentableContext<UIKitIntrospectionView> | ||
) { | ||
uiView.didMoveToWindowHandler = { | ||
DispatchQueue.main.async { | ||
guard let targetView = self.selector(uiView) else { | ||
return | ||
} | ||
self.customize(targetView) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import SwiftUI | ||
import UIKit | ||
|
||
extension View { | ||
func inject<SomeView>(_ view: SomeView) -> some View where SomeView: View { | ||
overlay(view.frame(width: 0, height: 0)) | ||
} | ||
} | ||
|
||
extension View { | ||
/// Finds a `TargetView` from a `SwiftUI.View` | ||
func introspect<TargetView: UIView>( | ||
selector: @escaping (IntrospectionUIView) -> TargetView?, | ||
customize: @escaping (TargetView) -> () | ||
) -> some View { | ||
inject(UIKitIntrospectionView( | ||
selector: selector, | ||
customize: customize | ||
)) | ||
} | ||
|
||
/// Finds a `UIScrollView` from a `SwiftUI.ScrollView`, or `SwiftUI.ScrollView` child. | ||
func introspectScrollView(customize: @escaping (UIScrollView) -> ()) -> some View { | ||
return introspect(selector: TargetViewSelector.siblingOfTypeOrAncestor, customize: customize) | ||
} | ||
} |