deep-linking-sample/Apps/Wikipedia/Wikipedia/Code/NavigationStateController.swift
Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

322 lines
16 KiB
Swift

import WMF
import UIKit
import CocoaLumberjackSwift
protocol DetailPresentingFromContentGroup {
var contentGroupIDURIString: String? { get }
}
@objc(WMFNavigationStateController)
final class NavigationStateController: NSObject {
private let dataStore: MWKDataStore
private var theme = Theme.standard
private weak var settingsNavController: UINavigationController?
@objc init(dataStore: MWKDataStore) {
self.dataStore = dataStore
super.init()
}
private typealias ViewController = NavigationState.ViewController
private typealias Presentation = ViewController.Presentation
private typealias Info = ViewController.Info
@objc func restoreNavigationState(for navigationController: UINavigationController, in moc: NSManagedObjectContext, with theme: Theme, completion: @escaping () -> Void) {
guard let tabBarController = navigationController.viewControllers.first as? UITabBarController else {
assertionFailure("Expected root view controller to be UITabBarController")
completion()
return
}
guard let navigationState = moc.navigationState else {
completion()
return
}
self.theme = theme
let restore = {
completion()
for viewController in navigationState.viewControllers {
self.restore(viewController: viewController, for: tabBarController, navigationController: navigationController, in: moc)
}
}
if navigationState.shouldAttemptLogin {
dataStore.authenticationManager.attemptLogin {
restore()
}
} else {
restore()
}
}
func allPreservedArticleKeys(in moc: NSManagedObjectContext) -> [String]? {
return moc.navigationState?.viewControllers.compactMap { $0.info?.articleKey }
}
private func pushOrPresent(_ viewController: UIViewController & Themeable, navigationController: UINavigationController, presentation: Presentation, animated: Bool = false) {
viewController.apply(theme: theme)
switch presentation {
case .push:
navigationController.pushViewController(viewController, animated: animated)
case .modal:
viewController.modalPresentationStyle = .overFullScreen
navigationController.present(viewController, animated: animated)
}
}
private func articleURL(from info: Info) -> URL? {
guard
let articleKey = info.articleKey,
var articleURL = URL(string: articleKey)
else {
return nil
}
if let sectionAnchor = info.articleSectionAnchor, let articleURLWithFragment = articleURL.wmf_URL(withFragment: sectionAnchor) {
articleURL = articleURLWithFragment
}
return articleURL
}
private func restore(viewController: ViewController, for tabBarController: UITabBarController, navigationController: UINavigationController, in moc: NSManagedObjectContext) {
var newNavigationController: UINavigationController?
if let info = viewController.info, let selectedIndex = info.selectedIndex {
tabBarController.selectedIndex = selectedIndex
switch tabBarController.selectedViewController {
case let savedViewController as SavedViewController:
guard let currentSavedViewRawValue = info.currentSavedViewRawValue else {
return
}
savedViewController.toggleCurrentView(currentSavedViewRawValue)
case let searchViewController as SearchViewController:
searchViewController.searchAndMakeResultsVisibleForSearchTerm(info.searchTerm, animated: false)
case let exploreViewController as ExploreViewController:
exploreViewController.presentedContentGroupKey = info.presentedContentGroupKey
exploreViewController.shouldRestoreScrollPosition = true
default:
break
}
} else {
switch (viewController.kind, viewController.info) {
case (.random, let info?) :
guard
let articleURL = articleURL(from: info),
let randomArticleVC = RandomArticleViewController(articleURL: articleURL, dataStore: dataStore, theme: theme)
else {
return
}
pushOrPresent(randomArticleVC, navigationController: navigationController, presentation: viewController.presentation)
case (.article, let info?):
guard let articleURL = articleURL(from: info) else {
return
}
guard let articleVC = ArticleViewController(articleURL: articleURL, dataStore: dataStore, theme: theme) else {
return
}
articleVC.isRestoringState = true
// never present an article modal, the nav bar disappears
pushOrPresent(articleVC, navigationController: navigationController, presentation: .push)
case (.themeableNavigationController, _):
let themeableNavigationController = WMFThemeableNavigationController()
pushOrPresent(themeableNavigationController, navigationController: navigationController, presentation: viewController.presentation)
newNavigationController = themeableNavigationController
case (.settings, _):
let settingsVC = WMFSettingsViewController(dataStore: dataStore)
self.settingsNavController = navigationController
pushOrPresent(settingsVC, navigationController: navigationController, presentation: viewController.presentation)
settingsVC.navigationController?.interactivePopGestureRecognizer?.delegate = self
case (.account, _):
let accountVC = AccountViewController()
accountVC.dataStore = dataStore
pushOrPresent(accountVC, navigationController: navigationController, presentation: viewController.presentation)
case (.talkPage, let info?):
guard
let siteURLString = info.talkPageSiteURLString,
let siteURL = URL(string: siteURLString),
let title = info.talkPageTitle,
let typeRawValue = info.talkPageTypeRawValue,
let type = OldTalkPageType(rawValue: typeRawValue)
else {
return
}
if FeatureFlags.needsNewTalkPage {
assertionFailure("Need to set up state restoration for new talk pages.")
return
} else {
let talkPageContainer = TalkPageContainerViewController.talkPageContainer(title: title, siteURL: siteURL, type: type, dataStore: dataStore, theme: theme)
navigationController.pushViewController(talkPageContainer, animated: false)
}
navigationController.isNavigationBarHidden = true
case (.talkPageReplyList, let info?):
if FeatureFlags.needsNewTalkPage {
DDLogDebug("Attempted to restore old talk page reply list. Ignoring.")
return
} else {
guard
let talkPageTopic = managedObject(with: info.contentGroupIDURIString, in: moc) as? TalkPageTopic,
let talkPageContainerVC = navigationController.viewControllers.last as? TalkPageContainerViewController
else {
return
}
talkPageContainerVC.pushToReplyThread(topic: talkPageTopic, animated: false)
}
case (.readingListDetail, let info?):
guard let readingList = managedObject(with: info.readingListURIString, in: moc) as? ReadingList else {
return
}
let readingListDetailVC = ReadingListDetailViewController(for: readingList, with: dataStore)
pushOrPresent(readingListDetailVC, navigationController: navigationController, presentation: viewController.presentation)
case (.detail, let info?):
guard
let contentGroup = managedObject(with: info.contentGroupIDURIString, in: moc) as? WMFContentGroup,
let detailVC = contentGroup.detailViewControllerWithDataStore(dataStore, theme: theme) as? UIViewController & Themeable
else {
return
}
if let onThisDayVC = detailVC as? OnThisDayViewController, let shouldShowNavigationBar = viewController.info?.shouldShowNavigationBar {
onThisDayVC.shouldShowNavigationBar = shouldShowNavigationBar
}
pushOrPresent(detailVC, navigationController: navigationController, presentation: viewController.presentation)
case (.singleWebPage, let info):
guard let url = info?.url else {
return
}
pushOrPresent(SinglePageWebViewController(url: url, theme: theme), navigationController: navigationController, presentation: .push)
default:
return
}
}
for child in viewController.children {
restore(viewController: child, for: tabBarController, navigationController: newNavigationController ?? navigationController, in: moc)
}
}
private func managedObject<T: NSManagedObject>(with uriString: String?, in moc: NSManagedObjectContext) -> T? {
guard
let uriString = uriString,
let uri = URL(string: uriString),
let id = moc.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: uri),
let object = try? moc.existingObject(with: id) as? T
else {
return nil
}
return object
}
var shouldAttemptLogin: Bool = false
@objc func saveNavigationState(for navigationController: UINavigationController, in moc: NSManagedObjectContext) {
var viewControllers = [ViewController]()
shouldAttemptLogin = false
for viewController in navigationController.viewControllers {
viewControllers.append(contentsOf: viewControllersToSave(from: viewController, presentedVia: .push))
}
moc.navigationState = NavigationState(viewControllers: viewControllers, shouldAttemptLogin: shouldAttemptLogin)
}
private func viewControllerToSave(from viewController: UIViewController, presentedVia presentation: Presentation) -> ViewController? {
let kind: ViewController.Kind?
let info: Info?
switch viewController {
case let tabBarController as UITabBarController:
kind = .tab
switch tabBarController.selectedViewController {
case let savedViewController as SavedViewController:
info = Info(selectedIndex: tabBarController.selectedIndex, currentSavedViewRawValue: savedViewController.currentView.rawValue)
case let searchViewController as SearchViewController:
info = Info(selectedIndex: tabBarController.selectedIndex, searchTerm: searchViewController.searchTerm)
case let exploreViewController as ExploreViewController:
info = Info(selectedIndex: tabBarController.selectedIndex, presentedContentGroupKey: exploreViewController.presentedContentGroupKey)
default:
info = Info(selectedIndex: tabBarController.selectedIndex)
}
case is WMFThemeableNavigationController:
kind = .themeableNavigationController
info = nil
case is WMFSettingsViewController:
kind = .settings
info = nil
case is AccountViewController:
kind = .account
info = nil
shouldAttemptLogin = true
case let talkPageContainerVC as TalkPageContainerViewController:
let result = determineKindInfoForArticleOrTalk(obj: talkPageContainerVC)
kind = result.kind
info = result.info
case let talkPageReplyListVC as TalkPageReplyListViewController:
kind = .talkPageReplyList
info = Info(contentGroupIDURIString: talkPageReplyListVC.topic.objectID.uriRepresentation().absoluteString)
case let readingListDetailVC as ReadingListDetailViewController:
kind = .readingListDetail
info = Info(readingListURIString: readingListDetailVC.readingList.objectID.uriRepresentation().absoluteString)
case let detailPresenting as DetailPresentingFromContentGroup:
kind = .detail
let shouldShowNavigationBar = (viewController as? OnThisDayViewController)?.shouldShowNavigationBar
info = Info(shouldShowNavigationBar: shouldShowNavigationBar, contentGroupIDURIString: detailPresenting.contentGroupIDURIString)
case let singlePageWebViewController as SinglePageWebViewController:
kind = .singleWebPage
info = Info(url: singlePageWebViewController.url)
default:
let result = determineKindInfoForArticleOrTalk(obj: viewController)
kind = result.kind
info = result.info
}
return ViewController(kind: kind, presentation: presentation, info: info)
}
private func determineKindInfoForArticleOrTalk(obj: Any) -> (kind: ViewController.Kind?, info: Info?) {
let kind: ViewController.Kind?
let info: Info?
switch obj {
case let articleViewController as ArticleViewController:
kind = obj is RandomArticleViewController ? .random : .article
info = Info(articleKey: articleViewController.articleURL.wmf_databaseKey)
case let talkPageContainerVC as TalkPageContainerViewController:
kind = .talkPage
info = Info(talkPageSiteURLString: talkPageContainerVC.siteURL.absoluteString, talkPageTitle: talkPageContainerVC.talkPageTitle, talkPageTypeRawValue: talkPageContainerVC.type.rawValue)
default:
kind = nil
info = nil
}
return (kind: kind, info: info)
}
private func viewControllersToSave(from viewController: UIViewController, presentedVia presentation: Presentation) -> [ViewController] {
var viewControllers = [ViewController]()
var append = true
if var viewControllerToSave = viewControllerToSave(from: viewController, presentedVia: presentation) {
if let presentedViewController = viewController.presentedViewController {
viewControllerToSave.updateChildren(viewControllersToSave(from: presentedViewController, presentedVia: .modal))
}
if let navigationController = viewController as? UINavigationController {
var children = [ViewController]()
for viewController in navigationController.viewControllers {
children.append(contentsOf: viewControllersToSave(from: viewController, presentedVia: .push))
}
append = !children.isEmpty
viewControllerToSave.updateChildren(children)
}
if append {
viewControllers.append(viewControllerToSave)
}
}
return viewControllers
}
}
extension NavigationStateController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let controller = self.settingsNavController?.viewControllers, controller.count > 1 {
return true
}
return false
}
}