Slide 1

Slide 1 text

4XJGU6*Ͱ ࢖͍΍͍͢5PBTUͷ࡞Γํ f o r Ϟ ό ν Ω

Slide 2

Slide 2 text

} 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 {

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

5PBTUͱ͸ w ؆୯ͳϫʔχϯάใࠂʹ࢖͍͍ͨ௨஌ w Ϣʔβͷૢ࡞ΛϒϩοΫ͠ͳ͍ w ϢʔβʹΞΫγϣϯΛٻΊͳ͍ w "OESPJEͰ͸Α͘ݟ͔͚Δ͕J04ʹ͸७ਖ਼෦඼͕ͳ͍ w "MFSU͸Ϣʔβͷૢ࡞ΛϒϩοΫͯ͠͠·͏ w 6//PUJpDBUJPO͸େ܌࠰Ͱ༨ܭͳݖݶ΋ඞཁ

Slide 5

Slide 5 text

࢖͍΍͍͢5PBTUͷཁ݅ w ͍ΖΜͳϏϡʔ͔Β؆୯ʹग़ͤΔ w -PHͱಉ͡Α͏ͳײ֮ͳͷͰɺͲ͜ͰԿΛग़͍͔ͨ͠͸ॊ ೈʹରԠ͍ͨ͠ w ΞϓϦҰׅͰදࣔΛ؅ཧ w ෳ਺ͷϏϡʔ͔Β5PBTUग़͍ͨ͜͠ͱ΋͋ΔͷͰɺҰ੪ ʹग़͞ΕͨΒࠔΔ͔ΒͪΌΜͱҰݩ؅ཧ͍ͨ͠ ͋͘·Ͱݸਓͷײ૝Ͱ͢

Slide 6

Slide 6 text

Alertͱಉ͡ײ֮Ͱ࡞Ε͹ ͍͍ͷͰ͸ʁ

Slide 7

Slide 7 text

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͕͋ͬͨΒ ΞϥʔτΛग़͢

Slide 8

Slide 8 text

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Λग़͢

Slide 9

Slide 9 text

ͱ͍͏Θ͚Ͱ.toastͷ࡞ΓํΛ ڭ͑·͢

Slide 10

Slide 10 text

ͱͰ΋ݴ͏ͱࢥͬͨʁ

Slide 11

Slide 11 text

࢖ͬͯΈͯΘ͔ͬͨ.toastͷσϝϦοτ w ʢ΄΅΄΅શͯͷϏϡʔʹʣ!4UBUFQSJWBUFWBSXBSOJOH 4USJOH ࡞ͬͯߋʹ.PEJpFS͔͚Δͷ७ਮʹ໘౗ͯ͘͘͞ ਏ͔ͬͨ w ҰͭͷϏϡʔ͔ΒҰ౓ʹෳ਺ͷϫʔχϯάΛग़͢ͷ͕೉͍͠ w ෳ਺ͷϏϡʔͷϫʔχϯάͷҰݩ؅ཧͷ͜ͱߟ͑ͨΒɺ͜Ε ࣮࣭4JOHMF4PVSDFPG5SVUIݪଇ͕कΓʹ͍͘

Slide 12

Slide 12 text

࢖ͬͯΈͯΘ͔ͬͨ.toastͷσϝϦοτ w ʢ΄΅΄΅શͯͷϏϡʔʹʣ!4UBUFQSJWBUFWBSXBSOJOH 4USJOH ࡞ͬͯߋʹ.PEJpFS͔͚Δͷ७ਮʹ໘౗ͯ͘͘͞ ਏ͔ͬͨ w ҰͭͷϏϡʔ͔ΒҰ౓ʹෳ਺ͷϫʔχϯάΛग़͢ͷ͕೉͍͠ w ෳ਺ͷϏϡʔͷϫʔχϯάͷҰݩ؅ཧͷ͜ͱߟ͑ͨΒɺ͜Ε ࣮࣭4JOHMF4PVSDFPG5SVUIݪଇ͕कΓʹ͍͘ Α͘Α͘ߟ͑ͯΈͨΒ 4UBUF%SJWFOͰ͋Δඞཁ ͳ͘ͳ͍ʁʁʁ

Slide 13

Slide 13 text

ͳͥ4UBUF%SJWFOͰ࡞͔ͬͨ w 4XJGU6*͸4UBUF%SJWFOͳϑϨʔϜϫʔΫ w ը໘ͷঢ়گ͸શͯ4UBUFͰ؅ཧ͞ΕΔ w &WFOU͸શͯԿ͔͠Βͷ4UBUFʹམͱ͠ࠐΉඞཁ͕͋Δ w Ͱ΋5PBTUΛग़͢ଆ͸ผʹ4UBUFͰ؅ཧ͢Δඞཁͳ͍ΑͶ w ͦ΋ͦ΋ϫʔχϯάΛใࠂ͍ͨ͠&WFOUʹରͯ͠5PBTU ग़͢ॲཧॻ͘ͷͰ&WFOU%SJWFOͰΑ͘ͳ͍ʁ

Slide 14

Slide 14 text

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Λग़͢

Slide 15

Slide 15 text

7FS struct Content: View { @Environment(\.displayToast) var displayToast var body: some View { MyContentView() .refreshable { do { try await doSomething() } catch { displayToast?("\(error)") } } } } 5PBTUΛදࣔ͢ΔͨΊͷ࣮૷Λ @Environmentͱͯ͠ड͚औΔ Ϧϩʔυ࣌ʹԿ͔͠Βॲཧ Τϥʔىͨ͜͠Β ௚઀5PBTUΛग़͢

Slide 16

Slide 16 text

ͦ͏ɺΉ͠Ζ.alertΑΓ @Environment(\.dismiss)دΓ

Slide 17

Slide 17 text

ͱݴ͏Θ͚Ͱ @Environment(\.displayToast)ͷ ࡞ΓํΛڭ͑·͢

Slide 18

Slide 18 text

·ͣ͸&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 } } }

Slide 19

Slide 19 text

import Observation @Observable final class ToastHandler { @MainActor private (set) var currentToastMessage: String? @ObservationIgnored private var toastQueue: [String] = [] @ObservationIgnored private var currentToastShowingTask: Task? private var toastShowingDuration: Duration { .seconds(3) } private var defaultToastHidingDuration: Duration { .milliseconds(450) } @MainActor func queueMessage(_ message: String) { ࣍͸5PBTU)BOEMFSΛ࡞Δ දࣔ͢Δ5PBTUΛ؅ཧ͢ΔͨΊͷ ϓϩύςΟʔΛ࡞Δ

Slide 20

Slide 20 text

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Λදࣔͤ͞Δ

Slide 21

Slide 21 text

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ΛεΩοϓ͢Δ

Slide 22

Slide 22 text

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Λ࡟আ͢Δ࣮૷

Slide 23

Slide 23 text

import SwiftUI struct ToastDisplayModifier: 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( on alignment: Alignment, handledBy toastHandler: ToastHandler, ͦͯ͠7JFX.PEJpFSΛ࡞Δ 5PBTUͷදࣔͷ࢓ํ΍ ڞ௨Ͱ࢖͏5PBTU௥Ճ༻ͷॲཧΛ࣮૷ 5PBTUͷදࣔΛ PWFSMBZʹ͢Δ ͍ΖΜͳը໘Ͱ5PBTU௥ՃͰ͖ΔΑ͏ FOWJSPONFOUΛઃఆ

Slide 24

Slide 24 text

func body(content: Content) -> some View { content .overlay(alignment: alignment) { toastMaker(toastHandler) } .environment(\.displayToast, toastHandler.queueMessage(_:)) } } extension View { func displayToast( on alignment: Alignment, handledBy toastHandler: ToastHandler, toastMaker: @escaping (ToastHandler) -> Toast ) -> some View { self.modifier( ToastDisplayModifier( alignment: alignment, toastHandler: toastHandler, toastMaker: toastMaker ) ) } } ͦͯ͠7JFX.PEJpFSΛ࡞Δ .PEJpFSΛద༻͢ΔͨΊͷ ֦ுΛ࣮૷

Slide 25

Slide 25 text

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Λอ࣋͢Δ

Slide 26

Slide 26 text

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ݺͿ͜ͱͰ Ϣʔβ͕खಈͰࠓͷϝοηʔδΛফͤΔ

Slide 27

Slide 27 text

// ελΠϧͷઃఆ .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Λָʹ࢖͏ͨΊͷ ֦ு΋࡞͓ͬͯ͘

Slide 28

Slide 28 text

࢖͏࣌

Slide 29

Slide 29 text

Ұ൪େݩͷ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) } } }

Slide 30

Slide 30 text

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)") } } } }

Slide 31

Slide 31 text

ʲએ఻ʳϥΠϒϥϦʔ࡞Γ·ͨ͠!

Slide 32

Slide 32 text

IUUQTHJUIVCDPNFMIPTIJOP5BSEJOFTT