Commit 77c252e5 by Orkhan Alikhanov

Added Dialogs

parent 88afd83a
......@@ -174,6 +174,9 @@
96E3C39A1D3A1CC20086A024 /* ErrorTextField.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96E3C39C1D3A1CC20086A024 /* Offset.swift in Headers */ = {isa = PBXBuildFile; fileRef = 968C99461D377849000074FF /* Offset.swift */; settings = {ATTRIBUTES = (Public, ); }; };
96F1A5531F24F17A001D8CAF /* TabsController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E09DC71F2287E50000B121 /* TabsController.swift */; settings = {ATTRIBUTES = (Public, ); }; };
9D13671A2006A8170004DE2D /* DialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D1367192006A8170004DE2D /* DialogView.swift */; };
9D13671C2006A8D80004DE2D /* DialogController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D13671B2006A8D80004DE2D /* DialogController.swift */; };
9D13671E2006A9450004DE2D /* DialogBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D13671D2006A9450004DE2D /* DialogBuilder.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
......@@ -285,6 +288,9 @@
96E09DC71F2287E50000B121 /* TabsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsController.swift; sourceTree = "<group>"; };
96E3C3931D397AE90086A024 /* Material+UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIView.swift"; sourceTree = "<group>"; };
96F1DC871D654FDF0025F925 /* Material+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CALayer.swift"; sourceTree = "<group>"; };
9D1367192006A8170004DE2D /* DialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogView.swift; sourceTree = "<group>"; };
9D13671B2006A8D80004DE2D /* DialogController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogController.swift; sourceTree = "<group>"; };
9D13671D2006A9450004DE2D /* DialogBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogBuilder.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
......@@ -523,6 +529,7 @@
96BCB8001CB40F0300C806FE /* Color */,
96328B9A1E05C135009A4C90 /* Data */,
96BCB80B1CB410CC00C806FE /* Device */,
9D1367172006A5730004DE2D /* Dialogs */,
96230AB61D6A51FD00AF47DC /* Divider */,
96BCB80A1CB410A100C806FE /* Extension */,
963FBF021D6696D0008F8512 /* FABMenu */,
......@@ -729,6 +736,16 @@
name = Animation;
sourceTree = "<group>";
};
9D1367172006A5730004DE2D /* Dialogs */ = {
isa = PBXGroup;
children = (
9D1367192006A8170004DE2D /* DialogView.swift */,
9D13671B2006A8D80004DE2D /* DialogController.swift */,
9D13671D2006A9450004DE2D /* DialogBuilder.swift */,
);
name = Dialogs;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
......@@ -936,12 +953,14 @@
965E81171DD4D5C800D61E4B /* TransitionController.swift in Sources */,
965E81181DD4D5C800D61E4B /* Snackbar.swift in Sources */,
965E81191DD4D5C800D61E4B /* SnackbarController.swift in Sources */,
9D13671C2006A8D80004DE2D /* DialogController.swift in Sources */,
9618006D1F4D384200CD77A1 /* Material+UIViewController.swift in Sources */,
965E811A1DD4D5C800D61E4B /* StatusBarController.swift in Sources */,
965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */,
965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */,
965E811D1DD4D5C800D61E4B /* TableViewCell.swift in Sources */,
965E811E1DD4D5C800D61E4B /* TextField.swift in Sources */,
9D13671E2006A9450004DE2D /* DialogBuilder.swift in Sources */,
965E811F1DD4D5C800D61E4B /* ErrorTextField.swift in Sources */,
965E81211DD4D5C800D61E4B /* TextStorage.swift in Sources */,
965E81221DD4D5C800D61E4B /* TextView.swift in Sources */,
......@@ -982,6 +1001,7 @@
961E6BDF1DDA2A95004E6C93 /* Application.swift in Sources */,
965E80D71DD4C50600D61E4B /* Icon.swift in Sources */,
965E80FC1DD4D59500D61E4B /* SearchBarController.swift in Sources */,
9D13671A2006A8170004DE2D /* DialogView.swift in Sources */,
965E80D81DD4C50600D61E4B /* Layer.swift in Sources */,
965E80D91DD4C50600D61E4B /* Layout.swift in Sources */,
965E80DA1DD4C50600D61E4B /* Border.swift in Sources */,
......
//
// DialogBuilder.swift
// Material
//
// Created by Orkhan Alikhanov on 1/11/18.
// Copyright © 2018 CosmicMind, Inc. All rights reserved.
//
import UIKit
public typealias Dialog = DialogBuilder<DialogView>
open class DialogBuilder<T: DialogView> {
public init() {}
open let controller = DialogController<T>()
open func title(_ text: String?) -> DialogBuilder {
dialogView.titleLabel.text = text
return self
}
open func details(_ text: String?) -> DialogBuilder {
dialogView.detailsLabel.text = text
return self
}
open func isCancelable(_ value: Bool, handler: (() -> Void)? = nil) -> DialogBuilder {
controller.isCancelable = value
controller.canceledHandler = handler
return self
}
open func shouldDismiss(handler: ((Button?) -> Bool)?) -> DialogBuilder {
controller.shouldDismissHandler = handler
return self
}
open func positiveButton(_ title: String?, handler: (() -> Void)?) -> DialogBuilder {
dialogView.positiveButton.title = title
controller.positiveHandler = handler
return self
}
open func negativeButton(_ title: String?, handler: (() -> Void)?) -> DialogBuilder {
dialogView.negativeButton.title = title
controller.negativeHandler = handler
return self
}
open func neutralButton(_ title: String?, handler: (() -> Void)?) -> DialogBuilder {
dialogView.neutralButton.title = title
controller.neutralHandler = handler
return self
}
@discardableResult
open func show(_ vc: UIViewController) -> DialogBuilder {
vc.present(controller, animated: true, completion: nil)
return self
}
}
extension DialogBuilder {
private var dialogView: T {
return controller.dialogView
}
}
//
// DialogController.swift
// Material
//
// Created by Orkhan Alikhanov on 1/11/18.
// Copyright © 2018 CosmicMind, Inc. All rights reserved.
//
import UIKit
open class DialogController<T: DialogView>: UIViewController {
open let dialogView = T()
open var isCancelable = false
open func prepare() {
isMotionEnabled = true
motionTransitionType = .fade
modalPresentationStyle = .overFullScreen
}
open override func viewDidLoad() {
super.viewDidLoad()
view = UIControl()
view.backgroundColor = UIColor.black.withAlphaComponent(0.33)
view.layout(dialogView)
.center()
(view as? UIControl)?.addTarget(self, action: #selector(didTapView), for: .touchUpInside)
dialogView.buttonArea.subviews.forEach {
($0 as? Button)?.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside)
}
}
open override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
dialogView.maxSize = CGSize(width: Screen.width * 0.8, height: Screen.height * 0.9)
}
open var canceledHandler: (() -> Void)?
open var shouldDismissHandler: ((Button?) -> Bool)?
open var positiveHandler: (() -> Void)?
open var negativeHandler: (() -> Void)?
open var neutralHandler: (() -> Void)?
@objc
private func didTapView() {
guard isCancelable else { return }
dismiss(nil)
canceledHandler?()
}
@objc
private func didTapButton(_ sender: Button) {
switch sender {
case dialogView.positiveButton:
positiveHandler?()
case dialogView.negativeButton:
negativeHandler?()
case dialogView.neutralButton:
neutralHandler?()
default:
break
}
dismiss(sender)
}
open func dismiss(_ button: Button?) {
if shouldDismissHandler?(button) ?? true {
presentingViewController?.dismiss(animated: true, completion: nil)
}
}
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
prepare()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
prepare()
}
}
//
// DialogView.swift
// Material
//
// Created by Orkhan Alikhanov on 1/10/18.
// Copyright © 2018 CosmicMind, Inc. All rights reserved.
//
import UIKit
open class DialogView: UIView {
open let titleArea = UIView()
open let titleLabel = UILabel()
open let detailsLabel = UILabel()
open private(set) lazy var scrollView: UIScrollView = {
class DialogScrollView: UIScrollView {
weak var dialogView: DialogView?
override func layoutSubviews() {
super.layoutSubviews()
dialogView?.layoutDividers()
}
}
let scrollView = DialogScrollView()
scrollView.dialogView = self
return scrollView
}()
open let contentView = UIView()
open let buttonArea = UIView()
open let neutralButton = Button()
open let positiveButton = Button()
open let negativeButton = Button()
/// Maximum size of the dialog
open var maxSize = CGSize(width: 200, height: 300) {
didSet {
guard oldValue != maxSize else { return }
invalidateIntrinsicContentSize()
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
prepare()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
prepare()
}
open func prepare() {
backgroundColor = Color.grey.lighten5
depthPreset = .depth5
prepareTitleArea()
prepareTitleLabel()
prepareScrollView()
prepareContentView()
prepareDetailsLabel()
prepareButtonArea()
prepareButtons()
}
open override var intrinsicContentSize: CGSize {
return sizeThatFits(maxSize)
}
open override func sizeThatFits(_ size: CGSize) -> CGSize {
var w: CGFloat = 0
func setW(_ newW: CGFloat) {
w = max(w, newW)
w = min(w, size.width)
}
setW(titleAreaSizeThatFits(width: size.width).width)
setW(buttonAreaSizeThatFits(width: size.width).width)
setW(contentViewSizeThatFits(width: size.width).width)
var h: CGFloat = 0
h += titleAreaSizeThatFits(width: w).height
h += buttonAreaSizeThatFits(width: w).height
h += contentViewSizeThatFits(width: w).height
return CGSize(width: w, height: min(h, size.height))
}
open override func layoutSubviews() {
super.layoutSubviews()
layoutTitleArea()
layoutButtonArea()
layoutContentView()
layoutScrollView()
layoutDividers()
// Position button area
buttonArea.frame.origin.y = scrollView.frame.maxY
}
/// Override this if you are using custom view in title area
open func titleAreaSizeThatFits(width: CGFloat) -> CGSize {
guard !titleLabel.isEmpty else { return .zero }
var size = titleLabel.sizeThatFits(CGSize(width: width - 24 - 24, height: .greatestFiniteMagnitude))
size.width += 24 + 24
size.height += 24 + 20
return size
}
open func buttonAreaSizeThatFits(width: CGFloat) -> CGSize {
guard !nonHiddenButtons.isEmpty else { return .zero }
let isStacked = requiredButtonAreaWidth > width
let w = min(width, isStacked ? requiredButtonAreaWidthForStacked : requiredButtonAreaWidth)
let h = isStacked ? CGFloat(8 + nonHiddenButtons.count * 48) : 52
return CGSize(width: w, height: h)
}
open func contentViewSizeThatFits(width: CGFloat) -> CGSize {
guard !detailsLabel.isEmpty else { return .zero }
var size = detailsLabel.sizeThatFits(CGSize(width: width - 24 - 24, height: .greatestFiniteMagnitude))
size.width += 24 + 24
let additional: CGFloat = titleLabel.isEmpty ? 20 : 0 // if no title area, will be pushed 20 points below
size.height += 24 + 0 + additional
return size
}
}
private extension DialogView {
func layoutTitleArea() {
let size = CGSize(width: frame.width, height: titleAreaSizeThatFits(width: frame.width).height)
titleArea.frame.size = size
guard !titleLabel.isEmpty else { return }
titleLabel.frame = CGRect(x: 24, y: 24, width: size.width - 24 - 24, height: size.height - 24 - 20)
}
func layoutButtonArea() {
let width = frame.width
buttonArea.frame.size.width = width
buttonArea.frame.size.height = buttonAreaSizeThatFits(width: width).height
let buttons = nonHiddenButtons
guard !buttons.isEmpty else { return }
let isStacked = requiredButtonAreaWidth > width
if isStacked {
buttons.forEach {
let w = min($0.optimalWidth, width - 8 - 8)
$0.frame.size = CGSize(width: w, height: 36)
$0.frame.origin.x = width - 8 - w
}
positiveButton.frame.origin.y = 6
let belowPositive = positiveButton.isHidden ? 6 : positiveButton.frame.maxY + 6 + 6
negativeButton.frame.origin.y = belowPositive
neutralButton.frame.origin.y = negativeButton.isHidden ? belowPositive : (negativeButton.frame.maxY + 6 + 6)
} else {
buttons.forEach {
$0.frame.size = CGSize(width: $0.optimalWidth, height: 36)
$0.frame.origin.y = 8
}
neutralButton.frame.origin.x = 8
positiveButton.frame.origin.x = width - 8 - positiveButton.frame.width
negativeButton.frame.origin.x = (positiveButton.isHidden ? width : positiveButton.frame.minX) - 8 - negativeButton.frame.width
}
}
func layoutContentView() {
let size = CGSize(width: frame.width, height: contentViewSizeThatFits(width: frame.width).height)
contentView.frame.size = size
guard !detailsLabel.isEmpty else { return }
let additional: CGFloat = titleArea.frame.height == 0 ? 20 : 0 // if no title area, push 20 points below
detailsLabel.frame = CGRect(x: 24, y: additional, width: size.width - 24 - 24, height: size.height - 24)
}
func layoutScrollView() {
let h = titleArea.frame.height + buttonArea.frame.height
let allowed = min(maxSize.height - h, contentView.frame.height)
scrollView.frame.size = CGSize(width: frame.width, height: max(allowed, 0))
scrollView.frame.origin.y = titleArea.frame.maxY
scrollView.contentSize = contentView.frame.size
}
/// Lays out dividers
///
/// This method is also called (by scrollView) when scrolling happens
func layoutDividers() {
let isScrollable = contentView.frame.height > scrollView.frame.height
titleArea.isDividerHidden = titleLabel.isEmpty || !isScrollable || scrollView.isAtTop
buttonArea.isDividerHidden = nonHiddenButtons.isEmpty || !isScrollable || scrollView.isAtBottom
titleArea.layoutDivider()
buttonArea.layoutDivider()
}
}
private extension Button {
var optimalWidth: CGFloat {
return max(64, sizeThatFits(CGSize(width: .max, height: 36)).width)
}
}
private extension UILabel {
var isEmpty: Bool {
let empty = text?.isEmpty ?? true
isHidden = empty
return empty
}
}
private extension DialogView {
var requiredButtonAreaWidth: CGFloat {
let buttons = nonHiddenButtons
guard !buttons.isEmpty else { return 0 }
let buttonsWidth: CGFloat = buttons.reduce(0) { $0 + $1.optimalWidth }
let additional: CGFloat = neutralButton.isHidden ? 0 : 8 // additional spacing for neutral button
return buttonsWidth + CGFloat(buttons.count * 8) + additional
}
var requiredButtonAreaWidthForStacked: CGFloat {
return 8 + 8 + nonHiddenButtons.reduce(0) {
max($0, $1.optimalWidth)
}
}
var nonHiddenButtons: [Button] {
positiveButton.isHidden = positiveButton.title(for: .normal)?.isEmpty ?? true
negativeButton.isHidden = negativeButton.title(for: .normal)?.isEmpty ?? true
neutralButton.isHidden = neutralButton.title(for: .normal)?.isEmpty ?? true
return [positiveButton, negativeButton, neutralButton].filter { !$0.isHidden }
}
}
private extension DialogView {
func prepareTitleArea() {
addSubview(titleArea)
titleArea.dividerColor = Color.darkText.dividers
titleArea.dividerThickness = 1
titleArea.dividerAlignment = .bottom
}
func prepareTitleLabel() {
titleArea.addSubview(titleLabel)
titleLabel.font = RobotoFont.bold(with: 19)
titleLabel.textColor = Color.darkText.primary
titleLabel.numberOfLines = 0
}
func prepareButtonArea() {
addSubview(buttonArea)
buttonArea.dividerColor = Color.darkText.dividers
buttonArea.dividerThickness = 1
buttonArea.dividerAlignment = .top
}
func prepareButtons() {
[positiveButton, negativeButton, neutralButton].forEach {
buttonArea.addSubview($0)
$0.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
}
}
func prepareScrollView() {
addSubview(scrollView)
}
func prepareContentView() {
scrollView.addSubview(contentView)
}
func prepareDetailsLabel() {
contentView.addSubview(detailsLabel)
detailsLabel.numberOfLines = 0
detailsLabel.textColor = Color.darkText.secondary
}
}
private extension UIScrollView {
var isAtTop: Bool {
return contentOffset.y <= 0
}
var isAtBottom: Bool {
return contentOffset.y >= (contentSize.height - frame.height - 1)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment