2 Commits

Author SHA1 Message Date
Paul Kraft 430ae744cc Update XUI 2021-03-01 19:53:58 +01:00
Paul Kraft 98d565ba0d XUI adaptions 2021-03-01 18:00:55 +01:00
30 changed files with 572 additions and 372 deletions
+90
View File
@@ -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/
-1
View File
@@ -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.
+79 -28
View File
@@ -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
}
@@ -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>
+6
View File
@@ -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)
+33 -1
View File
@@ -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
)
}
}
+2 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
//
// RatingStars.swift
// Recipes
// Recipes (iOS)
//
// Created by Paul Kraft on 02.01.21.
//
+12 -1
View File
@@ -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)
}
}
}