listless

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

commit 290c374647d13a74d09cbcb61942bcf2722b99e1
parent d75fd2e1405a12158d541fdc156c429fd3539372
Author: Michael Camilleri <[email protected]>
Date:   Thu, 26 Mar 2026 19:04:40 +0900

Sync theme across platforms

This commit syncs the theme used across platforms. This is most
important for the Apple Watch which does not have a way to independently
adjust the theme.

Co-Authored-By: Claude 4.6 Opus <[email protected]>

Diffstat:
MListless/Sync/KeyValueSyncBridge.swift | 8++++----
MListlessMac/ListlessMacApp.swift | 3+++
MListlessWatch/ListlessWatchApp.swift | 2+-
MListlessWatch/Views/ItemListView.swift | 4++++
MListlessWatch/Views/ItemRowView.swift | 5+++--
MListlessiOS/ListlessiOSApp.swift | 2+-
6 files changed, 16 insertions(+), 8 deletions(-)

diff --git a/Listless/Sync/KeyValueSyncBridge.swift b/Listless/Sync/KeyValueSyncBridge.swift @@ -13,7 +13,7 @@ final class KeyValueSyncBridge { cloud.synchronize() for key in keys { - if let cloudValue = cloud.string(forKey: key) { + if let cloudValue = cloud.object(forKey: key) { isSyncing = true UserDefaults.standard.set(cloudValue, forKey: key) isSyncing = false @@ -43,7 +43,7 @@ final class KeyValueSyncBridge { let cloud = NSUbiquitousKeyValueStore.default isSyncing = true for key in changedKeys where keys.contains(key) { - UserDefaults.standard.set(cloud.string(forKey: key), forKey: key) + UserDefaults.standard.set(cloud.object(forKey: key), forKey: key) } isSyncing = false } @@ -54,8 +54,8 @@ final class KeyValueSyncBridge { let cloud = NSUbiquitousKeyValueStore.default isSyncing = true for key in keys { - let localValue = defaults.string(forKey: key) - let cloudValue = cloud.string(forKey: key) + let localValue = defaults.object(forKey: key) as? NSObject + let cloudValue = cloud.object(forKey: key) as? NSObject if localValue != cloudValue { cloud.set(localValue, forKey: key) } diff --git a/ListlessMac/ListlessMacApp.swift b/ListlessMac/ListlessMacApp.swift @@ -16,6 +16,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation { private static let appearanceModeKey = "appearanceMode" private static let colorThemeKey = "colorTheme" + private let keyValueSyncBridge = KeyValueSyncBridge(keys: ["headingText", "colorTheme"]) + private var keyWindowCoordinator: WindowCoordinator? { guard let window = NSApp.keyWindow else { return nil } return coordinators.object(forKey: window) @@ -34,6 +36,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation { func applicationDidFinishLaunching(_ notification: Notification) { NSWindow.allowsAutomaticWindowTabbing = false applyAppearanceMode(UserDefaults.standard.integer(forKey: Self.appearanceModeKey)) + keyValueSyncBridge.start() installMainMenu() openNewWindow() } diff --git a/ListlessWatch/ListlessWatchApp.swift b/ListlessWatch/ListlessWatchApp.swift @@ -3,7 +3,7 @@ import SwiftUI @main struct ListlessWatchApp: App { private let persistenceController = PersistenceController.shared - private let keyValueSyncBridge = KeyValueSyncBridge(keys: ["headingText"]) + private let keyValueSyncBridge = KeyValueSyncBridge(keys: ["headingText", "colorTheme"]) init() { keyValueSyncBridge.start() diff --git a/ListlessWatch/Views/ItemListView.swift b/ListlessWatch/Views/ItemListView.swift @@ -6,6 +6,8 @@ struct ItemListView: View { let syncMonitor: CloudKitSyncMonitor @AppStorage("headingText") private var headingText = "Items" + @AppStorage("colorTheme") private var colorThemeRaw = 0 + private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara } @FetchRequest( sortDescriptors: [ @@ -35,6 +37,7 @@ struct ItemListView: View { item: item, index: index, totalActive: activeItems.count, + colorTheme: colorTheme, onToggle: { toggleItem($0) } ) } @@ -46,6 +49,7 @@ struct ItemListView: View { item: item, index: 0, totalActive: 0, + colorTheme: colorTheme, onToggle: { toggleItem($0) } ) } diff --git a/ListlessWatch/Views/ItemRowView.swift b/ListlessWatch/Views/ItemRowView.swift @@ -4,6 +4,7 @@ struct ItemRowView: View { let item: ItemEntity let index: Int let totalActive: Int + let colorTheme: ColorTheme let onToggle: (ItemEntity) -> Void var body: some View { @@ -15,7 +16,7 @@ struct ItemRowView: View { .foregroundColor( item.isCompleted ? .secondary - : cachedItemColor(forIndex: index, total: totalActive) + : cachedItemColor(forIndex: index, total: totalActive, theme: colorTheme) ) .font(.system(size: 17)) @@ -33,7 +34,7 @@ struct ItemRowView: View { : AnyView( ZStack(alignment: .top) { Color(white: 0.15) - cachedItemColor(forIndex: index, total: totalActive) + cachedItemColor(forIndex: index, total: totalActive, theme: colorTheme) .frame(height: 3) } .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) diff --git a/ListlessiOS/ListlessiOSApp.swift b/ListlessiOS/ListlessiOSApp.swift @@ -63,7 +63,7 @@ struct ListlessiOSApp: App { @UIApplicationDelegateAdaptor(IOSAppDelegate.self) var appDelegate @AppStorage("appearanceMode") private var appearanceMode = 0 private let persistenceController: PersistenceController - private let keyValueSyncBridge = KeyValueSyncBridge(keys: ["headingText"]) + private let keyValueSyncBridge = KeyValueSyncBridge(keys: ["headingText", "colorTheme"]) init() { let isUITesting = ProcessInfo.processInfo.arguments.contains("UI_TESTING")