Skip to content

Commit d1da779

Browse files
authored
refactor: 💡 remove introspect spm dependency (SAP#1153)
* refactor: 💡 remove introspect spm dependency * fix: 🐛 remove unnecessary dependency
1 parent 8cce09c commit d1da779

16 files changed

+1888
-14
lines changed

.xcodegen/project.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ options:
1313
visionOS: "1.3"
1414
watchOS: "7.0"
1515

16-
packages:
17-
SwiftUIIntrospect:
18-
url: https://github.com/siteline/SwiftUI-Introspect.git
19-
version: 1.3.0
20-
2116
############
2217
# Targets
2318
############
@@ -117,8 +112,6 @@ targets:
117112
dependencies:
118113
- target: FioriThemeManager
119114
- target: FioriCharts
120-
- package: SwiftUIIntrospect
121-
product: SwiftUIIntrospect
122115

123116
aggregateTargets:
124117
buildBinaryFramework:

Package.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ let package = Package(
2525
targets: ["FioriThemeManager"]
2626
)
2727
],
28-
dependencies: [
29-
.package(url: "https://github.com/siteline/swiftui-introspect.git", from: "1.3.0")
30-
],
3128
targets: [
3229
.target(
3330
name: "FioriSwiftUI",
@@ -42,8 +39,7 @@ let package = Package(
4239
name: "FioriSwiftUICore",
4340
dependencies: [
4441
.target(name: "FioriThemeManager", condition: .when(platforms: [.iOS, .macCatalyst, .visionOS])),
45-
.target(name: "FioriCharts", condition: .when(platforms: [.iOS, .macCatalyst, .visionOS])),
46-
.product(name: "SwiftUIIntrospect", package: "swiftui-introspect", condition: .when(platforms: [.iOS, .macCatalyst]))
42+
.target(name: "FioriCharts", condition: .when(platforms: [.iOS, .macCatalyst, .visionOS]))
4743
],
4844
resources: [.process("_localization")]
4945
),
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Source: https://github.com/siteline/swiftui-introspect
2+
/**
3+
MIT License:
4+
5+
Copyright 2019 Timber Software
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8+
9+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10+
11+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12+
*/
13+
#if !os(watchOS)
14+
import SwiftUI
15+
16+
/// The scope of introspection i.e. where introspect should look to find
17+
/// the desired target view relative to the applied `.introspect(...)`
18+
/// modifier.
19+
struct IntrospectionScope: OptionSet, Sendable {
20+
/// Look within the `receiver` of the `.introspect(...)` modifier.
21+
static let receiver = Self(rawValue: 1 << 0)
22+
/// Look for an `ancestor` relative to the `.introspect(...)` modifier.
23+
static let ancestor = Self(rawValue: 1 << 1)
24+
25+
let rawValue: UInt
26+
27+
init(rawValue: UInt) {
28+
self.rawValue = rawValue
29+
}
30+
}
31+
32+
extension View {
33+
/// Introspects a SwiftUI view to find its underlying UIKit/AppKit instance.
34+
///
35+
/// - Parameters:
36+
/// - viewType: The type of view to be introspected.
37+
/// - platforms: A list of version predicates that specify platform-specific entities associated with the view.
38+
/// - scope: Optionally overrides the view's default scope of introspection.
39+
/// - customize: A closure that hands over the underlying UIKit/AppKit instance ready for customization.
40+
///
41+
/// Here's an example usage:
42+
///
43+
/// ```swift
44+
/// struct ContentView: View {
45+
/// @State var text = ""
46+
///
47+
/// var body: some View {
48+
/// TextField("Placeholder", text: $text)
49+
/// .introspect(.textField, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26)) {
50+
/// print(type(of: $0)) // UITextField
51+
/// }
52+
/// }
53+
/// }
54+
/// ```
55+
@MainActor
56+
func introspect<SwiftUIViewType: IntrospectableViewType, PlatformSpecificEntity: PlatformEntity>(
57+
_ viewType: SwiftUIViewType,
58+
on platforms: PlatformViewVersionPredicate<SwiftUIViewType, PlatformSpecificEntity>...,
59+
scope: IntrospectionScope? = nil,
60+
customize: @escaping (PlatformSpecificEntity) -> Void
61+
) -> some View {
62+
self.modifier(IntrospectModifier(viewType, platforms: platforms, scope: scope, customize: customize))
63+
}
64+
}
65+
66+
struct IntrospectModifier<SwiftUIViewType: IntrospectableViewType, PlatformSpecificEntity: PlatformEntity>: ViewModifier {
67+
let id = IntrospectionViewID()
68+
let scope: IntrospectionScope
69+
let selector: IntrospectionSelector<PlatformSpecificEntity>?
70+
let customize: (PlatformSpecificEntity) -> Void
71+
72+
@MainActor
73+
init(
74+
_ viewType: SwiftUIViewType,
75+
platforms: [PlatformViewVersionPredicate<SwiftUIViewType, PlatformSpecificEntity>],
76+
scope: IntrospectionScope?,
77+
customize: @escaping (PlatformSpecificEntity) -> Void
78+
) {
79+
self.scope = scope ?? viewType.scope
80+
self.selector = platforms.lazy.compactMap(\.selector).first
81+
self.customize = customize
82+
}
83+
84+
func body(content: Content) -> some View {
85+
if let selector {
86+
content
87+
.background(
88+
Group {
89+
// box up content for more accurate `.view` introspection
90+
if SwiftUIViewType.self == ViewType.self {
91+
Color.white
92+
.opacity(0)
93+
.accessibility(hidden: true)
94+
}
95+
}
96+
)
97+
.background(
98+
IntrospectionAnchorView(id: self.id)
99+
.frame(width: 0, height: 0)
100+
.accessibility(hidden: true)
101+
)
102+
.overlay(
103+
IntrospectionView(id: self.id, selector: { selector($0, self.scope) }, customize: self.customize)
104+
.frame(width: 0, height: 0)
105+
.accessibility(hidden: true)
106+
)
107+
} else {
108+
content
109+
}
110+
}
111+
}
112+
113+
@MainActor
114+
protocol PlatformEntity: AnyObject {
115+
associatedtype Base: PlatformEntity
116+
117+
var ancestor: Base? { get }
118+
119+
var descendants: [Base] { get }
120+
121+
func isDescendant(of other: Base) -> Bool
122+
}
123+
124+
extension PlatformEntity {
125+
var ancestor: Base? { nil }
126+
127+
var descendants: [Base] { [] }
128+
129+
func isDescendant(of other: Base) -> Bool { false }
130+
}
131+
132+
extension PlatformEntity {
133+
var ancestors: some Sequence<Base> {
134+
sequence(first: self~, next: { $0.ancestor~ }).dropFirst()
135+
}
136+
137+
var allDescendants: some Sequence<Base> {
138+
recursiveSequence([self~], children: { $0.descendants~ }).dropFirst()
139+
}
140+
141+
func nearestCommonAncestor(with other: Base) -> Base? {
142+
var nearestAncestor: Base? = self~
143+
144+
while let currentEntity = nearestAncestor, !other.isDescendant(of: currentEntity~) {
145+
nearestAncestor = currentEntity.ancestor~
146+
}
147+
148+
return nearestAncestor
149+
}
150+
151+
func allDescendants(between bottomEntity: Base, and topEntity: Base) -> some Sequence<Base> {
152+
self.allDescendants
153+
.lazy
154+
.drop(while: { $0 !== bottomEntity })
155+
.prefix(while: { $0 !== topEntity })
156+
}
157+
158+
func receiver<PlatformSpecificEntity: PlatformEntity>(
159+
ofType type: PlatformSpecificEntity.Type
160+
) -> PlatformSpecificEntity? {
161+
let frontEntity = self
162+
guard
163+
let backEntity = frontEntity.introspectionAnchorEntity,
164+
let commonAncestor = backEntity.nearestCommonAncestor(with: frontEntity~)
165+
else {
166+
return nil
167+
}
168+
169+
return commonAncestor
170+
.allDescendants(between: backEntity~, and: frontEntity~)
171+
.filter { !$0.isIntrospectionPlatformEntity }
172+
.compactMap { $0 as? PlatformSpecificEntity }
173+
.first
174+
}
175+
176+
func ancestor<PlatformSpecificEntity: PlatformEntity>(
177+
ofType type: PlatformSpecificEntity.Type
178+
) -> PlatformSpecificEntity? {
179+
self.ancestors
180+
.lazy
181+
.filter { !$0.isIntrospectionPlatformEntity }
182+
.compactMap { $0 as? PlatformSpecificEntity }
183+
.first
184+
}
185+
}
186+
187+
extension PlatformView: PlatformEntity {
188+
var ancestor: PlatformView? {
189+
superview
190+
}
191+
192+
var descendants: [PlatformView] {
193+
subviews
194+
}
195+
}
196+
197+
extension PlatformViewController: PlatformEntity {
198+
var ancestor: PlatformViewController? {
199+
parent
200+
}
201+
202+
var descendants: [PlatformViewController] {
203+
children
204+
}
205+
206+
func isDescendant(of other: PlatformViewController) -> Bool {
207+
self.ancestors.contains(other)
208+
}
209+
}
210+
211+
#if canImport(UIKit)
212+
extension UIPresentationController: PlatformEntity {
213+
typealias Base = UIPresentationController
214+
}
215+
216+
#elseif canImport(AppKit)
217+
extension NSWindow: PlatformEntity {
218+
typealias Base = NSWindow
219+
}
220+
#endif
221+
#endif
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Source: https://github.com/siteline/swiftui-introspect
2+
/**
3+
MIT License:
4+
5+
Copyright 2019 Timber Software
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8+
9+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10+
11+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12+
*/
13+
#if !os(watchOS)
14+
@MainActor
15+
protocol IntrospectableViewType {
16+
/// The scope of introspection for this particular view type, i.e. where introspect
17+
/// should look to find the desired target view relative to the applied
18+
/// `.introspect(...)` modifier.
19+
///
20+
/// While the scope can be overridden by the user in their `.introspect(...)` call,
21+
/// most of the time it's preferable to defer to the view type's own scope,
22+
/// as it guarantees introspection is working as intended by the vendor.
23+
///
24+
/// Defaults to `.receiver` if left unimplemented, which is a sensible one in
25+
/// most cases if you're looking to implement your own view type.
26+
var scope: IntrospectionScope { get }
27+
}
28+
29+
extension IntrospectableViewType {
30+
var scope: IntrospectionScope { .receiver }
31+
}
32+
#endif
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Source: https://github.com/siteline/swiftui-introspect
2+
/**
3+
MIT License:
4+
5+
Copyright 2019 Timber Software
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8+
9+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10+
11+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12+
*/
13+
#if !os(watchOS)
14+
15+
@MainActor
16+
struct IntrospectionSelector<Target: PlatformEntity> {
17+
static var `default`: Self { .from(Target.self, selector: { $0 }) }
18+
19+
static func from<Entry: PlatformEntity>(_ entryType: Entry.Type, selector: @MainActor @escaping (Entry) -> Target?) -> Self {
20+
.init(
21+
receiverSelector: { controller in
22+
controller.as(Entry.Base.self)?.receiver(ofType: Entry.self).flatMap(selector)
23+
},
24+
ancestorSelector: { controller in
25+
controller.as(Entry.Base.self)?.ancestor(ofType: Entry.self).flatMap(selector)
26+
}
27+
)
28+
}
29+
30+
private var receiverSelector: @MainActor (IntrospectionPlatformViewController) -> Target?
31+
private var ancestorSelector: @MainActor (IntrospectionPlatformViewController) -> Target?
32+
33+
private init(
34+
receiverSelector: @MainActor @escaping (IntrospectionPlatformViewController) -> Target?,
35+
ancestorSelector: @MainActor @escaping (IntrospectionPlatformViewController) -> Target?
36+
) {
37+
self.receiverSelector = receiverSelector
38+
self.ancestorSelector = ancestorSelector
39+
}
40+
41+
func withReceiverSelector(_ selector: @MainActor @escaping (PlatformViewController) -> Target?) -> Self {
42+
var copy = self
43+
copy.receiverSelector = selector
44+
return copy
45+
}
46+
47+
func withAncestorSelector(_ selector: @MainActor @escaping (PlatformViewController) -> Target?) -> Self {
48+
var copy = self
49+
copy.ancestorSelector = selector
50+
return copy
51+
}
52+
53+
func callAsFunction(_ controller: IntrospectionPlatformViewController, _ scope: IntrospectionScope) -> Target? {
54+
if
55+
scope.contains(.receiver),
56+
let target = receiverSelector(controller)
57+
{
58+
return target
59+
}
60+
if
61+
scope.contains(.ancestor),
62+
let target = ancestorSelector(controller)
63+
{
64+
return target
65+
}
66+
return nil
67+
}
68+
}
69+
70+
extension PlatformViewController {
71+
func `as`<Base: PlatformEntity>(_ baseType: Base.Type) -> (any PlatformEntity)? {
72+
if Base.self == PlatformView.self {
73+
#if canImport(UIKit)
74+
return viewIfLoaded
75+
#elseif canImport(AppKit)
76+
return isViewLoaded ? view : nil
77+
#endif
78+
} else if Base.self == PlatformViewController.self {
79+
return self
80+
}
81+
return nil
82+
}
83+
}
84+
#endif

0 commit comments

Comments
 (0)