Release 1.2.1: iCloud sync improvements + ASO multilingual metadata
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>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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: {})
|
||||
}
|
||||
Reference in New Issue
Block a user