MINRYUL
류링류링
MINRYUL
전체 방문자
오늘
어제
  • 분류 전체보기
    • Swift
      • 학습
    • iOS
      • Toy-Project
      • 학습
      • Metal
    • CS
    • TIL

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • AttributeText
  • BDD
  • urlsession
  • static framework
  • Clean Architecture
  • TDD
  • opaque type
  • RxSwift
  • tuist
  • RxTest
  • RxCocoa
  • collectionView
  • WWDC
  • Existential any
  • ViewStore
  • some
  • Swift
  • Any
  • Existential type
  • WWDC 2024
  • METAL
  • RxNimble
  • demangle
  • Custom Calendar
  • CollectionView Cell
  • configuration_bulid_dir
  • TableView Cell
  • Protocol
  • ios
  • dynamic frameworkm

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
MINRYUL

류링류링

iOS/학습

Storyboard 에서 Generic Type 의존성 주입하기

2022. 3. 20. 17:13

기존 글에 매우 큰 오류가 있습니다!!

https://minryul.tistory.com/21 이 글로 가주세요!!!

 

 StoryBoard를 사용하는 상황에서 ViewController(class) - ViewModel(struct) - Repository(struct) - Network(struct) 순으로 진행되는 Architecture에서 화면은 같으나 데이터를 받아오는 EndPoint가 다를 때 ViewModel과 ViewController는 그대로 유지하면서 Repository의 변경만으로 코드를 유지할 수 있을까?

 

 의존성 주입 자체는 iOS 13 이상 버젼에서 func instantiateInitialViewController<ViewController>(creator: ((NSCoder) -> ViewController?)?) -> ViewController? 함수가 존재해서 Storyboard의 ViewController Object와 연결되어 있는 identifier를 찾아 NSCoder를 주는 클로져가 존재했습니다. 따라서 쉽게 의존성을 주입할 수 있었는데 iOS 13 이전 버젼에서는 이러한 동작이 불가능 했습니다. 결국 프로젝트가 iOS 11 버젼을 지원하므로 고민을 더 해보아야 했습니다.

 

 따라서 고민을 해본 결과 프로젝트에 모든 ViewController가 상속받는 BaseViewController를 만들고 이것이 생성될 때 NSCoder를 BaseViewController 내부 프로퍼티로 저장한 뒤, 이것을 사용해 Generic Type의 Final ViewController를 만들면 Generic Type의 의존성 주입이 가능할 것 같아 시도해보았습니다. 물론 이렇게 되면 iOS 13 이전 버젼에서는 ViewController가 두번 만들어지므로 Overhead가 생기겠지만, 다른 방법이 생각나지 않아 이러한 방식으로 진행했습니다.

 

 결과적으로 구조는

GenericViewController<T: Repository>: ViewController

<-(상속)- ViewController: BaseViewController

<-(상속)- BaseViewController: UIViewController

하는 구조로 완성되었습니다. 

 

import UIKit

class BaseViewController: UIViewController {
    var coder: NSCoder?
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.coder = coder
    }
}

 

 BaseViewController 에서는 NSCoder로 만들어지는 init 에서 Storyboard와 ViewController 연결할 수 있는 객체인 NSCoder Object를 저장할 수 있는 프로퍼티를 만들어줬습니다. 이것을 이용해 iOS 13 이전 버젼에서의 의존성 주입을 할 것입니다.

 

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
    }
}

 

 의존성을 주입할 객체인 _DependencyInjectionStoryboard 를 만들었습니다. 나중 iOS 13으로 버젼을 올릴 경우 코드의 변경이 없을 수 있도록 typealias 로 UIKit.UIStoryboard와 같은 이름인 UIStoryboard를 사용했습니다. Scope Proirity에 따라서 UIKit.Storyboard 보다 Project.UIStoryboard가 우선적으로 적용되므로 UIStoryboard를 사용할 경우 _DependencyInjectionStoryboard로 코드가 동작할 것입니다. 함수의 이름과 매개변수도 기존 Storyboard의 함수인 func instantiateInitialViewController(creator: ((NSCoder) -> ViewController?)?) -> ViewController? 와 동일하게 만들어 줬습니다. 함수 내부 동작에는 iOS 13 버젼 이상과 iOS 11 버젼 이하의 버젼에서 다르게 코드가 동작합니다. iOS 13 이상에서는 기존 로직과 동일하도록 동작하게 작성되었고, iOS 13 이전 버젼에서는 StoryBoard의 identifier에 따라서 ViewController가 한번 생성되고, 생성된 ViewController 를 이용해 NSCoder를 얻어내어 다시한번 viewController가 생성될 것입니다. 

 

import UIKit

class ViewController: BaseViewController {
    
    @IBOutlet weak var label: UILabel!
    
}
    
final class GenericViewController<R: Repository>: ViewController {
        
    static func instantiate(viewModel: ViewModel<R>) -> ViewController? {
        let storyBoard = UIStoryboard.init(name: "Main", bundle: nil)
        let viewController = storyBoard.instantiateViewController(
            identifier: "ViewController"
        ) {
            GenericViewController(coder: $0, viewModel: viewModel)
        } as? ViewController
        
        return viewController
    }
    
    init?(coder: NSCoder, viewModel: ViewModel<R>) {
        super.init(coder: coder)
        self.viewModel = viewModel
    }
    
    required init?(coder: NSCoder) {
        fatalError("use instantiate(param:) instead")
    }
    
    private var viewModel: ViewModel<R>?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.configureView()
        self.configureBinding()
    }
    
    private func configureView() {
        self.view.backgroundColor = .systemTeal
        
        self.label.text = "안녕하세요!"
    }
    
    private func configureBinding() { }
}

 

 최종적으로 생성될 GenericViewController에서는 UIKit.Storyboard를 사용할 때와 동일하게 사용할 수 있습니다. 따라서 코드가 방대한 프로젝트에서 최소한의 코드의 변경으로 Generic 한 Repository를 사용할 수 있도록 만들 수 있게 되는 것입니다. ViewController는 Storyboard의 Outlet을 연결할 수 있고, View Update 코드들은 GenericViewController에 담을 수 있게 됩니다.

 

import Foundation

struct ViewModel<R: Repository> {

    private var repository: R?
    
    init(repository: R) {
        self.repository = repository
    }
}

protocol Repository { }

struct DefaultRepository: Repository { }

 

 ViewModel과 Repository 코드입니다.

 

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<DefaultRepository>
            .instantiate(
                viewModel: ViewModel(
                    repository: DefaultRepository()
                )
            ) else {
            return true
        }
        
        window?.rootViewController = viewController
        window?.makeKeyAndVisible()
    
        return true
    }

}

 

 Appdelegate에서 window의 rootViewController 적용 입니다. Generic Type을 지정 후, instantiate 함수를 호출해 줍니다.

 

 최대한 기존 로직에 변경이 없게 작성하려니 Overhead가 생기는 구조로 작성이 되었습니다. 다른 방법도 많습니다!

피드백은 언제나 환영입니다.

 

전체코드

https://github.com/MINRYUL/DependencyInjectionStoryboard

 

GitHub - MINRYUL/DependencyInjectionStoryboard

Contribute to MINRYUL/DependencyInjectionStoryboard development by creating an account on GitHub.

github.com

 

참고문서

 

Apple Developer Documentation

 

developer.apple.com

 

Generic View Controllers with Storyboards

Trying to use storyboards with generic view controllers as always been problematic. The problem is the storyboard also encodes the class of the view controller for each “scene”. But you don’t know the class beforehand since it can it’s a generic cl

pfandrade.me

 

iOS 13 에서 변경된 UIModalPresentationStyle 해킹하기

스코프 우선순위를 해킹해서 iOS 13의 UIModalPresentationStyle 기본값 변경에 대응한 경험을 공유합니다.

medium.com

 

Imagining Dependency Injection nt of storyboards – the lack of dependency injection via initializer.

holko.pl

 

'iOS > 학습' 카테고리의 다른 글

iOS View 에서의 init 함수들  (0) 2022.05.08
iOS RxDataSource로 Custom Xib CalendarView 만들기  (0) 2022.05.07
iOS에서의 Frame vs Bounds  (0) 2022.04.24
translatesAutoresizingMaskIntoConstraints  (0) 2022.02.13
Diffable DataSource Infinite Scroll  (0) 2022.01.24
    'iOS/학습' 카테고리의 다른 글
    • iOS RxDataSource로 Custom Xib CalendarView 만들기
    • iOS에서의 Frame vs Bounds
    • translatesAutoresizingMaskIntoConstraints
    • Diffable DataSource Infinite Scroll
    MINRYUL
    MINRYUL
    열심히 살자

    티스토리툴바