← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


ListlessiOS/ListlessiOSApp.swift (7615B)

1import SwiftUI
2
3// MARK: - App Delegate
4
5class 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
113struct 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
183struct 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}