Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 70 additions & 46 deletions App/Main/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,33 +57,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
return URLCache(memoryCapacity: megabytes(5), diskCapacity: megabytes(50), diskPath: nil)
#endif
}()

window = UIWindow(frame: UIScreen.main.bounds)
window?.tintColor = Theme.defaultTheme()["tintColor"]

if ForumsClient.shared.isLoggedIn {
setRootViewController(rootViewControllerStack.rootViewController, animated: false, completion: nil)
} else {
setRootViewController(loginViewController.enclosingNavigationController, animated: false, completion: nil)
}

openCopiedURLController = OpenCopiedURLController(window: window!, router: {
[unowned self] in
self.open(route: $0)
})

window?.makeKeyAndVisible()


return true
}

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Don't want to lazily create it now.
_rootViewControllerStack?.didAppear()

ignoreSilentSwitchWhenPlayingEmbeddedVideo()

showPromptIfLoginCookieExpiresSoon()
Expand Down Expand Up @@ -144,10 +125,26 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {

func applicationDidBecomeActive(_ application: UIApplication) {
SmilieKeyboardSetIsAwfulAppActive(true)

// Screen brightness may have changed while the app wasn't paying attention.
automaticallyUpdateDarkModeEnabledIfNecessary()
}

func applicationDidEnterBackground(_ application: UIApplication) {
do {
try managedObjectContext.save()
} catch {
logger.error("Failed to save context on background: \(error)")
}
}

func applicationWillTerminate(_ application: UIApplication) {
do {
try managedObjectContext.save()
} catch {
logger.error("Failed to save context on termination: \(error)")
}
}

func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return ForumsClient.shared.isLoggedIn
Expand Down Expand Up @@ -221,7 +218,16 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
urlRouter?.route(route)
}

private func updateShortcutItems() {
func automaticallyUpdateDarkModeEnabledIfNecessary() {
guard automaticDarkTheme else { return }

let shouldDarkModeBeEnabled = (window ?? keyWindow)?.traitCollection.userInterfaceStyle == .dark
if shouldDarkModeBeEnabled != darkMode {
darkMode.toggle()
}
}

func updateShortcutItems() {
guard urlRouter != nil else {
UIApplication.shared.shortcutItems = []
return
Expand Down Expand Up @@ -268,41 +274,64 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {

private var _rootViewControllerStack: RootViewControllerStack?
private var urlRouter: AwfulURLRouter?
private var rootViewControllerStack: RootViewControllerStack {
var rootViewControllerStack: RootViewControllerStack {
if let stack = _rootViewControllerStack { return stack }
let stack = RootViewControllerStack(managedObjectContext: managedObjectContext)
urlRouter = AwfulURLRouter(rootViewController: stack.rootViewController, managedObjectContext: managedObjectContext)
stack.userInterfaceStyleDidChange = { [weak self] in self?.automaticallyUpdateDarkModeEnabledIfNecessary() }
_rootViewControllerStack = stack
return stack
}
private lazy var loginViewController: LoginViewController! = {

lazy var loginViewController: LoginViewController! = {
let loginVC = LoginViewController.newFromStoryboard()
loginVC.completionBlock = { [weak self] (login) in
guard let self = self else { return }
self.setRootViewController(self.rootViewControllerStack.rootViewController, animated: true, completion: { [weak self] in
guard let self = self else { return }
self.rootViewControllerStack.didAppear()
self.loginViewController = nil
})
self.swapToMainViewController()
}
return loginVC
}()

func swapToMainViewController() {
guard let window = self.window ?? self.keyWindow else { return }

UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: {
window.rootViewController = self.rootViewControllerStack.rootViewController
}) { [weak self] _ in
self?.rootViewControllerStack.didAppear()
self?.loginViewController = nil
}
}

func setupOpenCopiedURLController() {
guard openCopiedURLController == nil else { return }
guard let window = self.window ?? self.keyWindow else { return }

openCopiedURLController = OpenCopiedURLController(window: window, router: { [unowned self] in
self.open(route: $0)
})
}

private var keyWindow: UIWindow? {
return UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }
}
}

private extension AppDelegate {
func setRootViewController(_ rootViewController: UIViewController, animated: Bool, completion: (() -> Void)?) {
guard let window = window else { return }
UIView.transition(with: window, duration: animated ? 0.3 : 0, options: .transitionCrossDissolve, animations: {
guard let window = window ?? keyWindow else { return }
UIView.transition(with: window, duration: animated ? 0.3 : 0, options: .transitionCrossDissolve, animations: {
window.rootViewController = rootViewController
}) { (completed) in
completion?()
}
}

func themeDidChange() {
guard let window = window else { return }
guard let window = window ?? keyWindow else { return }

window.tintColor = Theme.defaultTheme()["tintColor"]

Expand All @@ -320,7 +349,11 @@ private extension AppDelegate {
}

private func showSnapshotDuringThemeDidChange() {
if let window = window, let snapshot = window.snapshotView(afterScreenUpdates: false) {
guard let window = window ?? keyWindow else {
themeDidChange()
return
}
if let snapshot = window.snapshotView(afterScreenUpdates: false) {
window.addSubview(snapshot)
themeDidChange()

Expand All @@ -338,15 +371,6 @@ private extension AppDelegate {
}
}

private func automaticallyUpdateDarkModeEnabledIfNecessary() {
guard automaticDarkTheme else { return }

let shouldDarkModeBeEnabled = window?.traitCollection.userInterfaceStyle == .dark
if shouldDarkModeBeEnabled != darkMode {
darkMode.toggle()
}
}

@objc func preferredContentSizeDidChange(_ notification: Notification) {
themeDidChange()
}
Expand All @@ -358,15 +382,15 @@ private extension AppDelegate {
else { return }
let lastPromptDate = UserDefaults.standard.object(forKey: loginCookieLastExpiryPromptDateKey) as? Date ?? .distantFuture
guard lastPromptDate.timeIntervalSinceNow < -loginCookieExpiryPromptFrequency else { return }

let alert = UIAlertController(
title: LocalizedString("session-expiry-imminent.title"),
message: String(format: LocalizedString("session-expiry-imminent.message"), DateFormatter.localizedString(from: expiryDate, dateStyle: .short, timeStyle: .none)),
alertActions: [.default(title: LocalizedString("ok"), handler: {
UserDefaults.standard.set(Date(), forKey: loginCookieLastExpiryPromptDateKey)
})]
)
window?.rootViewController?.present(alert, animated: true, completion: nil)
(window ?? keyWindow)?.rootViewController?.present(alert, animated: true, completion: nil)
}
}

Expand Down
53 changes: 53 additions & 0 deletions App/Main/AwfulApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// AwfulApp.swift
//
// Copyright 2025 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app

import AwfulCore
import os
import Smilies
import SwiftUI

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AwfulApp")

@main
struct AwfulApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@SwiftUI.Environment(\.scenePhase) private var scenePhase

var body: some Scene {
WindowGroup {
RootViewControllerRepresentable()
.ignoresSafeArea()
.onOpenURL { url in handleURL(url) }
}
.onChange(of: scenePhase) { newPhase in
handleScenePhaseChange(newPhase)
}
}

private func handleURL(_ url: URL) {
guard ForumsClient.shared.isLoggedIn,
let route = try? AwfulRoute(url)
else { return }
appDelegate.open(route: route)
}

private func handleScenePhaseChange(_ newPhase: ScenePhase) {
switch newPhase {
case .active:
SmilieKeyboardSetIsAwfulAppActive(true)
appDelegate.automaticallyUpdateDarkModeEnabledIfNecessary()
case .inactive:
SmilieKeyboardSetIsAwfulAppActive(false)
appDelegate.updateShortcutItems()
case .background:
do {
try appDelegate.managedObjectContext.save()
} catch {
logger.error("Failed to save on background: \(error)")
}
@unknown default:
break
}
}
}
62 changes: 0 additions & 62 deletions App/Main/LaunchScreen.storyboard

This file was deleted.

30 changes: 30 additions & 0 deletions App/Main/RootViewControllerRepresentable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// RootViewControllerRepresentable.swift
//
// Copyright 2025 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app

import AwfulCore
import SwiftUI
import UIKit

struct RootViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
guard let appDelegate = AppDelegate.instance else {
fatalError("AppDelegate not initialized")
}

let rootVC: UIViewController
if ForumsClient.shared.isLoggedIn {
rootVC = appDelegate.rootViewControllerStack.rootViewController
appDelegate.rootViewControllerStack.didAppear()
} else {
rootVC = appDelegate.loginViewController.enclosingNavigationController
}

appDelegate.setupOpenCopiedURLController()

return rootVC
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
9 changes: 0 additions & 9 deletions App/Main/main.swift

This file was deleted.

9 changes: 7 additions & 2 deletions App/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,13 @@
<string>com.awfulapp.Awful.activity.listing-threads</string>
<string>com.awfulapp.Awful.activity.reading-message</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string>LaunchBackground</string>
<key>UIImageRespectsSafeAreaInsets</key>
<true/>
</dict>
<key>UIPrerenderedIcon</key>
<true/>
<key>UIStatusBarStyle</key>
Expand Down
Loading
Loading