-
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
274c510
commit b6ac6b8
Showing
19 changed files
with
2,206 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 1](https://www.pointfree.co/episodes/ep169-uikit-navigation-part-1) | ||
> | ||
> What does all the work we’ve done with navigation in SwiftUI have to say about UIKit? Turns out a lot! Without making a single change to the view models we can rewrite the entire view layer in UIKit, and the application will work exactly as it did before, deep-linking and all! |
564 changes: 564 additions & 0 deletions
564
0169-uikit-navigation-pt1/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
...t1/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
...-pt1/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
0169-uikit-navigation-pt1/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
0169-uikit-navigation-pt1/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.