SwiftUI Navigation のすべて

52.1K Views

September 11, 22

スライド概要

iOSDC Japan 2022 で発表した内容です

profile-image

iOS エンジニアをやっています。

シェア

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

SwiftUI Navigation のすべて iOSDC Japan 2022 kalupas226

2.

自己紹介 • アイカワ • @kalupas226 • Cookpad Inc. iOS Developer 2

3.

アジェンダ • SwiftUI Navigation の俯瞰 • 様々な種類 の Navigation • OS による変化が激しい Navigation • Fire and forget / State driven Navigation API • SwiftUI の Navigation API における課題 • Navigation API の課題にどう立ち向かうか • 「swiftui-navigation」のアプローチ • まとめ 3

4.

􀁟 本トークで話さないこと • 開発者がそれぞれで定義する独自の Custom Navigation • iOS 以外のプラットフォームにおける Navigation • アプリケーションにおける Navigation はこうあるべきというような デザイン的な話 4

5.

􀁟 本トークで主に参考にしている情報 • Point-Free 製の「swiftui-navigation」というライブラリ • https://github.com/pointfreeco/swiftui-navigation • 発表内では以下のように区別して説明します • SwiftUI Navigation API: Apple 純正の API についての話 • 「swiftui-navigation」: ライブラリについての話 5

6.

SwiftUI Navigation の俯瞰

7.

SwiftUI Navigation の俯瞰 様々な種類の Navigation

8.

SwiftUI Navigation の俯瞰 > 様々な種類の Navigation 様々な種類の Navigation API が存在する • Tab • Alert • Sheet • Full Screen Cover • Con rmation Dialog • Popover • Navigation Link… fi 8

9.

SwiftUI Navigation の俯瞰 > 様々な種類の Navigation Tab TabView { FirstView() .tabItem { Image(systemName: "exclamationmark.circle") Text("Alert") } SecondView() .tabItem {…} ThirdView() .tabItem {…} FourthView() .tabItem {…} }

10.
[beta]
SwiftUI Navigation の俯瞰 > 様々な種類の Navigation

Alert

BaseView()
.alert(
“Alertが表示されました”,
isPresented: $isPresentedAlert,
actions: {
Button("OK", action: {})
}
)

11.
[beta]
SwiftUI Navigation の俯瞰 > 様々な種類の Navigation

Sheet

BaseView()
.sheet(
isPresented: $isPresentedAlert,
content: {
Text("Sheetが表示されました")
}
)

12.
[beta]
SwiftUI Navigation の俯瞰 > 様々な種類の Navigation

NavigationView・NavigationLink
(Deprecated)
NavigationView {
ForEach(1...10, id: \.self) { id in
NavigationLink(
"Go to \(id) simple destination",
destination: {
Text("This is \(id) destination")
}
)
}
}

13.

SwiftUI Navigation の俯瞰 OS による変化が激しい Navigation

14.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation OS が進化するにつれ API の形も変わってきた • ActionSheet API (deprecated) → Con rmation Dialog API • Alert API • deprecated になったものと、そうでないものがある • Navigation-base API • NavigationView → NavigationStack • NavigationLink fi 14

15.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation ActionSheet API (deprecated) .actionSheet(isPresented: $showActionSheet) { ActionSheet( title: Text(...), message: Text(...), buttons: [ .cancel(), .destructive( Text(...), action: {} ), .default( Text(...), action: {} ) ] ) }

16.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation ActionSheet API (deprecated) .actionSheet(isPresented: $showActionSheet) { ActionSheet( title: Text(...), message: Text(...), buttons: [ .cancel(), .destructive( Text(...), action: {} ), .default( Text(...), action: {} ) ] ) }

17.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation ActionSheet API (deprecated) .actionSheet(isPresented: $showActionSheet) { ActionSheet( title: Text(...), message: Text(...), buttons: [ .cancel(), .destructive( Text(...), action: {} ), .default( Text(...), action: {} ) ] ) }

18.
[beta]
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation

Con rmation Dialog API(iOS 15~)

fi

.confirmationDialog(
“Title",
isPresented: $isConfirming
presenting: dialogDetail
) { detail in
Button {
} label: {
Text("Import \(detail.name)")
}
Button("Cancel", role: .cancel) {
dialogDetail = nil
}
} message: { detail in
Text(\(detail.name) \(detail.type))
}

19.
[beta]
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation

Con rmation Dialog API(iOS 15~)

.confirmationDialog(

“Title",
isPresented: $isConfirming,

fi

presenting: dialogDetail
) { detail in
Button {
} label: {
Text("Import \(detail.name)")
}
Button("Cancel", role: .cancel) {
dialogDetail = nil
}
} message: { detail in
Text(\(detail.name) \(detail.type))
}

20.
[beta]
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation

Con rmation Dialog API(iOS 15~)

.confirmationDialog(
“Title",
isPresented: $isConfirming,

fi

presenting: dialogDetail
) { detail in
Button {
} label: {
Text("Import \(detail.name)")
}
Button("Cancel", role: .cancel) {
dialogDetail = nil
}
} message: { detail in
Text(\(detail.name) \(detail.type))
}

21.
[beta]
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation

Alert API (deprecated)

.alert(isPresented: $showAlert) {
Alert(
title: Text("Title"),
message: Text("Message")
)
}

22.
[beta]
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation

Alert API (deprecated)

.alert(item: $alertDetails) { details in
Alert(
title: Text("Title"),
message: Text("""
Imported \(details.name) \n
Filetype: \(details.fileType).
"""),
dismissButton: .default(Text("Dismiss"))
)
}

23.
[beta]
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation

Alert API (iOS 15~)

.alert(
"Alertが表示されました",
isPresented: $isPresentedAlert,
actions: {
Button("OK", action: {})
}
)

24.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation NavigationView・Old NavigationLink (deprecated) NavigationView { List(model.notes) { note in NavigationLink( note.title, destination: NoteEditor(id: note.id) ) } }

25.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation NavigationView・Old NavigationLink (deprecated) NavigationView { List(model.notes) { note in NavigationLink( note.title, destination: NoteEditor(id: note.id) ) } }

26.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation NavigationView・Old NavigationLink (deprecated) NavigationView { List(model.notes) { note in NavigationLink( note.title, // 遷移元の View に表示される title destination: NoteEditor(id: note.id) // 遷移後の画面 ) } }

27.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation Simple NavigationStack・New NavigationLink NavigationStack { List(parks) { park in NavigationLink( park.name, value: park ) } .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } }

28.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation Simple NavigationStack・New NavigationLink NavigationStack { List(parks) { park in NavigationLink( park.name, value: park ) } .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } }

29.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation Simple NavigationStack・New NavigationLink NavigationStack { List(parks) { park in NavigationLink( park.name, // 遷移元の画面に表示する title value: park // 任意の型の value ) } .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } }

30.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation Simple NavigationStack・New NavigationLink NavigationStack { List(parks) { park in NavigationLink( park.name, value: park ) } // value の型に応じて navigationDestination が反応する .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } }

31.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation Binding NavigationStack・New NavigationLink @State private var presentedParks: [Park] = [] // ... NavigationStack(path: $presentedParks) { List(parks) { park in NavigationLink(park.name, value: park) } .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } }

32.

SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation Binding NavigationStack・New NavigationLink @State private var presentedParks: [Park] = [] // ... NavigationStack(path: $presentedParks) { List(parks) { park in NavigationLink(park.name, value: park) } .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } }

33.
[beta]
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation

Binding NavigationStack・New NavigationLink

@State private var presentedParks: [Park] = []
// ...
NavigationStack(path: $presentedParks) {
// ...
}
func showParks() {
// RootView -> Park("Yosemite") -> Park("Sequoia")
presentedParks = [Park("Yosemite"), Park("Sequoia")]
}
※ iOS 16 の API についての FB: https://gist.github.com/mbrandonw/f8b94957031160336cac6898a919cbb7

34.

SwiftUI Navigation の俯瞰 Fire and forget / State driven Navigation API

35.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Navigation API は大きく 2 つに分類できる • 「swiftui-navigation」README から引用 • Fire and forget Navigation API • State driven Navigation API 35

36.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Navigation API は大きく 2 つに分類できる • Fire and forget Navigation API • State driven Navigation API 36

37.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Fire and forget Navigation API • Binding 引数を取らない • SwiftUI が Navigation の状態を完全に内部で管理する • Navigation を素早く実現できるが、Navigation に対して プログラム的な制御ができなくなる 37

38.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Fire and forget API - Tab - TabView { ReceivedView() .tabItem { // ... } SentView() .tabItem { // ... } AccountView() .tabItem { // ... } }

39.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Fire and forget API - Tab - // Binding 引数を取らない。タップでしか navigation できない TabView { ReceivedView() .tabItem { // ... } SentView() .tabItem { // ... } AccountView() .tabItem { // ... } }

40.
[beta]
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API

Fire and forget API - NavigationLink -

NavigationView {
List(model.notes) { note in
NavigationLink(
note.title,
destination: NoteEditor(id: note.id)
)
}
Text("Select a Note")
}

41.
[beta]
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API

Fire and forget API - NavigationLink -

NavigationView {
List(model.notes) { note in
// Binding 引数を取らない。タップでしか navigation できない
NavigationLink(
note.title,
destination: NoteEditor(id: note.id)
)
}
Text("Select a Note")
}

42.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Navigation API は大きく 2 つに分類できる • Fire and forget Navigation API • State driven Navigation API 42

43.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API State driven Navigation API • Binding 引数を取る • Navigation を有効・無効にする時にドメインの状態も変更できる • Fire and forget API より複雑ではあるが、プログラム的な制御が 可能となるため、使い方によっては Deep Link なども実現できる 43

44.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API State driven API - Tab - struct ContentView: View { @State var selectedTab: Tab = .received var body: some View { TabView(selection: $selectedTab) { ReceivedView().tabItem { ... } .tag(Tab.received) SentView().tabItem { ... } .tag(Tab.sent) AccountView().tabItem { ... } .tag(Tab.account) } } }

45.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API State driven API - Tab - struct ContentView: View { @State var selectedTab: Tab = .received var body: some View { TabView(selection: $selectedTab) { ReceivedView().tabItem { ... } .tag(Tab.received) SentView().tabItem { ... } .tag(Tab.sent) AccountView().tabItem { ... } .tag(Tab.account) } } }

46.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API State driven API - Tab - struct ContentView: View { @State var selectedTab: Tab = .sent var body: some View { TabView(selection: $selectedTab) { ReceivedView().tabItem { ... } .tag(Tab.received) SentView().tabItem { ... } .tag(Tab.sent) AccountView().tabItem { ... } .tag(Tab.account) } } }

47.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API 異なる View で包むと Deep Link できない struct ContainerView: View { var body: some View { ContentView(selectedTab: .received) } }

48.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API ContainerView に状態を持たせて解決する? struct ContainerView: View { let selectedTab: Tab var body: some View { ContentView(selectedTab: selectedTab) } }

49.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API ContainerView に状態を持たせて解決する? struct ContainerView: View { // ContainerView に関係ない Tab の状態を持たせるのは微妙 // しかも ContentView の selectedTab は @State なので誤り let selectedTab: Tab var body: some View { ContentView(selectedTab: selectedTab) } }

50.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API アプリの状態を管理する ViewModel を導入し 解決する class AppViewModel: ObservableObject { @Published var selectedTab: Tab init(selectedTab: Tab = .received) { self.selectedTab = selectedTab } }

51.

SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API アプリの状態を管理する ViewModel を導入し 解決する struct ContentView: View { @ObservedObject var viewModel: AppViewModel var body: some View { TabView(selection: $viewModel.selectedTab) { // ... } } } // ContentView の initialize 時に Tab を操作できるようになった ContentView(viewModel: .init(selectedTab: .two))

52.

SwiftUI の Navigation API における課題

53.

SwiftUI の Navigation API における課題 Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える

54.

SwiftUI の Navigation API における課題 > 遷移先の画面に Binding value を渡す機構を備えていない Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える

55.

SwiftUI の Navigation API における課題 > 遷移先の画面に Binding value を渡す機構を備えていない 遷移先の画面に Binding value を渡す機構を 備えていない struct ContentView: View { @State var draft: Post? var body: some View { Button("Edit") { draft = Post() } .sheet(item: $draft) { (draft: Post) in EditPostView(post: draft) } } } struct EditPostView: View { let post: Post var body: some View { ... } }

56.

SwiftUI の Navigation API における課題 > 遷移先の画面に Binding value を渡す機構を備えていない 遷移先の画面に Binding value を渡す機構を 備えていない struct ContentView: View { @State var draft: Post? var body: some View { Button("Edit") { draft = Post() } .sheet(item: $draft) { (draft: Post) in EditPostView(post: draft) } } } struct EditPostView: View { let post: Post var body: some View { ... } }

57.

Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない 遷移先の画面に Binding value を渡す機構を 備えていない struct ContentView: View { @State var draft: Post? var body: some View { Button("Edit") { draft = Post() } Binding Valueを EditPostViewに 渡せない .sheet(item: $draft) { (draft: Post) in EditPostView(post: draft) } } } struct EditPostView: View { let post: Post var body: some View { ... } } EditPostViewでの 変更がContentView に反映されない

58.

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える

59.

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している Deprecated / New Alert API // Deprecated API // New API // Binding<Bool> を受け取る // Binding<Bool> を受け取る func alert( isPresented: Binding<Bool>, content: () -> Alert ) -> some View func alert<A>( _ title: Text, isPresented: Binding<Bool>, actions: () -> A ) -> some View where A : View // Binding<Item?> を受け取る func alert<Item>( item: Binding<Item?>, content: (Item) -> Alert ) -> some View where Item : Identifiable // Binding<Bool> と data を受け取る func alert<A, T>( _ title: Text, isPresented: Binding<Bool>, presenting data: T?, actions: (T) -> A ) -> some View where A : View

60.

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している Deprecated / New Alert API // Deprecated API // New API // Binding<Bool> を受け取る // Binding<Bool> を受け取る func alert( isPresented: Binding<Bool>, content: () -> Alert ) -> some View func alert<A>( _ title: Text, isPresented: Binding<Bool>, actions: () -> A ) -> some View where A : View // Binding<Item?> を受け取る func alert<Item>( item: Binding<Item?>, content: (Item) -> Alert ) -> some View where Item : Identifiable // Binding<Bool> と data を受け取る func alert<A, T>( _ title: Text, isPresented: Binding<Bool>, presenting data: T?, actions: (T) -> A ) -> some View where A : View

61.

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している Deprecated boolean binding API struct ExampleView: View { @State private var showAlert = false var body: some View { BaseView(...) .alert(isPresented: $showAlert) { Alert(...) } } }

62.

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している Deprecated / New Alert API // Deprecated API // New API // Binding<Bool> を受け取る // Binding<Bool> を受け取る func alert( isPresented: Binding<Bool>, content: () -> Alert ) -> some View func alert<A>( _ title: Text, isPresented: Binding<Bool>, actions: () -> A ) -> some View where A : View // Binding<Item?> を受け取る func alert<Item>( item: Binding<Item?>, content: (Item) -> Alert ) -> some View where Item : Identifiable // Binding<Bool> と data を受け取る func alert<A, T>( _ title: Text, isPresented: Binding<Bool>, presenting data: T?, actions: (T) -> A ) -> some View where A : View

63.

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している Deprecated optional item binding API struct ExampleView: View { @State private var item: Item? var body: some View { BaseView(...) .alert(item: $item) { item in Alert(...) // item を利用して alert をカスタマイズできる } } }

64.

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している Deprecated / New Alert API // Deprecated API // New API // Binding<Bool> を受け取る // Binding<Bool> を受け取る func alert( isPresented: Binding<Bool>, content: () -> Alert ) -> some View func alert<A>( _ title: Text, isPresented: Binding<Bool>, actions: () -> A ) -> some View where A : View // Binding<Item?> を受け取る func alert<Item>( item: Binding<Item?>, content: (Item) -> Alert ) -> some View where Item : Identifiable // Binding<Bool> と data を受け取る func alert<A, T>( _ title: Text, isPresented: Binding<Bool>, presenting data: T?, actions: (T) -> A ) -> some View where A : View

65.
[beta]
SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している

New boolean binding API

struct ExampleView: View {
@State private var showAlert = false
var body: some View {
BaseView(...)
.alert(
"Alert title",
isPresented: $showAlert,
action: { ... }
)
}
}

66.

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している Deprecated / New Alert API // Deprecated API // New API // Binding<Bool> を受け取る // Binding<Bool> を受け取る func alert( isPresented: Binding<Bool>, content: () -> Alert ) -> some View func alert<A>( _ title: Text, isPresented: Binding<Bool>, actions: () -> A ) -> some View where A : View // Binding<Item?> を受け取る func alert<Item>( item: Binding<Item?>, content: (Item) -> Alert ) -> some View where Item : Identifiable // Binding<Bool> と data を受け取る func alert<A, T>( _ title: Text, isPresented: Binding<Bool>, presenting data: T?, actions: (T) -> A ) -> some View where A : View

67.
[beta]
SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している

New boolean binding and optional item API

struct ExampleView: View {
@State private var showAlert = false
@State private var item: Item?
var body: some View {
BaseView(...)
.alert(
"Alert title",
isPresented: $showAlert,
presenting: item
action: { item in ... },
message: { item in ... }
)
}
}

68.
[beta]
􀁣

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している

New boolean binding and optional item API

struct ExampleView: View {

@State private var showAlert = false
@State private var item: Item?
var body: some View {
BaseView(...)

.alert(
"Alert title",
isPresented: $showAlert,
presenting: item
action: { item in ... },
message: { item in ... }
)
}
}

showAlert: true
item: non null

69.
[beta]
􀁡

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している

New boolean binding and optional item API

struct ExampleView: View {

@State private var showAlert = false
@State private var item: Item?
var body: some View {
BaseView(...)

.alert(
"Alert title",
isPresented: $showAlert,
presenting: item
action: { item in ... },
message: { item in ... }
)
}
}

showAlert: true
item: null

70.

SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している New boolean binding and optional item API は 状態管理を複雑にさせてしまう • 開発者が以下の状態の整合性を気にしなければいけないのが辛い • Alert を表示するための boolean binding • Alert の内容を作るための optional item • Navigation がシンプルなうちはそこまで困らないかもしれないが、 Navigation が増えれば容易に状態管理も複雑になってしまう

71.

SwiftUI の Navigation API における課題 > 複数の Navigation を管理しだすと無駄な状態が増える Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える

72.

SwiftUI の Navigation API における課題 > 複数の Navigation を管理しだすと無駄な状態が増える 複数の Navigation を管理しだすと 無駄な状態が増える struct ContentView: View { @State var draft: Post? @State var settings: Settings? @State var userProfile: Profile? var body: some View { BaseView(...) .sheet(item: self.$draft) { (draft: Post) in EditPostView(post: draft) } .sheet(item: self.$settings) { (settings: Settings) in SettingsView(settings: settings) } .sheet(item: self.$userProfile) { (userProfile: Profile) in UserProfile(profile: userProfile) } } }

73.

SwiftUI の Navigation API における課題 > 複数の Navigation を管理しだすと無駄な状態が増える 複数の Navigation を管理しだすと 無駄な状態が増える struct ContentView: View { @State var draft: Post? @State var settings: Settings? @State var userProfile: Profile? var body: some View { BaseView(...) .sheet(item: self.$draft) { (draft: Post) in EditPostView(post: draft) } .sheet(item: self.$settings) { (settings: Settings) in SettingsView(settings: settings) } .sheet(item: self.$userProfile) { (userProfile: Profile) in UserProfile(profile: userProfile) } } }

74.

SwiftUI の Navigation API における課題 > 複数の Navigation を管理しだすと無駄な状態が増える 複数の Navigation を管理しだすと 無駄な状態が増える struct ContentView: View { @State var draft: Post? // null + non null = 2 @State var settings: Settings? // null + non null = 2 @State var userProfile: Profile? // null + non null = 2 var body: some View { BaseView(...) .sheet(item: self.$draft) { (draft: Post) in EditPostView(post: draft) } .sheet(item: self.$settings) { (settings: Settings) in SettingsView(settings: settings) } .sheet(item: self.$userProfile) { (userProfile: Profile) in UserProfile(profile: userProfile) } } }

75.

SwiftUI の Navigation API における課題 > 複数の Navigation を管理しだすと無駄な状態が増える fi 様々なパターンの状態が発生してしまう Draft Settings Pro le パターン1 Null Null Null パターン2 Non null Null Null パターン3 Null Non null Null パターン4 Null Null Non null パターン5 Non null Non null Null パターン6 Non null Null Non null パターン7 Null Non null Non null パターン8 Non null Non null Non null

76.

SwiftUI の Navigation API における課題 > 複数の Navigation を管理しだすと無駄な状態が増える fi 様々なパターンの状態が発生してしまう Draft Settings Pro le パターン1 Null Null Null パターン2 Non null Null Null パターン3 Null Non null Null パターン4 Null Null Non null パターン5 Non null Non null Null パターン6 Non null Null Non null パターン7 Null Non null Non null パターン8 Non null Non null Non null 有効な状態はこれらのみ

77.

Navigation API の課題に どう立ち向かうか

78.

Navigation API の課題にどう立ち向かうか Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える

79.

Navigation API の課題にどう立ち向かうか 「swiftui-navigation 」のアプローチ 1. 遷移先の画面に Binding value を渡す機構を備えていない • Binding value を渡すことができる API overloads 2. 状態管理を複雑にさせる API が存在している • 最低限の状態のみを要求する API overloads 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation を enum で管理できるようにする API overloads

80.

Navigation API の課題にどう立ち向かうか swiftui-navigation とは? • SwiftUI の Navigation に focus した Point-Free 製のライブラリ • SwiftUI の Navigation を利用する際の様々な課題を解決するために以下の 3 つのツールを提供している • Navigation API overloads • この後話します • Navigation views ( `IfLet`, `IfCaseLet`, `Switch`/`CaseLet` ) • 名前の通りの View 群。Binding value を扱えるように設計されている • Binding transformations • 独自の Navigation を作る際に有用なツール

81.

Navigation API の課題にどう立ち向かうか 「swiftui-navigation 」のアプローチ 1. 遷移先の画面に Binding value を渡す機構を備えていない • Binding value を渡すことができる API overloads 2. 状態管理を複雑にさせる API が存在している • 最低限の状態のみを要求する API overloads 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation を enum で管理できるようにする API overloads

82.

Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads 「swiftui-navigation 」のアプローチ 1. 遷移先の画面に Binding value を渡す機構を備えていない • Binding value を渡すことができる API overloads 2. 状態管理を複雑にさせる API が存在している • 最低限の状態のみを要求する API overloads 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation を enum で管理できるようにする API overloads

83.

Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads 遷移先の画面に Binding value を渡す機構を 備えていない struct ContentView: View { @State var draft: Post? var body: some View { Button("Edit") { draft = Post() } .sheet(item: $draft) { (draft: Post) in EditPostView(post: draft) } } } struct EditPostView: View { let post: Post var body: some View { ... } }

84.

Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads 遷移先の画面に Binding value を渡す機構を 備えていない struct ContentView: View { @State var draft: Post? var body: some View { Button("Edit") { draft = Post() } Binding Valueを EditPostViewに 渡せない .sheet(item: $draft) { (draft: Post) in EditPostView(post: draft) } } } struct EditPostView: View { let post: Post var body: some View { ... } } EditPostViewでの 変更がContentView に反映されない

85.

Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads Binding Value を渡せる sheet API を考える struct ContentView: View { @State var draft: Post? var body: some View { Button("Edit") { draft = Post() } .sheet(item: $draft) { (draft: Post) in EditPostView(post: draft) } } } struct EditPostView: View { let post: Post var body: some View { ... } }

86.

Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads Binding Value を渡せる sheet API を考える struct ContentView: View { @State var draft: Post? var body: some View { Button("Edit") { draft = Post() } .sheet(item: $draft) { ($draft: Post) in // closure に Binding value を渡せるようになったら良さそう EditPostView(post: $draft) } } } struct EditPostView: View { @Binding var post: Post var body: some View { ... } }

87.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

Binding Value を渡せる sheet API を考える

extension View {
func sheet<Value, Content>(
unwrapping optionalValue: Binding<Value?>,
@ViewBuilder content: @escaping (Binding<Value>) -> Content
) -> some View where Value: Identifiable, Content: View {
self.sheet(item: optionalValue) { _ in
// Binding<Value?> を Binding<Value> に変換する
value = ???
content(value)
}
}
}

88.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

Binding Value を渡せる sheet API を考える

extension View {
func sheet<Value, Content>(
unwrapping optionalValue: Binding<Value?>,
@ViewBuilder content: @escaping (Binding<Value>) -> Content
) -> some View where Value: Identifiable, Content: View {
self.sheet(item: optionalValue) { _ in
// Binding<Value?> を Binding<Value> に変換する
value = ???
content(value)
}
}
}

89.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

Binding Value を渡せる sheet API を考える

extension View {
func sheet<Value, Content>(
unwrapping optionalValue: Binding<Value?>,
@ViewBuilder content: @escaping (Binding<Value>) -> Content
) -> some View where Value: Identifiable, Content: View {
self.sheet(item: optionalValue) { _ in
// Binding<Value?> を Binding<Value> に変換する
value = ???
content(value)
}
}
}

90.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

Binding Value を渡せる sheet API を考える

extension View {
func sheet<Value, Content>(
unwrapping optionalValue: Binding<Value?>,
@ViewBuilder content: @escaping (Binding<Value>) -> Content
) -> some View where Value: Identifiable, Content: View {
self.sheet(item: optionalValue) { _ in
// Binding<Value?> を Binding<Value> に変換する
value = ???
content(value)
}
}
}

91.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

Binding<Value?> を Binding<Value> に変換する

extension Binding {
init?(unwrapping binding: Binding<Value?>) {
guard let wrappedValue = binding.wrappedValue
else { return nil }
self.init(
get: { wrappedValue },
set: { binding.wrappedValue = $0 }
)
}
}
※ Binding failable initializer は標準で存在するが、バグがある: https://gist.github.com/stephencelis/3a232a1b718bab0ae1127ebd5fcf6f97

92.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

Binding<Value?> を Binding<Value> に変換する

extension Binding {
init?(unwrapping binding: Binding<Value?>) {
guard let wrappedValue = binding.wrappedValue
else { return nil }
self.init(
get: { wrappedValue },
set: { binding.wrappedValue = $0 }
)
}
}

93.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

Binding<Value?> を Binding<Value> に変換する

extension Binding {
init?(unwrapping binding: Binding<Value?>) {
guard let wrappedValue = binding.wrappedValue
else { return nil }
self.init(
get: { wrappedValue },
set: { binding.wrappedValue = $0 }
)
}
}

94.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

Binding<Value?> を Binding<Value> に変換する

extension Binding {
init?(unwrapping binding: Binding<Value?>) {
guard let wrappedValue = binding.wrappedValue
else { return nil }
self.init(
get: { wrappedValue },
set: { binding.wrappedValue = $0 }
)
}
}

95.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

作成した init?(unwrapping:) を適用する

extension View {
func sheet<Value, Content>(
unwrapping optionalValue: Binding<Value?>,
@ViewBuilder content: @escaping (Binding<Value>) -> Content
) -> some View where Value: Identifiable, Content: View {
self.sheet(item: optionalValue) { _ in
if let value = Binding(unwrapping: optionalValue) {
content(value)
}
}
}
}

96.
[beta]
Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads

作成した init?(unwrapping:) を適用する

extension View {
func sheet<Value, Content>(
unwrapping optionalValue: Binding<Value?>,
@ViewBuilder content: @escaping (Binding<Value>) -> Content
) -> some View where Value: Identifiable, Content: View {
self.sheet(item: optionalValue) { _ in
if let value = Binding(unwrapping: optionalValue) {
content(value)
}
}
}
}

97.

Navigation API の課題にどう立ち向かうか > 遷移先の画面に Binding value を渡す機構を備えていない > Binding value を渡すことができる API overloads Before / After Before struct ContentView: View { @State var draft: Post? var body: some View { Button("Edit") { draft = Post() } .sheet(item: $draft) { draft in EditPostView(post: draft) } } } After struct ContentView: View { @State var draft: Post? var body: some View { Button("Edit") { draft = Post() } .sheet(unwrapping: $draft) { $draft in EditPostView(post: $draft) } } }

98.

Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 最低限の状態のみを要求する API overloads 「swiftui-navigation 」のアプローチ 1. 遷移先の画面に Binding value を渡す機構を備えていない • Binding value を渡すことができる API overloads 2. 状態管理を複雑にさせる API が存在している • 最低限の状態のみを要求する API overloads 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation を enum で管理できるようにする API overloads

99.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 最低限の状態のみを要求する API overloads

状態管理を複雑にさせる API が存在している

struct ExampleView: View {
@State private var showAlert = false
@State private var item: Item?
var body: some View {
BaseView(...)
.alert(
"Alert title",
isPresented: $showAlert,
presenting: item
action: { item in ... },
message: { item in ... }
)
}
}

100.
[beta]
􀁣

Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 最低限の状態のみを要求する API overloads

状態管理を複雑にさせる API が存在している

struct ExampleView: View {

@State private var showAlert = false
@State private var item: Item?
var body: some View {
BaseView(...)

.alert(
"Alert title",
isPresented: $showAlert,
presenting: item
action: { item in ... },
message: { item in ... }
)
}
}

showAlert: true
item: non null

101.
[beta]
􀁡

Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 最低限の状態のみを要求する API overloads

状態管理を複雑にさせる API が存在している

struct ExampleView: View {

@State private var showAlert = false
@State private var item: Item?
var body: some View {
BaseView(...)

.alert(
"Alert title",
isPresented: $showAlert,
presenting: item
action: { item in ... },
message: { item in ... }
)
}
}

showAlert: true
item: null

102.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

なぜ状態管理を複雑にする API になっているのか

struct ExampleView: View {
@State private var showAlert = false
@State private var item: Item?
var body: some View {
BaseView(...)
.alert(
"Alert title",

isPresented: $showAlert, // Alert を表示するための Boolean
presenting: item // Alert の内容をカスタマイズするための optional item
action: { item in ... },
message: { item in ... }
)
}
}

103.

Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads 状態管理を複雑にさせない API はどんな形か struct ExampleView: View { @State private var showAlert = false @State private var item: Item? var body: some View { BaseView(...) .alert( title: { item in ... }, presenting: item // optional item のみで「alert の表示条件」「alertの内容」を決められる action: { item in ... }, message: { item in ... } ) } }

104.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Optional item のみを要求する API を考える

extension View {
func alert<A: View, M: View, T>(
title: (T) -> Text,
presenting data: Binding<T?>,
@ViewBuilder actions: @escaping (T) -> A,
@ViewBuilder message: @escaping (T) -> M
) -> some View {
self.alert(
???, // alert title
isPresented: ???,
presenting: data.wrappedValue,
actions: actions,
message: message
)
}
}

105.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Optional item のみを要求する API を考える

extension View {

func alert<A: View, M: View, T>(
title: (T) -> Text,
presenting data: Binding<T?>,
@ViewBuilder actions: @escaping (T) -> A,
@ViewBuilder message: @escaping (T) -> M
) -> some View {
self.alert(
???, // alert title
isPresented: ???,
presenting: data.wrappedValue,
actions: actions,
message: message
)
}
}

106.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Optional item のみを要求する API を考える

extension View {
func alert<A: View, M: View, T>(
title: (T) -> Text,
presenting data: Binding<T?>,
@ViewBuilder actions: @escaping (T) -> A,
@ViewBuilder message: @escaping (T) -> M
) -> some View {

self.alert(
???, // alert title
isPresented: ???,
presenting: data.wrappedValue,
actions: actions,
message: message
)
}
}

107.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Optional item のみを要求する API を考える

extension View {
func alert<A: View, M: View, T>(

title: (T) -> Text,
presenting data: Binding<T?>,
@ViewBuilder actions: @escaping (T) -> A,
@ViewBuilder message: @escaping (T) -> M
) -> some View {
self.alert(

data.wrappedValue.map(title) ?? Text(""),
isPresented: ???,
presenting: data.wrappedValue,
actions: actions,
message: message
)
}
}

108.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Optional item のみを要求する API を考える

extension View {
func alert<A: View, M: View, T>(
title: (T) -> Text,

presenting data: Binding<T?>,
@ViewBuilder actions: @escaping (T) -> A,
@ViewBuilder message: @escaping (T) -> M
) -> some View {
self.alert(
data.wrappedValue.map(title) ?? Text(""),
isPresented: ???,

presenting: data.wrappedValue,
actions: actions,
message: message
)
}
}

109.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Optional item のみを要求する API を考える

extension View {
func alert<A: View, M: View, T>(
title: (T) -> Text,

presenting data: Binding<T?>,
@ViewBuilder actions: @escaping (T) -> A,
@ViewBuilder message: @escaping (T) -> M
) -> some View {
self.alert(
data.wrappedValue.map(title) ?? Text(""),

isPresented: ???, // Binding<T?> を Binding<Bool> に変換できると良さそう
presenting: data.wrappedValue,
actions: actions,
message: message
)
}
}

110.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Binding<T?> を Binding<Bool> に変換する

extension Binding {
func isPresented<Wrapped>() -> Binding<Bool>
where Value == Wrapped? {
.init(
get: { self.wrappedValue != nil },
set: { isPresented in
if !isPresented {
self.wrappedValue = nil
}
}
)
}
}

111.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Binding<T?> を Binding<Bool> に変換する

extension Binding {
func isPresented<Wrapped>() -> Binding<Bool>
where Value == Wrapped? { // Binding<T?> を変換するためのものなので、optional に限定
.init(
get: { self.wrappedValue != nil },
set: { isPresented in
if !isPresented {
self.wrappedValue = nil
}
}
)
}
}

112.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Binding<T?> を Binding<Bool> に変換する

extension Binding {
func isPresented<Wrapped>() -> Binding<Bool>
where Value == Wrapped? {

.init(
get: { self.wrappedValue != nil },
set: { isPresented in
// isPresent が false = alert が閉じられるタイミング
// その際は Binding 自身を nil にすることで無効な状態の発生を防げる
if !isPresented {
self.wrappedValue = nil
}
}
)
}
}

113.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

作成した isPresented を適用する

extension View {
func alert<A: View, M: View, T>(
title: (T) -> Text,
presenting data: Binding<T?>,
@ViewBuilder actions: @escaping (T) -> A,
@ViewBuilder message: @escaping (T) -> M
) -> some View {
self.alert(
data.wrappedValue.map(title) ?? Text(""),

isPresented: data.isPresented(),
presenting: data.wrappedValue,
actions: actions,
message: message
)
}
}

114.
[beta]
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 状態管理を複雑にさせない API overloads

Before / After
Before

After

struct ExampleView: View {
@State private var showAlert = false
@State private var item: Item?

struct ExampleView: View {
@State private var item: Item?
var body: some View {
BaseView(...)
.alert(
Text($0.xxx),
presenting: item
action: { item in ... },
message: { item in ... }
)
}

var body: some View {
BaseView(...)
.alert(
"Alert title",
isPresented: $showAlert,
presenting: item
action: { item in ... },
message: { item in ... }
)
}
}

}

115.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads 「swiftui-navigation 」のアプローチ 1. 遷移先の画面に Binding value を渡す機構を備えていない • Binding value を渡すことができる API overloads 2. 状態管理を複雑にさせる API が存在している • 最低限の状態のみを要求する API overloads 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation を enum で管理できるようにする API overloads

116.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads 複数の Navigation を管理しだすと 無駄な状態が増える struct ContentView: View { @State var draft: Post? // null + non null = 2 @State var settings: Settings? // null + non null = 2 @State var userProfile: Profile? // null + non null = 2 var body: some View { BaseView(...) .sheet(item: self.$draft) { (draft: Post) in EditPostView(post: draft) } .sheet(item: self.$settings) { (settings: Settings) in SettingsView(settings: settings) } .sheet(item: self.$userProfile) { (userProfile: Profile) in UserProfile(profile: userProfile) } } }

117.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads fi 様々なパターンの状態が発生してしまう Draft Settings Pro le パターン1 Null Null Null パターン2 Non null Null Null パターン3 Null Non null Null パターン4 Null Null Non null パターン5 Non null Non null Null パターン6 Non null Null Non null パターン7 Null Non null Non null パターン8 Non null Non null Non null

118.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads fi 様々なパターンの状態が発生してしまう Draft Settings Pro le パターン1 Null Null Null パターン2 Non null Null Null パターン3 Null Non null Null パターン4 Null Null Non null パターン5 Non null Non null Null パターン6 Non null Null Non null パターン7 Null Non null Non null パターン8 Non null Non null Non null 有効な状態はこれらのみ

119.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads 複数の Navigation の最適なモデリングを考える // Navigation は同時に複数発生することがないため、もっと良いモデリングができそう @State var draft: Post? @State var settings: Settings? @State var userProfile: Profile?

120.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads 複数の Navigation の最適なモデリングを考える // 同時に複数発生しない = enum を使える @State var draft: Post? @State var settings: Settings? @State var userProfile: Profile? " enum Route { case draft(Post) case settings(Settings) case userProfile(Profile) }

121.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum で sheet などを表示しようとすると?

struct ExampleView: View {
@State private var route: Route?
// ...
.sheet(
item: Binding<Post?>(
get: {
if case let .draft(post) = route
return post
} else { return nil }
},
set: { post
if let post = post {
route = .draft(post)
}
}
)
) { post in /* ... */ }
}

122.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum で sheet などを表示しようとすると?

struct ExampleView: View {

@State private var route: Route?
// ...
.sheet(
item: Binding<Post?>(
get: {
if case let .draft(post) = route
return post
} else { return nil }
},
set: { post
if let post = post {
route = .draft(post)
}
}
)
) { post in /* ... */ }
}

123.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum で sheet などを表示しようとすると?

struct ExampleView: View {
@State private var route: Route?
// ...

.sheet(
item: Binding<Post?>(
get: {
if case let .draft(post) = route
return post
} else { return nil }
},
set: { post
if let post = post {
route = .draft(post)
}
}
)
) { post in /* ... */ }
}

124.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads enum でも扱いやすい sheet API を考える struct ExampleView: View { @State private var route: Route? // ... .sheet( unwrapping: $route, case: ???Route.draft??? ) { $post in // ... } }

125.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads enum でも扱いやすい sheet API を考える struct ExampleView: View { @State private var route: Route? // ... .sheet( unwrapping: $route, // enum 自体を渡す (Binding value) case: ???Route.draft??? ) { $post in // ... } }

126.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads enum でも扱いやすい sheet API を考える struct ExampleView: View { @State private var route: Route? // ... .sheet( unwrapping: $route, case: ???Route.draft??? // 何らかの形で enum の特定の case を指定する ) { $post in // ... } }

127.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads enum でも扱いやすい sheet API を考える struct ExampleView: View { @State private var route: Route? // ... .sheet( unwrapping: $route, case: ???Route.draft??? ) { $post in // Associated value を binding value で渡せるとなお良い // ... } }

128.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View<Content> {
func sheet(
unwrapping: ???, // enum の binding
case: ???, // enum の特定の case
@ViewBuilder content: @escaping (Binding<???>) -> Content
) -> some View where Content: View {
// ...
}
}

129.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View<Content> {
func sheet(
unwrapping: ???, // enum の binding
case: ???, // enum の特定の case
@ViewBuilder content: @escaping (Binding<???>) -> Content
) -> some View where Content: View {
// ...
}
}

130.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View<Enum, Content> {
func sheet(
unwrapping: Binding<Enum?>, // enum の binding
case: ???, // enum の特定の case
@ViewBuilder content: @escaping (Binding<???>) -> Content
) -> some View where Content: View {
// ...
}
}

131.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View<Enum, Content> {
func sheet(
unwrapping: Binding<Enum?>, // enum の binding
case: ???, // enum の特定の case
@ViewBuilder content: @escaping (Binding<???>) -> Content
) -> some View where Content: View {
// ...
}
}

132.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View<Enum, Case, Content> {
func sheet(
unwrapping: Binding<Enum?>, // enum の binding
case: CasePath<Enum, Case>, // enum の特定の case
@ViewBuilder content: @escaping (Binding<???>) -> Content
) -> some View where Content: View {
// ...
}
}

133.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View<Enum, Case, Content> {
func sheet(
unwrapping: Binding<Enum?>, // enum の binding
case: CasePath<Enum, Case>, // enum の特定の case
@ViewBuilder content: @escaping (Binding<???>) -> Content
[CasePaths]
) -> some View where Content: View {
• https://github.com/pointfreeco/swift-case-paths
// ...
• struct における Key Paths の enum 版のようなもの
}
}

• Key Paths は `\User.id` とするが、Case Paths は `/Kind.animal`

• Enum から「特定の Case の抽出」・「特定の Case への Associated value の
埋め込み」をしたい用途で利用している

134.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View<Enum, Case, Content> {
func sheet(
unwrapping: Binding<Enum?>, // enum の binding
case: CasePath<Enum, Case>, // enum の特定の case
@ViewBuilder content: @escaping (Binding<???>) -> Content
) -> some View where Content: View {
// ...
}
}

135.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View<Enum, Case, Content> {
func sheet(
unwrapping: Binding<Enum?>, // enum の binding
case: CasePath<Enum, Case>, // enum の特定の case
// AssociatedValue があればそれを利用できる (なければ Void) Binding
@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
// ...
}
}

136.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(
unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
self.sheet(
isPresented: ???
) {
???
}
}
}

137.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(
unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
self.sheet(
isPresented: ???
) {
???
}
}
}

138.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(

unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
self.sheet(

isPresented: `enum`.case(casePath).isPresented()
) {
???
}
}
}

139.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(

unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
self.sheet(

isPresented: `enum`.case(casePath).isPresented()
) {
???
}
}
}

case(casePath) は Case Paths を利用した function
Binding<Enum?> から Binding<Case?> を抽出する

140.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(

unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
self.sheet(

isPresented: `enum`.case(casePath).isPresented()
) {
???
}
}
}

case(casePath) は Case Paths を利用した function
Binding<Enum?> から Binding<Case?> を抽出する

`enum`.case(casePath)
"
Binding<Case?>

141.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(

unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,

`enum`.case(casePath)
"
Binding<Case?>

@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
self.sheet(

isPresented: `enum`.case(casePath).isPresented()
) {
???
}
}
}

case(casePath) は Case Paths を利用した function
Binding<Enum?> から Binding<Case?> を抽出する

`enum`.case(casePath)
.isPresented()
"
Binding<Bool>

142.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(
unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
self.sheet(
isPresented: `enum`.case(casePath).isPresented()

) {
???
}
}
}

143.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(

unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
self.sheet(
isPresented: `enum`.case(casePath).isPresented()

) {
Binding(
unwrapping: `enum`.case(casePath)
).map(content)
}
}
}

144.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(

unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder content: @escaping (Binding<Case>) -> Content
) -> some View where Content: View {
self.sheet(
isPresented: `enum`.case(casePath).isPresented()

) {
Binding(
unwrapping: `enum`.case(casePath)
).map(content)
}
}
}

`enum`.case(casePath)
"
Binding<Case?>

145.
[beta]
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads

enum でも扱いやすい sheet API を考える

extension View {
func sheet<Enum, Case, Content>(

unwrapping enum: Binding<Enum?>,
case casePath: CasePath<Enum, Case>,
@ViewBuilder content: @escaping (Binding<Case>) -> Content

`enum`.case(casePath)
"
Binding<Case?>

) -> some View where Content: View {
self.sheet(
isPresented: `enum`.case(casePath).isPresented()

) {
Binding(
unwrapping: `enum`.case(casePath)
).map(content)
}
}
}

Binding(unwrapping:)
"
Binding<Case>
"
map

146.

Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads Before / After Before struct ContentView: View { @State var draft: Post? @State var settings: Settings? @State var userProfile: Profile? var body: some View { BaseView(...) .sheet(item: self.$draft) { draft in EditPostView(post: draft) } .sheet(item: self.$settings) { settings in SettingsView(settings: settings) } .sheet(item: self.$userProfile) { userProfile in UserProfile(profile: userProfile) } } } After struct ContentView: View { enum Route { // ... } @State var route: Route? var body: some View { BaseView(...) .sheet( unwrapping: $route, case: /Route.draft ) { $draft in EditPostView(post: $draft) } // ... } }

147.

Navigation API の課題にどう立ち向かうか Navigation API の改善による効果 • SwiftUI の Navigation API における 課題を API overloads という シンプルな方法で解決できた (ライブラリの実装はもっと洗練されています) 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation API がより安全で扱いやすくなり、Navigation に関わる状態の管理も 容易になる

148.

まとめ

149.

まとめ • SwiftUI には様々な Navigation API があり、OS と共に激しい変化が あった • 大きく Navigation API は以下の 2 種類に区別できる • Fire and forget API • State driven API

150.

まとめ • SwiftUI の Navigation API には以下のような課題があり、API を適切に overload して解決する 「swiftui-navigation」のようなアプローチも存在している • 遷移先の画面に Binding value を渡す機構を備えていない • 状態管理を複雑にさせる API が存在している • 複数の Navigation を管理しだすと無駄な状態が増える • SwiftUI の Navigation を正しく理解して管理すれば色々な恩恵がある • Navigation に関わる状態管理に悩まされない • Deep Link が容易になる・Xcode Previews も活用しやすくなる

151.

参考 • https://www.pointfree.co/collections/swiftui/navigation • https://gist.github.com/mbrandonw/f8b94957031160336cac6898a919cbb7 • https://github.com/pointfreeco/swift-composable-architecture/discussions/1140 • https://developer.apple.com/documentation/swiftui • https://github.com/pointfreeco/swiftui-navigation • https://swiftwithmajid.com/2022/06/15/mastering-navigationstack-in-swiftui-navigatorpattern/ • https://swiftwithmajid.com/2022/06/21/mastering-navigationstack-in-swiftui-deeplinking/

152.

After Party iOSDC ぜひご参加ください!