diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 8415a442c..ad158c345 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -429,6 +429,7 @@ 4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */; }; 4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; }; 4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; }; + 4C23A5F2215C4ADE00534937 /* SheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C23A5F1215C4ADE00534937 /* SheetViewController.swift */; }; 4C2F454F214C00E1004871FF /* AvatarTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */; }; 4C3EF7FD2107DDEE0007EBF7 /* ParamParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */; }; 4C3EF802210918740007EBF7 /* SSKProtoEnvelopeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */; }; @@ -1115,6 +1116,7 @@ 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatusView.swift; sourceTree = ""; }; 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = ""; }; 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearchViewController.swift; sourceTree = ""; }; + 4C23A5F1215C4ADE00534937 /* SheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetViewController.swift; sourceTree = ""; }; 4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarTableViewCell.swift; sourceTree = ""; }; 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParamParserTest.swift; sourceTree = ""; }; 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKProtoEnvelopeTest.swift; sourceTree = ""; }; @@ -1972,6 +1974,7 @@ 34AC09DC211B39B100997B47 /* SharingThreadPickerViewController.m */, 34AC09BF211B39AE00997B47 /* ViewControllerUtils.h */, 34AC09D1211B39B000997B47 /* ViewControllerUtils.m */, + 4C23A5F1215C4ADE00534937 /* SheetViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -3219,6 +3222,7 @@ 346129C91FD2072E00532771 /* NSString+OWS.m in Sources */, 347850691FD9B78A007B8332 /* AppSetup.m in Sources */, 346941A3215D2EE400B5BFAD /* Theme.m in Sources */, + 4C23A5F2215C4ADE00534937 /* SheetViewController.swift in Sources */, 34AC0A14211B39EA00997B47 /* ContactCellView.m in Sources */, 34AC0A15211B39EA00997B47 /* ContactsViewHelper.m in Sources */, 346129FF1FD5F31400532771 /* OWS103EnableVideoCalling.m in Sources */, diff --git a/SignalMessaging/ViewControllers/SheetViewController.swift b/SignalMessaging/ViewControllers/SheetViewController.swift new file mode 100644 index 000000000..78d16bba8 --- /dev/null +++ b/SignalMessaging/ViewControllers/SheetViewController.swift @@ -0,0 +1,213 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc(OWSSheetViewControllerDelegate) +public protocol SheetViewControllerDelegate: class { + func sheetViewControllerRequestedDismiss(_ sheetViewController: SheetViewController) +} + +@objc(OWSSheetViewController) +public class SheetViewController: UIViewController { + + @objc + weak var delegate: SheetViewControllerDelegate? + + @objc + public let contentView: UIView = UIView() + + private let sheetView: SheetView = SheetView() + + deinit { + Logger.verbose("") + } + + @objc + public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + self.transitioningDelegate = self + self.modalPresentationStyle = .overCurrentContext + } + + required init?(coder aDecoder: NSCoder) { + notImplemented() + } + + // MARK: View LifeCycle + + var sheetViewVerticalConstraint: NSLayoutConstraint? + + override public func loadView() { + self.view = UIView() + + sheetView.preservesSuperviewLayoutMargins = true + + sheetView.addSubview(contentView) + contentView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom) + contentView.autoPinEdge(toSuperviewMargin: .bottom) + + view.addSubview(sheetView) + sheetView.autoPinWidthToSuperview() + sheetView.setContentHuggingVerticalHigh() + sheetView.setCompressionResistanceHigh() + self.sheetViewVerticalConstraint = sheetView.autoPinEdge(.top, to: .bottom, of: self.view) + + handleView.backgroundColor = Theme.backgroundColor + let kHandleViewHeight: CGFloat = 5 + handleView.autoSetDimensions(to: CGSize(width: 40, height: kHandleViewHeight)) + handleView.layer.cornerRadius = kHandleViewHeight / 2 + view.addSubview(handleView) + handleView.autoAlignAxis(.vertical, toSameAxisOf: sheetView) + handleView.autoPinEdge(.bottom, to: .top, of: sheetView, withOffset: -6) + + // Gestures + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapBackground)) + self.view.addGestureRecognizer(tapGesture) + } + + // MARK: Present / Dismiss animations + + fileprivate func animatePresentation(completion: @escaping (Bool) -> Void) { + guard let sheetViewVerticalConstraint = self.sheetViewVerticalConstraint else { + owsFailDebug("sheetViewVerticalConstraint was unexpectedly nil") + return + } + + let backgroundDuration: TimeInterval = 0.1 + UIView.animate(withDuration: backgroundDuration) { + let alpha: CGFloat = Theme.isDarkThemeEnabled ? 0.7 : 0.6 + self.view.backgroundColor = UIColor.black.withAlphaComponent(alpha) + } + + self.sheetView.superview?.layoutIfNeeded() + + NSLayoutConstraint.deactivate([sheetViewVerticalConstraint]) + self.sheetViewVerticalConstraint = self.sheetView.autoPinEdge(toSuperviewEdge: .bottom) + UIView.animate(withDuration: 0.2, + delay: backgroundDuration, + options: .curveEaseOut, + animations: { + self.sheetView.superview?.layoutIfNeeded() + }, + completion: completion) + } + + fileprivate func animateDismiss(completion: @escaping (Bool) -> Void) { + guard let sheetViewVerticalConstraint = self.sheetViewVerticalConstraint else { + owsFailDebug("sheetVerticalConstraint was unexpectedly nil") + // self.delegate?.sheetViewDidHide(self) + return + } + + self.sheetView.superview?.layoutIfNeeded() + NSLayoutConstraint.deactivate([sheetViewVerticalConstraint]) + + let dismissDuration: TimeInterval = 0.2 + self.sheetViewVerticalConstraint = self.sheetView.autoPinEdge(.top, to: .bottom, of: self.view) + UIView.animate(withDuration: dismissDuration, + delay: 0, + options: .curveEaseOut, + animations: { + self.view.backgroundColor = UIColor.clear + self.sheetView.superview?.layoutIfNeeded() + }, + completion: completion) + } + + // MARK: Actions + + @objc + func didTapBackground() { + // inform delegate to + delegate?.sheetViewControllerRequestedDismiss(self) + } +} + +extension SheetViewController: UIViewControllerTransitioningDelegate { + public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SheetViewPresentationController(sheetViewController: self) + } + + public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SheetViewDismissalController(sheetViewController: self) + } +} + +private class SheetViewPresentationController: NSObject, UIViewControllerAnimatedTransitioning { + + let sheetViewController: SheetViewController + init(sheetViewController: SheetViewController) { + self.sheetViewController = sheetViewController + } + + // This is used for percent driven interactive transitions, as well as for + // container controllers that have companion animations that might need to + // synchronize with the main animation. + public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.3 + } + + // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. + public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + Logger.debug("") + transitionContext.containerView.addSubview(sheetViewController.view) + sheetViewController.view.autoPinEdgesToSuperviewEdges() + sheetViewController.animatePresentation { didComplete in + Logger.debug("completed: \(didComplete)") + transitionContext.completeTransition(didComplete) + } + } +} + +private class SheetViewDismissalController: NSObject, UIViewControllerAnimatedTransitioning { + + let sheetViewController: SheetViewController + init(sheetViewController: SheetViewController) { + self.sheetViewController = sheetViewController + } + + // This is used for percent driven interactive transitions, as well as for + // container controllers that have companion animations that might need to + // synchronize with the main animation. + public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.3 + } + + // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. + public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + Logger.debug("") + sheetViewController.animateDismiss { didComplete in + Logger.debug("completed: \(didComplete)") + transitionContext.completeTransition(didComplete) + } + } +} + +private class SheetView: UIView { + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = Theme.isDarkThemeEnabled ? UIColor.ows_gray90 + : UIColor.ows_gray05 + } + + required init?(coder aDecoder: NSCoder) { + notImplemented() + } + + override var bounds: CGRect { + didSet { + updateMask() + } + } + + private func updateMask() { + let cornerRadius: CGFloat = 16 + let path: UIBezierPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) + let mask = CAShapeLayer() + mask.path = path.cgPath + self.layer.mask = mask + } +}