Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
MVVMをベースに複雑な振る舞いを しっかり把握できるアプリ開発
Search
yohei sugigami
August 26, 2015
Technology
14
5.9k
MVVMをベースに複雑な振る舞いを しっかり把握できるアプリ開発
Realm meetup #6 で発表した
Sync iOS開発の舞台裏についてです
プロジェクトの話や、MVVM、ViewBindingなど多義にわたり解説しています
yohei sugigami
August 26, 2015
Tweet
Share
More Decks by yohei sugigami
See All by yohei sugigami
Snapshot Testing in iOS
susieyy
6
3.3k
Redux with iOS
susieyy
0
1.3k
Why use Redux in iOS
susieyy
5
2.7k
ReduxRxを活用したアプリアーキテクチャ
susieyy
8
2.4k
Redux+Rxを活用したiOSアプリアーキテクチャ
susieyy
10
2.1k
Swaggerで始めるAPI定義管理とコードジェネレート
susieyy
14
7.6k
開発中のアプリをXcode9 & Swift4に移行しました
susieyy
0
3.7k
Wantedly People ViewModel and Rx
susieyy
7
7.3k
ReduxDevTools' power to the iOS development
susieyy
0
890
Other Decks in Technology
See All in Technology
多様なデジタルアイデンティティを攻撃からどうやって守るのか / 20251212
ayokura
0
440
生成AI活用の型ハンズオン〜顧客課題起点で設計する7つのステップ
yushin_n
0
140
AWS Security Agentの紹介/introducing-aws-security-agent
tomoki10
0
210
エンジニアリングマネージャー はじめての目標設定と評価
halkt
0
280
打 造 A I 驅 動 的 G i t H u b ⾃ 動 化 ⼯ 作 流 程
appleboy
0
310
re:Invent 2025 ~何をする者であり、どこへいくのか~
tetutetu214
0
210
Oracle Cloud Infrastructure IaaS 新機能アップデート 2025/09 - 2025/11
oracle4engineer
PRO
0
100
会社紹介資料 / Sansan Company Profile
sansan33
PRO
11
390k
エンジニアリングをやめたくないので問い続ける
estie
2
1.2k
.NET 10の概要
tomokusaba
0
100
年間40件以上の登壇を続けて見えた「本当の発信力」/ 20251213 Masaki Okuda
shift_evolve
PRO
1
130
AWSセキュリティアップデートとAWSを育てる話
cmusudakeisuke
0
250
Featured
See All Featured
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.3k
Intergalactic Javascript Robots from Outer Space
tanoku
273
27k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
10
720
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
359
30k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
35
3.3k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
710
Typedesign – Prime Four
hannesfritz
42
2.9k
Why You Should Never Use an ORM
jnunemaker
PRO
61
9.6k
Thoughts on Productivity
jonyablonski
73
5k
Imperfection Machines: The Place of Print at Facebook
scottboms
269
13k
Building Applications with DynamoDB
mza
96
6.8k
Exploring the Power of Turbo Streams & Action Cable | RailsConf2023
kevinliebholz
36
6.2k
Transcript
MVVMΛϕʔεʹෳࡶͳৼΔ͍Λ ͔ͬ͠ΓѲͰ͖ΔΞϓϦ։ൃ yohei SUGIGAMI 10/21 shibuya.swift
None
Wantedly
Sync Messenger 8FC %FTLUPQ"QQ "OESPJE BOEJ04 ϏδωείϛϡχέʔγϣϯಛԽϝοηʔδϯάαʔϏε
Sync iOS 4XJGU .77. '31 1SPNJTF Ϟμϯͳཁૉٕज़
Let’s Try TZOD Ұ൪࠷ॳͷݕࡧ݁Ռʹग़Δͣʂ
Projects Summary
Service Architecture $MJFOU$SPTT1MBUGPSN 4FSWFS.JDSPTFSWJDFT
Team "1* 8FC%FTLUPQ J04 "OESPJE GBTUMBOF
iOS Schedule ݄̑ ݄̒ ݄̓ ϓϩτλΠϓ։ൃ 'JSFCBTFબఆ +0*/ ਃ νʔϜ4MBDLஔ͖͑
ࣾ4MBDLஔ͖͑ +0*/ ຊ։ൃ ࣾ֎ϢʔβϑΟʔυόοΫ ϲ݄ɺ̒ਓ݄͙Β͍ +0*/
High Speed Development 13$MPTFE *TTVFT$MPTFE ݄࣌ $PNNJU
Review ϨϏϡʔΞʔΞαΠϯ 5FTU͕͚ͯ͜Δͷ $JSDMF$*ͷௐࢠ͕ѱ͍ɻɻ
Review Ϟνϕʔγϣϯ61Ͱ ੜ࢈ੑ্
Delivery by fastlane 1VMM 3FR .FSHF .BTUFS %FMJWFSZ %FW5FTUFS .BTUFSʹ.FSHF͢ΔʹλΠϜϦʔͳΞϓϦ
͡Ζ͏͘ΜۘͷGBTUpMF
Dog feeding ։ൃ̎िؒͰ4MBDL͔ΒΓ͑Δ ։ൃऀͰ͋Δ͔ࣗͨͪΒ·ͣϑΝϯʹͳΔ ։ൃ̍ϲ݄ͰࣾͰͬͯΒ͍ϑΟʔυόοΫΛΒ͏ ։ൃ̎ϲ݄Ͱۙͳࣾ֎ͷํʑʹͬͯΒ͍͞Βʹ ଟ͘ͷϑΟʔυόοΫΛ͖·ͨ͠
Very Slow Swift Compile ̍ਓ.BD#PPL1SP̎Ͱ։ൃ ίϯύΠϧͪ࣌ؒΛ༗ޮ׆༻ ܕਪͱΫϩδϟʔΩϟϓνϟ͕ͦ͏
Very Slow Swift Compile .BD#PPL1SP JODI -BUF ͰϑϧίϯύΠϧ࣌ؒ ˠɹ̐ඵ ίʔυΛݟͯ͠ίϯύΠϧߴԽ
ˠɹඵʢඵʣ ˠɹࠩίϯύΠϧͷՄೳੑΞοϓ ܕਪ͠ͳ͍ͰܕΛ໌ه͢Δʢ"SSBZ%JDUJPOBSZʣ ෳͷTXJGUϑΝΠϧΛҰͭʹ·ͱΊΔ ܧঝ͠ͳ͍DMBTTpOBMΛ໌ه͢Δ $BSUBHFରԠͷϥΠϒϥϦ1PETͰΠϯετʔϧ͠ͳ͍
App Architecture
MVVM (෩) $POUSPMMFS 7JFX.PEFM .PEFM 3FBDU,JU 4XJGU5BTL 4XJGUZ+40/ 4XJGU#POE 3FBMN
3BX%BUB 1FSTJTUFOU 4UBUF.BOBHFNFOU -PHJD 7JFX%BUB 6*,JU%FMFHBUF #JOEJOH -JCSBSZ 3FTQPOTJCJMJUZ $SFBUF7JFX 4%8FC*NBHF 7JFX7JFX -BZFS
ViewDataBinding 4UBUF 7JFX $POUSPMMFS 7JFX.PEFM %BUB ͱؔ৺ࣄͷ -PHJD 7JFX 7JFX.PEFMͷ-PHJD
4UBUF %BUBͷมߋ͕ 7JFXʹ͍ͭͯؔ͠ͳ͍ PS #JOE 7JFX4UBUF %BUB͕มߋ ͞ΕͨΒಈతʹը໘Λߋ৽ %BUB 4UBUFͷϓϩηεؔ͠ͳ͍ 7JFX #JOEJOH .FUIPE$BMM
Instane Relation 7JFX7JFX $POUSPMMFS 7JFX.PEFM .PEFM ʜ ʜ 3FGFSFODF #VTJOFTT-PHJD
4VC-PHJD 4IBSF-PHJD 4VC-PHJD %BUB %BUB %BUB %BUB %BUB %BUB .BQQJOH #JOEJOH 7JFX #JOEJOH 1SFTFOUBUJPO-PHJD 7JFX.PEFMଞͷϨΠϠʔͷࢀরΛ࣋ͨͳ͍
Request Data Flow Sequence 4XJGUZ+40/ 3FBMN 4XJGU5BTL 4XJGU#POE 1FSTJTUFODF 7JFX#JOEJOH
"1*
Request ViewBinding 4UBUF3FRVFTUJOH 4UBUF&SSPS JUFNT<> 4UBUF/POF *OEJDBUPS7JFX 3FUSZ7JFX /P%BUB7JFX
Request ViewBinding 4UBUF3FRVFTUJOH 4UBUF&SSPS JUFNT<JUFN JUFN JUFN ʜ> 4UBUF/POF *OEJDBUPS7JFX
3FUSZ7JFX /P%BUB7JFX
Request ViewBinding final class RequestListViewModel<T: Identifier> { let items: DynamicArray<T>
= DynamicArray([]) var requestListState = Dynamic<RequestListState>(.None) var noDataFirstViewHidden: Dynamic<Bool> { let a = indicatorViewHidden.map { $0 == false } let b = requestListFirstState.map { $0 == .Error } return reduce(a, b, c) { $0 || $1 == true } } var indicatorViewHidden: Dynamic<Bool> { let a = requestListFirstState.map { $0 != RequestListState.Requesting } let b = items.map { count($0) > 0 } return reduce(a, b) { $0 || $1 == true } } var retryViewHidden: Dynamic<Bool> { let a = requestListFirstState.map { $0 != RequestListState.Error } let b = items.map { count($0) > 0 } return reduce(a, b) { $0 || $1 == true } } 3FRVFTU4UBUFͱ*UFNTͷঢ়ଶͷΑΔڍಈΛએݴ
Request ViewBinding final class ContactsViewController: UIViewController { var tableViewDataSourceBond: UITableViewDataSourceBond<ContactCell>!
let viewModel = ContactsViewModel() let indicatorView = InstantiateFromNib(IndicatorView) let retryView = InstantiateFromNib(RetryView) let noDataView = InstantiateFromNib(NoDataView) override func viewDidLoad() { super.viewDidLoad() viewModel.requestList.indicatorViewHidden ->> indicatorView.dynHidden viewModel.requestList.retryViewHidden ->> retryView.dynHidden viewModel.requestList.noDataFirstViewHidden ->> noDataView.dynHidden } 7JFX$POUSPMMFSͰ7JFX.PEFMͷ4XJGU#POEͱ7JFXΛ#JOEJOH
Request ViewBinding final class RequestListViewModel<T: Identifier> { typealias RequestTask =
Task<Progress, ResponseCollection<T>, NSError> func requestFirst(task: RequestTask) -> RequestTask { self.requestListState.value = .Requesting task.success { [weak self] (collection: ResponseCollection<T>) -> Void in self?.items.setArray(collection.items) self?.requestListState.value = .None }.failure { [weak self] (errorInfo: RequestTask.ErrorInfo) -> Void in self?.requestListState.value = .Error } return task } 3FRVFTUͷ։࢝ɺਖ਼ৗྃɺҎ্ྃͰ4BUFΛߋ৽ ਖ਼ৗྃ࣌ʹJUFNTΛߋ৽
Request ViewBinding final class RequestListViewModel<T: Identifier> { lazy var stateChangedObserver
= Bond<RequestListState> { [weak self] state in switch state { case .Requesting: UIApplication.sharedApplication().networkActivityIndicatorVisible = true default: UIApplication.sharedApplication().networkActivityIndicatorVisible = false } } init() { requestListState ->| stateChangedObserver } OFUXPSL"DUJWJUZ*OEJDBUPS7JTJCMFΛ4UBUFͰ#JOEJOH
Input with Validation ViewBinding &OBCMF4JHO6Q#VUUPO 7BMJEBUJPO&NBJM1BTTXPSE
Validation ViewBinding final class SignUpViewModel { let email = Dynamic<String>("")
let password = Dynamic<String>("") var emailError: Dynamic<NSError?> { return email.map { Validator.Email($0).validate() } } var passwordError: Dynamic<NSError?> { return password.map { Validator.Password($0).validate() } } var isSignInInput: Dynamic<Bool> { let isEmailValid = emailError.map { nil == $0 } let isPasswordValid = passwordError.map { nil == $0 } return reduce(isEmailValid, isPasswordValid) { $0 && $1 == true } } &NBJMͱ1BTTXPSEͷ7BMJEBUJPOʹΑͬͯ 4JHO6Q͕&OBCMFʹͳΔ͔Λએݴ
Validation ViewBinding final class SignUpViewController { override func viewDidLoad() {
super.viewDidLoad() emailTextField ->> viewModel.email passwordTextField ->> viewModel.password viewModel.signUpState ->| signUpStateChangedObserver viewModel.isSignInInput ->> signUpButton.dynEnabled 7JFX$POUSPMMFSͰ7JFX.PEFMͷ4XJGU#POEͱ7JFXΛ#JOEJOH enum Validator { case Email(String?) case Password(String?) func validate() -> NSError? { switch self { case .Email(var value): return NGRValidator.validateValue(value, named: LocalizedString("Email")) { (validator: NGRPropertyValidator!) in validator.required().msgNil(LocalizedString("is required.")) validator.syntax(NGRSyntax.Email).msgWrongSyntax(NGRSyntax.Email, LocalizedString("is not valid syntax.")) } case .Password(var value): return NGRValidator.validateValue(value, named: LocalizedString("Password")) { (validator: NGRPropertyValidator!) in validator.required().msgNil(LocalizedString("is required.")) validator.minLength(6).msgTooShort(LocalizedString("is too short.")) }
No inherits 7JFX$POUSPMMFS 7JFX 7JFX.PEFM .PEFMʹ͓͍ͯ "CTUSBDUϨΠϠʔΛઃ͚ͳ͍ʢܧঝ͠ͳ͍ʣ ܧঝ͢Δͱɺɺɺ ϩδοΫڍಈΛཧղʹ͠ʹ͍͘
ڞ௨Խͨ͠ίʔυΛมߋ͠ʹ͍͘ SFGT J04ΞϓϦͷઃܭͰ#BTF7JFX$POUSPMMFSͷΑ͏ͳͷ࡞Γͨ͘ͳ͍CZࠓઌੜ IUUQRJJUBDPNZJNBKPJUFNTFGFCECGE
No inherits %FMFHBUF (FOFSJDT .PEFMΛҙͷ1SPUPDPMΛ४ڌͤͯ͞ (FOFSJDTͰϩδοΫΛڞ௨Խ
"TQFDU ϩΪϯά"OBMZUJDTૹ৴ $MBTT ڞ௨ॲཧΛ$MBTTͱͯ͠Γग़͠ ڞ௨Խ͍ͨ͠ίʔυҎԼͷ׆༻Λݕ౼͢Δ
Qiitaʹॻ͖·ͨ͠ IUUQRJJUBDPNTVTJFZZJUFNTBGCCEGG
Enjoy Sync life.
iOS, Android Rails ΤϯδχΞ ืूத
MVVM .7$%JBHSBN 7JFX$POUSPMMFS 7JFX .PEFM 7JFX$POUSPMMFSJTlEPFTBMMUIFUIJOHTz
MVVM .77.%JBHSBN 7JFX 7JFX$POUSPMMFS 7JFX.PEFM .PEFM 7JFX$POUSPMMFSCFDPNFTNBMMFS 7JFXJTTUBUFMFTT