diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..287c38f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ +Notes is an iOS app to keep notes, synchronized with CloudKit +Code is written in Swift, and the UI is in SwiftUI. diff --git a/Notes.xcodeproj/project.pbxproj b/Notes.xcodeproj/project.pbxproj index f82c6f3..cb6c651 100644 --- a/Notes.xcodeproj/project.pbxproj +++ b/Notes.xcodeproj/project.pbxproj @@ -7,19 +7,17 @@ objects = { /* Begin PBXBuildFile section */ - C4B45E8928FC348900AC6DAF /* NotesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B45E8828FC348900AC6DAF /* NotesPageViewController.swift */; }; - C4EEE41F28DB33D8003DDC24 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE41E28DB33D8003DDC24 /* AppDelegate.swift */; }; - C4EEE42128DB33D8003DDC24 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE42028DB33D8003DDC24 /* SceneDelegate.swift */; }; - C4EEE42328DB33D8003DDC24 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE42228DB33D8003DDC24 /* ViewController.swift */; }; - C4EEE42628DB33D8003DDC24 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C4EEE42428DB33D8003DDC24 /* Main.storyboard */; }; + C4A1B10128DB4A01003DDC24 /* NotesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10028DB4A01003DDC24 /* NotesApp.swift */; }; + C4A1B10328DB4A15003DDC24 /* NotesPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10228DB4A15003DDC24 /* NotesPageView.swift */; }; + C4A1B10528DB4A29003DDC24 /* NoteEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10428DB4A29003DDC24 /* NoteEditorView.swift */; }; + C4A1B10728DB4A3D003DDC24 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10628DB4A3D003DDC24 /* PersistenceController.swift */; }; + C4A1B10928DB4A51003DDC24 /* AutoSaveManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10828DB4A51003DDC24 /* AutoSaveManager.swift */; }; + C4A1B10B28DB4A65003DDC24 /* CloudKitSyncMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10A28DB4A65003DDC24 /* CloudKitSyncMonitor.swift */; }; C4EEE42928DB33D8003DDC24 /* Notes.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE42728DB33D8003DDC24 /* Notes.xcdatamodeld */; }; C4EEE42B28DB33D9003DDC24 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C4EEE42A28DB33D9003DDC24 /* Assets.xcassets */; }; - C4EEE42E28DB33D9003DDC24 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C4EEE42C28DB33D9003DDC24 /* LaunchScreen.storyboard */; }; C4EEE43928DB33D9003DDC24 /* NotesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE43828DB33D9003DDC24 /* NotesTests.swift */; }; C4EEE44328DB33D9003DDC24 /* NotesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE44228DB33D9003DDC24 /* NotesUITests.swift */; }; C4EEE44528DB33D9003DDC24 /* NotesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE44428DB33D9003DDC24 /* NotesUITestsLaunchTests.swift */; }; - C4EEE45328DB3423003DDC24 /* NoteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE45128DB3422003DDC24 /* NoteViewController.swift */; }; - C4EEE45428DB3423003DDC24 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE45228DB3423003DDC24 /* UIViewController+Extensions.swift */; }; C4EEE46728DB3790003DDC24 /* FileOperator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE46528DB3790003DDC24 /* FileOperator.swift */; }; C4EEE46928DB3790003DDC24 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE46628DB3790003DDC24 /* FileStorage.swift */; }; C4EEE46A28DB3790003DDC24 /* PreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE46428DB378F003DDC24 /* PreferencesStorage.swift */; }; @@ -47,23 +45,21 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - C4B45E8828FC348900AC6DAF /* NotesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesPageViewController.swift; sourceTree = ""; }; + C4A1B10028DB4A01003DDC24 /* NotesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesApp.swift; sourceTree = ""; }; + C4A1B10228DB4A15003DDC24 /* NotesPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesPageView.swift; sourceTree = ""; }; + C4A1B10428DB4A29003DDC24 /* NoteEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteEditorView.swift; sourceTree = ""; }; + C4A1B10628DB4A3D003DDC24 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; + C4A1B10828DB4A51003DDC24 /* AutoSaveManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoSaveManager.swift; sourceTree = ""; }; + C4A1B10A28DB4A65003DDC24 /* CloudKitSyncMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitSyncMonitor.swift; sourceTree = ""; }; C4EEE41B28DB33D8003DDC24 /* Notes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Notes.app; sourceTree = BUILT_PRODUCTS_DIR; }; - C4EEE41E28DB33D8003DDC24 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - C4EEE42028DB33D8003DDC24 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - C4EEE42228DB33D8003DDC24 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - C4EEE42528DB33D8003DDC24 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; C4EEE42828DB33D8003DDC24 /* Notes.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Notes.xcdatamodel; sourceTree = ""; }; C4EEE42A28DB33D9003DDC24 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - C4EEE42D28DB33D9003DDC24 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; C4EEE42F28DB33D9003DDC24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C4EEE43428DB33D9003DDC24 /* NotesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NotesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C4EEE43828DB33D9003DDC24 /* NotesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesTests.swift; sourceTree = ""; }; C4EEE43E28DB33D9003DDC24 /* NotesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NotesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C4EEE44228DB33D9003DDC24 /* NotesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesUITests.swift; sourceTree = ""; }; C4EEE44428DB33D9003DDC24 /* NotesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesUITestsLaunchTests.swift; sourceTree = ""; }; - C4EEE45128DB3422003DDC24 /* NoteViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteViewController.swift; sourceTree = ""; }; - C4EEE45228DB3423003DDC24 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; }; C4EEE45928DB358A003DDC24 /* Notes.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Notes.entitlements; sourceTree = ""; }; C4EEE46428DB378F003DDC24 /* PreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesStorage.swift; sourceTree = ""; }; C4EEE46528DB3790003DDC24 /* FileOperator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperator.swift; sourceTree = ""; }; @@ -125,17 +121,15 @@ isa = PBXGroup; children = ( C4EEE45928DB358A003DDC24 /* Notes.entitlements */, - C4EEE41E28DB33D8003DDC24 /* AppDelegate.swift */, - C4EEE42028DB33D8003DDC24 /* SceneDelegate.swift */, - C4EEE42228DB33D8003DDC24 /* ViewController.swift */, - C4EEE45128DB3422003DDC24 /* NoteViewController.swift */, - C4B45E8828FC348900AC6DAF /* NotesPageViewController.swift */, - C4EEE45228DB3423003DDC24 /* UIViewController+Extensions.swift */, + C4A1B10028DB4A01003DDC24 /* NotesApp.swift */, + C4A1B10228DB4A15003DDC24 /* NotesPageView.swift */, + C4A1B10428DB4A29003DDC24 /* NoteEditorView.swift */, + C4A1B10628DB4A3D003DDC24 /* PersistenceController.swift */, + C4A1B10828DB4A51003DDC24 /* AutoSaveManager.swift */, + C4A1B10A28DB4A65003DDC24 /* CloudKitSyncMonitor.swift */, C4EEE46B28DB379B003DDC24 /* Storage */, C4EEE46228DB3714003DDC24 /* Model */, - C4EEE42428DB33D8003DDC24 /* Main.storyboard */, C4EEE42A28DB33D9003DDC24 /* Assets.xcassets */, - C4EEE42C28DB33D9003DDC24 /* LaunchScreen.storyboard */, C4EEE42F28DB33D9003DDC24 /* Info.plist */, C4EEE42728DB33D8003DDC24 /* Notes.xcdatamodeld */, ); @@ -291,9 +285,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C4EEE42E28DB33D9003DDC24 /* LaunchScreen.storyboard in Resources */, C4EEE42B28DB33D9003DDC24 /* Assets.xcassets in Resources */, - C4EEE42628DB33D8003DDC24 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -321,16 +313,16 @@ C4EEE48428DB3B6B003DDC24 /* CoreDataStorage.swift in Sources */, C4EEE48228DB39F1003DDC24 /* Note+CoreDataProperties.swift in Sources */, C4EEE46A28DB3790003DDC24 /* PreferencesStorage.swift in Sources */, - C4EEE42328DB33D8003DDC24 /* ViewController.swift in Sources */, C4EEE46928DB3790003DDC24 /* FileStorage.swift in Sources */, - C4EEE41F28DB33D8003DDC24 /* AppDelegate.swift in Sources */, - C4B45E8928FC348900AC6DAF /* NotesPageViewController.swift in Sources */, - C4EEE42128DB33D8003DDC24 /* SceneDelegate.swift in Sources */, - C4EEE45328DB3423003DDC24 /* NoteViewController.swift in Sources */, C4EEE46728DB3790003DDC24 /* FileOperator.swift in Sources */, C4EEE48128DB39F1003DDC24 /* Note+CoreDataClass.swift in Sources */, - C4EEE45428DB3423003DDC24 /* UIViewController+Extensions.swift in Sources */, C4EEE42928DB33D8003DDC24 /* Notes.xcdatamodeld in Sources */, + C4A1B10128DB4A01003DDC24 /* NotesApp.swift in Sources */, + C4A1B10328DB4A15003DDC24 /* NotesPageView.swift in Sources */, + C4A1B10528DB4A29003DDC24 /* NoteEditorView.swift in Sources */, + C4A1B10728DB4A3D003DDC24 /* PersistenceController.swift in Sources */, + C4A1B10928DB4A51003DDC24 /* AutoSaveManager.swift in Sources */, + C4A1B10B28DB4A65003DDC24 /* CloudKitSyncMonitor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -366,25 +358,6 @@ }; /* End PBXTargetDependency section */ -/* Begin PBXVariantGroup section */ - C4EEE42428DB33D8003DDC24 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - C4EEE42528DB33D8003DDC24 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - C4EEE42C28DB33D9003DDC24 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - C4EEE42D28DB33D9003DDC24 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ C4EEE44628DB33D9003DDC24 /* Debug */ = { isa = XCBuildConfiguration; @@ -512,8 +485,6 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Notes/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( @@ -541,8 +512,6 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Notes/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Notes/AppDelegate.swift b/Notes/AppDelegate.swift deleted file mode 100644 index 72f8567..0000000 --- a/Notes/AppDelegate.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// AppDelegate.swift -// Notes -// -// Created by Laurent Morvillier on 21/09/2022. -// - -import UIKit -import CoreData - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - static var shared: AppDelegate { - return UIApplication.shared.delegate as! AppDelegate - } - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - -// if let content = PreferencesStorage.main.getContent(filename: "main.txt") { -// let note = Note(context: AppDelegate.viewContext) -// note.content = content -// AppDelegate.shared.saveContext() -// print("default note created") -// } - - // Override point for customization after application launch. - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - // MARK: - Core Data stack - - lazy var persistentContainer: NSPersistentCloudKitContainer = { - /* - The persistent container for the application. This implementation - creates and returns a container, having loaded the store for the - application to it. This property is optional since there are legitimate - error conditions that could cause the creation of the store to fail. - */ - let container = NSPersistentCloudKitContainer(name: "Notes") - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - - container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy -// container.viewContext.automaticallyMergesChangesFromParent = true - - return container - }() - - // MARK: - Core Data Saving support - - static var viewContext: NSManagedObjectContext { - return self.shared.persistentContainer.viewContext - } - - func saveContext () { - print("save context...") - let context = persistentContainer.viewContext - if context.hasChanges { - do { - try context.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nserror = error as NSError - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } - } - } - -} - diff --git a/Notes/AutoSaveManager.swift b/Notes/AutoSaveManager.swift new file mode 100644 index 0000000..6b6faf2 --- /dev/null +++ b/Notes/AutoSaveManager.swift @@ -0,0 +1,25 @@ +// +// AutoSaveManager.swift +// Notes +// +// Created by Claude Code on 13/10/2025. +// + +import Foundation +import Combine + +class AutoSaveManager: ObservableObject { + private var timer: Timer? + private let idleTimeBeforeSaving: TimeInterval = 2.0 + + func requestSave(action: @escaping () -> Void) { + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: idleTimeBeforeSaving, repeats: false) { _ in + action() + } + } + + deinit { + timer?.invalidate() + } +} diff --git a/Notes/Base.lproj/LaunchScreen.storyboard b/Notes/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e932..0000000 --- a/Notes/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Notes/Base.lproj/Main.storyboard b/Notes/Base.lproj/Main.storyboard deleted file mode 100644 index 00fa1e6..0000000 --- a/Notes/Base.lproj/Main.storyboard +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Notes/CloudKitSyncMonitor.swift b/Notes/CloudKitSyncMonitor.swift new file mode 100644 index 0000000..032e066 --- /dev/null +++ b/Notes/CloudKitSyncMonitor.swift @@ -0,0 +1,29 @@ +// +// CloudKitSyncMonitor.swift +// Notes +// +// Created by Claude Code on 13/10/2025. +// + +import Foundation +import CoreData +import Combine + +class CloudKitSyncMonitor: ObservableObject { + private var cancellables = Set() + + init() { + NotificationCenter.default.publisher(for: NSPersistentCloudKitContainer.eventChangedNotification) + .sink { [weak self] notification in + self?.handleCloudKitEvent(notification) + } + .store(in: &cancellables) + } + + private func handleCloudKitEvent(_ notification: Notification) { + print("CloudKit sync event received...") + // Handle CloudKit sync events if needed + // In SwiftUI with @FetchRequest, the UI updates automatically + // when Core Data changes, so additional handling may not be necessary + } +} diff --git a/Notes/Info.plist b/Notes/Info.plist index 581b5fd..c685d57 100644 --- a/Notes/Info.plist +++ b/Notes/Info.plist @@ -2,28 +2,16 @@ - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - UIBackgroundModes remote-notification + UILaunchScreen + + UIColorName + + UIImageName + + diff --git a/Notes/Model/Note+CoreDataProperties.swift b/Notes/Model/Note+CoreDataProperties.swift index 5410630..d2acc06 100644 --- a/Notes/Model/Note+CoreDataProperties.swift +++ b/Notes/Model/Note+CoreDataProperties.swift @@ -23,7 +23,7 @@ extension Note { static func fetchByDate() throws -> [Note] { let request = Note.fetchRequest() request.sortDescriptors = [NSSortDescriptor(key: "lastEditDate", ascending: true)] - return try AppDelegate.viewContext.fetch(request) + return try PersistenceController.shared.container.viewContext.fetch(request) } } diff --git a/Notes/NoteEditorView.swift b/Notes/NoteEditorView.swift new file mode 100644 index 0000000..3118eed --- /dev/null +++ b/Notes/NoteEditorView.swift @@ -0,0 +1,69 @@ +// +// NoteEditorView.swift +// Notes +// +// Created by Claude Code on 13/10/2025. +// + +import SwiftUI +import CoreData + +struct NoteEditorView: View { + @ObservedObject var note: Note + @Environment(\.managedObjectContext) private var viewContext + @StateObject private var autoSaveManager = AutoSaveManager() + @FocusState private var isFocused: Bool + + var body: some View { + VStack { + + TextEditor(text: Binding( + get: { note.content ?? "" }, + set: { newValue in + note.content = newValue + note.lastEditDate = Date() + autoSaveManager.requestSave { + PersistenceController.shared.save() + } + } + )) + .font(.system(size: 18, weight: .regular)) + .foregroundColor(Color(UIColor.label)) + .padding(8) + .focused($isFocused) + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + if let date = note.lastEditDate { + Text("last edit: \(date.formatted())") + .font(.footnote) + .foregroundStyle(.secondary) + .frame(width: 220.0) + } + + Spacer() + + Button { + isFocused = false + } label: { + Label("Done", systemImage: "text.badge.checkmark") + .labelStyle(.iconOnly) + } + .buttonStyle(.bordered) + .tint(.primary) + } + } + } +} + +struct NoteEditorView_Previews: PreviewProvider { + static var previews: some View { + let context = PersistenceController.preview.container.viewContext + let note = Note(context: context) + note.content = "Sample note content\n\nThis is a preview." + note.lastEditDate = Date() + + return NoteEditorView(note: note) + .environment(\.managedObjectContext, context) + } +} diff --git a/Notes/NoteViewController.swift b/Notes/NoteViewController.swift deleted file mode 100644 index 3735965..0000000 --- a/Notes/NoteViewController.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// NoteViewController.swift -// Notes -// -// Created by Laurent Morvillier on 04/09/2022. -// - -import Foundation -import UIKit -import CoreData - -class NoteViewController : UIViewController, UITextViewDelegate { - - var index = 0 - - var note: Note? = nil - - @IBOutlet weak var textView: UITextView! - - @IBOutlet weak var textViewBottomConstraint: NSLayoutConstraint! - - fileprivate weak var _lastEditLabel: UILabel? - - // MARK: - - - override func viewDidLoad() { - super.viewDidLoad() - - self.textView.text = self.note?.content - - self.textView.font = UIFont.systemFont(ofSize: 18.0, weight: .regular) - self.textView.delegate = self - self.textView.inputAccessoryView = self._inputAccessoryView() - - self._updateLastEdit() - - /// Store notifications - - NotificationCenter.default.addObserver(self, selector: #selector(self._storeRemoteChange(notification:)), name: NSPersistentCloudKitContainer.eventChangedNotification, object: nil) - - /// Keyboard notifications - - NotificationCenter.default.addObserver( - self, - selector: #selector(keyboardDidShow(notification:)), - name: UIResponder.keyboardDidShowNotification, - object: nil - ) - - NotificationCenter.default.addObserver( - self, - selector: #selector(keyboardWillHide), - name: UIResponder.keyboardWillHideNotification, - object: nil - ) - - } - - func loadText() { - self.textView.text = self.note?.content - } - - fileprivate func _loadLastNote() { - - let request = Note.fetchRequest() - request.sortDescriptors = [NSSortDescriptor(key: "lastEditDate", ascending: false)] - - do { - let notes = try AppDelegate.viewContext.fetch(request) - - print("notes in store : \(notes.count)") - - self.note = notes.first - } catch { - print("Fetch error = \(error)") - } - - } - - @objc fileprivate func _storeRemoteChange(notification: Notification) { - print("_storeRemoteChange...") - DispatchQueue.main.async { -// self._loadLastNote() - } - } - - func textViewDidChange(_ textView: UITextView) { - PreferencesStorage.main.requestStorage(filename: "main.txt", content: textView.text) - - let note: Note - if let n = self.note { - note = n - } else { - note = Note(context: AppDelegate.viewContext) - self.note = note - } - - CoreDataStorage.main.requestStorage(note: note, content: textView.text) - - self._updateLastEdit() - } - - fileprivate func _updateLastEdit() { - - if let date = self.note?.lastEditDate { - let formattedDate: String = date.formatted() - self._lastEditLabel?.text = "last edit: \(formattedDate)" - } - } - - fileprivate func _inputAccessoryView() -> UIView { - - let toolbar: UIToolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 333, height: 44.0)) - - let lastEditLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 280, height: 44)) - lastEditLabel.textColor = UIColor.gray - let lastEditButtonItem = UIBarButtonItem(customView: lastEditLabel) - self._lastEditLabel = lastEditLabel - - let flexButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - let dismissButton = UIBarButtonItem(image: UIImage(systemName: "checkmark"), style: .done, target: self, action: #selector(dismissKeyboard)) - toolbar.items = [lastEditButtonItem, flexButton, dismissButton] - - let swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(dismissKeyboard)) - swipeDown.direction = .down - toolbar.addGestureRecognizer(swipeDown) - - return toolbar - } - - // MARK: - Keyboard - - @objc func dismissKeyboard() { - self.textView.resignFirstResponder() - } - - @objc func keyboardDidShow(notification: NSNotification) { - guard let keyboardRect = notification - .userInfo?[UIResponder.keyboardFrameEndUserInfoKey] - as? NSValue else { return } - let frameKeyboard = keyboardRect.cgRectValue - - self.textViewBottomConstraint.constant = frameKeyboard.size.height - } - - @objc func keyboardWillHide() { - self.textViewBottomConstraint.constant = 0.0 - } - - // MARK: - Business - - deinit { - NotificationCenter.default.removeObserver(self) - } - -} diff --git a/Notes/NotesApp.swift b/Notes/NotesApp.swift new file mode 100644 index 0000000..7151fe4 --- /dev/null +++ b/Notes/NotesApp.swift @@ -0,0 +1,22 @@ +// +// NotesApp.swift +// Notes +// +// Created by Claude Code on 13/10/2025. +// + +import SwiftUI + +@main +struct NotesApp: App { + let persistenceController = PersistenceController.shared + @StateObject private var cloudKitMonitor = CloudKitSyncMonitor() + + var body: some Scene { + WindowGroup { + NotesPageView() + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(cloudKitMonitor) + } + } +} diff --git a/Notes/NotesPageView.swift b/Notes/NotesPageView.swift new file mode 100644 index 0000000..ba56589 --- /dev/null +++ b/Notes/NotesPageView.swift @@ -0,0 +1,144 @@ +// +// NotesPageView.swift +// Notes +// +// Created by Claude Code on 13/10/2025. +// + +import SwiftUI +import CoreData + +struct NotesPageView: View { + @Environment(\.managedObjectContext) private var viewContext + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Note.lastEditDate, ascending: true)], + animation: .default) + private var notes: FetchedResults + + @State private var selectedIndex: Int = 0 + @State private var showDeleteAlert = false + + var body: some View { + NavigationStack { + VStack { + + if notes.isEmpty { + Text("No notes") + .foregroundColor(.gray) + } else { + TabView(selection: $selectedIndex) { + ForEach(Array(notes.enumerated()), id: \.element.objectID) { index, note in + NoteEditorView(note: note) + .tag(index) + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button(action: addNote) { + Image(systemName: "plus") + } + + Button(action: shareNote) { + Image(systemName: "square.and.arrow.up") + } + .disabled(notes.isEmpty) + + Button(action: { showDeleteAlert = true }) { + Image(systemName: "trash") + } + .disabled(notes.isEmpty) + } + } + .alert("Delete Note", isPresented: $showDeleteAlert) { + Button("Cancel", role: .cancel) { } + Button("Delete", role: .destructive) { + deleteNote() + } + } message: { + if selectedIndex < notes.count { + let content = notes[selectedIndex].content ?? "" + Text("Do you want to delete this: \(content.prefix(20))...?") + } + } + .onAppear { + createNoteIfNeeded() + selectLastNote() + } + } + } + + private func addNote() { + withAnimation { + let newNote = Note(context: viewContext) + newNote.content = "" + newNote.lastEditDate = Date() + + PersistenceController.shared.save() + + // Select the newly created note (last in the list) + selectedIndex = notes.count - 1 + } + } + + private func deleteNote() { + guard selectedIndex < notes.count else { return } + + withAnimation { + let noteToDelete = notes[selectedIndex] + viewContext.delete(noteToDelete) + PersistenceController.shared.save() + + // Select the last note after deletion + if !notes.isEmpty { + selectedIndex = min(selectedIndex, notes.count - 1) + } + } + } + + private func shareNote() { + guard selectedIndex < notes.count, + let content = notes[selectedIndex].content, + !content.isEmpty else { + return + } + + let activityVC = UIActivityViewController( + activityItems: [content], + applicationActivities: nil + ) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first, + let rootVC = window.rootViewController { + activityVC.popoverPresentationController?.sourceView = rootVC.view + rootVC.present(activityVC, animated: true) + } + } + + private func createNoteIfNeeded() { + if notes.isEmpty { + let newNote = Note(context: viewContext) + newNote.content = "" + newNote.lastEditDate = Date() + PersistenceController.shared.save() + } + } + + private func selectLastNote() { + if !notes.isEmpty { + selectedIndex = notes.count - 1 + } + } +} + +struct NotesPageView_Previews: PreviewProvider { + static var previews: some View { + NotesPageView() + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Notes/NotesPageViewController.swift b/Notes/NotesPageViewController.swift deleted file mode 100644 index ddb7001..0000000 --- a/Notes/NotesPageViewController.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// NotesPageViewController.swift -// Notes -// -// Created by Laurent Morvillier on 16/10/2022. -// - -import Foundation -import UIKit - -class NotesPageViewController : UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource { - - fileprivate var _notes: [Note] = [] - - fileprivate var _currentIndex: Int = 0 - - override func viewDidLoad() { - super.viewDidLoad() - - self._loadNotes() - - let shareButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(shareHandler)) - let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addHandler)) - let deleteButton = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(deleteHandler)) - self.navigationItem.rightBarButtonItems = [addButton, shareButton, deleteButton] - self.view.backgroundColor = UIColor(white: 0.95, alpha: 1.0) - - self.dataSource = self - self.delegate = self - - self._displayLastNote() - - } - - fileprivate func _displayLastNote(animated: Bool = false) { - if let note = self._notes.last { - let index = self._notes.count - 1 - let vc = self.viewController(note: note, index: index) - self.setViewControllers([vc], direction: .forward, animated: animated) - self._currentIndex = index - } - } - - fileprivate func _displayNote(note: Note, animated: Bool = false) { - if let index = self._notes.firstIndex(of: note) { - let vc = self.viewController(note: note, index: index) - self.setViewControllers([vc], direction: .forward, animated: animated) - } else { - print("note not found") - } - } - - fileprivate func viewController(note: Note, index: Int) -> NoteViewController { - if let vc = self.storyboard?.instantiateViewController(withIdentifier: "note") as? NoteViewController { - vc.note = note - vc.index = index - return vc - } else { - fatalError("error with storyboard") - } - } - - fileprivate func _loadNotes() { - - do { - self._notes = try Note.fetchByDate() - if self._notes.isEmpty { - self._createNote() - self._loadNotes() - } - } catch { - print("error = \(error)") - } - - } - - // MARK: - UIPageViewControllerDataSource - - func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - - guard let vc = viewController as? NoteViewController else { - return nil - } - - let previousIndex = vc.index - 1 - if previousIndex < 0 { - return nil - } - - let note = self._notes[previousIndex] - return self.viewController(note: note, index: previousIndex) - } - - func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - - guard let vc = viewController as? NoteViewController else { - return nil - } - - let nextIndex = vc.index + 1 - if nextIndex == self._notes.count { - return nil - } - - let note = self._notes[nextIndex] - return self.viewController(note: note, index: nextIndex) - } - - func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { - - if completed, let vc = pageViewController.viewControllers?.first as? NoteViewController { - self._currentIndex = vc.index - } - - } - - // MARK: - Business - - @objc func shareHandler() { - - guard let text = self._notes[self._currentIndex].content else { - return - } - - let activityViewController = UIActivityViewController(activityItems: [text], applicationActivities: nil) - self.present(activityViewController, animated: true) - } - - @objc func addHandler() { - self._createNote() - } - - @objc func deleteHandler() { - - let note = self._notes[self._currentIndex] - guard let content = note.content else { - self.showAlert(message: "Sorry!", title: "Note not found") - return - } - - areYouSure(message: "Do you want to delete this: \(content.prefix(20))...?") { - AppDelegate.viewContext.delete(note) - AppDelegate.shared.saveContext() - self._loadNotes() - self._displayLastNote(animated: true) - } - - } - - fileprivate func _createNote() { - let note = Note(context: AppDelegate.viewContext) - self._loadNotes() - self._displayNote(note: note) - } - -} diff --git a/Notes/PersistenceController.swift b/Notes/PersistenceController.swift new file mode 100644 index 0000000..10e3ed4 --- /dev/null +++ b/Notes/PersistenceController.swift @@ -0,0 +1,63 @@ +// +// PersistenceController.swift +// Notes +// +// Created by Claude Code on 13/10/2025. +// + +import CoreData + +struct PersistenceController { + static let shared = PersistenceController() + + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + + // Create sample notes for preview + for i in 0..<3 { + let newNote = Note(context: viewContext) + newNote.content = "Sample note \(i + 1)\n\nThis is some sample content for testing." + newNote.lastEditDate = Date() + } + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + return result + }() + + let container: NSPersistentCloudKitContainer + + init(inMemory: Bool = false) { + container = NSPersistentCloudKitContainer(name: "Notes") + + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + + container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy + container.viewContext.automaticallyMergesChangesFromParent = true + } + + func save() { + let context = container.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + let nsError = error as NSError + print("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } +} diff --git a/Notes/SceneDelegate.swift b/Notes/SceneDelegate.swift deleted file mode 100644 index 25fb5f7..0000000 --- a/Notes/SceneDelegate.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SceneDelegate.swift -// Notes -// -// Created by Laurent Morvillier on 21/09/2022. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - - // Save changes in the application's managed object context when the application transitions to the background. - (UIApplication.shared.delegate as? AppDelegate)?.saveContext() - } - - -} - diff --git a/Notes/Storage/CoreDataStorage.swift b/Notes/Storage/CoreDataStorage.swift index 0290126..8ca3261 100644 --- a/Notes/Storage/CoreDataStorage.swift +++ b/Notes/Storage/CoreDataStorage.swift @@ -24,7 +24,7 @@ class CoreDataStorage { } @objc fileprivate func _storageRequested() { - AppDelegate.shared.saveContext() + PersistenceController.shared.save() } } diff --git a/Notes/UIViewController+Extensions.swift b/Notes/UIViewController+Extensions.swift deleted file mode 100644 index 550f51c..0000000 --- a/Notes/UIViewController+Extensions.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// UIViewController+Extensions.swift -// Notes -// -// Created by Laurent Morvillier on 19/09/2022. -// - -import Foundation -import UIKit - -extension UIViewController { - - @objc public func showAlert(message: String, title: String?, completion: (() -> ())? = nil) { - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let action = UIAlertAction(title: NSLocalizedString("OK", comment: "OK"), style: .default, handler: nil) - alertController.addAction(action) - self.present(alertController, animated: true, completion: completion) - } - - func areYouSure(title: String? = nil, - message: String? = NSLocalizedString("Please confirm the action", comment: ""), - executable: @escaping () -> Void) { - - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("No", comment:""), style: .cancel, handler: { _ in - // do nothing - })) - alert.addAction(UIAlertAction(title: NSLocalizedString("Yes", comment:""), style: .destructive, handler: { action in - executable() - })) - self.present(alert, animated: true, completion: nil) - - } - -} diff --git a/Notes/ViewController.swift b/Notes/ViewController.swift deleted file mode 100644 index 0a399bd..0000000 --- a/Notes/ViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ViewController.swift -// Notes -// -// Created by Laurent Morvillier on 21/09/2022. -// - -import UIKit - -class ViewController : UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} -