Backup bows out of the phone — the zipped-share future replaces it, later

IndieBackup comes out whole: the package, the Backup directory, the Settings section, the .kanbanbackup document type and its onOpenURL restore path. Settings is now the About section alone. The Files-app keys stay — they serve local-board visibility, not backups. Board sharing as a zipped archive is the planned replacement and is deliberately not started here.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-08 11:25:53 -04:00
parent cdba512512
commit adbc5ddd89
8 changed files with 7 additions and 189 deletions
+1 -33
View File
@@ -1,9 +1,7 @@
import SwiftUI
import IndieBackup
import os
/// The iPhone app's entry point. Two tabs, per the mobile MVP charter: Boards (the whole
/// board lane card navigation stack) and Settings (backup/restore).
/// board lane card navigation stack) and Settings.
@main
struct MobileApp: App {
@@ -13,40 +11,10 @@ struct MobileApp: App {
/// constructing one.
@State private var boardIndex = BoardIndexStore()
/// Backup/restore (indie-backup skill), built once `boardIndex.home` resolves to a real
/// `CloudHome` see `MobileBackupConfiguration`'s doc comment for why this can't happen at
/// `@State` init time. Stays `nil` for the lifetime of a run with no iCloud account;
/// `SettingsTabView` renders the quiet disabled state for exactly that case.
@State private var backupController: BackupController?
private static let logger = Logger(subsystem: "dev.rzen.indie.KanbanMobile", category: "backup")
var body: some Scene {
WindowGroup {
RootTabView()
.environment(boardIndex)
.environment(\.backupController, backupController)
.task(id: boardIndex.home) {
guard backupController == nil, let home = boardIndex.home else { return }
backupController = BackupController(
configuration: MobileBackupConfiguration(root: home.documentsURL, index: boardIndex)
)
}
.onOpenURL { url in
// A backup file opened from Files, AirDrop, or a share sheet. Silently
// ignored before `backupController` exists there is no resolved cloud home
// to restore into either, at that point.
guard url.pathExtension == MobileBackupConfiguration.fileExtension,
let backupController
else { return }
Task {
do {
try await backupController.restoreBackup(from: url)
} catch {
Self.logger.error("restore from opened file failed: \(error.localizedDescription, privacy: .public)")
}
}
}
}
}
}
@@ -1,68 +0,0 @@
import Foundation
import IndieBackup
import SwiftUI
/// IndieBackup's app-specific wiring (indie-backup skill): what gets backed up, and how the
/// app's in-memory board list is rebuilt after a restore.
///
/// **Why this type is constructed rather than defaulted.** `BackupConfiguration.backupRoot` is a
/// non-optional `URL`, but the ubiquity container resolves asynchronously and can fail outright
/// (`CloudHomeResolver`, CloudHome.swift) there is no root to hand IndieBackup until
/// `BoardIndexStore.home` exists. Rather than answer with a placeholder path, this type is only
/// ever constructed once a real `CloudHome` is in hand (`MobileApp`'s `.task(id:)`); before that
/// there is no `BackupController` at all, and `SettingsTabView` renders a quiet disabled
/// explanation instead of a section built on a fabricated root.
final class MobileBackupConfiguration: BackupConfiguration {
/// No dot. Shared by the Info.plist document-type/UTI registration
/// (`dev.rzen.indie.kanban-backup`) and the archive filenames IndieBackup writes.
static let fileExtension = "kanbanbackup"
private let root: URL
private let index: BoardIndexStore
/// - Parameters:
/// - root: The resolved `CloudHome.documentsURL` the same `Documents/` folder every
/// `.kanban` board lives directly inside. IndieBackup walks it file-by-file (it has no
/// notion of "package"), so a board's directory structure round-trips through a backup
/// as an ordinary tree of files no directory registration or special-casing needed.
/// - index: Rescanned after a restore replaces the tree, in `rebuildCacheAfterRestore`.
init(root: URL, index: BoardIndexStore) {
self.root = root
self.index = index
}
var backupFileExtension: String { Self.fileExtension }
var backupDisplayName: String { "Lanework Backup" }
var backupRoot: URL { root }
/// `BoardIndexStore` caches nothing durable no Core Data, no SwiftData, no file on disk
/// only an in-memory `[BoardSummary]` scanned straight from the files under `backupRoot`. A
/// restore already replaced that tree by the time this runs, so the only rebuild step is
/// asking the store to rescan it; a `BoardSession` for a board that's open elsewhere reloads
/// its own content from disk independently the next time it's asked (BoardIndexStore's doc
/// comment on the observer flow).
///
/// `refresh()` only *starts* the rescan (it hands off to a detached `Task` and returns), so
/// this reports done as soon as the request lands, not once the boards tab has repainted
/// same as any other call to `refresh()` in this app (e.g. pull-to-refresh).
func rebuildCacheAfterRestore(progress: @escaping (Double, String) -> Void) async throws {
progress(0, "Rescanning boards…")
await index.refresh()
progress(1, "Rescanning boards…")
}
}
/// Threads the lazily-constructed `BackupController` from `MobileApp` down to `SettingsTabView`
/// (the backup section) and back up to `MobileApp`'s own `onOpenURL` handler (imported backup
/// files), without forcing `RootTabView` or anything else between them to know backup exists.
/// `nil` until the cloud home resolves; see `MobileBackupConfiguration`.
private struct BackupControllerKey: EnvironmentKey {
static let defaultValue: BackupController? = nil
}
extension EnvironmentValues {
var backupController: BackupController? {
get { self[BackupControllerKey.self] }
set { self[BackupControllerKey.self] = newValue }
}
}
-2
View File
@@ -3,5 +3,3 @@
Version 1.0: Lanework comes to iPhone — browse your boards from iCloud Drive, move cards between lanes, and edit them on the go.
Swipe a board in the list to open its settings and move it between iCloud and this iPhone, which now works without an iCloud account.
Back up your boards from Settings and restore any backup later, even on a new phone.
+2 -35
View File
@@ -28,9 +28,8 @@
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<!-- Document-based, so App Store Connect requires this (indie-backup skill ▸ Register the
Backup File Type) — the in-place editing the imported-backup `onOpenURL` handler
(MobileApp.swift) relies on. -->
<!-- Document-based, so App Store Connect requires this — the in-place editing that lets a
board opened from the Files app (or "On My iPhone ▸ Lanework") be edited where it sits. -->
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<!-- Publishes the sandbox's Documents/ as "On My iPhone ▸ Lanework" in the Files app — the
@@ -59,21 +58,6 @@
<key>LSTypeIsPackage</key>
<true/>
</dict>
<!-- The backup archive IndieBackup writes/reads (indie-backup skill), so Files, AirDrop
and share sheets route a tapped `.kanbanbackup` file to this app's `onOpenURL`
instead of leaving it unopenable. -->
<dict>
<key>CFBundleTypeName</key>
<string>Lanework Backup</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>dev.rzen.indie.kanban-backup</string>
</array>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
@@ -93,23 +77,6 @@
<string>kanban</string>
</dict>
</dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Lanework Backup</string>
<key>UTTypeIdentifier</key>
<string>dev.rzen.indie.kanban-backup</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>kanbanbackup</string>
</array>
</dict>
</dict>
</array>
<!-- Publishes the container's Documents/ as a visible folder in iCloud Drive — on every
platform, including the Mac's Finder, which is how boards reach the Mac app before it
+2 -37
View File
@@ -1,19 +1,13 @@
import SwiftUI
import IndieAbout
import IndieBackup
/// Backup/restore (indie-backup skill) and the About section (indie-about skill).
/// The About section (indie-about skill).
struct SettingsTabView: View {
@Environment(BoardIndexStore.self) private var index
@Environment(\.backupController) private var backupController
var body: some View {
NavigationStack {
Form {
backupSection
// Last section by convention. The version label doubles as the changelog link,
// which is why the changelog sits in its dedicated slot and not in `documents`.
// Same voice as the Mac app's `AboutBox.configuration`; the changelog itself is
// the phone app's own (see project.yml separate product, separate 1.0).
Section {
@@ -27,36 +21,7 @@ struct SettingsTabView: View {
.navigationTitle("Settings")
}
// Idempotent (BoardIndexStore.start()), and harmless if BoardsTabView already called it
// this tab can be the first one the user opens, and `backupController` only ever appears
// once `index.home` resolves.
// this tab can be the first one the user opens.
.task { index.start() }
}
/// `backupController` is `nil` until `MobileApp` has a resolved `CloudHome` to build one from
/// which is also exactly the condition under which there is a `Documents/` folder to back
/// up. Everything short of that gets the same quiet explanation rather than a section that
/// looks broken or, worse, a crash on a nil root.
@ViewBuilder
private var backupSection: some View {
if let backupController {
BackupsSectionView(controller: backupController)
} else {
Section {
switch index.phase {
case .idle, .loading:
LabeledContent("Backup") {
ProgressView()
}
case .ready:
// `.ready` is both cases now: the run with no iCloud at all, and the runloop turn
// `backupController` lags `index.home` by (MobileApp's `.task(id:)`). Backup is
// still iCloud-only a known gap now that local boards exist.
Label("Backup and restore need iCloud Drive.", systemImage: "icloud.slash")
.foregroundStyle(.secondary)
}
} footer: {
Text("Sign into iCloud in Settings, then come back here to back up your boards.")
}
}
}
}