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
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
580
@Environment(\.keyPath)那么好我不允许你们不知道! / atEnvironment keyPath is so good and you should know it!
lovee
0
580
ゼロから始めるPreferenceの実装 / Let's implement Preferences from scratch
lovee
0
160
Kotlin エンジニアへ送る:Swift 案件に参加させられる日に備えて~似てるけど色々違う Swift の仕様 / from Kotlin to Swift
lovee
1
410
個人アプリを2年ぶりにアプデしたから褒めて / I just updated my personal app, praise me!
lovee
0
800
How did I build an Open-Source SwiftUI Toast Library
lovee
1
190
SwiftUIで二重スクロール作ってみた / When I tried to make a dual-scroll-ish view in SwiftUI
lovee
1
390
Observation のあれこれ / A brief introduction about Observation
lovee
3
460
ChatGPT 時代の勉強 / Learning under ChatGPT era
lovee
27
9.1k
Other Decks in Programming
See All in Programming
Python Lambdaにおけるテストしやすい設計とAI支援の活用 / Designing Testable Python Lambda Functions with AI
kitano_yuichi
1
250
TanStack Vue Query と Pinia Colada から学ぶ非同期状態の型設計
aoshi
0
440
現場と育てるRailsモデリング 〜Spreadsheetから始めたモデル設計〜
siroemk
2
1.1k
S3 TablesのIcebergデータに、AIエージェントからアクセスしてみた / 20261016developersio2026joetsu-accessing-iceberg-in-s3tables-via-agent
kasacchiful
PRO
0
240
Kotlinize Is Not Just Converting to Kotlin
chigichan24
1
200
オーケストレーション記述言語 Katari
yukikurage
0
210
AI時代のIaC入門 〜なぜインフラをコードで管理したいのか?〜 / Introduction to IaC in the AI Era
akihisaikeda
5
1.3k
Performance Optimization With LLMs
xleitix
0
230
Vue.jsで作る空間解析アプリとCapacitorプラグインのData Bridge
hal_spidernight
1
130
Coding with AI: How to trust the storyteller (AWS Community Day Adria, Ljubljana 2026)
slobodan
0
130
Web3Dライブラリを作ってVue Custom Rendererの仕組みを理解しよう / lets learn vue custom renderer by making 3d library
drumath2237
1
100
[JA] Domain Modeling with Vanilla Rails: 「操作」から再考するRailsの設計論
yasaichi
4
1.4k
Featured
See All Featured
Leo the Paperboy
mayatellez
10
2.4k
Measuring Dark Social's Impact On Conversion and Attribution
stephenakadiri
2
320
Design in an AI World
tapps
1
380
brightonSEO & MeasureFest 2025 - Winning Strategies for Black Friday CRO & PPC - Christian Goodrich
cargoodrich
3
880
Automating Front-end Workflow
addyosmani
1370
210k
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
730
Information Architects: The Missing Link in Design Systems
soysaucechin
1
1.2k
How to Ace a Technical Interview
jacobian
280
24k
What Being in a Rock Band Can Teach Us About Real World SEO
427marketing
0
1.1k
Test your architecture with Archunit
thirion
2
2.5k
Lessons Learnt from Crawling 1000+ Websites
charlesmeaden
PRO
1
1.7k
Technical Leadership for Architectural Decision Making
baasie
3
620
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