From ce2a814e5acc97061a6feb73c5d01af592f7893f Mon Sep 17 00:00:00 2001 From: Hendrik Hogertz Date: Sun, 15 Feb 2026 10:17:22 +0100 Subject: [PATCH] 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 --- .../project.pbxproj | 213 ++++++++++++++++-- .../Nextcloud Cookbook iOS Client.xcscheme | 102 +++++++++ .../xcschemes/ShareExtension.xcscheme | 97 ++++++++ .../Nextcloud_Cookbook_iOS_ClientApp.swift | 14 +- .../Views/MainView.swift | 16 ++ .../Views/Recipes/ImportURLSheet.swift | 6 + .../Views/Tabs/RecipeTabView.swift | 14 +- Nextcloud-Cookbook-iOS-Client-Info.plist | 11 + ShareExtension/Info.plist | 21 ++ ShareExtension/ShareViewController.swift | 79 +++++++ 10 files changed, 547 insertions(+), 26 deletions(-) create mode 100644 Nextcloud Cookbook iOS Client.xcodeproj/xcshareddata/xcschemes/Nextcloud Cookbook iOS Client.xcscheme create mode 100644 Nextcloud Cookbook iOS Client.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme create mode 100644 ShareExtension/Info.plist create mode 100644 ShareExtension/ShareViewController.swift diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index c35e636..2fd33ac 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -3,10 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ + A1B2C3D52F0A000100000001 /* AppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000001 /* AppearanceMode.swift */; }; A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */; }; A70171842AA8E71900064C43 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171832AA8E71900064C43 /* MainView.swift */; }; A70171862AA8E71F00064C43 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A70171852AA8E71F00064C43 /* Assets.xcassets */; }; @@ -20,10 +21,6 @@ A70171BE2AB4987900064C43 /* RecipeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BD2AB4987900064C43 /* RecipeListView.swift */; }; A70171C02AB498A900064C43 /* RecipeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BF2AB498A900064C43 /* RecipeView.swift */; }; A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C12AB498C600064C43 /* RecipeCardView.swift */; }; - B1C0DE022CF0000100000001 /* CategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE012CF0000100000001 /* CategoryCardView.swift */; }; - B1C0DE042CF0000200000002 /* RecentRecipesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */; }; - B1C0DE062CF0000300000003 /* AllRecipesCategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE052CF0000300000003 /* AllRecipesCategoryCardView.swift */; }; - B1C0DE082CF0000400000004 /* AllRecipesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE072CF0000400000004 /* AllRecipesListView.swift */; }; A70171C42AB4A31200064C43 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C32AB4A31200064C43 /* DataStore.swift */; }; A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C52AB4C43A00064C43 /* DataModels.swift */; }; A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserSettings.swift */; }; @@ -41,7 +38,6 @@ A79AA8ED2B063AD5007D25F2 /* NextcloudApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79AA8EC2B063AD5007D25F2 /* NextcloudApi.swift */; }; A7AEAE642AD5521400135378 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A7AEAE632AD5521400135378 /* Localizable.xcstrings */; }; A7CD3FD22B2C546A00D764AD /* CollapsibleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CD3FD12B2C546A00D764AD /* CollapsibleView.swift */; }; - DFCB4E9FD4E0884AF217E5C5 /* LiquidGlassModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6ECAD063AEE501543FC76 /* LiquidGlassModifiers.swift */; }; A7F3F8E82ACBFC760076C227 /* RecipeKeywordSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E72ACBFC760076C227 /* RecipeKeywordSection.swift */; }; A7FB0D7A2B25C66600A3469E /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D792B25C66600A3469E /* OnboardingView.swift */; }; A7FB0D7C2B25C68500A3469E /* TokenLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D7B2B25C68500A3469E /* TokenLoginView.swift */; }; @@ -65,16 +61,20 @@ A9CA6CEF2B4C086100F78AB5 /* RecipeExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */; }; A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */ = {isa = PBXBuildFile; productRef = A9CA6CF52B4C63F200F78AB5 /* TPPDF */; }; A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D89AAF2B4FE97800F49D92 /* TimerView.swift */; }; - A9D8F9052B99F3E5009BACAE /* RecipeImportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */; }; - C1F0AB022D0B000100000001 /* ImportURLSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F0AB012D0B000100000001 /* ImportURLSheet.swift */; }; A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A2A2BE7799F00206866 /* JsonAny.swift */; }; A9FA2AB62B5079B200A43702 /* alarm_sound_0.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */; }; - A1B2C3D52F0A000100000001 /* AppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000001 /* AppearanceMode.swift */; }; + B1C0DE022CF0000100000001 /* CategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE012CF0000100000001 /* CategoryCardView.swift */; }; + B1C0DE042CF0000200000002 /* RecentRecipesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */; }; + B1C0DE062CF0000300000003 /* AllRecipesCategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE052CF0000300000003 /* AllRecipesCategoryCardView.swift */; }; + B1C0DE082CF0000400000004 /* AllRecipesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE072CF0000400000004 /* AllRecipesListView.swift */; }; + C1F0AB022D0B000100000001 /* ImportURLSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F0AB012D0B000100000001 /* ImportURLSheet.swift */; }; D1A0CE012D0A000100000001 /* GroceryListMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0CE002D0A000100000001 /* GroceryListMode.swift */; }; D1A0CE032D0A000200000002 /* RemindersGroceryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0CE022D0A000200000002 /* RemindersGroceryStore.swift */; }; D1A0CE052D0A000300000003 /* GroceryListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0CE042D0A000300000003 /* GroceryListManager.swift */; }; + DFCB4E9FD4E0884AF217E5C5 /* LiquidGlassModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6ECAD063AEE501543FC76 /* LiquidGlassModifiers.swift */; }; E1B0CF072D0B000400000004 /* GroceryStateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B0CF062D0B000400000004 /* GroceryStateModels.swift */; }; E1B0CF092D0B000500000005 /* GroceryStateSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B0CF082D0B000500000005 /* GroceryStateSyncManager.swift */; }; + E498A7A42F41C35500D7D7A4 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E498A79A2F41C35500D7D7A4 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F1A0DE022E0C000100000001 /* MealPlanModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A0DE012E0C000100000001 /* MealPlanModels.swift */; }; F1A0DE042E0C000200000002 /* MealPlanManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A0DE032E0C000200000002 /* MealPlanManager.swift */; }; F1A0DE062E0C000300000003 /* MealPlanSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A0DE052E0C000300000003 /* MealPlanSyncManager.swift */; }; @@ -99,9 +99,32 @@ remoteGlobalIDString = A701717D2AA8E71900064C43; remoteInfo = "Nextcloud Cookbook iOS Client"; }; + E498A7A22F41C35500D7D7A4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A70171762AA8E71900064C43 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E498A7992F41C35500D7D7A4; + remoteInfo = ShareExtension; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + E498A7A52F41C35500D7D7A4 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + E498A7A42F41C35500D7D7A4 /* ShareExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ + 04B6ECAD063AEE501543FC76 /* LiquidGlassModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidGlassModifiers.swift; sourceTree = ""; }; + A1B2C3D42F0A000100000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = ""; }; A701717E2AA8E71900064C43 /* Nextcloud Cookbook iOS Client.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Nextcloud Cookbook iOS Client.app"; sourceTree = BUILT_PRODUCTS_DIR; }; A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientApp.swift; sourceTree = ""; }; A70171832AA8E71900064C43 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; @@ -119,10 +142,6 @@ A70171BD2AB4987900064C43 /* RecipeListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeListView.swift; sourceTree = ""; }; A70171BF2AB498A900064C43 /* RecipeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeView.swift; sourceTree = ""; }; A70171C12AB498C600064C43 /* RecipeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCardView.swift; sourceTree = ""; }; - B1C0DE012CF0000100000001 /* CategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryCardView.swift; sourceTree = ""; }; - B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentRecipesSection.swift; sourceTree = ""; }; - B1C0DE052CF0000300000003 /* AllRecipesCategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllRecipesCategoryCardView.swift; sourceTree = ""; }; - B1C0DE072CF0000400000004 /* AllRecipesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllRecipesListView.swift; sourceTree = ""; }; A70171C32AB4A31200064C43 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; A70171C52AB4C43A00064C43 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = ""; }; A70171CA2AB4CD1700064C43 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; @@ -140,7 +159,6 @@ A79AA8EC2B063AD5007D25F2 /* NextcloudApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudApi.swift; sourceTree = ""; }; A7AEAE632AD5521400135378 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; A7CD3FD12B2C546A00D764AD /* CollapsibleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleView.swift; sourceTree = ""; }; - 04B6ECAD063AEE501543FC76 /* LiquidGlassModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidGlassModifiers.swift; sourceTree = ""; }; A7F3F8E72ACBFC760076C227 /* RecipeKeywordSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeKeywordSection.swift; sourceTree = ""; }; A7FB0D792B25C66600A3469E /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; A7FB0D7B2B25C68500A3469E /* TokenLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenLoginView.swift; sourceTree = ""; }; @@ -164,16 +182,20 @@ A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeExporter.swift; sourceTree = ""; }; A9D89AAF2B4FE97800F49D92 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = ""; }; A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeImportSection.swift; sourceTree = ""; }; - C1F0AB012D0B000100000001 /* ImportURLSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportURLSheet.swift; sourceTree = ""; }; A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Nextcloud-Cookbook-iOS-Client-Info.plist"; sourceTree = SOURCE_ROOT; }; A9E78A2A2BE7799F00206866 /* JsonAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonAny.swift; sourceTree = ""; }; A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm_sound_0.mp3; sourceTree = ""; }; - A1B2C3D42F0A000100000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = ""; }; + B1C0DE012CF0000100000001 /* CategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryCardView.swift; sourceTree = ""; }; + B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentRecipesSection.swift; sourceTree = ""; }; + B1C0DE052CF0000300000003 /* AllRecipesCategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllRecipesCategoryCardView.swift; sourceTree = ""; }; + B1C0DE072CF0000400000004 /* AllRecipesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllRecipesListView.swift; sourceTree = ""; }; + C1F0AB012D0B000100000001 /* ImportURLSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportURLSheet.swift; sourceTree = ""; }; D1A0CE002D0A000100000001 /* GroceryListMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryListMode.swift; sourceTree = ""; }; D1A0CE022D0A000200000002 /* RemindersGroceryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemindersGroceryStore.swift; sourceTree = ""; }; D1A0CE042D0A000300000003 /* GroceryListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryListManager.swift; sourceTree = ""; }; E1B0CF062D0B000400000004 /* GroceryStateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryStateModels.swift; sourceTree = ""; }; E1B0CF082D0B000500000005 /* GroceryStateSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryStateSyncManager.swift; sourceTree = ""; }; + E498A79A2F41C35500D7D7A4 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F1A0DE012E0C000100000001 /* MealPlanModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPlanModels.swift; sourceTree = ""; }; F1A0DE032E0C000200000002 /* MealPlanManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPlanManager.swift; sourceTree = ""; }; F1A0DE052E0C000300000003 /* MealPlanSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPlanSyncManager.swift; sourceTree = ""; }; @@ -183,6 +205,20 @@ G1A0CE022F0B000200000002 /* CategoryReorderSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryReorderSheet.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + E498A7A82F41C35500D7D7A4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = E498A7992F41C35500D7D7A4 /* ShareExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + E498A79B2F41C35500D7D7A4 /* ShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (E498A7A82F41C35500D7D7A4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = ShareExtension; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ A701717B2AA8E71900064C43 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -206,6 +242,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E498A7972F41C35500D7D7A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -217,6 +260,7 @@ A70171802AA8E71900064C43 /* Nextcloud Cookbook iOS Client */, A70171922AA8E72000064C43 /* Nextcloud Cookbook iOS ClientTests */, A701719C2AA8E72000064C43 /* Nextcloud Cookbook iOS ClientUITests */, + E498A79B2F41C35500D7D7A4 /* ShareExtension */, A701717F2AA8E71900064C43 /* Products */, ); sourceTree = ""; @@ -227,6 +271,7 @@ A701717E2AA8E71900064C43 /* Nextcloud Cookbook iOS Client.app */, A701718F2AA8E72000064C43 /* Nextcloud Cookbook iOS ClientTests.xctest */, A70171992AA8E72000064C43 /* Nextcloud Cookbook iOS ClientUITests.xctest */, + E498A79A2F41C35500D7D7A4 /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -480,10 +525,12 @@ A701717A2AA8E71900064C43 /* Sources */, A701717B2AA8E71900064C43 /* Frameworks */, A701717C2AA8E71900064C43 /* Resources */, + E498A7A52F41C35500D7D7A4 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + E498A7A32F41C35500D7D7A4 /* PBXTargetDependency */, ); name = "Nextcloud Cookbook iOS Client"; packageProductDependencies = ( @@ -529,6 +576,28 @@ productReference = A70171992AA8E72000064C43 /* Nextcloud Cookbook iOS ClientUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + E498A7992F41C35500D7D7A4 /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = E498A7A92F41C35500D7D7A4 /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + E498A7962F41C35500D7D7A4 /* Sources */, + E498A7972F41C35500D7D7A4 /* Frameworks */, + E498A7982F41C35500D7D7A4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + E498A79B2F41C35500D7D7A4 /* ShareExtension */, + ); + name = ShareExtension; + packageProductDependencies = ( + ); + productName = ShareExtension; + productReference = E498A79A2F41C35500D7D7A4 /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -536,7 +605,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1430; + LastSwiftUpdateCheck = 2620; LastUpgradeCheck = 1500; TargetAttributes = { A701717D2AA8E71900064C43 = { @@ -550,6 +619,10 @@ CreatedOnToolsVersion = 14.3; TestTargetID = A701717D2AA8E71900064C43; }; + E498A7992F41C35500D7D7A4 = { + CreatedOnToolsVersion = 26.2; + LastSwiftMigration = 2620; + }; }; }; buildConfigurationList = A70171792AA8E71900064C43 /* Build configuration list for PBXProject "Nextcloud Cookbook iOS Client" */; @@ -574,6 +647,7 @@ A701717D2AA8E71900064C43 /* Nextcloud Cookbook iOS Client */, A701718E2AA8E72000064C43 /* Nextcloud Cookbook iOS ClientTests */, A70171982AA8E72000064C43 /* Nextcloud Cookbook iOS ClientUITests */, + E498A7992F41C35500D7D7A4 /* ShareExtension */, ); }; /* End PBXProject section */ @@ -604,6 +678,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E498A7982F41C35500D7D7A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -698,6 +779,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E498A7962F41C35500D7D7A4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -711,6 +799,11 @@ target = A701717D2AA8E71900064C43 /* Nextcloud Cookbook iOS Client */; targetProxy = A701719A2AA8E72000064C43 /* PBXContainerItemProxy */; }; + E498A7A32F41C35500D7D7A4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E498A7992F41C35500D7D7A4 /* ShareExtension */; + targetProxy = E498A7A22F41C35500D7D7A4 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -920,7 +1013,6 @@ A70171A72AA8E72000064C43 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -944,7 +1036,6 @@ A70171A82AA8E72000064C43 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -968,7 +1059,6 @@ A70171AA2AA8E72000064C43 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; @@ -991,7 +1081,6 @@ A70171AB2AA8E72000064C43 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; @@ -1011,6 +1100,79 @@ }; name = Release; }; + E498A7A62F41C35500D7D7A4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = EF2ABA36D9; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client.ShareExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E498A7A72F41C35500D7D7A4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = EF2ABA36D9; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client.ShareExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1050,6 +1212,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + E498A7A92F41C35500D7D7A4 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E498A7A62F41C35500D7D7A4 /* Debug */, + E498A7A72F41C35500D7D7A4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/xcshareddata/xcschemes/Nextcloud Cookbook iOS Client.xcscheme b/Nextcloud Cookbook iOS Client.xcodeproj/xcshareddata/xcschemes/Nextcloud Cookbook iOS Client.xcscheme new file mode 100644 index 0000000..16f50b0 --- /dev/null +++ b/Nextcloud Cookbook iOS Client.xcodeproj/xcshareddata/xcschemes/Nextcloud Cookbook iOS Client.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme b/Nextcloud Cookbook iOS Client.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme new file mode 100644 index 0000000..91132a6 --- /dev/null +++ b/Nextcloud Cookbook iOS Client.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift index dcf6168..03a3973 100644 --- a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift +++ b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift @@ -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 + } } } } diff --git a/Nextcloud Cookbook iOS Client/Views/MainView.swift b/Nextcloud Cookbook iOS Client/Views/MainView.swift index 3128546..01729d9 100644 --- a/Nextcloud Cookbook iOS Client/Views/MainView.swift +++ b/Nextcloud Cookbook iOS Client/Views/MainView.swift @@ -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 + } + } } } diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/ImportURLSheet.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/ImportURLSheet.swift index dc3e620..2fc18ba 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/ImportURLSheet.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/ImportURLSheet.swift @@ -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 + } + } } } diff --git a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift index cbded96..6fb04ce 100644 --- a/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift +++ b/Nextcloud Cookbook iOS Client/Views/Tabs/RecipeTabView.swift @@ -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) diff --git a/Nextcloud-Cookbook-iOS-Client-Info.plist b/Nextcloud-Cookbook-iOS-Client-Info.plist index f73bb80..5f291fa 100644 --- a/Nextcloud-Cookbook-iOS-Client-Info.plist +++ b/Nextcloud-Cookbook-iOS-Client-Info.plist @@ -2,6 +2,17 @@ + CFBundleURLTypes + + + CFBundleURLName + com.vincentmeilinger.nextcloud-cookbook + CFBundleURLSchemes + + nextcloud-cookbook + + + NSRemindersFullAccessUsageDescription This app uses Reminders to save your grocery list items. diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist new file mode 100644 index 0000000..ea3b19a --- /dev/null +++ b/ShareExtension/Info.plist @@ -0,0 +1,21 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareViewController + + + diff --git a/ShareExtension/ShareViewController.swift b/ShareExtension/ShareViewController.swift new file mode 100644 index 0000000..3f59633 --- /dev/null +++ b/ShareExtension/ShareViewController.swift @@ -0,0 +1,79 @@ +// +// ShareViewController.swift +// ShareExtension +// +// Created by Hendrik Hogertz on 15.02.26. +// + +import UIKit +import UniformTypeIdentifiers + +class ShareViewController: UIViewController { + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + handleSharedItems() + } + + private func handleSharedItems() { + guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else { + completeRequest() + return + } + + for item in extensionItems { + guard let attachments = item.attachments else { continue } + for provider in attachments { + if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { + provider.loadItem(forTypeIdentifier: UTType.url.identifier) { [weak self] item, _ in + if let url = item as? URL { + self?.openMainApp(with: url.absoluteString) + } else { + self?.completeRequest() + } + } + return + } else if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { + provider.loadItem(forTypeIdentifier: UTType.plainText.identifier) { [weak self] item, _ in + if let text = item as? String, let url = URL(string: text), url.scheme?.hasPrefix("http") == true { + self?.openMainApp(with: url.absoluteString) + } else { + self?.completeRequest() + } + } + return + } + } + } + + completeRequest() + } + + private func openMainApp(with urlString: String) { + guard let encoded = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let appURL = URL(string: "nextcloud-cookbook://import?url=\(encoded)") + else { + completeRequest() + return + } + + // Use the responder chain to open the URL + var responder: UIResponder? = self + while let r = responder { + if let application = r as? UIApplication { + application.open(appURL, options: [:], completionHandler: nil) + break + } + responder = r.next + } + + // Give the system a moment to process the URL before dismissing + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + self?.completeRequest() + } + } + + private func completeRequest() { + extensionContext?.completeRequest(returningItems: nil) + } +}