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
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
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
@Environment(\.keyPath)那么好我不允许你们不知道! / atEnvironment keyPath is so good and you should know it!
lovee
0
460
ゼロから始めるPreferenceの実装 / Let's implement Preferences from scratch
lovee
0
150
Kotlin エンジニアへ送る:Swift 案件に参加させられる日に備えて~似てるけど色々違う Swift の仕様 / from Kotlin to Swift
lovee
1
390
個人アプリを2年ぶりにアプデしたから褒めて / I just updated my personal app, praise me!
lovee
0
740
How did I build an Open-Source SwiftUI Toast Library
lovee
1
170
SwiftUIで二重スクロール作ってみた / When I tried to make a dual-scroll-ish view in SwiftUI
lovee
1
380
Observation のあれこれ / A brief introduction about Observation
lovee
3
440
ChatGPT 時代の勉強 / Learning under ChatGPT era
lovee
27
9k
属人化しない為の勉強会作り / To make tech meetups with less personal dependencies
lovee
0
370
Other Decks in Programming
See All in Programming
1B+ /day規模のログを管理する技術
broadleaf
0
120
LLM本来の能力を解き放つサンドボックス技術とAI民主化への適用
yukukotani
3
4.6k
Creating Composable Callables in Contemporary C++
rollbear
0
170
Developing with AI Agents — Codex, Claude Code & Cowork Practical Guide
x5gtrn
PRO
0
1.3k
JavaDoc 再入門
nagise
1
420
Performance Engineering for Everyone
elenatanasoiu
0
230
Hunting Vulnerabilities in Symfony with LLMs
vinceamstoutz
0
560
TypeScript+Orvalで実現する型安全かつ堅牢でスケーラブルなマルチチャネル通知基盤 / TSKaigi Night talks ~after conference~
d0riven
0
360
「なぜそう決めたのか」を残し続ける仕組み ― Notion AI カスタムエージェント × Slack連携による設計判断の自動記録 - NIKKEI Tech Talk #47
niftycorp
PRO
0
230
ADKを使って簡単にAIエージェントを作ってみよう
k1mu21
0
280
ふつうのFeature Flag実践入門
irof
8
4.2k
Hatena Engineer Seminar #37「言語モデルの活用に関する研究」
slashnephy
0
210
Featured
See All Featured
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
62
55k
Darren the Foodie - Storyboard
khoart
PRO
3
3.4k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
287
14k
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.3k
Design in an AI World
tapps
1
250
Fashionably flexible responsive web design (full day workshop)
malarkey
408
66k
RailsConf 2023
tenderlove
30
1.5k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
62k
Impact Scores and Hybrid Strategies: The future of link building
tamaranovitovic
0
310
Ecommerce SEO: The Keys for Success Now & Beyond - #SERPConf2024
aleyda
1
2k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
What the history of the web can teach us about the future of AI
inesmontani
PRO
1
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