diff --git "a/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/MVVM/README.md" "b/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/MVVM/README.md" new file mode 100644 index 0000000..cd49ace --- /dev/null +++ "b/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/MVVM/README.md" @@ -0,0 +1,74 @@ +# MVVM +## 출처 +[Kodeco mvc->mvvm 리팩토링](https://www.kodeco.com/6733535-ios-mvvm-tutorial-refactoring-from-mvc) +## MVVM을 왜 쓸까 +### MVC의 한계 +- MVC에서는 Controller가 Model과 소통하고, View의 업데이트를 담당합니다. + - View와 Controller는 분리가 힘들기 때문에 Model,View 관련 코드가 혼재되어 규모가 커질수록 Controller가 무거워집니다. +- 테스트 코드를 작성하기 쉽지 않다. + - controller에 model,view 코드가 혼재되어 있고 강하게 연결되어 있어 각각을 분리시킨 테스트가 불가능합니다. +### MVVM의 구조 +![mvvm architecture](https://koenig-media.raywenderlich.com/uploads/2019/12/MVVM-Diagram.png) + +- view - controller - viewmodel - model의 구조 +- controller와 model 사이에 viewmodel을 추가하여 controller에서 model과 관련된 로직을 걷어내 부담을 줄입니다. +- viewmodel은 controller과 비교하여 비즈니스 로직을 더 잘 표현할 수 있습니다. viewmodel의 역할은 다음과 같습니다. + - view 입력을 받고 model의 데이터를 업데이트합니다. + - 업데이트된 model의 데이터를 view로 전달합니다. + - 업데이트 과정에서의 모델 데이터 formatting 작업을 수행합니다. +- viewmodel은 원활한 테스트를 위해 public으로 설정하고 view와의 완벽한 분리를 위해 UIKit을 import하지 않습니다. + +## MVVM의 데이터 바인딩 +- 단순히 viewmodel만 추가했다면 MVC 패턴 + viewmodel 관련 코드가 추가된 것뿐 오히려 복잡합니다. +- ios에서는 `데이터 바인딩`이라는 방법을 통해서 view와 model간 변화를 반영하고 간결한 로직 작성이 가능합니다. +### 방법 +#### Key-Value Observer Pattern +- objective-c에 기반한 방식으로, objective 런타임에서 앱을 실행시키게 됩니다. +#### 함수형 반응형 프로그래밍(FRP) +- RXSwift, Combine과 같은 라이브러리를 사용하는 방식입니다. 러닝커브가 높습니다☠️ +#### Delegate Pattern +- notification을 통해 값의 변화를 감지합니다 +#### Boxing(속성 감시자 사용) +- didSet, willSet을 활용하여 값이 변할 때 로직을 반영할 수 있습니다. + +## MVC -> MVVM시 필요한 과정 +### ViewController에서의 변화 +- Model과 관련된 프로퍼티, 메서드들을 모두 viewmodel로 옮깁니다. +- view가 load된 후(ex : `viewDidLoad()`), viewmodel의 바인드 메서드를 호출하여 데이터 바인딩을 해줍니다. +### ViewModel, 필요에따라 Utility 클래스도 생성 +#### viewmodel 구성요소 +- view의 입력을 받아 업데이트 및 formatting할 model의 데이터를 프로퍼티로 선언 +- 비즈니스 로직이 구현된 메서드들 +#### Utility 클래스 +- boxing의 경우, 변화를 관찰하고 값의 변화마다 비즈니스 로직을 적용해 줄 수 있는 객체입니다. +- RXSwift처럼 Observer 객체가 미리 구현되어 있는 경우도 있습니다. + +
+ 예시 코드 + + ```swift + final class Box { + + typealias Listener = (T) -> Void + var listener: Listener? + + var value: T { + didSet { + listener?(value) + } + } + + init(_ value: T) { + self.value = value + } + + func bind(listener: Listener?) { + self.listener = listener + listener?(value) + } +} +``` + +
+
+ diff --git "a/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/README.md" "b/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/README.md" index 8a2ed53..481f339 100644 --- "a/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/README.md" +++ "b/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/README.md" @@ -1 +1,6 @@ -# MVVM, MVI, Ribs, VIP 등 자신이 알고있는 아키텍쳐를 설명하시오. \ No newline at end of file +# MVVM, MVI, Ribs, VIP 등 자신이 알고있는 아키텍쳐를 설명하시오. + +## [1. MVVM](./MVVM/README.md) + +## [2. RIBS](./RIBS/README.md) + diff --git "a/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/RIBS/README.md" "b/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/RIBS/README.md" new file mode 100644 index 0000000..a6f5378 --- /dev/null +++ "b/iOS/22\354\243\274\354\260\250/MVVM, MVI, Ribs, VIP \353\223\261 \354\236\220\354\213\240\354\235\264 \354\225\214\352\263\240\354\236\210\353\212\224 \354\225\204\355\202\244\355\205\215\354\263\220\353\245\274 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/RIBS/README.md" @@ -0,0 +1,213 @@ +# RIBS 아키텍처 +## 출처 +[Uber/RIBS ios 공식 튜토리얼](https://github.com/uber/RIBs/wiki) +## 나오게 된 배경 +### 안드로이드와의 협업 +ios와 안드로이드에서 동일한 아키텍처를 채택함으로써 협업과 생산성 향상에 기여합니다. +### 전역 상태 최소화 +전역 상태 값(ex: 싱글톤)의 원치 않는 변화 발생을 분리된 계층 구조와 캡슐화를 통해 예방 +### SOLID 개방-폐쇄 원칙 준수를 통한 테스트 용이함과 계층 별 균등한 책임 +DI 트리 구조와 RX, 프로토콜을 통해 Router, Interactor, Builder 등 각 계층의 독립성 보장 +### 비즈니스 로직 중심 +기존 VIPER 아키텍처는 앱의 상태가 View에 주도하에 결정되기 때문에 비즈니스 로직만 분리하는 것이 어려움. + +## 구조 + +

+ +### Router + +- 역할 : Interactor를 수신하고 하위 RIB를 부착/탈착합니다. +- 내부 요소 + - Interactable 프로토콜 : Interactor 구현체(Class)가 준수하는 프로토콜로 Interactor-Router간 통신이 가능하게 합니다. + - ViewControllerable 프로토콜 : View/ViewController 가 준수하는 프로토콜로 ViewController-Router간 통신이 가능하게 합니다. + - Router 구현체(Class) + - 생성시 다른 RIB들의 Builder객체들을 외부에서 주입받고 Interactor의 Router 속성에 자기자신을 할당함으로써 ViewController-Router를 서로 연결합니다.(delegate 패턴과 유사) + - Interactor의 Routing 프로토콜을 준수하여 다른 RIB로 전환하는 로직이 구현되어 있습니다. 화면 전환 시 `StoryBoard.instantiateViewController`를 통해 다음 ViewController 객체를 가져오는 것처럼, 다른 RIB의 Builder객체의 `build()`함수를 통해 RIB를 생성하고 탈/부착합니다. + +### Interactor + +- 역할 + - 비즈니스 로직(RX 구독-상태변경 로직,데이터 저장위치 결정, RIB연결 결정)이 포함되어 있는 계층 + - 상태관리 메서드 `willResignActive()`와 RX 메서드`dispose()`를 통해 Interactor가 존재할 때만 비즈니스 로직이 적용되도록 제한합니다. 이를 통해 전역상태 최소화의 장점을 가져갈 수 있습니다. +- 내부 요소 + - ViewableRouting 프로토콜 : 다른 RIB의 탈/부착용 메서드들이 선언되어 있고 Router에서 채택하여 라우팅 로직을 구현합니다. + - Presentable 프로토콜 : Interactor-Presenter간 통신을 위한 프로토콜로 PresentableListener 프로토콜타입 변수가 기본적으로 선언되어 있습니다. Presenter 구현체가 프로토콜을 채택하여 내부 메서드를 구현하게 됩니다. + - Listener 프로토콜 : AnyObject 상속 프로토콜로 상위 RIB Router의 Interactable 프로토콜이 상속받게 됩니다. 자손 RIB의 Builder는 Interactor를 생성 후 Listener에 상위 RIB의 Interactable 객체(Interactor클래스)를 주입합니다. + - Interactor 클래스 + - Router와 통신을 위한 Routing프로토콜 타입 변수, 부모 RIB와 통신을 위한 Listener프로토콜 타입 변수, 자손 RIB Interactor의 메서드가 구현되어 있습니다. + - 생성시 주입받은 presenter의 PresentableListener(위 Listener와 다름)에 자기자신을 할당함으로써 Interactor-Presenter를 서로 연결합니다. + +### Builder + +- 역할 + - RIB 구성 클래스 및 각 자손들의 Builder를 인스턴스화 + - DI 매커니즘이 활용되며 클래스 생성로직을 분리한다면 테스트를 위해 Mocking이 가능하다. +- 내부 요소 + - Dependency 프로토콜 : 외부에서 주입받는 종속성들이 포함되어 있습니다. + - Component 클래스 : 종속성 관리 클래스로 Dependency 프로토콜의 종속성들은 `fileprivate`으로 외부 접근을 제한하고 현재 RIB에서 사용하지 않는 종속성은 extension에 포함시킵니다. 부모 RIB의 Component는 자손 RIB의 빌더에 주입되어 자손이 부모의 종속성에 접근하는 것을 가능하게 만듭니다. + - Buildable 프로토콜 : Builder클래스가 준수하는 프로토콜로 `build()`메서드가 선언되어 있습니다. + - Builder 클래스 : 생성 시 dependency를 주입받아 `build()`메서드를 통해 RIB 각 요소들을 생성 후 Router로 제어를 넘깁니다. 자손 RIB라면 이때 부모의 Interactor와 Interactor를 연결합니다.(listener에 주입받은 interactor 할당) + +### Presenter, ViewController +- presenter 역할 : Presenter는 Interactor와 View/ViewController 사이에서 Model - ViewModel 변환을 담당합니다. 그러나 역할이 워낙 작기 때문에 ViewController나 Interactor가 대신하고 생성하지 않는 경우도 있습니다.(튜토리얼에서는 ViewController가 대신 수행) +- ViewController는 UI 관련 로직만 수행합니다. +- 구성요소 + - PresentableListener 프로토콜 : Interactor 구현체가 준수하는 프로토콜로 Interactor가 프로토콜 메서드를 구현하고 생성시 Presenter 클래스의 PresentableListener 타입의 Listener 변수에 자기자신을 할당함으로써 Interactor-Presenter를 연결합니다. + - Presenter 구현체(Class) : 채택한 Router의 프로토콜 Presentable과 ViewControllerable의 상세 구현과 PresentableListener 변수가 존재합니다. + +## 로직 예시 + +로그인 화면에서 사용자가 로그인 버튼을 눌렀을 때 다음 화면으로 전환하고자 합니다. + +이 앱의 RIB 구조는 다음과 같습니다. + +

+ +현재 로그인 화면은 LoggedOut RIB이고 로그인 버튼 클릭시 Root RIB -> LoggedIn -> OffGame RIB로 전환됩니다. + +LoggedIn RIB는 Viewless RIB로 Root의 ViewController를 주입받습니다. + +### 과정 +1. 사용자가 로그인 버튼을 누르면 LoggedOut RIB ViewController의 함수를 호출하고, 다시 함수 내부에서 ViewController의 listener의 `login` 메서드를 호출합니다. +```swift +protocol LoggedOutPresentableListener: AnyObject { + func login(withPlayer1Name: String?, player2Name: String?) +} + +final class LoggedOutViewController: UIViewController, LoggedOutPresentable, LoggedOutViewControllable { + + weak var listener: LoggedOutPresentableListener? + + private func buildLoginButton(withPlayer1Field player1Field: UITextField, player2Field: UITextField) { + let loginButton = UIButton() + + //LoggedOutViewController -> LoggedOutInteractor + loginButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.listener?.login(withPlayer1Name: player1Field.text, player2Name: player2Field.text) + }) + .disposed(by: disposeBag) + } +} +``` +2. listener는 LoggedOutInteractor 클래스 생성 시점에 LoggedOutInteractor를 주입받았기 때문에 LoggedoutInteractor로 제어권이 넘어갑니다. + +```swift + +protocol LoggedOutListener: AnyObject { + func didLogin(withPlayer1Name player1Name: String, player2Name: String) +} + +final class LoggedOutInteractor: PresentableInteractor, LoggedOutInteractable, LoggedOutPresentableListener { + + weak var listener: LoggedOutListener? + + override init(presenter: LoggedOutPresentable) { + super.init(presenter: presenter) + presenter.listener = self + // ViewController의 리스너에 자기자신을 주입 + } + + func login(withPlayer1Name player1Name: String?, player2Name: String?) { + + // LoggedOutInteractor -> RootInteractor + listener?.didLogin(withPlayer1Name: player1NameWithDefault, player2Name: player2NameWithDefault) + } +} + +``` + +3. 호출된 LoggedOutInteractor의 `login()`메서드는 내부에서 listener의 `didLogin()`메서드를 호출합니다. 이때 listener는 LoggedOutBuilder의 `build()`메서드에서 부모의 Interactor인 RootInteractor를 주입받았습니다. +```swift +final class RootRouter: LaunchRouter, RootRouting { + + private let loggedOutBuilder: LoggedOutBuildable + + // LoggedOutRIB로 전환하는 메서드. build 메서드 호출시 매개변수로 RootInteractor를 프로토콜 타입으로 전달합니다. + private func routeToLoggedOut() { + let loggedOut = loggedOutBuilder.build(withListener: interactor) + // ... 중략 + } +} + +final class LoggedOutBuilder: Builder, LoggedOutBuildable { + // listener 매개변수에 전달받은 RootInteractor를 LoggedInteractor의 listener에 할당합니다. + func build(withListener listener: LoggedOutListener) -> LoggedOutRouting { + _ = LoggedOutComponent(dependency: dependency) + let viewController = LoggedOutViewController() + let interactor = LoggedOutInteractor(presenter: viewController) + interactor.listener = listener + return LoggedOutRouter(interactor: interactor, viewController: viewController) + } +} +``` + +4. 호출된 RootInteractor의 `didLogin()` 메서드 내부에서 router 변수의 routeToLoggeIn 메서드를 호출합니다. RootRouter 생성 시점에 router 변수에 RootRouter를 할당했기 때문에 결과적으로 RootRouter로 제어권이 넘어갑니다. + +```swift +final class RootRouter: LaunchRouter, RootRouting { + // 생성 시점에 자기자신을 RootInteractor의 router 변수에 할당합니다. + init(interactor: RootInteractable, + viewController: RootViewControllable, + loggedOutBuilder: LoggedOutBuildable, + loggedInBuilder: LoggedInBuildable) { + super.init(interactor: interactor, viewController: viewController) + interactor.router = self + } + // loggeOut RIB를 탈착하고 loggedIn RIB를 부착합니다. + func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) { + + if let loggedOut = self.loggedOut { + detachChild(loggedOut) + viewController.dismiss(viewController: loggedOut.viewControllable) + self.loggedOut = nil + } + + let loggedIn = loggedInBuilder.build(withListener: interactor,player1Name: player1Name,player2Name: player2Name) + attachChild(loggedIn) + } +} + +protocol RootRouting: ViewableRouting { + func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) +} + +final class RootInteractor: PresentableInteractor, RootInteractable, RootPresentableListener { + + weak var router: RootRouting? + // RootInterActor -> RootRouter + func didLogin(withPlayer1Name player1Name: String, player2Name: String) { + router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name) + } +} +``` + +5. 호출된 RootRouter의 `routeToLoggedIn`메서드는 현재 RIB계층구조에서 현재 부착된 LoggedOutRIB를 탈착하고 LoggedInRIB를 부착합니다. `attachChild`메서드는 부착시키는 RIB의 Interactor를 활성화시키고 Router의 load 함수를 호출합니다. + +6. 호출된 LoggedInRouter의 `load(didload)`메서드는 OffGameBuilder의 `build` 메서드를 호출하여 OffGameRIB를 생성하고 RIB 계층구조에 부착합니다. LoggedInRIB는 부모RIB이므로 이번에는 탈착하지 않습니다. 최종적으로 OffGameViewController 화면으로 전환되게 됩니다. + +```swift + +final class LoggedInRouter: Router, LoggedInRouting { + + override func didLoad() { + super.didLoad() + attachOffGame() + } + + private let viewController: LoggedInViewControllable + private let offGameBuilder: OffGameBuildable + private var currentChild: ViewableRouting? + + private func attachOffGame() { + let offGame = offGameBuilder.build(withListener: interactor) + self.currentChild = offGame + attachChild(offGame) + viewController.present(viewController: offGame.viewControllable) + } +} +``` + + + diff --git "a/iOS/23\354\243\274\354\260\250/method swizzling\354\235\264 \353\254\264\354\227\207\354\235\264\352\263\240, \354\226\264\353\226\250 \353\225\214 \354\202\254\354\232\251\355\225\230\353\212\224\354\247\200 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/README.md" "b/iOS/23\354\243\274\354\260\250/method swizzling\354\235\264 \353\254\264\354\227\207\354\235\264\352\263\240, \354\226\264\353\226\250 \353\225\214 \354\202\254\354\232\251\355\225\230\353\212\224\354\247\200 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/README.md" new file mode 100644 index 0000000..e9ecb9f --- /dev/null +++ "b/iOS/23\354\243\274\354\260\250/method swizzling\354\235\264 \353\254\264\354\227\207\354\235\264\352\263\240, \354\226\264\353\226\250 \353\225\214 \354\202\254\354\232\251\355\225\230\353\212\224\354\247\200 \354\204\244\353\252\205\355\225\230\354\213\234\354\230\244./PAKA/README.md" @@ -0,0 +1,49 @@ +# method swizzling이 무엇이고, 어떨 때 사용하는지 설명하시오. +## 출처 +- [Method swizzling in iOS Swift - medium](https://abhimuralidharan.medium.com/method-swizzling-in-ios-swift-1f38edaf984f#:~:text=What%20is%20method%20swizzling%3F,an%20Objective%2DC%20runtime%20feature.) +- [Method Swizzling에 대해 알아보자 - 개발자 소들이님](https://babbab2.tistory.com/76) +- [Method Swizzling 응용 deinit 로그 찍기 - 김종권님 블로그](https://ios-development.tistory.com/911) + +## 정의 +iOS의 언어인 Swift, Objective-C 모두 런타임에 코드가 확정되는 'dynamic lanuguage'입니다. + +method Swizzling은 번역 시 메서드 뒤섞기로, 말그대로 코드로 작성된 메서드를 런타임에 다른 메서드로 바꿔치기 하는 것입니다. + +메서드를 수정할 수 없는 상태(시스템, 라이브러리의 블랙박스 메서드들)일 때 유용하게 사용가능합니다. + +## 사용법 +제약조건 : method swizzling은 Objective-C 런타임의 기능이기 때문에 swift, Objective-C 모두 사용가능하나 `@objc dynamic` 어노테이션을 대상 메서드 앞에 붙혀야 합니다. + +```swift +import UIKit + +class ViewController: UIViewController { + + @objc dynamic private func original() { + print("원본 메서드") + } +} +``` +`class_getInstanceMethod` 함수로 해당 메서드의 인스턴스를 획득하고 `method_exchangeImplemtations` 함수를 통해 런타임 때 원본 메서드를 교체합니다. +```swift +extension ViewController { + class func swizzleMethod() { + guard + let originalMethod = class_getInstanceMethod(Self.self, #selector(Self.original)), + let swizzledMethod = class_getInstanceMethod(Self.self, #selector(Self.dynamic)) + else { return } + method_exchangeImplementations(originalMethod, swizzledMethod) + } + + @objc private func dynamic() { + print("교체할 메서드") + } +} +``` +## 주의할 점 +시스템에서 정의된 viewDidLoad와 같은 메서드는 swizzling 후 교체된 메서드뿐만 아니라 기존 메서드도 함께 호출됩니다. + +최신 iOS 버전이 출시되면 스위즐링이 실패할 가능성이 있어 항상 확인해야 합니다. + +하위 클래스 내에서 스위즐링 시 예상치 못한 변경이 발생할 수 있습니다. +