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
910
enum で KeyPaths のような機能を実現する CasePaths
kalupas226
4
1.1k
SwiftUI Navigation のすべて
kalupas226
11
9.8k
Refreshable API を TCA で使う
kalupas226
0
310
Combineを使ったコードのテストをSchedulerで操る方法とその仕組み
kalupas226
2
1.9k
Composable FormsでTCAのボイラープレートとおさらばする
kalupas226
1
1.4k
Swiftの関数と代数学
kalupas226
0
910
Swiftのstruct・enumと代数学_part1
kalupas226
2
1.7k
Other Decks in Programming
See All in Programming
GemmaをJevのように使ってみる / Use Gemma like Jev
kishida
5
820
Kotlinize Is Not Just Converting to Kotlin
chigichan24
1
180
Optunaを作ったときにやりたかったこと、その後 (2026/10/16, Optuna Meetup #3)
iwiwi
2
1.2k
【加筆修正版】Laravel のアプリケーションをどこにデプロイするか #phpcon_ehime
akase244
0
160
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
330
AI時代のTerminal環境を UNIX哲学で設計・運用する
yasunori0418
1
160
Herb in Rails 8.2: Your ERB views, now HTML-aware @ Kaigi on Rails 2026, Tokyo, Japan
marcoroth
2
310
ActionController meets Protocol Buffers
sorah
0
660
ChromeでローカルLLM
ryu1013
0
160
IBM Bob でAI駆動開発『QC-Playbook』
ichirokano2
0
120
Apple Intelligence を用いた個人情報誤送信防止、及びユーザーリクエスト体験の改善について
yukiny
0
360
Workers Cache を知る
syumai
0
370
Featured
See All Featured
GraphQLとの向き合い方2022年版
quramy
50
15k
Neural Spatial Audio Processing for Sound Field Analysis and Control
skoyamalab
0
570
Measuring & Analyzing Core Web Vitals
bluesmoon
9
1k
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
410
Raft: Consensus for Rubyists
vanstee
142
7.8k
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Claude Code どこまでも/ Claude Code Everywhere
nwiizo
68
59k
エンジニアに許された特別な時間の終わり
watany
109
250k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.6k
Skip the Path - Find Your Career Trail
mkilby
1
260
Paper Plane
katiecoart
PRO
5
54k
The agentic SEO stack - context over prompts
schlessera
0
970
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