Commits
Kim De Vos authored feccef2f9b1 Merge
1 + | // The MIT License |
2 + | // |
3 + | // Original work sponsored and donated by The Danish Health Data Authority (http://www.sundhedsdatastyrelsen.dk) |
4 + | // |
5 + | // Copyright (C) 2020 The Danish Health Data Authority (http://www.sundhedsdatastyrelsen.dk) |
6 + | // |
7 + | // Permission is hereby granted, free of charge, to any person obtaining a copy of |
8 + | // this software and associated documentation files (the "Software"), to deal in |
9 + | // the Software without restriction, including without limitation the rights to |
10 + | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
11 + | // of the Software, and to permit persons to whom the Software is furnished to do |
12 + | // so, subject to the following conditions: |
13 + | // |
14 + | // The above copyright notice and this permission notice shall be included in all |
15 + | // copies or substantial portions of the Software. |
16 + | // |
17 + | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
18 + | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
19 + | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
20 + | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
21 + | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
22 + | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
23 + | // SOFTWARE. |
24 + | |
25 + | import SwiftUI |
26 + | |
27 + | // Based on: https://github.com/siteline/SwiftUI-Introspect/tree/9afddabecc749f531aed5ece47e30c3a0c736caa |
28 + | // This should be used VERY carefully. It was added because we need to scroll in AppointmentsView. |
29 + | // Apple will and can break this in any update. |
30 + | extension View { |
31 + | private func inject<SomeView>(_ view: SomeView) -> some View where SomeView: View { |
32 + | overlay(view.frame(width: 0, height: 0)) |
33 + | } |
34 + | |
35 + | /// Finds a `TargetView` from a `SwiftUI.View` |
36 + | private func introspect<TargetView: UIView>( |
37 + | selector: @escaping (IntrospectionUIView) -> TargetView?, |
38 + | customize: @escaping (TargetView) -> Void |
39 + | ) -> some View { |
40 + | inject(UIKitIntrospectionView( |
41 + | selector: selector, |
42 + | customize: customize |
43 + | )) |
44 + | } |
45 + | |
46 + | /// Finds a `UIScrollView` from a `SwiftUI.ScrollView`, or `SwiftUI.ScrollView` child. |
47 + | public func introspectScrollView(customize: @escaping (UIScrollView) -> Void) -> some View { |
48 + | introspect(selector: TargetViewSelector.ancestorOrSiblingContaining, customize: customize) |
49 + | } |
50 + | } |
51 + | |
52 + | @available(iOS 13.0, *) |
53 + | class IntrospectionUIView: UIView { |
54 + | |
55 + | required init() { |
56 + | super.init(frame: .zero) |
57 + | isHidden = true |
58 + | isUserInteractionEnabled = false |
59 + | } |
60 + | |
61 + | @available(*, unavailable) |
62 + | required init?(coder: NSCoder) { |
63 + | fatalError("init(coder:) has not been implemented") |
64 + | } |
65 + | } |
66 + | |
67 + | /// Introspection View that is injected into the UIKit hierarchy alongside the target view. |
68 + | /// After `updateUIView` is called, it calls `selector` to find the target view, then `customize` when the target view is found. |
69 + | @available(iOS 13.0, tvOS 13.0, macOS 10.15.0, *) |
70 + | struct UIKitIntrospectionView<TargetViewType: UIView>: UIViewRepresentable { |
71 + | |
72 + | /// Method that introspects the view hierarchy to find the target view. |
73 + | /// First argument is the introspection view itself, which is contained in a view host alongside the target view. |
74 + | let selector: (IntrospectionUIView) -> TargetViewType? |
75 + | |
76 + | /// User-provided customization method for the target view. |
77 + | let customize: (TargetViewType) -> Void |
78 + | |
79 + | init( |
80 + | selector: @escaping (IntrospectionUIView) -> TargetViewType?, |
81 + | customize: @escaping (TargetViewType) -> Void |
82 + | ) { |
83 + | self.selector = selector |
84 + | self.customize = customize |
85 + | } |
86 + | |
87 + | func makeUIView(context: UIViewRepresentableContext<UIKitIntrospectionView>) -> IntrospectionUIView { |
88 + | let view = IntrospectionUIView() |
89 + | view.accessibilityLabel = "IntrospectionUIView<\(TargetViewType.self)>" |
90 + | return view |
91 + | } |
92 + | |
93 + | /// When `updateUiView` is called after creating the Introspection view, it is not yet in the UIKit hierarchy. |
94 + | /// At this point, `introspectionView.superview.superview` is nil and we can't access the target UIKit view. |
95 + | /// To workaround this, we wait until the runloop is done inserting the introspection view in the hierarchy, then run the selector. |
96 + | /// Finding the target view fails silently if the selector yield no result. This happens when `updateUIView` |
97 + | /// gets called when the introspection view gets removed from the hierarchy. |
98 + | func updateUIView( |
99 + | _ uiView: IntrospectionUIView, |
100 + | context: UIViewRepresentableContext<UIKitIntrospectionView> |
101 + | ) { |
102 + | DispatchQueue.main.async { |
103 + | guard let targetView = self.selector(uiView) else { |
104 + | return |
105 + | } |
106 + | self.customize(targetView) |
107 + | } |
108 + | } |
109 + | } |
110 + | |
111 + | /// Utility methods to inspect the UIKit view hierarchy. |
112 + | enum Introspect { |
113 + | |
114 + | /// Finds a subview of the specified type. |
115 + | /// This method will recursively look for this view. |
116 + | /// Returns nil if it can't find a view of the specified type. |
117 + | static func findChild<AnyViewType: UIView>( |
118 + | ofType type: AnyViewType.Type, |
119 + | in root: UIView |
120 + | ) -> AnyViewType? { |
121 + | for subview in root.subviews { |
122 + | if let typed = subview as? AnyViewType { |
123 + | return typed |
124 + | } else if let typed = findChild(ofType: type, in: subview) { |
125 + | return typed |
126 + | } |
127 + | } |
128 + | return nil |
129 + | } |
130 + | |
131 + | /// Finds a previous sibling that contains a view of the specified type. |
132 + | /// This method inspects siblings recursively. |
133 + | /// Returns nil if no sibling contains the specified type. |
134 + | static func previousSibling<AnyViewType: UIView>( |
135 + | containing type: AnyViewType.Type, |
136 + | from entry: UIView |
137 + | ) -> AnyViewType? { |
138 + | |
139 + | guard let superview = entry.superview, |
140 + | let entryIndex = superview.subviews.firstIndex(of: entry), |
141 + | entryIndex > 0 |
142 + | else { |
143 + | return nil |
144 + | } |
145 + | |
146 + | for subview in superview.subviews[0..<entryIndex].reversed() { |
147 + | if let typed = findChild(ofType: type, in: subview) { |
148 + | return typed |
149 + | } |
150 + | } |
151 + | |
152 + | return nil |
153 + | } |
154 + | |
155 + | /// Finds an ancestor of the specified type. |
156 + | /// If it reaches the top of the view without finding the specified view type, it returns nil. |
157 + | static func findAncestor<AnyViewType: UIView>(ofType type: AnyViewType.Type, from entry: UIView) -> AnyViewType? { |
158 + | var superview = entry.superview |
159 + | while let s = superview { |
160 + | if let typed = s as? AnyViewType { |
161 + | return typed |
162 + | } |
163 + | superview = s.superview |
164 + | } |
165 + | return nil |
166 + | } |
167 + | |
168 + | /// Finds the view host of a specific view. |
169 + | /// SwiftUI wraps each UIView within a ViewHost, then within a HostingView. |
170 + | /// Returns nil if it couldn't find a view host. This should never happen when called with an IntrospectionView. |
171 + | static func findViewHost(from entry: UIView) -> UIView? { |
172 + | var superview = entry.superview |
173 + | while let s = superview { |
174 + | if NSStringFromClass(type(of: s)).contains("ViewHost") { |
175 + | return s |
176 + | } |
177 + | superview = s.superview |
178 + | } |
179 + | return nil |
180 + | } |
181 + | } |
182 + | |
183 + | enum TargetViewSelector { |
184 + | static func siblingContaining<TargetView: UIView>(from entry: UIView) -> TargetView? { |
185 + | guard let viewHost = Introspect.findViewHost(from: entry) else { |
186 + | return nil |
187 + | } |
188 + | return Introspect.previousSibling(containing: TargetView.self, from: viewHost) |
189 + | } |
190 + | |
191 + | static func ancestorOrSiblingContaining<TargetView: UIView>(from entry: UIView) -> TargetView? { |
192 + | if let tableView = Introspect.findAncestor(ofType: TargetView.self, from: entry) { |
193 + | return tableView |
194 + | } |
195 + | return siblingContaining(from: entry) |
196 + | } |
197 + | } |