listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

ListlessiOSApp.swift (7615B)


      1 import SwiftUI
      2 
      3 // MARK: - App Delegate
      4 
      5 class IOSAppDelegate: UIResponder, UIApplicationDelegate {
      6     func application(
      7         _ application: UIApplication,
      8         didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
      9     ) -> Bool {
     10         application.registerForRemoteNotifications()
     11         return true
     12     }
     13 
     14     func application(
     15         _ application: UIApplication,
     16         didReceiveRemoteNotification userInfo: [AnyHashable: Any],
     17         fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
     18     ) {
     19         completionHandler(.newData)
     20     }
     21 
     22     override func buildMenu(with builder: UIMenuBuilder) {
     23         guard builder.system == .main else {
     24             super.buildMenu(with: builder)
     25             return
     26         }
     27 
     28         // File menu — New Item (⌘N)
     29         let newItem = UIKeyCommand(
     30             title: "New Item",
     31             action: IOSMenuSelectors.newItem,
     32             input: "n",
     33             modifierFlags: .command
     34         )
     35         builder.insertChild(
     36             UIMenu(title: "", options: .displayInline, children: [newItem]),
     37             atStartOfMenu: .file
     38         )
     39 
     40         // Edit menu — Move Up (⌘↑), Move Down (⌘↓), Delete (⌘⌫),
     41         //              Mark as Complete (⌘Space)
     42         let moveUp = UIKeyCommand(
     43             title: "Move Up",
     44             action: IOSMenuSelectors.moveUp,
     45             input: UIKeyCommand.inputUpArrow,
     46             modifierFlags: .command
     47         )
     48         let moveDown = UIKeyCommand(
     49             title: "Move Down",
     50             action: IOSMenuSelectors.moveDown,
     51             input: UIKeyCommand.inputDownArrow,
     52             modifierFlags: .command
     53         )
     54         let delete = UIKeyCommand(
     55             title: "Delete",
     56             action: IOSMenuSelectors.deleteItem,
     57             input: "\u{8}",
     58             modifierFlags: .command
     59         )
     60         let markComplete = UIKeyCommand(
     61             title: "Mark as Complete",
     62             action: IOSMenuSelectors.markCompleted,
     63             input: " ",
     64             modifierFlags: .command
     65         )
     66         builder.insertChild(
     67             UIMenu(title: "", options: .displayInline, children: [
     68                 moveUp, moveDown, delete, markComplete,
     69             ]),
     70             atEndOfMenu: .edit
     71         )
     72 
     73         // Edit menu — Page Up (⌥↑), Page Down (⌥↓),
     74         //              Jump to Top (⌘⌥↑), Jump to Bottom (⌘⌥↓).
     75         // Provides Magic Keyboard users an alternative to the absent
     76         // Page/Home/End keys.
     77         let pageUp = UIKeyCommand(
     78             title: "Page Up",
     79             action: IOSMenuSelectors.navigatePageUp,
     80             input: UIKeyCommand.inputUpArrow,
     81             modifierFlags: .alternate
     82         )
     83         let pageDown = UIKeyCommand(
     84             title: "Page Down",
     85             action: IOSMenuSelectors.navigatePageDown,
     86             input: UIKeyCommand.inputDownArrow,
     87             modifierFlags: .alternate
     88         )
     89         let jumpToTop = UIKeyCommand(
     90             title: "Jump to Top",
     91             action: IOSMenuSelectors.navigateToFirst,
     92             input: UIKeyCommand.inputUpArrow,
     93             modifierFlags: [.command, .alternate]
     94         )
     95         let jumpToBottom = UIKeyCommand(
     96             title: "Jump to Bottom",
     97             action: IOSMenuSelectors.navigateToLast,
     98             input: UIKeyCommand.inputDownArrow,
     99             modifierFlags: [.command, .alternate]
    100         )
    101         builder.insertChild(
    102             UIMenu(title: "", options: .displayInline, children: [
    103                 pageUp, pageDown, jumpToTop, jumpToBottom,
    104             ]),
    105             atEndOfMenu: .edit
    106         )
    107     }
    108 }
    109 
    110 // MARK: - App
    111 
    112 @main
    113 struct ListlessiOSApp: App {
    114     @UIApplicationDelegateAdaptor(IOSAppDelegate.self) var appDelegate
    115     @AppStorage("appearanceMode") private var appearanceMode = 0
    116     @AppStorage("didCompleteTutorial") private var didCompleteTutorial = false
    117     private let persistenceController: PersistenceController
    118     private let keyValueSyncBridge = KeyValueSyncBridge(keys: ["listName", "colorTheme"])
    119 
    120     init() {
    121         PerfSampler.markLaunchStart()
    122         let isUITesting = ProcessInfo.processInfo.arguments.contains("UI_TESTING")
    123         persistenceController = isUITesting ? PersistenceController(inMemory: true) : .shared
    124         keyValueSyncBridge.start()
    125 
    126         if isUITesting {
    127             UserDefaults.standard.set(true, forKey: "didCompleteTutorial")
    128             let theme = ProcessInfo.processInfo.arguments.contains("THEME_COLLAROY") ? 1 : 0
    129             UserDefaults.standard.set(theme, forKey: "colorTheme")
    130         }
    131     }
    132 
    133     var body: some Scene {
    134         WindowGroup {
    135             if didCompleteTutorial {
    136                 mainListView
    137             } else {
    138                 TutorialListView { didCompleteTutorial = true }
    139             }
    140         }
    141     }
    142 
    143     private var mainListView: some View {
    144         ItemListView(
    145             store: ItemStore(persistenceController: persistenceController),
    146             syncMonitor: persistenceController.syncMonitor
    147         )
    148         .safeAreaInset(edge: .top) {
    149             Color.clear.frame(height: 8)
    150         }
    151         .environment(\.managedObjectContext, persistenceController.viewContext)
    152         .onChange(of: appearanceMode, initial: true) { _, newValue in
    153             applyAppearanceMode(newValue)
    154         }
    155         .task {
    156             KeyboardWarmup.prime()
    157         }
    158         .overlay(alignment: .top) {
    159             Color.outerBackground
    160                 .opacity(0.9)
    161                 .ignoresSafeArea(edges: .top)
    162                 .frame(height: 0)
    163         }
    164     }
    165 
    166     private func applyAppearanceMode(_ mode: Int) {
    167         let style: UIUserInterfaceStyle = switch mode {
    168         case 1: .light
    169         case 2: .dark
    170         default: .unspecified
    171         }
    172         for scene in UIApplication.shared.connectedScenes {
    173             guard let windowScene = scene as? UIWindowScene else { continue }
    174             for window in windowScene.windows {
    175                 window.overrideUserInterfaceStyle = style
    176             }
    177         }
    178     }
    179 }
    180 
    181 // MARK: - Tutorial
    182 
    183 struct TutorialListView: View {
    184     @AppStorage("appearanceMode") private var appearanceMode = 0
    185     @State private var persistenceController = PersistenceController(inMemory: true)
    186     var onFinishTutorial: () -> Void
    187 
    188     var body: some View {
    189         let store = ItemStore(persistenceController: persistenceController)
    190         TutorialSeeder.seed(store: store)
    191         return ItemListView(
    192             store: store,
    193             syncMonitor: persistenceController.syncMonitor,
    194             onFinishTutorial: onFinishTutorial
    195         )
    196         .safeAreaInset(edge: .top) {
    197             Color.clear.frame(height: 8)
    198         }
    199         .environment(\.managedObjectContext, persistenceController.viewContext)
    200         .onChange(of: appearanceMode, initial: true) { _, newValue in
    201             applyAppearanceMode(newValue)
    202         }
    203         .overlay(alignment: .top) {
    204             Color.outerBackground
    205                 .opacity(0.9)
    206                 .ignoresSafeArea(edges: .top)
    207                 .frame(height: 0)
    208         }
    209     }
    210 
    211     private func applyAppearanceMode(_ mode: Int) {
    212         let style: UIUserInterfaceStyle = switch mode {
    213         case 1: .light
    214         case 2: .dark
    215         default: .unspecified
    216         }
    217         for scene in UIApplication.shared.connectedScenes {
    218             guard let windowScene = scene as? UIWindowScene else { continue }
    219             for window in windowScene.windows {
    220                 window.overrideUserInterfaceStyle = style
    221             }
    222         }
    223     }
    224 }