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を新規プロダクトで用いた話
Search
sakaue
May 19, 2021
Programming
560
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
SwiftUIを新規プロダクトで用いた話
sakaue
May 19, 2021
More Decks by sakaue
See All by sakaue
先端事例「Amazon Chime SDK」を用いた1on1ビデオ通話の構築
tosaka07
0
1.7k
UICollectionView 基礎編
tosaka07
0
140
Other Decks in Programming
See All in Programming
AIチームを指揮するOSS「TAKT」活用術 / How to Use “TAKT,” an OSS Tool for Orchestrating AI Teams
nrslib
6
830
密結合なバックエンドから TypeScript のコードを生成する
kemuridama
1
740
RTSPクライアントを自作してみた話
simotin13
0
490
代数的データ型って何が嬉しいの? #frontend_phpcon_do
kajitack
8
3.2k
気づいたらRubyで100作品 ー クリエイティブコーディングが生活の一部になるまで / 100 Ruby Sketches Later: How Creative Coding Became Part of My Life
chobishiba
3
540
柔軟なPDFレイアウトエディタを支える型システム設計 — Discriminated UnionとConditional Typeの実践
minako__ph
4
1.4k
TypeSpec で繋ぐ複数プロダクトの型安全
maroon8021
1
380
脅威をエンジニアリングの糧にして――現場編 / Turning Threats into Engineering Fuel — Field Edition
nrslib
0
250
JavaDoc 再入門
nagise
0
280
Javaの型とAI時代に型が大事な理由 / java types and type in AI era
kishida
2
110
OSもどきOS
arkw
0
450
Make SRE Operations Easier with Azure SRE Agent
kkamegawa
0
4.1k
Featured
See All Featured
Getting science done with accelerated Python computing platforms
jacobtomlinson
2
220
Building a Scalable Design System with Sketch
lauravandoore
463
34k
Producing Creativity
orderedlist
PRO
348
40k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
17k
Accessibility Awareness
sabderemane
1
130
Embracing the Ebb and Flow
colly
88
5.1k
First, design no harm
axbom
PRO
2
1.2k
RailsConf 2023
tenderlove
30
1.5k
DBのスキルで生き残る技術 - AI時代におけるテーブル設計の勘所
soudai
PRO
65
55k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
320
Lightning Talk: Beautiful Slides for Beginners
inesmontani
PRO
2
570
Optimising Largest Contentful Paint
csswizardry
37
3.7k
Transcript
4XJGU6*Λ ৽نϓϩμΫτͰ༻͍ͨ $"TXJGUʙ͍·ߟ͑Δ࣍ੈͷઃܭʙ
ࣗݾհͱϓϩμΫτ 4XJGU6*ͷྑ͞ͱ࠾༻ܦҢ Ͳ͏࡞͍͔ͬͯͬͨ w ڞଘΞʔΩςΫνϟ෦ ·ͱΊ
࣍
ࣗݾհͱϓϩμΫτ 4XJGU6*ͷྑ͞ͱ࠾༻ܦҢ Ͳ͏࡞͍͔ͬͯͬͨ w ڞଘΞʔΩςΫνϟ෦ ·ͱΊ
࣍
J04%FWFMPQFS w גࣜձࣾ$". w ಈը৴ΞϓϦ w ήολʔζ൧ాͷ͍ w 'FOTJ w
ౡݝग़ ࡔ্ᠳޛ4BLBVF4IPHP UPTBLB@UFDI UPTBLB
None
None
ֹ݄՝ۚ 'FOTJɺ ͋ΒΏΔදݱ׆ಈΛ͢ΔํΛ શྗͰԠԉ͢ΔαʔϏεͰ͢ɻ ΦϑΟγϟϧαΠτ ݶఆίϛϡχςΟ άοζൢച POτʔΫ
FUD
͜Μͳײ͡ͷ ΞϓϦͰ͢
ࣗݾհͱϓϩμΫτ 4XJGU6*ͷྑ͞ͱ࠾༻ܦҢ Ͳ͏࡞͍͔ͬͯͬͨ w ڞଘΞʔΩςΫνϟ෦ ·ͱΊ
࣍
4XJGU6*ͷྑ͞ w એݴత w ࠶ར༻Λҙࣝͨ͠࡞Γ w 1SFWJFX w "VUP-BZPVU4UPSZCPBSE9JC͔Βͷղ์ w
ίʔυϨϏϡʔ͍͢͠
4XJGU6*Λ࠾༻ͨ͠ܦҢ w 8FCϢʔβʔJ04Ҏ্͕΄ͱΜͲͩͬͨ w 4XJGU6*͕ൃද͞ΕͯҰ͘Β͍ܦ͍ͬͯͯɺ ίϛϡχςΟͷݟ͋Δఔஷ·͍ͬͯͨ w ৽͍ٕ͠ज़ʹ৮ΕΔػձΛ࡞Γ͔ͨͬͨ w
"VUP-BZPVU͔Βͷղ์
։ൃώετϦʔ ։ൃελʔτ ΞϓϦϦϦʔε POϦϦʔε ݱࡏ ։ൃਓ
ਓ ਓ ਓ 4XJGU6*
ࣗݾհͱϓϩμΫτ 4XJGU6*ͷྑ͞ͱ࠾༻ܦҢ Ͳ͏࡞͍͔ͬͯͬͨ w ڞଘΞʔΩςΫνϟ෦ ·ͱΊ
࣍
Ͳ͏࡞͍͔ͬͯͬͨ ·ͣ J04ͷ4XJGU6*ͱدΓఴ͏🤝
دΓఴͬͨ݁Ռ👻 w 4XJGU6*ʹରԠͯ͠ͳ͍෦͕ଟ͍ w J04Ͱ͢Βόʔδϣϯ͝ͱʹڍಈ͕ҧ͏ w ͱ͜ΖͲ͜ΖόάͬΆ͍ڍಈ
دΓఴͬͨ݁Ռ👻 6*,JU🤝 4XJGU6*🥲
دΓఴͬͨ݁Ռ👻 6*,JU🤝 4XJGU6*🥲 6*,JUΛجຊͱ͠ɺ4XJGU6*Λ෦తʹͬͨ΄͏͕ ϦεΫ͕͍ɻ
6*,JUͱ4XJGU6*ͷڞଘ w 6*7JFX$POUSPMMFSʹ 4XJGU6*7JFXΛͷ͚ͬΔ w ભҠ6*,JUΛ͏ w 7JFX.PEFM͔Β
ભҠΠϕϯτΛड͚औΔͷ 6*7JFX$POUSPMMFS 6*7JFX$POUSPMMFS 4XJGU6* 7JFX 7JFX.PEFM
4XJGU6*7JFXΛ7$ʹͷ͚ͬΔ class SampleViewModel: ObservableObject { @Published var title: String =
"" func fetch() { self.title = "Hello, World!" } } struct SampleView: View { @ObservedObject var viewModel: SampleViewModel var body: some View { Text(viewModel.title) } }
4XJGU6*7JFXΛ7$ʹͷ͚ͬΔ class SampleViewController: UIViewController { let viewModel = SampleViewModel() override
func viewDidLoad() { super.viewDidLoad() let vc = UIHostingController( rootView: SampleView(viewModel: viewModel) ) addChild(vc) view.addSubview(vc.view) NSLayoutConstraint.activate([/* ... */]) vc.didMove(toParent: self) viewModel.fetch() } }
ભҠ class SampleViewModel: ObservableObject { @Published var presentDetail: String? func
presentDetail(id: String) { presentDetail = id } } struct SampleView: View { @ObservedObject var viewModel: SampleViewModel var body: some View { Button("present to Detail") { viewModel.presentDetail(id: "1") } } }
ભҠ class SampleViewController: UIViewController { var cancellables = Set<AnyCancellable>() let
viewModel = SampleViewModel() override func viewDidLoad() { super.viewDidLoad() viewModel.$presentDetail .compactMap { $0 } .sink { [weak self] id in self?.present( DetailViewController(id: id), animated: true, completion: nil ) } .store(in: &cancellables) } }
ڞଘͷϝϦοτ w ͍͔Α͏ʹͭ͘ΕΔ w τϥϯδγϣϯΞχϝʔγϣϯ w ϧʔςΟϯά w ճస੍ޚ w
Ұը໘͝ͱʹཧͰ͖Δ
ΞʔΩςΫνϟ
ΞʔΩςΫνϟ w ϓϩμΫτνʔϜʹ߹ΘͤΔ w 6OJEJSFDUJPOBM w 4PVSDFPGUSVUI w 'FOTJͰ$PNCJOFʹஔ͖ ͑ͨ3FBDUPS,JUΛࣗ࡞
IUUQTEFWFMPQFSBQQMFDPNEPDVNFOUBUJPOTXJGUVJTUBUFBOEEBUB fl PX
3FBDUPS,JU w νʔϜͰͷܦݧ͕͋ͬͨ w 6OJEJSFDUJPOBM͕4XJGU6* ʹ߹ͬͯͨ w طଘͷઃܭ͔Β4XJGU6*ʹ ߹ΘͤΔͷଟগͷख͕ؒ͋ Δ
IUUQTHJUIVCDPN3FBDUPS,JU3FBDUPS,JU
3FBDUPS,JU public protocol ReactorCore: class { associatedtype Action associatedtype Mutation
= Action associatedtype State func transform(action: AnyPublisher<Action, Never>) -> AnyPublisher<Action, Never> func mutate(state: State, action: Action) -> AnyPublisher<Mutation, Never> func transform(mutation: AnyPublisher<Mutation, Never>) -> AnyPublisher<Mutation, Never> func reduce(state: State, mutation: Mutation) -> State func transform(state: AnyPublisher<State, Never>) -> AnyPublisher<State, Never> } class Reactor<Core: ReactorCore> { var state: CurrentValueSubject<Core.State, Never> init(initialState: Core.State, core: Core) func send(_ action: Core.Action) }
3FBDUPS,JU class SampleCore: ReactorCore { enum Action { case increment,
decrement } enum Mutation { case setNumber(Int) } struct State { var number: Int } func mutate(state: State, action: Action) -> AnyPublisher<Mutation, Never> { switch action { case .increment: return Just(Mutation.setNumber(state.number + 1)).eraseToAnyPublisher() case .decrement: return Just(Mutation.setNumber(state.number - 1)).eraseToAnyPublisher() } } func reduce(state: State, mutation: Mutation) -> State { var state = state switch mutation { case .setNumber(let number): state.number = number } return state } }
3FBDUPS,JU class SampleViewController: UIViewController { var cancellables = Set<AnyCancellable>() let
reactor: Reactor<SampleCore> override func viewDidLoad() { super.viewDidLoad() bind(reactor: reactor) } func bind(reactor: Reactor<SampleCore>) { let store = ViewStore(reactor) store.publisher.number .sink { (number) in // UIKit ʹόΠϯυ } .store(in: &cancellables) let view = UIHostingController( rootView: SampleView(viewStore: store) ) // addSubView... } } struct SampleView: View { @ObservedObject var viewStore: ViewStore< SampleCore.State, SampleCore.Action > var body: some View { HStack { Button("-") { viewStore.send(.decrement) } Text("\(viewStore.number)") Button("-") { viewStore.send(.increment) } } } }
7JFX4UPSF @dynamicMemberLookup public class ViewStore<State, Action>: ObservableObject { public let
objectWillChange = ObservableObjectPublisher() public private(set) var state: State { willSet { objectWillChange.send() } } public let publisher: StatePublisher<State> private var viewCancellable: AnyCancellable? private let _send: (Action) -> Void init<Core: ReactorCore>( _ reactor: Reactor<Core> ) where Core.State == State, Core.Action == Action { publisher = StatePublisher(reactor.$state) state = reactor.state _send = { [weak reactor] in reactor?.send($0) } viewCancellable = publisher .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] in self?.state = $0 }) } public subscript<LocalState>(dynamicMember keyPath: KeyPath<State, LocalState>) -> LocalState { state[keyPath: keyPath] } public func send(_ action: Action) { DispatchQueue.main.async { [weak self] in self?._send(action) } } w 5$"ͰΘΕ͍ͯΔ w EZOBNJD.FNCFS-PPLVQ w ॏෳͷআ w #JOEJOH IUUQTHJUIVCDPNQPJOUGSFFDPTXJGUDPNQPTBCMFBSDIJUFDUVSF
෦
࡞ͬͨ w 5FYU w #VUUPO w 5FYU'JFME w *NBHF w
ͱ͔ˠҎ֎ͷ෦ 4XJGU6*Ͱ࡞ͬͨ෦ ࡞Βͳ͔ͬͨ w 5BC7JFX w 8FC7JFX w 5FYU7JFX w $PMMFDUJPO7JFX w $VTUPN7JFXT w 6*,JU-JCSBSZ -PUUJF
ૉʹ 6*7JFX3FQSFTFOUBCMF 6*7JFX$POUSPMMFS3FQSFTFOUBCMF Λ͍·͠ΐ͏ ඇରԠͰ4XJGU6*ʹΈࠐΈ͍ͨ struct WebView: UIViewRepresentable
{ var url: URL func makeUIView(context: Context) -> WKWebView { return WKWebView(frame: .zero) } func updateUIView(_ uiView: WKWebView, context: Context) { uiView.load(URLRequest(url: url)) } }
$PMMFDUJPO7JFXʹٻΊΔͷ w 6*$PMMFDUJPO7JFXY4XJGU6*🤔 w 6*$PMMFDUJPO7JFX$PNQPTJUJPOBM-BZPVU͍͍ͨ w 6*$PMMFDUJPO7JFX%J ff BCMF%BUB4PVSDF͍͍ͨ
$PMMFDUJPO7JFXY4XJGU6* 6*$PMMFDUJPO7JFX$FMM 6*$PMMFDUJPO7JFX 4XJGU6*7JFX 4FDUJPO*% JE4FDUJPO*% EBUB-JTUσʔλྻ
EBUB*EσʔλͷҰҙੑΛද͢,FZ1BUI DPOUFOUσʔλΛݩʹੜ͢Δ7JFX 4UBUF 4FDUJPO $FMM
3FOEFSFS public protocol CollectionRenderer { associatedtype ID: Hashable associatedtype Action
associatedtype State func render(state: State, store: ViewStore<State, Action>) func createSections(state: State, store: ViewStore<State, Action>) -> [Section<ID>] func createLayoutSection( id: ID, environment: NSCollectionLayoutEnvironment ) -> NSCollectionLayoutSection }
3FOEFSFS class TodoListCollectionRenderer: CollectionRenderer { typealias State = ToDoListReactor.State typealias
Store = ViewStore<State, ToDoListReactor.Action> enum ID: Int { case todo case empty } @SectionBuilder func createSections(state: State, store: Store) -> [Section<ID>] { if state.todoList.isEmpty { empty() } else { todo(state: state, store: store) } } func createLayoutSection(id: ID, env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { switch id { case .list: return .list(itemWidth: .fractionalWidth(1.0), itemHeight: .estimated(.s222)) case .empty: return .list(itemWidth: .fractionalWidth(1.0), itemHeight: .fractionalHeight(1.0)) } } } func todo(state: State, store: Store) -> Section<ID> { Section( id: ID.todo, data: state.todoList, dataID: \.self ) { todo in TodoItemView( Title: todo.content, done: todo.done ) .onTapGesture { store.send(.showDetail(todo)) } } }
ࣗݾհͱϓϩμΫτ 4XJGU6*ͷྑ͞ͱ࠾༻ܦҢ Ͳ͏࡞͍͔ͬͯͬͨ w ڞଘΞʔΩςΫνϟ෦ ·ͱΊ
࣍
·ͱΊ w J04αϙʔτͭΒ͍ ˠJ04ʹͰ͖ΔͳΒ͠·͠ΐ͏ w શʹ4XJGU6*ʹدͤΔ͜ͱϦεΫ͕Ͱ͔͍ ˠ6*,JUͷಀ͛ΒΕΔઃܭΛ͠·͠ΐ͏ w
88%$ʹظʜʜ🙏
͋Γ͕ͱ͏͍͟͝·ͨ͠🙇