Watch: waiting-count complication and multi-select questions
A WidgetKit extension (accessoryCircular, Rectangular, Inline, Corner) shows GET /v1/summary. The device token moves to the App Group's Keychain access group on launch so the extension can read it without re-pairing; the app reloads the timeline when its feed counts change and on notifications. Multi-select questions toggle options and send an array of values. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d4d67295c6
commit
0994fd7d5a
18 files changed
+800
-23
No files matched your search
@@ -1,14 +1,12 @@
|
||||
// App-wide state: whether the watch is paired, the navigation path, and the latest feed.
|
||||
// App-wide state: whether the watch is paired, the navigation path, and the latest feed. It also
|
||||
// keeps the complication current: it shares the mode and latest counts through the App Group and
|
||||
// reloads the complication's timeline when they change.
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
import UserNotifications
|
||||
import WatchKit
|
||||
|
||||
enum Route: Hashable, Sendable {
|
||||
case thread(hostId: String, threadId: String)
|
||||
case newThread
|
||||
}
|
||||
import WidgetKit
|
||||
|
||||
@MainActor @Observable
|
||||
final class AppModel {
|
||||
@@ -22,12 +20,16 @@ final class AppModel {
|
||||
/// The one instance the app delegate and notification handler share with the views.
|
||||
static let shared = AppModel(launchArguments: ProcessInfo.processInfo.arguments)
|
||||
|
||||
var mode: Mode
|
||||
var mode: Mode {
|
||||
didSet { if mode != oldValue { shareMode() } }
|
||||
}
|
||||
var path: [Route] = []
|
||||
private(set) var feed: Feed?
|
||||
private(set) var feedError: String?
|
||||
private var apnsToken: String?
|
||||
private var refreshing = false
|
||||
/// The counts the complication was last told about.
|
||||
private var sharedSummary: Summary?
|
||||
|
||||
init(mode: Mode, feed: Feed? = nil, path: [Route] = []) {
|
||||
self.mode = mode
|
||||
@@ -38,6 +40,7 @@ final class AppModel {
|
||||
/// `-demo` starts in demo mode. `-screen new` or `-thread <hostId>/<threadId>` opens that
|
||||
/// screen, which is how the screenshots in watch/screenshots are taken.
|
||||
convenience init(launchArguments args: [String]) {
|
||||
DeviceStore.migrateToSharedGroup()
|
||||
let demo = args.contains("-demo")
|
||||
let mode: Mode = demo ? .demo : DeviceStore.load().map(Mode.paired) ?? .unpaired
|
||||
var path: [Route] = []
|
||||
@@ -49,6 +52,7 @@ final class AppModel {
|
||||
if parts.count == 2 { path = [.thread(hostId: parts[0], threadId: parts[1])] }
|
||||
}
|
||||
self.init(mode: mode, path: path)
|
||||
shareMode()
|
||||
}
|
||||
|
||||
var isPaired: Bool {
|
||||
@@ -114,8 +118,10 @@ final class AppModel {
|
||||
refreshing = true
|
||||
defer { refreshing = false }
|
||||
do {
|
||||
feed = try await api.feed()
|
||||
let feed = try await api.feed()
|
||||
self.feed = feed
|
||||
feedError = nil
|
||||
share(Summary(feed))
|
||||
} catch {
|
||||
feedError = message(for: error)
|
||||
}
|
||||
@@ -125,6 +131,38 @@ final class AppModel {
|
||||
path = [route]
|
||||
}
|
||||
|
||||
/// Opens a `wristcode://` link from the complication.
|
||||
func open(url: URL) {
|
||||
guard mode != .unpaired, let route = Route(url: url) else { return }
|
||||
open(route)
|
||||
}
|
||||
|
||||
// MARK: - Complication
|
||||
|
||||
/// Tells the complication about the mode. Counts from another mode, or from before signing
|
||||
/// out, are forgotten.
|
||||
private func shareMode() {
|
||||
let demo = mode == .demo
|
||||
if SharedState.demo != demo || mode == .unpaired { SharedState.lastSummary = nil }
|
||||
SharedState.demo = demo
|
||||
sharedSummary = nil
|
||||
reloadComplication()
|
||||
}
|
||||
|
||||
/// Hands the complication fresh counts. The feed refreshes every 10 s while on screen, so the
|
||||
/// timeline only reloads when the counts or the next thread change.
|
||||
private func share(_ summary: Summary) {
|
||||
guard summary != sharedSummary else { return }
|
||||
sharedSummary = summary
|
||||
SharedState.lastSummary = summary
|
||||
reloadComplication()
|
||||
}
|
||||
|
||||
/// Asks the complication to fetch the summary again, as after a push or a notification action.
|
||||
func reloadComplication() {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
func enableNotifications() async {
|
||||
@@ -156,6 +194,7 @@ final class AppModel {
|
||||
}
|
||||
|
||||
func notificationArrived() {
|
||||
reloadComplication()
|
||||
Task { await refreshFeed() }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user