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
SwiftUIで使いやすいToastの作り方 / How to build a Toast s...
Search
Elvis Shi
April 17, 2024
Programming
1.3k
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
SwiftUIで使いやすいToastの作り方 / How to build a Toast system which is easy to use in SwiftUI
Elvis Shi
April 17, 2024
More Decks by Elvis Shi
See All by Elvis Shi
為什麼你並不需要ViewModel / No, you don't need a ViewModel
lovee
1
490
@Environment(\.keyPath)那么好我不允许你们不知道! / atEnvironment keyPath is so good and you should know it!
lovee
0
520
ゼロから始めるPreferenceの実装 / Let's implement Preferences from scratch
lovee
0
150
Kotlin エンジニアへ送る:Swift 案件に参加させられる日に備えて~似てるけど色々違う Swift の仕様 / from Kotlin to Swift
lovee
1
400
個人アプリを2年ぶりにアプデしたから褒めて / I just updated my personal app, praise me!
lovee
0
760
How did I build an Open-Source SwiftUI Toast Library
lovee
1
180
SwiftUIで二重スクロール作ってみた / When I tried to make a dual-scroll-ish view in SwiftUI
lovee
1
390
Observation のあれこれ / A brief introduction about Observation
lovee
3
450
ChatGPT 時代の勉強 / Learning under ChatGPT era
lovee
27
9k
Other Decks in Programming
See All in Programming
今さら聞けない .NET CLI
htkym
0
190
ソフトウェア設計に溶けるインフラ ― AWS CDK のインフラ認識論
konokenj
3
780
仕様駆動開発へのトライを機に チームに適合する手法を模索し続けている話
freee
PRO
0
490
Augmenting AI with the Power of Jakarta EE
ivargrimstad
0
560
PostgreSQL 18で考えるUUID主キー
kazuhiro1982
0
470
AWS DevOps AgentのAzure接続機能を検証して見えた活用法/Use Cases Verified for the AWS DevOps Agent's Azure Connectivity Feature
masakiokuda
1
230
型も通る、synthも通る、それでも危ない 〜AIのCDKの権限とコストを機械で検証する〜 / It Passes Type Checks, It Passes Synth Checks, but It’s Still Risky — Automatically Verifying Permissions and Costs in AI’s CDK —
seike460
PRO
1
550
What's New in Android 2026
veronikapj
0
260
Terraform標準の組織で AWS CDKをどう使うか
mu7889yoon
1
500
PHP Application における Kubernetes 内 gRPC 通信
ganchiku
0
590
PHP に部分適用が来るぞ!……ところで何それ?おいしいの? #phpcon / phpcon-2026
shogogg
0
640
Google Apps Script で Ruby を動かす
kawahara
0
200
Featured
See All Featured
The Pragmatic Product Professional
lauravandoore
37
7.4k
Build your cross-platform service in a week with App Engine
jlugia
234
19k
Done Done
chrislema
186
16k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
950
HU Berlin: Industrial-Strength Natural Language Processing with spaCy and Prodigy
inesmontani
PRO
0
640
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
790
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
2k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
23k
Practical Orchestrator
shlominoach
191
12k
How to Think Like a Performance Engineer
csswizardry
28
2.7k
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.5k
BBQ
matthewcrist
89
10k
Transcript
4XJGU6*Ͱ ͍͍͢5PBTUͷ࡞Γํ f o r Ϟ ό ν Ω
} var employedBy = "YUMEMI Inc." var job = "iOS
Developer" var favoriteLanguage = "Swift" var twitter = "@lovee" var qiita = "lovee" var github = "el-hoshino" var additionalInfo = """ ૹͷϑϦʔϨϯͷαϯτϥຊൃചͰ͢ʂ """ final class Me: Developable, Talkable {
None
5PBTUͱ w ؆୯ͳϫʔχϯάใࠂʹ͍͍ͨ௨ w Ϣʔβͷૢ࡞ΛϒϩοΫ͠ͳ͍ w ϢʔβʹΞΫγϣϯΛٻΊͳ͍ w "OESPJEͰΑ͘ݟ͔͚Δ͕J04ʹ७ਖ਼෦͕ͳ͍ w
"MFSUϢʔβͷૢ࡞ΛϒϩοΫͯ͠͠·͏ w 6//PUJpDBUJPOେ܌࠰Ͱ༨ܭͳݖݶඞཁ
͍͍͢ 5PBTUͷཁ݅ w ͍ΖΜͳϏϡʔ͔Β؆୯ʹग़ͤΔ w -PHͱಉ͡Α͏ͳײ֮ͳͷͰɺͲ͜ͰԿΛग़͍͔ͨ͠ॊ ೈʹରԠ͍ͨ͠ w ΞϓϦҰׅͰදࣔΛཧ w
ෳͷϏϡʔ͔Β5PBTUग़͍ͨ͜͠ͱ͋ΔͷͰɺҰ੪ ʹग़͞ΕͨΒࠔΔ͔ΒͪΌΜͱҰݩཧ͍ͨ͠ ͋͘·ͰݸਓͷײͰ͢
Alertͱಉ͡ײ֮Ͱ࡞Ε ͍͍ͷͰʁ
7FS struct Content: View { @State private var warning: String?
var body: some View { MyContentView() .refreshable { do { try await doSomething() } catch { warning = "\(error)" } } .alert(item: $warning) { warning in Alert(title: Text("\(warning)")) } } } warningΛ@Stateͱͯ࣋ͭ͠ Ϧϩʔυ࣌ʹԿ͔͠Βॲཧ Τϥʔىͨ͜͠Β warningʹॻ͖ࠐΈ warning͕͋ͬͨΒ ΞϥʔτΛग़͢
7FS struct Content: View { @State private var warning: String?
var body: some View { MyContentView() .refreshable { do { try await doSomething() } catch { warning = "\(error)" } } .toast(item: $warning) } } warningΛ@Stateͱͯ࣋ͭ͠ Ϧϩʔυ࣌ʹԿ͔͠Βॲཧ Τϥʔىͨ͜͠Β warningʹॻ͖ࠐΈ warning͕͋ͬͨΒ 5PBTUΛग़͢
ͱ͍͏Θ͚Ͱ.toastͷ࡞ΓํΛ ڭ͑·͢
ͱͰݴ͏ͱࢥͬͨʁ
ͬͯΈͯΘ͔ͬͨ.toastͷσϝϦοτ w ʢ΄΅΄΅શͯͷϏϡʔʹʣ!4UBUFQSJWBUFWBSXBSOJOH 4USJOH ࡞ͬͯߋʹ.PEJpFS͔͚Δͷ७ਮʹ໘ͯ͘͘͞ ਏ͔ͬͨ w ҰͭͷϏϡʔ͔ΒҰʹෳͷϫʔχϯάΛग़͢ͷ͕͍͠ w ෳͷϏϡʔͷϫʔχϯάͷҰݩཧͷ͜ͱߟ͑ͨΒɺ͜Ε
࣮࣭4JOHMF4PVSDFPG5SVUIݪଇ͕कΓʹ͍͘
ͬͯΈͯΘ͔ͬͨ.toastͷσϝϦοτ w ʢ΄΅΄΅શͯͷϏϡʔʹʣ!4UBUFQSJWBUFWBSXBSOJOH 4USJOH ࡞ͬͯߋʹ.PEJpFS͔͚Δͷ७ਮʹ໘ͯ͘͘͞ ਏ͔ͬͨ w ҰͭͷϏϡʔ͔ΒҰʹෳͷϫʔχϯάΛग़͢ͷ͕͍͠ w ෳͷϏϡʔͷϫʔχϯάͷҰݩཧͷ͜ͱߟ͑ͨΒɺ͜Ε
࣮࣭4JOHMF4PVSDFPG5SVUIݪଇ͕कΓʹ͍͘ Α͘Α͘ߟ͑ͯΈͨΒ 4UBUF%SJWFOͰ͋Δඞཁ ͳ͘ͳ͍ʁʁʁ
ͳͥ4UBUF%SJWFOͰ࡞͔ͬͨ w 4XJGU6*4UBUF%SJWFOͳϑϨʔϜϫʔΫ w ը໘ͷঢ়گશͯ4UBUFͰཧ͞ΕΔ w &WFOUશͯԿ͔͠Βͷ4UBUFʹམͱ͠ࠐΉඞཁ͕͋Δ w Ͱ5PBTUΛग़͢ଆผʹ4UBUFͰཧ͢Δඞཁͳ͍ΑͶ w
ͦͦϫʔχϯάΛใࠂ͍ͨ͠&WFOUʹରͯ͠5PBTU ग़͢ॲཧॻ͘ͷͰ&WFOU%SJWFOͰΑ͘ͳ͍ʁ
7FS struct Content: View { @State private var warning: String?
var body: some View { MyContentView() .refreshable { do { try await doSomething() } catch { warning = "\(error)" } } .toast(item: $warning) } } warningΛ@Stateͱͯ࣋ͭ͠ Ϧϩʔυ࣌ʹԿ͔͠Βॲཧ Τϥʔىͨ͜͠Β warningʹॻ͖ࠐΈ warning͕͋ͬͨΒ 5PBTUΛग़͢
7FS struct Content: View { @Environment(\.displayToast) var displayToast var body:
some View { MyContentView() .refreshable { do { try await doSomething() } catch { displayToast?("\(error)") } } } } 5PBTUΛදࣔ͢ΔͨΊͷ࣮Λ @Environmentͱͯ͠ड͚औΔ Ϧϩʔυ࣌ʹԿ͔͠Βॲཧ Τϥʔىͨ͜͠Β 5PBTUΛग़͢
ͦ͏ɺΉ͠Ζ.alertΑΓ @Environment(\.dismiss)دΓ
ͱݴ͏Θ͚Ͱ @Environment(\.displayToast)ͷ ࡞ΓํΛڭ͑·͢
·ͣ&OWJSPONFOUΛ࡞Δ import SwiftUI typealias DisplayToastAction = @MainActor (String) -> Void
struct DisplayToastKey: EnvironmentKey { static var defaultValue: DisplayToastAction? = nil } extension EnvironmentValues { var displayToast: DisplayToastAction? { get { self[DisplayToastKey.self] } set { self[DisplayToastKey.self] = newValue } } }
import Observation @Observable final class ToastHandler { @MainActor private (set)
var currentToastMessage: String? @ObservationIgnored private var toastQueue: [String] = [] @ObservationIgnored private var currentToastShowingTask: Task<Void, Never>? private var toastShowingDuration: Duration { .seconds(3) } private var defaultToastHidingDuration: Duration { .milliseconds(450) } @MainActor func queueMessage(_ message: String) { ࣍5PBTU)BOEMFSΛ࡞Δ දࣔ͢Δ5PBTUΛཧ͢ΔͨΊͷ ϓϩύςΟʔΛ࡞Δ
private var toastShowingDuration: Duration { .seconds(3) } private var defaultToastHidingDuration:
Duration { .milliseconds(450) } @MainActor func queueMessage(_ message: String) { toastQueue.append(message) displayNextToastIfAvailable() } @MainActor func skipCurrent(in duration: Duration) { removeCurrentToast() Task { try? await Task.sleep(for: duration) displayNextToastIfAvailable() } } @MainActor private func displayNextToastIfAvailable() { guard currentToastMessage == nil, let message = toastQueue.first else { return } ࣍5PBTU)BOEMFSΛ࡞Δ 5PBTUͷՃεΩοϓͳͲͷ ॲཧΛ࣮ ϝοηʔδ͕Ճ͞ΕͨΒ ඞཁʹԠͯ͡5PBTUΛදࣔͤ͞Δ TLJQ$VSSFOUݺΕͨΒ ·ͣݱࡏදࣔதͷͷΛফ͢ ͦͯ͠ಉ͘͡ඞཁʹԠͯ͡ ࣍ͷ5PBTUΛදࣔͤ͞Δ
try? await Task.sleep(for: duration) displayNextToastIfAvailable() } } @MainActor private func
displayNextToastIfAvailable() { guard currentToastMessage == nil, let message = toastQueue.first else { return } toastQueue.removeFirst() currentToastMessage = message currentToastShowingTask?.cancel() currentToastShowingTask = Task { do { try await Task.sleep(for: toastShowingDuration) if Task.isCancelled { return } skipCurrent(in: defaultToastHidingDuration) } catch { print("Task.sleep failed. Try Again") } } } @MainActor private func removeCurrentToast() { ࣍5PBTU)BOEMFSΛ࡞Δ ࣮ࡍͷ5PBTUͷදࣔΛ ੍ޚ͢Δ࣮ લఏͱͯ͠ࠓଞͷ5PBTUද͍ࣔͯ͠ͳ͍ͷͱ දࣔՄೳͳ5PBTUࣗମଘࡏ͢Δ͜ͱ Ұఆͷ͕࣌ؒܦͭͱࠓͷ5PBTUΛεΩοϓ͢Δ
guard currentToastMessage == nil, let message = toastQueue.first else {
return } toastQueue.removeFirst() currentToastMessage = message currentToastShowingTask?.cancel() currentToastShowingTask = Task { do { try await Task.sleep(for: toastShowingDuration) if Task.isCancelled { return } skipCurrent(in: defaultToastHidingDuration) } catch { print("Task.sleep failed. Try Again") } } } @MainActor private func removeCurrentToast() { if currentToastMessage == nil { return } currentToastShowingTask?.cancel() currentToastMessage = nil } } ࣍5PBTU)BOEMFSΛ࡞Δ ࠓදࣔதͷ5PBTUΛআ͢Δ࣮
import SwiftUI struct ToastDisplayModifier<Toast: View>: ViewModifier { var alignment: Alignment
var toastHandler: ToastHandler var toastMaker: (ToastHandler) -> Toast func body(content: Content) -> some View { content .overlay(alignment: alignment) { toastMaker(toastHandler) } .environment(\.displayToast, toastHandler.queueMessage(_:)) } } extension View { func displayToast<Toast: View>( on alignment: Alignment, handledBy toastHandler: ToastHandler, ͦͯ͠7JFX.PEJpFSΛ࡞Δ 5PBTUͷදࣔͷํ ڞ௨Ͱ͏5PBTUՃ༻ͷॲཧΛ࣮ 5PBTUͷදࣔΛ PWFSMBZʹ͢Δ ͍ΖΜͳը໘Ͱ5PBTUՃͰ͖ΔΑ͏ FOWJSPONFOUΛઃఆ
func body(content: Content) -> some View { content .overlay(alignment: alignment)
{ toastMaker(toastHandler) } .environment(\.displayToast, toastHandler.queueMessage(_:)) } } extension View { func displayToast<Toast: View>( on alignment: Alignment, handledBy toastHandler: ToastHandler, toastMaker: @escaping (ToastHandler) -> Toast ) -> some View { self.modifier( ToastDisplayModifier( alignment: alignment, toastHandler: toastHandler, toastMaker: toastMaker ) ) } } ͦͯ͠7JFX.PEJpFSΛ࡞Δ .PEJpFSΛద༻͢ΔͨΊͷ ֦ுΛ࣮
import SwiftUI struct ToastView: View { var toastHandler: ToastHandler private
var toastHidingDuration: Duration { .milliseconds(10) } var body: some View { Group { if let toastMessage = toastHandler.currentToastMessage { Text(toastMessage) // ελΠϧͷઃఆ .transition(MoveTransition.move(edge: .top). combined(with: .opacity)) } } .animation(.easeInOut, value: toastHandler.currentToastMessage) ࠷ޙʹ5PBTU7JFXΛ࡞Δ UPBTU)BOEMFSΛอ࣋͢Δ
private var toastHidingDuration: Duration { .milliseconds(10) } var body: some
View { Group { if let toastMessage = toastHandler.currentToastMessage { Text(toastMessage) // ελΠϧͷઃఆ .transition(MoveTransition.move(edge: .top). combined(with: .opacity)) } } .animation(.easeInOut, value: toastHandler.currentToastMessage) .onTapGesture { toastHandler.skipCurrent(in: toastHidingDuration) } } } extension View { func displayToast(handledBy toastHandler: ToastHandler) -> some View { self.displayToast( ࠷ޙʹ5PBTU7JFXΛ࡞Δ ͖ͳΑ͏ʹ 5PBTU7JFXΛ࡞Δ .transitionઃఆ͢Δ͜ͱͰ ৽͍͠5PBTUͷදࣔΛεϥΠυͰ͖Δ (SPVQͰ.animationઃఆ͢Δ͜ͱͰ 5PBTUͷද͕ࣔࣗಈతʹΞχϝʔγϣϯద༻͞ΕΔ .onTapGestureͰTLJQݺͿ͜ͱͰ Ϣʔβ͕खಈͰࠓͷϝοηʔδΛফͤΔ
// ελΠϧͷઃఆ .transition(MoveTransition.move(edge: .top). combined(with: .opacity)) } } .animation(.easeInOut, value:
toastHandler.currentToastMessage) .onTapGesture { toastHandler.skipCurrent(in: toastHidingDuration) } } } extension View { func displayToast(handledBy toastHandler: ToastHandler) -> some View { self.displayToast( on: .top, handledBy: toastHandler, toastMaker: { ToastView(toastHandler: $0) } ) } } ࠷ޙʹ5PBTU7JFXΛ࡞Δ 5PBTU7JFXΛָʹ͏ͨΊͷ ֦ு࡞͓ͬͯ͘
͏࣌
Ұ൪େݩͷ7JFXͰToastHandlerอ࣋ͯ͠ .displayToast.PEJpFSΛೖΕΔ import SwiftUI @main struct MyApp: App { @State
private var toastHandler: ToastHandler = .init() var body: some Scene { WindowGroup { ContentView() .displayToast(handledBy: toastHandler) } } }
5PBTUΛද͍ࣔͨ͠7JFXͰ displayToastϓϩύςΟʔΛݺͼग़͢ import SwiftUI struct Content: View { @Environment(\.displayToast) var
displayToast var body: some View { MyContentView() .refreshable { do { try await doSomething() } catch { displayToast?("\(error)") } } } }
ʲએʳϥΠϒϥϦʔ࡞Γ·ͨ͠!
IUUQTHJUIVCDPNFMIPTIJOP5BSEJOFTT