60.7K Views
September 11, 22
スライド概要
iOSDC Japan 2022 で発表した内容です
iOS エンジニアをやっています。
SwiftUI Navigation のすべて iOSDC Japan 2022 kalupas226
自己紹介 • アイカワ • @kalupas226 • Cookpad Inc. iOS Developer 2
アジェンダ • SwiftUI Navigation の俯瞰 • 様々な種類 の Navigation • OS による変化が激しい Navigation • Fire and forget / State driven Navigation API • SwiftUI の Navigation API における課題 • Navigation API の課題にどう立ち向かうか • 「swiftui-navigation」のアプローチ • まとめ 3
本トークで話さないこと • 開発者がそれぞれで定義する独自の Custom Navigation • iOS 以外のプラットフォームにおける Navigation • アプリケーションにおける Navigation はこうあるべきというような デザイン的な話 4
本トークで主に参考にしている情報 • Point-Free 製の「swiftui-navigation」というライブラリ • https://github.com/pointfreeco/swiftui-navigation • 発表内では以下のように区別して説明します • SwiftUI Navigation API: Apple 純正の API についての話 • 「swiftui-navigation」: ライブラリについての話 5
SwiftUI Navigation の俯瞰
SwiftUI Navigation の俯瞰 様々な種類の Navigation
SwiftUI Navigation の俯瞰 > 様々な種類の Navigation 様々な種類の Navigation API が存在する • Tab • Alert • Sheet • Full Screen Cover • Con rmation Dialog • Popover • Navigation Link… fi 8
SwiftUI Navigation の俯瞰 > 様々な種類の Navigation Tab TabView { FirstView() .tabItem { Image(systemName: "exclamationmark.circle") Text("Alert") } SecondView() .tabItem {…} ThirdView() .tabItem {…} FourthView() .tabItem {…} }
SwiftUI Navigation の俯瞰 > 様々な種類の Navigation
Alert
BaseView()
.alert(
“Alertが表示されました”,
isPresented: $isPresentedAlert,
actions: {
Button("OK", action: {})
}
)
SwiftUI Navigation の俯瞰 > 様々な種類の Navigation
Sheet
BaseView()
.sheet(
isPresented: $isPresentedAlert,
content: {
Text("Sheetが表示されました")
}
)
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")
}
)
}
}
SwiftUI Navigation の俯瞰 OS による変化が激しい Navigation
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation OS が進化するにつれ API の形も変わってきた • ActionSheet API (deprecated) → Con rmation Dialog API • Alert API • deprecated になったものと、そうでないものがある • Navigation-base API • NavigationView → NavigationStack • NavigationLink fi 14
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation ActionSheet API (deprecated) .actionSheet(isPresented: $showActionSheet) { ActionSheet( title: Text(...), message: Text(...), buttons: [ .cancel(), .destructive( Text(...), action: {} ), .default( Text(...), action: {} ) ] ) }
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation ActionSheet API (deprecated) .actionSheet(isPresented: $showActionSheet) { ActionSheet( title: Text(...), message: Text(...), buttons: [ .cancel(), .destructive( Text(...), action: {} ), .default( Text(...), action: {} ) ] ) }
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation ActionSheet API (deprecated) .actionSheet(isPresented: $showActionSheet) { ActionSheet( title: Text(...), message: Text(...), buttons: [ .cancel(), .destructive( Text(...), action: {} ), .default( Text(...), action: {} ) ] ) }
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))
}
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))
}
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))
}
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation
Alert API (deprecated)
.alert(isPresented: $showAlert) {
Alert(
title: Text("Title"),
message: Text("Message")
)
}
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"))
)
}
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation
Alert API (iOS 15~)
.alert(
"Alertが表示されました",
isPresented: $isPresentedAlert,
actions: {
Button("OK", action: {})
}
)
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation NavigationView・Old NavigationLink (deprecated) NavigationView { List(model.notes) { note in NavigationLink( note.title, destination: NoteEditor(id: note.id) ) } }
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation NavigationView・Old NavigationLink (deprecated) NavigationView { List(model.notes) { note in NavigationLink( note.title, destination: NoteEditor(id: note.id) ) } }
SwiftUI Navigation の俯瞰 > OS による変化が激しい Navigation NavigationView・Old NavigationLink (deprecated) NavigationView { List(model.notes) { note in NavigationLink( note.title, // 遷移元の View に表示される title destination: NoteEditor(id: note.id) // 遷移後の画面 ) } }
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) } }
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) } }
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) } }
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) } }
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) } }
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) } }
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
SwiftUI Navigation の俯瞰 Fire and forget / State driven Navigation API
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
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Navigation API は大きく 2 つに分類できる • Fire and forget Navigation API • State driven Navigation API 36
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Fire and forget Navigation API • Binding 引数を取らない • SwiftUI が Navigation の状態を完全に内部で管理する • Navigation を素早く実現できるが、Navigation に対して プログラム的な制御ができなくなる 37
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Fire and forget API - Tab - TabView { ReceivedView() .tabItem { // ... } SentView() .tabItem { // ... } AccountView() .tabItem { // ... } }
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Fire and forget API - Tab - // Binding 引数を取らない。タップでしか navigation できない TabView { ReceivedView() .tabItem { // ... } SentView() .tabItem { // ... } AccountView() .tabItem { // ... } }
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")
}
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")
}
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API Navigation API は大きく 2 つに分類できる • Fire and forget Navigation API • State driven Navigation API 42
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API State driven Navigation API • Binding 引数を取る • Navigation を有効・無効にする時にドメインの状態も変更できる • Fire and forget API より複雑ではあるが、プログラム的な制御が 可能となるため、使い方によっては Deep Link なども実現できる 43
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) } } }
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) } } }
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) } } }
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API 異なる View で包むと Deep Link できない struct ContainerView: View { var body: some View { ContentView(selectedTab: .received) } }
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API ContainerView に状態を持たせて解決する? struct ContainerView: View { let selectedTab: Tab var body: some View { ContentView(selectedTab: selectedTab) } }
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) } }
SwiftUI Navigation の俯瞰 > Fire and forget / State driven Navigation API アプリの状態を管理する ViewModel を導入し 解決する class AppViewModel: ObservableObject { @Published var selectedTab: Tab init(selectedTab: Tab = .received) { self.selectedTab = selectedTab } }
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))
SwiftUI の Navigation API における課題
SwiftUI の Navigation API における課題 Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える
SwiftUI の Navigation API における課題 > 遷移先の画面に Binding value を渡す機構を備えていない Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える
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 { ... } }
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 { ... } }
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 に反映されない
SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える
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
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
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(...) } } }
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
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 をカスタマイズできる } } }
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
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: { ... }
)
}
}
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
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 ... }
)
}
}
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
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
SwiftUI の Navigation API における課題 > 状態管理を複雑にさせる API が存在している New boolean binding and optional item API は 状態管理を複雑にさせてしまう • 開発者が以下の状態の整合性を気にしなければいけないのが辛い • Alert を表示するための boolean binding • Alert の内容を作るための optional item • Navigation がシンプルなうちはそこまで困らないかもしれないが、 Navigation が増えれば容易に状態管理も複雑になってしまう
SwiftUI の Navigation API における課題 > 複数の Navigation を管理しだすと無駄な状態が増える Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える
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) } } }
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) } } }
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) } } }
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
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 有効な状態はこれらのみ
Navigation API の課題に どう立ち向かうか
Navigation API の課題にどう立ち向かうか Navigation API における課題 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える
Navigation API の課題にどう立ち向かうか 「swiftui-navigation 」のアプローチ 1. 遷移先の画面に Binding value を渡す機構を備えていない • Binding value を渡すことができる API overloads 2. 状態管理を複雑にさせる API が存在している • 最低限の状態のみを要求する API overloads 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation を enum で管理できるようにする API overloads
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 を作る際に有用なツール
Navigation API の課題にどう立ち向かうか 「swiftui-navigation 」のアプローチ 1. 遷移先の画面に Binding value を渡す機構を備えていない • Binding value を渡すことができる API overloads 2. 状態管理を複雑にさせる API が存在している • 最低限の状態のみを要求する API overloads 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation を enum で管理できるようにする API overloads
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
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 { ... } }
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 に反映されない
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 { ... } }
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 { ... } }
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)
}
}
}
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)
}
}
}
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)
}
}
}
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)
}
}
}
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
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 }
)
}
}
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 }
)
}
}
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 }
)
}
}
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)
}
}
}
}
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)
}
}
}
}
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) } } }
Navigation API の課題にどう立ち向かうか > 状態管理を複雑にさせる API が存在している > 最低限の状態のみを要求する API overloads 「swiftui-navigation 」のアプローチ 1. 遷移先の画面に Binding value を渡す機構を備えていない • Binding value を渡すことができる API overloads 2. 状態管理を複雑にさせる API が存在している • 最低限の状態のみを要求する API overloads 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation を enum で管理できるようにする API overloads
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 ... }
)
}
}
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
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
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 ... }
)
}
}
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 ... } ) } }
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
)
}
}
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
)
}
}
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
)
}
}
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
)
}
}
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
)
}
}
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
)
}
}
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
}
}
)
}
}
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
}
}
)
}
}
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
}
}
)
}
}
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
)
}
}
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 ... }
)
}
}
}
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
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) } } }
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
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 有効な状態はこれらのみ
Navigation API の課題にどう立ち向かうか > 複数の Navigation を管理しだすと無駄な状態が増える > Navigation を enum で管理できるようにする API overloads 複数の Navigation の最適なモデリングを考える // Navigation は同時に複数発生することがないため、もっと良いモデリングができそう @State var draft: Post? @State var settings: Settings? @State var userProfile: Profile?
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) }
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 /* ... */ }
}
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 /* ... */ }
}
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 /* ... */ }
}
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 // ... } }
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 // ... } }
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 // ... } }
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 で渡せるとなお良い // ... } }
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 {
// ...
}
}
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 {
// ...
}
}
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 {
// ...
}
}
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 {
// ...
}
}
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 {
// ...
}
}
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 の
埋め込み」をしたい用途で利用している
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 {
// ...
}
}
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 {
// ...
}
}
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: ???
) {
???
}
}
}
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: ???
) {
???
}
}
}
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()
) {
???
}
}
}
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?> を抽出する
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?>
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>
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()
) {
???
}
}
}
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)
}
}
}
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?>
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
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) } // ... } }
Navigation API の課題にどう立ち向かうか Navigation API の改善による効果 • SwiftUI の Navigation API における 課題を API overloads という シンプルな方法で解決できた (ライブラリの実装はもっと洗練されています) 1. 遷移先の画面に Binding value を渡す機構を備えていない 2. 状態管理を複雑にさせる API が存在している 3. 複数の Navigation を管理しだすと無駄な状態が増える • Navigation API がより安全で扱いやすくなり、Navigation に関わる状態の管理も 容易になる
まとめ
まとめ • SwiftUI には様々な Navigation API があり、OS と共に激しい変化が あった • 大きく Navigation API は以下の 2 種類に区別できる • Fire and forget API • State driven API
まとめ • SwiftUI の Navigation API には以下のような課題があり、API を適切に overload して解決する 「swiftui-navigation」のようなアプローチも存在している • 遷移先の画面に Binding value を渡す機構を備えていない • 状態管理を複雑にさせる API が存在している • 複数の Navigation を管理しだすと無駄な状態が増える • SwiftUI の Navigation を正しく理解して管理すれば色々な恩恵がある • Navigation に関わる状態管理に悩まされない • Deep Link が容易になる・Xcode Previews も活用しやすくなる
参考 • 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/
After Party iOSDC ぜひご参加ください!