diff --git a/Sources/Introspect.swift b/Sources/Introspect.swift index 3adf8ddff..d7b99469a 100644 --- a/Sources/Introspect.swift +++ b/Sources/Introspect.swift @@ -26,6 +26,14 @@ extension View { /// - scope: Optionally overrides the view's default scope of introspection. /// - customize: A closure that hands over the underlying UIKit/AppKit instance ready for customization. /// + /// Note there is no guarantee of one-time execution for this closure. As `customize` may fire multiple times, + /// make sure to guard against repeated or heavy work in your closure by keeping track of its completeness. + /// + /// Additionally, note mutating SwiftUI state within `customize` will trigger runtime warnings unless that mutation + /// is wrapped in a `DispatchQueue.main.async { ... }` call. This is because introspect attempts to hand you + /// the requested view as soon as possible, and this might mean SwiftUI isn't ready for state mutations at that + /// particular moment. + /// /// Here's an example usage: /// /// ```swift diff --git a/Sources/IntrospectionView.swift b/Sources/IntrospectionView.swift index c464b5b3b..cb80bac38 100644 --- a/Sources/IntrospectionView.swift +++ b/Sources/IntrospectionView.swift @@ -121,15 +121,6 @@ struct IntrospectionView: PlatformViewControllerRepresen customize(target) controller.handler = nil } - - // - Workaround - - // iOS/tvOS 13 sometimes need a nudge on the next run loop. - if #available(iOS 14, tvOS 14, *) {} else { - DispatchQueue.main.async { [weak controller] in - controller?.handler?() - } - } - return controller } @@ -159,7 +150,13 @@ final class IntrospectionPlatformViewController: PlatformViewController { guard let self else { return } - handler?(self) + + // NB: .introspect makes no guarantees about the number of times its callback is invoked, + // so the below is fair play to maximize compatibility and predictability + handler?(self) // we call this eagerly as most customization can successfully happen without a thread hop + DispatchQueue.main.async { + handler?(self) // we also thread hop to cover the rest of the cases where the underlying UI component isn't quite ready for customization + } } self.isIntrospectionPlatformEntity = true IntrospectionStore.shared[id, default: .init()].controller = self