From 77c252e579e954da3d0ecbd52e53da8839cc2eba Mon Sep 17 00:00:00 2001 From: Orkhan Alikhanov Date: Sat, 13 Jan 2018 03:07:31 +0400 Subject: [PATCH 1/8] Added Dialogs --- Material.xcodeproj/project.pbxproj | 20 ++ Sources/iOS/DialogBuilder.swift | 68 +++++++ Sources/iOS/DialogController.swift | 83 ++++++++ Sources/iOS/DialogView.swift | 296 +++++++++++++++++++++++++++++ 4 files changed, 467 insertions(+) create mode 100644 Sources/iOS/DialogBuilder.swift create mode 100644 Sources/iOS/DialogController.swift create mode 100644 Sources/iOS/DialogView.swift diff --git a/Material.xcodeproj/project.pbxproj b/Material.xcodeproj/project.pbxproj index bd9bf254e..d2d5cd1f0 100644 --- a/Material.xcodeproj/project.pbxproj +++ b/Material.xcodeproj/project.pbxproj @@ -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 = ""; }; 96E3C3931D397AE90086A024 /* Material+UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIView.swift"; sourceTree = ""; }; 96F1DC871D654FDF0025F925 /* Material+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CALayer.swift"; sourceTree = ""; }; + 9D1367192006A8170004DE2D /* DialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogView.swift; sourceTree = ""; }; + 9D13671B2006A8D80004DE2D /* DialogController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogController.swift; sourceTree = ""; }; + 9D13671D2006A9450004DE2D /* DialogBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogBuilder.swift; sourceTree = ""; }; /* 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 = ""; }; + 9D1367172006A5730004DE2D /* Dialogs */ = { + isa = PBXGroup; + children = ( + 9D1367192006A8170004DE2D /* DialogView.swift */, + 9D13671B2006A8D80004DE2D /* DialogController.swift */, + 9D13671D2006A9450004DE2D /* DialogBuilder.swift */, + ); + name = Dialogs; + sourceTree = ""; + }; /* 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 */, diff --git a/Sources/iOS/DialogBuilder.swift b/Sources/iOS/DialogBuilder.swift new file mode 100644 index 000000000..39b95327a --- /dev/null +++ b/Sources/iOS/DialogBuilder.swift @@ -0,0 +1,68 @@ +// +// DialogBuilder.swift +// Material +// +// Created by Orkhan Alikhanov on 1/11/18. +// Copyright © 2018 CosmicMind, Inc. All rights reserved. +// + +import UIKit + +public typealias Dialog = DialogBuilder +open class DialogBuilder { + + public init() {} + open let controller = DialogController() + + 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 + } +} diff --git a/Sources/iOS/DialogController.swift b/Sources/iOS/DialogController.swift new file mode 100644 index 000000000..9de9a0827 --- /dev/null +++ b/Sources/iOS/DialogController.swift @@ -0,0 +1,83 @@ +// +// DialogController.swift +// Material +// +// Created by Orkhan Alikhanov on 1/11/18. +// Copyright © 2018 CosmicMind, Inc. All rights reserved. +// + +import UIKit + +open class DialogController: 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() + } +} diff --git a/Sources/iOS/DialogView.swift b/Sources/iOS/DialogView.swift new file mode 100644 index 000000000..7be9ee724 --- /dev/null +++ b/Sources/iOS/DialogView.swift @@ -0,0 +1,296 @@ +// +// 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) + } +} From a404c51dee82926329c112d8c6a67a7507051130 Mon Sep 17 00:00:00 2001 From: Orkhan Alikhanov Date: Sat, 13 Jan 2018 18:13:30 +0400 Subject: [PATCH 2/8] Fixed minor dialogView bugs --- Sources/iOS/DialogView.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/iOS/DialogView.swift b/Sources/iOS/DialogView.swift index 7be9ee724..98909e128 100644 --- a/Sources/iOS/DialogView.swift +++ b/Sources/iOS/DialogView.swift @@ -179,13 +179,12 @@ private extension DialogView { func layoutScrollView() { let h = titleArea.frame.height + buttonArea.frame.height - let allowed = min(maxSize.height - h, contentView.frame.height) + let allowed = min(frame.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 @@ -194,8 +193,8 @@ private extension DialogView { 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.isDividerHidden = titleArea.frame.height == 0 || !isScrollable || scrollView.isAtTop + buttonArea.isDividerHidden = buttonArea.frame.height == 0 || !isScrollable || scrollView.isAtBottom titleArea.layoutDivider() buttonArea.layoutDivider() From f58b6471f691921f64e63412ac85ba685c4a4298 Mon Sep 17 00:00:00 2001 From: Orkhan Alikhanov Date: Sat, 13 Jan 2018 22:05:15 +0400 Subject: [PATCH 3/8] Adjusted Dialog fonts --- Sources/iOS/DialogView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/iOS/DialogView.swift b/Sources/iOS/DialogView.swift index 98909e128..ad4c73faf 100644 --- a/Sources/iOS/DialogView.swift +++ b/Sources/iOS/DialogView.swift @@ -250,7 +250,7 @@ private extension DialogView { func prepareTitleLabel() { titleArea.addSubview(titleLabel) - titleLabel.font = RobotoFont.bold(with: 19) + titleLabel.font = RobotoFont.bold(with: 20) titleLabel.textColor = Color.darkText.primary titleLabel.numberOfLines = 0 } @@ -265,6 +265,7 @@ private extension DialogView { func prepareButtons() { [positiveButton, negativeButton, neutralButton].forEach { buttonArea.addSubview($0) + $0.titleLabel?.font = RobotoFont.medium(with: 14) $0.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) } } From 85c13ac240f2a5aa431d65a88de902fe3a3db7d4 Mon Sep 17 00:00:00 2001 From: Orkhan Alikhanov Date: Sat, 13 Jan 2018 22:05:44 +0400 Subject: [PATCH 4/8] Removed ..Button prefix from DialogBuilder --- Sources/iOS/DialogBuilder.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/iOS/DialogBuilder.swift b/Sources/iOS/DialogBuilder.swift index 39b95327a..f6e3a5454 100644 --- a/Sources/iOS/DialogBuilder.swift +++ b/Sources/iOS/DialogBuilder.swift @@ -35,25 +35,24 @@ open class DialogBuilder { return self } - open func positiveButton(_ title: String?, handler: (() -> Void)?) -> DialogBuilder { + open func positive(_ title: String?, handler: (() -> Void)?) -> DialogBuilder { dialogView.positiveButton.title = title controller.positiveHandler = handler return self } - open func negativeButton(_ title: String?, handler: (() -> Void)?) -> DialogBuilder { + open func negative(_ title: String?, handler: (() -> Void)?) -> DialogBuilder { dialogView.negativeButton.title = title controller.negativeHandler = handler return self } - open func neutralButton(_ title: String?, handler: (() -> Void)?) -> DialogBuilder { + open func neutral(_ 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) From 62a7d0e021d318fa12fa56ffd52131af593a29e4 Mon Sep 17 00:00:00 2001 From: Orkhan Alikhanov Date: Sun, 14 Jan 2018 00:55:28 +0400 Subject: [PATCH 5/8] Renamed DialogController event handlers --- Sources/iOS/DialogBuilder.swift | 8 ++++---- Sources/iOS/DialogController.swift | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/iOS/DialogBuilder.swift b/Sources/iOS/DialogBuilder.swift index f6e3a5454..4869ae7b6 100644 --- a/Sources/iOS/DialogBuilder.swift +++ b/Sources/iOS/DialogBuilder.swift @@ -26,7 +26,7 @@ open class DialogBuilder { open func isCancelable(_ value: Bool, handler: (() -> Void)? = nil) -> DialogBuilder { controller.isCancelable = value - controller.canceledHandler = handler + controller.didCancelHandler = handler return self } @@ -37,19 +37,19 @@ open class DialogBuilder { open func positive(_ title: String?, handler: (() -> Void)?) -> DialogBuilder { dialogView.positiveButton.title = title - controller.positiveHandler = handler + controller.didTapPositiveButtonHandler = handler return self } open func negative(_ title: String?, handler: (() -> Void)?) -> DialogBuilder { dialogView.negativeButton.title = title - controller.negativeHandler = handler + controller.didTapNegativeButtonHandler = handler return self } open func neutral(_ title: String?, handler: (() -> Void)?) -> DialogBuilder { dialogView.neutralButton.title = title - controller.neutralHandler = handler + controller.didTapNeutralButtonHandler = handler return self } diff --git a/Sources/iOS/DialogController.swift b/Sources/iOS/DialogController.swift index 9de9a0827..e176aa21b 100644 --- a/Sources/iOS/DialogController.swift +++ b/Sources/iOS/DialogController.swift @@ -36,29 +36,29 @@ open class DialogController: UIViewController { dialogView.maxSize = CGSize(width: Screen.width * 0.8, height: Screen.height * 0.9) } - open var canceledHandler: (() -> Void)? + open var didCancelHandler: (() -> Void)? open var shouldDismissHandler: ((Button?) -> Bool)? - open var positiveHandler: (() -> Void)? - open var negativeHandler: (() -> Void)? - open var neutralHandler: (() -> Void)? + open var didTapPositiveButtonHandler: (() -> Void)? + open var didTapNegativeButtonHandler: (() -> Void)? + open var didTapNeutralButtonHandler: (() -> Void)? @objc private func didTapView() { guard isCancelable else { return } dismiss(nil) - canceledHandler?() + didCancelHandler?() } @objc private func didTapButton(_ sender: Button) { switch sender { case dialogView.positiveButton: - positiveHandler?() + didTapPositiveButtonHandler?() case dialogView.negativeButton: - negativeHandler?() + didTapNegativeButtonHandler?() case dialogView.neutralButton: - neutralHandler?() + didTapNeutralButtonHandler?() default: break } From e29304cea74ce9b7bde474c6034b7be0dd53f176 Mon Sep 17 00:00:00 2001 From: Orkhan Alikhanov Date: Sun, 14 Jan 2018 00:57:11 +0400 Subject: [PATCH 6/8] Added DialogView parameter to DialogController.shouldDismissHandler --- Sources/iOS/DialogBuilder.swift | 2 +- Sources/iOS/DialogController.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/iOS/DialogBuilder.swift b/Sources/iOS/DialogBuilder.swift index 4869ae7b6..23a3c2f02 100644 --- a/Sources/iOS/DialogBuilder.swift +++ b/Sources/iOS/DialogBuilder.swift @@ -30,7 +30,7 @@ open class DialogBuilder { return self } - open func shouldDismiss(handler: ((Button?) -> Bool)?) -> DialogBuilder { + open func shouldDismiss(handler: ((T, Button?) -> Bool)?) -> DialogBuilder { controller.shouldDismissHandler = handler return self } diff --git a/Sources/iOS/DialogController.swift b/Sources/iOS/DialogController.swift index e176aa21b..75405bffb 100644 --- a/Sources/iOS/DialogController.swift +++ b/Sources/iOS/DialogController.swift @@ -37,7 +37,7 @@ open class DialogController: UIViewController { } open var didCancelHandler: (() -> Void)? - open var shouldDismissHandler: ((Button?) -> Bool)? + open var shouldDismissHandler: ((T, Button?) -> Bool)? open var didTapPositiveButtonHandler: (() -> Void)? open var didTapNegativeButtonHandler: (() -> Void)? @@ -66,7 +66,7 @@ open class DialogController: UIViewController { } open func dismiss(_ button: Button?) { - if shouldDismissHandler?(button) ?? true { + if shouldDismissHandler?(dialogView, button) ?? true { presentingViewController?.dismiss(animated: true, completion: nil) } } From 037ec0eeb068ef77f7da0a949e33608cd34532c0 Mon Sep 17 00:00:00 2001 From: Orkhan Alikhanov Date: Wed, 17 Jan 2018 20:20:24 +0400 Subject: [PATCH 7/8] Moved hardcoded magic numbers, to the `Constants` struct --- Sources/iOS/DialogView.swift | 94 +++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/Sources/iOS/DialogView.swift b/Sources/iOS/DialogView.swift index ad4c73faf..bf38e39ca 100644 --- a/Sources/iOS/DialogView.swift +++ b/Sources/iOS/DialogView.swift @@ -83,8 +83,9 @@ open class DialogView: UIView { h += titleAreaSizeThatFits(width: w).height h += buttonAreaSizeThatFits(width: w).height h += contentViewSizeThatFits(width: w).height + h = min(h, size.height) - return CGSize(width: w, height: min(h, size.height)) + return CGSize(width: w, height: h) } open override func layoutSubviews() { @@ -103,9 +104,10 @@ open class DialogView: UIView { /// 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 + let insets = Constants.titleArea.insets + var size = titleLabel.sizeThatFits(CGSize(width: width - insets.left - insets.right, height: .greatestFiniteMagnitude)) + size.width += insets.left + insets.right + size.height += insets.top + insets.bottom return size } @@ -113,28 +115,57 @@ open class DialogView: UIView { guard !nonHiddenButtons.isEmpty else { return .zero } let isStacked = requiredButtonAreaWidth > width + let buttonsHeight = Constants.button.height * CGFloat(isStacked ? nonHiddenButtons.count : 1) + let buttonsInterim = isStacked ? CGFloat(nonHiddenButtons.count - 1) * Constants.button.interimStacked : 0 + let insets = isStacked ? Constants.buttonArea.insetsStacked : Constants.buttonArea.insets + let h = buttonsInterim + buttonsHeight + insets.bottom + insets.top 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 + let insets = titleLabel.isEmpty ? Constants.contentView.insetsNoTitle : Constants.contentView.insets + var size = detailsLabel.sizeThatFits(CGSize(width: width - insets.left - insets.right, height: .greatestFiniteMagnitude)) + size.width += insets.left + insets.right + size.height += insets.top + insets.bottom return size } } +private struct Constants { + struct titleArea { + static let insets = UIEdgeInsets(top: 24, left: 24, bottom: 20, right: 24) + } + + struct contentView { + static let insets = UIEdgeInsets(top: 0, left: 24, bottom: 24, right: 24) + static let insetsNoTitle = UIEdgeInsets(top: 20, left: 24, bottom: 24, right: 24) + } + + struct buttonArea { + static let insets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) + static let insetsStacked = UIEdgeInsets(top: 6, left: 8, bottom: 14, right: 8) + } + + struct button { + static let insets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) + static let minWidth: CGFloat = 64 + static let height: CGFloat = 36 + static let interimStacked: CGFloat = 12 + static let interim: CGFloat = 8 + } +} + 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) + let rect = CGRect(origin: .zero, size: size) + titleLabel.frame = UIEdgeInsetsInsetRect(rect, Constants.titleArea.insets) } func layoutButtonArea() { @@ -147,25 +178,29 @@ private extension DialogView { let isStacked = requiredButtonAreaWidth > width if isStacked { + let insets = Constants.buttonArea.insetsStacked 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 + let w = min($0.optimalWidth, width - insets.left - insets.right) + $0.frame.size = CGSize(width: w, height: Constants.button.height) + $0.frame.origin.x = width - insets.right - w } - positiveButton.frame.origin.y = 6 - let belowPositive = positiveButton.isHidden ? 6 : positiveButton.frame.maxY + 6 + 6 + positiveButton.frame.origin.y = insets.top + let belowPositive = positiveButton.isHidden ? insets.top : positiveButton.frame.maxY + Constants.button.interimStacked negativeButton.frame.origin.y = belowPositive - neutralButton.frame.origin.y = negativeButton.isHidden ? belowPositive : (negativeButton.frame.maxY + 6 + 6) + neutralButton.frame.origin.y = negativeButton.isHidden ? belowPositive : negativeButton.frame.maxY + Constants.button.interimStacked } else { + let insets = Constants.buttonArea.insets buttons.forEach { - $0.frame.size = CGSize(width: $0.optimalWidth, height: 36) - $0.frame.origin.y = 8 + $0.frame.size = CGSize(width: $0.optimalWidth, height: Constants.button.height) + $0.frame.origin.y = insets.top } - 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 + neutralButton.frame.origin.x = insets.left + positiveButton.frame.origin.x = width - insets.right - positiveButton.frame.width + + let maxX = positiveButton.isHidden ? width - insets.right : positiveButton.frame.minX - Constants.button.interim + negativeButton.frame.origin.x = maxX - negativeButton.frame.width } } @@ -173,8 +208,9 @@ private extension DialogView { 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) + let rect = CGRect(origin: .zero, size: size) + let insets = titleArea.frame.height == 0 ? Constants.contentView.insetsNoTitle : Constants.contentView.insets + detailsLabel.frame = UIEdgeInsetsInsetRect(rect, insets) } func layoutScrollView() { @@ -204,7 +240,8 @@ private extension DialogView { private extension Button { var optimalWidth: CGFloat { - return max(64, sizeThatFits(CGSize(width: .max, height: 36)).width) + let size = CGSize(width: .greatestFiniteMagnitude, height: Constants.button.height) + return max(Constants.button.minWidth, sizeThatFits(size).width) } } @@ -223,11 +260,13 @@ private extension DialogView { 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 + let insets = Constants.buttonArea.insets + return buttonsWidth + insets.left + insets.right + CGFloat((buttons.count - 1)) * Constants.button.interim + additional } var requiredButtonAreaWidthForStacked: CGFloat { - return 8 + 8 + nonHiddenButtons.reduce(0) { + let insets = Constants.buttonArea.insetsStacked + return insets.left + insets.right + nonHiddenButtons.reduce(0) { max($0, $1.optimalWidth) } } @@ -266,7 +305,7 @@ private extension DialogView { [positiveButton, negativeButton, neutralButton].forEach { buttonArea.addSubview($0) $0.titleLabel?.font = RobotoFont.medium(with: 14) - $0.contentEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) + $0.contentEdgeInsets = Constants.button.insets } } @@ -291,6 +330,7 @@ private extension UIScrollView { } var isAtBottom: Bool { + // - 1 is to get rid of precision errors which makes divider appear even when scroll is at bottom return contentOffset.y >= (contentSize.height - frame.height - 1) } } From bc2542878f15b0a954e6a5b8e193d366620f4975 Mon Sep 17 00:00:00 2001 From: Orkhan Alikhanov Date: Wed, 17 Jan 2018 20:42:22 +0400 Subject: [PATCH 8/8] Added missing corner radius for dialog buttons --- Sources/iOS/DialogView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/iOS/DialogView.swift b/Sources/iOS/DialogView.swift index bf38e39ca..703647855 100644 --- a/Sources/iOS/DialogView.swift +++ b/Sources/iOS/DialogView.swift @@ -306,6 +306,7 @@ private extension DialogView { buttonArea.addSubview($0) $0.titleLabel?.font = RobotoFont.medium(with: 14) $0.contentEdgeInsets = Constants.button.insets + $0.cornerRadiusPreset = .cornerRadius1 } }