parent
e7f08760f8
commit
30fcbff421
|
@ -20,17 +20,18 @@ extension View {
|
||||||
customize: @escaping (PlatformSpecificEntity) -> Void
|
customize: @escaping (PlatformSpecificEntity) -> Void
|
||||||
) -> some View {
|
) -> some View {
|
||||||
if let platform = platforms.first(where: \.isCurrent) {
|
if let platform = platforms.first(where: \.isCurrent) {
|
||||||
let anchorID = IntrospectionAnchorID()
|
let introspectionViewID = IntrospectionViewID()
|
||||||
self.background(
|
self.background(
|
||||||
IntrospectionAnchorView(
|
IntrospectionAnchorView(
|
||||||
id: anchorID
|
id: introspectionViewID
|
||||||
)
|
)
|
||||||
.frame(width: 0, height: 0)
|
.frame(width: 0, height: 0)
|
||||||
)
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
IntrospectionView(
|
IntrospectionView(
|
||||||
selector: { entity in
|
id: introspectionViewID,
|
||||||
(platform.selector ?? .default)(entity, scope ?? viewType.scope, anchorID)
|
selector: { controller in
|
||||||
|
(platform.selector ?? .default)(controller, scope ?? viewType.scope)
|
||||||
},
|
},
|
||||||
customize: customize
|
customize: customize
|
||||||
)
|
)
|
||||||
|
@ -53,9 +54,6 @@ public protocol PlatformEntity: AnyObject {
|
||||||
|
|
||||||
@_spi(Internals)
|
@_spi(Internals)
|
||||||
func isDescendant(of other: Base) -> Bool
|
func isDescendant(of other: Base) -> Bool
|
||||||
|
|
||||||
@_spi(Internals)
|
|
||||||
func entityWithTag(_ tag: Int) -> Base?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PlatformEntity {
|
extension PlatformEntity {
|
||||||
|
@ -87,12 +85,11 @@ extension PlatformEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
func receiver<PlatformSpecificEntity: PlatformEntity>(
|
func receiver<PlatformSpecificEntity: PlatformEntity>(
|
||||||
ofType type: PlatformSpecificEntity.Type,
|
ofType type: PlatformSpecificEntity.Type
|
||||||
anchorID: IntrospectionAnchorID
|
|
||||||
) -> PlatformSpecificEntity? {
|
) -> PlatformSpecificEntity? {
|
||||||
let frontEntity = self
|
let frontEntity = self
|
||||||
guard
|
guard
|
||||||
let backEntity = ContiguousArray(frontEntity.ancestors).last?.entityWithTag(anchorID.hashValue), // optimize this... maybe there's a way to hold a ref somewhere in memory without having to use tags?
|
let backEntity = frontEntity.introspectionAnchorEntity,
|
||||||
let commonAncestor = backEntity.nearestCommonAncestor(with: frontEntity~)
|
let commonAncestor = backEntity.nearestCommonAncestor(with: frontEntity~)
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -124,11 +121,6 @@ extension PlatformView: PlatformEntity {
|
||||||
public var descendants: [PlatformView] {
|
public var descendants: [PlatformView] {
|
||||||
subviews
|
subviews
|
||||||
}
|
}
|
||||||
|
|
||||||
@_spi(Internals)
|
|
||||||
public func entityWithTag(_ tag: Int) -> PlatformView? {
|
|
||||||
viewWithTag(tag)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PlatformViewController: PlatformEntity {
|
extension PlatformViewController: PlatformEntity {
|
||||||
|
@ -146,17 +138,4 @@ extension PlatformViewController: PlatformEntity {
|
||||||
public func isDescendant(of other: PlatformViewController) -> Bool {
|
public func isDescendant(of other: PlatformViewController) -> Bool {
|
||||||
self.ancestors.contains(other)
|
self.ancestors.contains(other)
|
||||||
}
|
}
|
||||||
|
|
||||||
@_spi(Internals)
|
|
||||||
public func entityWithTag(_ tag: Int) -> PlatformViewController? {
|
|
||||||
if self.view.tag == tag {
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
for child in children {
|
|
||||||
if let childWithTag = child.entityWithTag(tag) {
|
|
||||||
return childWithTag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,20 +6,20 @@ public struct IntrospectionSelector<Target: PlatformEntity> {
|
||||||
@_spi(Internals)
|
@_spi(Internals)
|
||||||
public static func from<Entry: PlatformEntity>(_ entryType: Entry.Type, selector: @escaping (Entry) -> Target?) -> Self {
|
public static func from<Entry: PlatformEntity>(_ entryType: Entry.Type, selector: @escaping (Entry) -> Target?) -> Self {
|
||||||
.init(
|
.init(
|
||||||
receiverSelector: { controller, anchorID in
|
receiverSelector: { controller in
|
||||||
controller.as(Entry.self)?.receiver(ofType: Entry.self, anchorID: anchorID).flatMap(selector)
|
controller.as(Entry.Base.self)?.receiver(ofType: Entry.self).flatMap(selector)
|
||||||
},
|
},
|
||||||
ancestorSelector: { controller in
|
ancestorSelector: { controller in
|
||||||
controller.as(Entry.self)?.ancestor(ofType: Entry.self).flatMap(selector)
|
controller.as(Entry.Base.self)?.ancestor(ofType: Entry.self).flatMap(selector)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var receiverSelector: (IntrospectionPlatformViewController, IntrospectionAnchorID) -> Target?
|
private var receiverSelector: (IntrospectionPlatformViewController) -> Target?
|
||||||
private var ancestorSelector: (IntrospectionPlatformViewController) -> Target?
|
private var ancestorSelector: (IntrospectionPlatformViewController) -> Target?
|
||||||
|
|
||||||
private init(
|
private init(
|
||||||
receiverSelector: @escaping (IntrospectionPlatformViewController, IntrospectionAnchorID) -> Target?,
|
receiverSelector: @escaping (IntrospectionPlatformViewController) -> Target?,
|
||||||
ancestorSelector: @escaping (IntrospectionPlatformViewController) -> Target?
|
ancestorSelector: @escaping (IntrospectionPlatformViewController) -> Target?
|
||||||
) {
|
) {
|
||||||
self.receiverSelector = receiverSelector
|
self.receiverSelector = receiverSelector
|
||||||
|
@ -27,7 +27,7 @@ public struct IntrospectionSelector<Target: PlatformEntity> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@_spi(Internals)
|
@_spi(Internals)
|
||||||
public func withReceiverSelector(_ selector: @escaping (PlatformViewController, IntrospectionAnchorID) -> Target?) -> Self {
|
public func withReceiverSelector(_ selector: @escaping (PlatformViewController) -> Target?) -> Self {
|
||||||
var copy = self
|
var copy = self
|
||||||
copy.receiverSelector = selector
|
copy.receiverSelector = selector
|
||||||
return copy
|
return copy
|
||||||
|
@ -40,14 +40,10 @@ public struct IntrospectionSelector<Target: PlatformEntity> {
|
||||||
return copy
|
return copy
|
||||||
}
|
}
|
||||||
|
|
||||||
func callAsFunction(
|
func callAsFunction(_ controller: IntrospectionPlatformViewController, _ scope: IntrospectionScope) -> Target? {
|
||||||
_ controller: IntrospectionPlatformViewController,
|
|
||||||
_ scope: IntrospectionScope,
|
|
||||||
_ anchorID: IntrospectionAnchorID
|
|
||||||
) -> Target? {
|
|
||||||
if
|
if
|
||||||
scope.contains(.receiver),
|
scope.contains(.receiver),
|
||||||
let target = receiverSelector(controller, anchorID)
|
let target = receiverSelector(controller)
|
||||||
{
|
{
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
|
@ -62,14 +58,14 @@ public struct IntrospectionSelector<Target: PlatformEntity> {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PlatformViewController {
|
extension PlatformViewController {
|
||||||
func `as`<Entity: PlatformEntity>(_ entityType: Entity.Type) -> (any PlatformEntity)? {
|
func `as`<Base: PlatformEntity>(_ baseType: Base.Type) -> (any PlatformEntity)? {
|
||||||
if Entity.Base.self == PlatformView.self {
|
if Base.self == PlatformView.self {
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
return viewIfLoaded
|
return viewIfLoaded
|
||||||
#elseif canImport(AppKit)
|
#elseif canImport(AppKit)
|
||||||
return isViewLoaded ? view : nil
|
return isViewLoaded ? view : nil
|
||||||
#endif
|
#endif
|
||||||
} else if Entity.Base.self == PlatformViewController.self {
|
} else if Base.self == PlatformViewController.self {
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,7 +1,30 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@_spi(Internals)
|
typealias IntrospectionViewID = UUID
|
||||||
public typealias IntrospectionAnchorID = UUID
|
|
||||||
|
fileprivate enum IntrospectionStore {
|
||||||
|
static var shared: [IntrospectionViewID: Pair] = [:]
|
||||||
|
|
||||||
|
struct Pair {
|
||||||
|
weak var controller: IntrospectionPlatformViewController?
|
||||||
|
weak var anchor: IntrospectionAnchorPlatformViewController?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PlatformEntity {
|
||||||
|
var introspectionAnchorEntity: Base? {
|
||||||
|
if let introspectionController = self as? IntrospectionPlatformViewController {
|
||||||
|
return IntrospectionStore.shared[introspectionController.id]?.anchor~
|
||||||
|
}
|
||||||
|
if
|
||||||
|
let view = self as? PlatformView,
|
||||||
|
let introspectionController = view.introspectionController
|
||||||
|
{
|
||||||
|
return IntrospectionStore.shared[introspectionController.id]?.anchor?.view~
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ⚓️
|
/// ⚓️
|
||||||
struct IntrospectionAnchorView: PlatformViewControllerRepresentable {
|
struct IntrospectionAnchorView: PlatformViewControllerRepresentable {
|
||||||
|
@ -14,9 +37,9 @@ struct IntrospectionAnchorView: PlatformViewControllerRepresentable {
|
||||||
@Binding
|
@Binding
|
||||||
private var observed: Void // workaround for state changes not triggering view updates
|
private var observed: Void // workaround for state changes not triggering view updates
|
||||||
|
|
||||||
let id: IntrospectionAnchorID
|
let id: IntrospectionViewID
|
||||||
|
|
||||||
init(id: IntrospectionAnchorID) {
|
init(id: IntrospectionViewID) {
|
||||||
self._observed = .constant(())
|
self._observed = .constant(())
|
||||||
self.id = id
|
self.id = id
|
||||||
}
|
}
|
||||||
|
@ -31,11 +54,9 @@ struct IntrospectionAnchorView: PlatformViewControllerRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
final class IntrospectionAnchorPlatformViewController: PlatformViewController {
|
final class IntrospectionAnchorPlatformViewController: PlatformViewController {
|
||||||
let id: IntrospectionAnchorID
|
init(id: IntrospectionViewID) {
|
||||||
|
|
||||||
init(id: IntrospectionAnchorID) {
|
|
||||||
self.id = id
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
IntrospectionStore.shared[id, default: .init()].anchor = self
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
|
@ -43,24 +64,9 @@ final class IntrospectionAnchorPlatformViewController: PlatformViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
#if canImport(UIKit)
|
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
view.tag = id.hashValue
|
|
||||||
}
|
|
||||||
#elseif canImport(AppKit)
|
|
||||||
final class TaggableView: NSView {
|
|
||||||
private var _tag: Int?
|
|
||||||
override var tag: Int {
|
|
||||||
get { _tag ?? super.tag }
|
|
||||||
set { _tag = newValue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
let view = TaggableView()
|
view = NSView()
|
||||||
view.tag = id.hashValue
|
|
||||||
self.view = view
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -78,14 +84,17 @@ struct IntrospectionView<Target: PlatformEntity>: PlatformViewControllerRepresen
|
||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
private var observed: Void // workaround for state changes not triggering view updates
|
private var observed: Void // workaround for state changes not triggering view updates
|
||||||
|
private let id: IntrospectionViewID
|
||||||
private let selector: (IntrospectionPlatformViewController) -> Target?
|
private let selector: (IntrospectionPlatformViewController) -> Target?
|
||||||
private let customize: (Target) -> Void
|
private let customize: (Target) -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
id: IntrospectionViewID,
|
||||||
selector: @escaping (IntrospectionPlatformViewController) -> Target?,
|
selector: @escaping (IntrospectionPlatformViewController) -> Target?,
|
||||||
customize: @escaping (Target) -> Void
|
customize: @escaping (Target) -> Void
|
||||||
) {
|
) {
|
||||||
self._observed = .constant(())
|
self._observed = .constant(())
|
||||||
|
self.id = id
|
||||||
self.selector = selector
|
self.selector = selector
|
||||||
self.customize = customize
|
self.customize = customize
|
||||||
}
|
}
|
||||||
|
@ -95,7 +104,7 @@ struct IntrospectionView<Target: PlatformEntity>: PlatformViewControllerRepresen
|
||||||
}
|
}
|
||||||
|
|
||||||
func makePlatformViewController(context: Context) -> IntrospectionPlatformViewController {
|
func makePlatformViewController(context: Context) -> IntrospectionPlatformViewController {
|
||||||
let controller = IntrospectionPlatformViewController { controller in
|
let controller = IntrospectionPlatformViewController(id: id) { controller in
|
||||||
guard let target = selector(controller) else {
|
guard let target = selector(controller) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -128,9 +137,14 @@ struct IntrospectionView<Target: PlatformEntity>: PlatformViewControllerRepresen
|
||||||
}
|
}
|
||||||
|
|
||||||
final class IntrospectionPlatformViewController: PlatformViewController {
|
final class IntrospectionPlatformViewController: PlatformViewController {
|
||||||
|
let id: IntrospectionViewID
|
||||||
var handler: (() -> Void)? = nil
|
var handler: (() -> Void)? = nil
|
||||||
|
|
||||||
fileprivate init(handler: ((IntrospectionPlatformViewController) -> Void)?) {
|
fileprivate init(
|
||||||
|
id: IntrospectionViewID,
|
||||||
|
handler: ((IntrospectionPlatformViewController) -> Void)?
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
self.handler = { [weak self] in
|
self.handler = { [weak self] in
|
||||||
guard let self = self else {
|
guard let self = self else {
|
||||||
|
@ -138,6 +152,7 @@ final class IntrospectionPlatformViewController: PlatformViewController {
|
||||||
}
|
}
|
||||||
handler?(self)
|
handler?(self)
|
||||||
}
|
}
|
||||||
|
IntrospectionStore.shared[id, default: .init()].controller = self
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
|
@ -146,13 +161,14 @@ final class IntrospectionPlatformViewController: PlatformViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
override func didMove(toParent parent: UIViewController?) {
|
override func viewDidLoad() {
|
||||||
super.didMove(toParent: parent)
|
super.viewDidLoad()
|
||||||
|
view.introspectionController = self
|
||||||
handler?()
|
handler?()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func didMove(toParent parent: UIViewController?) {
|
||||||
super.viewDidLoad()
|
super.didMove(toParent: parent)
|
||||||
handler?()
|
handler?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +184,7 @@ final class IntrospectionPlatformViewController: PlatformViewController {
|
||||||
#elseif canImport(AppKit)
|
#elseif canImport(AppKit)
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
view = NSView()
|
view = NSView()
|
||||||
|
view.introspectionController = self
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
@ -181,3 +198,18 @@ final class IntrospectionPlatformViewController: PlatformViewController {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import ObjectiveC
|
||||||
|
|
||||||
|
extension PlatformView {
|
||||||
|
fileprivate var introspectionController: IntrospectionPlatformViewController? {
|
||||||
|
get {
|
||||||
|
let key = unsafeBitCast(Selector(#function), to: UnsafeRawPointer.self)
|
||||||
|
return objc_getAssociatedObject(self, key) as? IntrospectionPlatformViewController
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
let key = unsafeBitCast(Selector(#function), to: UnsafeRawPointer.self)
|
||||||
|
objc_setAssociatedObject(self, key, newValue, .OBJC_ASSOCIATION_ASSIGN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue