← All repositories

listless

A simple list app for Apple platforms

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


commit 805c6cd40c41e5f7f513084c8f69b0332301a0bf
parent 5f7fc7b1bb683b22910a85f6ffe92ffeac74de69
Author: Michael Camilleri <[email protected]>
Date:   Wed,  4 Feb 2026 12:10:51 +0900

Keyboard navigation working

Co-Authored-By: Claude 4.5 Sonnet <[email protected]>

Diffstat

MListless.xcodeproj/project.pbxproj | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListless/Models/Listless.xcdatamodeld/.xccurrentversion | 8++++++++
AListless/Models/Listless.xcdatamodeld/Listless.xcdatamodel/contents | 10++++++++++
MListless/Models/TaskItem.swift | 34++++++++++++++++++++++++++--------
AListless/Models/TaskStore.swift | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListless/Sync/PersistenceController.swift | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListless/Views/KeyboardNavigationModifier.swift | 20++++++++++++++++++++
AListless/Views/TaskListView.swift | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListless/Views/TaskRowView.swift | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListlessMac/Listless.entitlements | 20++++++++++++++++++++
MListlessMac/ListlessMacApp.swift | 11+++--------
AListlessMac/Views/PlatformScrollIndicatorsModifier.swift | 38++++++++++++++++++++++++++++++++++++++
AListlessMac/Views/PlatformTextFieldWidthModifier.swift | 50++++++++++++++++++++++++++++++++++++++++++++++++++
AListlessiOS/Listless.entitlements | 20++++++++++++++++++++
MListlessiOS/ListlessiOSApp.swift | 11+++--------
AListlessiOS/Views/PlatformScrollIndicatorsModifier.swift | 7+++++++
AListlessiOS/Views/PlatformTextFieldWidthModifier.swift | 7+++++++

17 files changed, 824 insertions(+), 24 deletions(-)

diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj

@@ -8,22 +8,50 @@
 
 /* Begin PBXBuildFile section */
 		0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; };
+		3ABE52A15C2059D8D5570528 /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEE364304587D280C5672 /* TaskStore.swift */; };
+		42E4CDE1D17463554CC4F41F /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537A913AC421BAEF60D26D9C /* TaskListView.swift */; };
+		5B60B409CE4BA668DB30A65D /* Listless.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C093494053E6C348F245D4EC /* Listless.xcdatamodeld */; };
 		614FCCA450EC0BFFD8B40640 /* ListlessMacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */; };
+		6C252050E62AED3A0A684EBF /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7B37EF6A2389656D105FF8 /* KeyboardNavigationModifier.swift */; };
+		8E08F4C82D6F9E67667CB20A /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537A913AC421BAEF60D26D9C /* TaskListView.swift */; };
+		8E680EC18B0339931E785F0C /* PlatformScrollIndicatorsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48AE1AE43296C1692FA6F755 /* PlatformScrollIndicatorsModifier.swift */; };
+		9041B7CED5298439BF7DC2C1 /* TaskRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007D500D2EFF3E66B780ADE0 /* TaskRowView.swift */; };
+		91EDF52C7C5C0B35E9D8B51E /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEE364304587D280C5672 /* TaskStore.swift */; };
+		96617677059FABDBB80D642B /* Listless.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C093494053E6C348F245D4EC /* Listless.xcdatamodeld */; };
+		A0AA8FD4C542E9AEB2437BC2 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */; };
+		A8D13DD6196772ECA146CF2A /* PlatformScrollIndicatorsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D74AE2501F35974F57D21F /* PlatformScrollIndicatorsModifier.swift */; };
 		C1FE091454864C4BBBBEB077 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; };
+		C2400278D5F6F79C85A68897 /* TaskRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 007D500D2EFF3E66B780ADE0 /* TaskRowView.swift */; };
+		D878CD3A552C6A9685A30AA8 /* PlatformTextFieldWidthModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF6B0195EA0176B72DE9B092 /* PlatformTextFieldWidthModifier.swift */; };
+		D8EFF49E9156083D675D47F0 /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7B37EF6A2389656D105FF8 /* KeyboardNavigationModifier.swift */; };
+		DC71C45D92524C3893BF9FDB /* PlatformTextFieldWidthModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81880970CCFC2CB00B65047E /* PlatformTextFieldWidthModifier.swift */; };
+		DC73A39A269AB495BCE1AC48 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */; };
 		F0B2B806BD84A4F2FDF8E038 /* ListlessiOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
+		007D500D2EFF3E66B780ADE0 /* TaskRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowView.swift; sourceTree = "<group>"; };
 		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>"; };
 		126108860D7878DDC3BECC4B /* Listless iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "Listless iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
 		1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListlessMacApp.swift; sourceTree = "<group>"; };
 		3313FEDB101EECA4B344EEF4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
+		48AE1AE43296C1692FA6F755 /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; 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; };
 		74255E6B6C40899E9B17D927 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
+		7C73E9D4C42CCABBF0F33543 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; };
+		81880970CCFC2CB00B65047E /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
+		9262207DAC21619BD9EDEE15 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; };
 		944BAE054AAC1B9C4FC954F9 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
+		9FBEB58DD41817F09B0EB9F0 /* Listless.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Listless.xcdatamodel; 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>"; };
+		C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
 		D10B5491A53E77C80F8F75CD /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
 		D123BB181208FC825777B0A7 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
+		DC3DEE364304587D280C5672 /* TaskStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStore.swift; sourceTree = "<group>"; };
+		EF6B0195EA0176B72DE9B092 /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
 		FBB8A3BEB346267B30B4675F /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -32,6 +60,9 @@
 			isa = PBXGroup;
 			children = (
 				D123BB181208FC825777B0A7 /* .gitkeep */,
+				0E7B37EF6A2389656D105FF8 /* KeyboardNavigationModifier.swift */,
+				537A913AC421BAEF60D26D9C /* TaskListView.swift */,
+				007D500D2EFF3E66B780ADE0 /* TaskRowView.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -56,11 +87,22 @@
 			path = Listless;
 			sourceTree = "<group>";
 		};
+		58F917D865E0BDF4EF282306 /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				48AE1AE43296C1692FA6F755 /* PlatformScrollIndicatorsModifier.swift */,
+				81880970CCFC2CB00B65047E /* PlatformTextFieldWidthModifier.swift */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
 		8656EEF3161BE20196B8042E /* Models */ = {
 			isa = PBXGroup;
 			children = (
 				74255E6B6C40899E9B17D927 /* .gitkeep */,
+				C093494053E6C348F245D4EC /* Listless.xcdatamodeld */,
 				FBB8A3BEB346267B30B4675F /* TaskItem.swift */,
+				DC3DEE364304587D280C5672 /* TaskStore.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -69,7 +111,9 @@
 			isa = PBXGroup;
 			children = (
 				3313FEDB101EECA4B344EEF4 /* Info.plist */,
+				9262207DAC21619BD9EDEE15 /* Listless.entitlements */,
 				AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */,
+				58F917D865E0BDF4EF282306 /* Views */,
 			);
 			path = ListlessiOS;
 			sourceTree = "<group>";
@@ -82,11 +126,22 @@
 			path = Infrastructure;
 			sourceTree = "<group>";
 		};
+		D12ECC901ABED96B86CC85B5 /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				B4D74AE2501F35974F57D21F /* PlatformScrollIndicatorsModifier.swift */,
+				EF6B0195EA0176B72DE9B092 /* PlatformTextFieldWidthModifier.swift */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
 		D5B197AFF26144948D032299 /* ListlessMac */ = {
 			isa = PBXGroup;
 			children = (
 				01E141436176F83594E2F26B /* Info.plist */,
+				7C73E9D4C42CCABBF0F33543 /* Listless.entitlements */,
 				1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */,
+				D12ECC901ABED96B86CC85B5 /* Views */,
 			);
 			path = ListlessMac;
 			sourceTree = "<group>";
@@ -95,6 +150,7 @@
 			isa = PBXGroup;
 			children = (
 				944BAE054AAC1B9C4FC954F9 /* .gitkeep */,
+				C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */,
 			);
 			path = Sync;
 			sourceTree = "<group>";
@@ -154,6 +210,16 @@
 			attributes = {
 				BuildIndependentTargetsInParallel = YES;
 				LastUpgradeCheck = 1430;
+				TargetAttributes = {
+					0FB4F07A37999BBC6DFE4DBB = {
+						DevelopmentTeam = 7TD7PZBNXP;
+						ProvisioningStyle = Automatic;
+					};
+					34A03D42B91730DEAC2EBD8E = {
+						DevelopmentTeam = 7TD7PZBNXP;
+						ProvisioningStyle = Automatic;
+					};
+				};
 			};
 			buildConfigurationList = CAACA40A09D5F78ECE7A0EDF /* Build configuration list for PBXProject "Listless" */;
 			compatibilityVersion = "Xcode 14.0";
@@ -180,8 +246,16 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				D8EFF49E9156083D675D47F0 /* 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 */,
 				C1FE091454864C4BBBBEB077 /* TaskItem.swift in Sources */,
+				8E08F4C82D6F9E67667CB20A /* TaskListView.swift in Sources */,
+				C2400278D5F6F79C85A68897 /* TaskRowView.swift in Sources */,
+				3ABE52A15C2059D8D5570528 /* TaskStore.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -189,8 +263,16 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				6C252050E62AED3A0A684EBF /* 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 */,
 				0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */,
+				42E4CDE1D17463554CC4F41F /* TaskListView.swift in Sources */,
+				9041B7CED5298439BF7DC2C1 /* TaskRowView.swift in Sources */,
+				91EDF52C7C5C0B35E9D8B51E /* TaskStore.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -201,7 +283,10 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_ENTITLEMENTS = ListlessMac/Listless.entitlements;
+				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
+				DEVELOPMENT_TEAM = 7TD7PZBNXP;
 				INFOPLIST_FILE = ListlessMac/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
@@ -216,7 +301,10 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_ENTITLEMENTS = ListlessMac/Listless.entitlements;
+				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
+				DEVELOPMENT_TEAM = 7TD7PZBNXP;
 				INFOPLIST_FILE = ListlessMac/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
@@ -350,7 +438,10 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_ENTITLEMENTS = ListlessiOS/Listless.entitlements;
 				CODE_SIGN_IDENTITY = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = 7TD7PZBNXP;
 				INFOPLIST_FILE = ListlessiOS/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
@@ -366,7 +457,10 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_ENTITLEMENTS = ListlessiOS/Listless.entitlements;
 				CODE_SIGN_IDENTITY = "iPhone Developer";
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = 7TD7PZBNXP;
 				INFOPLIST_FILE = ListlessiOS/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
@@ -409,6 +503,19 @@
 			defaultConfigurationName = Debug;
 		};
 /* End XCConfigurationList section */
+
+/* Begin XCVersionGroup section */
+		C093494053E6C348F245D4EC /* Listless.xcdatamodeld */ = {
+			isa = XCVersionGroup;
+			children = (
+				9FBEB58DD41817F09B0EB9F0 /* Listless.xcdatamodel */,
+			);
+			currentVersion = 9FBEB58DD41817F09B0EB9F0 /* Listless.xcdatamodel */;
+			path = Listless.xcdatamodeld;
+			sourceTree = "<group>";
+			versionGroupType = wrapper.xcdatamodel;
+		};
+/* End XCVersionGroup section */
 	};
 	rootObject = 3256C2BF8F1DAF371DA32120 /* Project object */;
 }

diff --git a/Listless/Models/Listless.xcdatamodeld/.xccurrentversion b/Listless/Models/Listless.xcdatamodeld/.xccurrentversion

@@ -0,0 +1,8 @@
+<?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>_XCCurrentVersionName</key>
+    <string>Listless.xcdatamodel</string>
+</dict>
+</plist>

diff --git a/Listless/Models/Listless.xcdatamodeld/Listless.xcdatamodel/contents b/Listless/Models/Listless.xcdatamodeld/Listless.xcdatamodel/contents

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24512" systemVersion="24G419" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
+    <entity name="TaskItem" representedClassName="TaskItem" syncable="YES">
+        <attribute name="createdAt" attributeType="Date" optional="YES" usesScalarValueType="NO"/>
+        <attribute name="id" attributeType="UUID" optional="YES" usesScalarValueType="NO"/>
+        <attribute name="isCompleted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
+        <attribute name="title" attributeType="String" defaultValueString=""/>
+        <attribute name="updatedAt" attributeType="Date" optional="YES" usesScalarValueType="NO"/>
+    </entity>
+</model>

diff --git a/Listless/Models/TaskItem.swift b/Listless/Models/TaskItem.swift

@@ -1,13 +1,31 @@
 import Foundation
+import CoreData
 
-public struct TaskItem: Identifiable, Codable {
-    public let id: UUID
-    public var title: String
-    public var isCompleted: Bool
+@objc(TaskItem)
+public class TaskItem: NSManagedObject, Identifiable {
+    @NSManaged public var id: UUID
+    @NSManaged public var title: String
+    @NSManaged public var isCompleted: Bool
+    @NSManaged public var createdAt: Date
+    @NSManaged public var updatedAt: Date
 
-    public init(id: UUID = UUID(), title: String, isCompleted: Bool = false) {
-        self.id = id
-        self.title = title
-        self.isCompleted = isCompleted
+    @nonobjc public class func fetchRequest() -> NSFetchRequest<TaskItem> {
+        return NSFetchRequest<TaskItem>(entityName: "TaskItem")
+    }
+
+    public override func awakeFromInsert() {
+        super.awakeFromInsert()
+        setPrimitiveValue(UUID(), forKey: "id")
+        setPrimitiveValue(Date(), forKey: "createdAt")
+        setPrimitiveValue(Date(), forKey: "updatedAt")
+        setPrimitiveValue(false, forKey: "isCompleted")
+        setPrimitiveValue("", forKey: "title")
+    }
+
+    public override func willSave() {
+        super.willSave()
+        if !isDeleted && changedValues().keys.contains(where: { $0 != "updatedAt" }) {
+            setPrimitiveValue(Date(), forKey: "updatedAt")
+        }
     }
 }

diff --git a/Listless/Models/TaskStore.swift b/Listless/Models/TaskStore.swift

@@ -0,0 +1,79 @@
+import Foundation
+import CoreData
+import Observation
+
+@MainActor
+@Observable
+final class TaskStore {
+    private let persistenceController: PersistenceController
+    private var context: NSManagedObjectContext {
+        persistenceController.viewContext
+    }
+
+    init(persistenceController: PersistenceController = .shared) {
+        self.persistenceController = persistenceController
+    }
+
+    func fetchTasks() -> [TaskItem] {
+        let request = TaskItem.fetchRequest()
+        request.sortDescriptors = [
+            NSSortDescriptor(keyPath: \TaskItem.isCompleted, ascending: true),
+            NSSortDescriptor(keyPath: \TaskItem.createdAt, ascending: true),
+        ]
+
+        do {
+            return try context.fetch(request)
+        } catch {
+            print("Failed to fetch tasks: \(error)")
+            return []
+        }
+    }
+
+    func createTask(title: String = "") -> TaskItem {
+        let task = TaskItem(context: context)
+        task.title = title
+        save()
+        return task
+    }
+
+    func complete(taskID: UUID) {
+        guard let task = findTask(id: taskID) else { return }
+        task.isCompleted = true
+        save()
+    }
+
+    func uncomplete(taskID: UUID) {
+        guard let task = findTask(id: taskID) else { return }
+        task.isCompleted = false
+        save()
+    }
+
+    func update(taskID: UUID, title: String) {
+        guard let task = findTask(id: taskID) else { return }
+        task.title = title
+        save()
+    }
+
+    func delete(taskID: UUID) {
+        guard let task = findTask(id: taskID) else { return }
+        context.delete(task)
+        save()
+    }
+
+    private func findTask(id: UUID) -> TaskItem? {
+        let request = TaskItem.fetchRequest()
+        request.predicate = NSPredicate(format: "id == %@", id as CVarArg)
+        request.fetchLimit = 1
+
+        do {
+            return try context.fetch(request).first
+        } catch {
+            print("Failed to find task: \(error)")
+            return nil
+        }
+    }
+
+    private func save() {
+        persistenceController.save()
+    }
+}

diff --git a/Listless/Sync/PersistenceController.swift b/Listless/Sync/PersistenceController.swift

@@ -0,0 +1,55 @@
+import CoreData
+import Foundation
+
+@MainActor
+final class PersistenceController {
+    static let shared = PersistenceController()
+
+    let container: NSPersistentCloudKitContainer
+
+    var viewContext: NSManagedObjectContext {
+        container.viewContext
+    }
+
+    init(inMemory: Bool = false) {
+        container = NSPersistentCloudKitContainer(name: "Listless")
+
+        if inMemory {
+            container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
+        } else {
+            // Configure CloudKit sync
+            guard let description = container.persistentStoreDescriptions.first else {
+                fatalError("Failed to retrieve persistent store description")
+            }
+
+            description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
+                containerIdentifier: "iCloud.net.inqk.listless"
+            )
+
+            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
+            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
+        }
+
+        container.loadPersistentStores { storeDescription, error in
+            if let error = error as NSError? {
+                fatalError("Unresolved error \(error), \(error.userInfo)")
+            }
+        }
+
+        container.viewContext.automaticallyMergesChangesFromParent = true
+        container.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
+    }
+
+    func save() {
+        let context = container.viewContext
+
+        if context.hasChanges {
+            do {
+                try context.save()
+            } catch {
+                let nsError = error as NSError
+                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+            }
+        }
+    }
+}

diff --git a/Listless/Views/KeyboardNavigationModifier.swift b/Listless/Views/KeyboardNavigationModifier.swift

@@ -0,0 +1,20 @@
+import SwiftUI
+
+extension View {
+    func keyboardNavigation(
+        onUpArrow: @escaping () -> Void,
+        onDownArrow: @escaping () -> Void
+    ) -> some View {
+        self
+            .onKeyPress(.upArrow) {
+                print("KeyboardNavigation: upArrow pressed")
+                onUpArrow()
+                return .handled
+            }
+            .onKeyPress(.downArrow) {
+                print("KeyboardNavigation: downArrow pressed")
+                onDownArrow()
+                return .handled
+            }
+    }
+}

diff --git a/Listless/Views/TaskListView.swift b/Listless/Views/TaskListView.swift

@@ -0,0 +1,224 @@
+import SwiftUI
+
+struct TaskListView: View {
+    enum FocusField: Hashable {
+        case task(UUID)
+    }
+
+    @State private var store: TaskStore
+    @State private var tasks: [TaskItem] = []
+    @FocusState private var focusedField: FocusField?
+    @FocusState private var scrollViewFocused: Bool
+    @State private var selectedTaskID: UUID?
+    @State private var refreshID = UUID()
+
+    init(store: TaskStore = TaskStore()) {
+        _store = State(wrappedValue: store)
+    }
+
+    var body: some View {
+        ScrollView {
+            ScrollViewReader { proxy in
+                VStack(alignment: .leading, spacing: 0) {
+                    if !completedTasks.isEmpty {
+                        Text("Completed")
+                            .font(.caption)
+                            .foregroundStyle(.secondary)
+                            .padding(.leading, 16)
+                            .padding(.bottom, 6)
+                    }
+
+                    ForEach(completedTasks) { task in
+                        TaskRowView(
+                            task: task,
+                            taskID: task.id,
+                            isSelected: selectedTaskID == task.id,
+                            focusedField: $focusedField,
+                            onToggle: toggleCompletion(_:),
+                            onSubmit: handleSubmit(_:),
+                            onTitleChange: updateTitle(_:_:),
+                            onDelete: deleteTask(_:),
+                            onSelect: { selectTask(task.id) }
+                        )
+                    }
+
+                    if !activeTasks.isEmpty && !completedTasks.isEmpty {
+                        Divider()
+                            .padding(.vertical, 8)
+                    }
+
+                    ForEach(activeTasks) { task in
+                        TaskRowView(
+                            task: task,
+                            taskID: task.id,
+                            isSelected: selectedTaskID == task.id,
+                            focusedField: $focusedField,
+                            onToggle: toggleCompletion(_:),
+                            onSubmit: handleSubmit(_:),
+                            onTitleChange: updateTitle(_:_:),
+                            onDelete: deleteTask(_:),
+                            onSelect: { selectTask(task.id) }
+                        )
+                    }
+                }
+                .frame(maxWidth: .infinity, alignment: .topLeading)
+                .onChange(of: selectedTaskID) { oldValue, newValue in
+                    if let newValue {
+                        withAnimation(.easeInOut(duration: 0.2)) {
+                            proxy.scrollTo(newValue, anchor: .center)
+                        }
+                    }
+                }
+            }
+        }
+        .contentShape(Rectangle())
+        .onTapGesture {
+            print("TaskListView: background tap")
+            handleBackgroundTap()
+        }
+        .focusable()
+        .focused($scrollViewFocused)
+        .focusEffectDisabled()
+        .keyboardNavigation(
+            onUpArrow: navigateUp,
+            onDownArrow: navigateDown
+        )
+        .onAppear {
+            reloadTasks()
+            scrollViewFocused = true
+        }
+        .onChange(of: focusedField) { oldValue, newValue in
+            handleFocusChange(from: oldValue, to: newValue)
+            // When a text field gets focus, remove ScrollView focus
+            // When text field loses focus, restore ScrollView focus
+            scrollViewFocused = (newValue == nil)
+        }
+    }
+
+    private var activeTasks: [TaskItem] {
+        tasks.filter { !$0.isCompleted }
+    }
+
+    private var completedTasks: [TaskItem] {
+        tasks.filter { $0.isCompleted }
+    }
+
+    private var allTasksInDisplayOrder: [TaskItem] {
+        completedTasks + activeTasks
+    }
+
+    private func reloadTasks() {
+        tasks = store.fetchTasks()
+    }
+
+    private func createTaskAndFocus() {
+        let task = store.createTask(title: "")
+        reloadTasks()
+        selectedTaskID = task.id
+        focusedField = .task(task.id)
+    }
+
+    private func handleBackgroundTap() {
+        if focusedField != nil || selectedTaskID != nil {
+            focusedField = nil
+            selectedTaskID = nil
+        } else {
+            createTaskAndFocus()
+        }
+    }
+
+    private func handleFocusChange(from oldValue: FocusField?, to newValue: FocusField?) {
+        let oldID = taskID(from: oldValue)
+        let newID = taskID(from: newValue)
+
+        guard oldID != newID, let oldID else { return }
+        deleteIfEmpty(taskID: oldID)
+    }
+
+    private func taskID(from field: FocusField?) -> UUID? {
+        guard case let .task(id) = field else { return nil }
+        return id
+    }
+
+    private func deleteIfEmpty(taskID: UUID) {
+        guard let task = tasks.first(where: { $0.id == taskID }) else { return }
+        let trimmedTitle = task.title.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard trimmedTitle.isEmpty else { return }
+        deleteTask(task)
+    }
+
+    private func handleSubmit(_ task: TaskItem) {
+        createTaskAndFocus()
+    }
+
+    private func updateTitle(_ task: TaskItem, _ title: String) {
+        guard task.title != title else { return }
+        store.update(taskID: task.id, title: title)
+    }
+
+    private func toggleCompletion(_ task: TaskItem) {
+        if task.isCompleted {
+            store.uncomplete(taskID: task.id)
+        } else {
+            store.complete(taskID: task.id)
+        }
+        reloadTasks()
+        refreshID = UUID()
+    }
+
+    private func selectTask(_ taskID: UUID) {
+        selectedTaskID = taskID
+    }
+
+    private func deleteTask(_ task: TaskItem) {
+        let taskID = task.id
+        if focusedField == .task(taskID) {
+            focusedField = nil
+        }
+        if selectedTaskID == taskID {
+            selectedTaskID = nil
+        }
+        store.delete(taskID: taskID)
+        reloadTasks()
+    }
+
+    private func navigateUp() {
+        print("TaskListView: navigateUp called, selectedTaskID: \(String(describing: selectedTaskID))")
+        guard let currentID = selectedTaskID else {
+            selectedTaskID = activeTasks.last?.id
+            print("TaskListView: No selection, selected last active: \(String(describing: selectedTaskID))")
+            return
+        }
+
+        let displayOrder = allTasksInDisplayOrder
+        guard let currentIndex = displayOrder.firstIndex(where: { $0.id == currentID }) else {
+            print("TaskListView: Current task not found in display order")
+            return
+        }
+
+        if currentIndex > 0 {
+            selectedTaskID = displayOrder[currentIndex - 1].id
+            print("TaskListView: Moved to previous task: \(String(describing: selectedTaskID))")
+        }
+    }
+
+    private func navigateDown() {
+        print("TaskListView: navigateDown called, selectedTaskID: \(String(describing: selectedTaskID))")
+        guard let currentID = selectedTaskID else {
+            selectedTaskID = completedTasks.first?.id ?? activeTasks.first?.id
+            print("TaskListView: No selection, selected first: \(String(describing: selectedTaskID))")
+            return
+        }
+
+        let displayOrder = allTasksInDisplayOrder
+        guard let currentIndex = displayOrder.firstIndex(where: { $0.id == currentID }) else {
+            print("TaskListView: Current task not found in display order")
+            return
+        }
+
+        if currentIndex < displayOrder.count - 1 {
+            selectedTaskID = displayOrder[currentIndex + 1].id
+            print("TaskListView: Moved to next task: \(String(describing: selectedTaskID))")
+        }
+    }
+}

diff --git a/Listless/Views/TaskRowView.swift b/Listless/Views/TaskRowView.swift

@@ -0,0 +1,147 @@
+import SwiftUI
+
+struct TaskRowView: View {
+    let task: TaskItem
+    let taskID: UUID
+    let isSelected: Bool
+    let onToggle: (TaskItem) -> Void
+    let onSubmit: (TaskItem) -> Void
+    let onTitleChange: (TaskItem, String) -> Void
+    let onDelete: (TaskItem) -> Void
+    let onSelect: () -> Void
+    @FocusState.Binding var focusedField: TaskListView.FocusField?
+
+    @State private var title: String
+
+    init(
+        task: TaskItem,
+        taskID: UUID,
+        isSelected: Bool,
+        focusedField: FocusState<TaskListView.FocusField?>.Binding,
+        onToggle: @escaping (TaskItem) -> Void,
+        onSubmit: @escaping (TaskItem) -> Void,
+        onTitleChange: @escaping (TaskItem, String) -> Void,
+        onDelete: @escaping (TaskItem) -> Void,
+        onSelect: @escaping () -> Void
+    ) {
+        self.task = task
+        self.taskID = taskID
+        self.isSelected = isSelected
+        self.onToggle = onToggle
+        self.onSubmit = onSubmit
+        self.onTitleChange = onTitleChange
+        self.onDelete = onDelete
+        self.onSelect = onSelect
+        _focusedField = focusedField
+        _title = State(initialValue: task.title)
+    }
+
+    var body: some View {
+        HStack(spacing: 12) {
+            Button {
+                onToggle(task)
+            } label: {
+                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
+                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
+                    .frame(width: 20, height: 20)
+            }
+            .buttonStyle(.borderless)
+
+            TextField("New task", text: $title)
+                .textFieldStyle(.plain)
+                .font(.body)
+                .focused($focusedField, equals: .task(taskID))
+                .onSubmit {
+                    onSubmit(task)
+                }
+                .platformTextFieldWidth(text: title, placeholder: "New task")
+                .disabled(task.isCompleted)
+                .strikethrough(task.isCompleted, color: .secondary)
+                .foregroundStyle(task.isCompleted ? .secondary : .primary)
+        }
+        .padding(.vertical, 8)
+        .padding(.horizontal, 16)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .contentShape(Rectangle())
+        .onTapGesture {
+            print("TaskRowView: row tap \(taskID)")
+            onSelect()
+            focusedField = nil
+        }
+        .background(selectionBackground)
+        .contextMenu {
+            Button(task.isCompleted ? "Mark as Incomplete" : "Mark as Complete") {
+                onToggle(task)
+            }
+            Divider()
+            Button("Cut") {
+                cutToPasteboard()
+            }
+            Button("Copy") {
+                copyToPasteboard()
+            }
+            Button("Paste") {
+                pasteFromPasteboard()
+            }
+            Divider()
+            Button("Delete", role: .destructive) {
+                onDelete(task)
+            }
+        }
+        .onChange(of: title) {
+            guard !task.isCompleted else { return }
+            onTitleChange(task, title)
+        }
+        .onChange(of: task.title) {
+            if task.title != title {
+                title = task.title
+            }
+        }
+        .onChange(of: focusedField) { oldValue, newValue in
+            if newValue == .task(taskID) && oldValue != .task(taskID) {
+                onSelect()
+            }
+        }
+    }
+
+    private var selectionBackground: some View {
+        Group {
+            if isSelected {
+                RoundedRectangle(cornerRadius: 6, style: .continuous)
+                    .fill(selectionFill)
+            } else {
+                Color.clear
+            }
+        }
+    }
+
+    private var selectionFill: Color {
+        Color.accentColor.opacity(0.2)
+    }
+
+    private func cutToPasteboard() {
+        copyToPasteboard()
+        onDelete(task)
+    }
+
+    private func copyToPasteboard() {
+        guard !title.isEmpty else { return }
+        #if os(macOS)
+        let pasteboard = NSPasteboard.general
+        pasteboard.clearContents()
+        pasteboard.setString(title, forType: .string)
+        #else
+        UIPasteboard.general.string = title
+        #endif
+    }
+
+    private func pasteFromPasteboard() {
+        #if os(macOS)
+        guard let string = NSPasteboard.general.string(forType: .string) else { return }
+        #else
+        guard let string = UIPasteboard.general.string else { return }
+        #endif
+        title = string
+        onTitleChange(task, string)
+    }
+}

diff --git a/ListlessMac/Listless.entitlements b/ListlessMac/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/ListlessMac/ListlessMacApp.swift b/ListlessMac/ListlessMacApp.swift

@@ -2,16 +2,11 @@ import SwiftUI
 
 @main
 struct ListlessMacApp: App {
+    private let persistenceController = PersistenceController.shared
+
     var body: some Scene {
         WindowGroup {
-            ContentView()
+            TaskListView(store: TaskStore(persistenceController: persistenceController))
         }
     }
 }
-
-struct ContentView: View {
-    var body: some View {
-        Text("Listless for macOS")
-            .padding()
-    }
-}

diff --git a/ListlessMac/Views/PlatformScrollIndicatorsModifier.swift b/ListlessMac/Views/PlatformScrollIndicatorsModifier.swift

@@ -0,0 +1,38 @@
+import SwiftUI
+
+private struct ContentHeightKey: PreferenceKey {
+    static let defaultValue: CGFloat = 0
+
+    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
+        value = max(value, nextValue())
+    }
+}
+
+private struct PlatformScrollIndicatorsModifier: ViewModifier {
+    let verticalPadding: CGFloat
+    @State private var contentHeight: CGFloat = 0
+
+    func body(content: Content) -> some View {
+        GeometryReader { proxy in
+            let effectiveContentHeight = max(0, contentHeight - (verticalPadding * 2))
+            content
+                .background(
+                    GeometryReader { contentProxy in
+                        Color.clear.preference(key: ContentHeightKey.self, value: contentProxy.size.height)
+                    }
+                )
+                .scrollIndicators(effectiveContentHeight > proxy.size.height ? .visible : .hidden)
+                .onPreferenceChange(ContentHeightKey.self) { height in
+                    if height != contentHeight {
+                        contentHeight = height
+                    }
+                }
+        }
+    }
+}
+
+extension View {
+    func platformScrollIndicators(verticalPadding: CGFloat = 0) -> some View {
+        modifier(PlatformScrollIndicatorsModifier(verticalPadding: verticalPadding))
+    }
+}

diff --git a/ListlessMac/Views/PlatformTextFieldWidthModifier.swift b/ListlessMac/Views/PlatformTextFieldWidthModifier.swift

@@ -0,0 +1,50 @@
+import SwiftUI
+
+private struct TextFieldWidthPreferenceKey: PreferenceKey {
+    static let defaultValue: CGFloat = 0
+
+    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
+        value = max(value, nextValue())
+    }
+}
+
+private struct MacTextFieldWidthModifier: ViewModifier {
+    let text: String
+    let placeholder: String
+
+    @State private var measuredWidth: CGFloat = 60
+
+    func body(content: Content) -> some View {
+        content
+            .frame(width: max(44, measuredWidth + 12), alignment: .leading)
+            .background(widthMeasurer)
+            .onPreferenceChange(TextFieldWidthPreferenceKey.self) { width in
+                if width > 0, width != measuredWidth {
+                    measuredWidth = width
+                }
+            }
+    }
+
+    private var widthMeasurer: some View {
+        Text(displayText)
+            .font(.body)
+            .lineLimit(1)
+            .fixedSize()
+            .background(
+                GeometryReader { proxy in
+                    Color.clear.preference(key: TextFieldWidthPreferenceKey.self, value: proxy.size.width)
+                }
+            )
+            .hidden()
+    }
+
+    private var displayText: String {
+        text.isEmpty ? placeholder : text
+    }
+}
+
+extension View {
+    func platformTextFieldWidth(text: String, placeholder: String) -> some View {
+        modifier(MacTextFieldWidthModifier(text: text, placeholder: placeholder))
+    }
+}

diff --git a/ListlessiOS/Listless.entitlements b/ListlessiOS/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/ListlessiOS/ListlessiOSApp.swift b/ListlessiOS/ListlessiOSApp.swift

@@ -2,16 +2,11 @@ import SwiftUI
 
 @main
 struct ListlessiOSApp: App {
+    private let persistenceController = PersistenceController.shared
+
     var body: some Scene {
         WindowGroup {
-            ContentView()
+            TaskListView(store: TaskStore(persistenceController: persistenceController))
         }
     }
 }
-
-struct ContentView: View {
-    var body: some View {
-        Text("Listless for iOS")
-            .padding()
-    }
-}

diff --git a/ListlessiOS/Views/PlatformScrollIndicatorsModifier.swift b/ListlessiOS/Views/PlatformScrollIndicatorsModifier.swift

@@ -0,0 +1,7 @@
+import SwiftUI
+
+extension View {
+    func platformScrollIndicators(verticalPadding: CGFloat = 0) -> some View {
+        self
+    }
+}

diff --git a/ListlessiOS/Views/PlatformTextFieldWidthModifier.swift b/ListlessiOS/Views/PlatformTextFieldWidthModifier.swift

@@ -0,0 +1,7 @@
+import SwiftUI
+
+extension View {
+    func platformTextFieldWidth(text: String, placeholder: String) -> some View {
+        self
+    }
+}