Skip to content

Commit

Permalink
Merge pull request #47 from zenangst/feature/animations-on-mac-os
Browse files Browse the repository at this point in the history
Implement support for animated injections on macOS
  • Loading branch information
zenangst authored Oct 24, 2018
2 parents c65ea13 + 5c06cbf commit 92496ee
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 94 deletions.
162 changes: 81 additions & 81 deletions Source/iOS+tvOS/UIViewController+Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,51 +1,6 @@
import UIKit

@objc public extension UIViewController {
/// Removes all child view controllers.
private func removeChildViewControllers() {
#if swift(>=4.2)
children.forEach { controller in
controller.willMove(toParent: nil)
controller.view.removeFromSuperview()
controller.removeFromParent()
}
#else
childViewControllers.forEach { controller in
controller.willMove(toParentViewController: nil)
controller.view.removeFromSuperview()
controller.removeFromParentViewController()
}
#endif
}

/// Lock screen updates using a `CATransaction`.
///
/// - Parameter shouldLock: A boolean value indicating if the method should
/// be invoked or not. This is determined by the
/// amount of child view controllers in the controller.
private func lockScreenUpdates(_ shouldLock: Bool) {
guard shouldLock else { return }
CATransaction.begin()
CATransaction.lock()
}

/// Unlock screen updates using a `CATransaction`.
///
/// - Parameters:
/// - Parameter shouldLock: A boolean value indicating if the method should
/// be invoked or not. This is determined by the
/// amount of child view controllers in the controller.
/// - scrollViews: A dictionary of scroll views related to the view controller.
private func unlockScreenUpdates(_ shouldUnlock: Bool, scrollViews: [UIScrollView]) {
guard shouldUnlock else { return }
syncOldScrollViews(scrollViews, with: indexScrollViews())
let nearFuture = DispatchTime.now() + 0.3
DispatchQueue.main.asyncAfter(deadline: nearFuture) {
CATransaction.unlock()
CATransaction.commit()
}
}

/// Validate if the current class was injected by checking the contents
/// of the notification.
///
Expand All @@ -60,28 +15,15 @@ import UIKit
performInjection()
}

/// Clean up view hierarchy by removing child view controllers, view and layers.
private func performCleanUp() {
switch self {
case _ as UINavigationController:
break
case let tabBarController as UITabBarController:
tabBarController.setViewControllers([], animated: true)
default:
removeChildViewControllers()
removeViewsAndLayers()
}
}

/// Invoke all injection related methods in sequence.
/// If this method is invoked with animations enabled,
/// a snapshot of the current view will be created and
/// added to the applications window in order to nicely
/// transition to the new controller view state.
private func performInjection() {
let options: UIView.AnimationOptions = [.allowAnimatedContent,
.beginFromCurrentState,
.layoutSubviews]
.beginFromCurrentState,
.layoutSubviews]
if Injection.animations, let snapshot = self.view.snapshotView(afterScreenUpdates: false) {
let maskView = UIView()
maskView.frame.size = snapshot.frame.size
Expand All @@ -90,8 +32,8 @@ import UIKit
snapshot.mask = maskView
view.window?.addSubview(snapshot)
let oldScrollViews = indexScrollViews()
performCleanUp()
reloadUserInterface()
resetViewControllerState()
rebuildViewContorllerState()
syncOldScrollViews(oldScrollViews, with: indexScrollViews())
UIView.animate(withDuration: 0.25, delay: 0.0, options: options, animations: {
snapshot.alpha = 0.0
Expand All @@ -101,32 +43,16 @@ import UIKit
} else {
let scrollViews = indexScrollViews()
lockScreenUpdates(!scrollViews.isEmpty)
performCleanUp()
reloadUserInterface()
resetViewControllerState()
rebuildViewContorllerState()
unlockScreenUpdates(!scrollViews.isEmpty, scrollViews: scrollViews)
}
}

/// Sync two scroll views content offset if they are of the same type.
///
/// - Parameters:
/// - oldScrollViews: An array of scroll views from before the injection occured.
/// - newScrollViews: An array of new scroll views after the injection occured.
private func syncOldScrollViews(_ oldScrollViews: [UIScrollView], with newScrollViews: [UIScrollView]) {
for (offset, scrollView) in newScrollViews.enumerated() {
if offset < oldScrollViews.count {
let oldScrollView = oldScrollViews[offset]
if type(of: scrollView) == type(of: oldScrollView) {
scrollView.contentOffset = oldScrollView.contentOffset
}
}
}
}

/// Will invoke `viewDidLoad` to run view controllers setup operations.
/// In addition, it will force all subview to layout and collection & table views
/// to reload. This is to make sure that we are displaying the latest changes.
private func reloadUserInterface() {
private func rebuildViewContorllerState() {
viewDidLoad()
view.subviews.forEach { view in
view.setNeedsLayout()
Expand All @@ -142,6 +68,34 @@ import UIKit
}
}

/// Lock screen updates using a `CATransaction`.
///
/// - Parameter shouldLock: A boolean value indicating if the method should
/// be invoked or not. This is determined by the
/// amount of child view controllers in the controller.
private func lockScreenUpdates(_ shouldLock: Bool) {
guard shouldLock else { return }
CATransaction.begin()
CATransaction.lock()
}

/// Unlock screen updates using a `CATransaction`.
///
/// - Parameters:
/// - Parameter shouldLock: A boolean value indicating if the method should
/// be invoked or not. This is determined by the
/// amount of child view controllers in the controller.
/// - scrollViews: A dictionary of scroll views related to the view controller.
private func unlockScreenUpdates(_ shouldUnlock: Bool, scrollViews: [UIScrollView]) {
guard shouldUnlock else { return }
syncOldScrollViews(scrollViews, with: indexScrollViews())
let nearFuture = DispatchTime.now() + 0.3
DispatchQueue.main.asyncAfter(deadline: nearFuture) {
CATransaction.unlock()
CATransaction.commit()
}
}

/// Create an index of the content offsets for all underlying scroll views.
///
/// - Returns: A dictionary of scroll views and their current origin.
Expand All @@ -166,6 +120,52 @@ import UIKit
return scrollViews
}

/// Sync two scroll views content offset if they are of the same type.
///
/// - Parameters:
/// - oldScrollViews: An array of scroll views from before the injection occured.
/// - newScrollViews: An array of new scroll views after the injection occured.
private func syncOldScrollViews(_ oldScrollViews: [UIScrollView], with newScrollViews: [UIScrollView]) {
for (offset, scrollView) in newScrollViews.enumerated() {
if offset < oldScrollViews.count {
let oldScrollView = oldScrollViews[offset]
if type(of: scrollView) == type(of: oldScrollView) {
scrollView.contentOffset = oldScrollView.contentOffset
}
}
}
}

/// Removes all child view controllers.
private func removeChildViewControllers() {
#if swift(>=4.2)
children.forEach { controller in
controller.willMove(toParent: nil)
controller.view.removeFromSuperview()
controller.removeFromParent()
}
#else
childViewControllers.forEach { controller in
controller.willMove(toParentViewController: nil)
controller.view.removeFromSuperview()
controller.removeFromParentViewController()
}
#endif
}

/// Clean up view hierarchy by removing child view controllers, view and layers.
private func resetViewControllerState() {
switch self {
case _ as UINavigationController:
break
case let tabBarController as UITabBarController:
tabBarController.setViewControllers([], animated: true)
default:
removeChildViewControllers()
removeViewsAndLayers()
}
}

/// Removes all views and layers from a view.
private func removeViewsAndLayers() {
view.subviews.forEach {
Expand Down
Loading

0 comments on commit 92496ee

Please sign in to comment.