Add Share Extension for importing recipes via URL

Adds a Share Extension so users can share URLs from Safari (or any app)
to open the main app with the ImportURLSheet pre-filled. Uses a custom
URL scheme (nextcloud-cookbook://) as the bridge between the extension
and the main app.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 10:17:22 +01:00
parent 151e69ff28
commit ce2a814e5a
10 changed files with 547 additions and 26 deletions

View File

@@ -15,6 +15,8 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App {
@AppStorage("language") var language = Locale.current.language.languageCode?.identifier ?? "en"
@AppStorage("appearanceMode") var appearanceMode = AppearanceMode.system.rawValue
@State private var pendingImportURL: String?
var colorScheme: ColorScheme? {
switch appearanceMode {
case AppearanceMode.light.rawValue: return .light
@@ -29,7 +31,7 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App {
if onboarding {
OnboardingView()
} else {
MainView()
MainView(pendingImportURL: $pendingImportURL)
}
}
.preferredColorScheme(colorScheme)
@@ -39,6 +41,16 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App {
.init(identifier: language ==
SupportedLanguage.DEVICE.rawValue ? (Locale.current.language.languageCode?.identifier ?? "en") : language)
)
.onOpenURL { url in
guard !onboarding else { return }
guard url.scheme == "nextcloud-cookbook",
url.host == "import",
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let recipeURL = components.queryItems?.first(where: { $0.name == "url" })?.value,
!recipeURL.isEmpty
else { return }
pendingImportURL = recipeURL
}
}
}
}

View File

@@ -20,6 +20,8 @@ struct MainView: View {
@State private var selectedTab: Tab = .recipes
@Binding var pendingImportURL: String?
enum Tab {
case recipes, search, mealPlan, groceryList
}
@@ -106,5 +108,19 @@ struct MainView: View {
}
recipeViewModel.presentLoadingIndicator = false
}
.onChange(of: pendingImportURL) { _, newURL in
guard let url = newURL, !url.isEmpty else { return }
selectedTab = .recipes
recipeViewModel.pendingImportURL = url
// Dismiss any currently open import sheet before re-presenting
if recipeViewModel.showImportURLSheet {
recipeViewModel.showImportURLSheet = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
recipeViewModel.showImportURLSheet = true
}
} else {
recipeViewModel.showImportURLSheet = true
}
}
}
}

View File

@@ -10,6 +10,7 @@ struct ImportURLSheet: View {
@Environment(\.dismiss) private var dismiss
var onImport: (RecipeDetail) -> Void
var initialURL: String = ""
@State private var url: String = ""
@State private var isLoading: Bool = false
@@ -62,6 +63,11 @@ struct ImportURLSheet: View {
} message: {
Text(alertMessage)
}
.onAppear {
if !initialURL.isEmpty {
url = initialURL
}
}
}
}

View File

@@ -255,10 +255,15 @@ struct RecipeTabView: View {
}
}
}
.sheet(isPresented: $viewModel.showImportURLSheet) {
ImportURLSheet { recipeDetail in
viewModel.navigateToImportedRecipe(recipeDetail: recipeDetail)
}
.sheet(isPresented: $viewModel.showImportURLSheet, onDismiss: {
viewModel.pendingImportURL = nil
}) {
ImportURLSheet(
onImport: { recipeDetail in
viewModel.navigateToImportedRecipe(recipeDetail: recipeDetail)
},
initialURL: viewModel.pendingImportURL ?? ""
)
.environmentObject(appState)
}
.sheet(isPresented: $showManualReorderSheet) {
@@ -303,6 +308,7 @@ struct RecipeTabView: View {
@Published var showImportURLSheet: Bool = false
@Published var importedRecipeDetail: RecipeDetail? = nil
@Published var pendingImportURL: String? = nil
func navigateToSettings() {
sidebarPath.append(SidebarDestination.settings)