242 lines
6.9 KiB
Swift
242 lines
6.9 KiB
Swift
import UIKit
|
|
|
|
class CustomAlert<CustomView: UIView>: UIViewController {
|
|
|
|
private let alertTitle: String?
|
|
private let alertDetail: String?
|
|
|
|
private let customView: CustomView
|
|
private var callback: ((CustomView) -> Void)!
|
|
|
|
private let backgroundShadow: UIView = {
|
|
let shadow = UIView()
|
|
shadow.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
return shadow
|
|
}()
|
|
|
|
private let control: UIView = {
|
|
let x = UIView()
|
|
x.backgroundColor = .sysBackground
|
|
return x
|
|
}()
|
|
|
|
/// Default: `[Cancel, Save]`
|
|
let buttonsBar: UIStackView = {
|
|
let cancel = QuickUI.button("Cancel", target: self, action: #selector(didTapCancel))
|
|
let save = QuickUI.button("Save", target: self, action: #selector(didTapSave))
|
|
save.titleLabel?.font = save.titleLabel?.font.bold()
|
|
let bar = UIStackView(arrangedSubviews: [cancel, save])
|
|
bar.axis = .horizontal
|
|
bar.distribution = .equalSpacing
|
|
return bar
|
|
}()
|
|
|
|
|
|
// MARK: - Init
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
init(title: String? = nil, detail: String? = nil, view custom: CustomView) {
|
|
alertTitle = title
|
|
alertDetail = detail
|
|
customView = custom
|
|
super.init(nibName: nil, bundle: nil)
|
|
modalPresentationStyle = .custom
|
|
if #available(iOS 13.0, *) {
|
|
isModalInPresentation = true
|
|
}
|
|
}
|
|
|
|
internal override func loadView() {
|
|
view = UIView()
|
|
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
view.isHidden = true // otherwise control will flash on present
|
|
|
|
var h: CGFloat = 0
|
|
var prevView: UIView? = nil
|
|
func appendView(_ x: UIView, top: CGFloat, lr: CGFloat) {
|
|
control.addSubview(x)
|
|
// sticky edges horizontally
|
|
x.anchor([.leading, .trailing], to: control, margin: lr)
|
|
// chain views vertically
|
|
x.topAnchor =&= (prevView?.bottomAnchor ?? control.topAnchor) + top
|
|
prevView = x
|
|
h += x.frame.height + top
|
|
}
|
|
|
|
if let t = alertTitle {
|
|
let lbl = QuickUI.label(t, align: .center, style: .headline)
|
|
lbl.numberOfLines = 0
|
|
appendView(lbl, top: 16, lr: 16)
|
|
}
|
|
if let d = alertDetail {
|
|
let lbl = QuickUI.label(d, align: .center, style: .subheadline)
|
|
lbl.numberOfLines = 0
|
|
appendView(lbl, top: 16, lr: 16)
|
|
}
|
|
appendView(customView, top: (prevView == nil) ? 0 : 16, lr: 0)
|
|
appendView(buttonsBar, top: 0, lr: 25)
|
|
|
|
buttonsBar.bottomAnchor =&= control.bottomAnchor - 15
|
|
h += 15 // buttonsBar has 15px padding
|
|
|
|
let screen = UIScreen.main.bounds.size
|
|
control.frame = CGRect(x: 0, y: screen.height - h, width: screen.width, height: h)
|
|
|
|
view.addSubview(control)
|
|
control.anchor([.leading, .trailing, .bottom], to: view!)
|
|
}
|
|
|
|
|
|
// MARK: - User Interaction
|
|
|
|
override var keyCommands: [UIKeyCommand]? {
|
|
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(didTapCancel))]
|
|
}
|
|
|
|
@objc private func didTapCancel() {
|
|
callback = nil
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
@objc private func didTapSave() {
|
|
dismiss(animated: true) {
|
|
self.callback(self.customView)
|
|
self.callback = nil
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: - Present & Dismiss
|
|
|
|
func present(in viewController: UIViewController, onSuccess: @escaping (CustomView) -> Void) {
|
|
callback = onSuccess
|
|
loadViewIfNeeded()
|
|
viewController.present(self, animated: false) {
|
|
let prev = self.control.frame.origin.y
|
|
self.control.frame.origin.y += self.control.frame.height
|
|
self.view.isHidden = false
|
|
|
|
UIView.animate(withDuration: 0.3) {
|
|
self.view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
|
self.control.frame.origin.y = prev
|
|
}
|
|
}
|
|
}
|
|
|
|
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
|
UIView.animate(withDuration: 0.3, animations: {
|
|
self.view.backgroundColor = .clear
|
|
self.control.frame.origin.y += self.control.frame.height
|
|
}) { _ in
|
|
super.dismiss(animated: false, completion: completion)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ###################################
|
|
// #
|
|
// # MARK: - Date Picker Alert
|
|
// #
|
|
// ###################################
|
|
|
|
class DatePickerAlert : CustomAlert<UIDatePicker> {
|
|
|
|
let datePicker: UIDatePicker = {
|
|
let x = UIDatePicker()
|
|
x.frame.size.height = x.sizeThatFits(.zero).height
|
|
return x
|
|
}()
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
init(title: String? = nil, detail: String? = nil, initial date: Date? = nil) {
|
|
if let date = date {
|
|
datePicker.setDate(date, animated: false)
|
|
}
|
|
super.init(title: title, detail: detail, view: datePicker)
|
|
|
|
let now = QuickUI.button("Now", target: self, action: #selector(didTapNow))
|
|
now.titleLabel?.font = now.titleLabel?.font.bold()
|
|
now.setTitleColor(.sysLabel, for: .normal)
|
|
buttonsBar.insertArrangedSubview(now, at: 1)
|
|
}
|
|
|
|
@objc private func didTapNow() {
|
|
datePicker.date = Date()
|
|
}
|
|
|
|
func present(in viewController: UIViewController, onSuccess: @escaping (Date) -> Void) {
|
|
super.present(in: viewController) {
|
|
onSuccess($0.date)
|
|
}
|
|
}
|
|
}
|
|
|
|
// #######################################
|
|
// #
|
|
// # MARK: - Duration Picker Alert
|
|
// #
|
|
// #######################################
|
|
|
|
class DurationPickerAlert: CustomAlert<UIPickerView>, UIPickerViewDataSource, UIPickerViewDelegate {
|
|
|
|
private let dataSource: [[String]]
|
|
private let compWidths: [CGFloat]
|
|
let pickerView: UIPickerView = {
|
|
let x = UIPickerView()
|
|
x.frame.size.height = x.sizeThatFits(.zero).height
|
|
return x
|
|
}()
|
|
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
/// - Parameter options: [[List of labels] per component]
|
|
/// - Parameter widths: If `nil` set all components to equal width
|
|
init(title: String? = nil, detail: String? = nil, options: [[String]], widths: [CGFloat]? = nil) {
|
|
assert(widths == nil || widths!.count == options.count, "widths.count != options.count")
|
|
|
|
dataSource = options
|
|
compWidths = widths ?? options.map { _ in 1 / CGFloat(options.count) }
|
|
|
|
super.init(title: title, detail: detail, view: pickerView)
|
|
|
|
pickerView.dataSource = self
|
|
pickerView.delegate = self
|
|
}
|
|
|
|
func numberOfComponents(in _: UIPickerView) -> Int {
|
|
dataSource.count
|
|
}
|
|
func pickerView(_: UIPickerView, numberOfRowsInComponent c: Int) -> Int {
|
|
dataSource[c].count
|
|
}
|
|
func pickerView(_: UIPickerView, titleForRow r: Int, forComponent c: Int) -> String? {
|
|
dataSource[c][r]
|
|
}
|
|
func pickerView(_ pickerView: UIPickerView, widthForComponent c: Int) -> CGFloat {
|
|
compWidths[c] * pickerView.frame.width
|
|
}
|
|
|
|
func present(in viewController: UIViewController, onSuccess: @escaping ([Int]) -> Void) {
|
|
super.present(in: viewController) {
|
|
onSuccess($0.selection)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension UIPickerView {
|
|
var selection: [Int] {
|
|
get { (0..<numberOfComponents).map { selectedRow(inComponent: $0) } }
|
|
set { setSelection(newValue) }
|
|
}
|
|
/// - Warning: Does not check for boundaries!
|
|
func setSelection(_ selection: [Int], animated: Bool = false) {
|
|
assert(selection.count == numberOfComponents, "selection.count != components.count")
|
|
for (c, i) in selection.enumerated() {
|
|
selectRow(i, inComponent: c, animated: animated)
|
|
}
|
|
}
|
|
}
|