Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 430ae744cc | |||
| 98d565ba0d |
+90
@@ -0,0 +1,90 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# Accio dependency management
|
||||
Dependencies/
|
||||
.accio/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
||||
# Code Injection
|
||||
#
|
||||
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||
# https://github.com/johnno1962/injectionforxcode
|
||||
|
||||
iOSInjectionProject/
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
This example app is part of our blog article [How to Use the Coordinator Pattern in SwiftUI](https://quickbirdstudios.com/blog/coordinator-pattern-in-swiftui/). While the article introduces the different techniques and components of our approach to the Coordinator Pattern in SwiftUI on a general level, the Recipes App acts as a demonstration and can be used as a starting point to experimenting with it.
|
||||
|
||||
In a follow-up article [Navigation and Deep-Links in SwiftUI](https://quickbirdstudios.com/blog/swiftui-navigation-deep-links/), we have further adapted the example app to use the [XUI library](https://github.com/quickbirdstudios/XUI). These adaptions can be found on the [xui branch](https://github.com/quickbirdstudios/SwiftUI-Coordinators-Example/tree/xui).
|
||||
## Recipes App
|
||||
|
||||
The Recipes App lists different recipes with instructions on how to prepare it and ratings from previous users having tried it. In its current form, the app does not provide this functionality, but rather displays mock data.
|
||||
|
||||
@@ -3,28 +3,34 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objectVersion = 52;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
9B024EA225A6E76E000BE823 /* DefaultHomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B024EA125A6E76E000BE823 /* DefaultHomeCoordinator.swift */; };
|
||||
9B024EA325A6E76E000BE823 /* DefaultHomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B024EA125A6E76E000BE823 /* DefaultHomeCoordinator.swift */; };
|
||||
9B024EA925A6E78A000BE823 /* DefaultRecipeListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B024EA825A6E78A000BE823 /* DefaultRecipeListCoordinator.swift */; };
|
||||
9B024EAA25A6E78A000BE823 /* DefaultRecipeListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B024EA825A6E78A000BE823 /* DefaultRecipeListCoordinator.swift */; };
|
||||
9B0BC35B25A34D9600C018D3 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0BC35A25A34D9600C018D3 /* SafariView.swift */; };
|
||||
9B0BC36525A394E100C018D3 /* AsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4FCF9225A2CFC5006BE60E /* AsyncImage.swift */; };
|
||||
9B0BC36A25A394E400C018D3 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0BC35A25A34D9600C018D3 /* SafariView.swift */; };
|
||||
9B0BC36F25A394E600C018D3 /* RatingStars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8D00A9259FE1BE00684D22 /* RatingStars.swift */; };
|
||||
9B0BC37425A3952700C018D3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8D0097259FDFF100684D22 /* SettingsView.swift */; };
|
||||
9B4FCF9425A2CFC5006BE60E /* AsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B4FCF9225A2CFC5006BE60E /* AsyncImage.swift */; };
|
||||
9B692CAF25A6E7BB008D7FB9 /* DefaultRecipeListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B692CAE25A6E7BB008D7FB9 /* DefaultRecipeListViewModel.swift */; };
|
||||
9B692CB025A6E7BB008D7FB9 /* DefaultRecipeListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B692CAE25A6E7BB008D7FB9 /* DefaultRecipeListViewModel.swift */; };
|
||||
9B692CB625A6E7CF008D7FB9 /* DefaultRecipeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B692CB525A6E7CF008D7FB9 /* DefaultRecipeViewModel.swift */; };
|
||||
9B692CB725A6E7CF008D7FB9 /* DefaultRecipeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B692CB525A6E7CF008D7FB9 /* DefaultRecipeViewModel.swift */; };
|
||||
9B692CBD25A6E7E5008D7FB9 /* DefaultRatingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B692CBC25A6E7E5008D7FB9 /* DefaultRatingViewModel.swift */; };
|
||||
9B692CBE25A6E7E5008D7FB9 /* DefaultRatingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B692CBC25A6E7E5008D7FB9 /* DefaultRatingViewModel.swift */; };
|
||||
9B77A4DE25ED208400EBDCD6 /* XUI in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77A4DD25ED208400EBDCD6 /* XUI */; };
|
||||
9B77A4E425ED208E00EBDCD6 /* XUI in Frameworks */ = {isa = PBXBuildFile; productRef = 9B77A4E325ED208E00EBDCD6 /* XUI */; };
|
||||
9B8D0098259FDFF100684D22 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8D0097259FDFF100684D22 /* SettingsView.swift */; };
|
||||
9B8D00AA259FE1BE00684D22 /* RatingStars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B8D00A9259FE1BE00684D22 /* RatingStars.swift */; };
|
||||
9BB59DC925A4BDB600946BFB /* RecipeListCoordinatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59DC825A4BDB600946BFB /* RecipeListCoordinatorView.swift */; };
|
||||
9BB59DCA25A4BDB600946BFB /* RecipeListCoordinatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59DC825A4BDB600946BFB /* RecipeListCoordinatorView.swift */; };
|
||||
9BB59DD425A4BDF100946BFB /* RecipeListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59DD325A4BDF100946BFB /* RecipeListCoordinator.swift */; };
|
||||
9BB59DD525A4BDF100946BFB /* RecipeListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59DD325A4BDF100946BFB /* RecipeListCoordinator.swift */; };
|
||||
9BB59DF825A4C19500946BFB /* SheetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59DF725A4C19500946BFB /* SheetModifier.swift */; };
|
||||
9BB59DF925A4C19500946BFB /* SheetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59DF725A4C19500946BFB /* SheetModifier.swift */; };
|
||||
9BB59DFF25A4C1A000946BFB /* PopoverModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59DFE25A4C1A000946BFB /* PopoverModifier.swift */; };
|
||||
9BB59E0025A4C1A000946BFB /* PopoverModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59DFE25A4C1A000946BFB /* PopoverModifier.swift */; };
|
||||
9BB59E0625A4C32200946BFB /* View+Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59E0525A4C32200946BFB /* View+Navigation.swift */; };
|
||||
9BB59E0725A4C32200946BFB /* View+Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59E0525A4C32200946BFB /* View+Navigation.swift */; };
|
||||
9BB59E1E25A4C5BF00946BFB /* URL+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59E1D25A4C5BF00946BFB /* URL+Identifiable.swift */; };
|
||||
9BB59E1F25A4C5BF00946BFB /* URL+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB59E1D25A4C5BF00946BFB /* URL+Identifiable.swift */; };
|
||||
9BE362A62583CD1F00807BFC /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE362A52583CD1F00807BFC /* Tests_iOS.swift */; };
|
||||
@@ -73,15 +79,17 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
9B024EA125A6E76E000BE823 /* DefaultHomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultHomeCoordinator.swift; sourceTree = "<group>"; };
|
||||
9B024EA825A6E78A000BE823 /* DefaultRecipeListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultRecipeListCoordinator.swift; sourceTree = "<group>"; };
|
||||
9B0BC35A25A34D9600C018D3 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
|
||||
9B4FCF9225A2CFC5006BE60E /* AsyncImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncImage.swift; sourceTree = "<group>"; };
|
||||
9B692CAE25A6E7BB008D7FB9 /* DefaultRecipeListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultRecipeListViewModel.swift; sourceTree = "<group>"; };
|
||||
9B692CB525A6E7CF008D7FB9 /* DefaultRecipeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultRecipeViewModel.swift; sourceTree = "<group>"; };
|
||||
9B692CBC25A6E7E5008D7FB9 /* DefaultRatingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultRatingViewModel.swift; sourceTree = "<group>"; };
|
||||
9B8D0097259FDFF100684D22 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
9B8D00A9259FE1BE00684D22 /* RatingStars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingStars.swift; sourceTree = "<group>"; };
|
||||
9BB59DC825A4BDB600946BFB /* RecipeListCoordinatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeListCoordinatorView.swift; sourceTree = "<group>"; };
|
||||
9BB59DD325A4BDF100946BFB /* RecipeListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeListCoordinator.swift; sourceTree = "<group>"; };
|
||||
9BB59DF725A4C19500946BFB /* SheetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetModifier.swift; sourceTree = "<group>"; };
|
||||
9BB59DFE25A4C1A000946BFB /* PopoverModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverModifier.swift; sourceTree = "<group>"; };
|
||||
9BB59E0525A4C32200946BFB /* View+Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Navigation.swift"; sourceTree = "<group>"; };
|
||||
9BB59E1D25A4C5BF00946BFB /* URL+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Identifiable.swift"; sourceTree = "<group>"; };
|
||||
9BE3628A2583CD1700807BFC /* RecipesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipesApp.swift; sourceTree = "<group>"; };
|
||||
9BE3628C2583CD1E00807BFC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@@ -113,6 +121,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B77A4DE25ED208400EBDCD6 /* XUI in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -120,6 +129,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9B77A4E425ED208E00EBDCD6 /* XUI in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -144,11 +154,19 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9BE3633A25841BA200807BFC /* RecipeListViewModel.swift */,
|
||||
9B692CAE25A6E7BB008D7FB9 /* DefaultRecipeListViewModel.swift */,
|
||||
9BE3630A2583CFCF00807BFC /* RecipeList.swift */,
|
||||
);
|
||||
path = RecipeList;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9B6D639C25A6B6EE006A458E /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9B8D0096259FDFE200684D22 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -171,21 +189,12 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9BB59DD325A4BDF100946BFB /* RecipeListCoordinator.swift */,
|
||||
9B024EA825A6E78A000BE823 /* DefaultRecipeListCoordinator.swift */,
|
||||
9BB59DC825A4BDB600946BFB /* RecipeListCoordinatorView.swift */,
|
||||
);
|
||||
path = RecipeListCoordinator;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9BB59E0C25A4C44100946BFB /* ViewModifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9BB59DF725A4C19500946BFB /* SheetModifier.swift */,
|
||||
9BB59DFE25A4C1A000946BFB /* PopoverModifier.swift */,
|
||||
9BB59E0525A4C32200946BFB /* View+Navigation.swift */,
|
||||
);
|
||||
path = ViewModifiers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9BB59E1C25A4C5AE00946BFB /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -203,6 +212,7 @@
|
||||
9BE362A42583CD1F00807BFC /* Tests iOS */,
|
||||
9BE362AF2583CD1F00807BFC /* Tests macOS */,
|
||||
9BE362922583CD1E00807BFC /* Products */,
|
||||
9B6D639C25A6B6EE006A458E /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -212,7 +222,6 @@
|
||||
9BE3628A2583CD1700807BFC /* RecipesApp.swift */,
|
||||
9BE362F92583CF4100807BFC /* Scenes */,
|
||||
9B8D00A8259FE1AF00684D22 /* Views */,
|
||||
9BB59E0C25A4C44100946BFB /* ViewModifiers */,
|
||||
9BB59E1C25A4C5AE00946BFB /* Extensions */,
|
||||
9BE362FE2583CF4B00807BFC /* Model */,
|
||||
9BE3628C2583CD1E00807BFC /* Assets.xcassets */,
|
||||
@@ -270,6 +279,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9BE362D22583CDB300807BFC /* HomeCoordinator.swift */,
|
||||
9B024EA125A6E76E000BE823 /* DefaultHomeCoordinator.swift */,
|
||||
9BE363032583CFC400807BFC /* HomeCoordinatorView.swift */,
|
||||
);
|
||||
path = Home;
|
||||
@@ -279,6 +289,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9BE362D92583CE0200807BFC /* RecipeViewModel.swift */,
|
||||
9B692CB525A6E7CF008D7FB9 /* DefaultRecipeViewModel.swift */,
|
||||
9BE363202583D19600807BFC /* RecipeView.swift */,
|
||||
);
|
||||
path = Recipe;
|
||||
@@ -288,6 +299,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9BE362E02583CE2C00807BFC /* RatingViewModel.swift */,
|
||||
9B692CBC25A6E7E5008D7FB9 /* DefaultRatingViewModel.swift */,
|
||||
9BE363272583D1A200807BFC /* RatingView.swift */,
|
||||
);
|
||||
path = Rating;
|
||||
@@ -331,6 +343,9 @@
|
||||
dependencies = (
|
||||
);
|
||||
name = "Recipes (iOS)";
|
||||
packageProductDependencies = (
|
||||
9B77A4DD25ED208400EBDCD6 /* XUI */,
|
||||
);
|
||||
productName = "Recipes (iOS)";
|
||||
productReference = 9BE362912583CD1E00807BFC /* Recipes.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@@ -348,6 +363,9 @@
|
||||
dependencies = (
|
||||
);
|
||||
name = "Recipes (macOS)";
|
||||
packageProductDependencies = (
|
||||
9B77A4E325ED208E00EBDCD6 /* XUI */,
|
||||
);
|
||||
productName = "Recipes (macOS)";
|
||||
productReference = 9BE362992583CD1E00807BFC /* Recipes.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@@ -395,7 +413,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1220;
|
||||
LastUpgradeCheck = 1220;
|
||||
LastUpgradeCheck = 1240;
|
||||
TargetAttributes = {
|
||||
9BE362902583CD1E00807BFC = {
|
||||
CreatedOnToolsVersion = 12.2;
|
||||
@@ -422,6 +440,9 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 9BE362842583CD1600807BFC;
|
||||
packageReferences = (
|
||||
9B77A4DC25ED208400EBDCD6 /* XCRemoteSwiftPackageReference "XUI" */,
|
||||
);
|
||||
productRefGroup = 9BE362922583CD1E00807BFC /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@@ -479,19 +500,21 @@
|
||||
9BE362CC2583CD3000807BFC /* Recipe.swift in Sources */,
|
||||
9BE362E82583CE9400807BFC /* RecipeService.swift in Sources */,
|
||||
9B8D00AA259FE1BE00684D22 /* RatingStars.swift in Sources */,
|
||||
9BB59DFF25A4C1A000946BFB /* PopoverModifier.swift in Sources */,
|
||||
9B024EA225A6E76E000BE823 /* DefaultHomeCoordinator.swift in Sources */,
|
||||
9BE363282583D1A200807BFC /* RatingView.swift in Sources */,
|
||||
9BB59E0625A4C32200946BFB /* View+Navigation.swift in Sources */,
|
||||
9BE362B32583CD1F00807BFC /* RecipesApp.swift in Sources */,
|
||||
9BE362E12583CE2C00807BFC /* RatingViewModel.swift in Sources */,
|
||||
9BB59DC925A4BDB600946BFB /* RecipeListCoordinatorView.swift in Sources */,
|
||||
9B692CB625A6E7CF008D7FB9 /* DefaultRecipeViewModel.swift in Sources */,
|
||||
9BE362DA2583CE0200807BFC /* RecipeViewModel.swift in Sources */,
|
||||
9BB59DD425A4BDF100946BFB /* RecipeListCoordinator.swift in Sources */,
|
||||
9BE363042583CFC400807BFC /* HomeCoordinatorView.swift in Sources */,
|
||||
9B0BC35B25A34D9600C018D3 /* SafariView.swift in Sources */,
|
||||
9BB59DF825A4C19500946BFB /* SheetModifier.swift in Sources */,
|
||||
9B8D0098259FDFF100684D22 /* SettingsView.swift in Sources */,
|
||||
9B692CBD25A6E7E5008D7FB9 /* DefaultRatingViewModel.swift in Sources */,
|
||||
9BE362D32583CDB300807BFC /* HomeCoordinator.swift in Sources */,
|
||||
9B692CAF25A6E7BB008D7FB9 /* DefaultRecipeListViewModel.swift in Sources */,
|
||||
9B024EA925A6E78A000BE823 /* DefaultRecipeListCoordinator.swift in Sources */,
|
||||
9BE3630B2583CFCF00807BFC /* RecipeList.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -507,19 +530,21 @@
|
||||
9BE362E92583CE9400807BFC /* RecipeService.swift in Sources */,
|
||||
9BE363292583D1A200807BFC /* RatingView.swift in Sources */,
|
||||
9BE362B42583CD1F00807BFC /* RecipesApp.swift in Sources */,
|
||||
9BB59E0025A4C1A000946BFB /* PopoverModifier.swift in Sources */,
|
||||
9B024EA325A6E76E000BE823 /* DefaultHomeCoordinator.swift in Sources */,
|
||||
9B0BC36A25A394E400C018D3 /* SafariView.swift in Sources */,
|
||||
9BB59E0725A4C32200946BFB /* View+Navigation.swift in Sources */,
|
||||
9BE362E22583CE2C00807BFC /* RatingViewModel.swift in Sources */,
|
||||
9B0BC37425A3952700C018D3 /* SettingsView.swift in Sources */,
|
||||
9BB59DCA25A4BDB600946BFB /* RecipeListCoordinatorView.swift in Sources */,
|
||||
9B692CB725A6E7CF008D7FB9 /* DefaultRecipeViewModel.swift in Sources */,
|
||||
9BE362DB2583CE0200807BFC /* RecipeViewModel.swift in Sources */,
|
||||
9BB59DD525A4BDF100946BFB /* RecipeListCoordinator.swift in Sources */,
|
||||
9BE363052583CFC400807BFC /* HomeCoordinatorView.swift in Sources */,
|
||||
9BE362D42583CDB300807BFC /* HomeCoordinator.swift in Sources */,
|
||||
9BB59DF925A4C19500946BFB /* SheetModifier.swift in Sources */,
|
||||
9B0BC36525A394E100C018D3 /* AsyncImage.swift in Sources */,
|
||||
9B692CBE25A6E7E5008D7FB9 /* DefaultRatingViewModel.swift in Sources */,
|
||||
9B0BC36F25A394E600C018D3 /* RatingStars.swift in Sources */,
|
||||
9B692CB025A6E7BB008D7FB9 /* DefaultRecipeListViewModel.swift in Sources */,
|
||||
9B024EAA25A6E78A000BE823 /* DefaultRecipeListCoordinator.swift in Sources */,
|
||||
9BE3630C2583CFCF00807BFC /* RecipeList.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -718,6 +743,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 77E79NGPCV;
|
||||
@@ -742,6 +768,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = 77E79NGPCV;
|
||||
@@ -898,6 +925,30 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
9B77A4DC25ED208400EBDCD6 /* XCRemoteSwiftPackageReference "XUI" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/quickbirdstudios/XUI";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
9B77A4DD25ED208400EBDCD6 /* XUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9B77A4DC25ED208400EBDCD6 /* XCRemoteSwiftPackageReference "XUI" */;
|
||||
productName = XUI;
|
||||
};
|
||||
9B77A4E325ED208E00EBDCD6 /* XUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 9B77A4DC25ED208400EBDCD6 /* XCRemoteSwiftPackageReference "XUI" */;
|
||||
productName = XUI;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 9BE362852583CD1600807BFC /* Project object */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "XUI",
|
||||
"repositoryURL": "https://github.com/quickbirdstudios/XUI",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "6bfaf4412d7fa0081a81650dc1551e6670a5c375",
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
BIN
Binary file not shown.
-19
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>Recipes (iOS).xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>Recipes (macOS).xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -117,6 +117,12 @@ class RecipeService {
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func fetchRecipe(id: String, _ completion: @escaping (Recipe?) -> Void) {
|
||||
fetchRecipes { recipes in
|
||||
completion(recipes.first { $0.id.uuidString == id })
|
||||
}
|
||||
}
|
||||
|
||||
func fetchRecipes(_ completion: @escaping ([Recipe]) -> Void) {
|
||||
completion(
|
||||
Mirror(reflecting: self)
|
||||
|
||||
@@ -12,14 +12,46 @@ struct RecipesApp: App {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@StateObject var coordinator = HomeCoordinator(recipeService: RecipeService())
|
||||
let recipeService: RecipeService
|
||||
@StateObject var coordinator: DefaultHomeCoordinator
|
||||
@State var hasOpenedURL = false
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init() {
|
||||
recipeService = RecipeService()
|
||||
let coordinator = DefaultHomeCoordinator(recipeService: recipeService)
|
||||
_coordinator = .init(wrappedValue: coordinator)
|
||||
}
|
||||
|
||||
// MARK: Scenes
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
HomeCoordinatorView(coordinator: coordinator)
|
||||
.onOpenURL { coordinator.startDeepLink(from: $0) }
|
||||
.onAppear { simulateURLOpening() }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
private func simulateURLOpening() {
|
||||
#if DEBUG
|
||||
guard !hasOpenedURL else {
|
||||
return
|
||||
}
|
||||
hasOpenedURL = true
|
||||
self.recipeService.fetchRecipes { recipes in
|
||||
guard let recipe = recipes.randomElement(),
|
||||
let url = URL(string: "recipes://ratings?recipeID=" + recipe.id.uuidString) else {
|
||||
assertionFailure("Could not find recipe or illegal url format.")
|
||||
return
|
||||
}
|
||||
|
||||
coordinator.startDeepLink(from: url)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// DefaultHomeCoordinator.swift
|
||||
// Recipes
|
||||
//
|
||||
// Created by Paul Kraft on 07.01.21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XUI
|
||||
|
||||
class DefaultHomeCoordinator: ObservableObject, HomeCoordinator {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@Published var tab = HomeTab.meat
|
||||
@Published private(set) var veggieCoordinator: RecipeListCoordinator!
|
||||
@Published private(set) var meatCoordinator: RecipeListCoordinator!
|
||||
@Published var openedURL: URL?
|
||||
|
||||
private let recipeService: RecipeService
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(recipeService: RecipeService) {
|
||||
self.recipeService = recipeService
|
||||
|
||||
self.veggieCoordinator = DefaultRecipeListCoordinator(
|
||||
title: "Veggie",
|
||||
recipeService: recipeService,
|
||||
parent: self,
|
||||
filter: { $0.isVegetarian }
|
||||
)
|
||||
|
||||
self.meatCoordinator = DefaultRecipeListCoordinator(
|
||||
title: "Meat",
|
||||
recipeService: recipeService,
|
||||
parent: self,
|
||||
filter: { !$0.isVegetarian }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func startDeepLink(from url: URL) {
|
||||
guard url.scheme == "recipes",
|
||||
url.host == "ratings",
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
let recipeID = components.queryItems?.first(where: { $0.name == "recipeID" })?.value else {
|
||||
assertionFailure("Trying to open app with illegal url \(url).")
|
||||
return
|
||||
}
|
||||
|
||||
openRatingsForRecipe(id: recipeID)
|
||||
}
|
||||
|
||||
func open(_ url: URL) {
|
||||
self.openedURL = url
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
private func openRatings(for recipe: Recipe) {
|
||||
tab = recipe.isVegetarian ? .veggie : .meat
|
||||
let recipeListCoordinator = firstReceiver(as: RecipeListCoordinator.self, where: { $0.filter(recipe) })
|
||||
recipeListCoordinator!.open(recipe)
|
||||
let recipeViewModel = firstReceiver(as: RecipeViewModel.self, where: { $0.recipe.id == recipe.id })
|
||||
recipeViewModel!.openRatings()
|
||||
}
|
||||
|
||||
private func openRatingsForRecipe(id: String) {
|
||||
recipeService.fetchRecipe(id: id) { [weak self] recipe in
|
||||
guard let recipe = recipe, let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.openRatings(for: recipe)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import XUI
|
||||
|
||||
enum HomeTab {
|
||||
case meat
|
||||
@@ -14,42 +15,22 @@ enum HomeTab {
|
||||
case settings
|
||||
}
|
||||
|
||||
class HomeCoordinator: ObservableObject {
|
||||
protocol HomeCoordinator: ViewModel {
|
||||
var tab: HomeTab { get set }
|
||||
var veggieCoordinator: RecipeListCoordinator! { get }
|
||||
var meatCoordinator: RecipeListCoordinator! { get }
|
||||
var openedURL: URL? { get set }
|
||||
|
||||
// MARK: Stored Properties
|
||||
func startDeepLink(from url: URL)
|
||||
func open(_ url: URL)
|
||||
}
|
||||
|
||||
@Published var tab = HomeTab.meat
|
||||
@Published var veggieCoordinator: RecipeListCoordinator!
|
||||
@Published var meatCoordinator: RecipeListCoordinator!
|
||||
extension HomeCoordinator {
|
||||
|
||||
@Published var openedURL: URL?
|
||||
|
||||
private let recipeService: RecipeService
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(recipeService: RecipeService) {
|
||||
self.recipeService = recipeService
|
||||
|
||||
self.veggieCoordinator = .init(
|
||||
title: "Veggie",
|
||||
recipeService: recipeService,
|
||||
parent: self,
|
||||
filter: { $0.isVegetarian }
|
||||
)
|
||||
|
||||
self.meatCoordinator = .init(
|
||||
title: "Meat",
|
||||
recipeService: recipeService,
|
||||
parent: self,
|
||||
filter: { !$0.isVegetarian }
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func open(_ url: URL) {
|
||||
self.openedURL = url
|
||||
@DeepLinkableBuilder
|
||||
var children: [DeepLinkable] {
|
||||
veggieCoordinator
|
||||
meatCoordinator
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XUI
|
||||
|
||||
struct HomeCoordinatorView: View {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@ObservedObject var coordinator: HomeCoordinator
|
||||
@Store var coordinator: HomeCoordinator
|
||||
|
||||
// MARK: Views
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// DefaultRatingViewModel.swift
|
||||
// Recipes
|
||||
//
|
||||
// Created by Paul Kraft on 07.01.21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class DefaultRatingViewModel: RatingViewModel, ObservableObject, Identifiable {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@Published private(set) var recipe: Recipe
|
||||
@Published private(set) var meanRating = 0.0
|
||||
@Published private(set) var ratings = [Recipe.Rating]()
|
||||
|
||||
private let recipeService: RecipeService
|
||||
private unowned let coordinator: RecipeListCoordinator
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(recipe: Recipe, recipeService: RecipeService,
|
||||
coordinator: RecipeListCoordinator) {
|
||||
self.coordinator = coordinator
|
||||
self.recipe = recipe
|
||||
self.recipeService = recipeService
|
||||
|
||||
recipeService.fetchRatings(for: recipe) { ratings in
|
||||
self.ratings = ratings
|
||||
self.meanRating = Double(ratings.map(\.value).reduce(0, +)) / Double(ratings.count)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func close() {
|
||||
self.coordinator.closeRatings()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import XUI
|
||||
|
||||
struct RatingView: View {
|
||||
|
||||
@@ -38,7 +39,7 @@ struct RatingView: View {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@ObservedObject var viewModel: RatingViewModel
|
||||
@Store var viewModel: RatingViewModel
|
||||
|
||||
// MARK: Views
|
||||
|
||||
|
||||
@@ -6,36 +6,12 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XUI
|
||||
|
||||
class RatingViewModel: ObservableObject, Identifiable {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@Published var recipe: Recipe
|
||||
@Published var meanRating = 0.0
|
||||
@Published var ratings = [Recipe.Rating]()
|
||||
|
||||
private let recipeService: RecipeService
|
||||
private unowned let coordinator: RecipeListCoordinator
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(recipe: Recipe, recipeService: RecipeService,
|
||||
coordinator: RecipeListCoordinator) {
|
||||
self.coordinator = coordinator
|
||||
self.recipe = recipe
|
||||
self.recipeService = recipeService
|
||||
|
||||
recipeService.fetchRatings(for: recipe) { ratings in
|
||||
self.ratings = ratings
|
||||
self.meanRating = Double(ratings.map(\.value).reduce(0, +)) / Double(ratings.count)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func close() {
|
||||
self.coordinator.closeRatings()
|
||||
}
|
||||
protocol RatingViewModel: ViewModel {
|
||||
var recipe: Recipe { get }
|
||||
var meanRating: Double { get }
|
||||
var ratings: [Recipe.Rating] { get }
|
||||
|
||||
func close()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// DefaultRecipeViewModel.swift
|
||||
// Recipes
|
||||
//
|
||||
// Created by Paul Kraft on 07.01.21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class DefaultRecipeViewModel: RecipeViewModel, ObservableObject, Identifiable {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@Published private(set) var recipe: Recipe
|
||||
|
||||
private unowned let coordinator: RecipeListCoordinator
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(recipe: Recipe, coordinator: RecipeListCoordinator) {
|
||||
self.coordinator = coordinator
|
||||
self.recipe = recipe
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func openRatings() {
|
||||
self.coordinator.openRatings(for: recipe)
|
||||
}
|
||||
|
||||
func open(_ url: URL) {
|
||||
self.coordinator.open(url)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,12 +6,13 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XUI
|
||||
|
||||
struct RecipeView<RatingModifier: ViewModifier>: View {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@ObservedObject var viewModel: RecipeViewModel
|
||||
@Store var viewModel: RecipeViewModel
|
||||
let ratingModifier: RatingModifier
|
||||
|
||||
// MARK: Views
|
||||
|
||||
@@ -6,30 +6,11 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XUI
|
||||
|
||||
class RecipeViewModel: ObservableObject, Identifiable {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@Published var recipe: Recipe
|
||||
|
||||
private unowned let coordinator: RecipeListCoordinator
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(recipe: Recipe, coordinator: RecipeListCoordinator) {
|
||||
self.coordinator = coordinator
|
||||
self.recipe = recipe
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func openRatings() {
|
||||
self.coordinator.openRatings(for: recipe)
|
||||
}
|
||||
|
||||
func open(_ url: URL) {
|
||||
self.coordinator.open(url)
|
||||
}
|
||||
protocol RecipeViewModel: ViewModel {
|
||||
var recipe: Recipe { get }
|
||||
|
||||
func openRatings()
|
||||
func open(_ url: URL)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// DefaultRecipeListViewModel.swift
|
||||
// Recipes
|
||||
//
|
||||
// Created by Paul Kraft on 07.01.21.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
class DefaultRecipeListViewModel: ObservableObject, RecipeListViewModel {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@Published private(set) var title: String
|
||||
@Published private(set) var recipes = [Recipe]()
|
||||
|
||||
private let recipeService: RecipeService
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private unowned let coordinator: RecipeListCoordinator
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(title: String,
|
||||
recipeService: RecipeService,
|
||||
coordinator: RecipeListCoordinator,
|
||||
filter: @escaping (Recipe) -> Bool) {
|
||||
|
||||
self.title = title
|
||||
self.coordinator = coordinator
|
||||
self.recipeService = recipeService
|
||||
|
||||
coordinator
|
||||
.objectWillChange
|
||||
.sink(receiveValue: objectWillChange.send)
|
||||
.store(in: &cancellables)
|
||||
|
||||
recipeService.fetchRecipes {
|
||||
self.recipes = $0.filter(filter)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func open(_ recipe: Recipe) {
|
||||
coordinator.open(recipe)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,12 +6,13 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XUI
|
||||
|
||||
struct RecipeList: View {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@ObservedObject var viewModel: RecipeListViewModel
|
||||
@Store var viewModel: RecipeListViewModel
|
||||
|
||||
// MARK: Views
|
||||
|
||||
|
||||
@@ -6,42 +6,11 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XUI
|
||||
|
||||
extension Identifiable where ID: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
class RecipeListViewModel: ObservableObject {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@Published var title: String
|
||||
@Published var recipes = [Recipe]()
|
||||
|
||||
private let recipeService: RecipeService
|
||||
private unowned let coordinator: RecipeListCoordinator
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(title: String,
|
||||
recipeService: RecipeService,
|
||||
coordinator: RecipeListCoordinator,
|
||||
filter: @escaping (Recipe) -> Bool) {
|
||||
self.title = title
|
||||
self.coordinator = coordinator
|
||||
self.recipeService = recipeService
|
||||
|
||||
recipeService.fetchRecipes {
|
||||
self.recipes = $0.filter(filter)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func open(_ recipe: Recipe) {
|
||||
self.coordinator.open(recipe)
|
||||
}
|
||||
|
||||
protocol RecipeListViewModel: ViewModel {
|
||||
var title: String { get }
|
||||
var recipes: [Recipe] { get }
|
||||
|
||||
func open(_ recipe: Recipe)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// DefaultRecipeListCoordinator.swift
|
||||
// Recipes
|
||||
//
|
||||
// Created by Paul Kraft on 07.01.21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XUI
|
||||
|
||||
class DefaultRecipeListCoordinator: ObservableObject, RecipeListCoordinator, Identifiable {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@Published private(set) var viewModel: RecipeListViewModel!
|
||||
@Published var detailViewModel: RecipeViewModel?
|
||||
@Published var ratingViewModel: RatingViewModel?
|
||||
|
||||
private let _filter: (Recipe) -> Bool
|
||||
private let recipeService: RecipeService
|
||||
private unowned let parent: HomeCoordinator
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(title: String,
|
||||
recipeService: RecipeService,
|
||||
parent: HomeCoordinator,
|
||||
filter: @escaping (Recipe) -> Bool) {
|
||||
self.parent = parent
|
||||
self.recipeService = recipeService
|
||||
self._filter = filter
|
||||
|
||||
self.viewModel = DefaultRecipeListViewModel(
|
||||
title: title,
|
||||
recipeService: recipeService,
|
||||
coordinator: self,
|
||||
filter: filter
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func filter(_ recipe: Recipe) -> Bool {
|
||||
_filter(recipe)
|
||||
}
|
||||
|
||||
func open(_ recipe: Recipe) {
|
||||
detailViewModel = DefaultRecipeViewModel(recipe: recipe, coordinator: self)
|
||||
}
|
||||
|
||||
func openRatings(for recipe: Recipe) {
|
||||
ratingViewModel = DefaultRatingViewModel(recipe: recipe, recipeService: recipeService, coordinator: self)
|
||||
}
|
||||
|
||||
func closeRatings() {
|
||||
ratingViewModel = nil
|
||||
}
|
||||
|
||||
func open(_ url: URL) {
|
||||
parent.open(url)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,51 +6,27 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XUI
|
||||
|
||||
class RecipeListCoordinator: ObservableObject, Identifiable {
|
||||
protocol RecipeListCoordinator: ViewModel {
|
||||
var viewModel: RecipeListViewModel! { get }
|
||||
var detailViewModel: RecipeViewModel? { get set }
|
||||
var ratingViewModel: RatingViewModel? { get set }
|
||||
|
||||
// MARK: Stored Properties
|
||||
func filter(_ recipe: Recipe) -> Bool
|
||||
func open(_ recipe: Recipe)
|
||||
func openRatings(for recipe: Recipe)
|
||||
func closeRatings()
|
||||
func open(_ url: URL)
|
||||
}
|
||||
|
||||
@Published var viewModel: RecipeListViewModel!
|
||||
@Published var detailViewModel: RecipeViewModel?
|
||||
@Published var ratingViewModel: RatingViewModel?
|
||||
extension RecipeListCoordinator {
|
||||
|
||||
private let recipeService: RecipeService
|
||||
private unowned let parent: HomeCoordinator
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(title: String,
|
||||
recipeService: RecipeService,
|
||||
parent: HomeCoordinator,
|
||||
filter: @escaping (Recipe) -> Bool) {
|
||||
self.parent = parent
|
||||
self.recipeService = recipeService
|
||||
|
||||
self.viewModel = .init(
|
||||
title: title,
|
||||
recipeService: recipeService,
|
||||
coordinator: self,
|
||||
filter: filter
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func open(_ recipe: Recipe) {
|
||||
self.detailViewModel = .init(recipe: recipe, coordinator: self)
|
||||
}
|
||||
|
||||
func openRatings(for recipe: Recipe) {
|
||||
self.ratingViewModel = .init(recipe: recipe, recipeService: recipeService, coordinator: self)
|
||||
}
|
||||
|
||||
func closeRatings() {
|
||||
self.ratingViewModel = nil
|
||||
}
|
||||
|
||||
func open(_ url: URL) {
|
||||
self.parent.open(url)
|
||||
@DeepLinkableBuilder
|
||||
var children: [DeepLinkable] {
|
||||
viewModel
|
||||
detailViewModel
|
||||
ratingViewModel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,19 +6,20 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XUI
|
||||
|
||||
struct RecipeListCoordinatorView: View {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@ObservedObject var coordinator: RecipeListCoordinator
|
||||
@Store var coordinator: RecipeListCoordinator
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
RecipeList(viewModel: coordinator.viewModel)
|
||||
.navigation(item: $coordinator.detailViewModel) { viewModel in
|
||||
.navigation(model: $coordinator.detailViewModel) { viewModel in
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
phoneRecipeView(viewModel)
|
||||
} else {
|
||||
@@ -32,7 +33,7 @@ struct RecipeListCoordinatorView: View {
|
||||
private func phoneRecipeView(_ viewModel: RecipeViewModel) -> some View {
|
||||
RecipeView(
|
||||
viewModel: viewModel,
|
||||
ratingModifier: SheetModifier(item: $coordinator.ratingViewModel) { viewModel in
|
||||
ratingModifier: SheetModifier(model: $coordinator.ratingViewModel) { viewModel in
|
||||
NavigationView {
|
||||
RatingView(viewModel: viewModel)
|
||||
}
|
||||
@@ -44,7 +45,7 @@ struct RecipeListCoordinatorView: View {
|
||||
private func padRecipeView(_ viewModel: RecipeViewModel) -> some View {
|
||||
RecipeView(
|
||||
viewModel: viewModel,
|
||||
ratingModifier: PopoverModifier(item: $coordinator.ratingViewModel) {
|
||||
ratingModifier: PopoverModifier(model: $coordinator.ratingViewModel) {
|
||||
RatingView(viewModel: $0)
|
||||
.frame(width: 500, height: 500)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// Recipes
|
||||
// Recipes (iOS)
|
||||
//
|
||||
// Created by Paul Kraft on 01.01.21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XUI
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
@ObservedObject var coordinator: HomeCoordinator
|
||||
@Store var coordinator: HomeCoordinator
|
||||
|
||||
// MARK: Views
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// PopoverModifier.swift
|
||||
// Recipes
|
||||
//
|
||||
// Created by Paul Kraft on 05.01.21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PopoverModifier<Item: Identifiable, Destination: View>: ViewModifier {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
private let item: Binding<Item?>
|
||||
private let destination: (Item) -> Destination
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(item: Binding<Item?>,
|
||||
@ViewBuilder content: @escaping (Item) -> Destination) {
|
||||
|
||||
self.item = item
|
||||
self.destination = content
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.popover(item: item, content: destination)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// SheetModifier.swift
|
||||
// Recipes
|
||||
//
|
||||
// Created by Paul Kraft on 05.01.21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SheetModifier<Item: Identifiable, Destination: View>: ViewModifier {
|
||||
|
||||
// MARK: Stored Properties
|
||||
|
||||
private let item: Binding<Item?>
|
||||
private let destination: (Item) -> Destination
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
init(item: Binding<Item?>,
|
||||
@ViewBuilder content: @escaping (Item) -> Destination) {
|
||||
|
||||
self.item = item
|
||||
self.destination = content
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.sheet(item: item, content: destination)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// View+Navigation.swift
|
||||
// Recipes
|
||||
//
|
||||
// Created by Paul Kraft on 05.01.21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
|
||||
func onNavigation(_ action: @escaping () -> Void) -> some View {
|
||||
let isActive = Binding(
|
||||
get: { false },
|
||||
set: { newValue in
|
||||
if newValue {
|
||||
action()
|
||||
}
|
||||
}
|
||||
)
|
||||
return NavigationLink(
|
||||
destination: EmptyView(),
|
||||
isActive: isActive
|
||||
) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
func navigation<Item, Destination: View>(
|
||||
item: Binding<Item?>,
|
||||
@ViewBuilder destination: (Item) -> Destination
|
||||
) -> some View {
|
||||
let isActive = Binding(
|
||||
get: { item.wrappedValue != nil },
|
||||
set: { value in
|
||||
if !value {
|
||||
item.wrappedValue = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
return navigation(isActive: isActive) {
|
||||
item.wrappedValue.map(destination)
|
||||
}
|
||||
}
|
||||
|
||||
func navigation<Destination: View>(
|
||||
isActive: Binding<Bool>,
|
||||
@ViewBuilder destination: () -> Destination
|
||||
) -> some View {
|
||||
overlay(
|
||||
NavigationLink(
|
||||
destination: isActive.wrappedValue ? destination() : nil,
|
||||
isActive: isActive,
|
||||
label: { EmptyView() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NavigationLink {
|
||||
|
||||
init<T: Identifiable, D: View>(item: Binding<T?>,
|
||||
@ViewBuilder destination: (T) -> D,
|
||||
@ViewBuilder label: () -> Label) where Destination == D? {
|
||||
let isActive = Binding(
|
||||
get: { item.wrappedValue != nil },
|
||||
set: { value in
|
||||
if !value {
|
||||
item.wrappedValue = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
self.init(
|
||||
destination: item.wrappedValue.map(destination),
|
||||
isActive: isActive,
|
||||
label: label
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
//
|
||||
// AsyncImage.swift
|
||||
// Recipes
|
||||
// QuickGit
|
||||
//
|
||||
// Created by Paul Kraft on 04.03.20.
|
||||
// Copyright © 2020 QuickBird Studios. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// RatingStars.swift
|
||||
// Recipes
|
||||
// Recipes (iOS)
|
||||
//
|
||||
// Created by Paul Kraft on 02.01.21.
|
||||
//
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// SafariView.swift
|
||||
// Recipes
|
||||
// Recipes (iOS)
|
||||
//
|
||||
// Created by Paul Kraft on 04.01.21.
|
||||
//
|
||||
@@ -27,3 +27,14 @@ struct SafariView: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func safariSheet(with binding: Binding<URL?>) -> some View {
|
||||
sheet(item: binding) { url in
|
||||
SafariView(url: url)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user