196 lines
6.3 KiB
Swift
196 lines
6.3 KiB
Swift
import UIKit
|
|
|
|
@main
|
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
var window: UIWindow?
|
|
private var context: AppContext?
|
|
|
|
func application(
|
|
_ application: UIApplication,
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
) -> Bool {
|
|
let window = UIWindow(frame: UIScreen.main.bounds)
|
|
let context = AppContext()
|
|
self.context = context
|
|
window.rootViewController = context.makeRootController()
|
|
window.makeKeyAndVisible()
|
|
self.window = window
|
|
return true
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class AppContext: NSObject, UITabBarControllerDelegate {
|
|
private let tabs = UITabBarController()
|
|
private var navigationControllers: [UINavigationController] = []
|
|
private var restoreTask: Task<Void, Never>?
|
|
|
|
func makeRootController() -> UIViewController {
|
|
let shell = mobileShellFixture(state: .loading)
|
|
navigationControllers = shell.pages.map { page in
|
|
let root = ShellViewController(page: page)
|
|
let navigation = UINavigationController(rootViewController: root)
|
|
navigation.navigationBar.prefersLargeTitles = true
|
|
navigation.tabBarItem = UITabBarItem(
|
|
title: page.title,
|
|
image: UIImage(systemName: page.systemImage),
|
|
selectedImage: UIImage(systemName: page.selectedSystemImage)
|
|
)
|
|
return navigation
|
|
}
|
|
tabs.viewControllers = navigationControllers
|
|
tabs.delegate = self
|
|
restoreSelectedTab()
|
|
return tabs
|
|
}
|
|
|
|
func tabBarController(
|
|
_ tabBarController: UITabBarController,
|
|
didSelect viewController: UIViewController
|
|
) {
|
|
guard
|
|
let navigation = viewController as? UINavigationController,
|
|
let root = navigation.viewControllers.first as? ShellViewController
|
|
else {
|
|
return
|
|
}
|
|
do {
|
|
try setSelectedMobileTab(tab: root.shellTab)
|
|
} catch {
|
|
let alert = UIAlertController(
|
|
title: "Selection Was Not Saved",
|
|
message: error.localizedDescription,
|
|
preferredStyle: .alert
|
|
)
|
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
tabs.present(alert, animated: true)
|
|
}
|
|
}
|
|
|
|
private func restoreSelectedTab() {
|
|
restoreTask?.cancel()
|
|
restoreTask = Task { [weak self] in
|
|
let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
|
|
guard !Task.isCancelled, let self else { return }
|
|
if let index = shell.pages.firstIndex(where: { $0.tab == shell.selectedTab }) {
|
|
tabs.selectedIndex = index
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class ShellViewController: UITableViewController {
|
|
fileprivate let shellTab: MobileTab
|
|
private var page: MobilePage
|
|
private var loadTask: Task<Void, Never>?
|
|
private var loadGeneration = 0
|
|
|
|
init(page: MobilePage) {
|
|
shellTab = page.tab
|
|
self.page = page
|
|
super.init(style: .insetGrouped)
|
|
title = page.title
|
|
navigationItem.largeTitleDisplayMode = .always
|
|
refreshControl = UIRefreshControl()
|
|
refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) is not supported")
|
|
}
|
|
|
|
deinit {
|
|
loadTask?.cancel()
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
apply(page)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
reloadShell()
|
|
}
|
|
|
|
override func viewDidDisappear(_ animated: Bool) {
|
|
super.viewDidDisappear(animated)
|
|
loadTask?.cancel()
|
|
}
|
|
|
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
|
page.state == .ready ? 1 : 0
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
1
|
|
}
|
|
|
|
override func tableView(
|
|
_ tableView: UITableView,
|
|
cellForRowAt indexPath: IndexPath
|
|
) -> UITableViewCell {
|
|
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
|
var content = cell.defaultContentConfiguration()
|
|
content.image = UIImage(systemName: page.systemImage)
|
|
content.text = page.stateTitle
|
|
content.secondaryText = page.stateDetail
|
|
content.secondaryTextProperties.numberOfLines = 0
|
|
cell.contentConfiguration = content
|
|
cell.selectionStyle = .none
|
|
cell.accessibilityLabel = "\(page.stateTitle). \(page.stateDetail)"
|
|
return cell
|
|
}
|
|
|
|
@objc private func refreshRequested() {
|
|
reloadShell()
|
|
}
|
|
|
|
private func reloadShell() {
|
|
loadGeneration += 1
|
|
let generation = loadGeneration
|
|
loadTask?.cancel()
|
|
loadTask = Task { [weak self] in
|
|
let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
|
|
guard
|
|
!Task.isCancelled,
|
|
let self,
|
|
generation == loadGeneration,
|
|
let page = shell.pages.first(where: { $0.tab == self.shellTab })
|
|
else { return }
|
|
apply(page)
|
|
}
|
|
}
|
|
|
|
private func apply(_ page: MobilePage) {
|
|
self.page = page
|
|
title = page.title
|
|
refreshControl?.endRefreshing()
|
|
tableView.reloadData()
|
|
|
|
guard page.state != .ready else {
|
|
contentUnavailableConfiguration = nil
|
|
return
|
|
}
|
|
var configuration = page.state == .loading
|
|
? UIContentUnavailableConfiguration.loading()
|
|
: UIContentUnavailableConfiguration.empty()
|
|
configuration.text = page.stateTitle
|
|
configuration.secondaryText = page.stateDetail
|
|
configuration.image = UIImage(systemName: stateImage(page.state))
|
|
contentUnavailableConfiguration = configuration
|
|
}
|
|
|
|
private func stateImage(_ state: MobileShellState) -> String {
|
|
switch state {
|
|
case .loading: "hourglass"
|
|
case .empty: "lock.shield"
|
|
case .ready: page.systemImage
|
|
case .locked: "lock.fill"
|
|
case .error: "exclamationmark.triangle"
|
|
}
|
|
}
|
|
}
|