WizardViewController.swift 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. //
  2. // WizardViewController.swift
  3. // Mastodon
  4. //
  5. // Created by Cirno MainasuK on 2021-11-2.
  6. //
  7. import os.log
  8. import UIKit
  9. import Combine
  10. import MastodonAsset
  11. import MastodonLocalization
  12. protocol WizardViewControllerDelegate: AnyObject {
  13. func readyToLayoutItem(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> Bool
  14. func layoutSpotlight(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> UIBezierPath
  15. func layoutWizardCard(_ wizardViewController: WizardViewController, item: WizardViewController.Item)
  16. }
  17. class WizardViewController: UIViewController {
  18. let logger = Logger(subsystem: "Wizard", category: "UI")
  19. var disposeBag = Set<AnyCancellable>()
  20. weak var delegate: WizardViewControllerDelegate?
  21. private(set) var items: [Item] = {
  22. var items: [Item] = []
  23. if !UserDefaults.shared.didShowMultipleAccountSwitchWizard {
  24. items.append(.multipleAccountSwitch)
  25. }
  26. return items
  27. }()
  28. let pendingItem = CurrentValueSubject<Item?, Never>(nil)
  29. let currentItem = CurrentValueSubject<Item?, Never>(nil)
  30. let backgroundView: UIView = {
  31. let view = UIView()
  32. view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
  33. return view
  34. }()
  35. deinit {
  36. os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
  37. }
  38. }
  39. extension WizardViewController {
  40. override func viewDidLoad() {
  41. super.viewDidLoad()
  42. setup()
  43. let backgroundTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
  44. backgroundTapGestureRecognizer.addTarget(self, action: #selector(WizardViewController.backgroundTapGestureRecognizerHandler(_:)))
  45. backgroundView.addGestureRecognizer(backgroundTapGestureRecognizer)
  46. }
  47. override func viewDidAppear(_ animated: Bool) {
  48. super.viewDidAppear(animated)
  49. // Create a timer to consume pending item
  50. Timer.publish(every: 0.5, on: .main, in: .default)
  51. .autoconnect()
  52. .receive(on: DispatchQueue.main)
  53. .sink { [weak self] _ in
  54. guard let self = self else { return }
  55. guard self.pendingItem.value != nil else { return }
  56. self.consume()
  57. }
  58. .store(in: &disposeBag)
  59. consume()
  60. }
  61. override func viewDidLayoutSubviews() {
  62. super.viewDidLayoutSubviews()
  63. invalidLayoutForCurrentItem()
  64. }
  65. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  66. super.viewWillTransition(to: size, with: coordinator)
  67. coordinator.animate { context in
  68. } completion: { [weak self] context in
  69. guard let self = self else { return }
  70. self.invalidLayoutForCurrentItem()
  71. }
  72. }
  73. }
  74. extension WizardViewController {
  75. enum Item {
  76. case multipleAccountSwitch
  77. var title: String {
  78. return L10n.Scene.Wizard.newInMastodon
  79. }
  80. var description: String {
  81. switch self {
  82. case .multipleAccountSwitch:
  83. return L10n.Scene.Wizard.multipleAccountSwitchIntroDescription
  84. }
  85. }
  86. func markAsRead() {
  87. switch self {
  88. case .multipleAccountSwitch:
  89. UserDefaults.shared.didShowMultipleAccountSwitchWizard = true
  90. }
  91. }
  92. }
  93. }
  94. extension WizardViewController {
  95. func setup() {
  96. assert(delegate != nil, "need set delegate before use")
  97. guard !items.isEmpty else { return }
  98. backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  99. backgroundView.frame = view.bounds
  100. view.addSubview(backgroundView)
  101. }
  102. func destroy() {
  103. view.removeFromSuperview()
  104. }
  105. func consume() {
  106. guard !items.isEmpty else {
  107. destroy()
  108. return
  109. }
  110. guard let first = items.first else { return }
  111. guard delegate?.readyToLayoutItem(self, item: first) == true else {
  112. pendingItem.value = first
  113. return
  114. }
  115. pendingItem.value = nil
  116. currentItem.value = nil
  117. let item = items.removeFirst()
  118. perform(item: item)
  119. }
  120. private func perform(item: Item) {
  121. guard let delegate = delegate else {
  122. assertionFailure()
  123. return
  124. }
  125. // prepare for reuse
  126. prepareForReuse()
  127. // set wizard item read
  128. item.markAsRead()
  129. // add spotlight
  130. let spotlight = delegate.layoutSpotlight(self, item: item)
  131. let maskLayer = CAShapeLayer()
  132. // expand rect to make sure view always fill the screen when device rotate
  133. let expandRect: CGRect = {
  134. var rect = backgroundView.bounds
  135. rect.size.width *= 2
  136. rect.size.height *= 2
  137. return rect
  138. }()
  139. let path = UIBezierPath(rect: expandRect)
  140. path.append(spotlight)
  141. maskLayer.fillRule = .evenOdd
  142. maskLayer.path = path.cgPath
  143. backgroundView.layer.mask = maskLayer
  144. // layout wizard card
  145. delegate.layoutWizardCard(self, item: item)
  146. currentItem.value = item
  147. }
  148. private func prepareForReuse() {
  149. backgroundView.subviews.forEach { subview in
  150. subview.removeFromSuperview()
  151. }
  152. backgroundView.mask = nil
  153. backgroundView.layer.mask = nil
  154. }
  155. private func invalidLayoutForCurrentItem() {
  156. if let item = currentItem.value {
  157. perform(item: item)
  158. }
  159. }
  160. }
  161. extension WizardViewController {
  162. @objc private func backgroundTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
  163. logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
  164. consume()
  165. }
  166. }