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
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
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
540
@Environment(\.keyPath)那么好我不允许你们不知道! / atEnvironment keyPath is so good and you should know it!
lovee
0
540
ゼロから始めるPreferenceの実装 / Let's implement Preferences from scratch
lovee
0
160
Kotlin エンジニアへ送る:Swift 案件に参加させられる日に備えて~似てるけど色々違う Swift の仕様 / from Kotlin to Swift
lovee
1
400
個人アプリを2年ぶりにアプデしたから褒めて / I just updated my personal app, praise me!
lovee
0
770
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
450
ChatGPT 時代の勉強 / Learning under ChatGPT era
lovee
27
9k
Other Decks in Programming
See All in Programming
DroidKaigi 2026 「個人開発という実験場: Android エンジニアが手にする4つの自由」
slashnephy
0
230
iOSDC Japan 2026 - Swiftで作って学ぼう!データベース自作入門
kaseken
2
170
フロントエンドUIフレームワークのこれまでとこれから
ssssota
5
2.8k
XP祭りでしか伝わらないフリップネタ #xpjug
murabayashi
0
150
『寄り添うラジオ』をAIで作る 体験価値から逆算した、会話しないUXと品質設計
theoriatec2024
3
180
C#の現在地 進化の歴史と、AI時代の.NET Everywhere
neuecc
1
780
スマート反転とウェブアクセシビリティ
camiha
0
210
Are APIs Still Relevant in the AI Era?
soyuka
0
230
GKE で Pod の見方を変えたら、スケールアウト時の挙動を真に捉えられた話
stkk
0
120
Security issues being discussed on Web Platforms
petamoriken
0
950
技術的負債を組織課題として解く-増えすぎたマイクロサービスとの戦い-
reimaru
1
1.9k
LoopHub - ローカルで動く GitHub で、AI と共同開発
jugyo
1
550
Featured
See All Featured
Automating Front-end Workflow
addyosmani
1369
210k
Impact Scores and Hybrid Strategies: The future of link building
tamaranovitovic
0
440
Testing 201, or: Great Expectations
jmmastey
46
8.3k
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
240
Jamie Indigo - Trashchat’s Guide to Black Boxes: Technical SEO Tactics for LLMs
techseoconnect
PRO
0
670
Typedesign – Prime Four
hannesfritz
42
3.2k
Building Adaptive Systems
keathley
44
3.2k
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
2
280
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
2.1k
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
Navigating Weather and Climate Data
rabernat
0
520
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