10f6d0ca20
iCloud sync: - Force viewContext.refreshAllObjects() on remote change notifications so data from other devices is picked up immediately without app restart - Call refreshFromCloudKit() on foreground to merge any changes made while the device was inactive - Wait up to 10s for initial CloudKit sync on launch before showing onboarding (shows "Checking iCloud..." during the wait) - New OnboardingICloudCheckView: shown on fresh installs with iCloud available, lets user restore from iCloud before starting onboarding from scratch Localization: - Added de, fr, it, ja, pt-BR lproj folders - New iCloud onboarding strings in en + es-ES (+ button literals) ASO metadata (fastlane): - Updated en-US: new subtitle, keywords, description (fixed "no cloud sync" claim), promotional text, release notes - Added full metadata for es-ES, de-DE, fr-FR, it, ja, pt-BR (63 files total) - All keyword fields validated ≤100 Unicode chars Infrastructure: - Gemfile + Gemfile.lock for fastlane - Scripts/archive_and_upload_appstore.sh for CI/CD Version: 1.2.1 (build 7) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
142 lines
4.7 KiB
Swift
142 lines
4.7 KiB
Swift
import SwiftUI
|
|
|
|
/// Shown before onboarding on a fresh install when iCloud is available.
|
|
/// Lets the user choose between restoring from iCloud or starting fresh.
|
|
struct OnboardingICloudCheckView: View {
|
|
/// Called when the user decides to start fresh (no iCloud restore).
|
|
let onSkip: () -> Void
|
|
|
|
@AppStorage("cloudSyncEnabled") private var cloudSyncEnabled = false
|
|
@State private var showRestartPrompt = false
|
|
|
|
var body: some View {
|
|
if showRestartPrompt {
|
|
restartPromptView
|
|
} else {
|
|
checkView
|
|
}
|
|
}
|
|
|
|
// MARK: - Check View
|
|
|
|
private var checkView: some View {
|
|
VStack {
|
|
Spacer()
|
|
|
|
VStack(spacing: 28) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.appPrimary.opacity(0.12))
|
|
.frame(width: 140, height: 140)
|
|
Circle()
|
|
.fill(Color.appPrimary.opacity(0.22))
|
|
.frame(width: 100, height: 100)
|
|
Image(systemName: "icloud.fill")
|
|
.font(.system(size: 50))
|
|
.foregroundColor(.appPrimary)
|
|
}
|
|
|
|
VStack(spacing: 14) {
|
|
Text(String(localized: "icloud_check_title"))
|
|
.font(.title.weight(.bold))
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(String(localized: "icloud_check_description"))
|
|
.font(.body)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 40)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
Spacer()
|
|
|
|
VStack(spacing: 12) {
|
|
Button {
|
|
cloudSyncEnabled = true
|
|
showRestartPrompt = true
|
|
} label: {
|
|
Label("Restore from iCloud", systemImage: "icloud.and.arrow.down")
|
|
.font(.headline)
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.appPrimary)
|
|
.cornerRadius(AppConstants.UI.cornerRadius)
|
|
}
|
|
|
|
Button {
|
|
onSkip()
|
|
} label: {
|
|
Text("Start Fresh")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.padding(.bottom, 40)
|
|
}
|
|
.background(AppBackground())
|
|
}
|
|
|
|
// MARK: - Restart Prompt View
|
|
|
|
private var restartPromptView: some View {
|
|
VStack {
|
|
Spacer()
|
|
|
|
VStack(spacing: 28) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.positiveGreen.opacity(0.12))
|
|
.frame(width: 140, height: 140)
|
|
Circle()
|
|
.fill(Color.positiveGreen.opacity(0.22))
|
|
.frame(width: 100, height: 100)
|
|
Image(systemName: "checkmark.icloud.fill")
|
|
.font(.system(size: 50))
|
|
.foregroundColor(.positiveGreen)
|
|
}
|
|
|
|
VStack(spacing: 14) {
|
|
Text(String(localized: "icloud_enabled_title"))
|
|
.font(.title.weight(.bold))
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(String(localized: "icloud_enabled_description"))
|
|
.font(.body)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 40)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
Spacer()
|
|
|
|
// "Got it" just acknowledges — the user must close and reopen manually.
|
|
// The button stays active so it doesn't look broken.
|
|
Button {
|
|
// No-op: user needs to close and reopen the app.
|
|
// Nothing to navigate to; this session has no CloudKit container.
|
|
} label: {
|
|
Text("Got it")
|
|
.font(.headline)
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.positiveGreen)
|
|
.cornerRadius(AppConstants.UI.cornerRadius)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.padding(.bottom, 40)
|
|
}
|
|
.background(AppBackground())
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
OnboardingICloudCheckView(onSkip: {})
|
|
}
|