SceneCoordinator.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. //
  2. // SceneCoordinator.swift
  3. // Mastodon
  4. //
  5. // Created by Cirno MainasuK on 2021-1-27.
  6. import UIKit
  7. import Combine
  8. import SafariServices
  9. import CoreDataStack
  10. import PanModal
  11. import MastodonSDK
  12. import MastodonCore
  13. import MastodonAsset
  14. import MastodonLocalization
  15. final public class SceneCoordinator {
  16. private var disposeBag = Set<AnyCancellable>()
  17. private weak var scene: UIScene!
  18. private weak var sceneDelegate: SceneDelegate!
  19. private(set) weak var appContext: AppContext!
  20. private(set) var authContext: AuthContext?
  21. let id = UUID().uuidString
  22. private(set) weak var tabBarController: MainTabBarController!
  23. private(set) weak var splitViewController: RootSplitViewController?
  24. private(set) var wizardViewController: WizardViewController?
  25. private(set) var secondaryStackHashValues = Set<Int>()
  26. init(
  27. scene: UIScene,
  28. sceneDelegate: SceneDelegate,
  29. appContext: AppContext
  30. ) {
  31. self.scene = scene
  32. self.sceneDelegate = sceneDelegate
  33. self.appContext = appContext
  34. scene.session.sceneCoordinator = self
  35. appContext.notificationService.requestRevealNotificationPublisher
  36. .receive(on: DispatchQueue.main)
  37. .sink(receiveValue: { [weak self] pushNotification in
  38. guard let self = self else { return }
  39. Task {
  40. guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return }
  41. let accessToken = pushNotification.accessToken // use raw accessToken value without normalize
  42. if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
  43. // do nothing if notification for current account
  44. return
  45. } else {
  46. // switch to notification's account
  47. let request = MastodonAuthentication.sortedFetchRequest
  48. request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
  49. request.returnsObjectsAsFaults = false
  50. request.fetchLimit = 1
  51. do {
  52. guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
  53. return
  54. }
  55. let domain = authentication.domain
  56. let userID = authentication.userID
  57. let isSuccess = try await appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID)
  58. guard isSuccess else { return }
  59. self.setup()
  60. try await Task.sleep(nanoseconds: .second * 1)
  61. // redirect to notification tab
  62. self.switchToTabBar(tab: .notification)
  63. // Delay in next run loop
  64. DispatchQueue.main.async { [weak self] in
  65. guard let self = self else { return }
  66. // Note:
  67. // show (push) on phone and pad
  68. let from: UIViewController? = {
  69. if let splitViewController = self.splitViewController {
  70. if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil {
  71. // compact
  72. return splitViewController.compactMainTabBarViewController.topMost
  73. } else {
  74. // expand
  75. return splitViewController.contentSplitViewController.mainTabBarController.topMost
  76. }
  77. } else {
  78. return self.tabBarController.topMost
  79. }
  80. }()
  81. // show notification related content
  82. guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
  83. guard let authContext = self.authContext else { return }
  84. let notificationID = String(pushNotification.notificationID)
  85. switch type {
  86. case .follow:
  87. let profileViewModel = RemoteProfileViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
  88. _ = self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
  89. case .followRequest:
  90. // do nothing
  91. break
  92. case .mention, .reblog, .favourite, .poll, .status:
  93. let threadViewModel = RemoteThreadViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
  94. _ = self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
  95. case ._other:
  96. assertionFailure()
  97. break
  98. }
  99. } // end DispatchQueue.main.async
  100. } catch {
  101. assertionFailure(error.localizedDescription)
  102. return
  103. }
  104. }
  105. } // end Task
  106. })
  107. .store(in: &disposeBag)
  108. }
  109. }
  110. extension SceneCoordinator {
  111. enum Transition {
  112. case show // push
  113. case showDetail // replace
  114. case modal(animated: Bool, completion: (() -> Void)? = nil)
  115. case popover(sourceView: UIView)
  116. case panModal
  117. case custom(transitioningDelegate: UIViewControllerTransitioningDelegate)
  118. case customPush(animated: Bool)
  119. case safariPresent(animated: Bool, completion: (() -> Void)? = nil)
  120. case alertController(animated: Bool, completion: (() -> Void)? = nil)
  121. case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil)
  122. }
  123. enum Scene {
  124. // onboarding
  125. case welcome
  126. case mastodonPickServer(viewMode: MastodonPickServerViewModel)
  127. case mastodonRegister(viewModel: MastodonRegisterViewModel)
  128. case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
  129. case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
  130. case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
  131. case mastodonWebView(viewModel: WebViewModel)
  132. case mastodonLogin
  133. // search
  134. case searchDetail(viewModel: SearchDetailViewModel)
  135. // compose
  136. case compose(viewModel: ComposeViewModel)
  137. // thread
  138. case thread(viewModel: ThreadViewModel)
  139. // Hashtag Timeline
  140. case hashtagTimeline(viewModel: HashtagTimelineViewModel)
  141. // profile
  142. case accountList(viewModel: AccountListViewModel)
  143. case profile(viewModel: ProfileViewModel)
  144. case favorite(viewModel: FavoriteViewModel)
  145. case follower(viewModel: FollowerListViewModel)
  146. case following(viewModel: FollowingListViewModel)
  147. case familiarFollowers(viewModel: FamiliarFollowersViewModel)
  148. case rebloggedBy(viewModel: UserListViewModel)
  149. case favoritedBy(viewModel: UserListViewModel)
  150. case bookmark(viewModel: BookmarkViewModel)
  151. // setting
  152. case settings(viewModel: SettingsViewModel)
  153. // report
  154. case report(viewModel: ReportViewModel)
  155. case reportServerRules(viewModel: ReportServerRulesViewModel)
  156. case reportStatus(viewModel: ReportStatusViewModel)
  157. case reportSupplementary(viewModel: ReportSupplementaryViewModel)
  158. case reportResult(viewModel: ReportResultViewModel)
  159. // suggestion account
  160. case suggestionAccount(viewModel: SuggestionAccountViewModel)
  161. // media preview
  162. case mediaPreview(viewModel: MediaPreviewViewModel)
  163. // misc
  164. case safari(url: URL)
  165. case alertController(alertController: UIAlertController)
  166. case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
  167. var isOnboarding: Bool {
  168. switch self {
  169. case .welcome,
  170. .mastodonPickServer,
  171. .mastodonRegister,
  172. .mastodonLogin,
  173. .mastodonServerRules,
  174. .mastodonConfirmEmail,
  175. .mastodonResendEmail:
  176. return true
  177. default:
  178. return false
  179. }
  180. }
  181. } // end enum Scene { }
  182. }
  183. extension SceneCoordinator {
  184. func setup() {
  185. let rootViewController: UIViewController
  186. do {
  187. let request = MastodonAuthentication.activeSortedFetchRequest // use active order
  188. let _authentication = try appContext.managedObjectContext.fetch(request).first
  189. let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
  190. self.authContext = _authContext
  191. switch UIDevice.current.userInterfaceIdiom {
  192. case .phone:
  193. let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext)
  194. self.splitViewController = nil
  195. self.tabBarController = viewController
  196. rootViewController = viewController
  197. default:
  198. let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext)
  199. self.splitViewController = splitViewController
  200. self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
  201. rootViewController = splitViewController
  202. }
  203. sceneDelegate.window?.rootViewController = rootViewController // base: main
  204. if _authContext == nil { // entry #1: welcome
  205. DispatchQueue.main.async {
  206. _ = self.present(
  207. scene: .welcome,
  208. from: self.sceneDelegate.window?.rootViewController,
  209. transition: .modal(animated: true, completion: nil)
  210. )
  211. }
  212. } else {
  213. let wizardViewController = WizardViewController()
  214. if !wizardViewController.items.isEmpty,
  215. let delegate = rootViewController as? WizardViewControllerDelegate
  216. {
  217. // do not add as child view controller.
  218. // otherwise, the tab bar controller will add as a new tab
  219. wizardViewController.delegate = delegate
  220. wizardViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  221. wizardViewController.view.frame = rootViewController.view.bounds
  222. rootViewController.view.addSubview(wizardViewController.view)
  223. self.wizardViewController = wizardViewController
  224. }
  225. }
  226. } catch {
  227. assertionFailure(error.localizedDescription)
  228. Task {
  229. try? await Task.sleep(nanoseconds: .second * 2)
  230. setup() // entry #2: retry
  231. } // end Task
  232. }
  233. }
  234. @MainActor
  235. @discardableResult
  236. func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? {
  237. guard let viewController = get(scene: scene) else {
  238. return nil
  239. }
  240. guard var presentingViewController = sender ?? sceneDelegate.window?.rootViewController?.topMost else {
  241. return nil
  242. }
  243. // adapt for child controller
  244. if let navigationControllerVisibleViewController = presentingViewController.navigationController?.visibleViewController {
  245. switch viewController {
  246. case is ProfileViewController:
  247. let title: String = {
  248. let title = navigationControllerVisibleViewController.navigationItem.title ?? ""
  249. return title.count > 10 ? "" : title
  250. }()
  251. let barButtonItem = UIBarButtonItem(title: title, style: .plain, target: nil, action: nil)
  252. barButtonItem.tintColor = .white
  253. navigationControllerVisibleViewController.navigationItem.backBarButtonItem = barButtonItem
  254. default:
  255. navigationControllerVisibleViewController.navigationItem.backBarButtonItem = nil
  256. }
  257. }
  258. if let mainTabBarController = presentingViewController as? MainTabBarController,
  259. let navigationController = mainTabBarController.selectedViewController as? UINavigationController,
  260. let topViewController = navigationController.topViewController {
  261. presentingViewController = topViewController
  262. }
  263. switch transition {
  264. case .show:
  265. presentingViewController.show(viewController, sender: sender)
  266. case .showDetail:
  267. secondaryStackHashValues.insert(viewController.hashValue)
  268. let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
  269. presentingViewController.showDetailViewController(navigationController, sender: sender)
  270. case .modal(let animated, let completion):
  271. let modalNavigationController: UINavigationController = {
  272. if scene.isOnboarding {
  273. return OnboardingNavigationController(rootViewController: viewController)
  274. } else {
  275. return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
  276. }
  277. }()
  278. modalNavigationController.modalPresentationCapturesStatusBarAppearance = true
  279. if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate {
  280. modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate
  281. }
  282. presentingViewController.present(modalNavigationController, animated: animated, completion: completion)
  283. case .panModal:
  284. guard let panModalPresentable = viewController as? PanModalPresentable & UIViewController else {
  285. assertionFailure()
  286. return nil
  287. }
  288. // https://github.com/slackhq/PanModal/issues/74#issuecomment-572426441
  289. panModalPresentable.modalPresentationStyle = .custom
  290. panModalPresentable.modalPresentationCapturesStatusBarAppearance = true
  291. panModalPresentable.transitioningDelegate = PanModalPresentationDelegate.default
  292. presentingViewController.present(panModalPresentable, animated: true, completion: nil)
  293. //presentingViewController.presentPanModal(panModalPresentable)
  294. case .popover(let sourceView):
  295. viewController.modalPresentationStyle = .popover
  296. viewController.popoverPresentationController?.sourceView = sourceView
  297. (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil)
  298. case .custom(let transitioningDelegate):
  299. viewController.modalPresentationStyle = .custom
  300. viewController.transitioningDelegate = transitioningDelegate
  301. // viewController.modalPresentationCapturesStatusBarAppearance = true
  302. (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil)
  303. case .customPush(let animated):
  304. // set delegate in view controller
  305. assert(sender?.navigationController?.delegate != nil)
  306. sender?.navigationController?.pushViewController(viewController, animated: animated)
  307. case .safariPresent(let animated, let completion):
  308. if UserDefaults.shared.preferredUsingDefaultBrowser, case let .safari(url) = scene {
  309. UIApplication.shared.open(url, options: [:], completionHandler: nil)
  310. } else {
  311. viewController.modalPresentationCapturesStatusBarAppearance = true
  312. presentingViewController.present(viewController, animated: animated, completion: completion)
  313. }
  314. case .alertController(let animated, let completion):
  315. viewController.modalPresentationCapturesStatusBarAppearance = true
  316. presentingViewController.present(viewController, animated: animated, completion: completion)
  317. case .activityViewControllerPresent(let animated, let completion):
  318. viewController.modalPresentationCapturesStatusBarAppearance = true
  319. presentingViewController.present(viewController, animated: animated, completion: completion)
  320. }
  321. return viewController
  322. }
  323. func switchToTabBar(tab: MainTabBarController.Tab) {
  324. splitViewController?.contentSplitViewController.currentSupplementaryTab = tab
  325. splitViewController?.compactMainTabBarViewController.selectedIndex = tab.rawValue
  326. splitViewController?.compactMainTabBarViewController.currentTab = tab
  327. tabBarController.selectedIndex = tab.rawValue
  328. tabBarController.currentTab = tab
  329. }
  330. }
  331. private extension SceneCoordinator {
  332. func get(scene: Scene) -> UIViewController? {
  333. let viewController: UIViewController?
  334. switch scene {
  335. case .welcome:
  336. let _viewController = WelcomeViewController()
  337. viewController = _viewController
  338. case .mastodonPickServer(let viewModel):
  339. let _viewController = MastodonPickServerViewController()
  340. _viewController.viewModel = viewModel
  341. viewController = _viewController
  342. case .mastodonRegister(let viewModel):
  343. let _viewController = MastodonRegisterViewController()
  344. _viewController.viewModel = viewModel
  345. viewController = _viewController
  346. case .mastodonServerRules(let viewModel):
  347. let _viewController = MastodonServerRulesViewController()
  348. _viewController.viewModel = viewModel
  349. viewController = _viewController
  350. case .mastodonConfirmEmail(let viewModel):
  351. let _viewController = MastodonConfirmEmailViewController()
  352. _viewController.viewModel = viewModel
  353. viewController = _viewController
  354. case .mastodonLogin:
  355. let loginViewController = MastodonLoginViewController(appContext: appContext,
  356. authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false),
  357. sceneCoordinator: self)
  358. loginViewController.delegate = self
  359. viewController = loginViewController
  360. case .mastodonResendEmail(let viewModel):
  361. let _viewController = MastodonResendEmailViewController()
  362. _viewController.viewModel = viewModel
  363. viewController = _viewController
  364. case .mastodonWebView(let viewModel):
  365. let _viewController = WebViewController()
  366. _viewController.viewModel = viewModel
  367. viewController = _viewController
  368. case .searchDetail(let viewModel):
  369. let _viewController = SearchDetailViewController()
  370. _viewController.viewModel = viewModel
  371. viewController = _viewController
  372. case .compose(let viewModel):
  373. let _viewController = ComposeViewController()
  374. _viewController.viewModel = viewModel
  375. viewController = _viewController
  376. case .thread(let viewModel):
  377. let _viewController = ThreadViewController()
  378. _viewController.viewModel = viewModel
  379. viewController = _viewController
  380. case .hashtagTimeline(let viewModel):
  381. let _viewController = HashtagTimelineViewController()
  382. _viewController.viewModel = viewModel
  383. viewController = _viewController
  384. case .accountList(let viewModel):
  385. let _viewController = AccountListViewController()
  386. _viewController.viewModel = viewModel
  387. viewController = _viewController
  388. case .profile(let viewModel):
  389. let _viewController = ProfileViewController()
  390. _viewController.viewModel = viewModel
  391. viewController = _viewController
  392. case .bookmark(let viewModel):
  393. let _viewController = BookmarkViewController()
  394. _viewController.viewModel = viewModel
  395. viewController = _viewController
  396. case .favorite(let viewModel):
  397. let _viewController = FavoriteViewController()
  398. _viewController.viewModel = viewModel
  399. viewController = _viewController
  400. case .follower(let viewModel):
  401. let _viewController = FollowerListViewController()
  402. _viewController.viewModel = viewModel
  403. viewController = _viewController
  404. case .following(let viewModel):
  405. let _viewController = FollowingListViewController()
  406. _viewController.viewModel = viewModel
  407. viewController = _viewController
  408. case .familiarFollowers(let viewModel):
  409. let _viewController = FamiliarFollowersViewController()
  410. _viewController.viewModel = viewModel
  411. viewController = _viewController
  412. case .rebloggedBy(let viewModel):
  413. let _viewController = RebloggedByViewController()
  414. _viewController.viewModel = viewModel
  415. viewController = _viewController
  416. case .favoritedBy(let viewModel):
  417. let _viewController = FavoritedByViewController()
  418. _viewController.viewModel = viewModel
  419. viewController = _viewController
  420. case .report(let viewModel):
  421. let _viewController = ReportViewController()
  422. _viewController.viewModel = viewModel
  423. viewController = _viewController
  424. case .reportServerRules(let viewModel):
  425. let _viewController = ReportServerRulesViewController()
  426. _viewController.viewModel = viewModel
  427. viewController = _viewController
  428. case .reportStatus(let viewModel):
  429. let _viewController = ReportStatusViewController()
  430. _viewController.viewModel = viewModel
  431. viewController = _viewController
  432. case .reportSupplementary(let viewModel):
  433. let _viewController = ReportSupplementaryViewController()
  434. _viewController.viewModel = viewModel
  435. viewController = _viewController
  436. case .reportResult(let viewModel):
  437. let _viewController = ReportResultViewController()
  438. _viewController.viewModel = viewModel
  439. viewController = _viewController
  440. case .suggestionAccount(let viewModel):
  441. let _viewController = SuggestionAccountViewController()
  442. _viewController.viewModel = viewModel
  443. viewController = _viewController
  444. case .mediaPreview(let viewModel):
  445. let _viewController = MediaPreviewViewController()
  446. _viewController.viewModel = viewModel
  447. viewController = _viewController
  448. case .safari(let url):
  449. guard let scheme = url.scheme?.lowercased(),
  450. scheme == "http" || scheme == "https" else {
  451. return nil
  452. }
  453. let _viewController = SFSafariViewController(url: url)
  454. _viewController.preferredBarTintColor = ThemeService.shared.currentTheme.value.navigationBarBackgroundColor
  455. _viewController.preferredControlTintColor = Asset.Colors.brand.color
  456. viewController = _viewController
  457. case .alertController(let alertController):
  458. if let popoverPresentationController = alertController.popoverPresentationController {
  459. assert(
  460. popoverPresentationController.sourceView != nil ||
  461. popoverPresentationController.sourceRect != .zero ||
  462. popoverPresentationController.barButtonItem != nil
  463. )
  464. }
  465. viewController = alertController
  466. case .activityViewController(let activityViewController, let sourceView, let barButtonItem):
  467. activityViewController.popoverPresentationController?.sourceView = sourceView
  468. activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
  469. viewController = activityViewController
  470. case .settings(let viewModel):
  471. let _viewController = SettingsViewController()
  472. _viewController.viewModel = viewModel
  473. viewController = _viewController
  474. }
  475. setupDependency(for: viewController as? NeedsDependency)
  476. return viewController
  477. }
  478. private func setupDependency(for needs: NeedsDependency?) {
  479. needs?.context = appContext
  480. needs?.coordinator = self
  481. }
  482. }
  483. //MARK: - MastodonLoginViewControllerDelegate
  484. extension SceneCoordinator: MastodonLoginViewControllerDelegate {
  485. func backButtonPressed(_ viewController: MastodonLoginViewController) {
  486. viewController.navigationController?.popViewController(animated: true)
  487. }
  488. func nextButtonPressed(_ viewController: MastodonLoginViewController) {
  489. viewController.login()
  490. }
  491. }