Refactor UI from nibs to swiftUI with claude help

main
Laurent 4 weeks ago
parent dcfee339af
commit 533c34391f
  1. 2
      CLAUDE.md
  2. 79
      Notes.xcodeproj/project.pbxproj
  3. 101
      Notes/AppDelegate.swift
  4. 25
      Notes/AutoSaveManager.swift
  5. 25
      Notes/Base.lproj/LaunchScreen.storyboard
  6. 81
      Notes/Base.lproj/Main.storyboard
  7. 29
      Notes/CloudKitSyncMonitor.swift
  8. 26
      Notes/Info.plist
  9. 2
      Notes/Model/Note+CoreDataProperties.swift
  10. 69
      Notes/NoteEditorView.swift
  11. 156
      Notes/NoteViewController.swift
  12. 22
      Notes/NotesApp.swift
  13. 144
      Notes/NotesPageView.swift
  14. 156
      Notes/NotesPageViewController.swift
  15. 63
      Notes/PersistenceController.swift
  16. 55
      Notes/SceneDelegate.swift
  17. 2
      Notes/Storage/CoreDataStorage.swift
  18. 35
      Notes/UIViewController+Extensions.swift
  19. 19
      Notes/ViewController.swift

@ -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.

@ -7,19 +7,17 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
C4B45E8928FC348900AC6DAF /* NotesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B45E8828FC348900AC6DAF /* NotesPageViewController.swift */; }; C4A1B10128DB4A01003DDC24 /* NotesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10028DB4A01003DDC24 /* NotesApp.swift */; };
C4EEE41F28DB33D8003DDC24 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE41E28DB33D8003DDC24 /* AppDelegate.swift */; }; C4A1B10328DB4A15003DDC24 /* NotesPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10228DB4A15003DDC24 /* NotesPageView.swift */; };
C4EEE42128DB33D8003DDC24 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE42028DB33D8003DDC24 /* SceneDelegate.swift */; }; C4A1B10528DB4A29003DDC24 /* NoteEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10428DB4A29003DDC24 /* NoteEditorView.swift */; };
C4EEE42328DB33D8003DDC24 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE42228DB33D8003DDC24 /* ViewController.swift */; }; C4A1B10728DB4A3D003DDC24 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A1B10628DB4A3D003DDC24 /* PersistenceController.swift */; };
C4EEE42628DB33D8003DDC24 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C4EEE42428DB33D8003DDC24 /* Main.storyboard */; }; 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 */; }; C4EEE42928DB33D8003DDC24 /* Notes.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE42728DB33D8003DDC24 /* Notes.xcdatamodeld */; };
C4EEE42B28DB33D9003DDC24 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C4EEE42A28DB33D9003DDC24 /* Assets.xcassets */; }; 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 */; }; C4EEE43928DB33D9003DDC24 /* NotesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE43828DB33D9003DDC24 /* NotesTests.swift */; };
C4EEE44328DB33D9003DDC24 /* NotesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE44228DB33D9003DDC24 /* NotesUITests.swift */; }; C4EEE44328DB33D9003DDC24 /* NotesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE44228DB33D9003DDC24 /* NotesUITests.swift */; };
C4EEE44528DB33D9003DDC24 /* NotesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE44428DB33D9003DDC24 /* NotesUITestsLaunchTests.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 */; }; C4EEE46728DB3790003DDC24 /* FileOperator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE46528DB3790003DDC24 /* FileOperator.swift */; };
C4EEE46928DB3790003DDC24 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE46628DB3790003DDC24 /* FileStorage.swift */; }; C4EEE46928DB3790003DDC24 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE46628DB3790003DDC24 /* FileStorage.swift */; };
C4EEE46A28DB3790003DDC24 /* PreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE46428DB378F003DDC24 /* PreferencesStorage.swift */; }; C4EEE46A28DB3790003DDC24 /* PreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EEE46428DB378F003DDC24 /* PreferencesStorage.swift */; };
@ -47,23 +45,21 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
C4B45E8828FC348900AC6DAF /* NotesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesPageViewController.swift; sourceTree = "<group>"; }; C4A1B10028DB4A01003DDC24 /* NotesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesApp.swift; sourceTree = "<group>"; };
C4A1B10228DB4A15003DDC24 /* NotesPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesPageView.swift; sourceTree = "<group>"; };
C4A1B10428DB4A29003DDC24 /* NoteEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteEditorView.swift; sourceTree = "<group>"; };
C4A1B10628DB4A3D003DDC24 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
C4A1B10828DB4A51003DDC24 /* AutoSaveManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoSaveManager.swift; sourceTree = "<group>"; };
C4A1B10A28DB4A65003DDC24 /* CloudKitSyncMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitSyncMonitor.swift; sourceTree = "<group>"; };
C4EEE41B28DB33D8003DDC24 /* Notes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Notes.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; };
C4EEE42028DB33D8003DDC24 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
C4EEE42228DB33D8003DDC24 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
C4EEE42528DB33D8003DDC24 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
C4EEE42828DB33D8003DDC24 /* Notes.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Notes.xcdatamodel; sourceTree = "<group>"; }; C4EEE42828DB33D8003DDC24 /* Notes.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Notes.xcdatamodel; sourceTree = "<group>"; };
C4EEE42A28DB33D9003DDC24 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; C4EEE42A28DB33D9003DDC24 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C4EEE42D28DB33D9003DDC24 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
C4EEE42F28DB33D9003DDC24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; C4EEE42F28DB33D9003DDC24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C4EEE43428DB33D9003DDC24 /* NotesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NotesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; C4EEE43828DB33D9003DDC24 /* NotesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesTests.swift; sourceTree = "<group>"; };
C4EEE43E28DB33D9003DDC24 /* NotesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NotesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; C4EEE44228DB33D9003DDC24 /* NotesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesUITests.swift; sourceTree = "<group>"; };
C4EEE44428DB33D9003DDC24 /* NotesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesUITestsLaunchTests.swift; sourceTree = "<group>"; }; C4EEE44428DB33D9003DDC24 /* NotesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesUITestsLaunchTests.swift; sourceTree = "<group>"; };
C4EEE45128DB3422003DDC24 /* NoteViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteViewController.swift; sourceTree = "<group>"; };
C4EEE45228DB3423003DDC24 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = "<group>"; };
C4EEE45928DB358A003DDC24 /* Notes.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Notes.entitlements; sourceTree = "<group>"; }; C4EEE45928DB358A003DDC24 /* Notes.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Notes.entitlements; sourceTree = "<group>"; };
C4EEE46428DB378F003DDC24 /* PreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesStorage.swift; sourceTree = "<group>"; }; C4EEE46428DB378F003DDC24 /* PreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesStorage.swift; sourceTree = "<group>"; };
C4EEE46528DB3790003DDC24 /* FileOperator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperator.swift; sourceTree = "<group>"; }; C4EEE46528DB3790003DDC24 /* FileOperator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperator.swift; sourceTree = "<group>"; };
@ -125,17 +121,15 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4EEE45928DB358A003DDC24 /* Notes.entitlements */, C4EEE45928DB358A003DDC24 /* Notes.entitlements */,
C4EEE41E28DB33D8003DDC24 /* AppDelegate.swift */, C4A1B10028DB4A01003DDC24 /* NotesApp.swift */,
C4EEE42028DB33D8003DDC24 /* SceneDelegate.swift */, C4A1B10228DB4A15003DDC24 /* NotesPageView.swift */,
C4EEE42228DB33D8003DDC24 /* ViewController.swift */, C4A1B10428DB4A29003DDC24 /* NoteEditorView.swift */,
C4EEE45128DB3422003DDC24 /* NoteViewController.swift */, C4A1B10628DB4A3D003DDC24 /* PersistenceController.swift */,
C4B45E8828FC348900AC6DAF /* NotesPageViewController.swift */, C4A1B10828DB4A51003DDC24 /* AutoSaveManager.swift */,
C4EEE45228DB3423003DDC24 /* UIViewController+Extensions.swift */, C4A1B10A28DB4A65003DDC24 /* CloudKitSyncMonitor.swift */,
C4EEE46B28DB379B003DDC24 /* Storage */, C4EEE46B28DB379B003DDC24 /* Storage */,
C4EEE46228DB3714003DDC24 /* Model */, C4EEE46228DB3714003DDC24 /* Model */,
C4EEE42428DB33D8003DDC24 /* Main.storyboard */,
C4EEE42A28DB33D9003DDC24 /* Assets.xcassets */, C4EEE42A28DB33D9003DDC24 /* Assets.xcassets */,
C4EEE42C28DB33D9003DDC24 /* LaunchScreen.storyboard */,
C4EEE42F28DB33D9003DDC24 /* Info.plist */, C4EEE42F28DB33D9003DDC24 /* Info.plist */,
C4EEE42728DB33D8003DDC24 /* Notes.xcdatamodeld */, C4EEE42728DB33D8003DDC24 /* Notes.xcdatamodeld */,
); );
@ -291,9 +285,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C4EEE42E28DB33D9003DDC24 /* LaunchScreen.storyboard in Resources */,
C4EEE42B28DB33D9003DDC24 /* Assets.xcassets in Resources */, C4EEE42B28DB33D9003DDC24 /* Assets.xcassets in Resources */,
C4EEE42628DB33D8003DDC24 /* Main.storyboard in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -321,16 +313,16 @@
C4EEE48428DB3B6B003DDC24 /* CoreDataStorage.swift in Sources */, C4EEE48428DB3B6B003DDC24 /* CoreDataStorage.swift in Sources */,
C4EEE48228DB39F1003DDC24 /* Note+CoreDataProperties.swift in Sources */, C4EEE48228DB39F1003DDC24 /* Note+CoreDataProperties.swift in Sources */,
C4EEE46A28DB3790003DDC24 /* PreferencesStorage.swift in Sources */, C4EEE46A28DB3790003DDC24 /* PreferencesStorage.swift in Sources */,
C4EEE42328DB33D8003DDC24 /* ViewController.swift in Sources */,
C4EEE46928DB3790003DDC24 /* FileStorage.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 */, C4EEE46728DB3790003DDC24 /* FileOperator.swift in Sources */,
C4EEE48128DB39F1003DDC24 /* Note+CoreDataClass.swift in Sources */, C4EEE48128DB39F1003DDC24 /* Note+CoreDataClass.swift in Sources */,
C4EEE45428DB3423003DDC24 /* UIViewController+Extensions.swift in Sources */,
C4EEE42928DB33D8003DDC24 /* Notes.xcdatamodeld 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; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -366,25 +358,6 @@
}; };
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
C4EEE42428DB33D8003DDC24 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
C4EEE42528DB33D8003DDC24 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
C4EEE42C28DB33D9003DDC24 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
C4EEE42D28DB33D9003DDC24 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
C4EEE44628DB33D9003DDC24 /* Debug */ = { C4EEE44628DB33D9003DDC24 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
@ -512,8 +485,6 @@
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Notes/Info.plist; INFOPLIST_FILE = Notes/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -541,8 +512,6 @@
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Notes/Info.plist; INFOPLIST_FILE = Notes/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (

@ -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<UISceneSession>) {
// 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)")
}
}
}
}

@ -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()
}
}

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

@ -1,81 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="TxU-Fl-tNx">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="hez-bJ-diu">
<objects>
<navigationController id="TxU-Fl-tNx" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" id="WFB-v3-7ji">
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="bLT-f5-baA" kind="relationship" relationship="rootViewController" id="zSe-W8-vnK"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="It9-d6-eH5" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="810" y="-664"/>
</scene>
<!--Notes Page View Controller-->
<scene sceneID="mCo-Xm-AqT">
<objects>
<pageViewController autoresizesArchivedViewToFullSize="NO" transitionStyle="scroll" navigationOrientation="horizontal" spineLocation="none" id="bLT-f5-baA" customClass="NotesPageViewController" customModule="Notes" customModuleProvider="target" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="1nB-b5-m4A"/>
</pageViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="XzJ-cd-L4C" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1594" y="-664"/>
</scene>
<!--Note View Controller-->
<scene sceneID="3me-1s-GmT">
<objects>
<viewController storyboardIdentifier="note" id="NKc-C6-bTZ" customClass="NoteViewController" customModule="Notes" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="zfQ-Xr-jsC">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="zPN-eq-BkD">
<rect key="frame" x="8" y="56" width="398" height="798"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" name="DINAlternate-Bold" family="DIN Alternate" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="Qu6-9S-tel"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="Qu6-9S-tel" firstAttribute="trailing" secondItem="zPN-eq-BkD" secondAttribute="trailing" constant="8" id="9rz-bm-QIs"/>
<constraint firstItem="Qu6-9S-tel" firstAttribute="bottom" secondItem="zPN-eq-BkD" secondAttribute="bottom" constant="8" id="ElV-mT-Day"/>
<constraint firstItem="zPN-eq-BkD" firstAttribute="leading" secondItem="Qu6-9S-tel" secondAttribute="leading" constant="8" id="hUX-Ie-RB0"/>
<constraint firstItem="zPN-eq-BkD" firstAttribute="top" secondItem="Qu6-9S-tel" secondAttribute="top" constant="8" id="jaW-K0-O4W"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="obh-lB-Hmb"/>
<connections>
<outlet property="textView" destination="zPN-eq-BkD" id="gC1-fy-xI4"/>
<outlet property="textViewBottomConstraint" destination="ElV-mT-Day" id="esj-WS-G5P"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="BQh-DG-myD" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2291" y="-664"/>
</scene>
</scenes>
<resources>
<systemColor name="labelColor">
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

@ -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<AnyCancellable>()
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
}
}

@ -2,28 +2,16 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string></string>
<key>UIImageName</key>
<string></string>
</dict>
</dict> </dict>
</plist> </plist>

@ -23,7 +23,7 @@ extension Note {
static func fetchByDate() throws -> [Note] { static func fetchByDate() throws -> [Note] {
let request = Note.fetchRequest() let request = Note.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "lastEditDate", ascending: true)] request.sortDescriptors = [NSSortDescriptor(key: "lastEditDate", ascending: true)]
return try AppDelegate.viewContext.fetch(request) return try PersistenceController.shared.container.viewContext.fetch(request)
} }
} }

@ -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)
}
}

@ -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)
}
}

@ -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)
}
}
}

@ -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<Note>
@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)
}
}

@ -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)
}
}

@ -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)")
}
}
}
}

@ -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()
}
}

@ -24,7 +24,7 @@ class CoreDataStorage {
} }
@objc fileprivate func _storageRequested() { @objc fileprivate func _storageRequested() {
AppDelegate.shared.saveContext() PersistenceController.shared.save()
} }
} }

@ -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)
}
}

@ -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.
}
}
Loading…
Cancel
Save