123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- //
- // WizardViewController.swift
- // Mastodon
- //
- // Created by Cirno MainasuK on 2021-11-2.
- //
- import os.log
- import UIKit
- import Combine
- import MastodonAsset
- import MastodonLocalization
- protocol WizardViewControllerDelegate: AnyObject {
- func readyToLayoutItem(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> Bool
- func layoutSpotlight(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> UIBezierPath
- func layoutWizardCard(_ wizardViewController: WizardViewController, item: WizardViewController.Item)
- }
- class WizardViewController: UIViewController {
-
- let logger = Logger(subsystem: "Wizard", category: "UI")
-
- var disposeBag = Set<AnyCancellable>()
- weak var delegate: WizardViewControllerDelegate?
-
- private(set) var items: [Item] = {
- var items: [Item] = []
- if !UserDefaults.shared.didShowMultipleAccountSwitchWizard {
- items.append(.multipleAccountSwitch)
- }
- return items
- }()
-
- let pendingItem = CurrentValueSubject<Item?, Never>(nil)
- let currentItem = CurrentValueSubject<Item?, Never>(nil)
-
- let backgroundView: UIView = {
- let view = UIView()
- view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
- return view
- }()
-
- deinit {
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- }
-
- }
- extension WizardViewController {
- override func viewDidLoad() {
- super.viewDidLoad()
-
- setup()
-
- let backgroundTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
- backgroundTapGestureRecognizer.addTarget(self, action: #selector(WizardViewController.backgroundTapGestureRecognizerHandler(_:)))
- backgroundView.addGestureRecognizer(backgroundTapGestureRecognizer)
- }
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- // Create a timer to consume pending item
- Timer.publish(every: 0.5, on: .main, in: .default)
- .autoconnect()
- .receive(on: DispatchQueue.main)
- .sink { [weak self] _ in
- guard let self = self else { return }
- guard self.pendingItem.value != nil else { return }
- self.consume()
- }
- .store(in: &disposeBag)
-
- consume()
- }
-
- override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
-
- invalidLayoutForCurrentItem()
- }
-
- override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
-
- coordinator.animate { context in
-
- } completion: { [weak self] context in
- guard let self = self else { return }
- self.invalidLayoutForCurrentItem()
- }
- }
- }
- extension WizardViewController {
- enum Item {
- case multipleAccountSwitch
-
- var title: String {
- return L10n.Scene.Wizard.newInMastodon
- }
-
- var description: String {
- switch self {
- case .multipleAccountSwitch:
- return L10n.Scene.Wizard.multipleAccountSwitchIntroDescription
- }
- }
-
- func markAsRead() {
- switch self {
- case .multipleAccountSwitch:
- UserDefaults.shared.didShowMultipleAccountSwitchWizard = true
- }
- }
- }
- }
- extension WizardViewController {
-
- func setup() {
- assert(delegate != nil, "need set delegate before use")
-
- guard !items.isEmpty else { return }
-
- backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- backgroundView.frame = view.bounds
- view.addSubview(backgroundView)
- }
-
- func destroy() {
- view.removeFromSuperview()
- }
-
- func consume() {
- guard !items.isEmpty else {
- destroy()
- return
- }
-
- guard let first = items.first else { return }
- guard delegate?.readyToLayoutItem(self, item: first) == true else {
- pendingItem.value = first
- return
- }
- pendingItem.value = nil
- currentItem.value = nil
-
- let item = items.removeFirst()
- perform(item: item)
- }
-
- private func perform(item: Item) {
- guard let delegate = delegate else {
- assertionFailure()
- return
- }
-
- // prepare for reuse
- prepareForReuse()
-
- // set wizard item read
- item.markAsRead()
-
- // add spotlight
- let spotlight = delegate.layoutSpotlight(self, item: item)
- let maskLayer = CAShapeLayer()
- // expand rect to make sure view always fill the screen when device rotate
- let expandRect: CGRect = {
- var rect = backgroundView.bounds
- rect.size.width *= 2
- rect.size.height *= 2
- return rect
- }()
- let path = UIBezierPath(rect: expandRect)
- path.append(spotlight)
- maskLayer.fillRule = .evenOdd
- maskLayer.path = path.cgPath
- backgroundView.layer.mask = maskLayer
-
- // layout wizard card
- delegate.layoutWizardCard(self, item: item)
-
- currentItem.value = item
- }
-
- private func prepareForReuse() {
- backgroundView.subviews.forEach { subview in
- subview.removeFromSuperview()
- }
- backgroundView.mask = nil
- backgroundView.layer.mask = nil
- }
-
- private func invalidLayoutForCurrentItem() {
- if let item = currentItem.value {
- perform(item: item)
- }
- }
-
- }
- extension WizardViewController {
- @objc private func backgroundTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
- logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
-
- consume()
- }
- }
|