-
Notifications
You must be signed in to change notification settings - Fork 47
/
Copy pathUIViewController+Extensions.swift
135 lines (108 loc) · 5.55 KB
/
UIViewController+Extensions.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/*
* Copyright 2020 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if canImport(UIKit)
import UIKit
extension UpdateChildScreenViewController where Self: UIViewController {
/// Updates the view controller at the given `child` key path with the
/// `ViewControllerDescription` from `screen`. If the type of the underlying view
/// controller changes between update passes, this method will remove
/// the old view controller, create a new one, update it, and insert it into the view controller hierarchy.
///
/// The view controller at `child` must be a child of `self`.
///
/// - Parameters:
/// - parameter child: The `KeyPath` which describes what view controller to update. This view controller must be a direct child of `self`.
/// - parameter screen: The `Screen` instance to apply to the view controller.
/// - parameter environment: The `environment` to used when updating the view controller.
/// - parameter onChange: A callback called if the view controller instance changed.
///
public func update<VC: UIViewController, ScreenType: Screen>(
child: ReferenceWritableKeyPath<Self, VC>,
with screen: ScreenType,
in environment: ViewEnvironment,
animated: Bool = true,
onChange: (VC) -> Void = { _ in }
) {
let description = screen.viewControllerDescription(environment: environment)
let existing = self[keyPath: child]
if description.canUpdate(viewController: existing) {
// Easy path: Just update the existing view controller if we can do that.
description.update(viewController: existing)
} else {
// If we can't update the view controller, that means its type changed.
// We'll need to make a new view controller and swap over to it.
let old = existing
// Make the new view controller.
let new = description.buildViewController() as! VC
// We already have a reference to the old vc above, update the keypath to the new one.
self[keyPath: child] = new
// We should only add the view controller if the old one was already within the parent.
if let parent = old.parent {
precondition(
parent == self,
"""
The parent of the child view controller must be \(self). Instead, it was \(parent). \
Please call `update(child:)` on the correct parent view controller.
"""
)
// Begin the transition: Signal the new vc will begin moving in, and the old one, out.
parent.addChild(new)
old.willMove(toParent: nil)
if
parent.isViewLoaded,
old.isViewLoaded,
let container = old.view.superview {
// We will only add the view to the hierarchy if
// the parent's view is loaded, and the existing view
// is loaded, and the old view was in a superview.
// We will only perform appearance transitions if we're visible.
let isVisible = parent.view.window != nil
let animated = animated && isVisible
description.transition.transition(
from: old.view,
to: new.view,
in: container,
animated: animated,
setup: {
new.view.frame = old.view.frame
if isVisible {
new.beginAppearanceTransition(true, animated: animated)
old.beginAppearanceTransition(false, animated: animated)
}
container.insertSubview(new.view, aboveSubview: old.view)
},
completion: {
old.view.removeFromSuperview()
if isVisible {
new.endAppearanceTransition()
old.endAppearanceTransition()
}
new.didMove(toParent: parent)
old.removeFromParent()
}
)
} else {
new.didMove(toParent: parent)
old.removeFromParent()
}
}
onChange(new)
}
}
}
public protocol UpdateChildScreenViewController {}
extension UIViewController: UpdateChildScreenViewController {}
#endif