-
Notifications
You must be signed in to change notification settings - Fork 299
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
399f2e5
commit b4c08e9
Showing
21 changed files
with
2,513 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
## [Point-Free](https://www.pointfree.co) | ||
|
||
> #### This directory contains code from Point-Free Episode: [UIKit Navigation: Part 2](https://www.pointfree.co/episodes/ep170-uikit-navigation-part-2) | ||
> | ||
> We finish porting our SwiftUI application to UIKit by introducing a collection view. Along the way we will demonstrate how deep-linking works exactly as it did in SwiftUI, and we show the power of state driven navigation by seamlessly switching between the two view paradigms. |
572 changes: 572 additions & 0 deletions
572
0170-uikit-navigation-pt2/SwiftUINavigation/SwiftUINavigation.xcodeproj/project.pbxproj
Large diffs are not rendered by default.
Oops, something went wrong.
7 changes: 7 additions & 0 deletions
7
...wiftUINavigation/SwiftUINavigation.xcodeproj/project.xcworkspace/contents.xcworkspacedata
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
8 changes: 8 additions & 0 deletions
8
...ion/SwiftUINavigation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?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>IDEDidComputeMac32BitWarning</key> | ||
<true/> | ||
</dict> | ||
</plist> |
11 changes: 11 additions & 0 deletions
11
...t2/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AccentColor.colorset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"colors" : [ | ||
{ | ||
"idiom" : "universal" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
98 changes: 98 additions & 0 deletions
98
...-pt2/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/AppIcon.appiconset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
{ | ||
"images" : [ | ||
{ | ||
"idiom" : "iphone", | ||
"scale" : "2x", | ||
"size" : "20x20" | ||
}, | ||
{ | ||
"idiom" : "iphone", | ||
"scale" : "3x", | ||
"size" : "20x20" | ||
}, | ||
{ | ||
"idiom" : "iphone", | ||
"scale" : "2x", | ||
"size" : "29x29" | ||
}, | ||
{ | ||
"idiom" : "iphone", | ||
"scale" : "3x", | ||
"size" : "29x29" | ||
}, | ||
{ | ||
"idiom" : "iphone", | ||
"scale" : "2x", | ||
"size" : "40x40" | ||
}, | ||
{ | ||
"idiom" : "iphone", | ||
"scale" : "3x", | ||
"size" : "40x40" | ||
}, | ||
{ | ||
"idiom" : "iphone", | ||
"scale" : "2x", | ||
"size" : "60x60" | ||
}, | ||
{ | ||
"idiom" : "iphone", | ||
"scale" : "3x", | ||
"size" : "60x60" | ||
}, | ||
{ | ||
"idiom" : "ipad", | ||
"scale" : "1x", | ||
"size" : "20x20" | ||
}, | ||
{ | ||
"idiom" : "ipad", | ||
"scale" : "2x", | ||
"size" : "20x20" | ||
}, | ||
{ | ||
"idiom" : "ipad", | ||
"scale" : "1x", | ||
"size" : "29x29" | ||
}, | ||
{ | ||
"idiom" : "ipad", | ||
"scale" : "2x", | ||
"size" : "29x29" | ||
}, | ||
{ | ||
"idiom" : "ipad", | ||
"scale" : "1x", | ||
"size" : "40x40" | ||
}, | ||
{ | ||
"idiom" : "ipad", | ||
"scale" : "2x", | ||
"size" : "40x40" | ||
}, | ||
{ | ||
"idiom" : "ipad", | ||
"scale" : "1x", | ||
"size" : "76x76" | ||
}, | ||
{ | ||
"idiom" : "ipad", | ||
"scale" : "2x", | ||
"size" : "76x76" | ||
}, | ||
{ | ||
"idiom" : "ipad", | ||
"scale" : "2x", | ||
"size" : "83.5x83.5" | ||
}, | ||
{ | ||
"idiom" : "ios-marketing", | ||
"scale" : "1x", | ||
"size" : "1024x1024" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
0170-uikit-navigation-pt2/SwiftUINavigation/SwiftUINavigation/Assets.xcassets/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
248 changes: 248 additions & 0 deletions
248
0170-uikit-navigation-pt2/SwiftUINavigation/SwiftUINavigation/ContentView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
import Parsing | ||
import SwiftUI | ||
|
||
struct DeepLinkRequest { | ||
var pathComponents: ArraySlice<Substring> | ||
var queryItems: [String: ArraySlice<Substring?>] | ||
} | ||
|
||
extension DeepLinkRequest { | ||
init(url: URL) { | ||
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? [] | ||
|
||
self.init( | ||
pathComponents: url.path.split(separator: "/")[...], | ||
queryItems: queryItems.reduce(into: [:]) { dictionary, item in | ||
dictionary[item.name, default: []].append(item.value?[...]) | ||
} | ||
) | ||
} | ||
} | ||
|
||
struct PathComponent<ComponentParser>: Parser | ||
where | ||
ComponentParser: Parser, | ||
ComponentParser.Input == Substring | ||
{ | ||
let component: ComponentParser | ||
init(_ component: ComponentParser) { | ||
self.component = component | ||
} | ||
|
||
func parse(_ input: inout DeepLinkRequest) -> ComponentParser.Output? { | ||
guard | ||
var firstComponent = input.pathComponents.first, | ||
let output = self.component.parse(&firstComponent), | ||
firstComponent.isEmpty | ||
else { return nil } | ||
|
||
input.pathComponents.removeFirst() | ||
return output | ||
} | ||
} | ||
|
||
struct PathEnd: Parser { | ||
func parse(_ input: inout DeepLinkRequest) -> Void? { | ||
guard input.pathComponents.isEmpty | ||
else { return nil } | ||
return () | ||
} | ||
} | ||
|
||
struct QueryItem<ValueParser>: Parser | ||
where | ||
ValueParser: Parser, | ||
ValueParser.Input == Substring | ||
{ | ||
let name: String | ||
let valueParser: ValueParser | ||
|
||
init(_ name: String, _ valueParser: ValueParser) { | ||
self.name = name | ||
self.valueParser = valueParser | ||
} | ||
|
||
init(_ name: String) where ValueParser == Rest<Substring> { | ||
self.init(name, Rest()) | ||
} | ||
|
||
func parse(_ input: inout DeepLinkRequest) -> ValueParser.Output? { | ||
guard | ||
let wrapped = input.queryItems[self.name]?.first, | ||
var value = wrapped, | ||
let output = self.valueParser.parse(&value), | ||
value.isEmpty | ||
else { return nil } | ||
|
||
input.queryItems[self.name]?.removeFirst() | ||
return output | ||
} | ||
} | ||
|
||
enum AppRoute { | ||
case one | ||
case inventory(InventoryRoute?) | ||
case three | ||
} | ||
|
||
enum InventoryRoute { | ||
case add(Item, ItemRoute? = nil) | ||
case row(Item.ID, RowRoute) | ||
|
||
enum RowRoute { | ||
case delete | ||
case duplicate | ||
case edit | ||
} | ||
} | ||
|
||
enum ItemRoute { | ||
case colorPicker | ||
} | ||
|
||
let item = QueryItem("name").orElse(Always("")) | ||
.take(QueryItem("quantity", Int.parser()).orElse(Always(1))) | ||
.map { name, quantity in | ||
Item(name: String(name), status: .inStock(quantity: quantity)) | ||
} | ||
|
||
let inventoryDeepLinker = PathEnd() | ||
.map { AppRoute.inventory(nil) } | ||
.orElse( | ||
PathComponent("add") | ||
.skip(PathEnd()) | ||
.take(item) | ||
.map { .inventory(.add($0)) } | ||
) | ||
.orElse( | ||
PathComponent("add") | ||
.skip(PathComponent("colorPicker")) | ||
.skip(PathEnd()) | ||
.take(item) | ||
.map { .inventory(.add($0, .colorPicker)) } | ||
) | ||
.orElse( | ||
PathComponent(UUID.parser()) | ||
.skip(PathComponent("edit")) | ||
.skip(PathEnd()) | ||
.map { id in .inventory(.row(id, .edit)) } | ||
) | ||
.orElse( | ||
PathComponent(UUID.parser()) | ||
.skip(PathComponent("delete")) | ||
.skip(PathEnd()) | ||
.map { id in .inventory(.row(id, .delete)) } | ||
) | ||
|
||
let deepLinker = PathComponent("one") | ||
.skip(PathEnd()) | ||
.map { AppRoute.one } | ||
.orElse( | ||
PathComponent("inventory") | ||
.take(inventoryDeepLinker) | ||
) | ||
.orElse( | ||
PathComponent("three") | ||
.skip(PathEnd()) | ||
.map { .three } | ||
) | ||
|
||
enum Tab { | ||
case one, inventory, three | ||
} | ||
|
||
class AppViewModel: ObservableObject { | ||
@Published var inventoryViewModel: InventoryViewModel | ||
@Published var selectedTab: Tab | ||
|
||
init( | ||
inventoryViewModel: InventoryViewModel = .init(), | ||
selectedTab: Tab = .one | ||
) { | ||
self.inventoryViewModel = inventoryViewModel | ||
self.selectedTab = selectedTab | ||
} | ||
|
||
func open(url: URL) { | ||
var request = DeepLinkRequest(url: url) | ||
if let route = deepLinker.parse(&request) { | ||
switch route { | ||
case .one: | ||
self.selectedTab = .one | ||
|
||
case let .inventory(inventoryRoute): | ||
self.selectedTab = .inventory | ||
self.inventoryViewModel.navigate(to: inventoryRoute) | ||
|
||
case .three: | ||
self.selectedTab = .three | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension InventoryViewModel { | ||
func navigate(to route: InventoryRoute?) { | ||
switch route { | ||
case let .add(item, .none): | ||
self.route = .add(.init(item: item)) | ||
|
||
case let .add(item, .colorPicker): | ||
self.route = .add(.init(item: item, route: .colorPicker)) | ||
|
||
case let .row(id, rowRoute): | ||
guard let viewModel = self.inventory[id: id] | ||
else { break } | ||
viewModel.navigate(to: rowRoute) | ||
|
||
case .none: | ||
self.route = nil | ||
} | ||
} | ||
} | ||
|
||
extension ItemRowViewModel { | ||
func navigate(to route: InventoryRoute.RowRoute) { | ||
switch route { | ||
case .delete: | ||
self.route = .deleteAlert | ||
case .duplicate: | ||
self.route = .duplicate(.init(item: self.item)) | ||
case .edit: | ||
self.route = .edit(.init(item: self.item)) | ||
} | ||
} | ||
} | ||
|
||
struct ContentView: View { | ||
@ObservedObject var viewModel: AppViewModel | ||
|
||
var body: some View { | ||
TabView(selection: self.$viewModel.selectedTab) { | ||
Button("Go to 2nd tab") { | ||
self.viewModel.selectedTab = .inventory | ||
} | ||
.tabItem { Text("One") } | ||
.tag(Tab.one) | ||
|
||
NavigationView { | ||
InventoryView(viewModel: self.viewModel.inventoryViewModel) | ||
} | ||
.tabItem { Text("Inventory") } | ||
.tag(Tab.inventory) | ||
|
||
Text("Three") | ||
.tabItem { Text("Three") } | ||
.tag(Tab.three) | ||
} | ||
.onOpenURL { url in | ||
self.viewModel.open(url: url) | ||
} | ||
} | ||
} | ||
|
||
struct ContentView_Previews: PreviewProvider { | ||
static var previews: some View { | ||
ContentView(viewModel: .init(selectedTab: .inventory)) | ||
} | ||
} |
Oops, something went wrong.