import UIKit import AsyncDisplayKit internal class NavigationItemWrapper { let parentNode: ASDisplayNode private var navigationItem: UINavigationItem private var setTitleListenerKey: Int! private var setLeftBarButtonItemListenerKey: Int! private var setRightBarButtonItemListenerKey: Int! private var previousNavigationItem: UINavigationItem? private var previousItemSetTitleListenerKey: Int? private let titleNode: NavigationTitleNode private var backButtonNode: NavigationBackButtonNode private var leftBarButtonItem: UIBarButtonItem? private var leftBarButtonItemWrapper: BarButtonItemWrapper? private var rightBarButtonItem: UIBarButtonItem? private var rightBarButtonItemWrapper: BarButtonItemWrapper? var backPressed: () -> () = { } var suspendLayout = false init(parentNode: ASDisplayNode, navigationItem: UINavigationItem, previousNavigationItem: UINavigationItem?) { self.parentNode = parentNode self.navigationItem = navigationItem self.previousNavigationItem = previousNavigationItem self.titleNode = NavigationTitleNode(text: "") self.parentNode.addSubnode(titleNode) self.backButtonNode = NavigationBackButtonNode() backButtonNode.pressed = { [weak self] in if let backPressed = self?.backPressed { backPressed() } } self.parentNode.addSubnode(self.backButtonNode) self.previousItemSetTitleListenerKey = previousNavigationItem?.addSetTitleListener({ [weak self] title in self?.setBackButtonTitle(title) return }) self.setTitleListenerKey = navigationItem.addSetTitleListener({ [weak self] title in self?.setTitle(title) return }) self.setLeftBarButtonItemListenerKey = navigationItem.addSetLeftBarButtonItemListener({ [weak self] barButtonItem, animated in self?.setLeftBarButtonItem(barButtonItem, animated: animated) return }) self.setRightBarButtonItemListenerKey = navigationItem.addSetRightBarButtonItemListener({ [weak self] barButtonItem, animated in self?.setRightBarButtonItem(barButtonItem, animated: animated) return }) self.setTitle(navigationItem.title ?? "") self.setBackButtonTitle(previousNavigationItem?.title ?? "Back") self.setLeftBarButtonItem(navigationItem.leftBarButtonItem, animated: false) self.setRightBarButtonItem(navigationItem.rightBarButtonItem, animated: false) } deinit { self.navigationItem.removeSetTitleListener(self.setTitleListenerKey) self.navigationItem.removeSetLeftBarButtonItemListener(self.setLeftBarButtonItemListenerKey) self.navigationItem.removeSetRightBarButtonItemListener(self.setRightBarButtonItemListenerKey) if let previousItemSetTitleListenerKey = self.previousItemSetTitleListenerKey { self.previousNavigationItem?.removeSetTitleListener(previousItemSetTitleListenerKey) } self.titleNode.removeFromSupernode() self.backButtonNode.removeFromSupernode() } func setBackButtonTitle(backButtonTitle: String) { self.backButtonNode.text = backButtonTitle self.layoutItems() } func setTitle(title: String) { self.titleNode.text = title self.layoutItems() } func setLeftBarButtonItem(leftBarButtonItem: UIBarButtonItem?, animated: Bool) { if self.leftBarButtonItem !== leftBarButtonItem { self.leftBarButtonItem = leftBarButtonItem self.leftBarButtonItemWrapper = nil if let leftBarButtonItem = leftBarButtonItem { self.leftBarButtonItemWrapper = BarButtonItemWrapper(parentNode: self.parentNode, barButtonItem: leftBarButtonItem, layoutNeeded: { [weak self] in self?.layoutItems() return }) } } self.backButtonNode.hidden = self.previousNavigationItem == nil || self.leftBarButtonItemWrapper != nil } func setRightBarButtonItem(rightBarButtonItem: UIBarButtonItem?, animated: Bool) { if self.rightBarButtonItem !== rightBarButtonItem { self.rightBarButtonItem = rightBarButtonItem self.rightBarButtonItemWrapper = nil if let rightBarButtonItem = rightBarButtonItem { self.rightBarButtonItemWrapper = BarButtonItemWrapper(parentNode: self.parentNode, barButtonItem: rightBarButtonItem, layoutNeeded: { [weak self] in self?.layoutItems() return }) } } } private var collapsed: Bool { get { return self.parentNode.frame.size.height < (20.0 + 44.0) } } var titleFrame: CGRect { get { return CGRect(x: floor((self.parentNode.frame.size.width - self.titleNode.calculatedSize.width) / 2.0), y: self.collapsed ? 24.0 : 31.0, width: self.titleNode.calculatedSize.width, height: self.titleNode.calculatedSize.height) } } var titlePosition: CGPoint { get { let titleFrame = self.titleFrame return CGPoint(x: CGRectGetMidX(titleFrame), y: CGRectGetMidY(titleFrame)) } } var backButtonFrame: CGRect { get { return CGRect(x: self.collapsed ? 15.0 : 8.0, y: self.collapsed ? 24.0 : 31.0, width: backButtonNode.calculatedSize.width, height: backButtonNode.calculatedSize.height) } } var backButtonLabelFrame: CGRect { get { let backButtonFrame = self.backButtonFrame let labelFrame = self.backButtonNode.labelFrame return CGRect(origin: CGPoint(x: backButtonFrame.origin.x + labelFrame.origin.x, y: backButtonFrame.origin.y + labelFrame.origin.y), size: labelFrame.size) } } var backButtonLabelPosition: CGPoint { get { let backButtonLabelFrame = self.backButtonLabelFrame return CGPoint(x: CGRectGetMidX(backButtonLabelFrame), y: CGRectGetMidY(backButtonLabelFrame)) } } var leftButtonFrame: CGRect? { get { if let leftBarButtonItemWrapper = self.leftBarButtonItemWrapper { return CGRect(x: self.collapsed ? 15.0 : 8.0, y: self.collapsed ? 24.0 : 31.0, width: leftBarButtonItemWrapper.buttonNode.calculatedSize.width, height: leftBarButtonItemWrapper.buttonNode.calculatedSize.height) } else { return nil } } } var rightButtonFrame: CGRect? { get { if let rightBarButtonItemWrapper = self.rightBarButtonItemWrapper { return CGRect(x: self.parentNode.frame.size.width - rightBarButtonItemWrapper.buttonNode.calculatedSize.width - (self.collapsed ? 15.0 : 8.0), y: self.collapsed ? 24.0 : 31.0, width: rightBarButtonItemWrapper.buttonNode.calculatedSize.width, height: rightBarButtonItemWrapper.buttonNode.calculatedSize.height) } else { return nil } } } var transitionState: NavigationItemTransitionState { get { return NavigationItemTransitionState(backButtonPosition: self.backButtonNode.hidden ? nil : self.backButtonLabelPosition, titlePosition: self.titlePosition) } } func layoutItems() { if suspendLayout { return } self.titleNode.measure(self.parentNode.bounds.size) self.titleNode.frame = self.titleFrame self.backButtonNode.measure(self.parentNode.frame.size) self.backButtonNode.frame = self.backButtonFrame self.backButtonNode.layout() if let leftBarButtonItemWrapper = self.leftBarButtonItemWrapper { leftBarButtonItemWrapper.buttonNode.measure(self.parentNode.frame.size) leftBarButtonItemWrapper.buttonNode.frame = self.leftButtonFrame! } if let rightBarButtonItemWrapper = self.rightBarButtonItemWrapper { rightBarButtonItemWrapper.buttonNode.measure(self.parentNode.frame.size) rightBarButtonItemWrapper.buttonNode.frame = self.rightButtonFrame! } } func interpolatePosition(from: CGPoint, _ to: CGPoint, value: CGFloat) -> CGPoint { return CGPoint(x: from.x * (CGFloat(1.0) - value) + to.x * value, y: from.y * (CGFloat(1.0) - value) + to.y * value) } func interpolateValue(from: CGFloat, _ to: CGFloat, value: CGFloat) -> CGFloat { return (from * (CGFloat(1.0) - value)) + (to * value) } func applyPushAnimationProgress(previousItemState previousItemState: NavigationItemTransitionState, value: CGFloat) { let titleStartPosition = CGPoint(x: self.parentNode.frame.size.width + self.titleNode.frame.size.width / 2.0, y: self.titlePosition.y) let titleStartAlpha: CGFloat = 0.0 let titleEndPosition = self.titlePosition let titleEndAlpha: CGFloat = 1.0 self.titleNode.position = self.interpolatePosition(titleStartPosition, titleEndPosition, value: value) self.titleNode.alpha = self.interpolateValue(titleStartAlpha, titleEndAlpha, value: value) self.rightBarButtonItemWrapper?.buttonNode.alpha = self.interpolateValue(0.0, 1.0, value: value) self.leftBarButtonItemWrapper?.buttonNode.alpha = self.interpolateValue(0.0, 1.0, value: value) self.backButtonNode.label.position = self.interpolatePosition(CGPoint(x: previousItemState.titlePosition.x - self.backButtonFrame.origin.x, y: previousItemState.titlePosition.y - self.backButtonFrame.origin.y), CGPoint(x: self.backButtonLabelPosition.x - self.backButtonFrame.origin.x, y: self.backButtonLabelPosition.y - self.backButtonFrame.origin.y), value: value) self.backButtonNode.alpha = self.interpolateValue(0.0, 1.0, value: value) } func applyPushAnimationProgress(nextItemState nextItemState: NavigationItemTransitionState, value: CGFloat) { let titleStartPosition = self.titlePosition let titleStartAlpha: CGFloat = 1.0 var titleEndPosition = CGPoint(x: -self.titleNode.frame.size.width / 2.0, y: self.titlePosition.y) if let nextItemBackButtonPosition = nextItemState.backButtonPosition { titleEndPosition = nextItemBackButtonPosition } let titleEndAlpha: CGFloat = 0.0 self.titleNode.position = self.interpolatePosition(titleStartPosition, titleEndPosition, value: value) self.titleNode.alpha = self.interpolateValue(titleStartAlpha, titleEndAlpha, value: value) self.rightBarButtonItemWrapper?.buttonNode.alpha = self.interpolateValue(1.0, 0.0, value: value) self.leftBarButtonItemWrapper?.buttonNode.alpha = self.interpolateValue(1.0, 0.0, value: value) self.backButtonNode.label.position = self.interpolatePosition(CGPoint(x: self.backButtonLabelPosition.x - self.backButtonFrame.origin.x, y: self.backButtonLabelPosition.y - self.backButtonFrame.origin.y), CGPoint(x: -self.backButtonLabelFrame.size.width - self.backButtonFrame.origin.x, y: self.backButtonLabelPosition.y - self.backButtonFrame.origin.y), value: value) self.backButtonNode.label.alpha = self.interpolateValue(1.0, 0.0, value: value) self.backButtonNode.arrow.alpha = self.interpolateValue(1.0, nextItemState.backButtonPosition == nil ? 0.0 : 1.0, value: value) } func applyPopAnimationProgress(previousItemState previousItemState: NavigationItemTransitionState, value: CGFloat) { var titleStartPosition = CGPoint(x: -self.titleNode.frame.size.width / 2.0, y: self.titlePosition.y) if let previousItemBackButtonPosition = previousItemState.backButtonPosition { titleStartPosition = previousItemBackButtonPosition } let titleStartAlpha: CGFloat = 0.0 let titleEndPosition = self.titlePosition let titleEndAlpha: CGFloat = 1.0 self.titleNode.position = self.interpolatePosition(titleStartPosition, titleEndPosition, value: value) self.titleNode.alpha = self.interpolateValue(titleStartAlpha, titleEndAlpha, value: value) self.rightBarButtonItemWrapper?.buttonNode.alpha = self.interpolateValue(0.0, 1.0, value: value) self.leftBarButtonItemWrapper?.buttonNode.alpha = self.interpolateValue(0.0, 1.0, value: value) self.backButtonNode.label.position = self.interpolatePosition(CGPoint(x: -self.backButtonLabelFrame.size.width - self.backButtonFrame.origin.x, y: self.backButtonLabelPosition.y - self.backButtonFrame.origin.y), CGPoint(x: self.backButtonLabelPosition.x - self.backButtonFrame.origin.x, y: self.backButtonLabelPosition.y - self.backButtonFrame.origin.y), value: value) self.backButtonNode.label.alpha = self.interpolateValue(0.0, 1.0, value: value) self.backButtonNode.arrow.alpha = self.interpolateValue(previousItemState.backButtonPosition == nil ? 0.0 : 1.0, 1.0, value: value) } func applyPopAnimationProgress(nextItemState nextItemState: NavigationItemTransitionState, value: CGFloat) { let titleStartPosition = self.titlePosition let titleStartAlpha: CGFloat = 1.0 let titleEndPosition = CGPoint(x: self.parentNode.frame.size.width + self.titleNode.frame.size.width / 2.0, y: self.titlePosition.y) let titleEndAlpha: CGFloat = 0.0 self.titleNode.position = self.interpolatePosition(titleStartPosition, titleEndPosition, value: value) self.titleNode.alpha = self.interpolateValue(titleStartAlpha, titleEndAlpha, value: value) self.rightBarButtonItemWrapper?.buttonNode.alpha = self.interpolateValue(1.0, 0.0, value: value) self.leftBarButtonItemWrapper?.buttonNode.alpha = self.interpolateValue(1.0, 0.0, value: value) self.backButtonNode.label.position = self.interpolatePosition(CGPoint(x: self.backButtonLabelPosition.x - self.backButtonFrame.origin.x, y: self.backButtonLabelPosition.y - self.backButtonFrame.origin.y), CGPoint(x: nextItemState.titlePosition.x - self.backButtonFrame.origin.x, y: nextItemState.titlePosition.y - self.backButtonFrame.origin.y), value: value) self.backButtonNode.label.alpha = self.interpolateValue(1.0, 0.0, value: value) self.backButtonNode.arrow.alpha = self.interpolateValue(1.0, 0.0, value: value) } func animatePush(previousItemWrapper: NavigationItemWrapper?, duration: NSTimeInterval) { if let previousItemWrapper = previousItemWrapper { self.suspendLayout = true self.backButtonNode.suspendLayout = true let transitionState = self.transitionState let previousItemState = previousItemWrapper.transitionState self.applyPushAnimationProgress(previousItemState: previousItemState, value: 0.0) previousItemWrapper.applyPushAnimationProgress(nextItemState: transitionState, value: 0.0) UIView.animateWithDuration(duration, delay: 0.0, options: UIViewAnimationOptions(rawValue: 7 << 16), animations: { () -> Void in self.applyPushAnimationProgress(previousItemState: previousItemState, value: 1.0) previousItemWrapper.applyPushAnimationProgress(nextItemState: transitionState, value: 1.0) }, completion: { completed in self.suspendLayout = false self.backButtonNode.suspendLayout = false previousItemWrapper.applyPushAnimationProgress(nextItemState: self.transitionState, value: 1.0) }) } } func animatePop(previousItemWrapper: NavigationItemWrapper?, duration: NSTimeInterval) { if let previousItemWrapper = previousItemWrapper { self.applyPopAnimationProgress(previousItemState: previousItemWrapper.transitionState, value: 0.0) previousItemWrapper.applyPopAnimationProgress(nextItemState: self.transitionState, value: 0.0) UIView.animateWithDuration(duration, delay: 0.0, options: UIViewAnimationOptions(rawValue: 7 << 16), animations: { () -> Void in self.applyPopAnimationProgress(previousItemState: previousItemWrapper.transitionState, value: 1.0) previousItemWrapper.applyPopAnimationProgress(nextItemState: self.transitionState, value: 1.0) }, completion: { completed in previousItemWrapper.applyPopAnimationProgress(nextItemState: self.transitionState, value: 0.0) }) } } func setInteractivePopProgress(progress: CGFloat, previousItemWrapper: NavigationItemWrapper) { self.applyPopAnimationProgress(previousItemState: previousItemWrapper.transitionState, value: progress) previousItemWrapper.applyPopAnimationProgress(nextItemState: self.transitionState, value: progress) } }