diff --git a/Display.xcodeproj/project.pbxproj b/Display.xcodeproj/project.pbxproj index d153af1160..8ff689f0b6 100644 --- a/Display.xcodeproj/project.pbxproj +++ b/Display.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ D0078A681C92B21400DF6D92 /* StatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0078A671C92B21400DF6D92 /* StatusBar.swift */; }; D007B9A81D1D3B5400DA746D /* PresentableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007B9A71D1D3B5400DA746D /* PresentableViewController.swift */; }; + D00C7CD21E3657570080C3D5 /* TextFieldNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00C7CD11E3657570080C3D5 /* TextFieldNode.swift */; }; D015F7521D1AE08D00E269B5 /* ContainableController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7511D1AE08D00E269B5 /* ContainableController.swift */; }; D015F7541D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7531D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift */; }; D015F7581D1B467200E269B5 /* ActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7571D1B467200E269B5 /* ActionSheetController.swift */; }; @@ -80,6 +81,7 @@ D08E903C1D2417E000533158 /* ActionSheetButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E903B1D2417E000533158 /* ActionSheetButtonItem.swift */; }; D08E903E1D24187900533158 /* ActionSheetItemGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E903D1D24187900533158 /* ActionSheetItemGroup.swift */; }; D08E90471D243C2F00533158 /* HighlightTrackingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E90461D243C2F00533158 /* HighlightTrackingButton.swift */; }; + D0A749951E3A9E7B00AD786E /* SwitchNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A749941E3A9E7B00AD786E /* SwitchNode.swift */; }; D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AE2CA51C94548900F2FD3C /* GenerateImage.swift */; }; D0AE3D4D1D25C816001CCE13 /* NavigationBarTransitionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AE3D4C1D25C816001CCE13 /* NavigationBarTransitionState.swift */; }; D0B367201C94A53A00346D2E /* StatusBarProxyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B3671F1C94A53A00346D2E /* StatusBarProxyNode.swift */; }; @@ -102,6 +104,10 @@ D0C85DD61D1C600D00124894 /* ActionSheetButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C85DD51D1C600D00124894 /* ActionSheetButtonNode.swift */; }; D0CD12161CCFEB4E000DE7BC /* ScrollToTopProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CD12151CCFEB4E000DE7BC /* ScrollToTopProxyView.swift */; }; D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D94A161D3814F900740E02 /* UniversalTapRecognizer.swift */; }; + D0DA444C1E4DCA4A005FDCA7 /* AlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA444B1E4DCA4A005FDCA7 /* AlertController.swift */; }; + D0DA444E1E4DCA6E005FDCA7 /* AlertControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA444D1E4DCA6E005FDCA7 /* AlertControllerNode.swift */; }; + D0DA44501E4DCBDE005FDCA7 /* AlertContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA444F1E4DCBDE005FDCA7 /* AlertContentNode.swift */; }; + D0DA44521E4DCC11005FDCA7 /* TextAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DA44511E4DCC11005FDCA7 /* TextAlertController.swift */; }; D0DC48541BF93D8B00F672FD /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC48531BF93D8A00F672FD /* TabBarController.swift */; }; D0DC48561BF945DD00F672FD /* TabBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC48551BF945DD00F672FD /* TabBarNode.swift */; }; D0DC485F1BF949FB00F672FD /* TabBarContollerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DC485E1BF949FB00F672FD /* TabBarContollerNode.swift */; }; @@ -125,6 +131,7 @@ /* Begin PBXFileReference section */ D0078A671C92B21400DF6D92 /* StatusBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBar.swift; sourceTree = ""; }; D007B9A71D1D3B5400DA746D /* PresentableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentableViewController.swift; sourceTree = ""; }; + D00C7CD11E3657570080C3D5 /* TextFieldNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldNode.swift; sourceTree = ""; }; D015F7511D1AE08D00E269B5 /* ContainableController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainableController.swift; sourceTree = ""; }; D015F7531D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemContainedControllerTransitionCoordinator.swift; sourceTree = ""; }; D015F7571D1B467200E269B5 /* ActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetController.swift; sourceTree = ""; }; @@ -199,6 +206,7 @@ D08E903B1D2417E000533158 /* ActionSheetButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetButtonItem.swift; sourceTree = ""; }; D08E903D1D24187900533158 /* ActionSheetItemGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetItemGroup.swift; sourceTree = ""; }; D08E90461D243C2F00533158 /* HighlightTrackingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightTrackingButton.swift; sourceTree = ""; }; + D0A749941E3A9E7B00AD786E /* SwitchNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchNode.swift; sourceTree = ""; }; D0AE2CA51C94548900F2FD3C /* GenerateImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateImage.swift; sourceTree = ""; }; D0AE3D4C1D25C816001CCE13 /* NavigationBarTransitionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarTransitionState.swift; sourceTree = ""; }; D0B3671F1C94A53A00346D2E /* StatusBarProxyNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarProxyNode.swift; sourceTree = ""; }; @@ -221,6 +229,10 @@ D0C85DD51D1C600D00124894 /* ActionSheetButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetButtonNode.swift; sourceTree = ""; }; D0CD12151CCFEB4E000DE7BC /* ScrollToTopProxyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollToTopProxyView.swift; sourceTree = ""; }; D0D94A161D3814F900740E02 /* UniversalTapRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniversalTapRecognizer.swift; sourceTree = ""; }; + D0DA444B1E4DCA4A005FDCA7 /* AlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertController.swift; sourceTree = ""; }; + D0DA444D1E4DCA6E005FDCA7 /* AlertControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertControllerNode.swift; sourceTree = ""; }; + D0DA444F1E4DCBDE005FDCA7 /* AlertContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertContentNode.swift; sourceTree = ""; }; + D0DA44511E4DCC11005FDCA7 /* TextAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAlertController.swift; sourceTree = ""; }; D0DC48531BF93D8A00F672FD /* TabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; D0DC48551BF945DD00F672FD /* TabBarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarNode.swift; sourceTree = ""; }; D0DC485E1BF949FB00F672FD /* TabBarContollerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarContollerNode.swift; sourceTree = ""; }; @@ -312,6 +324,8 @@ children = ( D0CD12151CCFEB4E000DE7BC /* ScrollToTopProxyView.swift */, D0E35A021DE473B900BC6096 /* HighlightableButton.swift */, + D00C7CD11E3657570080C3D5 /* TextFieldNode.swift */, + D0A749941E3A9E7B00AD786E /* SwitchNode.swift */, ); name = Nodes; sourceTree = ""; @@ -520,6 +534,7 @@ D015F7551D1B142300E269B5 /* Tab Bar */, D015F7561D1B465600E269B5 /* Action Sheet */, D03725BF1D6DF57B007FC290 /* Context Menu */, + D0DA444A1E4DCA36005FDCA7 /* Alert */, ); name = Controllers; sourceTree = ""; @@ -543,6 +558,17 @@ name = "List Node"; sourceTree = ""; }; + D0DA444A1E4DCA36005FDCA7 /* Alert */ = { + isa = PBXGroup; + children = ( + D0DA444B1E4DCA4A005FDCA7 /* AlertController.swift */, + D0DA444D1E4DCA6E005FDCA7 /* AlertControllerNode.swift */, + D0DA444F1E4DCBDE005FDCA7 /* AlertContentNode.swift */, + D0DA44511E4DCC11005FDCA7 /* TextAlertController.swift */, + ); + name = Alert; + sourceTree = ""; + }; D0DC48521BF93D7C00F672FD /* Tabs */ = { isa = PBXGroup; children = ( @@ -708,8 +734,10 @@ D01E2BE01D90498E0066BF65 /* GridNodeScroller.swift in Sources */, D0C85DD61D1C600D00124894 /* ActionSheetButtonNode.swift in Sources */, D0C2DFD01CC4431D0044FF83 /* ListViewAccessoryItemNode.swift in Sources */, + D0DA44501E4DCBDE005FDCA7 /* AlertContentNode.swift in Sources */, D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */, D015F7581D1B467200E269B5 /* ActionSheetController.swift in Sources */, + D0DA444C1E4DCA4A005FDCA7 /* AlertController.swift in Sources */, D0C2DFC91CC4431D0044FF83 /* ListView.swift in Sources */, D03E7DF91C96C5F200C07816 /* NSWeakReference.m in Sources */, D0DC48541BF93D8B00F672FD /* TabBarController.swift in Sources */, @@ -727,9 +755,11 @@ D05CC31B1B695A9600E235A3 /* NavigationTitleNode.swift in Sources */, D05CC31C1B695A9600E235A3 /* BarButtonItemWrapper.swift in Sources */, D0C2DFCF1CC4431D0044FF83 /* ListViewScroller.swift in Sources */, + D00C7CD21E3657570080C3D5 /* TextFieldNode.swift in Sources */, D0DC485F1BF949FB00F672FD /* TabBarContollerNode.swift in Sources */, D05CC2FA1B6955D000E235A3 /* UINavigationItem+Proxy.m in Sources */, D0C2DFCE1CC4431D0044FF83 /* ListViewAccessoryItem.swift in Sources */, + D0A749951E3A9E7B00AD786E /* SwitchNode.swift in Sources */, D03725C51D6DF8B9007FC290 /* ContextMenuController.swift in Sources */, D03725C31D6DF7A6007FC290 /* ContextMenuAction.swift in Sources */, D007B9A81D1D3B5400DA746D /* PresentableViewController.swift in Sources */, @@ -737,6 +767,7 @@ D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */, D053CB611D22B4F200DD41DF /* CATracingLayer.m in Sources */, D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */, + D0DA44521E4DCC11005FDCA7 /* TextAlertController.swift in Sources */, D081229D1D19AA1C005F7395 /* ContainerViewLayout.swift in Sources */, D0C2DFC71CC4431D0044FF83 /* ListViewItemNode.swift in Sources */, D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */, @@ -757,6 +788,7 @@ D0C85DD41D1C1E6A00124894 /* ActionSheetItemGroupNode.swift in Sources */, D08E903E1D24187900533158 /* ActionSheetItemGroup.swift in Sources */, D02383861DE0E3B4004018B6 /* ListViewIntermediateState.swift in Sources */, + D0DA444E1E4DCA6E005FDCA7 /* AlertControllerNode.swift in Sources */, D0B367201C94A53A00346D2E /* StatusBarProxyNode.swift in Sources */, D05CC2A21B69326C00E235A3 /* Window.swift in Sources */, D015F7541D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift in Sources */, diff --git a/Display.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist b/Display.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist index b987a65bba..8f0ec3539d 100644 --- a/Display.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Display.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ Display.xcscheme orderHint - 20 + 23 DisplayTests.xcscheme orderHint - 14 + 24 SuppressBuildableAutocreation diff --git a/Display/AlertContentNode.swift b/Display/AlertContentNode.swift new file mode 100644 index 0000000000..c95d14ac95 --- /dev/null +++ b/Display/AlertContentNode.swift @@ -0,0 +1,10 @@ +import Foundation +import AsyncDisplayKit + +open class AlertContentNode: ASDisplayNode { + open func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + assertionFailure() + + return CGSize() + } +} diff --git a/Display/AlertController.swift b/Display/AlertController.swift new file mode 100644 index 0000000000..2e2ff29541 --- /dev/null +++ b/Display/AlertController.swift @@ -0,0 +1,57 @@ +import Foundation +import AsyncDisplayKit + +open class AlertController: ViewController { + private var controllerNode: AlertControllerNode { + return self.displayNode as! AlertControllerNode + } + + private let contentNode: AlertContentNode + + public init(contentNode: AlertContentNode) { + self.contentNode = contentNode + + super.init(navigationBar: NavigationBar()) + + self.navigationBar.isHidden = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func loadDisplayNode() { + self.displayNode = AlertControllerNode(contentNode: self.contentNode) + self.displayNodeDidLoad() + + self.controllerNode.dismiss = { [weak self] in + if let strongSelf = self { + strongSelf.controllerNode.animateOut { + self?.dismiss() + } + } + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.controllerNode.animateIn() + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } + + public func dismiss() { + self.presentingViewController?.dismiss(animated: false, completion: nil) + } + + public func dismissAnimated() { + self.controllerNode.animateOut { [weak self] in + self?.dismiss() + } + } +} diff --git a/Display/AlertControllerNode.swift b/Display/AlertControllerNode.swift new file mode 100644 index 0000000000..3655342629 --- /dev/null +++ b/Display/AlertControllerNode.swift @@ -0,0 +1,80 @@ +import Foundation +import AsyncDisplayKit + +final class AlertControllerNode: ASDisplayNode { + private let dimmingNode: ASDisplayNode + private let containerNode: ASDisplayNode + private let effectNode: ASDisplayNode + private let contentNode: AlertContentNode + + var dismiss: (() -> Void)? + + init(contentNode: AlertContentNode) { + self.dimmingNode = ASDisplayNode() + self.dimmingNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + self.containerNode = ASDisplayNode() + self.containerNode.backgroundColor = .white + self.containerNode.layer.cornerRadius = 14.0 + self.containerNode.layer.masksToBounds = true + + self.effectNode = ASDisplayNode(viewBlock: { + let view = UIView()//UIVisualEffectView(effect: UIBlurEffect(style: .light)) + return view + }, didLoad: nil) + + self.contentNode = contentNode + + super.init() + + self.addSubnode(self.dimmingNode) + + self.addSubnode(self.effectNode) + + self.containerNode.addSubnode(self.contentNode) + self.addSubnode(self.containerNode) + } + + override func didLoad() { + super.didLoad() + + self.dimmingNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:)))) + } + + func animateIn() { + self.dimmingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.containerNode.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil) + } + + func animateOut(completion: @escaping () -> Void) { + self.dimmingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.containerNode.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.dimmingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + var insets = layout.insets(options: [.statusBar, .input]) + let maxWidth = min(340.0, layout.size.width - 70.0) + insets.left = floor((layout.size.width - maxWidth) / 2.0) + insets.right = floor((layout.size.width - maxWidth) / 2.0) + let contentAvailableFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: layout.size.width - insets.right, height: layout.size.height - insets.top - insets.bottom)) + let contentSize = self.contentNode.updateLayout(size: contentAvailableFrame.size, transition: transition) + let containerSize = CGSize(width: contentSize.width, height: contentSize.height) + let containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: contentAvailableFrame.minY + floor((contentAvailableFrame.size.height - containerSize.height) / 2.0)), size: containerSize) + + transition.updateFrame(node: self.containerNode, frame: containerFrame) + transition.updateFrame(node: self.effectNode, frame: containerFrame) + transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size)) + } + + @objc func dimmingNodeTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.dismiss?() + } + } +} diff --git a/Display/CAAnimationUtils.swift b/Display/CAAnimationUtils.swift index 1ccd26ca10..93c89b6d99 100644 --- a/Display/CAAnimationUtils.swift +++ b/Display/CAAnimationUtils.swift @@ -39,7 +39,7 @@ public extension CAAnimation { } public extension CALayer { - public func animate(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + public func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation { if timingFunction == kCAMediaTimingFunctionSpring { let animation = makeSpringAnimation(keyPath) animation.fromValue = from @@ -59,7 +59,7 @@ public extension CALayer { animation.speed = speed * Float(animation.duration / duration) animation.isAdditive = additive - self.add(animation, forKey: keyPath) + return animation } else { let k = Float(UIView.animationDurationFactor()) var speed: Float = 1.0 @@ -84,9 +84,55 @@ public extension CALayer { animation.delegate = CALayerAnimationDelegate(completion: completion) } - self.add(animation, forKey: keyPath) + return animation } } + + public func animate(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) + self.add(animation, forKey: keyPath) + } + + public func animateGroup(_ animations: [CAAnimation], key: String) { + let animationGroup = CAAnimationGroup() + var timeOffset = 0.0 + for animation in animations { + animation.beginTime = animation.beginTime + timeOffset + timeOffset += animation.duration / Double(animation.speed) + } + animationGroup.animations = animations + animationGroup.duration = timeOffset + self.add(animationGroup, forKey: key) + } + + public func animateKeyframes(values: [AnyObject], duration: Double, keyPath: String, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + let animation = CAKeyframeAnimation(keyPath: keyPath) + animation.values = values + var keyTimes: [NSNumber] = [] + for i in 0 ..< values.count { + if i == 0 { + keyTimes.append(0.0) + } else if i == values.count - 1 { + keyTimes.append(1.0) + } else { + keyTimes.append((Double(i) / Double(values.count - 1)) as NSNumber) + } + } + animation.keyTimes = keyTimes + animation.speed = speed + animation.duration = duration + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(completion: completion) + } + + self.add(animation, forKey: keyPath) + } public func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, initialVelocity: CGFloat = 0.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { let animation = makeSpringBounceAnimation(keyPath, initialVelocity) @@ -169,6 +215,10 @@ public extension CALayer { self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true) } + public func animatePositionKeyframes(values: [CGPoint], duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + self.animateKeyframes(values: values.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position") + } + public func animateFrame(from: CGRect, to: CGRect, duration: Double, timingFunction: String, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { if from == to { if let completion = completion { @@ -179,7 +229,7 @@ public extension CALayer { var interrupted = false var completedPosition = false var completedBounds = false - var partialCompletion: () -> Void = { + let partialCompletion: () -> Void = { if interrupted || (completedPosition && completedBounds) { if let completion = completion { completion(!interrupted) diff --git a/Display/CATracingLayer.m b/Display/CATracingLayer.m index 0ea557c54c..bd8f209477 100644 --- a/Display/CATracingLayer.m +++ b/Display/CATracingLayer.m @@ -142,6 +142,12 @@ static void traceLayerSurfaces(int depth, CALayer * _Nonnull layer, NSMutableDic @implementation CATracingLayer +- (void)setNeedsDisplay { +} + +- (void)displayIfNeeded { +} + - (bool)isInvalidated { return [[self associatedObjectForKey:CATracingLayerInvalidatedKey] intValue] != 0; } diff --git a/Display/ContainableController.swift b/Display/ContainableController.swift index c734b5ac35..85182f228b 100644 --- a/Display/ContainableController.swift +++ b/Display/ContainableController.swift @@ -219,6 +219,38 @@ public extension ContainedViewLayoutTransition { }) } } + + func updateBackgroundColor(node: ASDisplayNode, color: UIColor, completion: ((Bool) -> Void)? = nil) { + if let nodeColor = node.backgroundColor, nodeColor.isEqual(color) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.backgroundColor = color + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + if let nodeColor = node.backgroundColor { + node.backgroundColor = color + node.layer.animate(from: nodeColor.cgColor, to: color.cgColor, keyPath: "backgroundColor", timingFunction: curve.timingFunction, duration: duration, completion: { result in + if let completion = completion { + completion(result) + } + }) + } else { + node.backgroundColor = color + if let completion = completion { + completion(true) + } + } + } + } + } public protocol ContainableController: class { diff --git a/Display/Font.swift b/Display/Font.swift index 99372d913d..9093a22637 100644 --- a/Display/Font.swift +++ b/Display/Font.swift @@ -22,13 +22,29 @@ public struct Font { } } + public static func light(_ size: CGFloat) -> UIFont { + if #available(iOS 8.2, *) { + return UIFont.systemFont(ofSize: size, weight: UIFontWeightLight) + } else { + return CTFontCreateWithName("HelveticaNeue-Light" as CFString, size, nil) + } + } + public static func italic(_ size: CGFloat) -> UIFont { return UIFont.italicSystemFont(ofSize: size) } } public extension NSAttributedString { - convenience init(string: String, font: UIFont, textColor: UIColor = UIColor.black) { - self.init(string: string, attributes: [NSFontAttributeName: font, NSForegroundColorAttributeName as String: textColor]) + convenience init(string: String, font: UIFont, textColor: UIColor = UIColor.black, paragraphAlignment: NSTextAlignment? = nil) { + var attributes: [String: AnyObject] = [:] + attributes[NSFontAttributeName] = font + attributes[NSForegroundColorAttributeName] = textColor + if let paragraphAlignment = paragraphAlignment { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = paragraphAlignment + attributes[NSParagraphStyleAttributeName] = paragraphStyle + } + self.init(string: string, attributes: attributes) } } diff --git a/Display/GenerateImage.swift b/Display/GenerateImage.swift index aaad94b53b..6119a70611 100644 --- a/Display/GenerateImage.swift +++ b/Display/GenerateImage.swift @@ -61,6 +61,41 @@ public func generateImage(_ size: CGSize, contextGenerator: (CGSize, CGContext) return UIImage(cgImage: image, scale: selectedScale, orientation: .up) } +public func generateImage(_ size: CGSize, opaque: Bool = false, scale: CGFloat? = nil, rotatedContext: (CGSize, CGContext) -> Void) -> UIImage? { + let selectedScale = scale ?? deviceScale + let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale) + let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) + let length = bytesPerRow * Int(scaledSize.height) + let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self) + + guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in + free(bytes) + }) + else { + return nil + } + + let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue)) + + guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else { + return nil + } + + context.scaleBy(x: selectedScale, y: selectedScale) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + rotatedContext(size, context) + + guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) + else { + return nil + } + + return UIImage(cgImage: image, scale: selectedScale, orientation: .up) +} + public func generateFilledCircleImage(diameter: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? { return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/Display/GridItem.swift b/Display/GridItem.swift index a417df781d..cd112ce5c0 100644 --- a/Display/GridItem.swift +++ b/Display/GridItem.swift @@ -11,4 +11,5 @@ public protocol GridSection { public protocol GridItem { var section: GridSection? { get } func node(layout: GridNodeLayout) -> GridItemNode + func update(node: GridItemNode) } diff --git a/Display/GridNode.swift b/Display/GridNode.swift index 09abd7f694..057b1454fb 100644 --- a/Display/GridNode.swift +++ b/Display/GridNode.swift @@ -256,7 +256,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { for updatedItem in transaction.updateItems { self.items[updatedItem.index] = updatedItem.item if let itemNode = self.itemNodes[updatedItem.index] { - //update node + updatedItem.item.update(node: itemNode) } } diff --git a/Display/ListView.swift b/Display/ListView.swift index 2fa306a7bd..8a6b34e8b7 100644 --- a/Display/ListView.swift +++ b/Display/ListView.swift @@ -17,6 +17,16 @@ private final class ListViewBackingLayer: CALayer { override func setNeedsDisplay() { } + + override func displayIfNeeded() { + } + + override func needsDisplay() -> Bool { + return false + } + + override func display() { + } } final class ListViewBackingView: UIView { @@ -32,6 +42,9 @@ final class ListViewBackingView: UIView { override func layoutSubviews() { } + override func setNeedsDisplay() { + } + override func touchesBegan(_ touches: Set, with event: UIEvent?) { self.target?.touchesBegan(touches, with: event) } @@ -1199,6 +1212,8 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if updateAdjacentItemsIndices.isEmpty { completion(state, operations) } else { + let updateAnimation: ListViewItemUpdateAnimation = animated ? .System(duration: insertionAnimationDuration) : .None + var updatedUpdateAdjacentItemsIndices = updateAdjacentItemsIndices let nodeIndex = updateAdjacentItemsIndices.first! @@ -1217,7 +1232,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel } else { self.async(f) } - }, node: referenceNode, width: state.visibleSize.width, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: index == self.items.count - 1 ? nil : self.items[index + 1], animation: .None, completion: { layout, apply in + }, node: referenceNode, width: state.visibleSize.width, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: index == self.items.count - 1 ? nil : self.items[index + 1], animation: updateAnimation, completion: { layout, apply in var updatedState = state var updatedOperations = operations @@ -1321,20 +1336,24 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel var operations = inputOperations var updateIndicesAndItems = updateIndicesAndItems - if updateIndicesAndItems.isEmpty { - completion(state, operations) - } else { - var updateItem = updateIndicesAndItems[0] - if let previousNode = previousNodes[updateItem.index] { - self.nodeForItem(synchronous: synchronous, item: updateItem.item, previousNode: previousNode, index: updateItem.index, previousItem: updateItem.index == 0 ? nil : self.items[updateItem.index - 1], nextItem: updateItem.index == (self.items.count - 1) ? nil : self.items[updateItem.index + 1], width: state.visibleSize.width, updateAnimation: animated ? .System(duration: insertionAnimationDuration) : .None, completion: { _, layout, apply in - state.updateNodeAtItemIndex(updateItem.index, layout: layout, direction: updateItem.directionHint, animation: animated ? .System(duration: insertionAnimationDuration) : .None, apply: apply, operations: &operations) - - updateIndicesAndItems.remove(at: 0) - self.updateNodes(synchronous: synchronous, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) - }) + while true { + if updateIndicesAndItems.isEmpty { + completion(state, operations) + break } else { - updateIndicesAndItems.remove(at: 0) - self.updateNodes(synchronous: synchronous, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) + let updateItem = updateIndicesAndItems[0] + if let previousNode = previousNodes[updateItem.index] { + self.nodeForItem(synchronous: synchronous, item: updateItem.item, previousNode: previousNode, index: updateItem.index, previousItem: updateItem.index == 0 ? nil : self.items[updateItem.index - 1], nextItem: updateItem.index == (self.items.count - 1) ? nil : self.items[updateItem.index + 1], width: state.visibleSize.width, updateAnimation: animated ? .System(duration: insertionAnimationDuration) : .None, completion: { _, layout, apply in + state.updateNodeAtItemIndex(updateItem.index, layout: layout, direction: updateItem.directionHint, animation: animated ? .System(duration: insertionAnimationDuration) : .None, apply: apply, operations: &operations) + + updateIndicesAndItems.remove(at: 0) + self.updateNodes(synchronous: synchronous, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) + }) + break + } else { + updateIndicesAndItems.remove(at: 0) + //self.updateNodes(synchronous: synchronous, animated: animated, updateIndicesAndItems: updateIndicesAndItems, inputState: state, previousNodes: previousNodes, inputOperations: operations, completion: completion) + } } } } @@ -1406,13 +1425,15 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel takenAnimation = true if abs(layout.size.height - previousApparentHeight) > CGFloat(FLT_EPSILON) { - node.addApparentHeightAnimation(layout.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress in + node.addApparentHeightAnimation(layout.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in if let node = node { - node.animateFrameTransition(progress) + node.animateFrameTransition(progress, currentValue) } }) - node.transitionOffset += previousApparentHeight - layout.size.height - node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + if node.rotated { + node.transitionOffset += previousApparentHeight - layout.size.height + node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } } } } @@ -1424,20 +1445,22 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel node.animateRemoved(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor()) } else if animated { if !takenAnimation { - node.addApparentHeightAnimation(nodeFrame.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress in - if let node = node { - node.animateFrameTransition(progress) - } - }) + if !nodeFrame.size.height.isEqual(to: node.apparentHeight) { + node.addApparentHeightAnimation(nodeFrame.size.height, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in + if let node = node { + node.animateFrameTransition(progress, currentValue) + } + }) + } if let previousFrame = previousFrame { if self.debugInfo { assert(true) } - let transitionOffsetDelta = nodeFrame.origin.y - previousFrame.origin.y - previousApparentHeight + layout.size.height + let transitionOffsetDelta = nodeFrame.origin.y - previousFrame.origin.y if node.rotated { - node.transitionOffset -= transitionOffsetDelta + node.transitionOffset -= transitionOffsetDelta - previousApparentHeight + layout.size.height } else { node.transitionOffset += transitionOffsetDelta } @@ -1450,6 +1473,12 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if self.debugInfo { assert(true) } + if !node.rotated { + if !node.insets.top.isZero { + node.transitionOffset += node.insets.top + node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } + } node.animateInsertion(timestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), short: false) } } @@ -1679,20 +1708,23 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if abs(updatedApparentHeight - previousApparentHeight) > CGFloat(FLT_EPSILON) { node.apparentHeight = previousApparentHeight - node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress in + node.animateFrameTransition(0.0, previousApparentHeight) + node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in if let node = node { - node.animateFrameTransition(progress) + node.animateFrameTransition(progress, currentValue) } }) let insetPart: CGFloat = previousInsets.top - layout.insets.top - node.transitionOffset += previousApparentHeight - layout.size.height - insetPart - node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + if node.rotated { + node.transitionOffset += previousApparentHeight - layout.size.height - insetPart + node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp) + } } else { if node.shouldAnimateHorizontalFrameTransition() { - node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress in + node.addApparentHeightAnimation(updatedApparentHeight, duration: insertionAnimationDuration * UIView.animationDurationFactor(), beginAt: timestamp, update: { [weak node] progress, currentValue in if let node = node { - node.animateFrameTransition(progress) + node.animateFrameTransition(progress, currentValue) } }) } @@ -1957,13 +1989,15 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel previousLowerBound = previousFrame.maxY } } else { - offset = previousNode.apparentFrame.minY - previousFrame.minY + if previousNode.canBeUsedAsScrollToItemAnchor { + offset = previousNode.apparentFrame.minY - previousFrame.minY + } } } if offset == nil { let updatedUpperBound = self.itemNodes[0].apparentFrame.minY - let updatedLowerBound = self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY + let updatedLowerBound = max(self.itemNodes[self.itemNodes.count - 1].apparentFrame.maxY, self.visibleSize.height) switch scrollToItem.directionHint { case .Up: @@ -2161,7 +2195,10 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel case let .animated(duration, curve): let previousFrame = headerNode.frame headerNode.frame = headerFrame - let offset = -(headerFrame.minY - previousFrame.minY + transition.2) + var offset = headerFrame.minY - previousFrame.minY + transition.2 + if headerNode.isRotated { + offset = -offset + } switch curve { case .spring: transition.0.animateOffsetAdditive(node: headerNode, offset: offset) @@ -2617,7 +2654,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel if strongSelf.items[index].selectable { strongSelf.highlightedItemIndex = index for itemNode in strongSelf.itemNodes { - if itemNode.index == index { + if itemNode.index == index && itemNode.canBeSelected { if true { //!(itemNode.hitTest(CGPoint(x: strongSelf.touchesPosition.x - itemNode.frame.minX, y: strongSelf.touchesPosition.y - itemNode.frame.minY), with: event) is UIControl) { if !itemNode.isLayerBacked { strongSelf.view.bringSubview(toFront: itemNode.view) @@ -2663,7 +2700,7 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel return nil } - public func forEachItemNode(_ f: @noescape(ASDisplayNode) -> Void) { + public func forEachItemNode(_ f: (ASDisplayNode) -> Void) { for itemNode in self.itemNodes { if itemNode.index != nil { f(itemNode) @@ -2709,13 +2746,17 @@ open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDel self.highlightedItemIndex = index for itemNode in self.itemNodes { if itemNode.index == index { - if !itemNode.isLayerBacked { - self.view.bringSubview(toFront: itemNode.view) - for (_, headerNode) in self.itemHeaderNodes { - self.view.bringSubview(toFront: headerNode.view) + if itemNode.canBeSelected { + if !itemNode.isLayerBacked { + self.view.bringSubview(toFront: itemNode.view) + for (_, headerNode) in self.itemHeaderNodes { + self.view.bringSubview(toFront: headerNode.view) + } } + itemNode.setHighlighted(true, animated: false) + } else { + self.highlightedItemIndex = nil } - itemNode.setHighlighted(true, animated: false) break } } diff --git a/Display/ListViewItem.swift b/Display/ListViewItem.swift index 153847d2ad..7f5db6fe76 100644 --- a/Display/ListViewItem.swift +++ b/Display/ListViewItem.swift @@ -4,6 +4,14 @@ import SwiftSignalKit public enum ListViewItemUpdateAnimation { case None case System(duration: Double) + + public var isAnimated: Bool { + if case .None = self { + return false + } else { + return true + } + } } public struct ListViewItemConfigureNodeFlags: OptionSet { diff --git a/Display/ListViewItemHeader.swift b/Display/ListViewItemHeader.swift index e41f6b7866..25e851f184 100644 --- a/Display/ListViewItemHeader.swift +++ b/Display/ListViewItemHeader.swift @@ -17,6 +17,7 @@ public protocol ListViewItemHeader: class { open class ListViewItemHeaderNode: ASDisplayNode { private final var spring: ListViewItemSpring? let wantsScrollDynamics: Bool + let isRotated: Bool final private(set) var internalStickLocationDistanceFactor: CGFloat = 0.0 final var internalStickLocationDistance: CGFloat = 0.0 private var isFlashingOnScrolling = false @@ -50,8 +51,9 @@ open class ListViewItemHeaderNode: ASDisplayNode { open func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { } - public init(dynamicBounce: Bool = false) { + public init(dynamicBounce: Bool = false, isRotated: Bool = false) { self.wantsScrollDynamics = dynamicBounce + self.isRotated = isRotated if dynamicBounce { self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85) } diff --git a/Display/ListViewItemNode.swift b/Display/ListViewItemNode.swift index 38296f0aad..72e0c8c6a2 100644 --- a/Display/ListViewItemNode.swift +++ b/Display/ListViewItemNode.swift @@ -83,11 +83,18 @@ open class ListViewItemNode: ASDisplayNode { public final var scrollPositioningInsets: UIEdgeInsets = UIEdgeInsets() + public final var canBeUsedAsScrollToItemAnchor: Bool = true + + open var canBeSelected: Bool { + return true + } + public final var insets: UIEdgeInsets = UIEdgeInsets() { didSet { let effectiveInsets = self.insets self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: self.contentSize.width, height: self.contentSize.height + effectiveInsets.top + effectiveInsets.bottom)) - self.bounds = CGRect(origin: CGPoint(x: 0.0, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: self.bounds.size) + let bounds = self.bounds + self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size) if oldValue != self.insets { if let accessoryItemNode = self.accessoryItemNode { @@ -110,14 +117,16 @@ open class ListViewItemNode: ASDisplayNode { private var contentOffset: CGFloat = 0.0 { didSet { let effectiveInsets = self.insets - self.bounds = CGRect(origin: CGPoint(x: 0.0, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: self.bounds.size) + let bounds = self.bounds + self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size) } } public var transitionOffset: CGFloat = 0.0 { didSet { let effectiveInsets = self.insets - self.bounds = CGRect(origin: CGPoint(x: 0.0, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: self.bounds.size) + let bounds = self.bounds + self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size) } } @@ -378,12 +387,12 @@ open class ListViewItemNode: ASDisplayNode { self.setAnimationForKey("insets", animation: animation) } - public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat) -> Void)? = nil) { + public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) { let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in if let strongSelf = self { strongSelf.apparentHeight = currentValue if let update = update { - update(progress) + update(progress, currentValue) } } }) @@ -432,7 +441,7 @@ open class ListViewItemNode: ASDisplayNode { open func setHighlighted(_ highlighted: Bool, animated: Bool) { } - open func animateFrameTransition(_ progress: CGFloat) { + open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) { } diff --git a/Display/NavigationBar.swift b/Display/NavigationBar.swift index f48a44128c..e4fdf024b3 100644 --- a/Display/NavigationBar.swift +++ b/Display/NavigationBar.swift @@ -68,8 +68,13 @@ open class NavigationBar: ASDisplayNode { private var itemTitleListenerKey: Int? private var itemTitleViewListenerKey: Int? + private var itemLeftButtonListenerKey: Int? + private var itemLeftButtonSetEnabledListenerKey: Int? + private var itemRightButtonListenerKey: Int? + private var itemRightButtonSetEnabledListenerKey: Int? + private var _item: UINavigationItem? public var item: UINavigationItem? { get { @@ -80,10 +85,26 @@ open class NavigationBar: ASDisplayNode { previousValue.removeSetTitleListener(itemTitleListenerKey) self.itemTitleListenerKey = nil } + if let itemLeftButtonListenerKey = self.itemLeftButtonListenerKey { previousValue.removeSetLeftBarButtonItemListener(itemLeftButtonListenerKey) self.itemLeftButtonListenerKey = nil } + + if let itemLeftButtonSetEnabledListenerKey = self.itemLeftButtonSetEnabledListenerKey { + previousValue.leftBarButtonItem?.removeSetEnabledListener(itemLeftButtonSetEnabledListenerKey) + self.itemLeftButtonSetEnabledListenerKey = nil + } + + if let itemRightButtonListenerKey = self.itemRightButtonListenerKey { + previousValue.removeSetRightBarButtonItemListener(itemRightButtonListenerKey) + self.itemRightButtonListenerKey = nil + } + + if let itemRightButtonSetEnabledListenerKey = self.itemRightButtonSetEnabledListenerKey { + previousValue.rightBarButtonItem?.removeSetEnabledListener(itemRightButtonSetEnabledListenerKey) + self.itemRightButtonSetEnabledListenerKey = nil + } } self._item = value @@ -105,16 +126,34 @@ open class NavigationBar: ASDisplayNode { } } - self.itemLeftButtonListenerKey = item.addSetLeftBarButtonItemListener { [weak self] _, _ in + self.itemLeftButtonListenerKey = item.addSetLeftBarButtonItemListener { [weak self] previousItem, _, _ in if let strongSelf = self { + if let itemLeftButtonSetEnabledListenerKey = strongSelf.itemLeftButtonSetEnabledListenerKey { + previousItem?.removeSetEnabledListener(itemLeftButtonSetEnabledListenerKey) + strongSelf.itemLeftButtonSetEnabledListenerKey = nil + } + strongSelf.updateLeftButton() strongSelf.invalidateCalculatedLayout() strongSelf.setNeedsLayout() } } - self.itemRightButtonListenerKey = item.addSetRightBarButtonItemListener { [weak self] _, _ in + self.itemRightButtonListenerKey = item.addSetRightBarButtonItemListener { [weak self] previousItem, currentItem, _ in if let strongSelf = self { + if let itemRightButtonSetEnabledListenerKey = strongSelf.itemRightButtonSetEnabledListenerKey { + previousItem?.removeSetEnabledListener(itemRightButtonSetEnabledListenerKey) + strongSelf.itemRightButtonSetEnabledListenerKey = nil + } + + if let currentItem = currentItem { + strongSelf.itemRightButtonSetEnabledListenerKey = currentItem.addSetEnabledListener { _ in + if let strongSelf = self { + strongSelf.updateRightButton() + } + } + } + strongSelf.updateRightButton() strongSelf.invalidateCalculatedLayout() strongSelf.setNeedsLayout() @@ -197,6 +236,8 @@ open class NavigationBar: ASDisplayNode { self.backButtonArrow.removeFromSupernode() self.leftButtonNode.text = leftBarButtonItem.title ?? "" + self.leftButtonNode.bold = leftBarButtonItem.style == .done + self.leftButtonNode.isEnabled = leftBarButtonItem.isEnabled if self.leftButtonNode.supernode == nil { self.clippingNode.addSubnode(self.leftButtonNode) } @@ -230,6 +271,9 @@ open class NavigationBar: ASDisplayNode { if let item = self.item { if let rightBarButtonItem = item.rightBarButtonItem { self.rightButtonNode.text = rightBarButtonItem.title ?? "" + self.rightButtonNode.image = rightBarButtonItem.image + self.rightButtonNode.bold = rightBarButtonItem.style == .done + self.rightButtonNode.isEnabled = rightBarButtonItem.isEnabled self.rightButtonNode.node = rightBarButtonItem.customDisplayNode if self.rightButtonNode.supernode == nil { self.clippingNode.addSubnode(self.rightButtonNode) diff --git a/Display/NavigationButtonNode.swift b/Display/NavigationButtonNode.swift index 6cb4611c91..10247d8e0f 100644 --- a/Display/NavigationButtonNode.swift +++ b/Display/NavigationButtonNode.swift @@ -21,7 +21,35 @@ public class NavigationButtonNode: ASTextNode { set(value) { _text = value - self.attributedString = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + } + } + + private var imageNode: ASImageNode? + + private var _image: UIImage? + public var image: UIImage? { + get { + return _image + } set(value) { + _image = value + + if let _ = value { + if self.imageNode == nil { + let imageNode = ASImageNode() + imageNode.displayWithoutProcessing = true + imageNode.displaysAsynchronously = false + self.imageNode = imageNode + self.addSubnode(imageNode) + } + self.imageNode?.image = image + } else if let imageNode = self.imageNode { + imageNode.removeFromSupernode() + self.imageNode = nil + } + + self.invalidateCalculatedLayout() + self.setNeedsLayout() } } @@ -41,7 +69,7 @@ public class NavigationButtonNode: ASTextNode { public var color: UIColor = UIColor(0x007ee5) { didSet { if let text = self._text { - self.attributedString = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) } } } @@ -55,7 +83,7 @@ public class NavigationButtonNode: ASTextNode { if _bold != value { _bold = value - self.attributedString = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) + self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState()) } } } @@ -78,6 +106,11 @@ public class NavigationButtonNode: ASTextNode { if let node = self.node { let nodeSize = node.measure(constrainedSize) return CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height)) + } else if let imageNode = self.imageNode { + let nodeSize = imageNode.measure(constrainedSize) + let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height)) + imageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0), y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize) + return size } return superSize } diff --git a/Display/NavigationController.swift b/Display/NavigationController.swift index bc081974b9..3f8dd36518 100644 --- a/Display/NavigationController.swift +++ b/Display/NavigationController.swift @@ -226,9 +226,9 @@ open class NavigationController: NavigationControllerProxy, ContainableControlle self.setViewControllers(controllers, animated: animated) } - public func replaceTopController(_ controller: ViewController, animated: Bool, ready: ValuePromise?) { + public func replaceTopController(_ controller: ViewController, animated: Bool, ready: ValuePromise? = nil) { controller.containerLayoutUpdated(self.containerLayout, transition: .immediate) - self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: {[weak self] _ in + self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in if let strongSelf = self { ready?.set(true) var controllers = strongSelf.viewControllers @@ -239,6 +239,21 @@ open class NavigationController: NavigationControllerProxy, ContainableControlle })) } + public func replaceAllButRootController(_ controller: ViewController, animated: Bool, ready: ValuePromise? = nil) { + controller.containerLayoutUpdated(self.containerLayout, transition: .immediate) + self.currentPushDisposable.set((controller.ready.get() |> take(1)).start(next: { [weak self] _ in + if let strongSelf = self { + ready?.set(true) + var controllers = strongSelf.viewControllers + while controllers.count > 1 { + controllers.removeLast() + } + controllers.append(controller) + strongSelf.setViewControllers(controllers, animated: animated) + } + })) + } + open override func popViewController(animated: Bool) -> UIViewController? { var controller: UIViewController? var controllers = self.viewControllers diff --git a/Display/StatusBarManager.swift b/Display/StatusBarManager.swift index cf75310654..693435cc4d 100644 --- a/Display/StatusBarManager.swift +++ b/Display/StatusBarManager.swift @@ -66,7 +66,9 @@ class StatusBarManager { private func updateSurfaces(_ previousSurfaces: [StatusBarSurface]) { let statusBarFrame = self.host.statusBarFrame - let statusBarView = self.host.statusBarView! + guard let statusBarView = self.host.statusBarView else { + return + } var mappedSurfaces = self.surfaces.map({ optimizeMappedSurface(statusBarSize: statusBarFrame.size, surface: mappedSurface($0)) }) diff --git a/Display/SwitchNode.swift b/Display/SwitchNode.swift new file mode 100644 index 0000000000..b3515a33eb --- /dev/null +++ b/Display/SwitchNode.swift @@ -0,0 +1,10 @@ +import Foundation +import AsyncDisplayKit + +open class SwitchNode: ASDisplayNode { + override public init() { + super.init(viewBlock: { + return UISwitch() + }, didLoad: nil) + } +} diff --git a/Display/TabBarController.swift b/Display/TabBarController.swift index cd504c07c3..b24a87c480 100644 --- a/Display/TabBarController.swift +++ b/Display/TabBarController.swift @@ -31,6 +31,10 @@ open class TabBarController: ViewController { _selectedIndex = index self.updateSelectedIndex() + } else { + if let controller = self.currentController { + controller.scrollToTop?() + } } } } @@ -87,12 +91,13 @@ open class TabBarController: ViewController { currentController.navigationItem.setTarget(self.navigationItem) displayNavigationBar = currentController.displayNavigationBar - //currentController.displayNode.recursivelyEnsureDisplaySynchronously(true) + currentController.displayNode.recursivelyEnsureDisplaySynchronously(true) } else { self.navigationItem.title = nil self.navigationItem.leftBarButtonItem = nil self.navigationItem.rightBarButtonItem = nil self.navigationItem.titleView = nil + self.navigationItem.backBarButtonItem = nil displayNavigationBar = false } if self.displayNavigationBar != displayNavigationBar { diff --git a/Display/TextAlertController.swift b/Display/TextAlertController.swift new file mode 100644 index 0000000000..dc833c1c35 --- /dev/null +++ b/Display/TextAlertController.swift @@ -0,0 +1,221 @@ +import Foundation +import AsyncDisplayKit + +public enum TextAlertActionType { + case genericAction + case defaultAction +} + +public struct TextAlertAction { + public let type: TextAlertActionType + public let title: String + public let action: () -> Void + + public init(type: TextAlertActionType, title: String, action: @escaping () -> Void) { + self.type = type + self.title = title + self.action = action + } +} + +private final class TextAlertContentActionNode: HighlightableButtonNode { + private let backgroundNode: ASDisplayNode + + let action: TextAlertAction + + init(action: TextAlertAction) { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = UIColor(0xe0e5e6) + self.backgroundNode.alpha = 0.0 + + self.action = action + + super.init() + + self.setTitle(action.title, with: action.type == .defaultAction ? Font.medium(17.0) : Font.regular(17.0), with: UIColor(0x007ee5), for: []) + + self.highligthedChanged = { [weak self] value in + if let strongSelf = self { + if value { + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundNode.alpha = 1.0 + } else if !strongSelf.backgroundNode.alpha.isZero { + strongSelf.backgroundNode.alpha = 0.0 + strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + } + } + + override func didLoad() { + super.didLoad() + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc func pressed() { + self.action.action() + } + + override func layout() { + super.layout() + + self.backgroundNode.frame = self.bounds + } +} + +final class TextAlertContentNode: AlertContentNode { + private let titleNode: ASTextNode? + private let textNode: ASTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + init(title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction]) { + if let title = title { + let titleNode = ASTextNode() + titleNode.attributedText = title + titleNode.displaysAsynchronously = false + titleNode.isLayerBacked = true + titleNode.maximumNumberOfLines = 1 + titleNode.truncationMode = .byTruncatingTail + self.titleNode = titleNode + } else { + self.titleNode = nil + } + + self.textNode = ASTextNode() + self.textNode.attributedText = text + self.textNode.displaysAsynchronously = false + self.textNode.isLayerBacked = true + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + self.actionNodesSeparator.backgroundColor = UIColor(0xc9cdd7) + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + separatorNode.backgroundColor = UIColor(0xc9cdd7) + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + if let titleNode = self.titleNode { + self.addSubnode(titleNode) + } + self.addSubnode(self.textNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var titleSize: CGSize? + if let titleNode = self.titleNode { + titleSize = titleNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) + } + let textSize = self.textNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) + + let actionsHeight: CGFloat = 44.0 + + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.measure(CGSize(width: maxActionWidth, height: actionsHeight)) + minActionsWidth += actionTitleSize.width + actionTitleInsets + } + + let resultSize: CGSize + + if let titleNode = titleNode, let titleSize = titleSize { + let contentWidth = max(max(titleSize.width, textSize.width), minActionsWidth) + + let spacing: CGFloat = 6.0 + let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize) + transition.updateFrame(node: titleNode, frame: titleFrame) + + let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom) + } else { + let textFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + resultSize = CGSize(width: textSize.width + insets.left + insets.right, height: textSize.height + actionsHeight + insets.top + insets.bottom) + } + + self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + + let actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight)) + + actionOffset += currentActionWidth + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +public func textAlertController(title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction]) -> AlertController { + return AlertController(contentNode: TextAlertContentNode(title: title, text: text, actions: actions)) +} + +public func standardTextAlertController(title: String?, text: String, actions: [TextAlertAction]) -> AlertController { + var dismissImpl: (() -> Void)? + let controller = AlertController(contentNode: TextAlertContentNode(title: title != nil ? NSAttributedString(string: title!, font: Font.medium(17.0), textColor: .black, paragraphAlignment: .center) : nil, text: NSAttributedString(string: text, font: title == nil ? Font.medium(17.0) : Font.regular(13.0), textColor: .black, paragraphAlignment: .center), actions: actions.map { action in + return TextAlertAction(type: action.type, title: action.title, action: { + dismissImpl?() + action.action() + }) + })) + dismissImpl = { [weak controller] in + controller?.dismissAnimated() + } + return controller +} diff --git a/Display/TextFieldNode.swift b/Display/TextFieldNode.swift new file mode 100644 index 0000000000..8522926cc3 --- /dev/null +++ b/Display/TextFieldNode.swift @@ -0,0 +1,33 @@ +import Foundation +import AsyncDisplayKit + +public final class TextFieldNodeView: UITextField { + public var didDeleteBackwardWhileEmpty: (() -> Void)? + + override public func editingRect(forBounds bounds: CGRect) -> CGRect { + return bounds.offsetBy(dx: 0.0, dy: -UIScreenPixel) + } + + override public func placeholderRect(forBounds bounds: CGRect) -> CGRect { + return self.editingRect(forBounds: bounds) + } + + override public func deleteBackward() { + if self.text == nil || self.text!.isEmpty { + self.didDeleteBackwardWhileEmpty?() + } + super.deleteBackward() + } +} + +public class TextFieldNode: ASDisplayNode { + public var textField: TextFieldNodeView { + return self.view as! TextFieldNodeView + } + + override public init() { + super.init(viewBlock: { + return TextFieldNodeView() + }, didLoad: nil) + } +} diff --git a/Display/UIBarButtonItem+Proxy.m b/Display/UIBarButtonItem+Proxy.m index e9d486bc7a..945c12411a 100644 --- a/Display/UIBarButtonItem+Proxy.m +++ b/Display/UIBarButtonItem+Proxy.m @@ -20,7 +20,7 @@ static const void *customDisplayNodeKey = &customDisplayNodeKey; } - (instancetype)initWithCustomDisplayNode:(ASDisplayNode *)customDisplayNode { - self = [super init]; + self = [self init]; if (self != nil) { [self setAssociatedObject:customDisplayNode forKey:customDisplayNodeKey]; } diff --git a/Display/UINavigationItem+Proxy.h b/Display/UINavigationItem+Proxy.h index 8f3b22d0ab..a32d4b0199 100644 --- a/Display/UINavigationItem+Proxy.h +++ b/Display/UINavigationItem+Proxy.h @@ -2,7 +2,7 @@ typedef void (^UINavigationItemSetTitleListener)(NSString *); typedef void (^UINavigationItemSetTitleViewListener)(UIView *); -typedef void (^UINavigationItemSetBarButtonItemListener)(UIBarButtonItem *, BOOL); +typedef void (^UINavigationItemSetBarButtonItemListener)(UIBarButtonItem *, UIBarButtonItem *, BOOL); typedef void (^UITabBarItemSetBadgeListener)(NSString *); @interface UINavigationItem (Proxy) diff --git a/Display/UINavigationItem+Proxy.m b/Display/UINavigationItem+Proxy.m index b77620575f..daa72e99c5 100644 --- a/Display/UINavigationItem+Proxy.m +++ b/Display/UINavigationItem+Proxy.m @@ -62,6 +62,8 @@ static const void *setBadgeListenerBagKey = &setBadgeListenerBagKey; - (void)_ac91f40f_setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem animated:(BOOL)animated { + UIBarButtonItem *previousItem = self.leftBarButtonItem; + [self _ac91f40f_setLeftBarButtonItem:leftBarButtonItem animated:animated]; UINavigationItem *targetItem = [self associatedObjectForKey:targetItemKey]; @@ -69,7 +71,7 @@ static const void *setBadgeListenerBagKey = &setBadgeListenerBagKey; [targetItem setLeftBarButtonItem:leftBarButtonItem animated:animated]; } else { [(NSBag *)[self associatedObjectForKey:setLeftBarButtonItemListenerBagKey] enumerateItems:^(UINavigationItemSetBarButtonItemListener listener) { - listener(leftBarButtonItem, animated); + listener(previousItem, leftBarButtonItem, animated); }]; } } @@ -80,6 +82,8 @@ static const void *setBadgeListenerBagKey = &setBadgeListenerBagKey; - (void)_ac91f40f_setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem animated:(BOOL)animated { + UIBarButtonItem *previousItem = self.rightBarButtonItem; + [self _ac91f40f_setRightBarButtonItem:rightBarButtonItem animated:animated]; UINavigationItem *targetItem = [self associatedObjectForKey:targetItemKey]; @@ -87,7 +91,7 @@ static const void *setBadgeListenerBagKey = &setBadgeListenerBagKey; [targetItem setRightBarButtonItem:rightBarButtonItem animated:animated]; } else { [(NSBag *)[self associatedObjectForKey:setRightBarButtonItemListenerBagKey] enumerateItems:^(UINavigationItemSetBarButtonItemListener listener) { - listener(rightBarButtonItem, animated); + listener(previousItem, rightBarButtonItem, animated); }]; } } diff --git a/Display/UIViewController+Navigation.h b/Display/UIViewController+Navigation.h index 12c7dda690..1cbc548127 100644 --- a/Display/UIViewController+Navigation.h +++ b/Display/UIViewController+Navigation.h @@ -6,7 +6,7 @@ - (BOOL)ignoreAppearanceMethodInvocations; - (void)navigation_setNavigationController:(UINavigationController * _Nullable)navigationControlller; - (void)navigation_setPresentingViewController:(UIViewController * _Nullable)presentingViewController; -- (void)navigation_setDismiss:(void (^_Nullable)())dismiss rootController:(UIViewController *)rootController; +- (void)navigation_setDismiss:(void (^_Nullable)())dismiss rootController:( UIViewController * _Nullable )rootController; @end diff --git a/Display/ViewController.swift b/Display/ViewController.swift index f5f7b51c25..497e2141bf 100644 --- a/Display/ViewController.swift +++ b/Display/ViewController.swift @@ -16,6 +16,19 @@ private func findCurrentResponder(_ view: UIView) -> UIResponder? { } } +public enum ViewControllerPresentationAnimation { + case none + case modalSheet +} + +open class ViewControllerPresentationArguments { + public let presentationAnimation: ViewControllerPresentationAnimation + + public init(presentationAnimation: ViewControllerPresentationAnimation) { + self.presentationAnimation = presentationAnimation + } +} + @objc open class ViewController: UIViewController, ContainableController { private var containerLayout = ContainerViewLayout() private let presentationContext: PresentationContext