import SwiftUI /// One board's own settings, reached by swiping its row in the boards list. /// /// **Reached from the list on purpose.** Its one setting today — which home the board is stored in — /// is the only thing in the app that changes a board's URL, and a board's URL is its identity /// (`BoardSummary.id`). Presenting this from the list means no screen is holding the old identity /// when it retires; the sheet dismisses itself on success and the row underneath is already the new /// board. struct BoardSettingsSheet: View { /// A snapshot, taken when the swipe action fired. It goes stale exactly once — at the moment a /// move succeeds — and the sheet is gone by then. let board: BoardSummary @Environment(BoardIndexStore.self) private var index @Environment(\.dismiss) private var dismiss @State private var isConfirmingMove = false @State private var isMoving = false /// The last move's refusal, shown in the section footer rather than as an alert: the failure is /// about the row the user is looking at, and every one of them is a "try again" rather than a /// decision to make. @State private var failure: String? /// There are two homes, so there is one destination and the button can name it instead of asking. private var destination: BoardLocation { board.location == .icloud ? .local : .icloud } /// iCloud is a destination only when it resolved. In practice a run with no iCloud lists no iCloud /// boards at all — the query is dead — so this guards a state the app should not be able to reach /// rather than one it routinely does. private var canMove: Bool { destination == .local || index.cloudUnavailable == nil } var body: some View { NavigationStack { Form { Section { LabeledContent("Stored In", value: board.location.name) Button { isConfirmingMove = true } label: { HStack { Text("Move to \(destination.name)") if isMoving { Spacer() ProgressView() } } } .disabled(isMoving || !canMove) } header: { Text("Location") } footer: { Text(footer) } } .navigationTitle("Board Settings") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } } .confirmationDialog( confirmationTitle, isPresented: $isConfirmingMove, titleVisibility: .visible ) { // Destructive only in the direction that removes protection: a board leaving iCloud // stops syncing and stops being backed up by anything but this phone. The return trip // only adds, so it is an ordinary button. Button("Move", role: destination == .local ? .destructive : nil) { move() } Button("Cancel", role: .cancel) {} } message: { Text(confirmationMessage) } } } private var footer: String { if let failure { return failure } if !canMove { return "Sign into iCloud to move boards there." } switch board.location { case .icloud: return "This board is in iCloud Drive and syncs to your other devices." case .local: return "This board is on this iPhone only." } } private var confirmationTitle: String { destination == .local ? "Move “\(board.title)” out of iCloud?" : "Move “\(board.title)” to iCloud?" } private var confirmationMessage: String { destination == .local ? "It will stop syncing and live only on this iPhone. Your other devices won't see it." : "It will sync to your other devices." } private func move() { isMoving = true failure = nil Task { let outcome = await index.relocateBoard(at: board.rootURL, to: destination) isMoving = false switch outcome { case .success: // The list underneath has already refreshed, and this sheet's `board` names a package // that is no longer there. dismiss() case let .failure(reason): failure = reason.description } } } }