Welkin's Secret Garden

A Better Solution of Transition

Startup

If we want to customize the view controller’s transition animation, we have two steps: setting the animation’s delegate and returning a transition model conforming to UIViewControllerAnimatedTransitioning in the delegate method .

We usually set the delegate to self , making the same code copied again and again. What a mess! Some would like to set the delegate to AppDelegate to ensure they only need to write once, so why not go even further.

As I describe above, we now know that there are two roles in transition:

  1. Transition Model
  2. Transition Delegate

Transition Model is the one who implements the UIViewControllerAnimatedTransitioning to tell the system the duration of this transition and the implementation of the transition’s animation.

Transition Delegate is used to implement the UIViewControllerTransitioningDelegate or UINavigationControllerDelegate to return the corresponding Transition Model.

Then I will just code for navigation’s transition as an example.

Implementation

Transition Model

Suppose there will be many styles of transition in this project in the future, so we will as last create several transition model. Maybe there are some general characters between transitions, such as the duration. We can first generate a base transition to handle this. Also, all of the transition should have an instance variable indicating whether this transition is for “push” or “pop”.

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
class BaseTransition: NSObject, UIViewControllerAnimatedTransitioning {

enum TransitionType {
case push
case pop
}

let type: TransitionType

required init(theType: TransitionType) {
type = theType
super.init()
}

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewController(forKey: .from)
let toVC = transitionContext.viewController(forKey: .to)

fromVC?.beginAppearanceTransition(false, animated: true)
toVC?.beginAppearanceTransition(true, animated: true)

if type == .push {
push(using: transitionContext)
fromVC?.endAppearanceTransition()
toVC?.endAppearanceTransition()
} else {
pop(using: transitionContext)
fromVC?.endAppearanceTransition()
toVC?.endAppearanceTransition()
}
}

func push(using transitionContext: UIViewControllerContextTransitioning) {}
func pop(using transitionContext: UIViewControllerContextTransitioning) {}

}

It’s clear that all the complicated logics are placed in the base transition. The only task left for the subclass is to override the push and pop to implement its animation. More expediently, we can just implement a default animation of push and pop in the base transition, which may save us a whole day!

Transition Delegate

As for transition delegate, I have some different ideas. Just start with one of them.

Hoping to minimize the workload and make everything clear, the delegate should be created with the specific transition model’s type. The delegate holds the type with an instance variable whose type is the base transition like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TransitionDelegate: NSObject, UINavigationControllerDelegate {

let cls: BaseTransition.Type

init(theClass: BaseTransition.Type) {
cls = theClass
super.init()
}

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == .push {
return cls.init(theType: .push)
} else {
return cls.init(theType: .pop)
}
}

}

Til now, everything is done. But I think we can make thing better!

Make it better

Learning from experience, we know that there may be more than one transition style and several view controllers may share the same style. On this occasion, we use the Flyweight pattern.

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
class TransitionDelegateCollection {

enum FetchError: Error {
case noSuchClass
case doesNotInheritFromNavigationTransition
}

private static var delegates = [String: NavigationDelegate]()

public static func delegate(ofTransitionName transitionName: String) throws -> NavigationDelegate? {
var delegate = delegates[transitionName]
if delegate != nil {
return delegate
}
if let cls = NSClassFromString(transitionName) {
if cls is NavigationTransition.Type {
delegate = NavigationDelegate(theClass: (cls as! NavigationTransition.Type))
delegates[transitionName] = delegate
} else {
throw FetchError.noSuchClass
}
} else {
throw FetchError.doesNotInheritFromNavigationTransition
}
return delegate
}

}

Each style of transition belongs to a delegate and it will only be created when the first time it is fetched. After creation, the delegate will be stored in a Dictionary for use afterwards. This seems nice. :]

Conjecture of the Delegate

Maybe we can use a singleton delegate and associate a transitionType to view controller. When we perform the “push”, we first set the transitionType and in the delegate, we return a right transition according to the transitionType. This seems not bad too :).