Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
redacted を TCA でスマートに扱う
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
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.7k
Swift愛好会WWDC要約会 Build programmatic UI with Xcode Previews
kalupas226
2
890
enum で KeyPaths のような機能を実現する CasePaths
kalupas226
4
1k
SwiftUI Navigation のすべて
kalupas226
11
9.4k
Refreshable API を TCA で使う
kalupas226
0
300
Combineを使ったコードのテストをSchedulerで操る方法とその仕組み
kalupas226
2
1.9k
Composable FormsでTCAのボイラープレートとおさらばする
kalupas226
1
1.3k
Swiftの関数と代数学
kalupas226
0
880
Swiftのstruct・enumと代数学_part1
kalupas226
2
1.6k
Other Decks in Programming
See All in Programming
IBM Bobを活用したレガシーアプリの最新化
oniak3ibm
PRO
1
230
エンジニアと一緒にテストコードの設計と実装を改善した話
mototakatsu
0
240
自作OSでスライド発表する
uyuki234
0
3.4k
Hunting Vulnerabilities in Symfony with LLMs
vinceamstoutz
0
570
Observability in Practice:Grafana 與 Edge Device SRE 的那些事
blueswen
0
180
作って学ぶ、 JSX (TSX) ランタイムの基本
syumai
7
1.7k
A2UI という光を覗いてみる
satohjohn
1
170
セキュリティの専門家じゃなくてもできる。「セキュリティ意識」をアップデートして サプライチェーン攻撃への耐性を高めよう。
tk3fftk
5
1k
Semantic Version 単位で戦略を柔軟に変えて、パッケージアップデートを自動化する
daitasu
1
320
例外の正しい扱い方 そのエラー try-catchして大丈夫?
jinwatanabe
0
310
Creating Composable Callables in Contemporary C++
rollbear
0
180
そのテスト、説明できますか?~LWテスト戦略FW~のご紹介
nakahara
0
180
Featured
See All Featured
Ruling the World: When Life Gets Gamed
codingconduct
0
270
Reflections from 52 weeks, 52 projects
jeffersonlam
356
21k
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
220
YesSQL, Process and Tooling at Scale
rocio
174
15k
StorybookのUI Testing Handbookを読んだ
zakiyama
31
6.8k
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
1
300
DevOps and Value Stream Thinking: Enabling flow, efficiency and business value
helenjbeal
1
250
How to build a perfect <img>
jonoalderson
1
5.8k
Hiding What from Whom? A Critical Review of the History of Programming languages for Music
tomoyanonymous
2
880
Rails Girls Zürich Keynote
gr2m
96
14k
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
380
Impact Scores and Hybrid Strategies: The future of link building
tamaranovitovic
0
320
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