mastodon-ios/Mastodon/Coordinator/SceneCoordinator.swift

537 lines
25 KiB
Swift
Raw Normal View History

2021-01-27 07:50:13 +01:00
//
// SceneCoordinator.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-1-27.
import UIKit
import Combine
2021-01-27 07:50:13 +01:00
import SafariServices
2021-02-20 06:56:24 +01:00
import CoreDataStack
import MastodonSDK
2021-09-14 12:21:15 +02:00
import PanModal
import MastodonAsset
import MastodonLocalization
2021-01-27 07:50:13 +01:00
final public class SceneCoordinator {
private var disposeBag = Set<AnyCancellable>()
2021-01-27 07:50:13 +01:00
private weak var scene: UIScene!
private weak var sceneDelegate: SceneDelegate!
private weak var appContext: AppContext!
let id = UUID().uuidString
private(set) weak var tabBarController: MainTabBarController!
private(set) weak var splitViewController: RootSplitViewController?
private(set) var wizardViewController: WizardViewController?
private(set) var secondaryStackHashValues = Set<Int>()
2021-01-27 07:50:13 +01:00
init(scene: UIScene, sceneDelegate: SceneDelegate, appContext: AppContext) {
self.scene = scene
self.sceneDelegate = sceneDelegate
self.appContext = appContext
scene.session.sceneCoordinator = self
appContext.notificationService.requestRevealNotificationPublisher
.receive(on: DispatchQueue.main)
.compactMap { [weak self] pushNotification -> AnyPublisher<MastodonPushNotification?, Never> in
guard let self = self else { return Just(nil).eraseToAnyPublisher() }
// skip if no available account
guard let currentActiveAuthenticationBox = appContext.authenticationService.activeMastodonAuthenticationBox.value else {
return Just(nil).eraseToAnyPublisher()
}
let accessToken = pushNotification.accessToken // use raw accessToken value without normalize
if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
// do nothing if notification for current account
return Just(pushNotification).eraseToAnyPublisher()
} else {
// switch to notification's account
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
request.returnsObjectsAsFaults = false
request.fetchLimit = 1
do {
guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
return Just(nil).eraseToAnyPublisher()
}
let domain = authentication.domain
let userID = authentication.userID
return appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID)
.receive(on: DispatchQueue.main)
.map { [weak self] result -> MastodonPushNotification? in
guard let self = self else { return nil }
switch result {
case .success:
// reset view hierarchy
self.setup()
return pushNotification
case .failure:
return nil
}
}
.delay(for: 1, scheduler: DispatchQueue.main) // set delay to slow transition (not must)
.eraseToAnyPublisher()
} catch {
assertionFailure(error.localizedDescription)
return Just(nil).eraseToAnyPublisher()
}
}
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.sink { [weak self] pushNotification in
guard let self = self else { return }
guard let pushNotification = pushNotification else { return }
// redirect to notification tab
self.switchToTabBar(tab: .notification)
// Delay in next run loop
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Note:
// show (push) on phone and pad
let from: UIViewController? = {
if let splitViewController = self.splitViewController {
if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil {
// compact
return splitViewController.compactMainTabBarViewController.topMost
} else {
// expand
return splitViewController.contentSplitViewController.mainTabBarController.topMost
}
} else {
return self.tabBarController.topMost
}
}()
// show notification related content
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
let notificationID = String(pushNotification.notificationID)
switch type {
case .follow:
let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID)
self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
case .followRequest:
// do nothing
break
case .mention, .reblog, .favourite, .poll, .status:
let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID)
self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
case ._other:
assertionFailure()
break
}
} // end DispatchQueue.main.async
}
.store(in: &disposeBag)
2021-01-27 07:50:13 +01:00
}
}
extension SceneCoordinator {
enum Transition {
case show // push
case showDetail // replace
case modal(animated: Bool, completion: (() -> Void)? = nil)
case popover(sourceView: UIView)
2021-09-14 12:21:15 +02:00
case panModal
2021-01-27 07:50:13 +01:00
case custom(transitioningDelegate: UIViewControllerTransitioningDelegate)
case customPush(animated: Bool)
2021-01-27 07:50:13 +01:00
case safariPresent(animated: Bool, completion: (() -> Void)? = nil)
case alertController(animated: Bool, completion: (() -> Void)? = nil)
case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil)
2021-01-27 07:50:13 +01:00
}
enum Scene {
2021-02-26 11:27:47 +01:00
// onboarding
2021-02-20 06:56:24 +01:00
case welcome
2021-02-26 11:27:47 +01:00
case mastodonPickServer(viewMode: MastodonPickServerViewModel)
2021-02-05 10:53:00 +01:00
case mastodonRegister(viewModel: MastodonRegisterViewModel)
2021-02-22 09:20:44 +01:00
case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
2021-02-23 08:38:05 +01:00
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
case mastodonWebView(viewModel: WebViewModel)
// search
case searchDetail(viewModel: SearchDetailViewModel)
2021-03-11 08:41:27 +01:00
// compose
case compose(viewModel: ComposeViewModel)
2021-04-13 13:46:42 +02:00
// thread
case thread(viewModel: ThreadViewModel)
2021-04-01 04:12:57 +02:00
// Hashtag Timeline
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
2021-04-01 08:39:15 +02:00
// profile
2021-09-14 12:21:15 +02:00
case accountList
2021-04-01 08:39:15 +02:00
case profile(viewModel: ProfileViewModel)
2021-04-07 08:24:28 +02:00
case favorite(viewModel: FavoriteViewModel)
2021-11-01 12:54:07 +01:00
case follower(viewModel: FollowerListViewModel)
2021-11-02 07:56:42 +01:00
case following(viewModel: FollowingListViewModel)
2022-05-17 12:49:29 +02:00
case familiarFollowers(viewModel: FamiliarFollowersViewModel)
case rebloggedBy(viewModel: UserListViewModel)
case favoritedBy(viewModel: UserListViewModel)
case bookmark(viewModel: BookmarkViewModel)
2021-09-14 12:21:15 +02:00
// setting
case settings(viewModel: SettingsViewModel)
// report
case report(viewModel: ReportViewModel)
case reportServerRules(viewModel: ReportServerRulesViewModel)
case reportStatus(viewModel: ReportStatusViewModel)
2022-02-08 05:36:06 +01:00
case reportSupplementary(viewModel: ReportSupplementaryViewModel)
case reportResult(viewModel: ReportResultViewModel)
2021-04-21 08:46:31 +02:00
// suggestion account
case suggestionAccount(viewModel: SuggestionAccountViewModel)
// media preview
case mediaPreview(viewModel: MediaPreviewViewModel)
2021-02-26 11:27:47 +01:00
// misc
case safari(url: URL)
case alertController(alertController: UIAlertController)
case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
2021-02-26 11:27:47 +01:00
var isOnboarding: Bool {
switch self {
case .welcome,
.mastodonPickServer,
.mastodonRegister,
.mastodonServerRules,
.mastodonConfirmEmail,
.mastodonResendEmail:
return true
default:
return false
}
}
} // end enum Scene { }
2021-01-27 07:50:13 +01:00
}
extension SceneCoordinator {
func setup() {
let rootViewController: UIViewController
2021-09-24 13:58:50 +02:00
switch UIDevice.current.userInterfaceIdiom {
case .phone:
let viewController = MainTabBarController(context: appContext, coordinator: self)
self.splitViewController = nil
self.tabBarController = viewController
rootViewController = viewController
2021-09-24 13:58:50 +02:00
default:
let splitViewController = RootSplitViewController(context: appContext, coordinator: self)
self.splitViewController = splitViewController
2021-10-28 13:17:41 +02:00
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
rootViewController = splitViewController
2021-09-24 13:58:50 +02:00
}
let wizardViewController = WizardViewController()
if !wizardViewController.items.isEmpty,
let delegate = rootViewController as? WizardViewControllerDelegate
{
// do not add as child view controller.
// otherwise, the tab bar controller will add as a new tab
wizardViewController.delegate = delegate
wizardViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
wizardViewController.view.frame = rootViewController.view.bounds
rootViewController.view.addSubview(wizardViewController.view)
self.wizardViewController = wizardViewController
}
sceneDelegate.window?.rootViewController = rootViewController
2021-02-26 11:27:47 +01:00
}
func setupOnboardingIfNeeds(animated: Bool) {
// Check user authentication status and show onboarding if needs
2021-02-20 06:56:24 +01:00
do {
2021-02-26 11:27:47 +01:00
let request = MastodonAuthentication.sortedFetchRequest
if try appContext.managedObjectContext.count(for: request) == 0 {
2021-02-26 11:27:47 +01:00
DispatchQueue.main.async {
self.present(
scene: .welcome,
2021-12-31 09:47:13 +01:00
from: self.sceneDelegate.window?.rootViewController,
2021-02-26 11:27:47 +01:00
transition: .modal(animated: animated, completion: nil)
)
2021-02-20 06:56:24 +01:00
}
}
} catch {
2021-02-26 11:27:47 +01:00
assertionFailure(error.localizedDescription)
2021-02-20 06:56:24 +01:00
}
2021-01-27 07:50:13 +01:00
}
@discardableResult
@MainActor
2021-01-27 07:50:13 +01:00
func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? {
guard let viewController = get(scene: scene) else {
return nil
}
guard var presentingViewController = sender ?? sceneDelegate.window?.rootViewController?.topMost else {
return nil
}
// adapt for child controller
if let navigationControllerVisibleViewController = presentingViewController.navigationController?.visibleViewController {
switch viewController {
case is ProfileViewController:
let title: String = {
let title = navigationControllerVisibleViewController.navigationItem.title ?? ""
return title.count > 10 ? "" : title
}()
let barButtonItem = UIBarButtonItem(title: title, style: .plain, target: nil, action: nil)
barButtonItem.tintColor = .white
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = barButtonItem
default:
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = nil
}
}
2021-01-27 07:50:13 +01:00
if let mainTabBarController = presentingViewController as? MainTabBarController,
let navigationController = mainTabBarController.selectedViewController as? UINavigationController,
let topViewController = navigationController.topViewController {
presentingViewController = topViewController
}
switch transition {
case .show:
2021-10-28 13:17:41 +02:00
presentingViewController.show(viewController, sender: sender)
2021-01-27 07:50:13 +01:00
case .showDetail:
secondaryStackHashValues.insert(viewController.hashValue)
2021-04-01 08:39:15 +02:00
let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
2021-01-27 07:50:13 +01:00
presentingViewController.showDetailViewController(navigationController, sender: sender)
case .modal(let animated, let completion):
let modalNavigationController: UINavigationController = {
if scene.isOnboarding {
return OnboardingNavigationController(rootViewController: viewController)
} else {
2022-05-07 05:42:10 +02:00
return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
}
}()
modalNavigationController.modalPresentationCapturesStatusBarAppearance = true
2021-01-27 07:50:13 +01:00
if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate {
modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate
}
presentingViewController.present(modalNavigationController, animated: animated, completion: completion)
2021-09-14 12:21:15 +02:00
case .panModal:
guard let panModalPresentable = viewController as? PanModalPresentable & UIViewController else {
assertionFailure()
return nil
}
// https://github.com/slackhq/PanModal/issues/74#issuecomment-572426441
panModalPresentable.modalPresentationStyle = .custom
panModalPresentable.modalPresentationCapturesStatusBarAppearance = true
panModalPresentable.transitioningDelegate = PanModalPresentationDelegate.default
presentingViewController.present(panModalPresentable, animated: true, completion: nil)
//presentingViewController.presentPanModal(panModalPresentable)
case .popover(let sourceView):
viewController.modalPresentationStyle = .popover
viewController.popoverPresentationController?.sourceView = sourceView
(splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil)
2021-01-27 07:50:13 +01:00
case .custom(let transitioningDelegate):
viewController.modalPresentationStyle = .custom
viewController.transitioningDelegate = transitioningDelegate
// viewController.modalPresentationCapturesStatusBarAppearance = true
(splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil)
2021-01-27 07:50:13 +01:00
case .customPush(let animated):
2021-01-27 07:50:13 +01:00
// set delegate in view controller
assert(sender?.navigationController?.delegate != nil)
sender?.navigationController?.pushViewController(viewController, animated: animated)
2021-01-27 07:50:13 +01:00
case .safariPresent(let animated, let completion):
if UserDefaults.shared.preferredUsingDefaultBrowser, case let .safari(url) = scene {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
viewController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(viewController, animated: animated, completion: completion)
}
2021-01-27 07:50:13 +01:00
case .alertController(let animated, let completion):
2021-04-01 08:39:15 +02:00
viewController.modalPresentationCapturesStatusBarAppearance = true
2021-01-27 07:50:13 +01:00
presentingViewController.present(viewController, animated: animated, completion: completion)
case .activityViewControllerPresent(let animated, let completion):
2021-04-01 08:39:15 +02:00
viewController.modalPresentationCapturesStatusBarAppearance = true
2021-01-27 07:50:13 +01:00
presentingViewController.present(viewController, animated: animated, completion: completion)
}
return viewController
}
2021-04-21 08:46:31 +02:00
func switchToTabBar(tab: MainTabBarController.Tab) {
splitViewController?.contentSplitViewController.currentSupplementaryTab = tab
splitViewController?.compactMainTabBarViewController.selectedIndex = tab.rawValue
2022-05-06 08:29:34 +02:00
splitViewController?.compactMainTabBarViewController.currentTab = tab
tabBarController.selectedIndex = tab.rawValue
2022-05-06 08:29:34 +02:00
tabBarController.currentTab = tab
2021-04-21 08:46:31 +02:00
}
2021-01-27 07:50:13 +01:00
}
private extension SceneCoordinator {
func get(scene: Scene) -> UIViewController? {
let viewController: UIViewController?
2021-02-02 08:38:54 +01:00
switch scene {
2021-02-20 06:56:24 +01:00
case .welcome:
let _viewController = WelcomeViewController()
viewController = _viewController
2021-02-26 11:27:47 +01:00
case .mastodonPickServer(let viewModel):
let _viewController = MastodonPickServerViewController()
2021-02-02 08:38:54 +01:00
_viewController.viewModel = viewModel
viewController = _viewController
2021-02-05 10:53:00 +01:00
case .mastodonRegister(let viewModel):
let _viewController = MastodonRegisterViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2021-02-22 09:20:44 +01:00
case .mastodonServerRules(let viewModel):
let _viewController = MastodonServerRulesViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2021-02-23 08:38:05 +01:00
case .mastodonConfirmEmail(let viewModel):
let _viewController = MastodonConfirmEmailViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .mastodonResendEmail(let viewModel):
let _viewController = MastodonResendEmailViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .mastodonWebView(let viewModel):
let _viewController = WebViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .searchDetail(let viewModel):
let _viewController = SearchDetailViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2021-03-11 08:41:27 +01:00
case .compose(let viewModel):
let _viewController = ComposeViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2021-04-13 13:46:42 +02:00
case .thread(let viewModel):
let _viewController = ThreadViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .hashtagTimeline(let viewModel):
let _viewController = HashtagTimelineViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2021-09-14 12:21:15 +02:00
case .accountList:
let _viewController = AccountListViewController()
viewController = _viewController
2021-04-01 08:39:15 +02:00
case .profile(let viewModel):
let _viewController = ProfileViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .bookmark(let viewModel):
let _viewController = BookmarkViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2021-04-07 08:24:28 +02:00
case .favorite(let viewModel):
let _viewController = FavoriteViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2021-11-01 12:54:07 +01:00
case .follower(let viewModel):
let _viewController = FollowerListViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2021-11-02 07:56:42 +01:00
case .following(let viewModel):
let _viewController = FollowingListViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2022-05-17 12:49:29 +02:00
case .familiarFollowers(let viewModel):
let _viewController = FamiliarFollowersViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .rebloggedBy(let viewModel):
let _viewController = RebloggedByViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .favoritedBy(let viewModel):
let _viewController = FavoritedByViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2022-02-08 05:36:06 +01:00
case .report(let viewModel):
let _viewController = ReportViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .reportServerRules(let viewModel):
let _viewController = ReportServerRulesViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .reportStatus(let viewModel):
let _viewController = ReportStatusViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2022-02-08 05:36:06 +01:00
case .reportSupplementary(let viewModel):
let _viewController = ReportSupplementaryViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .reportResult(let viewModel):
let _viewController = ReportResultViewController()
_viewController.viewModel = viewModel
viewController = _viewController
2021-04-21 08:46:31 +02:00
case .suggestionAccount(let viewModel):
let _viewController = SuggestionAccountViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .mediaPreview(let viewModel):
let _viewController = MediaPreviewViewController()
_viewController.viewModel = viewModel
2021-04-21 08:46:31 +02:00
viewController = _viewController
case .safari(let url):
guard let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https" else {
return nil
}
let _viewController = SFSafariViewController(url: url)
2021-07-05 10:07:17 +02:00
_viewController.preferredBarTintColor = ThemeService.shared.currentTheme.value.navigationBarBackgroundColor
2022-06-02 11:31:23 +02:00
_viewController.preferredControlTintColor = Asset.Colors.brand.color
viewController = _viewController
2021-02-03 09:01:08 +01:00
case .alertController(let alertController):
if let popoverPresentationController = alertController.popoverPresentationController {
assert(
popoverPresentationController.sourceView != nil ||
popoverPresentationController.sourceRect != .zero ||
popoverPresentationController.barButtonItem != nil
)
}
viewController = alertController
case .activityViewController(let activityViewController, let sourceView, let barButtonItem):
activityViewController.popoverPresentationController?.sourceView = sourceView
activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
viewController = activityViewController
case .settings(let viewModel):
2021-04-19 14:34:08 +02:00
let _viewController = SettingsViewController()
_viewController.viewModel = viewModel
2021-04-19 14:34:08 +02:00
viewController = _viewController
2021-02-02 08:38:54 +01:00
}
2021-01-27 07:50:13 +01:00
setupDependency(for: viewController as? NeedsDependency)
return viewController
}
private func setupDependency(for needs: NeedsDependency?) {
needs?.context = appContext
needs?.coordinator = self
}
}