r/SwiftUI • • 4d ago

Promotion (must include link to source code) [Open Source] Thought Meadow: SwiftUI notes with Arabic/English RTL and native Markdown/checklist preview

I'm sharing the MIT-licensed SwiftUI source for Thought Meadow, my small iPhone notes app. The full Xcode project, assets and build instructions are browsable here:

Source and build instructions

A few implementation details that may be useful to other SwiftUI developers:

  • Language and direction: the root sets both locale and layoutDirection for English/Arabic, and changing the language recreates the root view using its language ID. WaraqaApp.swift
  • Editor and preview: writing uses TextEditor bindings into the note store. A separate native SwiftUI preview renders basic headings, quotes, bullets and task lists. Tapping a task updates its Markdown line through the same store. NoteEditor.swift
  • Local state: a main-actor ObservableObject owns the notebook, with atomic local writes, recoverable trash, Files import/export and optional password encryption. The repo also contains storage/encryption checks and a separate 500-note data-layer check.

I'd welcome code feedback on the editor bindings and language-switching approach, especially with mixed Arabic/English notes and accessibility. The preview intentionally supports a small Markdown subset rather than a complete CommonMark renderer.

The current app is free, requires iOS 17+, and has no account, ads or analytics. App Store. The internal project name remains Waraqa.

3 Upvotes

16 comments sorted by

1

u/LividIllustrator8915 4d ago

Skimmed through WaraqaApp.swift and NoteEditor.swift—really clean codebase and refreshing to see someone tackle native bidirectional text in pure SwiftUI.

A couple of concrete observations from dealing with similar document setups on iOS:

The .id(language) trick on the root view is pragmatic for forcing a total hierarchy reload on direction changes, but watch out if the language toggle is ever nested inside NavigationStack or sheet presentations down the road, as blowing away the root identity will abruptly unmount navigation state and cancel dismiss transitions. For the layout direction itself, relying on the environment works well for the overall UI layout, but for the actual note body in TextEditor, the underlying UITextView typically respects paragraph-level base direction (the natural BiDi rule where the first strong directional character sets the line direction). If you force the container's layoutDirection onto TextEditor, mixed notes (like an Arabic paragraph containing English variable names, code snippets, or trailing punctuation) can sometimes exhibit odd cursor jumps or trailing parentheses flipping to the opposite margin.

In NoteEditor.swift, placing TextEditor inside an outer ScrollView with a minHeight can occasionally trigger scroll-gesture collisions on iOS, where the inner text container and outer scroll view compete for interactive keyboard dismissal. On iOS 16+, TextField(..., axis: .vertical) with scrollContentBackground(.hidden) often plays much nicer inside custom scroll containers if you don't need rich formatting attributes inside the raw input.

Also a quick edge case on the checkbox toggle in NoteEditor: lines[index].dropFirst(6) works nicely for top-level "- [ ] " items, but if users indent subtasks with spaces or tabs (like " - [ ] "), dropping a fixed 6 characters will slice into the actual task text. Replacing the prefix via regex match or matching the bracket indices avoids corrupting indented checklists.

Really solid work keeping it local-first with zero trackers.

1

u/Old-Construction-594 4d ago

Thanks for taking the time to review the code — these were useful, concrete cases. I've pushed two source changes.

Indented tasks previously weren't recognized as interactive tasks in the preview. They now use the same parser for rendering and editing, and a toggle replaces only the matched checkbox-state character. Spaces/tabs, Arabic/English text, emoji, punctuation and line endings are preserved. Invalid lines and read-only notes are left alone.

I also removed .id(language) from the root and pass the language as a view input while updating the locale/direction environment, avoiding the deliberate replacement of the root hierarchy.

The 14 focused checklist checks and 26 existing native checks passed on macOS, including save/reopen and encrypted persistence. The iOS Simulator build also succeeded.

Interactive language/navigation, mixed-script cursor and keyboard-dismissal checks are still pending because CoreSimulator is unavailable here today. I'm keeping TextEditor until those long-note/input cases can be tested properly. These changes are in the source; they aren't in the App Store build yet. Thanks again for pointing me to the edge cases.

1

u/cocoabuilder 4d ago

Nice one, and kudos for going MIT — genuinely useful to have a native bidi notes example to point at. A few things from experience with mixed-script editors:

  • App-level layoutDirection works for the chrome, but for the note content itself, paragraph-level direction matters more. The bidi algorithm already decides each paragraph's direction from its first strong character, so if you force the whole note to one direction, an English-leading paragraph inside an Arabic note ends up with the wrong alignment. Worth resolving each paragraph's base writing direction from the paragraph text (in both the editor and the Markdown preview) rather than from the app language.

  • On the .id(language) root-view recreation: pragmatic, and it guarantees a clean reload. One side effect worth checking is the undo stack — TextEditor's undo manager belongs to the view hierarchy, so recreating the root discards it. If someone switches language right after typing, their undo history is gone. If that's acceptable, fine; otherwise driving the switch through .environment(\.locale, ...) plus an explicit .environment(\.layoutDirection, ...) avoids the identity reset entirely.

  • Since you call out mixed Arabic/English notes: test how spell-checking and autocorrection behave when the keyboard switches mid-note, and double-check that your editor font covers Arabic glyphs well at body sizes. System fonts are fine here; custom fonts often miss them.

  • On accessibility: VoiceOver follows paragraph direction, so if the preview forces a single direction, mixed paragraphs can be read back in a confusing order. Making the preview's per-paragraph direction match what the user typed is the thing to verify.

  • Minor thing on the password encryption: make sure you're not rolling your own crypto and that the key lives in the Keychain rather than alongside the notes.

Good call on keeping the Markdown preview to a subset, by the way — a full CommonMark renderer is where note apps go to die.

1

u/Old-Construction-594 4d ago

Thanks — the undo-history point is a good reason to avoid replacing the whole view hierarchy. I've now removed the root .id(language) reset, keeping the locale/direction environment and passing language through the views that need to refresh. Interactive undo/navigation verification is still pending, so I'm not claiming that has been tested yet.

I agree that paragraph direction, keyboard switching/autocorrection and VoiceOver need their own mixed-script checks; changing the app's chrome direction doesn't establish those. The body editor already uses the system .body font. I've recorded those input/accessibility cases for verification before changing the text control. CoreSimulator isn't working here today; the source compiles for iOS Simulator and the 40 data-layer checks pass on macOS.

On encryption: the app uses Apple's CryptoKit AES-256-GCM and CommonCrypto PBKDF2-SHA-256 (600,000 iterations). The password-derived key is kept only in memory while unlocked and is discarded when locking succeeds. Neither the password nor key is written alongside the notes or persisted in Keychain; each unlock needs the password again. The vault stores the salt, nonce and authenticated ciphertext. That implementation hasn't had an independent security audit.

Appreciate the specific review cases — these source changes aren't in the App Store build yet.

1

u/cocoabuilder 3d ago

Glad the feedback landed — and nice turnaround. Dropping the root .id(language) reset is absolutely the right call. Rebuilding the whole view hierarchy identity to fix layout is exactly the kind of thing that silently invalidates the undo registration, and undo breakage is one of those bugs that's miserable to chase after the fact, because it only surfaces in very specific typing and editing sequences.

On the pending verification, two cases I'd put at the top of the list: (1) a mid-note language switch with undo — type a few lines, switch language, then undo and confirm the text state is what you'd expect rather than something half-reverted; (2) a mixed-script paragraph with VoiceOver on, checking the reading order follows the paragraph direction rather than the app chrome. Those are the ones most likely to bite.

On the encryption side: keeping the derived key only in memory and requiring the password on every unlock is a solid posture. Keep that no-independent-audit caveat in the README until you get one — that kind of honesty actually builds trust.

Happy to keep reviewing as the mixed-script checks land.

→ More replies (7)

1

u/Axons-One 3d ago

nice to see the source changes from the feedback here. one thing i would check on a real phone is switching language while a long note is open with the keyboard up. does it keep the cursor and scroll position too? would be useful to include that when you do the interactive checks.

1

u/Old-Construction-594 3d ago

Thanks — I've added that exact case to the editor interaction checklist: a long note, a mid-note insertion point and the on-screen keyboard open, comparing selection, visible paragraph, scroll position and text before/after the language transition.

The simulator is working again. I verified that Settings stays open when switching to Arabic and back, and that an indented Arabic task keeps its text and checked state after reopening the note. Those checks don't establish cursor or scroll preservation on a real phone, so that case is still explicitly pending. The app-language setting currently lives on the root screen; I'll distinguish leaving/reopening the editor from an actual in-place language update. Appreciate the precise test suggestion.

1

u/imsnk81 3d ago

Recreating the root on a language switch is the simple way, just watch that people don't lose their scroll position or open note when it happens. For mixed notes I'd test a line that starts in English and ends in Arabic with a checklist item, that's usually where alignment gets weird. Nice to see RTL treated as a real thing and not an afterthought.

1

u/Old-Construction-594 3d ago

Thanks for the concrete case. I removed the language-based root identity reset; that fix is now in 1.0.2. I also added your English-leading/Arabic-ending checklist case to the interaction checks.

On the iPhone 17 Pro Max simulator, the three task controls rendered, toggling the indented task changed only its checkbox marker, and its state survived reopening the note. That is a narrow rendering/toggle check: caret, scroll and a keyboard-open language transition still need a physical-phone pass. Appreciate the useful example.