Refactors internal Inspect code

This commit is contained in:
Shaps Benkau 2023-05-09 12:52:15 +01:00
parent 56f08c54e0
commit 8befb327b0
12 changed files with 330 additions and 257 deletions

View File

@ -1,178 +0,0 @@
import SwiftUI
import SwiftBackports
#if os(iOS) || os(macOS)
internal extension PlatformView {
func ancestor<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
var view = superview
while let s = view {
if let typed = s as? ViewType {
return typed
}
view = s.superview
}
return nil
}
var host: PlatformView? {
var view = superview
while let s = view {
if NSStringFromClass(type(of: s)).contains("ViewHost") {
return s
}
view = s.superview
}
return nil
}
func sibling<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
guard let superview = superview, let index = superview.subviews.firstIndex(of: self) else { return nil }
var views = superview.subviews
views.remove(at: index)
for subview in views.reversed() {
if let typed = subview as? ViewType {
return typed
} else if let typed = subview.descendent(ofType: type) {
return typed
}
}
return nil
}
func descendent<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
for subview in subviews {
if let typed = subview as? ViewType {
return typed
} else if let typed = subview.descendent(ofType: type) {
return typed
}
}
return nil
}
}
internal struct Inspector {
var hostView: PlatformView
var sourceView: PlatformView
var sourceController: PlatformViewController
func `any`<ViewType: PlatformView>(ofType: ViewType.Type) -> ViewType? {
ancestor(ofType: ViewType.self)
?? sibling(ofType: ViewType.self)
?? descendent(ofType: ViewType.self)
}
func ancestor<ViewType: PlatformView>(ofType: ViewType.Type) -> ViewType? {
hostView.ancestor(ofType: ViewType.self)
}
func sibling<ViewType: PlatformView>(ofType: ViewType.Type) -> ViewType? {
hostView.sibling(ofType: ViewType.self)
}
func descendent<ViewType: PlatformView>(ofType: ViewType.Type) -> ViewType? {
hostView.descendent(ofType: ViewType.self)
}
}
extension View {
private func inject<Wrapped>(_ wrapped: Wrapped) -> some View where Wrapped: View {
overlay(wrapped.frame(width: 0, height: 0))
}
func inspect<ViewType: PlatformView>(selector: @escaping (_ inspector: Inspector) -> ViewType?, customize: @escaping (ViewType) -> Void) -> some View {
inject(InspectionView(selector: selector, customize: customize))
}
func controller(_ customize: @escaping (PlatformViewController?) -> Void) -> some View {
inspect { inspector in
inspector.sourceController.view
} customize: { view in
customize(view.parentController)
}
}
}
private struct InspectionView<ViewType: PlatformView>: View {
let selector: (Inspector) -> ViewType?
let customize: (ViewType) -> Void
var body: some View {
Representable(parent: self)
}
}
private class SourceView: PlatformView {
required init() {
super.init(frame: .zero)
isHidden = true
#if os(iOS)
isUserInteractionEnabled = false
#endif
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#endif
#if os(iOS)
private extension InspectionView {
struct Representable: UIViewRepresentable {
let parent: InspectionView
func makeUIView(context: Context) -> UIView { .init() }
func updateUIView(_ view: UIView, context: Context) {
DispatchQueue.main.async {
guard let host = view.host else { return }
let inspector = Inspector(
hostView: host,
sourceView: view,
sourceController: view.parentController
?? view.window?.rootViewController
?? UIViewController()
)
guard let targetView = parent.selector(inspector) else { return }
parent.customize(targetView)
}
}
}
}
#elseif os(macOS)
private extension InspectionView {
struct Representable: NSViewRepresentable {
let parent: InspectionView
func makeNSView(context: Context) -> NSView {
.init(frame: .zero)
}
func updateNSView(_ view: NSView, context: Context) {
DispatchQueue.main.async {
guard let host = view.host else { return }
let inspector = Inspector(
hostView: host,
sourceView: view,
sourceController: view.parentController ?? NSViewController(nibName: nil, bundle: nil)
)
guard let targetView = parent.selector(inspector) else { return }
parent.customize(targetView)
}
}
}
}
#endif

View File

@ -0,0 +1,292 @@
import SwiftUI
import SwiftBackports
#if os(iOS) || os(macOS)
internal extension PlatformViewController {
func ancestor<ControllerType: PlatformViewController>(ofType type: ControllerType.Type) -> ControllerType? {
var controller = parent
while let c = controller {
if let typed = c as? ControllerType {
return typed
}
controller = c.parent
}
return nil
}
func sibling<ControllerType: PlatformViewController>(ofType type: ControllerType.Type) -> ControllerType? {
guard let controller = parent, let index = controller.children.firstIndex(of: self) else { return nil }
var children = controller.children
children.remove(at: index)
for c in children.reversed() {
if let typed = c as? ControllerType {
return typed
} else if let typed = c.descendent(ofType: type) {
return typed
}
}
return nil
}
func descendent<ControllerType: PlatformViewController>(ofType type: ControllerType.Type) -> ControllerType? {
for c in children {
if let typed = c as? ControllerType {
return typed
} else if let typed = c.descendent(ofType: type) {
return typed
}
}
return nil
}
}
internal extension PlatformView {
func ancestor<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
var view = superview
while let s = view {
if let typed = s as? ViewType {
return typed
}
view = s.superview
}
return nil
}
func sibling<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
guard let superview = superview, let index = superview.subviews.firstIndex(of: self) else { return nil }
var views = superview.subviews
views.remove(at: index)
for subview in views.reversed() {
if let typed = subview as? ViewType {
return typed
} else if let typed = subview.descendent(ofType: type) {
return typed
}
}
return nil
}
func descendent<ViewType: PlatformView>(ofType type: ViewType.Type) -> ViewType? {
for subview in subviews {
if let typed = subview as? ViewType {
return typed
} else if let typed = subview.descendent(ofType: type) {
return typed
}
}
return nil
}
var host: PlatformView? {
var view = superview
while let s = view {
if NSStringFromClass(type(of: s)).contains("ViewHost") {
return s
}
view = s.superview
}
return nil
}
}
internal struct Inspector {
var hostView: PlatformView
var sourceView: PlatformView
var sourceController: PlatformViewController
func `any`<ViewType: PlatformView>(ofType: ViewType.Type) -> ViewType? {
ancestor(ofType: ViewType.self)
?? sibling(ofType: ViewType.self)
?? descendent(ofType: ViewType.self)
}
func ancestor<ViewType: PlatformView>(ofType: ViewType.Type) -> ViewType? {
hostView.ancestor(ofType: ViewType.self)
}
func sibling<ViewType: PlatformView>(ofType: ViewType.Type) -> ViewType? {
hostView.sibling(ofType: ViewType.self)
}
func descendent<ViewType: PlatformView>(ofType: ViewType.Type) -> ViewType? {
hostView.descendent(ofType: ViewType.self)
}
func `any`<ControllerType: PlatformViewController>(ofType: ControllerType.Type) -> ControllerType? {
ancestor(ofType: ControllerType.self)
?? sibling(ofType: ControllerType.self)
?? descendent(ofType: ControllerType.self)
}
func ancestor<ControllerType: PlatformViewController>(ofType: ControllerType.Type) -> ControllerType? {
sourceController.ancestor(ofType: ControllerType.self)
}
func sibling<ControllerType: PlatformViewController>(ofType: ControllerType.Type) -> ControllerType? {
sourceController.sibling(ofType: ControllerType.self)
}
func descendent<ControllerType: PlatformViewController>(ofType: ControllerType.Type) -> ControllerType? {
sourceController.descendent(ofType: ControllerType.self)
}
}
internal struct Proxy<T> {
let inspector: Inspector
let instance: T
}
extension View {
private func inject<Wrapped>(_ wrapped: Wrapped) -> some View where Wrapped: View {
overlay(wrapped.frame(width: 0, height: 0))
}
func `any`<T: PlatformView>(forType type: T.Type, body: @escaping (Proxy<T>) -> Void) -> some View {
inject(InspectionView { inspector in
inspector.any(ofType: T.self)
} customize: { proxy in
body(proxy)
})
}
func ancestor<T: PlatformView>(forType type: T.Type, body: @escaping (Proxy<T>) -> Void) -> some View {
inject(InspectionView { inspector in
inspector.ancestor(ofType: T.self)
} customize: { proxy in
body(proxy)
})
}
func sibling<T: PlatformView>(forType type: T.Type, body: @escaping (Proxy<T>) -> Void) -> some View {
inject(InspectionView { inspector in
inspector.sibling(ofType: T.self)
} customize: { proxy in
body(proxy)
})
}
func descendent<T: PlatformView>(forType type: T.Type, body: @escaping (Proxy<T>) -> Void) -> some View {
inject(InspectionView { inspector in
inspector.descendent(ofType: T.self)
} customize: { proxy in
body(proxy)
})
}
func `any`<T: PlatformViewController>(forType type: T.Type, body: @escaping (Proxy<T>) -> Void) -> some View {
inject(InspectionView { inspector in
inspector.any(ofType: T.self)
} customize: { proxy in
body(proxy)
})
}
func ancestor<T: PlatformViewController>(forType type: T.Type, body: @escaping (Proxy<T>) -> Void) -> some View {
inject(InspectionView { inspector in
inspector.ancestor(ofType: T.self)
} customize: { proxy in
body(proxy)
})
}
func sibling<T: PlatformViewController>(forType type: T.Type, body: @escaping (Proxy<T>) -> Void) -> some View {
inject(InspectionView { inspector in
inspector.sibling(ofType: T.self)
} customize: { proxy in
body(proxy)
})
}
func descendent<T: PlatformViewController>(forType type: T.Type, body: @escaping (Proxy<T>) -> Void) -> some View {
inject(InspectionView { inspector in
inspector.descendent(ofType: T.self)
} customize: { proxy in
body(proxy)
})
}
}
private struct InspectionView<T>: View {
let selector: (Inspector) -> T?
let customize: (Proxy<T>) -> Void
var body: some View {
Representable(parent: self)
}
}
private class SourceView: PlatformView {
required init() {
super.init(frame: .zero)
isHidden = true
#if os(iOS)
isUserInteractionEnabled = false
#endif
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
#endif
#if os(iOS)
private extension InspectionView {
struct Representable: UIViewRepresentable {
let parent: InspectionView
func makeUIView(context: Context) -> UIView { .init() }
func updateUIView(_ view: UIView, context: Context) {
DispatchQueue.main.async {
guard let host = view.host else { return }
let inspector = Inspector(
hostView: host,
sourceView: view,
sourceController: view.parentController
?? view.window?.rootViewController
?? UIViewController()
)
guard let target = parent.selector(inspector) else { return }
parent.customize(.init(inspector: inspector, instance: target))
}
}
}
}
#elseif os(macOS)
private extension InspectionView {
struct Representable: NSViewRepresentable {
let parent: InspectionView
func makeNSView(context: Context) -> NSView {
.init(frame: .zero)
}
func updateNSView(_ view: NSView, context: Context) {
DispatchQueue.main.async {
guard let host = view.host else { return }
let inspector = Inspector(
hostView: host,
sourceView: view,
sourceController: view.parentController ?? NSViewController(nibName: nil, bundle: nil)
)
guard let target = parent.selector(inspector) else { return }
parent.customize(target)
}
}
}
}
#endif

View File

@ -21,9 +21,7 @@ extension Backport where Wrapped == Any {
} else {
TitleAndIconLabelStyle().makeBody(configuration: configuration)
#if os(iOS)
.inspect { inspector in
inspector.ancestor(ofType: UINavigationBar.self)
} customize: { _ in
.ancestor(forType: UINavigationBar.self) { _ in
isToolbarElement = true
}
#endif

View File

@ -28,9 +28,8 @@ extension Backport where Wrapped: View {
#if os(iOS)
wrapped
.environment(\.backportRefresh, Backport<Any>.RefreshAction(action))
.inspect { inspector in
inspector.sibling(ofType: UIScrollView.self)
} customize: { scrollView in
.any(forType: UIScrollView.self) { proxy in
let scrollView = proxy.instance
guard scrollView.refreshControl == nil else { return }
scrollView.refreshControl = RefreshControl {
await action()

View File

@ -122,18 +122,19 @@ struct ToolbarModifier: ViewModifier {
func body(content: Content) -> some View {
content
.navigationBarItems(leading: leading, trailing: trailing)
.controller { controller in
.ancestor(forType: UIViewController.self) { proxy in
let controller = proxy.instance
if !principalItems.isEmpty {
controller?.navigationItem.titleView = UIHostingController(
controller.navigationItem.titleView = UIHostingController(
rootView: principal.backport.largeScale(),
ignoreSafeArea: false
).view
controller?.navigationItem.titleView?.backgroundColor = .clear
controller.navigationItem.titleView?.backgroundColor = .clear
}
if !bottomBarItems.isEmpty {
controller?.navigationController?.setToolbarHidden(false, animated: false)
controller?.toolbarItems = bottomBarItems.map {
controller.navigationController?.setToolbarHidden(false, animated: false)
controller.toolbarItems = bottomBarItems.map {
let view = UIHostingController(rootView: $0.content.backport.largeScale()).view!
view.backgroundColor = .clear
return .init(customView: view)

View File

@ -11,8 +11,8 @@ internal struct ToolbarBackgroundModifier: ViewModifier {
func body(content: Content) -> some View {
content
.controller { controller in
wrapper.controller = controller
.ancestor(forType: UIViewController.self) { proxy in
wrapper.controller = proxy.instance
}
.backport.task {
updateNavigationBar()

View File

@ -94,15 +94,11 @@ private struct AutoCapitalizationModifier: ViewModifier {
func body(content: Content) -> some View {
content
.inspect { inspector in
inspector.any(ofType: UITextField.self)
} customize: { view in
view.autocapitalizationType = capitalization
.any(forType: UITextField.self) { proxy in
proxy.instance.autocapitalizationType = capitalization
}
.inspect { inspector in
inspector.any(ofType: UITextView.self)
} customize: { view in
view.autocapitalizationType = capitalization
.any(forType: UITextView.self) { proxy in
proxy.instance.autocapitalizationType = capitalization
}
}
}

View File

@ -18,9 +18,8 @@ private struct FocusModifier<Value: Hashable>: ViewModifier {
content
// this ensures when the field goes out of view, it doesn't retain focus
.onWillDisappear { focused = nil }
.inspect { inspector in
inspector.sibling(ofType: UITextField.self)
} customize: { view in
.sibling(forType: UITextField.self) { proxy in
let view = proxy.instance
coordinator.observe(field: view)
coordinator.onBegin = {

View File

@ -43,33 +43,23 @@ extension Backport where Wrapped: View {
/// - Parameter disabled: A Boolean that indicates whether scrolling is
/// disabled.
public func scrollDisabled(_ disabled: Bool) -> some View {
#if os(iOS)
wrapped
.environment(\.backportIsScrollEnabled, !disabled)
.inspect { inspector in
#if os(iOS)
inspector.sibling(ofType: UIScrollView.self)
?? inspector.ancestor(ofType: UIScrollView.self)
?? inspector.descendent(ofType: UIScrollView.self)
#elseif os(macOS)
inspector.sibling(ofType: NSScrollView.self)
?? inspector.ancestor(ofType: NSScrollView.self)
?? inspector.descendent(ofType: NSScrollView.self)
#endif
} customize: { scrollView in
#if os(iOS)
#if os(iOS)
.any(forType: UIScrollView.self) { proxy in
let scrollView = proxy.instance
scrollView.isScrollEnabled = !disabled
scrollView.alwaysBounceVertical = !disabled
scrollView.alwaysBounceHorizontal = !disabled
#elseif os(macOS)
}
#endif
#if os(macOS)
.any(forType: NSScrollView.self) { proxy in
let scrollView = proxy.instance
scrollView.hasHorizontalScroller = !disabled
scrollView.hasVerticalScroller = !disabled
#endif
}
#else
wrapped
.environment(\.backportIsScrollEnabled, !disabled)
#endif
#endif
}
}

View File

@ -37,18 +37,12 @@ extension Backport where Wrapped: View {
///
/// - Returns: A view with the specified scroll indicator visibility.
public func scrollIndicators(_ visibility: Backport<Any>.ScrollIndicatorVisibility, axes: Axis.Set = [.vertical]) -> some View {
#if os(iOS)
wrapped
.environment(\.backportHorizontalScrollIndicatorVisibility, axes.contains(.horizontal) ? visibility : .automatic)
.environment(\.backportVerticalScrollIndicatorVisibility, axes.contains(.vertical) ? visibility : .automatic)
.inspect { inspector in
#if os(iOS)
inspector.sibling(ofType: UIScrollView.self)
#else
inspector.sourceView
#endif
} customize: { scrollView in
#if os(iOS)
#if os(iOS)
.sibling(forType: UIScrollView.self) { proxy in
let scrollView = proxy.instance
if axes.contains(.horizontal) {
scrollView.showsHorizontalScrollIndicator = visibility.scrollViewVisible
scrollView.alwaysBounceHorizontal = true
@ -62,13 +56,8 @@ extension Backport where Wrapped: View {
} else {
scrollView.alwaysBounceVertical = false
}
#endif
}
#else
wrapped
.environment(\.backportHorizontalScrollIndicatorVisibility, axes.contains(.horizontal) ? visibility : .automatic)
.environment(\.backportVerticalScrollIndicatorVisibility, axes.contains(.vertical) ? visibility : .automatic)
#endif
#endif
}
}

View File

@ -42,25 +42,15 @@ extension Backport where Wrapped: View {
///
/// - Returns: A view that uses the specified keyboard dismissal mode.
public func scrollDismissesKeyboard(_ mode: Backport<Any>.ScrollDismissesKeyboardMode) -> some View {
#if os(iOS)
wrapped
.environment(\.backportScrollDismissesKeyboardMode, mode)
.inspect { inspector in
#if os(iOS)
inspector.sibling(ofType: UIScrollView.self)
#else
inspector.sourceView
#endif
} customize: { scrollView in
#if os(iOS)
#if os(iOS)
.sibling(forType: UIScrollView.self) { proxy in
let scrollView = proxy.instance
guard scrollView.keyboardDismissMode != mode.scrollViewDismissMode else { return }
scrollView.keyboardDismissMode = mode.scrollViewDismissMode
#endif
}
#else
wrapped
.environment(\.backportScrollDismissesKeyboardMode, mode)
#endif
#endif
}
}

View File

@ -134,14 +134,11 @@ private struct SubmitModifier: ViewModifier {
func body(content: Content) -> some View {
content
.inspect { inspector in
inspector.any(ofType: UITextView.self)
} customize: { view in
view.returnKeyType = label.returnKeyType
.any(forType: UITextView.self) { proxy in
proxy.instance.returnKeyType = label.returnKeyType
}
.inspect { inspector in
inspector.any(ofType: UITextField.self)
} customize: { view in
.any(forType: UITextField.self) { proxy in
let view = proxy.instance
view.returnKeyType = label.returnKeyType
coordinator.onReturn = { submit() }
coordinator.observe(view: view)