Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# macOS
.DS_Store

# Xcode — user-specific state
xcuserdata/
*.xcuserstate
*.xcscmblueprint
*.xccheckout

# Xcode — build output
DerivedData/
build/
*.hmap
*.ipa
*.dSYM.zip
*.dSYM

# Swift Package Manager
.swiftpm/
.build/

# CocoaPods (not used, prophylactic)
Pods/

# Carthage (not used, prophylactic)
Carthage/Build/

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
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).

## Requirements

iOS 14+, Xcode 12+. Open `Recipes/Recipes.xcodeproj` and run the `Recipes (iOS)` scheme.

## 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.
Expand Down
258 changes: 0 additions & 258 deletions Recipes/Recipes.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Binary file not shown.

This file was deleted.

2 changes: 1 addition & 1 deletion Recipes/Shared/Extensions/URL+Identifiable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

extension URL: Identifiable {
extension URL: @retroactive Identifiable {

public var id: String {
absoluteString
Expand Down
2 changes: 1 addition & 1 deletion Recipes/Shared/Scenes/Home/HomeCoordinator.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// HomeViewModel.swift
// HomeCoordinator.swift
// Recipes
//
// Created by Paul Kraft on 11.12.20.
Expand Down
2 changes: 1 addition & 1 deletion Recipes/Shared/Scenes/Home/HomeCoordinatorView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// HomeCoordinator.swift
// HomeCoordinatorView.swift
// Recipes
//
// Created by Paul Kraft on 11.12.20.
Expand Down
6 changes: 0 additions & 6 deletions Recipes/Shared/Scenes/RecipeList/RecipeListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@

import SwiftUI

extension Identifiable where ID: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

class RecipeListViewModel: ObservableObject {

// MARK: Stored Properties
Expand Down
2 changes: 1 addition & 1 deletion Recipes/Shared/Scenes/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct SettingsView: View {
Text("Nymphenburger Str. 13-15")
Text("80335 Munich")
}
.frame(maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, maxHeight: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.onTapGesture(perform: openWebsite)
.navigationTitle("Settings")
Expand Down
3 changes: 3 additions & 0 deletions Recipes/Shared/ViewModifiers/PopoverModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import SwiftUI

/// Presents `content(item)` as a popover whenever `item` is non-nil.
/// Pair with `SheetModifier` to let a coordinator pick the presentation
/// style for a leaf view based on device or size class.
struct PopoverModifier<Item: Identifiable, Destination: View>: ViewModifier {

// MARK: Stored Properties
Expand Down
5 changes: 5 additions & 0 deletions Recipes/Shared/ViewModifiers/SheetModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

import SwiftUI

/// Presents `content(item)` as a sheet whenever `item` is non-nil.
///
/// Exists as a `ViewModifier` so a coordinator can inject either this or
/// `PopoverModifier` into a leaf view, switching the presentation style
/// (e.g. by device or size class) without the leaf knowing about it.
struct SheetModifier<Item: Identifiable, Destination: View>: ViewModifier {

// MARK: Stored Properties
Expand Down
13 changes: 13 additions & 0 deletions Recipes/Shared/ViewModifiers/View+Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import SwiftUI

extension View {

/// Wraps `self` in a hidden `NavigationLink` whose activation fires `action`.
/// Use when the tap should trigger an imperative side effect (typically a
/// coordinator method) rather than navigate to a statically known destination.
func onNavigation(_ action: @escaping () -> Void) -> some View {
let isActive = Binding(
get: { false },
Expand All @@ -26,6 +29,10 @@ extension View {
}
}

/// Pushes `destination(item)` whenever `item` becomes non-nil; clearing
/// `item` (e.g. when the user pops) is propagated back through the binding.
/// Lets a coordinator drive navigation by mutating an optional `@Published`
/// view model rather than embedding a hardcoded `NavigationLink` in the view.
func navigation<Item, Destination: View>(
item: Binding<Item?>,
@ViewBuilder destination: (Item) -> Destination
Expand All @@ -43,6 +50,9 @@ extension View {
}
}

/// Pushes `destination()` while `isActive` is true. The destination is
/// constructed lazily — only when the link is active — and the link itself
/// lives in an `overlay` so it doesn't disturb the receiver's layout.
func navigation<Destination: View>(
isActive: Binding<Bool>,
@ViewBuilder destination: () -> Destination
Expand All @@ -60,6 +70,9 @@ extension View {

extension NavigationLink {

/// Optional-item-driven `NavigationLink`: active iff `item` is non-nil,
/// and pops by clearing the binding. Mirrors `sheet(item:content:)`
/// for push-style navigation.
init<T: Identifiable, D: View>(item: Binding<T?>,
@ViewBuilder destination: (T) -> D,
@ViewBuilder label: () -> Label) where Destination == D? {
Expand Down
22 changes: 0 additions & 22 deletions Recipes/Tests macOS/Info.plist

This file was deleted.

42 changes: 0 additions & 42 deletions Recipes/Tests macOS/Tests_macOS.swift

This file was deleted.

26 changes: 0 additions & 26 deletions Recipes/macOS/Info.plist

This file was deleted.

10 changes: 0 additions & 10 deletions Recipes/macOS/macOS.entitlements

This file was deleted.

Loading