기존 Storyboard 에서 Generic Type 의존성 주입하기에서 큰 이슈가 있었다. 바로 iOS 13 미만 버젼에서 IBOutlet이 연결되지 않아서 앱이 크래쉬가 나는 이슈가 났음..
import UIKit
typealias UIStoryboard = _DependencyInjectionStoryboard
final class _DependencyInjectionStoryboard {
private let storyBoard: UIKit.UIStoryboard
init(name: String, bundle: Bundle?) {
self.storyBoard = UIKit.UIStoryboard.init(
name: name,
bundle: bundle
)
}
func instantiateViewController(
identifier: String,
creator: ((NSCoder) -> UIViewController?)? = nil
) -> UIViewController? {
var viewController: UIViewController? = nil
if #available(iOS 13.0, *) {
guard creator != nil else {
return self.storyBoard.instantiateViewController(
identifier: identifier
)
}
viewController = self.storyBoard.instantiateViewController(
identifier: identifier
) {
creator?($0)
}
} else {
viewController = self.storyBoard.instantiateViewController(
withIdentifier: identifier
)
guard creator != nil else {
return viewController
}
guard let baseViewController = viewController as? BaseViewController,
let coder = baseViewController.coder else {
return viewController
}
viewController = creator?(coder)
}
return viewController
}
}
여기 코드가 문제였다.
저때 화면 전환이 되는 것만 보고 IBOutlet이 연결된 것을 확인하지 않았던게 패착이었다.
StoryBoard로 ViewController를 생성할때 NSCoder객체로 ViewController가 생성되서 13 이상의 버젼에서는 클로져로 coder 객체를 줘서 쉽게 init() 커스텀이 가능했는데 13 미만의 버젼에서는 그런 클로져가 없어서 ViewController를 생성 후 coder를 저장한 뒤 한번더 coder 객체로 ViewController를 만들면 되겠구나 했지만 내부 API에서 내 생각대로 ViewController가 생성되지 않았던 것이다.. 정확히는 ViewController가 생성되긴 했지만, IBOutlet 등 StoryBoard와 연결된 객체가 모두 nil인 상태로 생성되었다.
그런데 생각해보니 ViewModel을 Optional로 생성했는데 꼭 생성자에 viewModel을 주입하지 않아도 됐다는 것을 깨닫고 코드를 수정했다.
import Foundation
import UIKit
typealias UIStoryboard = _DependencyInjectionStoryboard
final class _DependencyInjectionStoryboard {
private let storyBoard: UIKit.UIStoryboard
init(name: String, bundle: Bundle?) {
self.storyBoard = UIKit.UIStoryboard.init(
name: name,
bundle: bundle
)
}
func instantiateViewController(
identifier: String
) -> UIViewController? {
var viewController: UIViewController? = nil
if #available(iOS 13.0, *) {
return self.storyBoard.instantiateViewController(
identifier: identifier
)
} else {
viewController = self.storyBoard.instantiateViewController(
withIdentifier: identifier
)
}
return viewController
}
}
import Foundation
import UIKit
class BaseViewController<VM: ViewModel>: UIViewController {
private var viewModel: VM?
static func instantiate(
viewModel: VM,
storyBoardName: String,
identifier: String,
bundle: Bundle? = nil
) -> BaseViewController? {
let storyBoard = UIStoryboard.init(name: storyBoardName, bundle: bundle)
let viewController = storyBoard.instantiateViewController(
identifier: identifier
) as? Self
viewController?.viewModel = viewModel
return viewController
}
}
_DependencyInjectionStoryboard 에서는 코드의 분기만 처리해주고, BaseViewController에서 viewModel만 주입해주는 형식으로 코드를 바꾸니 훨씬 코드량도 줄어들고, 보기도 편해졌다. 이렇게 사용한 이유는 나중에 iOS 버젼을 13으로 올려서 13 미만의 버젼의 지원을 멈추게 된다면 _DependencyInjectionStoryboard만을 지워도 동작이 가능하도록, 코드의 변경을 최소화하기 위함이었다.
모든 ViewModel은 ViewModel 프로토콜이 채택되어있고 내부에 input, output이 associatedtype으로 되어있어 storyBoard에서 의존성을 주입하려면 귀찮지만 이러한 방법을 써야했다.
protocol ViewModel {
associatedtype Input
associatedtype Output
var input: Input { get }
var output: Output { get }
}
protocol ViewModelInput { }
protocol ViewModelOutput { }
struct GenericViewModelInput: ViewModelInput { }
struct GenericViewModelOuput: ViewModelOutput { }
struct DefaultGenericViewModel<Input: ViewModelInput, Output: ViewModelOutput>: ViewModel {
var input: GenericViewModelInput
var output: GenericViewModelOuput
init() {
self.input = GenericViewModelInput()
self.output = GenericViewModelOuput()
}
}
import UIKit
final class GenericViewController: BaseViewController<DefaultGenericViewModel<GenericViewModelInput, GenericViewModelOuput>> {
@IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.configureView()
self.configureBinding()
}
private func configureView() {
self.view.backgroundColor = .systemTeal
self.label.text = "안녕하세요!"
}
private func configureBinding() { }
}
생성되는 ViewController에 제네릭 타입을 붙여버리면 StoryBoard가 가지는 ViewController 이름까지 영향이 가게 되어서 생성이 불가능해 진다. 따라서 상속을 이용해 BaseViewController에서 제네릭 타입을 생성 하는 것.
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
guard let viewController = GenericViewController
.instantiate(
viewModel: DefaultGenericViewModel(),
storyBoardName: "Main",
identifier: GenericViewController.className,
bundle: nil
) else {
return true
}
window?.rootViewController = viewController
window?.makeKeyAndVisible()
return true
}
}
따라서 모든 ViewController는 이러한 형식으로 만들어진다.
코드
GitHub - MINRYUL/DependencyInjectionStoryboard
Contribute to MINRYUL/DependencyInjectionStoryboard development by creating an account on GitHub.
github.com
'iOS > 학습' 카테고리의 다른 글
SwiftUI) iOS13 부터 지원하는 간단한 커스텀 Attribute Text 만들기 (0) | 2023.02.15 |
---|---|
TCA) Store vs ViewStore (0) | 2023.02.13 |
Custom Horizontal FlowLayout (0) | 2022.06.21 |
RxNimble + Quick Unit Test (0) | 2022.05.26 |
iOS View 에서의 init 함수들 (0) | 2022.05.08 |