Commit d21da35d by Daniel Dahan

reworked MotionContext

parent aa31695e
...@@ -29,341 +29,462 @@ ...@@ -29,341 +29,462 @@
import UIKit import UIKit
public class MotionContext { public class MotionContext {
internal var motionIdentifierToSourceView = [String: UIView]() /// A reference of motion identifiers to source views.
internal var motionIdentifierToDestinationView = [String: UIView]() internal var motionIdentifierToSourceView = [String: UIView]()
internal var snapshotViews = [UIView: UIView]()
internal var viewAlphas = [UIView: CGFloat]() /// A reference of motion identifiers to destination views.
internal var targetStates = [UIView: MotionTargetState]() internal var motionIdentifierToDestinationView = [String: UIView]()
internal var superviewToNoSnapshotSubviewMap: [UIView: [(Int, UIView)]] = [:]
/// A reference of the snapshot to source/destination view.
internal var defaultCoordinateSpace: MotionCoordinateSpace = .local internal var viewToSnapshot = [UIView: UIView]()
internal init(container: UIView) { /// A reference to the view to view alpha value.
self.container = container internal var viewToAlphas = [UIView: CGFloat]()
}
/// A reference of view to transition target state.
internal func set(fromViews: [UIView], toViews: [UIView]) { internal var viewToTargetState = [UIView: MotionTargetState]()
self.fromViews = fromViews
self.toViews = toViews /// A reference of the superview to the subviews snapshots.
process(views: fromViews, idMap: &motionIdentifierToSourceView) internal var superviewToNoSnapshotSubviewMap = [UIView: [(Int, UIView)]]()
process(views: toViews, idMap: &motionIdentifierToDestinationView)
} /// A reference to the default coordinate space for transitions.
internal var defaultCoordinateSpace = MotionCoordinateSpace.local
/// The container view holding all of the animating views.
public let container: UIView
/// A flattened list of all views from the source view controller.
public var fromViews: [UIView]!
/// A flattened list of all views from the destination view controller.
public var toViews: [UIView]!
/**
An initializer that accepts a container transition view.
- Parameter container: A UIView.
*/
internal init(container: UIView) {
self.container = container
}
}
internal func process(views: [UIView], idMap: inout [String: UIView]) { internal extension MotionContext {
for view in views { /**
view.layer.removeAllAnimations() Sets the from-views and to-views within the transition context.
if container.convert(view.bounds, from: view).intersects(container.bounds) { - Parameter fromViews: An Array of UIViews.
if let motionIdentifier = view.motionIdentifier { - Parameter toViews: An Array of UIViews.
idMap[motionIdentifier] = view */
} func set(fromViews: [UIView], toViews: [UIView]) {
if let transitions = view.motionTransitions { self.fromViews = fromViews
targetStates[view] = MotionTargetState(transitions: transitions) self.toViews = toViews
map(views: fromViews, identifierMap: &motionIdentifierToSourceView)
map(views: toViews, identifierMap: &motionIdentifierToDestinationView)
}
/**
Maps the views to their respective identifier index.
- Parameter views: An Array of UIViews.
- Parameter identifierMap: A Dicionary of String to UIView pairs.
*/
func map(views: [UIView], identifierMap: inout [String: UIView]) {
for v in views {
v.layer.removeAllAnimations()
if container.convert(v.bounds, from: v).intersects(container.bounds) {
if let i = v.motionIdentifier {
identifierMap[i] = v
}
if let i = v.motionTransitions {
viewToTargetState[v] = MotionTargetState(transitions: i)
}
}
} }
}
} }
}
/**
The container holding all of the animating views
*/
public let container: UIView
/**
A flattened list of all views from source ViewController
*/
public var fromViews: [UIView]!
/**
A flattened list of all views from destination ViewController
*/
public var toViews: [UIView]!
} }
// public public extension MotionContext {
extension MotionContext { /**
A subscript that takes a given view and retrieves a
/** MotionTargetState if one exists.
- Returns: a source view matching the motionIdentifier, nil if not found - Parameter view: A UIView.
*/ - Returns: An optional MotionTargetState.
public func sourceView(for motionIdentifier: String) -> UIView? { */
return motionIdentifierToSourceView[motionIdentifier] subscript(view: UIView) -> MotionTargetState? {
} get {
return viewToTargetState[view]
/** }
- Returns: a destination view matching the motionIdentifier, nil if not found set {
*/ viewToTargetState[view] = newValue
public func destinationView(for motionIdentifier: String) -> UIView? { }
return motionIdentifierToDestinationView[motionIdentifier]
}
/**
- Returns: a view with the same motionIdentifier, but on different view controller, nil if not found
*/
public func pairedView(for view: UIView) -> UIView? {
if let id = view.motionIdentifier {
if sourceView(for: id) == view {
return destinationView(for: id)
} else if destinationView(for: id) == view {
return sourceView(for: id)
}
} }
return nil }
}
/** public extension MotionContext {
- Returns: a snapshot view for animation /**
*/ Retrieves a source view matching the motionIdentifier, nil if not found.
public func snapshotView(for view: UIView) -> UIView { - Parameter for motionIdentifier: A String.
if let snapshot = snapshotViews[view] { - Returns: An optional UIView.
return snapshot */
func sourceView(for motionIdentifier: String) -> UIView? {
return motionIdentifierToSourceView[motionIdentifier]
} }
var containerView = container /**
let coordinateSpace = targetStates[view]?.coordinateSpace ?? defaultCoordinateSpace Retrieves a destination view matching the motionIdentifier, nil if not found.
switch coordinateSpace { - Parameter for motionIdentifier: A String.
case .local: - Returns: An optional UIView.
containerView = view */
while containerView != container, snapshotViews[containerView] == nil, let superview = containerView.superview { func destinationView(for motionIdentifier: String) -> UIView? {
containerView = superview return motionIdentifierToDestinationView[motionIdentifier]
}
if let snapshot = snapshotViews[containerView] {
containerView = snapshot
}
case .sameParent:
containerView = view.superview!
case .global:
break
} }
unhide(view: view) /**
Retrieves the matching view with the same motionIdentifier found in the
// capture a snapshot without alpha & cornerRadius source and destination view controllers.
let oldCornerRadius = view.layer.cornerRadius - Returns: An optional UIView.
let oldAlpha = view.alpha */
view.layer.cornerRadius = 0 func transitionPairedView(for view: UIView) -> UIView? {
view.alpha = 1 if let i = view.motionIdentifier {
if view == sourceView(for: i) {
let snapshot: UIView return destinationView(for: i)
let snapshotType: MotionSnapshotType = self[view]?.snapshotType ?? .optimized
} else if view == destinationView(for: i) {
switch snapshotType { return sourceView(for: i)
case .normal: }
snapshot = view.snapshotView(afterScreenUpdates: true)!
case .layerRender:
snapshot = view.slowSnapshotView()
case .noSnapshot:
if superviewToNoSnapshotSubviewMap[view.superview!] == nil {
superviewToNoSnapshotSubviewMap[view.superview!] = []
}
superviewToNoSnapshotSubviewMap[view.superview!]!.append((view.superview!.subviews.index(of: view)!, view))
snapshot = view
case .optimized:
#if os(tvOS)
snapshot = view.snapshotView(afterScreenUpdates: true)!
#else
if #available(iOS 9.0, *), let stackView = view as? UIStackView {
snapshot = stackView.slowSnapshotView()
} else if let imageView = view as? UIImageView, view.subviews.isEmpty {
let contentView = UIImageView(image: imageView.image)
contentView.frame = imageView.bounds
contentView.contentMode = imageView.contentMode
contentView.tintColor = imageView.tintColor
contentView.backgroundColor = imageView.backgroundColor
let snapShotView = UIView()
snapShotView.addSubview(contentView)
snapshot = snapShotView
} else if let barView = view as? UINavigationBar, barView.isTranslucent {
let newBarView = UINavigationBar(frame: barView.frame)
newBarView.barStyle = barView.barStyle
newBarView.tintColor = barView.tintColor
newBarView.barTintColor = barView.barTintColor
newBarView.clipsToBounds = false
// take a snapshot without the background
barView.layer.sublayers![0].opacity = 0
let realSnapshot = barView.snapshotView(afterScreenUpdates: true)!
barView.layer.sublayers![0].opacity = 1
newBarView.addSubview(realSnapshot)
snapshot = newBarView
} else if let effectView = view as? UIVisualEffectView {
snapshot = UIVisualEffectView(effect: effectView.effect)
snapshot.frame = effectView.bounds
} else {
snapshot = view.snapshotView(afterScreenUpdates: true)!
} }
#endif
return nil
} }
#if os(tvOS) /**
if let imageView = view as? UIImageView, imageView.adjustsImageWhenAncestorFocused { Retrieves the snapshot view for a given view.
snapshot.frame = imageView.focusedFrameGuide.layoutFrame - Parameter for view: A UIView.
} - Returns: A UIView.
#endif */
func snapshotView(for view: UIView) -> UIView {
view.layer.cornerRadius = oldCornerRadius if let v = viewToSnapshot[view] {
view.alpha = oldAlpha return v
}
if snapshotType != .noSnapshot { var containerView = container
snapshot.layer.allowsGroupOpacity = false let coordinateSpace = viewToTargetState[view]?.coordinateSpace ?? defaultCoordinateSpace
switch coordinateSpace {
case .local:
containerView = view
while containerView != container, nil == viewToSnapshot[containerView], let superview = containerView.superview {
containerView = superview
}
if let snapshot = viewToSnapshot[containerView] {
containerView = snapshot
}
case .sameParent:
containerView = view.superview!
case .global:
break
}
if !(view is UINavigationBar), let contentView = snapshot.subviews.get(0) { unhide(view: view)
// the Snapshot's contentView must have hold the cornerRadius value,
// since the snapshot might not have maskToBounds set
contentView.layer.cornerRadius = view.layer.cornerRadius
contentView.layer.masksToBounds = true
}
snapshot.layer.cornerRadius = view.layer.cornerRadius // Capture a snapshot without the alpha & cornerRadius values.
snapshot.layer.zPosition = view.layer.zPosition let oldCornerRadius = view.layer.cornerRadius
snapshot.layer.opacity = view.layer.opacity view.layer.cornerRadius = 0
snapshot.layer.isOpaque = view.layer.isOpaque
snapshot.layer.anchorPoint = view.layer.anchorPoint let oldAlpha = view.alpha
snapshot.layer.masksToBounds = view.layer.masksToBounds view.alpha = 1
snapshot.layer.borderColor = view.layer.borderColor
snapshot.layer.borderWidth = view.layer.borderWidth
snapshot.layer.transform = view.layer.transform
snapshot.layer.contentsRect = view.layer.contentsRect
snapshot.layer.contentsScale = view.layer.contentsScale
if self[view]?.displayShadow ?? true { let snapshot: UIView
snapshot.layer.shadowRadius = view.layer.shadowRadius let snapshotType: MotionSnapshotType = self[view]?.snapshotType ?? .optimized
snapshot.layer.shadowOpacity = view.layer.shadowOpacity
snapshot.layer.shadowColor = view.layer.shadowColor switch snapshotType {
snapshot.layer.shadowOffset = view.layer.shadowOffset case .normal:
snapshot.layer.shadowPath = view.layer.shadowPath snapshot = view.snapshotView(afterScreenUpdates: true)!
}
} case .layerRender:
snapshot = view.slowSnapshotView()
case .noSnapshot:
if nil == superviewToNoSnapshotSubviewMap[view.superview!] {
superviewToNoSnapshotSubviewMap[view.superview!] = []
}
superviewToNoSnapshotSubviewMap[view.superview!]!.append((view.superview!.subviews.index(of: view)!, view))
snapshot = view
case .optimized:
#if os(tvOS)
snapshot = view.snapshotView(afterScreenUpdates: true)!
#else
if #available(iOS 9.0, *), let stackView = view as? UIStackView {
snapshot = stackView.slowSnapshotView()
} else if let imageView = view as? UIImageView, view.subviews.isEmpty {
let contentView = UIImageView(image: imageView.image)
contentView.frame = imageView.bounds
contentView.contentMode = imageView.contentMode
contentView.tintColor = imageView.tintColor
contentView.backgroundColor = imageView.backgroundColor
let snapShotView = UIView()
snapShotView.addSubview(contentView)
snapshot = snapShotView
} else if let navigationBar = view as? UINavigationBar, navigationBar.isTranslucent {
let newNavigationBar = UINavigationBar(frame: navigationBar.frame)
newNavigationBar.barStyle = navigationBar.barStyle
newNavigationBar.tintColor = navigationBar.tintColor
newNavigationBar.barTintColor = navigationBar.barTintColor
newNavigationBar.clipsToBounds = false
// Take a snapshot without the background.
navigationBar.layer.sublayers![0].opacity = 0
let realSnapshot = navigationBar.snapshotView(afterScreenUpdates: true)!
navigationBar.layer.sublayers![0].opacity = 1
newNavigationBar.addSubview(realSnapshot)
snapshot = newNavigationBar
} else if let effectView = view as? UIVisualEffectView {
snapshot = UIVisualEffectView(effect: effectView.effect)
snapshot.frame = effectView.bounds
} else {
snapshot = view.snapshotView(afterScreenUpdates: true)!
}
#endif
}
snapshot.frame = containerView.convert(view.bounds, from: view) #if os(tvOS)
snapshot.motionIdentifier = view.motionIdentifier if let imageView = view as? UIImageView, imageView.adjustsImageWhenAncestorFocused {
snapshot.frame = imageView.focusedFrameGuide.layoutFrame
}
#endif
hide(view: view) view.layer.cornerRadius = oldCornerRadius
view.alpha = oldAlpha
if let pairedView = pairedView(for: view), let pairedSnapshot = snapshotViews[pairedView] { if .noSnapshot != snapshotType {
let siblingViews = pairedView.superview!.subviews snapshot.layer.allowsGroupOpacity = false
let nextSiblings = siblingViews[siblingViews.index(of: pairedView)!+1..<siblingViews.count]
containerView.addSubview(pairedSnapshot) if !(view is UINavigationBar), let contentView = snapshot.subviews.get(0) {
containerView.addSubview(snapshot) /**
for subview in pairedView.subviews { The snapshot's contentView must have the cornerRadius value,
insertGlobalViewTree(view: subview) since the snapshot might not have maskToBounds set
} */
for sibling in nextSiblings { contentView.layer.cornerRadius = view.layer.cornerRadius
insertGlobalViewTree(view: sibling) contentView.layer.masksToBounds = true
} }
} else {
containerView.addSubview(snapshot) snapshot.layer.cornerRadius = view.layer.cornerRadius
} snapshot.layer.zPosition = view.layer.zPosition
containerView.addSubview(snapshot) snapshot.layer.opacity = view.layer.opacity
snapshotViews[view] = snapshot snapshot.layer.isOpaque = view.layer.isOpaque
return snapshot snapshot.layer.anchorPoint = view.layer.anchorPoint
} snapshot.layer.masksToBounds = view.layer.masksToBounds
snapshot.layer.borderColor = view.layer.borderColor
snapshot.layer.borderWidth = view.layer.borderWidth
snapshot.layer.transform = view.layer.transform
snapshot.layer.contentsRect = view.layer.contentsRect
snapshot.layer.contentsScale = view.layer.contentsScale
if self[view]?.displayShadow ?? true {
snapshot.layer.shadowRadius = view.layer.shadowRadius
snapshot.layer.shadowOpacity = view.layer.shadowOpacity
snapshot.layer.shadowColor = view.layer.shadowColor
snapshot.layer.shadowOffset = view.layer.shadowOffset
snapshot.layer.shadowPath = view.layer.shadowPath
}
}
func insertGlobalViewTree(view: UIView) { snapshot.frame = containerView.convert(view.bounds, from: view)
if targetStates[view]?.coordinateSpace == .global, let snapshot = snapshotViews[view] { snapshot.motionIdentifier = view.motionIdentifier
container.addSubview(snapshot)
} hide(view: view)
for subview in view.subviews {
insertGlobalViewTree(view: subview) if let pairedView = transitionPairedView(for: view), let pairedSnapshot = viewToSnapshot[pairedView] {
let siblingViews = pairedView.superview!.subviews
let nextSiblings = siblingViews[siblingViews.index(of: pairedView)!+1..<siblingViews.count]
containerView.addSubview(pairedSnapshot)
containerView.addSubview(snapshot)
for subview in pairedView.subviews {
insertGlobalViewTree(view: subview)
}
for sibling in nextSiblings {
insertGlobalViewTree(view: sibling)
}
} else {
containerView.addSubview(snapshot)
}
containerView.addSubview(snapshot)
viewToSnapshot[view] = snapshot
return snapshot
} }
}
public subscript(view: UIView) -> MotionTargetState? { /**
get { Inserts the given view into the global context space.
return targetStates[view] - Parameter view: A UIView.
} */
set { func insertGlobalViewTree(view: UIView) {
targetStates[view] = newValue if .global == viewToTargetState[view]?.coordinateSpace, let snapshot = viewToSnapshot[view] {
container.addSubview(snapshot)
}
for v in view.subviews {
insertGlobalViewTree(view: v)
}
} }
}
public func clean() { /// Restores the transition subview map with its superview.
for (superview, subviews) in superviewToNoSnapshotSubviewMap { func clean() {
for (index, view) in subviews.reversed() { for (superview, subviews) in superviewToNoSnapshotSubviewMap {
superview.insertSubview(view, at: index) for (index, view) in subviews.reversed() {
} superview.insertSubview(view, at: index)
}
}
} }
}
} }
// internal internal extension MotionContext {
extension MotionContext { /**
public func hide(view: UIView) { Hides a given view.
if viewAlphas[view] == nil, self[view]?.snapshotType != .noSnapshot { - Parameter view: A UIView.
if view is UIVisualEffectView { */
view.isHidden = true func hide(view: UIView) {
viewAlphas[view] = 1 guard nil == viewToAlphas[view], .noSnapshot != self[view]?.snapshotType else {
} else { return
viewAlphas[view] = view.isOpaque ? .infinity : view.alpha }
view.alpha = 0
} if view is UIVisualEffectView {
view.isHidden = true
viewToAlphas[view] = 1
} else {
viewToAlphas[view] = view.isOpaque ? .infinity : view.alpha
view.alpha = 0
}
} }
}
public func unhide(view: UIView) { /**
if let oldAlpha = viewAlphas[view] { Shows a given view that was hidden.
if view is UIVisualEffectView { - Parameter view: A UIView.
view.isHidden = false */
} else if oldAlpha == .infinity { func unhide(view: UIView) {
view.alpha = 1 guard let oldAlpha = viewToAlphas[view] else {
view.isOpaque = true return
} else { }
view.alpha = oldAlpha
} if view is UIVisualEffectView {
viewAlphas[view] = nil view.isHidden = false
} else if oldAlpha == .infinity {
view.alpha = 1
view.isOpaque = true
} else {
view.alpha = oldAlpha
}
viewToAlphas[view] = nil
} }
}
internal func unhideAll() { /// Shows all given views that are hidden.
for view in viewAlphas.keys { func unhideAll() {
unhide(view: view) for v in viewToAlphas.keys {
unhide(view: v)
}
viewToAlphas.removeAll()
} }
viewAlphas.removeAll()
} /**
internal func unhide(rootView: UIView) { Show a given view and its subviews that are hidden.
unhide(view: rootView) - Parameter rootView: A UIView.
for subview in rootView.subviews { */
unhide(rootView: subview) func unhide(rootView: UIView) {
unhide(view: rootView)
for subview in rootView.subviews {
unhide(rootView: subview)
}
} }
}
internal func removeAllSnapshots() { /// Removes all snapshots that are not using .useNoSnapshot.
for (view, snapshot) in snapshotViews { func removeAllSnapshots() {
if view != snapshot { for (k, v) in viewToSnapshot {
// do not remove when it is using .useNoSnapshot if k != v {
snapshot.removeFromSuperview() v.removeFromSuperview()
} }
} }
}
internal func removeSnapshots(rootView: UIView) {
if let snapshot = snapshotViews[rootView], snapshot != rootView {
snapshot.removeFromSuperview()
}
for subview in rootView.subviews {
removeSnapshots(rootView: subview)
} }
}
internal func snapshots(rootView: UIView) -> [UIView] { /**
var snapshots = [UIView]() Removes the snapshots for a given view and all its subviews.
for v in rootView.flattenedViewHierarchy { - Parameter rootView: A UIVIew.
if let snapshot = snapshotViews[v] { */
snapshots.append(snapshot) func removeSnapshots(rootView: UIView) {
} if let v = viewToSnapshot[rootView], v != rootView {
v.removeFromSuperview()
}
for v in rootView.subviews {
removeSnapshots(rootView: v)
}
} }
return snapshots
} /**
internal func loadViewAlpha(rootView: UIView) { Retrieves the snapshots for a given view and all its subviews.
if let storedAlpha = rootView.motionAlpha { - Parameter rootView: A UIView.
rootView.alpha = storedAlpha - Returns: An Array of UIViews.
rootView.motionAlpha = nil */
func snapshots(rootView: UIView) -> [UIView] {
var snapshots = [UIView]()
for v in rootView.flattenedViewHierarchy {
if let snapshot = viewToSnapshot[v] {
snapshots.append(snapshot)
}
}
return snapshots
} }
for subview in rootView.subviews {
loadViewAlpha(rootView: subview) /**
Sets the alpha values for a given view and its subviews to the
stored alpha value.
- Parameter rootView: A UIView.
*/
func loadViewAlpha(rootView: UIView) {
if let storedAlpha = rootView.motionAlpha {
rootView.alpha = storedAlpha
rootView.motionAlpha = nil
}
for subview in rootView.subviews {
loadViewAlpha(rootView: subview)
}
} }
}
internal func storeViewAlpha(rootView: UIView) { /**
rootView.motionAlpha = viewAlphas[rootView] Stores the alpha values for a given view and its subviews.
for subview in rootView.subviews { - Parameter rootView: A UIView.
storeViewAlpha(rootView: subview) */
func storeViewAlpha(rootView: UIView) {
rootView.motionAlpha = viewToAlphas[rootView]
for subview in rootView.subviews {
storeViewAlpha(rootView: subview)
}
} }
}
} }
...@@ -297,7 +297,7 @@ public extension MotionController { ...@@ -297,7 +297,7 @@ public extension MotionController {
} }
let s = MotionTargetState(transitions: transitions) let s = MotionTargetState(transitions: transitions)
let v = context.pairedView(for: view) ?? view let v = context.transitionPairedView(for: view) ?? view
for a in animators { for a in animators {
a.apply(state: s, to: v) a.apply(state: s, to: v)
......
...@@ -94,9 +94,9 @@ class CascadePreprocessor: BasePreprocessor { ...@@ -94,9 +94,9 @@ class CascadePreprocessor: BasePreprocessor {
let delay = TimeInterval(i) * deltaTime + initialDelay let delay = TimeInterval(i) * deltaTime + initialDelay
func applyDelay(view: UIView) { func applyDelay(view: UIView) {
if context.pairedView(for: view) == nil { if context.transitionPairedView(for: view) == nil {
context[view]?.delay = delay context[view]?.delay = delay
} else if delayMatchedViews, let paired = context.pairedView(for: view) { } else if delayMatchedViews, let paired = context.transitionPairedView(for: view) {
context[view]?.delay = finalDelay context[view]?.delay = finalDelay
context[paired]?.delay = finalDelay context[paired]?.delay = finalDelay
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment