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
@Environment(\.keyPath)那么好我不允许你们不知道! / atEnvironment keyPath is so good and you should know it!
lovee
0
450
ゼロから始める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
730
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
IBM Bobを活用したレガシーアプリの最新化
oniak3ibm
PRO
1
190
代数的データ型って何が嬉しいの? #frontend_phpcon_do
kajitack
8
3.3k
Webフレームワークの ベンチマークについて
yusukebe
0
160
TypeScript+Orvalで実現する型安全かつ堅牢でスケーラブルなマルチチャネル通知基盤 / TSKaigi Night talks ~after conference~
d0riven
0
320
Java × distroless で 軽量なコンテナイメージを / Java on Distroless
contour_gara
0
530
AI時代のUIはどこへ行く?その2!
yusukebe
21
7k
These Five Tricks Can Make Your Apps Greener, Cheaper, & Nicer
hollycummins
0
280
ローカルLLMでどこまでコードが書けるか -拡張版 / How much code can be written on a local LLM Extended
kishida
2
1.1k
JJUG CCC 2026 Spring: JSpecify で実現する Kotlin フレンドリーな Java API 設計
ternbusty
1
160
運用エージェントは "作る" から "育てる" へ - 記憶と自己進化の3層設計パターン / self-evolving-agents-three-layer-agent-design
gawa
12
3.6k
RTSPクライアントを自作してみた話
simotin13
0
560
ECSアプリログをFireLensでコスト削減しようとしたけど諦めた話 in Fargate×Node.js
akihisaikeda
2
4k
Featured
See All Featured
Balancing Empowerment & Direction
lara
6
1.2k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.2k
GraphQLの誤解/rethinking-graphql
sonatard
75
12k
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
What does AI have to do with Human Rights?
axbom
PRO
1
2.2k
More Than Pixels: Becoming A User Experience Designer
marktimemedia
3
440
Crafting Experiences
bethany
1
180
Google's AI Overviews - The New Search
badams
0
1k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
62k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.2k
Why Your Marketing Sucks and What You Can Do About It - Sophie Logan
marketingsoph
0
170
Leadership Guide Workshop - DevTernity 2021
reverentgeek
1
300
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