← All repositories

listless

A simple list app for Apple platforms

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


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")