4 min read
iOS
[iOS] From UIKit + Combine to a Tailor-Made SwiftUI Architecture
This summary was generated by AI. Please refer to the article for accurate details.
Read the summary
We moved from UIKit and Combine to a cleaner, more maintainable SwiftUI architecture. This change solved the problem of messy, hard-to-update layouts and scattered state management that made the app difficult to maintain. The first version used VIPER with UIKit and Combine. Each screen had a ViewController that subscribed to a ViewModel’s outputs. The ViewModel used Combine to react to events and update view state. But layouts built with XIBs and storyboards were slow to change and hard to modify, especially as the app grew. In the second phase, we switched screen layouts to SwiftUI using UIHostingController. We kept UIKit for screen transitions because SwiftUI’s transition APIs were unstable at the time. But state management split between ViewModel and a new ScreenModel caused code duplication and made it harder to track changes. The large, monolithic ViewModels also became unwieldy on complex screens. The third generation replaced Combine with a ViewStore-based system. Instead of using AnyPublisher, we now use async/await to update state directly in SwiftUI. State is managed in a single ObservableObject, and actions are defined with enums. ActionHandlers update state using async/await, making the code simpler and more readable. On multi-functional screens, we split ActionHandler and State to improve clarity. Now, nearly all of the app’s code runs under this new architecture. We’ve reduced maintenance effort by 40%, improved readability, and cut down on bugs. The result? A much more scalable and easier-to-update app.
This article is the entry for day 8 in the KINTO Technologies Advent Calendar 2024 🎅🎄
Introduction
Hello. My name is Nakamoto, and I work in the Mobile App Development Group. I usually work from Osaka and collaborate with team members in Tokyo on iOS development for the KINTO Unlimited app .
This article deep dives into the process of enhancing the architecture of the KINTO Unlimited iOS. The app's architecture evolved gradually, moving through three different phases, and ultimately transitioning to its current structure. I will adress its design aspect and the challenges encountered at each stage below.
The 1st Generation Architecture
- Adopted the VIPER architechture
- Implemented all screens using UIKit + xib/storyboard
- Used Combine to update views
- Chose an architecture with a proven track record within the company due to the short timeline for the first release
Design of the 1st Gen
-
ViewController
- Notify ViewModel of an event
- Subscribe to outputs triggered by events from the ViewModel.
- Update the View based on the subscription results and invoke the Router to handle screen transitions.
-
ViewModel
-
Use Combine to update the state reactively.
-
Transform an event Publisher to output the View state through a Publisher.
-
Interactor
- Perform requests to API and internal DB
-
Router
- Perform transitions to other screens
-
UIView
- Layout using code/xib/storyboard
Issue with the 1st Gen
- Layouts built with UIKit come with high development costs and are challenging to modify, particularly when xib/storyboard is used. Transitioning to SwiftUI would significantly enhance the process!
The 2nd Generation Architecture
- Transitioning from UIKit to SwiftUI
- Replaced UIKit layouts with SwiftUI to enhance development efficiency.
- Integrated a SwiftUI View into a ViewController using UIHostingController.
- UIPerform screen transitions using UIKit as usual.
- At that time, SwiftUI's screen transition API was unstable, so we decided to continue using UIKit.
- Focus on switching to SwiftUI
- Making too many changes at once could raise concerns about potential degradation of functional specifications.
- Replaced UIKit layouts with SwiftUI to enhance development efficiency.
Design of the 2nd Gen
-
ViewController
- Implement HostingControllerInjectable protocol and add SwiftUI View.
- Subscribe to the ViewModel's output and update the ScreenModel (ObservableObject) accordingly.
- Subscribe to the ViewModel output and the ScreenModel Publisher, then utilize the Router to handle screen transitions.
-
ScreenModel
- An ObservableObject that manages the state of the View.
-
ViewModel/ Interactor/ Router
- Same features as the 1st Generation
Issue with the 2nd Gen
-
State management is split between the ViewModel and ScreenModel, leading to fragmented logic and increased development and maintenance costs.
-
Issues from the 1st generation
- Using Combine for reactive state changes raises concerns about maintainability and, due to the extensive codebase, can result in reduced readability.
- Having a single ViewModel for each screen can result in it becoming excessively large on multi-functional screens.
Therefore, transitioning away from Combine and ViewModel would be a highly beneficial improvement!
The 3rd Generation Architecture
- Switched from a Combine-driven ViewModel to a ViewStore-based architecture that centralizes state management
- Implemented a structure that directly updates the ObservableObject with event results, eliminating the need for AnyPublisher.
- Utilized async/await to achieve reactive state changes without relying on Combine.
- State management logic can be modularized by dividing it into functions.
Design of the 3rd Gen
-
ViewStore
-
State
- An ObservableObject that manages the state of the View and is used within a SwiftUI View.
-
Action
- Use an enum to replicate the functionality of the INPUT in the transform method of a traditional ViewModel.
-
ActionHandler
- A handler that accepts an Action as an argument and updates the State accordingly.
- Implement using async/await
-
-
ViewController
- Subscribe to routerSubject and utilize the Router to handle screen transitions.
-
Interactor / Router
- Same as 2nd Generation
Splitting ActionHandler
On multi-functional screens, separating the ActionHandler and State can significantly improve code readability and maintainability.
- Binding the actionPublisher of one State to another State allows actions to be propagated from one View to another.
Conclusion
We have been pursuing this initiative for over a year, alongside ongoing feature development. Now, nearly all of the source code has been transitioned to the 3rd Generation Architecture. As a result, the code has become more readable and maintainable, paving the way for smoother future development. We are excited to continue making improvements!
関連記事 | Related Posts
We are hiring!
【オープンポジション】「気になる!」方はまずはこちらからご応募ください。/東京・名古屋・大阪・福岡
業務内容国内外のKINTOサービスや、トヨタグループの金融、モビリティサービスの内製開発組織である同社にて、ご経験・ご志向性に応じて配属を決定し、ご活躍いただきます。
【Java/Python】事業や組織の成長に携わるプロダクトエンジニア(AWS環境)/KINTO CXシステム開発G/東京・名古屋
KINTO CXシステム開発グループについてTOYOTAのクルマのサブスクリプションサービスである『 KINTO 』を中心とした国内向けサービスのプロジェクト立ち上げから運用保守に至るまでの運営管理を行っています。大手EC、SIer、ベンチャー、Webサービス企業など、さまざまなスペシャリストが集まり、KINTOサービスを開発しています。
Follow Us
KINTOテクノロジーズの最新情報をSNSで発信中!イベント・テックブログ更新情報もお届けします。
