Generic SheetViewController

This commit is contained in:
Michael Kirk 2018-09-26 16:41:36 -06:00
parent 6b9133e5e6
commit 95a6df6496
2 changed files with 217 additions and 0 deletions

View File

@ -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 = "<group>"; };
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = "<group>"; };
4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearchViewController.swift; sourceTree = "<group>"; };
4C23A5F1215C4ADE00534937 /* SheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetViewController.swift; sourceTree = "<group>"; };
4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarTableViewCell.swift; sourceTree = "<group>"; };
4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParamParserTest.swift; sourceTree = "<group>"; };
4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKProtoEnvelopeTest.swift; sourceTree = "<group>"; };
@ -1972,6 +1974,7 @@
34AC09DC211B39B100997B47 /* SharingThreadPickerViewController.m */,
34AC09BF211B39AE00997B47 /* ViewControllerUtils.h */,
34AC09D1211B39B000997B47 /* ViewControllerUtils.m */,
4C23A5F1215C4ADE00534937 /* SheetViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
@ -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 */,

View File

@ -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
}
}