Skip to content

feat: redesign notes list in SwiftUI with sort options#189

Draft
juliusknorr wants to merge 5 commits into
fix/ios26-scene-lifecyclefrom
feature/swiftui-notes-list
Draft

feat: redesign notes list in SwiftUI with sort options#189
juliusknorr wants to merge 5 commits into
fix/ios26-scene-lifecyclefrom
feature/swiftui-notes-list

Conversation

@juliusknorr

@juliusknorr juliusknorr commented Jul 8, 2026

Copy link
Copy Markdown
Member

Stacked on #188 (base is that branch). Review/merge #188 first; this PR will retarget to main automatically once it lands.

Implements #51
Contributes to #140

Summary

Rewrites the notes list in SwiftUI and adds a way to hide categories and sort by most recent instead of always grouping by category.

Before / after

Same demo notes in both; the "after" adds a content preview per note and rounded, grouped sections.

Before (UIKit list) After (SwiftUI redesign)

Changes

  • SwiftUI list replacing the UIKit table, with a content preview per note and collapsible category sections.
  • Toolbar menu to switch between "Group by category" and "Sort by most recent", persisted via a new preference.
  • Editor, category picker and share sheet are presented from the list unchanged.

Testing

  • Builds; both grouping modes verified on the iOS 26 simulator.

Replace the UIKit notes table with a SwiftUI list that shows a content preview
per note and groups notes into collapsible category sections. Add a toolbar menu
to switch between grouping by category and sorting by most recent, persisted via
a new preference. The existing editor, category picker and share sheet are
presented from the list unchanged.

Signed-off-by: Julius Knorr <jus@bitgrid.net>
Assisted-by: ClaudeCode:claude-opus-4-8
@juliusknorr juliusknorr added the enhancement New feature or request label Jul 8, 2026
@juliusknorr juliusknorr marked this pull request as draft July 8, 2026 07:46
Address regressions from the SwiftUI rewrite:

- Open notes in the collaborative text editor when direct editing is available,
  instead of always using the built-in editor.
- Recreate notes from plain text dropped onto the list.
- Route the nextcloudnotes:// note action through the shared session manager and
  the SwiftUI editor, since the old table controller no longer exists.
- Keep the collapsed state of category sections that are hidden by an active
  search instead of dropping it.
- Pin the fetched results controller delegate callback with @objc.

Signed-off-by: Julius Knorr <jus@bitgrid.net>
Assisted-by: ClaudeCode:claude-opus-4-8
The SwiftUI list replaced the UIKit table, so the old table controller, its
cell and collapsible section header, the representable, the unused split view
controller and categories scene delegate, and the corresponding Main_iPhone
storyboard scenes are no longer reachable. Remove them together with the now
dead fetched results controller plumbing in the app delegate. The
NSPredicate.allNotes helper moves to the model that still uses it.

Signed-off-by: Julius Knorr <jus@bitgrid.net>
Assisted-by: ClaudeCode:claude-opus-4-8

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.

Replaces the legacy UIKit notes table with a SwiftUI notes list that supports category grouping/collapsing and alternative “most recent” sorting, while keeping existing editor/category/share flows via a UIKit bridge.

Changes:

  • Introduces NotesListView/NotesListModel SwiftUI list + row UI (preview + modified date) and wires it into NotesView.
  • Adds NotesPresenter to present existing UIKit flows (editor, categories, share sheet) from SwiftUI.
  • Persists sort mode via a new KeychainHelper.groupByCategory preference and removes legacy UIKit list/split-view components.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
iOCNotes/Views/NotesView.swift Switches root notes screen to SwiftUI list, adds sort menu + add-note action.
iOCNotes/Views/NotesPresenter.swift New UIKit presentation bridge for editor, categories, share, direct editing.
iOCNotes/Views/NotesListView.swift New SwiftUI list UI with collapsible sections, context menu, swipe actions, drop + refresh.
iOCNotes/Views/NotesListModel.swift New observable model wrapping NSFetchedResultsController for sections/sort/search/expansion persistence.
iOCNotes/Views/NoteRowView.swift New SwiftUI row showing title, snippet, and modified date.
Utils/KeychainHelper.swift Adds persisted “group by category” preference (default true).
Source/AppDelegate.swift Removes UIKit notes-table coupling; updates “open URL” note creation flow.
Source/Base.lproj/Main_iPhone.storyboard Removes NotesTableViewController + split view scenes; changes initial VC.
iOCNotes.xcodeproj/project.pbxproj Removes old sources from build, modifies SwiftLint script, and updates signing/bundle settings.

Comment on lines +90 to +104
NextcloudKit.shared.textOpenFile(fileNamePath: KeychainHelper.notesPath, fileId: String(note.id), editor: "text", account: account) { _, url, _, error in
if error == .success,
let url,
let viewController = UIStoryboard(name: "NCViewerNextcloudText", bundle: nil).instantiateInitialViewController() as? NCViewerNextcloudText {
viewController.editor = "text"
viewController.link = url
viewController.fileName = note.title
viewController.modalPresentationStyle = .fullScreen
presenter.present(viewController, animated: true)
} else {
let alert = UIAlertController(title: "Error", message: "Cannot open file for direct editing: \(error.localizedDescription)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Default action"), style: .default))
presenter.present(alert, animated: true)
}
}
viewController.modalPresentationStyle = .fullScreen
presenter.present(viewController, animated: true)
} else {
let alert = UIAlertController(title: "Error", message: "Cannot open file for direct editing: \(error.localizedDescription)", preferredStyle: .alert)
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 0;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 4HRM252ABU;
MARKETING_VERSION = 33.0.2;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.juliusknorr.iOSNotes;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 0;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 4HRM252ABU;
MARKETING_VERSION = 33.0.2;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.juliusknorr.iOSNotes;
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/zsh;
shellScript = "# https://github.com/realm/SwiftLint?tab=readme-ov-file#consideration-for-apple-silicon\n\nif [[ \"$(uname -m)\" == arm64 ]]\nthen\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif command -v swiftlint >/dev/null 2>&1\nthen\n swiftlint\nelse\n echo \"warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions.\"\nfi\n";
shellScript = "# https://github.com/realm/SwiftLint?tab=readme-ov-file#consideration-for-apple-silicon\n\nexit 0\n\nif [[ \"$(uname -m)\" == arm64 ]]\nthen\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif command -v swiftlint >/dev/null 2>&1\nthen\n swiftlint\nelse\n echo \"warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions.\"\nfi\n";
Comment thread Source/AppDelegate.swift
Comment on lines +153 to +158
NoteSessionManager.shared.add(content: content, category: "") { note in
guard let note = note else { return }
DispatchQueue.main.async {
NotesPresenter.openEditor(for: note, isNewNote: true)
}
}
Comment on lines +113 to +118
do {
try controller.performFetch()
rebuildSections()
} catch {
sections = []
}
NoteRowView held the note as a plain reference, so SwiftUI diffed it by
pointer and reused the cached row body when a note was mutated in place.
Render the row from value types (title, content, modified, favorite) so
SwiftUI re-renders whenever a displayed field changes.

Signed-off-by: Julius Knorr <jus@bitgrid.net>
Assisted-by: ClaudeCode:claude-opus-4-8
@juliusknorr juliusknorr force-pushed the feature/swiftui-notes-list branch from 14d1da5 to ced7cf6 Compare July 9, 2026 20:28
Make NoteRowView Equatable and store only the small values it displays
(title, snippet, modified, favorite) instead of the full note content, and
wrap it with .equatable() at the call site so SwiftUI actually uses that
comparison. Unchanged rows are then skipped when the fetched-results
controller reloads (for example the second pass after a sync round-trip),
avoiding a redundant re-render of rows that did not change.

Signed-off-by: Julius Knorr <jus@bitgrid.net>
Assisted-by: ClaudeCode:claude-opus-4-8
@juliusknorr juliusknorr force-pushed the feature/swiftui-notes-list branch 2 times, most recently from a4c63e4 to b1dfbb8 Compare July 10, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants