196 lines
6.2 KiB
Swift
196 lines
6.2 KiB
Swift
import UIKit
|
|
|
|
fileprivate let margin: CGFloat = 20
|
|
fileprivate let cornerRadius: CGFloat = 15
|
|
fileprivate let uniRect = CGRect(x: 0, y: 0, width: 500, height: 500)
|
|
|
|
class TutorialSheet: UIViewController, UIScrollViewDelegate {
|
|
|
|
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, .width, .height], to: pageScroll)
|
|
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: cornerRadius/2) | .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)
|
|
}
|
|
}
|
|
}
|
|
}
|