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 }