Skip to content

Commit 7783df0

Browse files
authored
Fix swipe back cancellation leaving views in inconsistent state (#78)
1 parent 0989352 commit 7783df0

File tree

7 files changed

+112
-44
lines changed

7 files changed

+112
-44
lines changed

Sources/Animator/AnimatorTransientView.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,14 @@ public class AnimatorTransientView {
4747
self.uiView = uiView
4848
}
4949

50-
@_spi(package) public func setUIViewProperties(to properties: KeyPath<AnimatorTransientView, Properties>) {
51-
self[keyPath: properties].assignToUIView(uiView)
50+
@_spi(package) public func setUIViewProperties(
51+
to properties: KeyPath<AnimatorTransientView, Properties>,
52+
force: Bool = false
53+
) {
54+
self[keyPath: properties].assignToUIView(uiView, force: force)
55+
}
56+
57+
@_spi(package) public func resetUIViewProperties() {
58+
Properties.default.assignToUIView(uiView, force: true)
5259
}
5360
}

Sources/Animator/AnimatorTransientViewProperties.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public struct AnimatorTransientViewProperties: Equatable {
1616
}
1717

1818
extension AnimatorTransientViewProperties {
19+
static let `default` = Self(
20+
alpha: 1,
21+
transform: .identity,
22+
zPosition: 0
23+
)
24+
1925
init(of uiView: UIView) {
2026
self.init(
2127
alpha: uiView.alpha,
@@ -24,9 +30,9 @@ extension AnimatorTransientViewProperties {
2430
)
2531
}
2632

27-
func assignToUIView(_ uiView: UIView) {
28-
$alpha.assignTo(uiView, \.alpha)
29-
$transform?.assignToUIView(uiView)
30-
$zPosition.assignTo(uiView, \.layer.zPosition)
33+
func assignToUIView(_ uiView: UIView, force: Bool) {
34+
$alpha.assign(to: uiView, \.alpha, force: force)
35+
$transform.assign(to: uiView, force: force)
36+
$zPosition.assign(to: uiView, \.layer.zPosition, force: force)
3137
}
3238
}
Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
@propertyWrapper
22
public struct OptionalWithDefault<Value> {
3-
public private(set) var projectedValue: Value? = nil
3+
public var projectedValue: Self { self }
44

5-
private var defaultValue: Value
5+
public private(set) var value: Value? = nil
6+
public private(set) var defaultValue: Value
67

78
public var wrappedValue: Value {
8-
get { projectedValue ?? defaultValue }
9-
set { projectedValue = newValue }
9+
get { value ?? defaultValue }
10+
set { value = newValue }
1011
}
1112

1213
public init(wrappedValue: Value) {
@@ -16,10 +17,20 @@ public struct OptionalWithDefault<Value> {
1617

1718
extension OptionalWithDefault: Equatable where Value: Equatable {}
1819

19-
extension Optional {
20-
func assignTo<Root: AnyObject>(_ root: Root, _ valueKeyPath: ReferenceWritableKeyPath<Root, Wrapped>) {
21-
if let value = self {
22-
root[keyPath: valueKeyPath] = value
20+
extension OptionalWithDefault {
21+
func assign<Root: AnyObject>(to root: Root, _ valueKeyPath: ReferenceWritableKeyPath<Root, Value>, force: Bool) {
22+
assign(force: force) { root[keyPath: valueKeyPath] = $0 }
23+
}
24+
25+
func assign(force: Bool, handler: (Value) -> Void) {
26+
if let value = { () -> Value? in
27+
if force {
28+
return wrappedValue
29+
} else {
30+
return value
31+
}
32+
}() {
33+
handler(value)
2334
}
2435
}
2536
}

Sources/Animator/Transform.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import UIKit
22

33
@dynamicMemberLookup
44
public struct Transform: Equatable {
5-
private var transform: CATransform3D
5+
fileprivate var transform: CATransform3D
66

77
public subscript<T>(dynamicMember keyPath: WritableKeyPath<CATransform3D, T>) -> T {
88
get { transform[keyPath: keyPath] }
@@ -12,12 +12,16 @@ public struct Transform: Equatable {
1212
init(_ transform: CATransform3D) {
1313
self.transform = transform
1414
}
15+
}
1516

16-
func assignToUIView(_ uiView: UIView) {
17-
if let transform = transform.affineTransform {
18-
uiView.transform = transform
19-
} else {
20-
uiView.transform3D = transform
17+
extension OptionalWithDefault where Value == Transform {
18+
func assign(to uiView: UIView, force: Bool) {
19+
self.assign(force: force) {
20+
if let transform = $0.transform.affineTransform {
21+
uiView.transform = transform
22+
} else {
23+
uiView.transform3D = $0.transform
24+
}
2125
}
2226
}
2327
}

Sources/NavigationTransitions/NavigationTransitionDelegate.swift

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -89,25 +89,54 @@ final class NavigationTransitionAnimatorProvider: NSObject, UIViewControllerAnim
8989
)
9090
cachedAnimators[ObjectIdentifier(transitionContext)] = animator
9191

92+
let container = transitionContext.containerView
93+
guard
94+
let fromUIView = transitionContext.view(forKey: .from),
95+
let toUIView = transitionContext.view(forKey: .to)
96+
else {
97+
return animator
98+
}
99+
100+
fromUIView.isUserInteractionEnabled = false
101+
toUIView.isUserInteractionEnabled = false
102+
92103
switch transition.handler {
93104
case .transient(let handler):
94-
if let (fromView, toView) = transientViews(for: handler, animator: animator, context: transitionContext) {
95-
fromView.setUIViewProperties(to: \.initial)
96-
animator.addAnimations { fromView.setUIViewProperties(to: \.animation) }
97-
animator.addCompletion { _ in fromView.setUIViewProperties(to: \.completion) }
98-
99-
toView.setUIViewProperties(to: \.initial)
100-
animator.addAnimations { toView.setUIViewProperties(to: \.animation) }
101-
animator.addCompletion { _ in toView.setUIViewProperties(to: \.completion) }
105+
if let (fromView, toView) = transientViews(
106+
for: handler,
107+
animator: animator,
108+
context: (container, fromUIView, toUIView)
109+
) {
110+
for view in [fromView, toView] {
111+
view.setUIViewProperties(to: \.initial)
112+
animator.addAnimations { view.setUIViewProperties(to: \.animation) }
113+
animator.addCompletion { _ in
114+
if transitionContext.transitionWasCancelled {
115+
view.resetUIViewProperties()
116+
} else {
117+
view.setUIViewProperties(to: \.completion)
118+
}
119+
}
120+
}
102121
}
103122
case .primitive(let handler):
104123
handler(animator, operation, transitionContext)
105124
}
106125

107126
animator.addCompletion { _ in
108127
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
109-
transitionContext.view(forKey: .from)?.isUserInteractionEnabled = true
110-
transitionContext.view(forKey: .to)?.isUserInteractionEnabled = true
128+
129+
fromUIView.isUserInteractionEnabled = true
130+
toUIView.isUserInteractionEnabled = true
131+
132+
// iOS 16 workaround to nudge views into becoming responsive after transition
133+
if transitionContext.transitionWasCancelled {
134+
fromUIView.removeFromSuperview()
135+
container.addSubview(fromUIView)
136+
} else {
137+
toUIView.removeFromSuperview()
138+
container.addSubview(toUIView)
139+
}
111140
}
112141

113142
return animator
@@ -116,19 +145,10 @@ final class NavigationTransitionAnimatorProvider: NSObject, UIViewControllerAnim
116145
private func transientViews(
117146
for handler: AnyNavigationTransition.TransientHandler,
118147
animator: Animator,
119-
context: UIViewControllerContextTransitioning
148+
context: (container: UIView, fromUIView: UIView, toUIView: UIView)
120149
) -> (fromView: AnimatorTransientView, toView: AnimatorTransientView)? {
121-
guard
122-
let fromUIView = context.view(forKey: .from),
123-
let toUIView = context.view(forKey: .to)
124-
else {
125-
return nil
126-
}
127-
128-
fromUIView.isUserInteractionEnabled = false
129-
toUIView.isUserInteractionEnabled = false
150+
let (container, fromUIView, toUIView) = context
130151

131-
let container = context.containerView
132152
switch operation {
133153
case .push:
134154
container.insertSubview(toUIView, aboveSubview: fromUIView)
@@ -141,6 +161,6 @@ final class NavigationTransitionAnimatorProvider: NSObject, UIViewControllerAnim
141161

142162
handler(fromView, toView, operation, container)
143163

144-
return (fromView: fromView, toView: toView)
164+
return (fromView, toView)
145165
}
146166
}

Sources/TestUtils/AnimatorTransientView+Mocks.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ final class UnimplementedAnimatorTransientView: AnimatorTransientView {
4848
super.init(UIView())
4949
}
5050

51-
override public func setUIViewProperties(to properties: KeyPath<AnimatorTransientView, AnimatorTransientView.Properties>) {
51+
override public func setUIViewProperties(
52+
to properties: KeyPath<AnimatorTransientView, AnimatorTransientView.Properties>,
53+
force: Bool
54+
) {
5255
XCTFail("\(Self.self).\(#function) is unimplemented")
5356
}
5457
}

Tests/AnimatorTests/AnimatorTransientViewPropertiesTests.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,26 @@ extension AnimatorTransientViewPropertiesTests {
1616
sut.transform = .init(.identity.scaled(5))
1717
sut.zPosition = 15
1818

19-
sut.assignToUIView(view)
19+
sut.assignToUIView(view, force: false)
2020
XCTAssertEqual(view.alpha, 0.5)
2121
XCTAssertEqual(view.transform3D, .identity.scaled(5))
2222
XCTAssertEqual(view.layer.zPosition, 15)
2323
}
24+
25+
func testForceAssignToUIView() {
26+
let view = UIView()
27+
view.transform = .identity.scaledBy(x: 5, y: 5)
28+
view.layer.zPosition = 15
29+
XCTAssertEqual(view.alpha, 1)
30+
XCTAssertEqual(view.transform, .identity.scaledBy(x: 5, y: 5))
31+
XCTAssertEqual(view.layer.zPosition, 15)
32+
33+
var sut = AnimatorTransientView.Properties.default
34+
sut.alpha = 0.5
35+
36+
sut.assignToUIView(view, force: true)
37+
XCTAssertEqual(view.alpha, 0.5)
38+
XCTAssertEqual(view.transform3D, .identity)
39+
XCTAssertEqual(view.layer.zPosition, 0)
40+
}
2441
}

0 commit comments

Comments
 (0)