From b91dc283b6609fdc445e36dc84de69d8ac647dc7 Mon Sep 17 00:00:00 2001 From: commiekong <30882689+dfsm@users.noreply.github.com> Date: Sat, 29 Nov 2025 11:54:28 +1100 Subject: [PATCH 1/2] Initial commit for migration to SwiftUI lifecycle. No different features or changes, retain original UIKit navigation. --- App/Main/AppDelegate.swift | 116 ++++++---- App/Main/AwfulApp.swift | 50 +++++ App/Main/LaunchScreen.storyboard | 62 ------ .../RootViewControllerRepresentable.swift | 30 +++ App/Main/main.swift | 9 - App/Resources/Info.plist | 9 +- App/View Controllers/Login.storyboard | 207 ------------------ .../LoginViewController.swift | 167 +++++++++++++- .../RootTabBarController.storyboard | 27 --- .../RootTabBarController.swift | 35 +-- Awful.xcodeproj/project.pbxproj | 26 +-- 11 files changed, 328 insertions(+), 410 deletions(-) create mode 100644 App/Main/AwfulApp.swift delete mode 100644 App/Main/LaunchScreen.storyboard create mode 100644 App/Main/RootViewControllerRepresentable.swift delete mode 100644 App/Main/main.swift delete mode 100644 App/View Controllers/Login.storyboard delete mode 100644 App/View Controllers/RootTabBarController.storyboard diff --git a/App/Main/AppDelegate.swift b/App/Main/AppDelegate.swift index f0fe31c7f..531fcba0d 100644 --- a/App/Main/AppDelegate.swift +++ b/App/Main/AppDelegate.swift @@ -57,23 +57,7 @@ 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 } @@ -81,9 +65,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - // Don't want to lazily create it now. - _rootViewControllerStack?.didAppear() - ignoreSilentSwitchWhenPlayingEmbeddedVideo() showPromptIfLoginCookieExpiresSoon() @@ -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 @@ -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 @@ -268,7 +274,7 @@ 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) @@ -276,33 +282,56 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { _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"] @@ -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() @@ -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() } @@ -358,7 +382,7 @@ 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)), @@ -366,7 +390,7 @@ private extension AppDelegate { UserDefaults.standard.set(Date(), forKey: loginCookieLastExpiryPromptDateKey) })] ) - window?.rootViewController?.present(alert, animated: true, completion: nil) + (window ?? keyWindow)?.rootViewController?.present(alert, animated: true, completion: nil) } } diff --git a/App/Main/AwfulApp.swift b/App/Main/AwfulApp.swift new file mode 100644 index 000000000..3689090a5 --- /dev/null +++ b/App/Main/AwfulApp.swift @@ -0,0 +1,50 @@ +// AwfulApp.swift +// +// Copyright 2025 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app + +import AwfulCore +import Smilies +import SwiftUI + +@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 { + print("Failed to save on background: \(error)") + } + @unknown default: + break + } + } +} diff --git a/App/Main/LaunchScreen.storyboard b/App/Main/LaunchScreen.storyboard deleted file mode 100644 index e13de12c8..000000000 --- a/App/Main/LaunchScreen.storyboard +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/App/Main/RootViewControllerRepresentable.swift b/App/Main/RootViewControllerRepresentable.swift new file mode 100644 index 000000000..0456071a2 --- /dev/null +++ b/App/Main/RootViewControllerRepresentable.swift @@ -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) { + } +} diff --git a/App/Main/main.swift b/App/Main/main.swift deleted file mode 100644 index 214570523..000000000 --- a/App/Main/main.swift +++ /dev/null @@ -1,9 +0,0 @@ -// main.swift -// -// Copyright 2018 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app - -import UIKit - -// Don't bother loading the app delegate when we're running tests. -private let appDelegateClassName = NSClassFromString("XCTestCase") == nil ? NSStringFromClass(AppDelegate.self) : nil -UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, appDelegateClassName) diff --git a/App/Resources/Info.plist b/App/Resources/Info.plist index 3aecf995b..c37271767 100644 --- a/App/Resources/Info.plist +++ b/App/Resources/Info.plist @@ -76,8 +76,13 @@ com.awfulapp.Awful.activity.listing-threads com.awfulapp.Awful.activity.reading-message - UILaunchStoryboardName - LaunchScreen + UILaunchScreen + + UIColorName + LaunchBackground + UIImageRespectsSafeAreaInsets + + UIPrerenderedIcon UIStatusBarStyle diff --git a/App/View Controllers/Login.storyboard b/App/View Controllers/Login.storyboard deleted file mode 100644 index cd3f27dec..000000000 --- a/App/View Controllers/Login.storyboard +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/App/View Controllers/LoginViewController.swift b/App/View Controllers/LoginViewController.swift index 3def7c4c0..054124208 100644 --- a/App/View Controllers/LoginViewController.swift +++ b/App/View Controllers/LoginViewController.swift @@ -16,14 +16,14 @@ private let termsOfServiceURL = URL(string: "https://www.somethingawful.com/foru class LoginViewController: ViewController { var completionBlock: ((LoginViewController) -> Void)? - - @IBOutlet weak var scrollView: UIScrollView! - @IBOutlet weak var usernameTextField: UITextField! - @IBOutlet weak var passwordTextField: UITextField! - @IBOutlet weak var nextBarButtonItem: UIBarButtonItem! - @IBOutlet weak var forgotPasswordButton: UIButton! - @IBOutlet weak var activityIndicator: UIActivityIndicatorView! - @IBOutlet private var consentToTermsTextView: UITextView! + + private(set) var scrollView: UIScrollView! + private(set) var usernameTextField: UITextField! + private(set) var passwordTextField: UITextField! + private(set) var nextBarButtonItem: UIBarButtonItem! + private(set) var forgotPasswordButton: UIButton! + private(set) var activityIndicator: UIActivityIndicatorView! + private var consentToTermsTextView: UITextView! @FoilDefaultStorage(Settings.canSendPrivateMessages) private var canSendPrivateMessages @FoilDefaultStorageOptional(Settings.userID) private var userID @@ -77,13 +77,17 @@ class LoginViewController: ViewController { } class func newFromStoryboard() -> LoginViewController { - return UIStoryboard(name: "Login", bundle: nil).instantiateInitialViewController() as! LoginViewController + return LoginViewController() } + override func loadView() { + super.loadView() + setupViews() + } + override func viewDidLoad() { super.viewDidLoad() - - // Can't set this in the storyboard for some reason. + nextBarButtonItem.isEnabled = false consentToTermsTextView.attributedText = { @@ -158,6 +162,147 @@ class LoginViewController: ViewController { @IBAction func didTapForgetPassword() { UIApplication.shared.open(lostPasswordURL) } + + private func setupViews() { + title = "Log In" + + scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + + let contentView = UIView() + contentView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentView) + + let textFieldContainer = UIView() + textFieldContainer.translatesAutoresizingMaskIntoConstraints = false + textFieldContainer.backgroundColor = .systemBackground + contentView.addSubview(textFieldContainer) + + let usernameLabel = UILabel() + usernameLabel.translatesAutoresizingMaskIntoConstraints = false + usernameLabel.text = "Username" + usernameLabel.font = .preferredFont(forTextStyle: .body) + usernameLabel.textAlignment = .right + textFieldContainer.addSubview(usernameLabel) + + usernameTextField = UITextField() + usernameTextField.translatesAutoresizingMaskIntoConstraints = false + usernameTextField.placeholder = "Forums Poster" + usernameTextField.font = .preferredFont(forTextStyle: .body) + usernameTextField.autocorrectionType = .no + usernameTextField.spellCheckingType = .no + usernameTextField.delegate = self + usernameTextField.addTarget(self, action: #selector(didChangeUsername(_:)), for: .editingChanged) + textFieldContainer.addSubview(usernameTextField) + + let separator1 = UIView() + separator1.translatesAutoresizingMaskIntoConstraints = false + separator1.backgroundColor = UIColor(red: 0.902, green: 0.902, blue: 0.902, alpha: 1.0) + textFieldContainer.addSubview(separator1) + + let passwordLabel = UILabel() + passwordLabel.translatesAutoresizingMaskIntoConstraints = false + passwordLabel.text = "Password" + passwordLabel.font = .preferredFont(forTextStyle: .body) + passwordLabel.textAlignment = .right + textFieldContainer.addSubview(passwordLabel) + + passwordTextField = UITextField() + passwordTextField.translatesAutoresizingMaskIntoConstraints = false + passwordTextField.placeholder = "Required" + passwordTextField.font = .preferredFont(forTextStyle: .body) + passwordTextField.autocorrectionType = .no + passwordTextField.spellCheckingType = .no + passwordTextField.isSecureTextEntry = true + passwordTextField.delegate = self + passwordTextField.addTarget(self, action: #selector(didChangePassword(_:)), for: .editingChanged) + textFieldContainer.addSubview(passwordTextField) + + let separator2 = UIView() + separator2.translatesAutoresizingMaskIntoConstraints = false + separator2.backgroundColor = UIColor(red: 0.902, green: 0.902, blue: 0.902, alpha: 1.0) + textFieldContainer.addSubview(separator2) + + forgotPasswordButton = UIButton(type: .system) + forgotPasswordButton.translatesAutoresizingMaskIntoConstraints = false + forgotPasswordButton.setTitle("Forgot your password?", for: .normal) + forgotPasswordButton.addTarget(self, action: #selector(didTapForgetPassword), for: .touchUpInside) + contentView.addSubview(forgotPasswordButton) + + activityIndicator = UIActivityIndicatorView(style: .medium) + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.hidesWhenStopped = true + contentView.addSubview(activityIndicator) + + consentToTermsTextView = UITextView() + consentToTermsTextView.translatesAutoresizingMaskIntoConstraints = false + consentToTermsTextView.isEditable = false + consentToTermsTextView.isScrollEnabled = false + consentToTermsTextView.backgroundColor = .clear + consentToTermsTextView.delegate = self + contentView.addSubview(consentToTermsTextView) + + nextBarButtonItem = UIBarButtonItem(title: "Next", style: .plain, target: self, action: #selector(didTapNext)) + navigationItem.rightBarButtonItem = nextBarButtonItem + + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + textFieldContainer.topAnchor.constraint(equalTo: contentView.topAnchor), + textFieldContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + textFieldContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + textFieldContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: 320), + textFieldContainer.widthAnchor.constraint(lessThanOrEqualToConstant: 480), + + usernameLabel.leadingAnchor.constraint(equalTo: textFieldContainer.leadingAnchor, constant: 8), + usernameLabel.topAnchor.constraint(equalTo: textFieldContainer.topAnchor, constant: 36), + usernameLabel.widthAnchor.constraint(equalToConstant: 90), + + usernameTextField.leadingAnchor.constraint(equalTo: usernameLabel.trailingAnchor, constant: 14), + usernameTextField.trailingAnchor.constraint(equalTo: textFieldContainer.trailingAnchor, constant: -8), + usernameTextField.firstBaselineAnchor.constraint(equalTo: usernameLabel.firstBaselineAnchor), + + separator1.leadingAnchor.constraint(equalTo: usernameLabel.leadingAnchor), + separator1.trailingAnchor.constraint(equalTo: usernameTextField.trailingAnchor), + separator1.topAnchor.constraint(equalTo: usernameTextField.bottomAnchor, constant: 7), + separator1.heightAnchor.constraint(equalToConstant: 0.5), + + passwordLabel.leadingAnchor.constraint(equalTo: textFieldContainer.leadingAnchor, constant: 8), + passwordLabel.topAnchor.constraint(equalTo: separator1.bottomAnchor, constant: 16), + passwordLabel.widthAnchor.constraint(equalTo: usernameLabel.widthAnchor), + + passwordTextField.leadingAnchor.constraint(equalTo: usernameTextField.leadingAnchor), + passwordTextField.trailingAnchor.constraint(equalTo: usernameTextField.trailingAnchor), + passwordTextField.firstBaselineAnchor.constraint(equalTo: passwordLabel.firstBaselineAnchor), + + separator2.leadingAnchor.constraint(equalTo: passwordLabel.leadingAnchor), + separator2.trailingAnchor.constraint(equalTo: passwordTextField.trailingAnchor), + separator2.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 7), + separator2.heightAnchor.constraint(equalToConstant: 0.5), + separator2.bottomAnchor.constraint(equalTo: textFieldContainer.bottomAnchor), + + forgotPasswordButton.topAnchor.constraint(equalTo: textFieldContainer.bottomAnchor, constant: 16), + forgotPasswordButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + + activityIndicator.topAnchor.constraint(equalTo: forgotPasswordButton.bottomAnchor, constant: 16), + activityIndicator.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + + consentToTermsTextView.topAnchor.constraint(equalTo: activityIndicator.bottomAnchor, constant: 16), + consentToTermsTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + consentToTermsTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + consentToTermsTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16) + ]) + } } extension LoginViewController: UITextFieldDelegate { diff --git a/App/View Controllers/RootTabBarController.storyboard b/App/View Controllers/RootTabBarController.storyboard deleted file mode 100644 index c4e1d1c92..000000000 --- a/App/View Controllers/RootTabBarController.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/App/View Controllers/RootTabBarController.swift b/App/View Controllers/RootTabBarController.swift index fbb59fa07..574acbed2 100644 --- a/App/View Controllers/RootTabBarController.swift +++ b/App/View Controllers/RootTabBarController.swift @@ -6,22 +6,16 @@ import AwfulSettings import AwfulTheming import UIKit -/// A themeable tab bar controller that fixes an iOS 11 layout problem. +/// A themeable tab bar controller. final class RootTabBarController: UITabBarController, UITabBarControllerDelegate, Themeable { @FoilDefaultStorage(Settings.enableHaptics) private var enableHaptics - /// Returns a tab bar controller whose tab bar is an instance of `TabBar_FixiOS11iPadLayout`. static func makeWithTabBarFixedForiOS11iPadLayout() -> RootTabBarController { - let storyboard = UIStoryboard(name: "RootTabBarController", bundle: Bundle(for: RootTabBarController.self)) - guard let tabBarController = storyboard.instantiateInitialViewController() as? RootTabBarController else { - fatalError("initial view controller in RootTabBarController.storyboard should be a RootTabBarController") - } - return tabBarController + return RootTabBarController(nibName: nil, bundle: nil) } - /// Use `makeWithTabBarFixedForiOS11iPadLayout()` instead. - private override init(nibName: String?, bundle: Bundle?) { + override init(nibName: String?, bundle: Bundle?) { super.init(nibName: nibName, bundle: bundle) commonInit() } @@ -40,8 +34,8 @@ final class RootTabBarController: UITabBarController, UITabBarControllerDelegate #endif } - override var tabBar: RootTabBar { - return super.tabBar as! RootTabBar + private var customTabBar: RootTabBar? { + return super.tabBar as? RootTabBar } var theme: Theme { @@ -67,7 +61,7 @@ final class RootTabBarController: UITabBarController, UITabBarControllerDelegate tabBar.barTintColor = theme["tabBarBackgroundColor"] tabBar.isTranslucent = theme[bool: "tabBarIsTranslucent"] ?? true tabBar.tintColor = theme["tintColor"] - tabBar.topBorderColor = theme["bottomBarTopBorderColor"] + customTabBar?.topBorderColor = theme["bottomBarTopBorderColor"] let barAppearance = UITabBarAppearance() if tabBar.isTranslucent { @@ -93,20 +87,11 @@ final class RootTabBarController: UITabBarController, UITabBarControllerDelegate } /** - A tab bar that fixes some issues we've come across. Some fixes are specific to Awful's particular use of the tab bar, so this may not be suitable as a general-purpose fix-it subclass. - - On iOS 11, `UITabBar` lays out its items with title and icon stacked horizontally whenever we're in a horizontally regular size class, and it does not do well if we constrict its width. Everything falls apart when the tab bar is in the primary view controller of a split view controller. This subclass overrides `traitCollection` and forces an always compact horizontal size class. + A tab bar that fixes layout issues with safe area insets. On iOS 12, `UITabBar` has a hard time with safe area insets and can completely mess up its own layout after some combination of `hidesBottomBarOnPush` and/or full-screen modal presentation. The layout mess usually fixes itself after the pop animation that results in the tab bar appearing. This subclass overrides `sizeThatFits(_:)` and ensures that the returned height considers the safe area insets. - Hilariously, the most reasonable way to convince a `UITabBarController` to use a custom `UITabBar` subclass is in a storyboard, so we use `RootTabBarController.storyboard`. - - - Seealso: `RootTabBarController.makeWithTabBarFixedForiOS11iPadLayout()` - - Seealso: https://github.com/Awful/Awful.app/issues/357 where we were trying to puzzle this out. - - Seealso: https://stackoverflow.com/a/45945937 which has this subclassing solution. - - Seealso: https://github.com/bnickel/HidingTabBar which mentions that storyboard is the only reasonable way to crowbar a `UITabBar` subclass into a `UITabBarController`. - Seealso: https://stackoverflow.com/a/53524635 which overrides `sizeThatFits(_:)` to force safe area consideration. - - Seealso: `UITabBar+FixiOS12_1Layout.h` which addresses another UITabBar issue in iOS 12. */ final class RootTabBar: UITabBar { @@ -122,12 +107,6 @@ final class RootTabBar: UITabBar { set { topBorder.backgroundColor = newValue } } - override var traitCollection: UITraitCollection { - return UITraitCollection(traitsFrom: [ - super.traitCollection, - UITraitCollection(horizontalSizeClass: .compact)]) - } - override func sizeThatFits(_ size: CGSize) -> CGSize { var size = super.sizeThatFits(size) let bottomInset = safeAreaInsets.bottom diff --git a/Awful.xcodeproj/project.pbxproj b/Awful.xcodeproj/project.pbxproj index b32003490..0e4f6f9c7 100644 --- a/Awful.xcodeproj/project.pbxproj +++ b/Awful.xcodeproj/project.pbxproj @@ -148,7 +148,6 @@ 1C8F680B222B8F06007E61ED /* NamedThreadTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C8F680A222B8F06007E61ED /* NamedThreadTag.swift */; }; 1C917CF81C4F21B800BBF672 /* HairlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC22AB419F972C200D5BABD /* HairlineView.swift */; }; 1C9AEBC6210C3B2300C9A567 /* CloseBBcodeTagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9AEBC5210C3B2300C9A567 /* CloseBBcodeTagTests.swift */; }; - 1C9AEBCE210C3BAF00C9A567 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9AEBCD210C3BAF00C9A567 /* main.swift */; }; 1CA3D6FC2D98A7E400D70964 /* OEmbedFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CA3D6FB2D98A7E100D70964 /* OEmbedFetcher.swift */; }; 1CA45D941F2C0AD1005BEEC5 /* RenderView.js in Resources */ = {isa = PBXBuildFile; fileRef = 1CA45D931F2C0AD1005BEEC5 /* RenderView.js */; }; 1CA56FEB1A009BDF009A91AE /* PotentiallyObjectionableTexts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1CA56FEA1A009BDF009A91AE /* PotentiallyObjectionableTexts.plist */; }; @@ -179,14 +178,11 @@ 1CD9FB641D1A38030070C8C7 /* NigglyRefreshView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD9FB631D1A38030070C8C7 /* NigglyRefreshView.swift */; }; 1CDC53DA21FCF38F0086BD2B /* OpenCopiedURLController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CDC53D921FCF38F0086BD2B /* OpenCopiedURLController.swift */; }; 1CDC53DC220131400086BD2B /* ImgurAnonymousAPI+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CDC53DB220131400086BD2B /* ImgurAnonymousAPI+Shared.swift */; }; - 1CDD0A4719B7D89B009811C4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1CDD0A4619B7D89B009811C4 /* LaunchScreen.storyboard */; }; 1CDD0A4919B7EB3F009811C4 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CDD0A4819B7EB3E009811C4 /* ProfileViewController.swift */; }; 1CE2B76819C2372200FDC33E /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CE2B76619C2372200FDC33E /* LoginViewController.swift */; }; - 1CE2B76B19C2374C00FDC33E /* Login.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1CE2B76A19C2374C00FDC33E /* Login.storyboard */; }; 1CE55A7A1A1072D900E474A6 /* ForumsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CE55A791A1072D900E474A6 /* ForumsTableViewController.swift */; }; 1CEB5BFF19AB9C1700C82C30 /* InAppActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CEB5BFE19AB9C1700C82C30 /* InAppActionViewController.swift */; }; 1CF186A617D48E5700B26717 /* Thread Tags in Resources */ = {isa = PBXBuildFile; fileRef = 1CF186A517D48E5700B26717 /* Thread Tags */; }; - 1CF264CA1F7811EA0059CCCA /* RootTabBarController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1CF264C91F7811EA0059CCCA /* RootTabBarController.storyboard */; }; 1CF280982055EB9B00913149 /* AwfulRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CF280972055EB9B00913149 /* AwfulRoute.swift */; }; 1CF521752C228E76009712A7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1CF521742C228E76009712A7 /* PrivacyInfo.xcprivacy */; }; 1CF521772C228F88009712A7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1CF521762C228F88009712A7 /* PrivacyInfo.xcprivacy */; }; @@ -204,6 +200,8 @@ 2D327DD627F468CE00D21AB0 /* BookmarkColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D327DD527F468CE00D21AB0 /* BookmarkColorPicker.swift */; }; 2D921269292F588100B16011 /* platinum-member.png in Resources */ = {isa = PBXBuildFile; fileRef = 2D921268292F588100B16011 /* platinum-member.png */; }; 2DAF1FE12E05D3ED006F6BC4 /* View+FontDesign.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAF1FE02E05D3EB006F6BC4 /* View+FontDesign.swift */; }; + 2DBE262F2EDA765700E095A6 /* RootViewControllerRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBE262E2EDA765700E095A6 /* RootViewControllerRepresentable.swift */; }; + 2DBE26302EDA765700E095A6 /* AwfulApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBE262D2EDA765700E095A6 /* AwfulApp.swift */; }; 2DD8209C25DDD9BF0015A90D /* CopyImageActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD8209B25DDD9BF0015A90D /* CopyImageActivity.swift */; }; 306F740B2D90AA01000717BC /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 306F740A2D90AA01000717BC /* KeychainAccess */; }; 30E0C51D2E35C89D0030DC0A /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E0C5162E35C89D0030DC0A /* AnimatedImageView.swift */; }; @@ -450,7 +448,6 @@ 1C9AEBC3210C3B2200C9A567 /* AwfulTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AwfulTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 1C9AEBC5210C3B2300C9A567 /* CloseBBcodeTagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseBBcodeTagTests.swift; sourceTree = ""; }; 1C9AEBC7210C3B2300C9A567 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 1C9AEBCD210C3BAF00C9A567 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 1CA3D6FB2D98A7E100D70964 /* OEmbedFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OEmbedFetcher.swift; sourceTree = ""; }; 1CA45D931F2C0AD1005BEEC5 /* RenderView.js */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.javascript; path = RenderView.js; sourceTree = ""; tabWidth = 2; }; 1CA56FEA1A009BDF009A91AE /* PotentiallyObjectionableTexts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = PotentiallyObjectionableTexts.plist; sourceTree = ""; }; @@ -492,15 +489,12 @@ 1CDC53D921FCF38F0086BD2B /* OpenCopiedURLController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenCopiedURLController.swift; sourceTree = ""; }; 1CDC53DB220131400086BD2B /* ImgurAnonymousAPI+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImgurAnonymousAPI+Shared.swift"; sourceTree = ""; }; 1CDC9F652AE5FB19005DA08D /* test.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; name = test.yml; path = .github/workflows/test.yml; sourceTree = SOURCE_ROOT; }; - 1CDD0A4619B7D89B009811C4 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 1CDD0A4819B7EB3E009811C4 /* ProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; 1CE2B76619C2372200FDC33E /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; - 1CE2B76A19C2374C00FDC33E /* Login.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Login.storyboard; sourceTree = ""; }; 1CE55A791A1072D900E474A6 /* ForumsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForumsTableViewController.swift; sourceTree = ""; }; 1CEB5BFE19AB9C1700C82C30 /* InAppActionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppActionViewController.swift; sourceTree = ""; }; 1CF0A6C520A50FC5004C26EA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 1CF186A517D48E5700B26717 /* Thread Tags */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Thread Tags"; sourceTree = ""; }; - 1CF264C91F7811EA0059CCCA /* RootTabBarController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RootTabBarController.storyboard; sourceTree = ""; }; 1CF280972055EB9B00913149 /* AwfulRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AwfulRoute.swift; sourceTree = ""; }; 1CF521742C228E76009712A7 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 1CF521762C228F88009712A7 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -519,9 +513,10 @@ 2D327DD527F468CE00D21AB0 /* BookmarkColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkColorPicker.swift; sourceTree = ""; }; 2D921268292F588100B16011 /* platinum-member.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "platinum-member.png"; sourceTree = ""; }; 2DAF1FE02E05D3EB006F6BC4 /* View+FontDesign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+FontDesign.swift"; sourceTree = ""; }; + 2DBE262D2EDA765700E095A6 /* AwfulApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AwfulApp.swift; sourceTree = ""; }; + 2DBE262E2EDA765700E095A6 /* RootViewControllerRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewControllerRepresentable.swift; sourceTree = ""; }; 2DD8209B25DDD9BF0015A90D /* CopyImageActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyImageActivity.swift; sourceTree = ""; }; 30E0C5162E35C89D0030DC0A /* AnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = ""; }; - 30E0C5172E35C89D0030DC0A /* SmilieData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmilieData.swift; sourceTree = ""; }; 30E0C5182E35C89D0030DC0A /* SmilieGridItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmilieGridItem.swift; sourceTree = ""; }; 30E0C5192E35C89D0030DC0A /* SmiliePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmiliePickerView.swift; sourceTree = ""; }; 30E0C51A2E35C89D0030DC0A /* SmilieSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmilieSearchViewModel.swift; sourceTree = ""; }; @@ -611,12 +606,12 @@ 1190F71C13BE4EA900B9D271 /* Main */ = { isa = PBXGroup; children = ( + 2DBE262D2EDA765700E095A6 /* AwfulApp.swift */, + 2DBE262E2EDA765700E095A6 /* RootViewControllerRepresentable.swift */, 1C2C1F131CEE90D900CD27DD /* AppDelegate.swift */, 1CBE1B1119CAAFA200510187 /* AwfulSplitViewController.swift */, 1C220E3C2B815AFC00DA92B0 /* Bundle+.swift */, 1C4506C31A2BAB3800767306 /* Handoff.swift */, - 1CDD0A4619B7D89B009811C4 /* LaunchScreen.storyboard */, - 1C9AEBCD210C3BAF00C9A567 /* main.swift */, 1CF521762C228F88009712A7 /* PrivacyInfo.xcprivacy */, 1C8D114619ACDAD9005D46CB /* RootViewControllerStack.swift */, ); @@ -1036,13 +1031,11 @@ 1C16FBB11CB86ACD00C88BD1 /* EmptyViewController.swift */, 1C29C3812258538A00E1217A /* Forums */, 1CBE1B0819CA050300510187 /* ImageViewController.swift */, - 1CE2B76A19C2374C00FDC33E /* Login.storyboard */, 1CE2B76619C2372200FDC33E /* LoginViewController.swift */, 1C29C3852258547C00E1217A /* Messages */, 1C29C382225853A300E1217A /* Posts */, 1CDD0A4819B7EB3E009811C4 /* ProfileViewController.swift */, 1C29C3842258544B00E1217A /* Rap Sheet */, - 1CF264C91F7811EA0059CCCA /* RootTabBarController.storyboard */, 1C29BD54225121F100E1217A /* RootTabBarController.swift */, 1C29C3872258551700E1217A /* Thread Tags */, 1C29C3832258543200E1217A /* Threads */, @@ -1455,7 +1448,6 @@ 1AB84FD92ADC611B00E7334D /* bat.svg in Resources */, 8C0F4F571682CCFA00E25D7E /* macinyos-heading-right.png in Resources */, 1C8F67F822210DED007E61ED /* Profile.html.stencil in Resources */, - 1CE2B76B19C2374C00FDC33E /* Login.storyboard in Resources */, 1CC10E3B1DD9558B00E0FB63 /* PotentiallyObjectionableThreadTags.plist in Resources */, 8C483FC216852E5D00D02482 /* macinyos-loading@2x.png in Resources */, 1C8F67FD2221BB57007E61ED /* PrivateMessage.html.stencil in Resources */, @@ -1479,7 +1471,6 @@ 1CA45D941F2C0AD1005BEEC5 /* RenderView.js in Resources */, 1C4EE1CE28470C2400A7507E /* Assets.xcassets in Resources */, 8C8813A218492AC80058009E /* mac-watch.png in Resources */, - 1CF264CA1F7811EA0059CCCA /* RootTabBarController.storyboard in Resources */, 8C9834E2169091CE009F0CD6 /* fyad-bubble.png in Resources */, 1C41B8A716CD573D00718F79 /* title-banned.gif in Resources */, 1C1E57512C2521580075E3B6 /* platinum-member-white.png in Resources */, @@ -1487,7 +1478,6 @@ 2D19BA3929C33303009DD94F /* niggly60.json in Resources */, 1C41B8A816CD573D00718F79 /* title-permabanned.gif in Resources */, 8CE6100818CAAF74002E92DA /* cat.gif in Resources */, - 1CDD0A4719B7D89B009811C4 /* LaunchScreen.storyboard in Resources */, F4B3645819165D5100CCE1EF /* SecondaryTags.plist in Resources */, 1C82AC4D199F5C1500CB15FE /* Selectotron.xib in Resources */, 1C5C2C5922D2586D00EA5A80 /* ARChromeActivity.xcassets in Resources */, @@ -1541,9 +1531,10 @@ 83410EF219A582B8002CD019 /* DateFormatters.swift in Sources */, 1C273A9E21B316DB002875A9 /* LoadMoreFooter.swift in Sources */, 1C2C1F0E1CE16FE200CD27DD /* CloseBBcodeTagCommand.swift in Sources */, - 30E0C51C2E35C89D0030DC0A /* SmilieData.swift in Sources */, 30E0C51D2E35C89D0030DC0A /* AnimatedImageView.swift in Sources */, 30E0C51E2E35C89D0030DC0A /* SmiliePickerView.swift in Sources */, + 2DBE262F2EDA765700E095A6 /* RootViewControllerRepresentable.swift in Sources */, + 2DBE26302EDA765700E095A6 /* AwfulApp.swift in Sources */, 30E0C51F2E35C89D0030DC0A /* SmilieSearchViewModel.swift in Sources */, 30E0C5202E35C89D0030DC0A /* SmilieGridItem.swift in Sources */, 1C16FBF31CBDC58B00C88BD1 /* URL+OpensInBrowser.swift in Sources */, @@ -1593,7 +1584,6 @@ 1CEB5BFF19AB9C1700C82C30 /* InAppActionViewController.swift in Sources */, 1CA3D6FC2D98A7E400D70964 /* OEmbedFetcher.swift in Sources */, 1C16FBAA1CB5D38700C88BD1 /* CompositionInputAccessoryView.swift in Sources */, - 1C9AEBCE210C3BAF00C9A567 /* main.swift in Sources */, 1C16FBE71CBC671A00C88BD1 /* PostRenderModel.swift in Sources */, 1CB5F7F7201547D90046D080 /* Thread+Presentation.swift in Sources */, 1C16FC181CD1848400C88BD1 /* ComposeTextViewController.swift in Sources */, From 084ea7ac39bb69271db52c136e1dc21de0f3b3ff Mon Sep 17 00:00:00 2001 From: commiekong <30882689+dfsm@users.noreply.github.com> Date: Sat, 29 Nov 2025 12:05:47 +1100 Subject: [PATCH 2/2] Change print to logger --- App/Main/AwfulApp.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/App/Main/AwfulApp.swift b/App/Main/AwfulApp.swift index 3689090a5..635c926fb 100644 --- a/App/Main/AwfulApp.swift +++ b/App/Main/AwfulApp.swift @@ -3,9 +3,12 @@ // 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 @@ -41,7 +44,7 @@ struct AwfulApp: App { do { try appDelegate.managedObjectContext.save() } catch { - print("Failed to save on background: \(error)") + logger.error("Failed to save on background: \(error)") } @unknown default: break