Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
redacted を TCA でスマートに扱う
Search
Aikawa
October 24, 2020
Programming
1k
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
redacted を TCA でスマートに扱う
Aikawa
October 24, 2020
More Decks by Aikawa
See All by Aikawa
DocC Tutorial と TCA におけるテスト機能の紹介
kalupas226
1
1.8k
Swift愛好会WWDC要約会 Build programmatic UI with Xcode Previews
kalupas226
2
900
enum で KeyPaths のような機能を実現する CasePaths
kalupas226
4
1k
SwiftUI Navigation のすべて
kalupas226
11
9.7k
Refreshable API を TCA で使う
kalupas226
0
310
Combineを使ったコードのテストをSchedulerで操る方法とその仕組み
kalupas226
2
1.9k
Composable FormsでTCAのボイラープレートとおさらばする
kalupas226
1
1.4k
Swiftの関数と代数学
kalupas226
0
900
Swiftのstruct・enumと代数学_part1
kalupas226
2
1.7k
Other Decks in Programming
See All in Programming
一人だけ、Kiroが静止する日
hideg
0
130
Security issues being discussed on Web Platforms
petamoriken
0
1.3k
テストを司るデーモンに会いに行く 〜隔離した仮想マシンでテストを通すまで〜
h1d3mun3
1
590
JPUG勉強会 OSSデータベースの内部構造を理解しよう(第2回)
oga5
0
280
カツオ、ご期待ください
suneo3476
0
110
JRuby: Past, Present, and Future
headius
0
210
[GoCon2026] When Goroutines Are Not Enough: Runtime Locality in High-Throughput Go
takehaya
6
2.6k
Heart of Swift Concurrency
koher
0
1.1k
標準パッケージに uuid が追加された 背景から見る Go らしい意思決定 / go_127_uuid_decision
convto
5
9k
setup-vp GitLab対応の裏側
naokihaba
0
140
世界の中心で、AI(App Intents)をさけぶ ー App Intents中心設計の実践ガイド
touyou
0
710
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
340
Featured
See All Featured
Designing Powerful Visuals for Engaging Learning
tmiket
1
580
Building Better People: How to give real-time feedback that sticks.
wjessup
370
20k
A brief & incomplete history of UX Design for the World Wide Web: 1989–2019
jct
2
510
Odyssey Design
rkendrick25
PRO
2
830
Why Your Marketing Sucks and What You Can Do About It - Sophie Logan
marketingsoph
0
410
Ten Tips & Tricks for a 🌱 transition
stuffmc
1
240
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.5k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.6k
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
120
120k
Unlocking the hidden potential of vector embeddings in international SEO
frankvandijk
0
960
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.4k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
12
1.3k
Transcript
redacted を TCA でスマートに扱う iOS アプリ開発のためのFunctional Architecture 情報共有会
⾃⼰紹介 アイカワ(@kalupas0930 ) 新卒 iOS エンジニア 函館出⾝ 最近は Flutter, 機械学習の勉強をしてます
SwiftUI と Combine もまだまだ勉強中です 2
redacted とは? SkeltetonView のようなもの SwiftUI で iOS14 から使⽤できる ViewModifier とても便利
3
4
SwiftUI でどう使う? Text("This is redacted") .redacted(reason: .placeholder) redacted Modifier を付けるだけ
5
ViewModifier ならではの使い⽅ VStack { Image("kuma") .resizable() .frame(width: 100, height: 100)
.clipShape(Circle()) Text("This is redacted") Text("kuma") } 6
ViewModifier ならではの使い⽅ ViewModifier なので簡単に Skeleton を 表現できる VStack { Image("kuma")
.resizable() .frame(width: 100, height: 100) .clipShape(Circle()) Text("This is redacted") Text("kuma") } .redacted(reason: .placeholder) 7
もう少しだけ redacted について深掘り redacted は以下のように定義されている func redacted(reason: RedactionReasons) -> some
View RedactionReasons は? 現時点では先ほど紹介した placeholder しか持っていない OptionSet に適合しているため、将来的には他の reason も 使えるようになるかもしれない ちなみに現時点でもオリジナルの ReactionReasons を作って、 reason を使い分けることはできる 8
状態管理含めた時の redacted を⾒ていきます まずは @ObservableObejct を利⽤した TCA を利⽤しない SwiftUI での使⽤⽅法を⾒ていく
単純なリスト表⽰をするだけのアプリを作る 9
10
扱う状態 struct Item: Equatable, Identifiable { let id: UUID let
title: String let description: String } 基本的な title と description を持っているだけ 11
プレースホルダー⽤の変数 let placeholderListItem = (0...10).map { _ in Item( id:
.init(), title: String(repeating: " ", count: .random(in: 50...100)), description: String(repeating: " ", count: .random(in: 10...30)) ) } title と description は適当にスペースで埋めてそれっぽくしている 12
ロード完了後⽤の変数 let liveListItem = [ Item(id: .init(), title: " これは
redacted", description: String(repeating: " おはよう", count: 10)), Item(id: .init(), title: "This is redacted", description: String(repeating: "Good morning", count: 10)), Item(id: .init(), title: " よろしくお願いします", description: String(repeating: "yes,yes", count: 10)) ] 中⾝は適当です 13
状態管理⽤の ObservableObject class ListItemViewModel: ObservableObject { @Published var listItem: [Item]
= [] @Published var isLoading = false init() { isLoading = true // 4s 経ったら⾃動的に動作するようにする DispatchQueue.main.asyncAfter(deadline: .now() + 4){ self.isLoading = false self.listItem = liveListItem } } } 14
View を少しずつ⾒ていきます @ObservedObject private var viewModel = ListItemViewModel() var body:
some View { List { if viewModel.isLoading { ActivityIndicator().frame(maxWidth: .infinity).padding() } ForEach( ... // 繰り返す Item Button(action: { ... // ボタンを押した時のアクション }) { ... // ボタンの View } } } 15
ForEach の中⾝ / ボタンのアクション @ObservedObject private var viewModel = ListItemViewModel()
... // 省略 ForEach( viewModel.isLoading ? placeholderListItem : viewModel.listItem) { item in Button(action: { guard !self.viewModel.isLoading else { return } print("Button was tapped") }) { ... // ボタンの View } } 16
ForEach の中⾝ / ボタンの View @ObservedObject private var viewModel =
ListItemViewModel() ... // 省略 ForEach( ... ) { item in Button(action: { ... }) { HStack(alignment: .top) { Image("kuma") .resizable() .frame(width: 80, height: 80) VStack(alignment: .leading, spacing: 10) { Text(item.title).font(.title2) Text(item.description).font(.body) } } } } 17
redacted を追加 @ObservedObject private var viewModel = ListItemViewModel() var body:
some View { List { if viewModel.isLoading { ActivityIndicator().frame(maxWidth: .infinity).padding() } ForEach( ... // 繰り返す Item Button(action: { ... // ボタンを押した時のアクション }) { ... // ボタンの View } .redacted(reason: viewModel.isLoading ? .placeholder: []) } } 18
disabled も追加 @ObservedObject private var viewModel = ListItemViewModel() var body:
some View { List { if viewModel.isLoading { ActivityIndicator().frame(maxWidth: .infinity).padding() } ForEach( ... // 繰り返す Item Button(action: { ... // ボタンを押した時のアクション }) { ... // ボタンの View } .redacted(reason: viewModel.isLoading ? .placeholder: []) .disabled(viewModel.isLoading) // これを追加 } 19
やりたいことは実現できた しかし、この⽅法には問題点がある View のあちこちで viewModel.isLoading を使っている 状態が増えてきた時に開発者が気にしなければならないことが 多くなってしまう disabled によってロード中はタップできないようにできたが、
disabled の利⽤シーンとしては微妙 もし onAppear などがあった際、それを防ぐことはできない ⾊が少し明るくなってしまうので、本来意図している View の⾊ とは異なるものになるかもしれない 20
The Composable Architecture なら? 基本的な TCA の流れ View から Action
を送る Action によって Reducer で Store の State が変更される イメージは isLoading によって Store を使い分ける isLoading が true ( ロード中) : プレースホルダー⽤の Store false ( ロード完了): 本物の Store 21
実際に TCA を使った例を紹介します まずは State struct ListItemState: Equatable { var
listItem: [Item] = [] var isLoading = false } 先ほどの @ObservableObject を利⽤した class と⼤きな差はない State は Action を通じてのみ変更されるため、struct 内に 状態を変化させるための関数はない 22
Action enum ListItemAction { case listItemResponse([Item]?) case onAppear } View
の onAppear 時に呼ばれる Action その Action によって発⽕する listItemResponse([Item]?) 23
Reducer let listItemReducer = Reducer<ListItemState, ListItemAction, Void> { state, action,
environment in switch action { case let .listItemResponse(listItem): state.isLoading = false state.listItem = listItem ?? [] return .none case .onAppear: state.isLoading = true return Effect(value: .listItemResponse(liveListItem)) .delay(for: 4, scheduler: DispatchQueue.main) .eraseToEffect() } } onAppear してから、わざと 4s 遅らせるようにして API 通信してる⾵ にしているだけ 24
View の全体像 let store: Store<ListItemState, ListItemAction> var body: some View
{ WithViewStore(store) { viewStore in List { if viewStore.isLoading { ActivityIndicator().padding().frame(maxWidth: .infinity) } ListItemView( // ( プレースホルダー store) or ( 本物 store) を渡す ) .redacted(reason: viewStore.isLoading ? .placeholder : []) } .onAppear { viewStore.send(.onAppear) } } } 25
ListItemView への Store の渡し⽅ ListItemView( store: viewStore.isLoading ? Store( initialState:
.init(listItem: placeholderListItem), reducer: .empty, environment: () ) : self.store ) .redacted(reason: viewStore.isLoading ? .placeholder : []) viewStore.isLoading によって以下を渡す true : placeholder ⽤ Store false : 本物の Store 26
⼀応 ListItemView の中⾝ let store: Store<ListItemState, ListItemAction> var body: some
View { WithViewStore(store) { viewStore in ForEach(viewStore.listItem) { item in // 本当は ForEachStore などを使うと良い Button(action: { // ここで viewStore.send() としても、placeholder store であれば // state に影響はないので、send し放題 }) { HStack(alignment: .top) { Image("kuma").resizable().frame(width: 80, height: 80) VStack(alignment: .leading, spacing: 10) { Text(item.title).font(.title2) Text(item.description).font(.body) } } } .buttonStyle(PlainButtonStyle()) } } 27
TCA と redacted を組み合わせれば 開発者は最初に isLoading の状態によって、Store を使い分ける という判断だけで良くなる disabled
を使⽤せずとも、「ローディング中のセルをタップ しても何も起きないようにする」という動作を実現できた 今回は扱う State を説明のために絞ったが、State が多くなれば なるほど TCA の恩恵を受けることができる 28
おわりに 今回発表した内容は Point-Free さんの Episode115~ の redacted についての記事を参考にしました https://www.pointfree.co/ 記事ではもっと深掘りされた内容が書かれています
扱う状態が増えた時の redacted の扱い⽅ 画⾯も⼀つではなく複数 コンテンツの多くは有料かつ動画は英語ですが、スクリプトが あるので英語が苦⼿でも翻訳頼りで理解できます 29