commit 61f90d7cd4c0681e709a2240e899e56614d773b9
parent 7d831d8ddffddb726ec25e3065572a83884564f4
Author: Michael Camilleri <[email protected]>
Date: Wed, 18 Feb 2026 03:15:51 +0900
Restructure and add first version of drag-and-drop reordering on iOS
Co-Authored-By: Claude 4.6 Opus <[email protected]>
Diffstat
25 files changed, 274 insertions(+), 320 deletions(-)
diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj
@@ -7,48 +7,48 @@
objects = {
/* Begin PBXBuildFile section */
- 074A81D9DAF58E8E088CBC89 /* TaskListView+Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431B0EA72D590AF0CC868515 /* TaskListView+Toolbar.swift */; };
0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; };
- 15B71073767FB4766A6BA2BE /* HoverCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41D6E0CED14D79F31C45062 /* HoverCursorModifier.swift */; };
+ 0F12D56A528FCBF8A67864CB /* TappableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECE0E961F87BA32FA87BF90 /* TappableTextField.swift */; };
+ 172F2DD978988E207610055F /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4669414A460FC0758D5B49A8 /* KeyboardNavigationModifier.swift */; };
+ 182D9FB61E3A3650D7D83D2A /* TaskRowSwipeGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067D666DF0AF1C53404ECF7C /* TaskRowSwipeGesture.swift */; };
1AA328A921EF8A7FDD03119A /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B048B19C5219862BBED2E7 /* TestHelpers.swift */; };
- 1E3857004DDD888BB4A6ED50 /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0630FFEEFF7662A414E9A8 /* AccentColor.swift */; };
+ 2374527E3629DD87AE3D2761 /* TaskListView+PullToCreate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A741111B4DEB3A7406A95C93 /* TaskListView+PullToCreate.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 */; };
+ 322EE74CBED492693B92AD59 /* TaskListView+Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A440253D045A0896D94ADD0 /* TaskListView+Toolbar.swift */; };
3ABE52A15C2059D8D5570528 /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEE364304587D280C5672 /* TaskStore.swift */; };
3D1F551A03B97ECF4E3DC8B0 /* TaskRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06485DBE35B60868E14202A /* TaskRowView.swift */; };
- 3FCEFB586D9A9085A201AE7D /* TaskListView+PullToCreate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BF73E1B8FE337B76D3E757 /* TaskListView+PullToCreate.swift */; };
42E4CDE1D17463554CC4F41F /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537A913AC421BAEF60D26D9C /* TaskListView.swift */; };
+ 5035EC4C7518A5FF9AD454CA /* TaskRowDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F416DD868A4C044F0D64F8D0 /* TaskRowDragGesture.swift */; };
+ 5761B201BF46FCA9C5C98CEF /* PlatformScrollIndicatorsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */; };
5B60B409CE4BA668DB30A65D /* Listless.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C093494053E6C348F245D4EC /* Listless.xcdatamodeld */; };
- 5CFFBC8A17E065640857540A /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0630FFEEFF7662A414E9A8 /* AccentColor.swift */; };
+ 5D3EE9526DA269EE9EE3AB52 /* AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E998119283F784B9ADEE28 /* AppColors.swift */; };
614FCCA450EC0BFFD8B40640 /* ListlessMacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */; };
- 6C252050E62AED3A0A684EBF /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7B37EF6A2389656D105FF8 /* KeyboardNavigationModifier.swift */; };
- 731477635D3F4DFF1F78D673 /* AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1FE1858BC9E8915A091D33 /* AppColors.swift */; };
- 7B2CD636BA3B63A586F93E31 /* TaskRowSwipeGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C16D36971A140364159FB9 /* TaskRowSwipeGesture.swift */; };
+ 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 */; };
7E366008FF9335A77FE1636D /* TaskRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 199CC2F58DD7CBA3F2229366 /* TaskRowView.swift */; };
- 83378A0575640417ED41FC25 /* ClickableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F2BB7ABCABA4EB67180A200 /* ClickableTextField.swift */; };
+ 80BCA654FE694D8A77D9BF93 /* TaskListView+PullToCreate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD8D4C29E09FEE78AE5AB79 /* TaskListView+PullToCreate.swift */; };
+ 882695E7AE463C0F39ACFF3C /* HoverCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5D8B5866362D422A2A331C /* HoverCursorModifier.swift */; };
8E08F4C82D6F9E67667CB20A /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537A913AC421BAEF60D26D9C /* TaskListView.swift */; };
- 8E680EC18B0339931E785F0C /* PlatformScrollIndicatorsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48AE1AE43296C1692FA6F755 /* PlatformScrollIndicatorsModifier.swift */; };
- 907478D07E5CCD4966579306 /* TaskListView+NavigationHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0C28F7AF2AFAF677F678087 /* TaskListView+NavigationHeader.swift */; };
91EDF52C7C5C0B35E9D8B51E /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEE364304587D280C5672 /* TaskStore.swift */; };
- 92932AEC64B65AD6F95A1E42 /* TaskListView+PullToCreate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF51ACA55E28AA1A11D3962C /* TaskListView+PullToCreate.swift */; };
96617677059FABDBB80D642B /* Listless.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C093494053E6C348F245D4EC /* Listless.xcdatamodeld */; };
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 */; };
- A8D13DD6196772ECA146CF2A /* PlatformScrollIndicatorsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D74AE2501F35974F57D21F /* PlatformScrollIndicatorsModifier.swift */; };
- AB5D681B98FC1D2FD7BF2FAA /* TaskListView+NavigationHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AF738EEB6ABDF86B0B781F /* TaskListView+NavigationHeader.swift */; };
- C1BD61D6DFA687E9CB9ACA60 /* TaskRowDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138612F350ECE29526F689B9 /* TaskRowDragGesture.swift */; };
+ BA6953E0EFE6F8255F05A3FD /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */; };
C1FE091454864C4BBBBEB077 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; };
- D6B3DBDD6A3F0A6E166CFFD5 /* TaskRowDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B3D0B4710C7A0EE4F227851 /* TaskRowDragGesture.swift */; };
- D878CD3A552C6A9685A30AA8 /* PlatformTextFieldWidthModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF6B0195EA0176B72DE9B092 /* PlatformTextFieldWidthModifier.swift */; };
- D8EFF49E9156083D675D47F0 /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7B37EF6A2389656D105FF8 /* KeyboardNavigationModifier.swift */; };
- D9DA553485AD0CA28D5BE0C8 /* TaskListView+Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93FF5B9F5B979D54D5DEE192 /* TaskListView+Toolbar.swift */; };
- DC71C45D92524C3893BF9FDB /* PlatformTextFieldWidthModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81880970CCFC2CB00B65047E /* PlatformTextFieldWidthModifier.swift */; };
+ CAC6A40C680611163775CDFE /* TaskListView+NavigationHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E67FE94ED8E9B248A0C3A97 /* TaskListView+NavigationHeader.swift */; };
+ DB5FF6C1AA57D4C9BDDD50FD /* ClickableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D640E7D21735C62A30A26DA4 /* ClickableTextField.swift */; };
DC73A39A269AB495BCE1AC48 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */; };
- E3C6D4AE5E729D34086F132D /* TappableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95568F6A132636D04B8CF593 /* TappableTextField.swift */; };
+ DEE187A790A4058FE4AFDB2E /* PlatformTextFieldWidthModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8448C5778F75F52719114AF /* PlatformTextFieldWidthModifier.swift */; };
E47136CA7428927395D8C7C7 /* PullToCreate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF7D84B54AE70036D205CA4 /* PullToCreate.swift */; };
+ E60F81C7B930AACE67746759 /* HoverCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7588879D0FA1C2A8BCEF14F /* HoverCursorModifier.swift */; };
ECD5E7EA05AE1C00B38C939E /* TaskStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967F7ECEB3915CEDCE584872 /* TaskStoreTests.swift */; };
F0B2B806BD84A4F2FDF8E038 /* ListlessiOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */; };
F50E8B8F73F64D8E641DC74C /* TaskStoreCompletionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E88DDD119EEEECCD45F36D2 /* TaskStoreCompletionTests.swift */; };
- FEC96DAA2BF8BB5D6D8504EB /* HoverCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82A3509AD32A54434BCC8017 /* HoverCursorModifier.swift */; };
+ F6587B84ECC6BFE92A5FB493 /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4669414A460FC0758D5B49A8 /* KeyboardNavigationModifier.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -63,53 +63,53 @@
/* Begin PBXFileReference section */
01E141436176F83594E2F26B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
- 0E7B37EF6A2389656D105FF8 /* KeyboardNavigationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardNavigationModifier.swift; sourceTree = "<group>"; };
- 0F2BB7ABCABA4EB67180A200 /* ClickableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClickableTextField.swift; sourceTree = "<group>"; };
+ 067D666DF0AF1C53404ECF7C /* TaskRowSwipeGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowSwipeGesture.swift; sourceTree = "<group>"; };
126108860D7878DDC3BECC4B /* Listless iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "Listless iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
- 138612F350ECE29526F689B9 /* TaskRowDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowDragGesture.swift; sourceTree = "<group>"; };
+ 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentColor.swift; sourceTree = "<group>"; };
199CC2F58DD7CBA3F2229366 /* TaskRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowView.swift; sourceTree = "<group>"; };
- 1B3D0B4710C7A0EE4F227851 /* TaskRowDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowDragGesture.swift; sourceTree = "<group>"; };
1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListlessMacApp.swift; sourceTree = "<group>"; };
+ 2A440253D045A0896D94ADD0 /* TaskListView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Toolbar.swift"; sourceTree = "<group>"; };
2D3A2DDCE24E54ABCCFBBD4C /* TaskStoreOrderingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStoreOrderingTests.swift; sourceTree = "<group>"; };
3313FEDB101EECA4B344EEF4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
- 431B0EA72D590AF0CC868515 /* TaskListView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Toolbar.swift"; sourceTree = "<group>"; };
- 44C16D36971A140364159FB9 /* TaskRowSwipeGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowSwipeGesture.swift; sourceTree = "<group>"; };
- 48AE1AE43296C1692FA6F755 /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; };
+ 3E67FE94ED8E9B248A0C3A97 /* TaskListView+NavigationHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+NavigationHeader.swift"; sourceTree = "<group>"; };
+ 4669414A460FC0758D5B49A8 /* KeyboardNavigationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardNavigationModifier.swift; sourceTree = "<group>"; };
+ 466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; };
4FC64B9F9370041BEDBD1E14 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
537A913AC421BAEF60D26D9C /* TaskListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListView.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>"; };
+ 6ECE0E961F87BA32FA87BF90 /* TappableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableTextField.swift; sourceTree = "<group>"; };
+ 712ADAD0FE66175DAA9A6D50 /* TaskListView+NavigationHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+NavigationHeader.swift"; sourceTree = "<group>"; };
74255E6B6C40899E9B17D927 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
75B048B19C5219862BBED2E7 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
7C73E9D4C42CCABBF0F33543 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; };
- 7F0630FFEEFF7662A414E9A8 /* AccentColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentColor.swift; sourceTree = "<group>"; };
- 81880970CCFC2CB00B65047E /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
- 82A3509AD32A54434BCC8017 /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; };
9262207DAC21619BD9EDEE15 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; };
- 93FF5B9F5B979D54D5DEE192 /* TaskListView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Toolbar.swift"; sourceTree = "<group>"; };
944BAE054AAC1B9C4FC954F9 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
- 95568F6A132636D04B8CF593 /* TappableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableTextField.swift; sourceTree = "<group>"; };
- 95BF73E1B8FE337B76D3E757 /* TaskListView+PullToCreate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+PullToCreate.swift"; sourceTree = "<group>"; };
967F7ECEB3915CEDCE584872 /* TaskStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStoreTests.swift; sourceTree = "<group>"; };
+ 9AD8D4C29E09FEE78AE5AB79 /* TaskListView+PullToCreate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+PullToCreate.swift"; sourceTree = "<group>"; };
9B2BBE01D99CDA278BCB9F49 /* TaskStoreEdgeCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStoreEdgeCaseTests.swift; sourceTree = "<group>"; };
+ 9F5D8B5866362D422A2A331C /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; };
9FBEB58DD41817F09B0EB9F0 /* Listless.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Listless.xcdatamodel; sourceTree = "<group>"; };
- A0C28F7AF2AFAF677F678087 /* TaskListView+NavigationHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+NavigationHeader.swift"; sourceTree = "<group>"; };
- A7AF738EEB6ABDF86B0B781F /* TaskListView+NavigationHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+NavigationHeader.swift"; sourceTree = "<group>"; };
+ A741111B4DEB3A7406A95C93 /* TaskListView+PullToCreate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+PullToCreate.swift"; sourceTree = "<group>"; };
AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListlessiOSApp.swift; sourceTree = "<group>"; };
- B4D74AE2501F35974F57D21F /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; };
+ B7588879D0FA1C2A8BCEF14F /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; 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>"; };
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>"; };
D10B5491A53E77C80F8F75CD /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
D123BB181208FC825777B0A7 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
- D41D6E0CED14D79F31C45062 /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; };
+ D57C3CC81C4380EFAE4DB910 /* TaskRowDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowDragGesture.swift; sourceTree = "<group>"; };
+ D640E7D21735C62A30A26DA4 /* ClickableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClickableTextField.swift; sourceTree = "<group>"; };
DC3DEE364304587D280C5672 /* TaskStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStore.swift; sourceTree = "<group>"; };
- DF51ACA55E28AA1A11D3962C /* TaskListView+PullToCreate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+PullToCreate.swift"; sourceTree = "<group>"; };
E06485DBE35B60868E14202A /* TaskRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowView.swift; sourceTree = "<group>"; };
- EF6B0195EA0176B72DE9B092 /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
+ E2174C43654733E9D4023157 /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
+ E8448C5778F75F52719114AF /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
+ F1E998119283F784B9ADEE28 /* AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppColors.swift; sourceTree = "<group>"; };
+ F416DD868A4C044F0D64F8D0 /* TaskRowDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowDragGesture.swift; sourceTree = "<group>"; };
+ FA5D58EC1FBAA96E83A79445 /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; };
FBB8A3BEB346267B30B4675F /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
- FD1FE1858BC9E8915A091D33 /* AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppColors.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
@@ -117,13 +117,25 @@
isa = PBXGroup;
children = (
D123BB181208FC825777B0A7 /* .gitkeep */,
- 7F0630FFEEFF7662A414E9A8 /* AccentColor.swift */,
- 0E7B37EF6A2389656D105FF8 /* KeyboardNavigationModifier.swift */,
537A913AC421BAEF60D26D9C /* TaskListView.swift */,
);
path = Views;
sourceTree = "<group>";
};
+ 19A2FAA566465414CA20E6D8 /* Helpers */ = {
+ isa = PBXGroup;
+ children = (
+ F1E998119283F784B9ADEE28 /* AppColors.swift */,
+ B7588879D0FA1C2A8BCEF14F /* HoverCursorModifier.swift */,
+ FA5D58EC1FBAA96E83A79445 /* PlatformScrollIndicatorsModifier.swift */,
+ E2174C43654733E9D4023157 /* PlatformTextFieldWidthModifier.swift */,
+ 6ECE0E961F87BA32FA87BF90 /* TappableTextField.swift */,
+ D57C3CC81C4380EFAE4DB910 /* TaskRowDragGesture.swift */,
+ 067D666DF0AF1C53404ECF7C /* TaskRowSwipeGesture.swift */,
+ );
+ path = Helpers;
+ sourceTree = "<group>";
+ };
3936BDEE64D16E6C4C85B3DD /* Products */ = {
isa = PBXGroup;
children = (
@@ -134,9 +146,22 @@
name = Products;
sourceTree = "<group>";
};
+ 42CC0393E2524624AEC54D03 /* Helpers */ = {
+ isa = PBXGroup;
+ children = (
+ D640E7D21735C62A30A26DA4 /* ClickableTextField.swift */,
+ 9F5D8B5866362D422A2A331C /* HoverCursorModifier.swift */,
+ 466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */,
+ E8448C5778F75F52719114AF /* PlatformTextFieldWidthModifier.swift */,
+ F416DD868A4C044F0D64F8D0 /* TaskRowDragGesture.swift */,
+ );
+ path = Helpers;
+ sourceTree = "<group>";
+ };
58051CBDE2390F9E13647235 /* Listless */ = {
isa = PBXGroup;
children = (
+ 78E0184C210B140892690CD4 /* Helpers */,
AA563E991F69ED14DCD9A1AB /* Infrastructure */,
8656EEF3161BE20196B8042E /* Models */,
E98EF3B4638A9E8473EA62FA /* Sync */,
@@ -148,22 +173,31 @@
58F917D865E0BDF4EF282306 /* Views */ = {
isa = PBXGroup;
children = (
- FD1FE1858BC9E8915A091D33 /* AppColors.swift */,
- 82A3509AD32A54434BCC8017 /* HoverCursorModifier.swift */,
- 48AE1AE43296C1692FA6F755 /* PlatformScrollIndicatorsModifier.swift */,
- 81880970CCFC2CB00B65047E /* PlatformTextFieldWidthModifier.swift */,
BFF7D84B54AE70036D205CA4 /* PullToCreate.swift */,
- 95568F6A132636D04B8CF593 /* TappableTextField.swift */,
- A0C28F7AF2AFAF677F678087 /* TaskListView+NavigationHeader.swift */,
- DF51ACA55E28AA1A11D3962C /* TaskListView+PullToCreate.swift */,
- 93FF5B9F5B979D54D5DEE192 /* TaskListView+Toolbar.swift */,
- 1B3D0B4710C7A0EE4F227851 /* TaskRowDragGesture.swift */,
- 44C16D36971A140364159FB9 /* TaskRowSwipeGesture.swift */,
199CC2F58DD7CBA3F2229366 /* TaskRowView.swift */,
);
path = Views;
sourceTree = "<group>";
};
+ 78E0184C210B140892690CD4 /* Helpers */ = {
+ isa = PBXGroup;
+ children = (
+ 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */,
+ 4669414A460FC0758D5B49A8 /* KeyboardNavigationModifier.swift */,
+ );
+ path = Helpers;
+ sourceTree = "<group>";
+ };
+ 8629C1C94770B3B0D08B580D /* Extensions */ = {
+ isa = PBXGroup;
+ children = (
+ 712ADAD0FE66175DAA9A6D50 /* TaskListView+NavigationHeader.swift */,
+ 9AD8D4C29E09FEE78AE5AB79 /* TaskListView+PullToCreate.swift */,
+ CB43816B8E7F083A2AD07F28 /* TaskListView+Toolbar.swift */,
+ );
+ path = Extensions;
+ sourceTree = "<group>";
+ };
8656EEF3161BE20196B8042E /* Models */ = {
isa = PBXGroup;
children = (
@@ -181,6 +215,8 @@
3313FEDB101EECA4B344EEF4 /* Info.plist */,
9262207DAC21619BD9EDEE15 /* Listless.entitlements */,
AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */,
+ 8629C1C94770B3B0D08B580D /* Extensions */,
+ 19A2FAA566465414CA20E6D8 /* Helpers */,
58F917D865E0BDF4EF282306 /* Views */,
);
path = ListlessiOS;
@@ -197,14 +233,6 @@
D12ECC901ABED96B86CC85B5 /* Views */ = {
isa = PBXGroup;
children = (
- 0F2BB7ABCABA4EB67180A200 /* ClickableTextField.swift */,
- D41D6E0CED14D79F31C45062 /* HoverCursorModifier.swift */,
- B4D74AE2501F35974F57D21F /* PlatformScrollIndicatorsModifier.swift */,
- EF6B0195EA0176B72DE9B092 /* PlatformTextFieldWidthModifier.swift */,
- A7AF738EEB6ABDF86B0B781F /* TaskListView+NavigationHeader.swift */,
- 95BF73E1B8FE337B76D3E757 /* TaskListView+PullToCreate.swift */,
- 431B0EA72D590AF0CC868515 /* TaskListView+Toolbar.swift */,
- 138612F350ECE29526F689B9 /* TaskRowDragGesture.swift */,
E06485DBE35B60868E14202A /* TaskRowView.swift */,
);
path = Views;
@@ -216,6 +244,8 @@
01E141436176F83594E2F26B /* Info.plist */,
7C73E9D4C42CCABBF0F33543 /* Listless.entitlements */,
1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */,
+ F7279CE4B2501F9F5111A3D8 /* Extensions */,
+ 42CC0393E2524624AEC54D03 /* Helpers */,
D12ECC901ABED96B86CC85B5 /* Views */,
);
path = ListlessMac;
@@ -265,6 +295,16 @@
path = Tests/Unit;
sourceTree = "<group>";
};
+ F7279CE4B2501F9F5111A3D8 /* Extensions */ = {
+ isa = PBXGroup;
+ children = (
+ 3E67FE94ED8E9B248A0C3A97 /* TaskListView+NavigationHeader.swift */,
+ A741111B4DEB3A7406A95C93 /* TaskListView+PullToCreate.swift */,
+ 2A440253D045A0896D94ADD0 /* TaskListView+Toolbar.swift */,
+ );
+ path = Extensions;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -369,21 +409,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 5CFFBC8A17E065640857540A /* AccentColor.swift in Sources */,
- 83378A0575640417ED41FC25 /* ClickableTextField.swift in Sources */,
- 15B71073767FB4766A6BA2BE /* HoverCursorModifier.swift in Sources */,
- D8EFF49E9156083D675D47F0 /* KeyboardNavigationModifier.swift in Sources */,
+ 99D17075DA3F00F52A18BB4D /* AccentColor.swift in Sources */,
+ DB5FF6C1AA57D4C9BDDD50FD /* ClickableTextField.swift in Sources */,
+ 882695E7AE463C0F39ACFF3C /* HoverCursorModifier.swift in Sources */,
+ 172F2DD978988E207610055F /* KeyboardNavigationModifier.swift in Sources */,
96617677059FABDBB80D642B /* Listless.xcdatamodeld in Sources */,
614FCCA450EC0BFFD8B40640 /* ListlessMacApp.swift in Sources */,
A0AA8FD4C542E9AEB2437BC2 /* PersistenceController.swift in Sources */,
- A8D13DD6196772ECA146CF2A /* PlatformScrollIndicatorsModifier.swift in Sources */,
- D878CD3A552C6A9685A30AA8 /* PlatformTextFieldWidthModifier.swift in Sources */,
+ 5761B201BF46FCA9C5C98CEF /* PlatformScrollIndicatorsModifier.swift in Sources */,
+ DEE187A790A4058FE4AFDB2E /* PlatformTextFieldWidthModifier.swift in Sources */,
C1FE091454864C4BBBBEB077 /* TaskItem.swift in Sources */,
- AB5D681B98FC1D2FD7BF2FAA /* TaskListView+NavigationHeader.swift in Sources */,
- 3FCEFB586D9A9085A201AE7D /* TaskListView+PullToCreate.swift in Sources */,
- 074A81D9DAF58E8E088CBC89 /* TaskListView+Toolbar.swift in Sources */,
+ CAC6A40C680611163775CDFE /* TaskListView+NavigationHeader.swift in Sources */,
+ 2374527E3629DD87AE3D2761 /* TaskListView+PullToCreate.swift in Sources */,
+ 322EE74CBED492693B92AD59 /* TaskListView+Toolbar.swift in Sources */,
8E08F4C82D6F9E67667CB20A /* TaskListView.swift in Sources */,
- C1BD61D6DFA687E9CB9ACA60 /* TaskRowDragGesture.swift in Sources */,
+ 5035EC4C7518A5FF9AD454CA /* TaskRowDragGesture.swift in Sources */,
3D1F551A03B97ECF4E3DC8B0 /* TaskRowView.swift in Sources */,
3ABE52A15C2059D8D5570528 /* TaskStore.swift in Sources */,
);
@@ -393,24 +433,24 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 1E3857004DDD888BB4A6ED50 /* AccentColor.swift in Sources */,
- 731477635D3F4DFF1F78D673 /* AppColors.swift in Sources */,
- FEC96DAA2BF8BB5D6D8504EB /* HoverCursorModifier.swift in Sources */,
- 6C252050E62AED3A0A684EBF /* KeyboardNavigationModifier.swift in Sources */,
+ BA6953E0EFE6F8255F05A3FD /* AccentColor.swift in Sources */,
+ 5D3EE9526DA269EE9EE3AB52 /* AppColors.swift in Sources */,
+ E60F81C7B930AACE67746759 /* HoverCursorModifier.swift in Sources */,
+ F6587B84ECC6BFE92A5FB493 /* KeyboardNavigationModifier.swift in Sources */,
5B60B409CE4BA668DB30A65D /* Listless.xcdatamodeld in Sources */,
F0B2B806BD84A4F2FDF8E038 /* ListlessiOSApp.swift in Sources */,
DC73A39A269AB495BCE1AC48 /* PersistenceController.swift in Sources */,
- 8E680EC18B0339931E785F0C /* PlatformScrollIndicatorsModifier.swift in Sources */,
- DC71C45D92524C3893BF9FDB /* PlatformTextFieldWidthModifier.swift in Sources */,
+ 763363F6F3C7D2D3C9A63977 /* PlatformScrollIndicatorsModifier.swift in Sources */,
+ 77FE96F070B1F7FE31A9CE51 /* PlatformTextFieldWidthModifier.swift in Sources */,
E47136CA7428927395D8C7C7 /* PullToCreate.swift in Sources */,
- E3C6D4AE5E729D34086F132D /* TappableTextField.swift in Sources */,
+ 0F12D56A528FCBF8A67864CB /* TappableTextField.swift in Sources */,
0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */,
- 907478D07E5CCD4966579306 /* TaskListView+NavigationHeader.swift in Sources */,
- 92932AEC64B65AD6F95A1E42 /* TaskListView+PullToCreate.swift in Sources */,
- D9DA553485AD0CA28D5BE0C8 /* TaskListView+Toolbar.swift in Sources */,
+ 26F609518DE1055DF09B0159 /* TaskListView+NavigationHeader.swift in Sources */,
+ 80BCA654FE694D8A77D9BF93 /* TaskListView+PullToCreate.swift in Sources */,
+ 77970A2323A598E830D95301 /* TaskListView+Toolbar.swift in Sources */,
42E4CDE1D17463554CC4F41F /* TaskListView.swift in Sources */,
- D6B3DBDD6A3F0A6E166CFFD5 /* TaskRowDragGesture.swift in Sources */,
- 7B2CD636BA3B63A586F93E31 /* TaskRowSwipeGesture.swift in Sources */,
+ 2C53717A0EAFA39240615C9A /* TaskRowDragGesture.swift in Sources */,
+ 182D9FB61E3A3650D7D83D2A /* TaskRowSwipeGesture.swift in Sources */,
7E366008FF9335A77FE1636D /* TaskRowView.swift in Sources */,
91EDF52C7C5C0B35E9D8B51E /* TaskStore.swift in Sources */,
);
diff --git a/Listless/Views/AccentColor.swift b/Listless/Helpers/AccentColor.swift
diff --git a/Listless/Views/KeyboardNavigationModifier.swift b/Listless/Helpers/KeyboardNavigationModifier.swift
diff --git a/Listless/Views/TaskListView.swift b/Listless/Views/TaskListView.swift
@@ -22,18 +22,12 @@ struct TaskListView: View {
@State var selectedTaskID: UUID?
@State private var refreshID = UUID()
@State private var draggedTaskID: UUID?
- @State private var swipingTaskID: UUID?
@State private var visualOrder: [UUID]?
@State private var pendingFocus: FocusField?
@State var pullOffset: CGFloat = 0
#if os(iOS)
- private struct IOSDragState {
- let taskID: UUID
- var fingerPosition: CGPoint
- }
- @State private var iosDragState: IOSDragState? = nil
+ @State private var isDragging: Bool = false
@State private var rowFrames: [UUID: CGRect] = [:]
- @State private var scrollViewMinY: CGFloat = 0
#endif
init(store: TaskStore = TaskStore()) {
@@ -48,39 +42,34 @@ struct TaskListView: View {
ForEach(Array(displayActiveTasks.enumerated()), id: \.element.id) { index, task in
let taskID = task.id
#if os(iOS)
- Group {
- if iosDragState?.taskID == taskID {
- // Ghost spacer: placeholder silhouette while card is floating
- UnevenRoundedRectangle(
- topLeadingRadius: 0, bottomLeadingRadius: 0,
- bottomTrailingRadius: 14, topTrailingRadius: 14
- )
- .fill(Color.taskCard.opacity(0.4))
- .frame(height: rowFrames[taskID]?.height ?? 56)
- } else {
- TaskRowView(
- task: task,
- taskID: taskID,
- index: index,
- totalTasks: displayActiveTasks.count,
- isSelected: selectedTaskID == taskID,
- focusedField: $focusedField,
- onToggle: { toggleCompletion($0) },
- onTitleChange: { updateTitle($0, $1) },
- onDelete: { deleteTask($0) },
- onSelect: { selectTask($0) },
- onStartEdit: { startEditing($0) },
- onEndEdit: { endEditing($0, shouldCreateNewTask: $1) }
- )
- .taskDragGesture(
- isActive: !task.isCompleted && swipingTaskID == nil,
- taskID: taskID,
- onDragStart: { startDrag(taskID: taskID) },
- onDragChanged: { point in handleIOSDragChanged(taskID: taskID, point: point) },
- onDragEnded: { commitIOSDrag() }
- )
- }
- }
+ TaskRowView(
+ task: task,
+ taskID: taskID,
+ index: index,
+ totalTasks: displayActiveTasks.count,
+ isSelected: selectedTaskID == taskID,
+ isDragging: $isDragging,
+ focusedField: $focusedField,
+ onToggle: { toggleCompletion($0) },
+ onTitleChange: { updateTitle($0, $1) },
+ onDelete: { deleteTask($0) },
+ onSelect: { selectTask($0) },
+ onStartEdit: { startEditing($0) },
+ onEndEdit: { endEditing($0, shouldCreateNewTask: $1) }
+ )
+ .scaleEffect(draggedTaskID == taskID ? 1.03 : 1.0)
+ .shadow(
+ color: draggedTaskID == taskID ? .black.opacity(0.15) : .clear,
+ radius: 8, y: 2
+ )
+ .zIndex(draggedTaskID == taskID ? 1 : 0)
+ .taskDragGesture(
+ isActive: !task.isCompleted,
+ taskID: taskID,
+ onDragStart: { startDrag(taskID: taskID) },
+ onDragChanged: { point in handleIOSDragChanged(taskID: taskID, point: point) },
+ onDragEnded: { commitIOSDrag() }
+ )
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { frame in
@@ -102,7 +91,7 @@ struct TaskListView: View {
onEndEdit: { endEditing($0, shouldCreateNewTask: $1) }
)
.taskDragGesture(
- isActive: !task.isCompleted && swipingTaskID == nil,
+ isActive: !task.isCompleted,
taskID: task.id,
onDragStart: { startDrag(taskID: task.id) }
)
@@ -192,50 +181,10 @@ struct TaskListView: View {
#endif
}
#if os(iOS)
+ .scrollDisabled(draggedTaskID != nil)
.background {
Color.outerBackground.ignoresSafeArea()
}
- .overlay(alignment: .topLeading) {
- if let state = iosDragState,
- let task = activeTasks.first(where: { $0.id == state.taskID }),
- let frame = rowFrames[state.taskID] {
- HStack(spacing: 0) {
- Rectangle()
- .fill(
- taskColor(
- forIndex: visualOrder?.firstIndex(of: state.taskID) ?? 0,
- total: displayActiveTasks.count
- )
- )
- .frame(width: 8)
- Text(task.title.isEmpty ? "New task" : task.title)
- .font(.body)
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding()
- .background(Color.taskCard)
- }
- .clipShape(
- UnevenRoundedRectangle(
- topLeadingRadius: 0, bottomLeadingRadius: 0,
- bottomTrailingRadius: 14, topTrailingRadius: 14
- )
- )
- .shadow(color: .black.opacity(0.18), radius: 10, y: 4)
- .frame(width: frame.width, height: frame.height)
- .position(
- x: UIScreen.main.bounds.midX,
- y: state.fingerPosition.y - scrollViewMinY
- )
- .ignoresSafeArea()
- .allowsHitTesting(false)
- .zIndex(100)
- }
- }
- .onGeometryChange(for: CGFloat.self) { proxy in
- proxy.frame(in: .global).minY
- } action: { minY in
- scrollViewMinY = minY
- }
.onScrollGeometryChange(for: CGFloat.self) { geo in
max(0, -(geo.contentOffset.y + geo.contentInsets.top))
} action: { _, pullDistance in
@@ -446,10 +395,6 @@ struct TaskListView: View {
deleteTask(task)
}
- private func handleSwipeActiveChanged(_ taskID: UUID, _ isActive: Bool) {
- swipingTaskID = isActive ? taskID : nil
- }
-
private func selectTask(_ taskID: UUID) {
selectedTaskID = taskID
}
@@ -631,10 +576,13 @@ struct TaskListView: View {
// MARK: - Drag and Drop
private func startDrag(taskID: UUID) {
+ guard draggedTaskID == nil else { return }
draggedTaskID = taskID
visualOrder = activeTasks.map(\.id)
#if os(iOS)
- iosDragState = IOSDragState(taskID: taskID, fingerPosition: .zero)
+ isDragging = true
+ let generator = UIImpactFeedbackGenerator(style: .medium)
+ generator.impactOccurred()
#endif
}
@@ -730,45 +678,45 @@ struct TaskListView: View {
// MARK: - iOS Drag Helpers
#if os(iOS)
- private func dropIndexForY(_ y: CGFloat) -> Int {
- let midpoints = displayActiveTasks.enumerated().compactMap { i, task -> (Int, CGFloat)? in
- guard let frame = rowFrames[task.id] else { return nil }
- return (i, frame.midY)
- }
- return midpoints.first(where: { $0.1 > y })?.0 ?? displayActiveTasks.count
- }
+ private func handleIOSDragChanged(taskID: UUID, point: CGPoint) {
+ guard let draggedID = draggedTaskID,
+ var order = visualOrder,
+ let currentIndex = order.firstIndex(of: draggedID),
+ let draggedFrame = rowFrames[draggedID] else { return }
- private func updateVisualOrderToIndex(_ index: Int) {
- guard let draggedID = draggedTaskID, let order = visualOrder else { return }
- var newOrder = order.filter { $0 != draggedID }
- let clamped = min(max(0, index), newOrder.count)
- newOrder.insert(draggedID, at: clamped)
- if newOrder != visualOrder {
+ let threshold = draggedFrame.height * 0.2
+
+ // Swap down: finger moved past the bottom edge of the dragged row
+ if currentIndex < order.count - 1 && point.y > draggedFrame.maxY + threshold {
+ order.swapAt(currentIndex, currentIndex + 1)
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
- visualOrder = newOrder
+ visualOrder = order
}
+ return
}
- }
- private func handleIOSDragChanged(taskID: UUID, point: CGPoint) {
- iosDragState?.fingerPosition = point
- let index = dropIndexForY(point.y)
- updateVisualOrderToIndex(index)
+ // Swap up: finger moved past the top edge of the dragged row
+ if currentIndex > 0 && point.y < draggedFrame.minY - threshold {
+ order.swapAt(currentIndex, currentIndex - 1)
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
+ visualOrder = order
+ }
+ }
}
private func commitIOSDrag() {
- guard let state = iosDragState,
+ guard let draggedID = draggedTaskID,
let order = visualOrder,
- let finalIndex = order.firstIndex(of: state.taskID) else {
- iosDragState = nil
+ let finalIndex = order.firstIndex(of: draggedID) else {
draggedTaskID = nil
visualOrder = nil
+ isDragging = false
return
}
- store.moveTask(taskID: state.taskID, toIndex: finalIndex)
- iosDragState = nil
+ store.moveTask(taskID: draggedID, toIndex: finalIndex)
draggedTaskID = nil
visualOrder = nil
+ isDragging = false
}
#endif
diff --git a/ListlessMac/Views/TaskListView+NavigationHeader.swift b/ListlessMac/Extensions/TaskListView+NavigationHeader.swift
diff --git a/ListlessMac/Views/TaskListView+PullToCreate.swift b/ListlessMac/Extensions/TaskListView+PullToCreate.swift
diff --git a/ListlessMac/Views/TaskListView+Toolbar.swift b/ListlessMac/Extensions/TaskListView+Toolbar.swift
diff --git a/ListlessMac/Views/ClickableTextField.swift b/ListlessMac/Helpers/ClickableTextField.swift
diff --git a/ListlessMac/Views/HoverCursorModifier.swift b/ListlessMac/Helpers/HoverCursorModifier.swift
diff --git a/ListlessMac/Views/PlatformScrollIndicatorsModifier.swift b/ListlessMac/Helpers/PlatformScrollIndicatorsModifier.swift
diff --git a/ListlessMac/Views/PlatformTextFieldWidthModifier.swift b/ListlessMac/Helpers/PlatformTextFieldWidthModifier.swift
diff --git a/ListlessMac/Views/TaskRowDragGesture.swift b/ListlessMac/Helpers/TaskRowDragGesture.swift
diff --git a/ListlessMac/Views/TaskRowView.swift b/ListlessMac/Views/TaskRowView.swift
@@ -6,6 +6,7 @@ struct TaskRowView: View {
let index: Int
let totalTasks: Int
let isSelected: Bool
+ let isDragging: Bool
let isEditing: Bool
let onToggle: (TaskItem) -> Void
let onTitleChange: (TaskItem, String) -> Void
@@ -38,6 +39,7 @@ struct TaskRowView: View {
index: Int = 0,
totalTasks: Int = 1,
isSelected: Bool,
+ isDragging: Bool = false,
isEditing: Bool = false,
focusedField: FocusState<TaskListView.FocusField?>.Binding,
onToggle: @escaping (TaskItem) -> Void,
@@ -52,6 +54,7 @@ struct TaskRowView: View {
self.index = index
self.totalTasks = totalTasks
self.isSelected = isSelected
+ self.isDragging = isDragging
self.isEditing = isEditing
self.onToggle = onToggle
self.onTitleChange = onTitleChange
diff --git a/ListlessiOS/Views/TaskListView+NavigationHeader.swift b/ListlessiOS/Extensions/TaskListView+NavigationHeader.swift
diff --git a/ListlessiOS/Views/TaskListView+PullToCreate.swift b/ListlessiOS/Extensions/TaskListView+PullToCreate.swift
diff --git a/ListlessiOS/Views/TaskListView+Toolbar.swift b/ListlessiOS/Extensions/TaskListView+Toolbar.swift
diff --git a/ListlessiOS/Views/AppColors.swift b/ListlessiOS/Helpers/AppColors.swift
diff --git a/ListlessiOS/Views/HoverCursorModifier.swift b/ListlessiOS/Helpers/HoverCursorModifier.swift
diff --git a/ListlessiOS/Views/PlatformScrollIndicatorsModifier.swift b/ListlessiOS/Helpers/PlatformScrollIndicatorsModifier.swift
diff --git a/ListlessiOS/Views/PlatformTextFieldWidthModifier.swift b/ListlessiOS/Helpers/PlatformTextFieldWidthModifier.swift
diff --git a/ListlessiOS/Views/TappableTextField.swift b/ListlessiOS/Helpers/TappableTextField.swift
diff --git a/ListlessiOS/Helpers/TaskRowDragGesture.swift b/ListlessiOS/Helpers/TaskRowDragGesture.swift
@@ -0,0 +1,54 @@
+import SwiftUI
+
+extension View {
+ func taskDragGesture(
+ isActive: Bool,
+ taskID: UUID,
+ onDragStart: @escaping () -> Void,
+ onDragChanged: @escaping (CGPoint) -> Void,
+ onDragEnded: @escaping () -> Void
+ ) -> some View {
+ self.modifier(
+ TaskRowDragGesture(
+ isActive: isActive,
+ taskID: taskID,
+ onDragStart: onDragStart,
+ onDragChanged: onDragChanged,
+ onDragEnded: onDragEnded
+ ))
+ }
+}
+
+struct TaskRowDragGesture: ViewModifier {
+ let isActive: Bool
+ let taskID: UUID
+ let onDragStart: () -> Void
+ let onDragChanged: (CGPoint) -> Void
+ let onDragEnded: () -> Void
+
+ func body(content: Content) -> some View {
+ content
+ .simultaneousGesture(
+ LongPressGesture(minimumDuration: 0.4)
+ .sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .global))
+ .onChanged { value in
+ switch value {
+ case .second(true, let drag):
+ if let drag {
+ // onDragStart is idempotent (guarded in TaskListView);
+ // wait for a real drag value so the overlay has a valid
+ // position from the very first frame.
+ onDragStart()
+ onDragChanged(drag.location)
+ }
+ default:
+ break
+ }
+ }
+ .onEnded { _ in
+ onDragEnded()
+ },
+ including: isActive ? .all : .none
+ )
+ }
+}
diff --git a/ListlessiOS/Views/TaskRowSwipeGesture.swift b/ListlessiOS/Helpers/TaskRowSwipeGesture.swift
@@ -5,14 +5,13 @@ extension View {
func taskSwipeGesture(
isActive: Bool,
isEditing: Bool,
- isDragging: Bool,
+ isDragging: Binding<Bool>,
swipeOffset: Binding<CGFloat>,
swipeDirection: Binding<TaskRowSwipeGesture.SwipeDirection>,
isTriggered: Binding<Bool>,
completeColor: Color = .green,
onComplete: @escaping () -> Void,
- onDelete: @escaping () -> Void,
- onSwipeActiveChanged: @escaping (Bool) -> Void = { _ in }
+ onDelete: @escaping () -> Void
) -> some View {
self.modifier(
TaskRowSwipeGesture(
@@ -24,8 +23,7 @@ extension View {
isTriggered: isTriggered,
completeColor: completeColor,
onComplete: onComplete,
- onDelete: onDelete,
- onSwipeActiveChanged: onSwipeActiveChanged
+ onDelete: onDelete
))
}
}
@@ -33,14 +31,13 @@ extension View {
struct TaskRowSwipeGesture: ViewModifier {
let isActive: Bool
let isEditing: Bool
- let isDragging: Bool
+ @Binding var isDragging: Bool
@Binding var swipeOffset: CGFloat
@Binding var swipeDirection: SwipeDirection
@Binding var isTriggered: Bool
let completeColor: Color
let onComplete: () -> Void
let onDelete: () -> Void
- let onSwipeActiveChanged: (Bool) -> Void
enum SwipeDirection: Equatable {
case left
@@ -68,6 +65,7 @@ struct TaskRowSwipeGesture: ViewModifier {
}
.gesture(
SwipePanGesture(
+ isEnabled: isActive && !isDragging,
onChanged: { translation in
guard isActive, !isEditing, !isDragging else { return }
handleDragChanged(
@@ -111,11 +109,6 @@ struct TaskRowSwipeGesture: ViewModifier {
return
}
- // Determine direction and notify that swipe is active
- if swipeDirection == .none {
- onSwipeActiveChanged(true)
- }
-
if horizontalTranslation > 0 {
swipeDirection = .right
} else if horizontalTranslation < 0 {
@@ -134,6 +127,13 @@ struct TaskRowSwipeGesture: ViewModifier {
}
private func handleDragEnded() {
+ guard !isDragging else {
+ // A drag-reorder started during or after this swipe — spring back, no action.
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
+ resetSwipeState()
+ }
+ return
+ }
if isTriggered {
if swipeDirection == .right {
// Complete: spring back and let SwiftUI animate the row to the completed section
@@ -170,9 +170,6 @@ struct TaskRowSwipeGesture: ViewModifier {
}
private func resetSwipeState() {
- if swipeDirection != .none {
- onSwipeActiveChanged(false)
- }
swipeOffset = 0
swipeDirection = .none
isTriggered = false
@@ -190,6 +187,7 @@ struct TaskRowSwipeGesture: ViewModifier {
/// recognizer; SwiftUI manages the lifecycle automatically — no manual
/// UIView host-finding or marker-based hit-testing needed.
private struct SwipePanGesture: UIGestureRecognizerRepresentable {
+ let isEnabled: Bool
let onChanged: (CGPoint) -> Void
let onEnded: () -> Void
@@ -202,6 +200,10 @@ private struct SwipePanGesture: UIGestureRecognizerRepresentable {
return pan
}
+ func updateUIGestureRecognizer(_ recognizer: UIPanGestureRecognizer, context: Context) {
+ context.coordinator.isEnabled = isEnabled
+ }
+
func handleUIGestureRecognizerAction(
_ recognizer: UIPanGestureRecognizer, context: Context
) {
@@ -220,6 +222,12 @@ private struct SwipePanGesture: UIGestureRecognizerRepresentable {
}
final class Coordinator: NSObject, UIGestureRecognizerDelegate {
+ var isEnabled: Bool = true
+
+ func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+ isEnabled
+ }
+
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
@@ -228,4 +236,3 @@ private struct SwipePanGesture: UIGestureRecognizerRepresentable {
}
}
}
-
diff --git a/ListlessiOS/Views/TaskRowDragGesture.swift b/ListlessiOS/Views/TaskRowDragGesture.swift
@@ -1,108 +0,0 @@
-import SwiftUI
-import UIKit
-
-extension View {
- func taskDragGesture(
- isActive: Bool,
- taskID: UUID,
- onDragStart: @escaping () -> Void,
- onDragChanged: @escaping (CGPoint) -> Void,
- onDragEnded: @escaping () -> Void
- ) -> some View {
- self.modifier(
- TaskRowDragGesture(
- isActive: isActive,
- taskID: taskID,
- onDragStart: onDragStart,
- onDragChanged: onDragChanged,
- onDragEnded: onDragEnded
- ))
- }
-}
-
-struct TaskRowDragGesture: ViewModifier {
- let isActive: Bool
- let taskID: UUID
- let onDragStart: () -> Void
- let onDragChanged: (CGPoint) -> Void
- let onDragEnded: () -> Void
-
- func body(content: Content) -> some View {
- content
- .gesture(
- LongPressDragGesture(
- isActive: isActive,
- onDragStart: onDragStart,
- onDragChanged: onDragChanged,
- onDragEnded: onDragEnded
- )
- )
- }
-}
-
-// MARK: - UIKit Long Press + Drag via UIGestureRecognizerRepresentable
-
-/// A UILongPressGestureRecognizer bridged into SwiftUI. Fires onDragStart after the
-/// minimum hold duration, then tracks finger movement via onDragChanged (window coords),
-/// and calls onDragEnded when the touch ends or is cancelled.
-private struct LongPressDragGesture: UIGestureRecognizerRepresentable {
- let isActive: Bool
- let onDragStart: () -> Void
- let onDragChanged: (CGPoint) -> Void
- let onDragEnded: () -> Void
-
- func makeUIGestureRecognizer(context: Context) -> UILongPressGestureRecognizer {
- let recognizer = UILongPressGestureRecognizer()
- recognizer.minimumPressDuration = 0.4
- recognizer.allowableMovement = .infinity
- recognizer.cancelsTouchesInView = false
- recognizer.delegate = context.coordinator
- return recognizer
- }
-
- func updateUIGestureRecognizer(_ recognizer: UILongPressGestureRecognizer, context: Context) {
- recognizer.isEnabled = isActive
- }
-
- func handleUIGestureRecognizerAction(
- _ recognizer: UILongPressGestureRecognizer, context: Context
- ) {
- switch recognizer.state {
- case .began:
- onDragStart()
- if let window = recognizer.view?.window {
- onDragChanged(recognizer.location(in: window))
- }
- case .changed:
- if let window = recognizer.view?.window {
- onDragChanged(recognizer.location(in: window))
- }
- case .ended, .cancelled, .failed:
- onDragEnded()
- default:
- break
- }
- }
-
- func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator {
- Coordinator()
- }
-
- final class Coordinator: NSObject, UIGestureRecognizerDelegate {
- func gestureRecognizer(
- _ gestureRecognizer: UIGestureRecognizer,
- shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
- ) -> Bool {
- // Allow simultaneous recognition with the scroll view's pan gesture
- if let pan = otherGestureRecognizer as? UIPanGestureRecognizer,
- pan.view is UIScrollView {
- return true
- }
- // Prevent simultaneous recognition with the swipe UIPanGestureRecognizer
- if otherGestureRecognizer is UIPanGestureRecognizer {
- return false
- }
- return true
- }
- }
-}
diff --git a/ListlessiOS/Views/TaskRowView.swift b/ListlessiOS/Views/TaskRowView.swift
@@ -6,6 +6,7 @@ struct TaskRowView: View {
let index: Int
let totalTasks: Int
let isSelected: Bool
+ @Binding var isDragging: Bool
let onToggle: (TaskItem) -> Void
let onTitleChange: (TaskItem, String) -> Void
let onDelete: (TaskItem) -> Void
@@ -27,6 +28,7 @@ struct TaskRowView: View {
index: Int = 0,
totalTasks: Int = 1,
isSelected: Bool,
+ isDragging: Binding<Bool> = .constant(false),
isEditing: Bool = false,
focusedField: FocusState<TaskListView.FocusField?>.Binding,
onToggle: @escaping (TaskItem) -> Void,
@@ -41,6 +43,7 @@ struct TaskRowView: View {
self.index = index
self.totalTasks = totalTasks
self.isSelected = isSelected
+ _isDragging = isDragging
self.onToggle = onToggle
self.onTitleChange = onTitleChange
self.onDelete = onDelete
@@ -142,10 +145,17 @@ struct TaskRowView: View {
.onChange(of: totalTasks) { _, _ in
cachedAccentColor = computeAccentColor()
}
+ .onChange(of: isDragging) { _, dragging in
+ if dragging {
+ swipeOffset = 0
+ swipeDirection = .none
+ isSwipeTriggered = false
+ }
+ }
.taskSwipeGesture(
isActive: true,
isEditing: isCurrentlyEditing,
- isDragging: false,
+ isDragging: $isDragging,
swipeOffset: $swipeOffset,
swipeDirection: $swipeDirection,
isTriggered: $isSwipeTriggered,