diff --git a/Examples/AppKitDemo/App.swift b/Examples/AppKitDemo/App.swift new file mode 100644 index 00000000..5cd5f86e --- /dev/null +++ b/Examples/AppKitDemo/App.swift @@ -0,0 +1,26 @@ +import CasePaths +import Observation + +@Observable +final class AppModel { + + var destination: Destination? + + init(destination: Destination? = nil) { + self.destination = destination + } + + func reminderSelectedInOutline(_ reminder: Reminder) { + self.destination = .reminder(ReminderDetailModel(reminder: reminder)) + } + + func remindersListSelectedInOutline(_ remindersList: RemindersList) { + self.destination = .remindersList(RemindersListDetailModel(remindersList: remindersList)) + } + + @CasePathable + enum Destination { + case reminder(ReminderDetailModel) + case remindersList(RemindersListDetailModel) + } +} diff --git a/Examples/AppKitDemo/AppDelegate.swift b/Examples/AppKitDemo/AppDelegate.swift new file mode 100644 index 00000000..07928eae --- /dev/null +++ b/Examples/AppKitDemo/AppDelegate.swift @@ -0,0 +1,89 @@ +import AppKit +import Dependencies +import SQLiteData + +@MainActor +public final class AppDelegate: NSObject, NSApplicationDelegate { + private var windowControllers: [DemoWindowController] = [] + + public func applicationWillFinishLaunching(_ notification: Notification) { + let appMenu = NSMenuItem() + appMenu.submenu = NSMenu() + appMenu.submenu?.items = [ + NSMenuItem( + title: "New Window", + action: #selector(AppDelegate.newDemoWindow), + keyEquivalent: "n" + ), + NSMenuItem( + title: "Close Window", + action: #selector(NSWindow.performClose(_:)), + keyEquivalent: "w" + ), + NSMenuItem( + title: "Quit", + action: #selector(NSApplication.terminate(_:)), + keyEquivalent: "q" + ), + ] + let mainMenu = NSMenu() + mainMenu.items = [appMenu] + NSApplication.shared.mainMenu = mainMenu + } + public func applicationDidFinishLaunching(_ notification: Notification) { + try! prepareDependencies { + try $0.bootstrapDatabase() + } + @Dependency(\.defaultDatabase) var database + try! database.write { db in + try db.seedSampleData() + } + newDemoWindow() + } +} + +extension AppDelegate { + @objc func newDemoWindow() { + let windowController = DemoWindowController() + windowController.showWindow(nil) + windowControllers.append(windowController) + } + func removeWindowController(_ controller: DemoWindowController) { + windowControllers.removeAll { $0 === controller } + } +} + +final class DemoWindowController: NSWindowController { + init() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [ + .fullSizeContentView, + .closable, + .miniaturizable, + .resizable, + .titled, + ], + backing: .buffered, + defer: false + ) + + window.titleVisibility = .visible + window.toolbarStyle = .unified + window.center() + + super.init(window: window) + + window.contentViewController = RootViewController( + model: AppModel() + ) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + public func windowWillClose(_ notification: Notification) { + if let appDelegate = NSApp.delegate as? AppDelegate { + appDelegate.removeWindowController(self) + } + } +} diff --git a/Examples/AppKitDemo/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/AppKitDemo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Examples/AppKitDemo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/AppKitDemo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/AppKitDemo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..3f00db43 --- /dev/null +++ b/Examples/AppKitDemo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/AppKitDemo/Assets.xcassets/Contents.json b/Examples/AppKitDemo/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Examples/AppKitDemo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/AppKitDemo/DetailViewController.swift b/Examples/AppKitDemo/DetailViewController.swift new file mode 100644 index 00000000..fec9d3be --- /dev/null +++ b/Examples/AppKitDemo/DetailViewController.swift @@ -0,0 +1,97 @@ +import AppKit +import CasePaths +import SQLiteData +import SwiftUI + +final class DetailViewController: NSViewController { + let model: AppModel + init(model: AppModel) { + self.model = model + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func loadView() { + let hostingView = FullSizeHostingView( + rootView: DetailView(model: model) + .frame( + minWidth: 600, + maxWidth: .infinity, + minHeight: 500, + maxHeight: .infinity + ) + ) + self.view = hostingView + } + class FullSizeHostingView: NSHostingView { + override var intrinsicContentSize: NSSize { + return NSSize(width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric) + } + } +} + +private struct DetailView: View { + let model: AppModel + var body: some View { + switch model.destination { + case .remindersList(let model): + RemindersListDetailView(model: model) + case .reminder(let model): + ReminderDetailView(model: model) + case .none: + ContentUnavailableView( + "Choose a reminder or list", + systemImage: "list.bullet" + ) + } + } +} + +#Preview("Empty") { + let remindersList = try! prepareDependencies { + try $0.bootstrapDatabase() + return try $0.defaultDatabase.read { db in + try RemindersList.all.fetchOne(db)! + } + } + DetailViewController( + model: AppModel() + ) +} + +#Preview("RemindersList") { + let remindersList = try! prepareDependencies { + try $0.bootstrapDatabase() + return try $0.defaultDatabase.read { db in + try RemindersList.all.fetchOne(db)! + } + } + DetailViewController( + model: AppModel( + destination: .remindersList( + RemindersListDetailModel( + remindersList: remindersList + ) + ) + ) + ) +} + +#Preview("Reminder") { + let reminder = try! prepareDependencies { + try $0.bootstrapDatabase() + return try $0.defaultDatabase.read { db in + try Reminder.all.fetchOne(db)! + } + } + DetailViewController( + model: AppModel( + destination: .reminder( + ReminderDetailModel( + reminder: reminder + ) + ) + ) + ) +} diff --git a/Examples/AppKitDemo/ReminderDetail.swift b/Examples/AppKitDemo/ReminderDetail.swift new file mode 100644 index 00000000..b897d426 --- /dev/null +++ b/Examples/AppKitDemo/ReminderDetail.swift @@ -0,0 +1,35 @@ +import SQLiteData +import SwiftUI + +@MainActor +@Observable +final class ReminderDetailModel { + @ObservationIgnored @FetchOne var reminder: Reminder + init(reminder: Reminder) { + _reminder = FetchOne( + wrappedValue: reminder, + Reminder.find(reminder.id) + ) + } +} + +struct ReminderDetailView: View { + let model: ReminderDetailModel + var body: some View { + Text(model.reminder.title) + } +} + +#Preview("Reminder") { + let reminder = try! prepareDependencies { + try $0.bootstrapDatabase() + return try $0.defaultDatabase.read { db in + try Reminder.all.fetchOne(db)! + } + } + ReminderDetailView( + model: ReminderDetailModel( + reminder: reminder + ) + ) +} diff --git a/Examples/AppKitDemo/RemindersListDetail.swift b/Examples/AppKitDemo/RemindersListDetail.swift new file mode 100644 index 00000000..ec5612bf --- /dev/null +++ b/Examples/AppKitDemo/RemindersListDetail.swift @@ -0,0 +1,52 @@ +import SQLiteData +import SwiftUI + +@MainActor +@Observable +final class RemindersListDetailModel { + @ObservationIgnored @FetchOne var remindersList: RemindersList + @ObservationIgnored @FetchAll var reminders: [Reminder] + var editableRemindersList: RemindersList.Draft? + init(remindersList: RemindersList) { + _remindersList = FetchOne( + wrappedValue: remindersList, + RemindersList.find(remindersList.id) + ) + _reminders = FetchAll( + Reminder.all + .where { $0.remindersListID.eq(remindersList.id) } + .order { + ($0.isCompleted, $0.title) + } + ) + } +} + +struct RemindersListDetailView: View { + let model: RemindersListDetailModel + var body: some View { + List { + ForEach(model.reminders) { reminder in + Text(reminder.title) + } + } + .safeAreaInset(edge: .top) { + Text(model.remindersList.title) + .font(.headline) + } + } +} + +#Preview { + let remindersList = try! prepareDependencies { + try $0.bootstrapDatabase() + return try $0.defaultDatabase.read { db in + try RemindersList.all.fetchOne(db)! + } + } + RemindersListDetailView( + model: RemindersListDetailModel( + remindersList: remindersList + ) + ) +} diff --git a/Examples/AppKitDemo/RootViewController.swift b/Examples/AppKitDemo/RootViewController.swift new file mode 100644 index 00000000..6eaf1ba1 --- /dev/null +++ b/Examples/AppKitDemo/RootViewController.swift @@ -0,0 +1,25 @@ +import AppKit + +final class RootViewController: NSSplitViewController { + let model: AppModel + init(model: AppModel) { + self.model = model + super.init(nibName: nil, bundle: nil) + + let sidebarViewController = SidebarViewController(model: model) + let detailViewController = DetailViewController(model: model) + + let sidebarItem = NSSplitViewItem(sidebarWithViewController: sidebarViewController) + let detailItem = NSSplitViewItem(viewController: detailViewController) + + sidebarItem.canCollapse = false + sidebarItem.minimumThickness = 250 + sidebarItem.maximumThickness = 350 + + self.addSplitViewItem(sidebarItem) + self.addSplitViewItem(detailItem) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Examples/AppKitDemo/Schema.swift b/Examples/AppKitDemo/Schema.swift new file mode 100644 index 00000000..10d7bd5c --- /dev/null +++ b/Examples/AppKitDemo/Schema.swift @@ -0,0 +1,186 @@ +import Dependencies +import Foundation +import IssueReporting +import OSLog +import SQLiteData +import SwiftUI +import Synchronization + +@Table +struct RemindersList: Hashable, Identifiable { + let id: UUID + var title = "" + + static var defaultColor: Color { Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255) } + static var defaultTitle: String { "Personal" } +} + +extension RemindersList.Draft: Identifiable {} + +@Table +struct Reminder: Hashable, Identifiable, Codable { + let id: UUID + var remindersListID: RemindersList.ID + var title = "" + var isCompleted: Bool = false +} + +extension Reminder.Draft: Identifiable {} + +extension DependencyValues { + mutating func bootstrapDatabase() throws { + defaultDatabase = try AppKitDemo.appDatabase() + // defaultSyncEngine = try SyncEngine( + // for: defaultDatabase, + // tables: RemindersList.self, + // Reminder.self, + // ) + } +} + +func appDatabase() throws -> any DatabaseWriter { + @Dependency(\.context) var context + var configuration = Configuration() + configuration.foreignKeysEnabled = true + configuration.prepareDatabase { db in + //try db.attachMetadatabase() + #if DEBUG + db.trace(options: .profile) { + if context == .live { + logger.debug("\($0.expandedDescription)") + } else { + print("\($0.expandedDescription)") + } + } + #endif + } + let database = try SQLiteData.defaultDatabase(configuration: configuration) + logger.debug( + """ + App database: + open "\(database.path)" + """ + ) + var migrator = DatabaseMigrator() + #if DEBUG + migrator.eraseDatabaseOnSchemaChange = true + #endif + migrator.registerMigration("Create initial tables") { db in + try #sql( + """ + CREATE TABLE "remindersLists" ( + "id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()), + "title" TEXT NOT NULL ON CONFLICT REPLACE DEFAULT '' + ) STRICT + """ + ) + .execute(db) + try #sql( + """ + CREATE TABLE "reminders" ( + "id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()), + "remindersListID" TEXT NOT NULL REFERENCES "remindersLists"("id") ON DELETE CASCADE, + "isCompleted" INTEGER NOT NULL DEFAULT 0, + "title" TEXT NOT NULL ON CONFLICT REPLACE DEFAULT '' + ) STRICT + """ + ) + .execute(db) + } + + try migrator.migrate(database) + + try database.write { db in + + if context != .live { + try db.seedSampleData() + } + } + + return database +} + +private let logger = Logger(subsystem: "Reminders", category: "Database") + +#if DEBUG + extension Database { + func seedSampleData() throws { + @Dependency(\.date.now) var now + @Dependency(\.uuid) var uuid + let remindersListIDs = (0...2).map { _ in uuid() } + let reminderIDs = (0...10).map { _ in uuid() } + try seed { + RemindersList( + id: remindersListIDs[0], + title: "Personal" + ) + RemindersList( + id: remindersListIDs[1], + title: "Family" + ) + RemindersList( + id: remindersListIDs[2], + title: "Business" + ) + Reminder( + id: reminderIDs[0], + remindersListID: remindersListIDs[0], + title: "Groceries" + ) + Reminder( + id: reminderIDs[1], + remindersListID: remindersListIDs[0], + title: "Haircut" + ) + Reminder( + id: reminderIDs[2], + remindersListID: remindersListIDs[0], + title: "Doctor appointment" + ) + Reminder( + id: reminderIDs[3], + remindersListID: remindersListIDs[0], + title: "Take a walk", + isCompleted: true, + ) + Reminder( + id: reminderIDs[4], + remindersListID: remindersListIDs[0], + title: "Buy concert tickets" + ) + Reminder( + id: reminderIDs[5], + remindersListID: remindersListIDs[1], + title: "Pick up kids from school" + ) + Reminder( + id: reminderIDs[6], + remindersListID: remindersListIDs[1], + title: "Get laundry", + isCompleted: true, + ) + Reminder( + id: reminderIDs[7], + remindersListID: remindersListIDs[1], + title: "Take out trash" + ) + Reminder( + id: reminderIDs[8], + remindersListID: remindersListIDs[2], + title: "Call accountant" + ) + Reminder( + id: reminderIDs[9], + remindersListID: remindersListIDs[2], + title: "Send weekly emails", + isCompleted: true, + ) + Reminder( + id: reminderIDs[10], + remindersListID: remindersListIDs[2], + title: "Prepare for WWDC" + ) + } + } + } +#endif diff --git a/Examples/AppKitDemo/SidebarViewController.swift b/Examples/AppKitDemo/SidebarViewController.swift new file mode 100644 index 00000000..edab955b --- /dev/null +++ b/Examples/AppKitDemo/SidebarViewController.swift @@ -0,0 +1,190 @@ +import AppKit +import AppKitNavigation +import SQLiteData + +final class SidebarViewController: NSViewController { + private let model: AppModel + private var outlineView: NSOutlineView! + private var scrollView: NSScrollView! + private var outlineItems: [OutlineItem] = [] + @FetchAll private var rows: [Row] + + @Selection + struct Row { + let remindersList: RemindersList + @Column(as: [Reminder].JSONRepresentation.self) + let reminders: [Reminder] + } + + init(model: AppModel) { + self.model = model + super.init(nibName: nil, bundle: nil) + + $rows = FetchAll( + RemindersList.all + .group(by: \.id) + .order(by: \.title) + .join(Reminder.all) { $0.id.eq($1.remindersListID) } + .select { + Row.Columns( + remindersList: $0, + reminders: $1.jsonGroupArray() // FIXME: order reminders + ) + } + ) + + observe { [weak self] in + guard let self else { return } + self.outlineItems = rows.map { row in + OutlineItem.remindersList( + row.remindersList, + row.reminders + ) + } + self.outlineView?.reloadData() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + self.view = NSView() + + scrollView = NSScrollView() + scrollView.autoresizingMask = [.width, .height] + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalScroller = true + + outlineView = NSOutlineView() + outlineView.autoresizingMask = [.width, .height] + + outlineView.dataSource = self + outlineView.delegate = self + + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("RemindersColumn")) + column.title = "Reminders" + outlineView.addTableColumn(column) + outlineView.outlineTableColumn = column + + outlineView.headerView = nil + + scrollView.documentView = outlineView + self.view.addSubview(scrollView) + } +} + +private enum OutlineItem { + case reminder(Reminder) + case remindersList(RemindersList, [Reminder]) +} + +extension SidebarViewController: NSOutlineViewDelegate { + func outlineView( + _ outlineView: NSOutlineView, + viewFor tableColumn: NSTableColumn?, + item: Any + ) -> NSView? { + guard let item = item as? OutlineItem else { return nil } + + let cellView = NSTableCellView() + + switch item { + case .reminder(let reminder): + let textField = NSTextField(labelWithString: reminder.title) + textField.translatesAutoresizingMaskIntoConstraints = false + cellView.textField = textField + + if reminder.isCompleted { + let checkmark = NSImageView( + image: NSImage( + systemSymbolName: "checkmark", + accessibilityDescription: "Completed" + )! + ) + + let stack = NSStackView(views: [checkmark, textField]) + cellView.addSubview(stack) + + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: cellView.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: cellView.trailingAnchor), + stack.centerYAnchor.constraint(equalTo: cellView.centerYAnchor), + ]) + } else { + cellView.addSubview(textField) + + NSLayoutConstraint.activate([ + textField.leadingAnchor.constraint(equalTo: cellView.leadingAnchor), + textField.trailingAnchor.constraint(equalTo: cellView.trailingAnchor), + textField.centerYAnchor.constraint(equalTo: cellView.centerYAnchor), + ]) + } + + case .remindersList(let remindersList, _): + + let textField = NSTextField(labelWithString: remindersList.title) + textField.translatesAutoresizingMaskIntoConstraints = false + cellView.textField = textField + + cellView.addSubview(textField) + + NSLayoutConstraint.activate([ + textField.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 3), + textField.trailingAnchor.constraint(equalTo: cellView.trailingAnchor), + textField.centerYAnchor.constraint(equalTo: cellView.centerYAnchor), + ]) + } + + return cellView + } + func outlineViewSelectionDidChange(_ notification: Notification) { + let row = outlineView.selectedRow + guard + row >= 0, + let item = outlineView.item(atRow: row) as? OutlineItem + else { + return + } + switch item { + case .reminder(let reminder): + model.reminderSelectedInOutline(reminder) + case .remindersList(let remindersList, _): + model.remindersListSelectedInOutline(remindersList) + } + } +} + +extension SidebarViewController: NSOutlineViewDataSource { + func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { + switch item as? OutlineItem { + case .reminder: outlineItems[index] + case .remindersList(_, let reminders): OutlineItem.reminder(reminders[index]) + case .none: outlineItems[index] + } + } + func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { + switch item as? OutlineItem { + case .reminder: 0 + case .remindersList(_, let reminders): reminders.count + case .none: outlineItems.count + } + } + func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { + switch item as? OutlineItem { + case .reminder: false + case .remindersList(_, let reminders): !reminders.isEmpty + case .none: false + } + } +} + +#Preview { + let _ = try! prepareDependencies { + try $0.bootstrapDatabase() + } + SidebarViewController( + model: AppModel() + ) +} diff --git a/Examples/AppKitDemo/main.swift b/Examples/AppKitDemo/main.swift new file mode 100644 index 00000000..195b6235 --- /dev/null +++ b/Examples/AppKitDemo/main.swift @@ -0,0 +1,10 @@ +import AppKit + +MainActor.assumeIsolated { + let app = NSApplication.shared + let delegate = AppDelegate() + app.delegate = delegate + app.setActivationPolicy(.regular) +} + +_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 2abda09e..76ecd913 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + C404E30F2E88A5F5000D23D2 /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = C404E30E2E88A5F5000D23D2 /* SQLiteData */; }; + C43A01E72E88FDB800E5168E /* AppKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = C43A01E62E88FDB800E5168E /* AppKitNavigation */; }; + C43A02052E8999C100E5168E /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = C43A02042E8999C100E5168E /* CasePaths */; }; CA14DBC92DA884C400E36852 /* CasePaths in Frameworks */ = {isa = PBXBuildFile; productRef = CA14DBC82DA884C400E36852 /* CasePaths */; }; CA2908C92D4AF70E003F165F /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA2908C82D4AF70E003F165F /* UIKitNavigation */; }; CA2BDE2A2E71C469000974D3 /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = CA2BDE292E71C469000974D3 /* SQLiteData */; }; @@ -48,6 +51,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + C4CD9A252E88A20900172F37 /* AppKitDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppKitDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C4CD9BE72E88A57D00172F37 /* sqlite-data */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "sqlite-data"; path = "/Users/rcarver/Code/OpenSource/sqlite-data"; sourceTree = ""; }; CA2BDD9D2E71C30B000974D3 /* CloudKitDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CloudKitDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; CA2BDE272E71C42B000974D3 /* sqlite-data */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "sqlite-data"; path = "/Users/brandon/projects/sqlite-data"; sourceTree = ""; }; CA5E46962DEBFE410069E0F8 /* RemindersTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RemindersTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -94,6 +99,11 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + C4CD9A262E88A20900172F37 /* AppKitDemo */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AppKitDemo; + sourceTree = ""; + }; CA2BDD9E2E71C30B000974D3 /* CloudKitDemo */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -144,6 +154,16 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + C4CD9A222E88A20900172F37 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C404E30F2E88A5F5000D23D2 /* SQLiteData in Frameworks */, + C43A01E72E88FDB800E5168E /* AppKitNavigation in Frameworks */, + C43A02052E8999C100E5168E /* CasePaths in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CA2BDD9A2E71C30B000974D3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -221,6 +241,7 @@ DCBE89CD2D483FB90071F499 /* SyncUps */, CAD0017E2D874E6F00FA977A /* SyncUpTests */, CA2BDD9E2E71C30B000974D3 /* CloudKitDemo */, + C4CD9A262E88A20900172F37 /* AppKitDemo */, CAF837022D4735C00047AEB5 /* Frameworks */, CAF836992D4735620047AEB5 /* Products */, ); @@ -236,6 +257,7 @@ CAD0017D2D874E6F00FA977A /* SyncUpTests.xctest */, CA5E46962DEBFE410069E0F8 /* RemindersTests.xctest */, CA2BDD9D2E71C30B000974D3 /* CloudKitDemo.app */, + C4CD9A252E88A20900172F37 /* AppKitDemo.app */, ); name = Products; sourceTree = ""; @@ -243,6 +265,7 @@ CAF837022D4735C00047AEB5 /* Frameworks */ = { isa = PBXGroup; children = ( + C4CD9BE72E88A57D00172F37 /* sqlite-data */, CA2BDE272E71C42B000974D3 /* sqlite-data */, ); name = Frameworks; @@ -251,6 +274,31 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + C4CD9A242E88A20900172F37 /* AppKitDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = C4CD9A2F2E88A20A00172F37 /* Build configuration list for PBXNativeTarget "AppKitDemo" */; + buildPhases = ( + C4CD9A212E88A20900172F37 /* Sources */, + C4CD9A222E88A20900172F37 /* Frameworks */, + C4CD9A232E88A20900172F37 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + C4CD9A262E88A20900172F37 /* AppKitDemo */, + ); + name = AppKitDemo; + packageProductDependencies = ( + C404E30E2E88A5F5000D23D2 /* SQLiteData */, + C43A01E62E88FDB800E5168E /* AppKitNavigation */, + C43A02042E8999C100E5168E /* CasePaths */, + ); + productName = AppKitDemo; + productReference = C4CD9A252E88A20900172F37 /* AppKitDemo.app */; + productType = "com.apple.product-type.application"; + }; CA2BDD9C2E71C30B000974D3 /* CloudKitDemo */ = { isa = PBXNativeTarget; buildConfigurationList = CA2BDDA72E71C30D000974D3 /* Build configuration list for PBXNativeTarget "CloudKitDemo" */; @@ -429,9 +477,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1640; + LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 1620; TargetAttributes = { + C4CD9A242E88A20900172F37 = { + CreatedOnToolsVersion = 26.0; + }; CA2BDD9C2E71C30B000974D3 = { CreatedOnToolsVersion = 16.4; }; @@ -486,11 +537,19 @@ DCBE89CB2D483FB90071F499 /* SyncUps */, CAD0017C2D874E6F00FA977A /* SyncUpTests */, CA2BDD9C2E71C30B000974D3 /* CloudKitDemo */, + C4CD9A242E88A20900172F37 /* AppKitDemo */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + C4CD9A232E88A20900172F37 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; CA2BDD9B2E71C30B000974D3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -543,6 +602,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + C4CD9A212E88A20900172F37 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; CA2BDD992E71C30B000974D3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -613,6 +679,70 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + C4CD9A2D2E88A20A00172F37 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_APP_SANDBOX = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.7; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.AppKitDemo.AppKitDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + C4CD9A2E2E88A20A00172F37 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_APP_SANDBOX = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 15.7; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.AppKitDemo.AppKitDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; CA2BDDA52E71C30D000974D3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1077,6 +1207,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + C4CD9A2F2E88A20A00172F37 /* Build configuration list for PBXNativeTarget "AppKitDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C4CD9A2D2E88A20A00172F37 /* Debug */, + C4CD9A2E2E88A20A00172F37 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; CA2BDDA72E71C30D000974D3 /* Build configuration list for PBXNativeTarget "CloudKitDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1194,6 +1333,20 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + C404E30E2E88A5F5000D23D2 /* SQLiteData */ = { + isa = XCSwiftPackageProductDependency; + productName = SQLiteData; + }; + C43A01E62E88FDB800E5168E /* AppKitNavigation */ = { + isa = XCSwiftPackageProductDependency; + package = DCF267372D48437300B680BE /* XCRemoteSwiftPackageReference "swift-navigation" */; + productName = AppKitNavigation; + }; + C43A02042E8999C100E5168E /* CasePaths */ = { + isa = XCSwiftPackageProductDependency; + package = DCBE8A122D4842BF0071F499 /* XCRemoteSwiftPackageReference "swift-case-paths" */; + productName = CasePaths; + }; CA14DBC82DA884C400E36852 /* CasePaths */ = { isa = XCSwiftPackageProductDependency; package = DCBE8A122D4842BF0071F499 /* XCRemoteSwiftPackageReference "swift-case-paths" */; diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 744551e2..fc47316e 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "1a549785266ada7e3202edc79fe44d94e238bd6e6494c714e09a66c2d74bc59f", + "originHash" : "5b6a9f5c8c2b757451c31f80c25e528163092e701c82d5f6f1c82ca48fb617dc", "pins" : [ { "identity" : "combine-schedulers", diff --git a/Examples/README.md b/Examples/README.md index 01accf0d..47bab48b 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -30,3 +30,7 @@ project. To work on each example app individually, select its scheme in Xcode. [scrumdinger]: https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger [reminders-app-store]: https://apps.apple.com/us/app/reminders/id1108187841 + +* **AppKitDemo** +
This application is a simplified Reminders app built with AppKit + SwiftUI. It shows basic patterns + for fetching, observing, and modifying data.