343 lines
12 KiB
Swift
343 lines
12 KiB
Swift
//
|
|
// PopupDialog.swift
|
|
//
|
|
// Copyright (c) 2016 Orderella Ltd. (http://orderella.co.uk)
|
|
// Author - Martin Wildfeuer (http://www.mwfire.de)
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
/// Creates a Popup dialog similar to UIAlertController
|
|
final public class PopupDialog: UIViewController {
|
|
|
|
// MARK: Private / Internal
|
|
|
|
/// First init flag
|
|
fileprivate var initialized = false
|
|
|
|
/// StatusBar display related
|
|
fileprivate let hideStatusBar: Bool
|
|
fileprivate var statusBarShouldBeHidden: Bool = false
|
|
|
|
/// Width for iPad displays
|
|
fileprivate let preferredWidth: CGFloat
|
|
|
|
/// The completion handler
|
|
fileprivate var completion: (() -> Void)?
|
|
|
|
/// The custom transition presentation manager
|
|
fileprivate var presentationManager: PresentationManager!
|
|
|
|
/// Interactor class for pan gesture dismissal
|
|
fileprivate lazy var interactor = InteractiveTransition()
|
|
|
|
/// Returns the controllers view
|
|
internal var popupContainerView: PopupDialogContainerView {
|
|
return view as! PopupDialogContainerView // swiftlint:disable:this force_cast
|
|
}
|
|
|
|
/// The set of buttons
|
|
fileprivate var buttons = [PopupDialogButton]()
|
|
|
|
/// Whether keyboard has shifted view
|
|
internal var keyboardShown = false
|
|
|
|
/// Keyboard height
|
|
internal var keyboardHeight: CGFloat?
|
|
|
|
// MARK: Public
|
|
|
|
/// The content view of the popup dialog
|
|
public var viewController: UIViewController
|
|
|
|
/// Whether or not to shift view for keyboard display
|
|
public var keyboardShiftsView = true
|
|
|
|
// MARK: - Initializers
|
|
|
|
/*!
|
|
Creates a standard popup dialog with title, message and image field
|
|
|
|
- parameter title: The dialog title
|
|
- parameter message: The dialog message
|
|
- parameter image: The dialog image
|
|
- parameter buttonAlignment: The dialog button alignment
|
|
- parameter transitionStyle: The dialog transition style
|
|
- parameter preferredWidth: The preferred width for iPad screens
|
|
- parameter tapGestureDismissal: Indicates if dialog can be dismissed via tap gesture
|
|
- parameter panGestureDismissal: Indicates if dialog can be dismissed via pan gesture
|
|
- parameter hideStatusBar: Whether to hide the status bar on PopupDialog presentation
|
|
- parameter completion: Completion block invoked when dialog was dismissed
|
|
|
|
- returns: Popup dialog default style
|
|
*/
|
|
@objc public convenience init(
|
|
title: String?,
|
|
message: String?,
|
|
image: UIImage? = nil,
|
|
buttonAlignment: NSLayoutConstraint.Axis = .vertical,
|
|
transitionStyle: PopupDialogTransitionStyle = .bounceUp,
|
|
preferredWidth: CGFloat = 340,
|
|
tapGestureDismissal: Bool = true,
|
|
panGestureDismissal: Bool = true,
|
|
hideStatusBar: Bool = false,
|
|
completion: (() -> Void)? = nil) {
|
|
|
|
// Create and configure the standard popup dialog view
|
|
let viewController = PopupDialogDefaultViewController()
|
|
viewController.titleText = title
|
|
viewController.messageText = message
|
|
viewController.image = image
|
|
|
|
// Call designated initializer
|
|
self.init(viewController: viewController,
|
|
buttonAlignment: buttonAlignment,
|
|
transitionStyle: transitionStyle,
|
|
preferredWidth: preferredWidth,
|
|
tapGestureDismissal: tapGestureDismissal,
|
|
panGestureDismissal: panGestureDismissal,
|
|
hideStatusBar: hideStatusBar,
|
|
completion: completion)
|
|
}
|
|
|
|
/*!
|
|
Creates a popup dialog containing a custom view
|
|
|
|
- parameter viewController: A custom view controller to be displayed
|
|
- parameter buttonAlignment: The dialog button alignment
|
|
- parameter transitionStyle: The dialog transition style
|
|
- parameter preferredWidth: The preferred width for iPad screens
|
|
- parameter tapGestureDismissal: Indicates if dialog can be dismissed via tap gesture
|
|
- parameter panGestureDismissal: Indicates if dialog can be dismissed via pan gesture
|
|
- parameter hideStatusBar: Whether to hide the status bar on PopupDialog presentation
|
|
- parameter completion: Completion block invoked when dialog was dismissed
|
|
|
|
- returns: Popup dialog with a custom view controller
|
|
*/
|
|
@objc public init(
|
|
viewController: UIViewController,
|
|
buttonAlignment: NSLayoutConstraint.Axis = .vertical,
|
|
transitionStyle: PopupDialogTransitionStyle = .bounceUp,
|
|
preferredWidth: CGFloat = 340,
|
|
tapGestureDismissal: Bool = true,
|
|
panGestureDismissal: Bool = true,
|
|
hideStatusBar: Bool = false,
|
|
completion: (() -> Void)? = nil) {
|
|
|
|
self.viewController = viewController
|
|
self.preferredWidth = preferredWidth
|
|
self.hideStatusBar = hideStatusBar
|
|
self.completion = completion
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
// Init the presentation manager
|
|
presentationManager = PresentationManager(transitionStyle: transitionStyle, interactor: interactor)
|
|
|
|
// Assign the interactor view controller
|
|
interactor.viewController = self
|
|
|
|
// Define presentation styles
|
|
transitioningDelegate = presentationManager
|
|
modalPresentationStyle = .custom
|
|
|
|
// StatusBar setup
|
|
modalPresentationCapturesStatusBarAppearance = true
|
|
|
|
// Add our custom view to the container
|
|
addChild(viewController)
|
|
popupContainerView.stackView.insertArrangedSubview(viewController.view, at: 0)
|
|
popupContainerView.buttonStackView.axis = buttonAlignment
|
|
viewController.didMove(toParent: self)
|
|
|
|
// Allow for dialog dismissal on background tap
|
|
if tapGestureDismissal {
|
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
|
tapRecognizer.cancelsTouchesInView = false
|
|
popupContainerView.addGestureRecognizer(tapRecognizer)
|
|
}
|
|
// Allow for dialog dismissal on dialog pan gesture
|
|
if panGestureDismissal {
|
|
let panRecognizer = UIPanGestureRecognizer(target: interactor, action: #selector(InteractiveTransition.handlePan))
|
|
panRecognizer.cancelsTouchesInView = false
|
|
popupContainerView.stackView.addGestureRecognizer(panRecognizer)
|
|
}
|
|
}
|
|
|
|
// Init with coder not implemented
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - View life cycle
|
|
|
|
/// Replaces controller view with popup view
|
|
public override func loadView() {
|
|
view = PopupDialogContainerView(frame: UIScreen.main.bounds, preferredWidth: preferredWidth)
|
|
}
|
|
|
|
public override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
addObservers()
|
|
|
|
guard !initialized else { return }
|
|
appendButtons()
|
|
initialized = true
|
|
}
|
|
|
|
public override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
statusBarShouldBeHidden = hideStatusBar
|
|
UIView.animate(withDuration: 0.15) {
|
|
self.setNeedsStatusBarAppearanceUpdate()
|
|
}
|
|
}
|
|
|
|
public override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
removeObservers()
|
|
}
|
|
|
|
deinit {
|
|
completion?()
|
|
completion = nil
|
|
}
|
|
|
|
// MARK: - Dismissal related
|
|
|
|
@objc fileprivate func handleTap(_ sender: UITapGestureRecognizer) {
|
|
|
|
// Make sure it's not a tap on the dialog but the background
|
|
let point = sender.location(in: popupContainerView.stackView)
|
|
guard !popupContainerView.stackView.point(inside: point, with: nil) else { return }
|
|
dismiss()
|
|
}
|
|
|
|
/*!
|
|
Dismisses the popup dialog
|
|
*/
|
|
@objc public func dismiss(_ completion: (() -> Void)? = nil) {
|
|
self.dismiss(animated: true) {
|
|
completion?()
|
|
}
|
|
}
|
|
|
|
// MARK: - Button related
|
|
|
|
/*!
|
|
Appends the buttons added to the popup dialog
|
|
to the placeholder stack view
|
|
*/
|
|
fileprivate func appendButtons() {
|
|
|
|
// Add action to buttons
|
|
let stackView = popupContainerView.stackView
|
|
let buttonStackView = popupContainerView.buttonStackView
|
|
if buttons.isEmpty {
|
|
stackView.removeArrangedSubview(popupContainerView.buttonStackView)
|
|
}
|
|
|
|
for (index, button) in buttons.enumerated() {
|
|
button.needsLeftSeparator = buttonStackView.axis == .horizontal && index > 0
|
|
buttonStackView.addArrangedSubview(button)
|
|
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
|
|
}
|
|
}
|
|
|
|
/*!
|
|
Adds a single PopupDialogButton to the Popup dialog
|
|
- parameter button: A PopupDialogButton instance
|
|
*/
|
|
@objc public func addButton(_ button: PopupDialogButton) {
|
|
buttons.append(button)
|
|
}
|
|
|
|
/*!
|
|
Adds an array of PopupDialogButtons to the Popup dialog
|
|
- parameter buttons: A list of PopupDialogButton instances
|
|
*/
|
|
@objc public func addButtons(_ buttons: [PopupDialogButton]) {
|
|
self.buttons += buttons
|
|
}
|
|
|
|
/// Calls the action closure of the button instance tapped
|
|
@objc fileprivate func buttonTapped(_ button: PopupDialogButton) {
|
|
if button.dismissOnTap {
|
|
dismiss({ button.buttonAction?() })
|
|
} else {
|
|
button.buttonAction?()
|
|
}
|
|
}
|
|
|
|
/*!
|
|
Simulates a button tap for the given index
|
|
Makes testing a breeze
|
|
- parameter index: The index of the button to tap
|
|
*/
|
|
public func tapButtonWithIndex(_ index: Int) {
|
|
let button = buttons[index]
|
|
button.buttonAction?()
|
|
}
|
|
|
|
// MARK: - StatusBar display related
|
|
|
|
public override var prefersStatusBarHidden: Bool {
|
|
return statusBarShouldBeHidden
|
|
}
|
|
|
|
public override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
|
return .slide
|
|
}
|
|
}
|
|
|
|
// MARK: - View proxy values
|
|
|
|
extension PopupDialog {
|
|
|
|
/// The button alignment of the alert dialog
|
|
@objc public var buttonAlignment: NSLayoutConstraint.Axis {
|
|
get {
|
|
return popupContainerView.buttonStackView.axis
|
|
}
|
|
set {
|
|
popupContainerView.buttonStackView .axis = newValue
|
|
popupContainerView.pv_layoutIfNeededAnimated()
|
|
}
|
|
}
|
|
|
|
/// The transition style
|
|
@objc public var transitionStyle: PopupDialogTransitionStyle {
|
|
get { return presentationManager.transitionStyle }
|
|
set { presentationManager.transitionStyle = newValue }
|
|
}
|
|
}
|
|
|
|
// MARK: - Shake
|
|
|
|
extension PopupDialog {
|
|
|
|
/// Performs a shake animation on the dialog
|
|
@objc public func shake() {
|
|
popupContainerView.pv_shake()
|
|
}
|
|
}
|