listless

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

commit 345ffc5480e9ddba748cc1f36ec1e4e367a3929f
parent 2207212a14fa405e171b4fe946eb0311c39fc0f7
Author: Michael Camilleri <[email protected]>
Date:   Sat,  7 Mar 2026 12:48:53 +0900

Add watchOS app

This commit adds the first attempt at a watchOS app.

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

Diffstat:
MListless.xcodeproj/project.pbxproj | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListless.xcodeproj/xcshareddata/xcschemes/Listless watchOS.xcscheme | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListlessWatch/Info.plist | 30++++++++++++++++++++++++++++++
AListlessWatch/Listless.entitlements | 20++++++++++++++++++++
AListlessWatch/ListlessWatchApp.swift | 16++++++++++++++++
AListlessWatch/Views/TaskListView.swift | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListlessWatch/Views/TaskRowView.swift | 30++++++++++++++++++++++++++++++
MMedia.xcassets/AppIcon.appiconset/Contents.json | 6++++++
MScripts/publish-ios.sh | 2++
Mproject.yml | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 497 insertions(+), 0 deletions(-)

diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj @@ -7,8 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 03BC4562430EC105221AB43B /* TaskRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D1AF0DE30657AAD86482CA /* TaskRowView.swift */; }; 060436CDDB388BC04C51581A /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E995954787F0A14CCFF348 /* AboutView.swift */; }; 072594B24D88AD6D1DF7AFE5 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C611E04943F1D82D6F975592 /* SettingsView.swift */; }; + 07E2BB2FF9E75A922C3756AB /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DEECB5DC31DE5F34FB55A3 /* TaskListView.swift */; }; 0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; }; 0F12D56A528FCBF8A67864CB /* TappableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECE0E961F87BA32FA87BF90 /* TappableTextField.swift */; }; 11AA75BE98CFBE44AEAB7100 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9404C09EE1A4D91DFF338464 /* Media.xcassets */; }; @@ -19,6 +21,7 @@ 19699EC4FF57EF0D636B65E3 /* KeyValueSyncBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE264D30C7692457B92E518 /* KeyValueSyncBridge.swift */; }; 1A66A0454558B207AF9265D4 /* UndoToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658295C1386BFF48CE3C2419 /* UndoToast.swift */; }; 1AA328A921EF8A7FDD03119A /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B048B19C5219862BBED2E7 /* TestHelpers.swift */; }; + 264BD64C1DD30376E8BDAF79 /* KeyValueSyncBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE264D30C7692457B92E518 /* KeyValueSyncBridge.swift */; }; 269B93D5543770B464DFB37A /* TaskStoreOrderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3A2DDCE24E54ABCCFBBD4C /* TaskStoreOrderingTests.swift */; }; 26F609518DE1055DF09B0159 /* TaskListView+NavigationHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 712ADAD0FE66175DAA9A6D50 /* TaskListView+NavigationHeader.swift */; }; 2C53717A0EAFA39240615C9A /* TaskRowDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C3CC81C4380EFAE4DB910 /* TaskRowDragGesture.swift */; }; @@ -28,16 +31,23 @@ 3ABE52A15C2059D8D5570528 /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEE364304587D280C5672 /* TaskStore.swift */; }; 3D1F551A03B97ECF4E3DC8B0 /* TaskRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06485DBE35B60868E14202A /* TaskRowView.swift */; }; 3EDB6A9A30B4226C15E7F44D /* AppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C46FC97E6DB6FF81AC5C22 /* AppCommands.swift */; }; + 4A0682E85B50DF42ECF83B48 /* Listless watchOS.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = C6812E535A24C599C28F9278 /* Listless watchOS.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4DD2030E321567BD25661760 /* SyncDiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9288507CE6023425D1DE724 /* SyncDiagnosticsView.swift */; }; 4E5A0A02121E02124F80E320 /* TaskListView+SyncUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7BD42B1E3C71333FA24893 /* TaskListView+SyncUI.swift */; }; 5035EC4C7518A5FF9AD454CA /* TaskRowDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F416DD868A4C044F0D64F8D0 /* TaskRowDragGesture.swift */; }; + 53700EA974FE4AD771FE89EC /* CloudKitSyncMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3E82F6093EEFC94A41FED9 /* CloudKitSyncMonitor.swift */; }; + 543C8A0C8A9E2F77B2C0060F /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */; }; 568635BB34CD7EBE24E66A15 /* SyncDiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF3374CE58E7D9378C6997D2 /* SyncDiagnosticsView.swift */; }; 5761B201BF46FCA9C5C98CEF /* PlatformScrollIndicatorsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */; }; + 57E864F71C9C84B63A28E14F /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEE364304587D280C5672 /* TaskStore.swift */; }; 5B60B409CE4BA668DB30A65D /* Listless.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C093494053E6C348F245D4EC /* Listless.xcdatamodeld */; }; 5D3EE9526DA269EE9EE3AB52 /* AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E998119283F784B9ADEE28 /* AppColors.swift */; }; 5DB1231FEF24A9E4BC1E56B0 /* TaskListView+PullGestures.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D9CDDA8913CD116FB4DA74 /* TaskListView+PullGestures.swift */; }; 614FCCA450EC0BFFD8B40640 /* ListlessMacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */; }; 642151C8EEA34DAD76C49FA1 /* TaskListView+SyncUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7BD42B1E3C71333FA24893 /* TaskListView+SyncUI.swift */; }; + 65E97DE8C190E9E9B71EC356 /* ListlessWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE7F4637B4F4C1FF4BE160B /* ListlessWatchApp.swift */; }; + 684A80B692237B74EDF3C7A8 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; }; + 6FE9D247153209BD4CFD9E34 /* Listless.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C093494053E6C348F245D4EC /* Listless.xcdatamodeld */; }; 763363F6F3C7D2D3C9A63977 /* PlatformScrollIndicatorsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5D58EC1FBAA96E83A79445 /* PlatformScrollIndicatorsModifier.swift */; }; 77970A2323A598E830D95301 /* TaskListView+Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB43816B8E7F083A2AD07F28 /* TaskListView+Toolbar.swift */; }; 77FE96F070B1F7FE31A9CE51 /* PlatformTextFieldWidthModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2174C43654733E9D4023157 /* PlatformTextFieldWidthModifier.swift */; }; @@ -55,6 +65,8 @@ 99977BFA37FBAAA49AF6B71E /* TaskStoreEdgeCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2BBE01D99CDA278BCB9F49 /* TaskStoreEdgeCaseTests.swift */; }; 99D17075DA3F00F52A18BB4D /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */; }; A0AA8FD4C542E9AEB2437BC2 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */; }; + A119D0130DB77E30FBCB5436 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */; }; + A8D5A7B0DFBEC87501FD0526 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9404C09EE1A4D91DFF338464 /* Media.xcassets */; }; B7CFDCA5EA48EDE1C768FA21 /* AppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C79ABB39A40D3E1828716C7 /* AppCommands.swift */; }; BA6953E0EFE6F8255F05A3FD /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */; }; C169823665158AA347A63990 /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51B8129962E5CC78ECDDC2B /* TaskListView.swift */; }; @@ -73,6 +85,7 @@ E60F81C7B930AACE67746759 /* HoverCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7588879D0FA1C2A8BCEF14F /* HoverCursorModifier.swift */; }; EA5E6FC7D61E235B70A139FA /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9404C09EE1A4D91DFF338464 /* Media.xcassets */; }; ECD5E7EA05AE1C00B38C939E /* TaskStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967F7ECEB3915CEDCE584872 /* TaskStoreTests.swift */; }; + F046B5A7F4C0BD7AAF46A69C /* CloudKitErrorClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A677CC11ACE0BF743AFCE5 /* CloudKitErrorClassifier.swift */; }; F0B2B806BD84A4F2FDF8E038 /* ListlessiOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */; }; F3DD1F167E4107456473B6B2 /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = 3DD31E8962F7EEC22EFC0CA9 /* Credits.html */; }; F50E8B8F73F64D8E641DC74C /* TaskStoreCompletionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E88DDD119EEEECCD45F36D2 /* TaskStoreCompletionTests.swift */; }; @@ -82,6 +95,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 642EE6A5908DB4216F049E9C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3256C2BF8F1DAF371DA32120 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9BDC1B2175AB9CE26790448D; + remoteInfo = "Listless watchOS"; + }; 6F7E207AE10E1516EF8A683C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 3256C2BF8F1DAF371DA32120 /* Project object */; @@ -91,6 +111,20 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + A9226A0DD67E77D754BA0E3F /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + 4A0682E85B50DF42ECF83B48 /* Listless watchOS.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 01E141436176F83594E2F26B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 067D666DF0AF1C53404ECF7C /* TaskRowSwipeGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowSwipeGesture.swift; sourceTree = "<group>"; }; @@ -110,23 +144,27 @@ 4FC64B9F9370041BEDBD1E14 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; }; 51C3B9FF63645B35E09CF1B1 /* CloudKitErrorClassifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitErrorClassifierTests.swift; sourceTree = "<group>"; }; 567DBAC2A39FA2760D006AAB /* PullToClear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToClear.swift; sourceTree = "<group>"; }; + 56DEECB5DC31DE5F34FB55A3 /* TaskListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListView.swift; sourceTree = "<group>"; }; 5A6D74EDDD7E3FC150064FB5 /* TaskListView+PullToClear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+PullToClear.swift"; sourceTree = "<group>"; }; 5B0E22B8F7B2B7283CAF749E /* Listless macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Listless macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 5E88DDD119EEEECCD45F36D2 /* TaskStoreCompletionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStoreCompletionTests.swift; sourceTree = "<group>"; }; 632DA39B24C4CF1528A1A24D /* TaskListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListView.swift; sourceTree = "<group>"; }; 658295C1386BFF48CE3C2419 /* UndoToast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoToast.swift; sourceTree = "<group>"; }; 68A677CC11ACE0BF743AFCE5 /* CloudKitErrorClassifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitErrorClassifier.swift; sourceTree = "<group>"; }; + 6BE7F4637B4F4C1FF4BE160B /* ListlessWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListlessWatchApp.swift; sourceTree = "<group>"; }; 6E3E82F6093EEFC94A41FED9 /* CloudKitSyncMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitSyncMonitor.swift; sourceTree = "<group>"; }; 6ECE0E961F87BA32FA87BF90 /* TappableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableTextField.swift; sourceTree = "<group>"; }; 6F9754B32B2FD5AF8552BC85 /* TaskListTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListTypes.swift; sourceTree = "<group>"; }; 712ADAD0FE66175DAA9A6D50 /* TaskListView+NavigationHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+NavigationHeader.swift"; sourceTree = "<group>"; }; 72B4668483D05A6ECA142B89 /* TaskListView+Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Logic.swift"; sourceTree = "<group>"; }; + 72BAC76C5B5ED291048705CF /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; }; 74255E6B6C40899E9B17D927 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; }; 75B048B19C5219862BBED2E7 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; }; 7A7BD42B1E3C71333FA24893 /* TaskListView+SyncUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+SyncUI.swift"; sourceTree = "<group>"; }; 7C73E9D4C42CCABBF0F33543 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; }; 8AACB5E40BEBA75E7FE8B930 /* TaskListView+Undo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Undo.swift"; sourceTree = "<group>"; }; 9262207DAC21619BD9EDEE15 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; }; + 92D1AF0DE30657AAD86482CA /* TaskRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowView.swift; sourceTree = "<group>"; }; 9404C09EE1A4D91DFF338464 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; }; 944BAE054AAC1B9C4FC954F9 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; }; 967F7ECEB3915CEDCE584872 /* TaskStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStoreTests.swift; sourceTree = "<group>"; }; @@ -138,9 +176,11 @@ AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListlessiOSApp.swift; sourceTree = "<group>"; }; B7588879D0FA1C2A8BCEF14F /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; }; BAE264D30C7692457B92E518 /* KeyValueSyncBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueSyncBridge.swift; sourceTree = "<group>"; }; + BC845482926A73B0BF820328 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; BFF7D84B54AE70036D205CA4 /* PullToCreate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToCreate.swift; sourceTree = "<group>"; }; C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; }; C611E04943F1D82D6F975592 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; + C6812E535A24C599C28F9278 /* Listless watchOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Listless watchOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; C71466C5CD1A5BA984352F8D /* Listless iOS Unit Tests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = "Listless iOS Unit Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; C9B14DC786A336008AAB78EE /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; }; CB43816B8E7F083A2AD07F28 /* TaskListView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Toolbar.swift"; sourceTree = "<group>"; }; @@ -182,12 +222,24 @@ path = Helpers; sourceTree = "<group>"; }; + 21CCBC7FC4A2D6E15D47B4D1 /* ListlessWatch */ = { + isa = PBXGroup; + children = ( + BC845482926A73B0BF820328 /* Info.plist */, + 72BAC76C5B5ED291048705CF /* Listless.entitlements */, + 6BE7F4637B4F4C1FF4BE160B /* ListlessWatchApp.swift */, + F0EB58775713E4C83A8D477A /* Views */, + ); + path = ListlessWatch; + sourceTree = "<group>"; + }; 3936BDEE64D16E6C4C85B3DD /* Products */ = { isa = PBXGroup; children = ( C71466C5CD1A5BA984352F8D /* Listless iOS Unit Tests.xctest */, 126108860D7878DDC3BECC4B /* Listless iOS.app */, 5B0E22B8F7B2B7283CAF749E /* Listless macOS.app */, + C6812E535A24C599C28F9278 /* Listless watchOS.app */, ); name = Products; sourceTree = "<group>"; @@ -351,12 +403,22 @@ 58051CBDE2390F9E13647235 /* Listless */, 954AAB8DDFF6E6D6FD6A0A2C /* ListlessiOS */, D5B197AFF26144948D032299 /* ListlessMac */, + 21CCBC7FC4A2D6E15D47B4D1 /* ListlessWatch */, D98A7EE79F8E29297555B801 /* Support */, F63E6E98ABFB43E1B32686B4 /* Unit */, 3936BDEE64D16E6C4C85B3DD /* Products */, ); sourceTree = "<group>"; }; + F0EB58775713E4C83A8D477A /* Views */ = { + isa = PBXGroup; + children = ( + 56DEECB5DC31DE5F34FB55A3 /* TaskListView.swift */, + 92D1AF0DE30657AAD86482CA /* TaskRowView.swift */, + ); + path = Views; + sourceTree = "<group>"; + }; F63E6E98ABFB43E1B32686B4 /* Unit */ = { isa = PBXGroup; children = ( @@ -406,10 +468,12 @@ buildPhases = ( 67F773566AF3BC117AA393B9 /* Sources */, EC5CE716D5948EC163595BF2 /* Resources */, + A9226A0DD67E77D754BA0E3F /* Embed Watch Content */, ); buildRules = ( ); dependencies = ( + 59112A75EB31AFE05E360567 /* PBXTargetDependency */, ); name = "Listless iOS"; packageProductDependencies = ( @@ -418,6 +482,24 @@ productReference = 126108860D7878DDC3BECC4B /* Listless iOS.app */; productType = "com.apple.product-type.application"; }; + 9BDC1B2175AB9CE26790448D /* Listless watchOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = A549D68557611D588CB834B2 /* Build configuration list for PBXNativeTarget "Listless watchOS" */; + buildPhases = ( + 4C15FB4D4E33FCF0F596A6E8 /* Sources */, + 36267AB69A85A7E9793142D8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Listless watchOS"; + packageProductDependencies = ( + ); + productName = "Listless watchOS"; + productReference = C6812E535A24C599C28F9278 /* Listless watchOS.app */; + productType = "com.apple.product-type.application"; + }; D533FDEDCE6DFCA2E8CB70F5 /* Listless iOS Unit Tests */ = { isa = PBXNativeTarget; buildConfigurationList = 1993E9E4443DC6AB506DE24C /* Build configuration list for PBXNativeTarget "Listless iOS Unit Tests" */; @@ -453,6 +535,10 @@ DevelopmentTeam = 7TD7PZBNXP; ProvisioningStyle = Automatic; }; + 9BDC1B2175AB9CE26790448D = { + DevelopmentTeam = 7TD7PZBNXP; + ProvisioningStyle = Automatic; + }; D533FDEDCE6DFCA2E8CB70F5 = { DevelopmentTeam = 7TD7PZBNXP; ProvisioningStyle = Automatic; @@ -476,11 +562,20 @@ 34A03D42B91730DEAC2EBD8E /* Listless iOS */, D533FDEDCE6DFCA2E8CB70F5 /* Listless iOS Unit Tests */, 0FB4F07A37999BBC6DFE4DBB /* Listless macOS */, + 9BDC1B2175AB9CE26790448D /* Listless watchOS */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 36267AB69A85A7E9793142D8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A8D5A7B0DFBEC87501FD0526 /* Media.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6BCB6F65428622E5AA66936D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -532,6 +627,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4C15FB4D4E33FCF0F596A6E8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 543C8A0C8A9E2F77B2C0060F /* AccentColor.swift in Sources */, + F046B5A7F4C0BD7AAF46A69C /* CloudKitErrorClassifier.swift in Sources */, + 53700EA974FE4AD771FE89EC /* CloudKitSyncMonitor.swift in Sources */, + 264BD64C1DD30376E8BDAF79 /* KeyValueSyncBridge.swift in Sources */, + 6FE9D247153209BD4CFD9E34 /* Listless.xcdatamodeld in Sources */, + 65E97DE8C190E9E9B71EC356 /* ListlessWatchApp.swift in Sources */, + A119D0130DB77E30FBCB5436 /* PersistenceController.swift in Sources */, + 684A80B692237B74EDF3C7A8 /* TaskItem.swift in Sources */, + 07E2BB2FF9E75A922C3756AB /* TaskListView.swift in Sources */, + 03BC4562430EC105221AB43B /* TaskRowView.swift in Sources */, + 57E864F71C9C84B63A28E14F /* TaskStore.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 67F773566AF3BC117AA393B9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -599,6 +712,11 @@ target = 34A03D42B91730DEAC2EBD8E /* Listless iOS */; targetProxy = 6F7E207AE10E1516EF8A683C /* PBXContainerItemProxy */; }; + 59112A75EB31AFE05E360567 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9BDC1B2175AB9CE26790448D /* Listless watchOS */; + targetProxy = 642EE6A5908DB4216F049E9C /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -620,6 +738,21 @@ }; name = Debug; }; + 8A9B0D66DB5817E1F012B831 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = ListlessWatch/Listless.entitlements; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = ListlessWatch/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = net.inqk.listless.watchos; + PRODUCT_NAME = Listless; + SDKROOT = watchos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 4; + }; + name = Release; + }; A5A377EA0FE470803E2B6BA1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -699,9 +832,25 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 6; + WATCHOS_DEPLOYMENT_TARGET = 11.0; }; name = Release; }; + B40F5AED88880BE1230F8BD0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = ListlessWatch/Listless.entitlements; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = ListlessWatch/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = net.inqk.listless.watchos; + PRODUCT_NAME = Listless; + SDKROOT = watchos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 4; + }; + name = Debug; + }; C00090C364E4A0B81DC878F0 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -788,6 +937,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 6; + WATCHOS_DEPLOYMENT_TARGET = 11.0; }; name = Debug; }; @@ -866,6 +1016,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + A549D68557611D588CB834B2 /* Build configuration list for PBXNativeTarget "Listless watchOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B40F5AED88880BE1230F8BD0 /* Debug */, + 8A9B0D66DB5817E1F012B831 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; B7581C7E1D90658668F3AAE2 /* Build configuration list for PBXNativeTarget "Listless iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Listless.xcodeproj/xcshareddata/xcschemes/Listless watchOS.xcscheme b/Listless.xcodeproj/xcshareddata/xcschemes/Listless watchOS.xcscheme @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1430" + version = "1.7"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <PreActions> + <ExecutionAction + ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction"> + <ActionContent + title = "Run Script" + scriptText = "cd &quot;${SRCROOT}&quot;&#10;buildYear=`git log -1 --format=%cd --date=format:%Y`&#10;buildVersion=`git rev-list HEAD | wc -l | tr -d &apos; &apos;`&#10;echo &quot;CURRENT_PROJECT_VERSION = $buildYear.$buildVersion&quot; &gt; Generated/BuildNumber.xcconfig&#10;"> + <EnvironmentBuildable> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" + BuildableName = "Listless.app" + BlueprintName = "Listless watchOS" + ReferencedContainer = "container:Listless.xcodeproj"> + </BuildableReference> + </EnvironmentBuildable> + </ActionContent> + </ExecutionAction> + </PreActions> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" + BuildableName = "Listless.app" + BlueprintName = "Listless watchOS" + ReferencedContainer = "container:Listless.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" + BuildableName = "Listless.app" + BlueprintName = "Listless watchOS" + ReferencedContainer = "container:Listless.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" + BuildableName = "Listless.app" + BlueprintName = "Listless watchOS" + ReferencedContainer = "container:Listless.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" + BuildableName = "Listless.app" + BlueprintName = "Listless watchOS" + ReferencedContainer = "container:Listless.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/ListlessWatch/Info.plist b/ListlessWatch/Info.plist @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleDisplayName</key> + <string>Listless</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>$(CURRENT_PROJECT_VERSION)</string> + <key>ITSAppUsesNonExemptEncryption</key> + <false/> + <key>WKApplication</key> + <true/> + <key>WKCompanionAppBundleIdentifier</key> + <string>net.inqk.listless</string> +</dict> +</plist> diff --git a/ListlessWatch/Listless.entitlements b/ListlessWatch/Listless.entitlements @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>aps-environment</key> + <string>development</string> + <key>com.apple.developer.icloud-container-identifiers</key> + <array> + <string>iCloud.net.inqk.listless</string> + </array> + <key>com.apple.developer.icloud-services</key> + <array> + <string>CloudKit</string> + </array> + <key>com.apple.developer.ubiquity-container-identifiers</key> + <array> + <string>iCloud.net.inqk.listless</string> + </array> +</dict> +</plist> diff --git a/ListlessWatch/ListlessWatchApp.swift b/ListlessWatch/ListlessWatchApp.swift @@ -0,0 +1,16 @@ +import SwiftUI + +@main +struct ListlessWatchApp: App { + private let persistenceController = PersistenceController.shared + + var body: some Scene { + WindowGroup { + TaskListView( + store: TaskStore(persistenceController: persistenceController), + syncMonitor: persistenceController.syncMonitor + ) + .environment(\.managedObjectContext, persistenceController.viewContext) + } + } +} diff --git a/ListlessWatch/Views/TaskListView.swift b/ListlessWatch/Views/TaskListView.swift @@ -0,0 +1,70 @@ +import CoreData +import SwiftUI + +struct TaskListView: View { + let store: TaskStore + let syncMonitor: CloudKitSyncMonitor + + @FetchRequest( + sortDescriptors: [ + SortDescriptor(\TaskItem.isCompleted, order: .forward), + SortDescriptor(\TaskItem.sortOrder, order: .forward), + ], + animation: .default + ) + private var tasks: FetchedResults<TaskItem> + + var body: some View { + let activeTasks = tasks.filter { !$0.isCompleted } + let completedTasks = tasks.filter { $0.isCompleted } + + NavigationStack { + Group { + if tasks.isEmpty { + ContentUnavailableView( + "No Items", + systemImage: "checklist", + description: Text("Add items on your iPhone or Mac.") + ) + } else { + List { + ForEach(Array(activeTasks.enumerated()), id: \.element.id) { index, task in + TaskRowView( + task: task, + index: index, + totalActive: activeTasks.count, + onToggle: { toggleTask($0) } + ) + } + + if !completedTasks.isEmpty { + Section("Completed") { + ForEach(completedTasks) { task in + TaskRowView( + task: task, + index: 0, + totalActive: 0, + onToggle: { toggleTask($0) } + ) + } + } + } + } + } + } + .navigationTitle("Listless") + } + } + + private func toggleTask(_ task: TaskItem) { + do { + if task.isCompleted { + try store.uncomplete(taskID: task.id) + } else { + try store.complete(taskID: task.id) + } + } catch { + // Sync monitor handles error reporting + } + } +} diff --git a/ListlessWatch/Views/TaskRowView.swift b/ListlessWatch/Views/TaskRowView.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct TaskRowView: View { + let task: TaskItem + let index: Int + let totalActive: Int + let onToggle: (TaskItem) -> Void + + var body: some View { + Button { + onToggle(task) + } label: { + HStack(spacing: 8) { + Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") + .foregroundStyle( + task.isCompleted + ? .secondary + : cachedTaskColor(forIndex: index, total: totalActive) + ) + .font(.system(size: 17)) + + Text(task.title) + .strikethrough(task.isCompleted) + .foregroundStyle(task.isCompleted ? .secondary : .primary) + .lineLimit(3) + } + } + .buttonStyle(.plain) + } +} diff --git a/Media.xcassets/AppIcon.appiconset/Contents.json b/Media.xcassets/AppIcon.appiconset/Contents.json @@ -87,6 +87,12 @@ "idiom" : "mac", "scale" : "2x", "size" : "512x512" + }, + { + "filename" : "Icon.png", + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" } ], "info" : { diff --git a/Scripts/publish-ios.sh b/Scripts/publish-ios.sh @@ -78,6 +78,8 @@ cat > "$EXPORT_PLIST" <<PLIST <dict> <key>net.inqk.listless</key> <string>Listless iOS Distribution</string> + <key>net.inqk.listless.watchos</key> + <string>Listless watchOS Distribution</string> </dict> <key>destination</key> <string>export</string> diff --git a/project.yml b/project.yml @@ -5,6 +5,7 @@ options: deploymentTarget: iOS: "18.0" macOS: "14.0" + watchOS: "11.0" configFiles: Debug: Generated/BuildNumber.xcconfig @@ -60,6 +61,8 @@ targets: - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight + dependencies: + - target: Listless watchOS settings: PRODUCT_BUNDLE_IDENTIFIER: net.inqk.listless INFOPLIST_FILE: ListlessiOS/Info.plist @@ -107,6 +110,42 @@ targets: ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon CODE_SIGN_STYLE: Automatic + Listless watchOS: + type: application + platform: watchOS + sources: + - path: Listless/Models + - path: Listless/Sync + - path: Listless/Helpers/AccentColor.swift + type: file + - ListlessWatch + - Media.xcassets + entitlements: + path: ListlessWatch/Listless.entitlements + properties: + com.apple.developer.icloud-container-identifiers: + - iCloud.net.inqk.listless + com.apple.developer.icloud-services: + - CloudKit + com.apple.developer.ubiquity-container-identifiers: + - iCloud.net.inqk.listless + aps-environment: development + info: + path: ListlessWatch/Info.plist + properties: + CFBundleDisplayName: Listless + CFBundleShortVersionString: "1.0" + CFBundleVersion: $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption: false + WKApplication: true + WKCompanionAppBundleIdentifier: net.inqk.listless + settings: + PRODUCT_BUNDLE_IDENTIFIER: net.inqk.listless.watchos + PRODUCT_NAME: Listless + INFOPLIST_FILE: ListlessWatch/Info.plist + ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon + CODE_SIGN_STYLE: Automatic + Listless iOS Unit Tests: type: bundle.unit-test platform: iOS @@ -168,3 +207,23 @@ schemes: config: Debug archive: config: Release + + Listless watchOS: + build: + targets: + Listless watchOS: all + preActions: + - script: | + cd "${SRCROOT}" + buildYear=`git log -1 --format=%cd --date=format:%Y` + buildVersion=`git rev-list HEAD | wc -l | tr -d ' '` + echo "CURRENT_PROJECT_VERSION = $buildYear.$buildVersion" > Generated/BuildNumber.xcconfig + settingsTarget: Listless watchOS + run: + config: Debug + profile: + config: Release + analyze: + config: Debug + archive: + config: Release