Slide 1

Slide 1 text

Swift ΞΫλʔϞσϧͱ Elm Architecture ͷ༥߹ 2022/09/11 iOSDC Japan 2022 Yasuhiro Inami / @inamiy

Slide 2

Slide 2 text

ΞδΣϯμ ΞΫλʔϞσϧͱ&MN"SDIJUFDUVSFͷઆ໌ "DUPNBUPOͷ঺հ "DUPNBUPOʹ͓͚Δʮঢ়ଶʯͱʮ෭࡞༻ʯͷ؅ཧ "DUPNBUPOΛ࢖ͬͨΞϓϦͷઃܭࢦ਑ ΞΫλʔϞσϧ ΦϒδΣΫτࢦ޲ϓϩάϥϛϯά ͱ 
 &MN"SDIJUFDUVSF ؔ਺ܕϓϩάϥϛϯά ͷ༥߹

Slide 3

Slide 3 text

ΞΫλʔϞσϧ

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Swift Concurrency 
 1. async /await 2. Structured Concurrency 3. Actors

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

actor Counter { var count: Int = 0 // ಺෦ঢ়ଶ func increment() { // τϥϯβΫγϣϯ count += 1 // ঢ়ଶߋ৽ print(count) // ෭࡞༻ͷ࣮ߦ } } // ΞΫλʔͷ࢖༻ྫ let counter = Counter() // ΞΫλʔ֎෦͔ΒΞΫλʔʹΞΫηε͢Δ৔߹ɺ async / await ඇಉظॲཧʹͳΔ await counter.increment() ΞΫλʔͷྫ ΞΫλʔঢ়ଶͱͦͷૢ࡞ΛҰՕॴʹ·ͱΊͨ ʮεϨου҆શʯͳΦϒδΣΫτ ΞΫλʔִ཭͞Εͨϝιου εϨου҆શΛอূ͢Δ τϥϯβΫγϣϯ

Slide 8

Slide 8 text

protocol CounterProtocol: Actor { func increment() // τϥϯβΫγϣϯͷந৅ } actor Counter { var count: Int = 0 // ಺෦ঢ়ଶ } extension Counter: CounterProtocol { func increment() { // τϥϯβΫγϣϯͷ࣮૷ count += 1 // ঢ়ଶߋ৽ print(count) // ෭࡞༻ͷ࣮ߦ } } ΞΫλʔͷྫ 1SPUPDPMʹΑΔந৅Խ QSPUPDPMʹΑΔ 
 ۩ମΞΫλʔͷ ϝιουͷந৅Խ QSPUPDPM४ڌʹΑΔ 
 ந৅ϝιουͷ࣮૷

Slide 9

Slide 9 text

Elm Architecture

Slide 10

Slide 10 text

Msg (Action) 􀉭 Real View Update (Reducer) State Virtual View view (render) Diff (New State) User Action Cmd (Effect) 💥 Runtime I/O Run Side-effect

Slide 11

Slide 11 text

enum Action: Sendable { case increment } struct State: Sendable, Equatable { var count: Int = 0 } let reducer = Reducer { action, state, _ in switch action { // ΞΫγϣϯͷύλʔϯϚον case .increment: state.count += 1 // ঢ়ଶߋ৽ return Effect { [state] in print(state.count) // ෭࡞༻ͷ࣮ߦ } } } &MN"SDIJUFDUVSFͷྫ 4XJGU຋༁ ܭࢉϩδοΫͷೖྗ 
 ΞΫγϣϯ ΛFOVNͰදݱ ܭࢉϩδοΫ 3FEVDFS ʮ"DUJPOʯͱʮݱࡏͷঢ়ଶʯΛ 
 Ҿ਺Ͱड͚औΓɺʮ৽͍͠ঢ়ଶʯͱ 
 ʮ෭࡞༻ʯΛฦؔ͢਺ ෭࡞༻ QSJOU΍BTZOD Λ & ff FDUͷதͰ࣮ߦ͢Δ

Slide 12

Slide 12 text

enum Action: Sendable { case increment } struct State: Sendable, Equatable { var count: Int = 0 } let reducer = Reducer { action, state, _ in switch action { // ΞΫγϣϯͷύλʔϯϚον case .increment: state.count += 1 // ঢ়ଶߋ৽ return Effect { [state] in print(state.count) // ෭࡞༻ͷ࣮ߦ } } } &MN"SDIJUFDUVSFͷྫ 4XJGU຋༁ Elm Architecture Ͱ͸ class / actor ͕ొ৔͠ͳ͍ (ΑΓ୯७ͳ enum, struct, func ͕ओ໾)

Slide 13

Slide 13 text

// Elm Architecture enum Action: Sendable { case increment } struct State: Sendable, Equatable { var count: Int = 0 } let reducer = Reducer { action, state, _ in switch action { // ΞΫγϣϯͷύλʔϯϚον case .increment: state.count += 1 // ঢ়ଶߋ৽ return Effect { [state] in print(state.count) // ෭࡞༻ͷ࣮ߦ } } } &MN"SDIJUFDUVSFͱΞΫλʔϞσϧͷൺֱ // ΞΫλʔϞσϧ protocol CounterProtocol: Actor { func increment() // τϥϯβΫγϣϯͷந৅ } actor Counter { var count: Int = 0 // ಺෦ঢ়ଶ } extension Counter: CounterProtocol { func increment() { // τϥϯβΫγϣϯͷ࣮૷ count += 1 // ঢ়ଶߋ৽ print(count) // ෭࡞༻ͷ࣮ߦ } }

Slide 14

Slide 14 text

enum Action { case increment } struct State { var count: Int = 0 } let reducer = Reducer { action, state in switch action { case .increment: state.count += 1 return Effect { print(state.count) 
 } } } &MN"SDIJUFDUVSFͷྫ 4XJGU຋༁ protocol CounterProtocol: Actor { func increment() } actor Counter { var count: Int = 0 } extension Counter: CounterProtocol { func increment() { count += 1 print(count) } } Actor = ΦϒδΣΫτࢦ޲ Elm Architecture = ؔ਺ܕ ྆ऀʹີ઀ͳؔ܎͕͋Δʂ

Slide 15

Slide 15 text

ΞΫλʔϞσϧͱ&MN"SDIJUFDUVSF wΞΫλʔϞσϧ ΦϒδΣΫτࢦ޲ϓϩάϥϛϯά w௚ྻॲཧͷ&YFDVUPSΛ࣋ͭɺίϯύΠϧ࣌εϨου҆શ ੑ͕อূ͞ΕͨΦϒδΣΫτ wΦϒδΣΫτ܈ͷ෼ࢄ؅ཧʹڧ͍ʢଟਆڭʣ w&MN"SDIJUFDUVSF ؔ਺ܕϓϩάϥϛϯά wΞΫγϣϯɺঢ়ଶɺ3FEVDFSͷ୯७σʔλߏ଄Λ߹੒ w4JOHMF4PVSDFPG5SVUIΛߏ੒Ͱ͖ΔʢҰਆڭʣ

Slide 16

Slide 16 text

࠷ۙͷ4XJGUJ04։ൃͷτϨϯυ .77. 7JFX.PEFM ʹΞΫλʔϞσϧΛద༻ w !.BJO"DUPSʹΑΔϝΠϯεϨου҆શੑ $PNQPTBCMF"SDIJUFDUVSF 5$" &MNελΠϧ w ঢ়ଶͷू໿؅ཧʹΑΔςετɾσόοΪϯά 🤔 2྆ํͷྑ͍ॴऔΓ͕Ͱ͖ͳ͍΋ͷ͔ʜʁ

Slide 17

Slide 17 text

Actomaton 
 http://github.com/Actomaton/Actomaton

Slide 18

Slide 18 text

J04%$+BQBO 3FBDUJWF4UBUF.BDIJOF IUUQTTQFBLFSEFDLDPNJOBNJZSFBDUJWFTUBUF NBDIJOFKBQBOFTF J04%$+BQBO 
 4XJGU6*࣌୅ͷ 
 'VODUJPOBMJ04"SDIJUFDUVSF IUUQTTQFBLFSEFDLDPNJOBNJZJPTED KBQBO

Slide 19

Slide 19 text

"DUPNBUPOͱ͸ʁʢ7FSݱࡏʣ w4XJGU$PODVSSFODZͱΞΫλʔϞσϧΛ࢖ͬͨ&MN"SDIJUFDUVSF w७ਮͳΞΫλʔ7JFX.PEFMʹൺ΂ͯɺΑΓ҆શʹঢ়ଶͱ෭࡞༻Λ؅ཧ wJ04%$+BQBOͰ঺հͨ͠)BSWFTU $PNCJOF Λ4XJGU $PODVSSFODZʹஔ͖׵͑ͨ࠷৽ܗ w%FQFOEFODZ*OKFDUJPOίϯςφΛ݉ͶΔ ςετͱσόοΪϯά w4XJGU6*6*,JU-JOVY؀ڥͰ࢖͑ɺ6*ϑϨʔϜϫʔΫ༗ແΛ໰Θͳ͍ w਺ઍສϢʔβʔن໛ͷJ04ΞϓϦͰಋೖ࣮੷

Slide 20

Slide 20 text

Demo 
 http://github.com/Actomaton/Actomaton-Gallery

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

ࢀߟɿ4XJGU6*ͰඥΛදݱ͢Δ IUUQT[FOOEFWJLFIBSUJDMFTBGFGEBCG IUUQTUXJUUFSDPN%-9TUBUVT

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

"DUPNBUPO(BMMFSZ σϞΞϓϦ ͷಛ௃ w࠶ར༻ੑͷߴ͍ϚΠΫϩϞδϡʔϧߏ੒ w4XJGU6*6*,JU.77.Ͱ͸࣮ݱ͕೉͍͠ػೳΛ࣮૷Ͱ͖Δ w೚ҙͷঢ়ଶ͔ΒͷΞϓϦىಈ σΟʔϓϦϯΫɺ։ൃαΠΫϧߴ଎Խ wঢ়ଶࠩ෼ͷσόοάϩά wλΠϜτϥϕϧʹΑΔ6OEP3FEP w෭࡞༻ͷΩϡʔ؅ཧ & ff FDU2VFVF &MN$PNQPTBCMF"SDIJUFDUVSF 
 ʹ͸උΘ͍ͬͯͳ͍ಠࣗػೳ

Slide 30

Slide 30 text

Actomaton ͷ࢖͍ํ

Slide 31

Slide 31 text

enum Action: Sendable { case increment } struct State: Sendable, Equatable { var count: Int = 0 } let reducer = Reducer { action, state, _ in switch action { // ΞΫγϣϯͷύλʔϯϚον case .increment: state.count += 1 // ঢ়ଶߋ৽ return Effect.fireAndForget { [state] in print(state.count) // ෭࡞༻ͷ࣮ߦ } } } ঢ়ଶ؅ཧͷίʔυྫ ϩδοΫ 3FEVDFSͷجຊܗɿ ύλʔϯϚον ঢ়ଶͷόϦσʔγϣϯͱߋ৽ ෭࡞༻ͷొ࿥ 
 ʢඇಉظܭࢉޙͷ࣍ͷΞΫγϣϯ ΋ࢦఆՄೳʣ Ϋϩʔδϟʹแ·Εͨ෭࡞༻͸ʮ࣮ߦલʯ 
 ͳͷͰ3FEVDFS͸ʮ७ਮؔ਺ʯ

Slide 32

Slide 32 text

enum Action: Sendable { // case ͕૿͑ͨ৔߹ case increment case decrementIfPositive } let reducer = Reducer { action, state, _ in switch action { // ΞΫγϣϯͷύλʔϯϚον case .increment: state.count += 1 // ঢ়ଶߋ৽ return Effect.fireAndForget { [state] in print(state.count) // ෭࡞༻ͷ࣮ߦ } case .decrementIfPositive: // ঢ়ଶͷόϦσʔγϣϯ guard state.count > 0 else { return Effect.empty } state.count -= 1 // ঢ়ଶߋ৽ return Effect.fireAndForget { [state] in print(state.count) // ෭࡞༻ͷ࣮ߦ } } }

Slide 33

Slide 33 text

enum Action: Sendable { // case ͕૿͑ͨ৔߹ case increment case decrementIfPositive } let reducer = Reducer { action, state, _ in switch action { // ΞΫγϣϯͷύλʔϯϚον case .increment: state.count += 1 // ঢ়ଶߋ৽ return Effect.fireAndForget { [state] in print(state.count) // ෭࡞༻ͷ࣮ߦ } case .decrementIfPositive: // ঢ়ଶͷόϦσʔγϣϯ guard state.count > 0 else { return Effect.empty } state.count -= 1 // ঢ়ଶߋ৽ return Effect.fireAndForget { [state] in print(state.count) // ෭࡞༻ͷ࣮ߦ } } } ύλʔϯϚον ঢ়ଶͷόϦσʔγϣϯͱߋ৽ ෭࡞༻ͷొ࿥ ৗʹಉ͡εςοϓͰهड़͢ΔͨΊɺ ঢ়ଶͷมߋՕॴ͕෼͔Γ΍͍͢ "DUPS3FFOUSBODZ໰୊ͷ ݕ஌͕͠΍͍͢ ·ͨɺ3FEVDFS͸TFMG BDUPS Λ ؚ·ͳ͍ͷͰɺෆ஫ҙʹΑΔTFMG॥ ؀ࢀর͕ى͖ͳ͍҆શઃܭʹͳΔ

Slide 34

Slide 34 text

let store = Store( state: State(), // ॳظঢ়ଶ reducer: reducer ) struct ContentView: View { let store: Store var body: some View { WithViewStore(store) { viewStore in Button("Count: \(viewStore.count)") { store.send(.increment) } } } } ঢ়ଶ؅ཧͷίʔυྫ 4UPSFԽͱ4XJGU6*ద༻ ΞΫγϣϯɺঢ়ଶɺ3FEVDFSΛ ଋͶͨ4UPSF .BJO"DUPS Λ࡞੒ 4UPSFΛ7JFX4UPSF 
 4XJGU6*!0CTFSWFE0CKFDU 
 ʹม׵ͯ͠࢖༻

Slide 35

Slide 35 text

Reducer State Action CounterView Store 4UPSFΛ࡞Γɺ 7JFXʹ࣋ͨͤΔ (CounterLogic) 4UPSF ঢ়ଶ؅ཧΦϒδΣΫτ Λߏ੒͢ΔͨΊͷܕͱϩδοΫ ؔ਺ܕελΠϧ (߹੒͠΍͍͢) ΦϒδΣΫτࢦ޲ (߹੒͠ʹ͍͘) Reducer State Action

Slide 36

Slide 36 text

ґଘ஫ೖͷίʔυྫ enum Action: Sendable { case fetch case _didFetch(Response) } struct State: Sendable, Equatable { var isLoading: Bool = false } struct Environment: Sendable { var fetch: @Sendable (Request) async throws -> Response } &OWJSPONFOU ґଘΛ஫ೖ͢Δܕ

Slide 37

Slide 37 text

ґଘ஫ೖͷίʔυྫ let reducer = Reducer { action, state, environment in switch action { case .fetch: state.isLoading = true return Effect { let response = try await environment.fetch(Request(...)) return Action._didFetch(response) 
 } case let ._didFetch(response): state.isLoading = false return Effect.fireAndForget { print(response) } } } &OWJSPONFOUΛ࢖ͬͯ ෭࡞༻Λ࣮ߦ

Slide 38

Slide 38 text

&GGFDU*%ʹΑΔ෭࡞༻ͷΩϟϯηϧ struct FetchEffectID: EffectIDProtocol {} let reducer = Reducer { action, state, environment in switch action { case .fetch: state.isLoading = true return Effect.cancel(id: FetchEffectID()) + Effect(id: FetchEffectID()) { let response = try await environment.fetch(Request(...)) return Action._didFetch(response) 
 } case let ._didFetch(response): state.isLoading = false return Effect.fireAndForget { print(response) } } } & ff FDU*%Λఆٛ & ff FDUʹ 
 & ff FDU*%Λઃఆ͠ɺ ΩϟϯηϧՄೳʹ͢Δ

Slide 39

Slide 39 text

&GGFDU2VFVFʹΑΔ෭࡞༻؅ཧ struct FetchEffectQueue: Newest1EffectQueueProtocol {} let reducer = Reducer { action, state, environment in switch action { case .fetch: state.isLoading = true return Effect(queue: FetchEffectQueue()) { let response = try await environment.fetch(Request(...)) return Action._didFetch(response) 
 } case let ._didFetch(response): state.isLoading = false return Effect.fireAndForget { print(response) } } } & ff FDU2VFVFΛఆٛ /FXFTU͸࠷৽݅ͷΈ࣮ߦ & ff FDUʹ 
 & ff FDU2VFVFΛઃఆ͠ɺ ෳ਺ͷ෭࡞༻ΛΩϡʔͰ؅ཧ͢Δ

Slide 40

Slide 40 text

&GGFDU2VFVFͷछྨ protocol EffectQueueProtocol: Hashable, Sendable { /// Runs newest or oldest with maxCount. var effectQueuePolicy: EffectQueuePolicy { get } /// Effect starting delay. var effectQueueDelay: EffectQueueDelay { get } } // Rx flatMapLatest / switchLatest protocol Newest1EffectQueueProtocol: EffectQueueProtocol { ... } // Rx flatMapFirst (or RxAction) protocol Oldest1DiscardNewEffectQueueProtocol: EffectQueueProtocol { ... } // Rx flatMapConcat protocol Oldest1SuspendNewEffectQueueProtocol: EffectQueueProtocol { ... }

Slide 41

Slide 41 text

%FGBVMU .FSHF 0MEFTU4VTQFOE/FX /FXFTU 0MEFTU%JTDBSE/FX

Slide 42

Slide 42 text

EffectQueue (Newest1) EffectQueue (Oldest1DiscardNew) EffectQueue (Oldest1SuspendNew) Store Reducer State Effect Action View Feedback Action Running Effects Environment

Slide 43

Slide 43 text

Actomaton Λ࢖ͬͨ 
 ΞϓϦͷઃܭࢦ਑

Slide 44

Slide 44 text

"DUPNBUPOΛ࢖ͬͨΞϓϦͷઃܭࢦ਑ ؔ਺ܕϓϩάϥϛϯάํࣜ 4JOHMF4PVSDFPG5SVUI w"DUJPO 4UBUF 3FEVDFSͷ߹੒ͱ4UPSFͷ෼ղɾ࠷దԽ w௕ॴɿঢ়ଶͷू໿؅ཧɺ୹ॴɿ߹੒ͱ෼ղͷ೉қ౓͕ߴ͍ ΦϒδΣΫτࢦ޲ํࣜ .VMUJ7JFX.PEFMT w4UPSFΛ7JFX.PEFMͱಉҰࢹ͠ɺ߹੒ͱ෼ղΛߦΘͳ͍ w௕ॴɿ୭Ͱ΋ཧղͰ͖Δɺ୹ॴɿঢ়ଶͷ෼ࢄ؅ཧ ΞϓϦશମͰ 4UPSF͕ͭ ֤ը໘͝ͱʹ 4UPSFΛ༻ҙ

Slide 45

Slide 45 text

ؔ਺ܕϓϩάϥϛϯάํࣜ 
 (Single Source of Truth)

Slide 46

Slide 46 text

Reducer State Action CounterView Store 4UPSFΛ࡞Γɺ 7JFXʹ࣋ͨͤΔ (CounterLogic) 4UPSF ঢ়ଶ؅ཧΦϒδΣΫτ Λߏ੒͢ΔͨΊͷܕͱϩδοΫ ؔ਺ܕελΠϧ (߹੒͠΍͍͢) ΦϒδΣΫτࢦ޲ (߹੒͠ʹ͍͘) Reducer State Action

Slide 47

Slide 47 text

Reducer State Action (CounterLogic) Reducer State Action (DownloaderLogic) Reducer State Action (PhysicsLogic) Environment Environment Environment ֤αϯϓϧը໘ͷϩδοΫ

Slide 48

Slide 48 text

(AppLogic) Environment Counter Environment Downloader Environment Physics Environment Reducer Counter Reducer Downloader Reducer Physics Reducer State Counter State Downloader State Physics State Action Counter Action Downloader Action Physics Action "DUJPO 4UBUF 3FEVDFS &OWJSPONFOU୯ҐͰ߹੒

Slide 49

Slide 49 text

Reducer State Action AppView Store (AppLogic) ΞϓϦͷશͯΛ໢ཏ͢Δ 
 ܕͱϩδοΫ Reducer State Action Environment Environment 4JOHMF4PVSDF PG5SVUIͳ །Ұਆͷ஀ੜ

Slide 50

Slide 50 text

CounterView (Computed) Store Counter Reducer Counter State Counter Action Counter Environment AppView Store App Reducer App State App Action App Environment ֤ը໘͸ ΞϓϦશମͷ ৘ใΛ஌Βͳ͍ ૄ݁߹ ࢠཁૉʹม׵ 4JOHMF 4PVSDF PG5SVUI 4UPSF෼ղ ೿ੜ4UPSF ݩͷ4UPSF͔Β ࢉग़ͨ͠΋ͷ

Slide 51

Slide 51 text

CounterView (Child)Store Counter Reducer Counter State Counter Action Counter Environment AppView Store App Reducer App State App Action App Environment ֤ը໘͸ ΞϓϦશମͷ ৘ใΛ஌Βͳ͍ ࢠཁૉʹม׵ 4JOHMF 4PVSDF PG5SVUI 4UPSF෼ղ ΦϒδΣΫτ߹੒ͷ୅ΘΓʹ ؔ਺ (ܕͱReducer) Λ߹੒ Single Source of Truth ͳ 
 ΦϒδΣΫτΛ֤ը໘ʹ෼ղ͢Δ

Slide 52

Slide 52 text

// App.Action enum Action: Sendable { case changeCurrent(State.Current?) // NavigationView ੾Γସ͑࣌ʹൃՐ case counter(Counter.Action) case gameOfLife(GameOfLife.Root.Action) case physics(PhysicsRoot.Action) case downloader(Downloader.Action) ... } // App.State struct State: Equatable, Sendable { var current: Current? // ݱࡏදࣔதͷը໘ʢnil ͷ৔߹ɺHome ը໘ʣ enum Current: Equatable, Sendable { case counter(Counter.State) case gameOfLife(GameOfLife.Root.State) case physics(PhysicsRoot.State) case downloader(Downloader.State) ... } } ֤ը໘ͷΞΫγϣϯΛ શମͷΞΫγϣϯʹ߹੒ ֤ը໘ͷঢ়ଶΛ શମͷঢ়ଶʹ߹੒ 
 /05&֤ը໘ͷঢ়ଶ͸ 
 ಉ࣌ʹଘࡏ͠ͳ͍ͷͰ FOVNͰఆٛ͢Δ

Slide 53

Slide 53 text

let appReducer = Reducer.combine( Counter.reducer .contramap(action: /Action.counter) .contramap(state: /State.Current.counter) .contramap(state: \State.current) .contramap(environment: { _ in () }), GameOfLife.Root.reducer() .contramap(action: /Action.gameOfLife) .contramap(state: /State.Current.gameOfLife) .contramap(state: \State.current) .contramap(environment: { $0.gameOfLife }), PhysicsRoot.reducer .contramap(action: /Action.physics) .contramap(state: /State.Current.physics) .contramap(state: \State.current) .contramap(environment: { .init(timer: $0.timer) }), Downloader.reducer .contramap(action: /Action.downloader) .contramap(state: /State.Current.downloader) .contramap(state: \State.current) .contramap(environment: \.downloader), ... // ҎԼɺ߹੒͍ͨ͠ը໘ͷ਺͚ͩม׵ίʔυΛॻ͘ ) ֤ը໘ͷ3FEVDFSΛ ࢠը໘͔Β਌ը໘ʹ޲͚ͯ ม׵ܭࢉͯ݁͠߹͢Δ 5$"ͷQVMMCBDL ؔ਺ܕϓϩάϥϛϯάʹ͓͚Δ 
 ൓มؔख DPOUSBNBQ -FOT 1SJTNΛ࢖͍ͬͯΔ J04%$ϓϨθϯࢀর

Slide 54

Slide 54 text

@MainActor struct AppView: View { let store: Store var body: some View { NavigationView { WithViewStore(store.map(state: \.common)) { viewStore in List(exampleList, id: \.exampleTitle) { example in navigationLink(example: example) } } } } func navigationLink(example: Example) -> some View { WithViewStore(store.map(state: \.current)) { viewStore in NavigationLink( destination: example.exampleView(store: self.store), isActive: viewStore .binding(onChange: Action.changeCurrent) .transform(...) ) { ... } } } } TUPSFNBQͰࢠ DPNQVUFE 4UPSFΛ࡞੒ 5$"ͷ4UPSFTDPQF

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

ΦϒδΣΫτࢦ޲ํࣜ 
 (Multi-ViewModels)

Slide 57

Slide 57 text

ViewController ViewModel VC Builder Router UseCase APIClient func showNext State VC Builder Shows next ViewController Owns Weakly references Calls Builds #VJMEFS6TF$BTF3PVUFSύλʔϯ

Slide 58

Slide 58 text

// ैདྷઃܭɿ ViewController, ViewModel, UseCase, Router Λͭͳ͗͜ΉϏϧμʔํࣜ func build(apiClient: any APIClientProtocol) -> UIViewController { let useCase = UseCase(apiClient: apiClient) let viewModel = ViewModel(useCase: useCase) let viewController = ViewController(viewModel: viewModel) let router = Router(viewController: viewController) viewController.router = router return viewController } // ViewController ͕ݺͼग़͢ϧʔςΟϯάॲཧ struct Router: RouterProtocol { weak var viewController: UIViewController? func handleRoute1(...) { ... } // ը໘ભҠύλʔϯ1 func handleRoute2(...) { ... } // ը໘ભҠύλʔϯ2 }

Slide 59

Slide 59 text

ViewController RouteStore VC Builder Router (closure) Environment fetch Reducer State Action Route Publisher VC Builder Shows next ViewController Sends Route Weakly references Builds 3PVUF4UPSF JO"DUPNBUPO

Slide 60

Slide 60 text

// Actomaton RouteStore Λ࢖ͬͨ Environment, Store, Router ͷͭͳ͗͜Έ func build(apiClient: any APIClientProtocol) -> UIViewController { let environment = Environment(fetch: { request in return apiClient.fetch(request) }) let store = RouteStore( state: State(), reducer: reducer environment: environment ) let viewController = ViewController(store: store) store.subscribeRoutes { [weak viewController] route in ... } // ը໘ભҠॲཧ return viewController } // ը໘ભҠύλʔϯͷܕΛ༻ҙ͢Δ enum Route { case route1(...) case route2(...) } ϧʔςΟϯάॲཧ͕Ͱ͖Δ 3PVUF4UPSFΛ࢖͏ &OWJSPONFOU͸ 
 6TF$BTFͱಉ͡ %*Մೳ

Slide 61

Slide 61 text

let reducer = Reducer { action, state, environment in switch action { case .fetch: state.isLoading = true return Effect(queue: FetchEffectQueue()) { let response = try await environment.fetch(Request(...)) return Action._didFetch(response) } case let ._didFetch(response): state.isLoading = false return Effect.fireAndForget { print(response) } case .showNext: return Effect.fireAndForget { environment.sendRoute(Route.route1(...)) } } } 3FEVDFSͷதͷFOJWPSONFOU͕ TFOE3PVUFΛ࢖͏͜ͱ͕Ͱ͖Δ

Slide 62

Slide 62 text

Action 􀉭 Real View Reducer State Virtual View Effect 💥 Runtime Action 􀉭 Real View Reducer State Virtual View Effect 💥 Runtime Screen 1 Screen 2 ֤ը໘͝ͱʹ 
 &MN"SDIJUFDUVSF3VOUJNF 
 3PVUF4UPSF Λ༻ҙ͢Δ

Slide 63

Slide 63 text

Action 􀉭 Real View Reducer State Virtual View Effect 💥 Runtime Action 􀉭 Real View Reducer State Virtual View Effect 💥 Runtime Screen 2 Shows next ViewController + Messaging forward Messaging backward sendRoute sendRoute ը໘ؒͷσʔλͷ΍ΓऔΓ ը໘ભҠΛؚΉ ΛTFOE3PVUFΛ࢖࣮ͬͯߦ

Slide 64

Slide 64 text

ΞΫλʔϞσϧͱ Elm Architecture ͷ༥߹

Slide 65

Slide 65 text

≒ ΦϒδΣΫτࢦ޲ͱ ؔ਺ܕϓϩάϥϛϯάͷ༥߹

Slide 66

Slide 66 text

Msg (Action) 􀉭 Real View Update (Reducer) State Virtual View view (render) Diff (New State) User Action Cmd (Effect) 💥 Runtime I/O Run Side-effect

Slide 67

Slide 67 text

Msg (Action) 􀉭 Real View Update (Reducer) State Virtual View view (render) Diff (New State) User Action Cmd (Effect) 💥 Runtime I/O Run Side-effect ؔ਺ܕϓϩάϥϛϯά

Slide 68

Slide 68 text

Msg (Action) 􀉭 Real View Update (Reducer) State Virtual View view (render) Diff (New State) User Action Cmd (Effect) 💥 Runtime I/O Run Side-effect ΦϒδΣΫτࢦ޲ ϓϩάϥϛϯά

Slide 69

Slide 69 text

ϜʔΞɾϚγϯ&MN"SDIJUFDUVSF indirect enum Moore { case runMoore(A, I -> Moore) } Moore ≅ (View, Action -> Moore) ≅ νX. (View, Action -> X) // ࠷େෆಈ఺ νX.F = ∃S. (S, (S -> F)) ≅ ∃S. (S, (S -> (View, Action -> S))) ≅ ∃S. (S, S -> View, S -> Action -> S) ≅ ∃S. (initialState: S, reducer: S -> Action -> S, render: S -> View) ϜʔΞɾϚγϯ ೖྗʹґଘ͠ͳ͍ঢ়ଶػց ৗʹग़ྗՄೳͳ"ͱ ೖྗ*͔Βࣗ਎΁ͷঢ়ଶભҠͷ 
 ϖΞΛ༨ؼೲతʹఆٛͨ͠ܕ J04%$ϓϨθϯࢀর

Slide 70

Slide 70 text

Msg (Action) 􀉭 Real View Update (Reducer) State Virtual View view (render) Diff (New State) User Action Cmd (Effect) 💥 Runtime I/O Run Side-effect Elm Architecture = ॳظঢ়ଶ × Reducer × render

Slide 71

Slide 71 text

Comonadic UI = Comonad × ࢀরΦϒδΣΫτ ʮ$PNPOBE ؔ਺ܕ ʯͱʮࢀরʯΛ࢖ͬͯ ΦϒδΣΫτࢦ޲6*ͷੈքΛ࠶ݱ͢Δ

Slide 72

Slide 72 text

Msg (Action) 􀉭 Real View Update (Reducer) State Virtual View view (render) Diff (New State) User Action Cmd (Effect) 💥 Runtime I/O Run Side-effect

Slide 73

Slide 73 text

Msg (Action) 􀉭 Real View Update (Reducer) State Virtual View view (render) Diff (New State) User Action Cmd (Effect) 💥 Runtime I/O Run Side-effect Comonad ʹΑΔ ؔ਺ܕϓϩάϥϛϯά

Slide 74

Slide 74 text

Msg (Action) 􀉭 Real View Update (Reducer) State Virtual View view (render) Diff (New State) User Action Cmd (Effect) 💥 Runtime I/O Run Side-effect ΞΫλʔϞσϧ (ࢀর) ʹΑΔ ΦϒδΣΫτࢦ޲ ϓϩάϥϛϯά

Slide 75

Slide 75 text

Actomaton = Moore (Elm) × ΞΫλʔϞσϧ

Slide 76

Slide 76 text

༨ஊ ϛʔϦɾϚγϯΛ࢖ͬͨΦϒδΣΫτࢦ޲ͷղऍ wDG)BTLFMMͰͷ߹੒ՄೳͳΦϒδΣΫτͷߏ੒ͱͦͷԠ༻ ໦ԼҮষ ࢁຊ࿨඙ w0CKFDU͸ʮܭࢉޮՌͷݍʹ͓͚Δࣹʯͱͯ͠ѻ͑Δ ߹੒Մೳ wΧϓηϧԽɾܧঝɾΦʔόʔϥΠυΛ࣮ݱ͢Δ indirect enum Mealy { case runMealy(I -> (A, Mealy)) } ϛʔϦɾϚγϯ ೖྗ*͕൐ͬͯ ग़ྗ"ͱࣗ਎΁ͷঢ়ଶભҠ Λฦ͢ɺ༨ؼೲతʹఆٛͨ͠ܕ indirect enum Object { case runObject(Msg -> IO<(A, Object)>) } .TH͔Β*0΁ͷࣗવม׵

Slide 77

Slide 77 text

༨ஊ ϛʔϦɾϚγϯΛ࢖ͬͨΦϒδΣΫτࢦ޲ͷղऍ wDG)BTLFMMͰͷ߹੒ՄೳͳΦϒδΣΫτͷߏ੒ͱͦͷԠ༻ w0CKFDU͸ʮܭࢉޮՌͷݍʹ͓͚Δࣹʯͱͯ͠ѻ͑Δ ߹੒Մೳ wΧϓηϧԽɾܧঝɾΦʔόʔϥΠυΛ࠶ݱ͢Δ indirect enum Mealy { case runMealy(I -> (A, Mealy)) } ϛʔϦɾϚγϯ ೖྗ*͕൐ͬͯ ग़ྗ"ͱࣗ਎΁ͷঢ়ଶભҠ Λฦ͢ɺ༨ؼೲతʹఆٛͨ͠ܕ indirect enum Object { case runObject(Msg -> IO<(A, Object)>) } .TH͔Β*0΁ͷࣗવม׵ ؔ਺ܕϓϩάϥϛϯά͔Β ΦϒδΣΫτࢦ޲΁ ʙ ༨ؼೲɾ༨୅਺ɾঢ়ଶػց ʙ

Slide 78

Slide 78 text

·ͱΊ

Slide 79

Slide 79 text

·ͱΊ wΞΫλʔϞσϧͱ&MN"SDIJUFDUVSF w"DUPNBUPO ͭͷܭࢉϞσϧͷ༥߹ w&MN"SDIJUFDUVSF͸.77.Λ࠶ݱ͢Δ w ༨ؼೲΛ࢖ͬͨ ؔ਺ܕϓϩάϥϛϯά͸ɺ 
 ঢ়ଶػցΛͳ͢ ΦϒδΣΫτࢦ޲ϓϩάϥϛϯάΛ 
 ࠶ݱ͢Δ

Slide 80

Slide 80 text

Thanks! Yasuhiro Inami @inamiy