Files
appchk-app/main/Common Classes/TutorialSheet.swift
2020-06-26 20:26:30 +02:00

204 lines
6.5 KiB
Swift

import UIKit
fileprivate var margin: CGFloat { 20 }
fileprivate var sheetInset: CGFloat { cornerRadius/2 }
fileprivate var cornerRadius: CGFloat { 15 }
fileprivate var uniRect: CGRect { CGRect(x: 0, y: 0, width: 500, height: 500) }
class TutorialSheet: UIViewController, UIScrollViewDelegate {
/// Maximum displayable width of a Tutorial Sheet in portrait mode.
public static var verticalWidth: CGFloat {
let s = UIScreen.main.bounds.size
return min(s.width, s.height) - 2 * (margin + sheetInset)
}
public var buttonTitleNext: String = "Next"
public var buttonTitleDone: String = "Close"
private var priorIndex: Int?
private var lastAnchor: NSLayoutConstraint?
private var shouldAnimate: Bool = true
private var shouldCloseBlock: (() -> Bool)? = nil
private var didCloseBlock: (() -> Void)? = nil
private let sheetBg: UIView = {
let x = UIView(frame: uniRect)
x.autoresizingMask = [.flexibleWidth, .flexibleHeight]
x.backgroundColor = .sysBg
x.layer.cornerRadius = cornerRadius
x.layer.shadowColor = UIColor.black.cgColor
x.layer.shadowRadius = 10
x.layer.shadowOpacity = 0.75
x.layer.shadowOffset = CGSize(width: 0, height: 4)
return x
}()
private let pager: UIPageControl = {
let x = UIPageControl(frame: uniRect)
x.frame.size.height = x.size(forNumberOfPages: 1).height
x.currentPageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.5)
x.pageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.25)
x.numberOfPages = 0
x.hidesForSinglePage = true
x.addTarget(self, action: #selector(pagerDidChange), for: .valueChanged)
return x
}()
private let pageScroll: UIScrollView = {
let x = UIScrollView(frame: uniRect)
x.bounces = false
x.isPagingEnabled = true
x.showsVerticalScrollIndicator = false
x.showsHorizontalScrollIndicator = false
let content = UIView()
x.addSubview(content)
content.anchor([.left, .right, .top, .bottom], to: x)
content.anchor([.width, .height], to: x) | .defaultLow
return x
}()
private let button: UIButton = {
let x = QuickUI.button("", target: self, action: #selector(buttonTapped))
x.contentEdgeInsets = UIEdgeInsets(all: 8)
return x
}()
// MARK: Init
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
required init() {
super.init(nibName: nil, bundle: nil)
view = makeControlUI()
modalPresentationStyle = .custom
if #available(iOS 13.0, *) {
isModalInPresentation = true
}
UIDevice.orientationDidChangeNotification.observe(call: #selector(didChangeOrientation), on: self)
}
/// Present Tutorial Sheet Controller
/// - Parameter viewController: If set to `nil`, use main application as canvas. (Default: `nil`)
/// - Parameter animate: Use `present` and `dismiss` animations. (Default: `true`)
/// - Parameter shouldClose: Called before the view controller is dismissed. Return `false` to prevent the dismissal.
/// Use this block to extract user data from input fields. (Default: `nil`)
/// - Parameter didClose: Called after the view controller is completely dismissed (with animations).
/// Use this block to update UI and visible changes. (Default: `nil`)
func present(in viewController: UIViewController? = nil, animate: Bool = true, shouldClose: (() -> Bool)? = nil, didClose: (() -> Void)? = nil) {
guard let vc = viewController ?? UIApplication.shared.keyWindow?.rootViewController else {
return
}
shouldCloseBlock = shouldClose
didCloseBlock = didClose
shouldAnimate = animate
vc.present(self, animated: animate)
}
// MARK: Dynamic UI
@discardableResult func addSheet(_ closure: ((UIStackView) -> Void)? = nil) -> UIStackView {
pager.numberOfPages += 1
updateButtonTitle()
let x = UIStackView(frame: pageScroll.bounds)
x.axis = .vertical
x.backgroundColor = UIColor.black
x.isOpaque = true
guard let content = pageScroll.subviews.first else {
return x
}
let prev = content.subviews.last
content.addSubview(x)
x.anchor([.top, .height], to: pageScroll)
x.widthAnchor =&= sheetBg.widthAnchor - 2 * sheetInset
x.leadingAnchor =&= (prev==nil ? content.leadingAnchor : prev!.trailingAnchor)
lastAnchor?.isActive = false
lastAnchor = (x.trailingAnchor =&= pageScroll.trailingAnchor)
closure?(x)
return x
}
// MARK: Static UI
private func makeControlUI() -> UIView {
pageScroll.delegate = self
sheetBg.addSubview(pager)
sheetBg.addSubview(pageScroll)
sheetBg.addSubview(button)
pager.anchor([.top, .left, .right], to: sheetBg)
pageScroll.topAnchor =&= pager.bottomAnchor
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh
button.topAnchor =&= pageScroll.bottomAnchor
button.anchor([.bottom, .centerX], to: sheetBg)
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30
// button.centerXAnchor =&= sheetBg.centerXAnchor
let bg = UIView(frame: uniRect)
bg.autoresizingMask = [.flexibleWidth, .flexibleHeight]
bg.addSubview(sheetBg)
let h: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : UIApplication.shared.statusBarFrame.height
sheetBg.frame = bg.frame.inset(by: UIEdgeInsets(all: margin, top: margin + h))
return bg
}
// MARK: Delegates
override func viewWillLayoutSubviews() {
priorIndex = pager.currentPage
}
@objc private func didChangeOrientation() {
if let i = priorIndex {
priorIndex = nil
switchToSheet(i, animated: false)
}
for case let x as UIStackView in pageScroll.subviews.first!.subviews {
x.axis = (x.frame.width > x.frame.height) ? .horizontal : .vertical
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let w = scrollView.frame.width
let new = Int((scrollView.contentOffset.x + w/2) / w)
if pager.currentPage != new {
pager.currentPage = new
updateButtonTitle()
}
}
@objc private func pagerDidChange(sender: UIPageControl) {
switchToSheet(sender.currentPage, animated: true)
}
private func switchToSheet(_ i: Int, animated: Bool) {
pageScroll.setContentOffset(CGPoint(x: CGFloat(i) * pageScroll.bounds.width, y: 0), animated: animated)
}
private func updateButtonTitle() {
let last = (pager.currentPage == pager.numberOfPages - 1)
let title = last ? buttonTitleDone : buttonTitleNext
if button.title(for: .normal) != title {
button.setTitle(title, for: .normal)
}
}
@objc private func buttonTapped() {
let next = pager.currentPage + 1
if next < pager.numberOfPages {
switchToSheet(next, animated: true)
} else {
if shouldCloseBlock?() ?? true {
dismiss(animated: shouldAnimate, completion: didCloseBlock)
}
}
}
}