Commits

Kim De Vos authored feccef2f9b1 Merge
Pull request #72: Scroll to first appointment

Merge in IA/gravid-i-dk from kdv/scroll-to-first-appointment to master * commit 'f8ffa25688ff4ff3fd7c258205d6214e4a27f7e5': Make some methods private and removed public Fix warnings Support iOS 13 scroll. Add view introspection Apply feed back from tkc Scroll to first appointment
No tags

MinGraviditet/MinGraviditet/Extensions/Views/View+Introspection.swift

Added
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 +}

Everything looks good. We'll let you know here if there's anything you should know about.

Add shortcut