Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- RxCocoa
- combine
- SWIFT
- UITextView
- 리팩토링
- Refactoring
- ios
- 스위프트
- uiscrollview
- 리펙토링
- Protocol
- swiftUI
- rxswift
- HIG
- collectionview
- ribs
- Clean Code
- map
- clean architecture
- MVVM
- swift documentation
- uitableview
- Observable
- Xcode
- Human interface guide
- 애니메이션
- 클린 코드
- 리펙터링
- UICollectionView
- tableView
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 4-5) RIBs 튜토리얼 (Dependency Injection, Rx Stream) 본문
Architecture (swift)/RIBs
[iOS - swift] 4-5) RIBs 튜토리얼 (Dependency Injection, Rx Stream)
jake-kim 2021. 4. 22. 23:44
Root -> LoggedIn 플레이어 이름 DI
- LoggedInBuildable에 받을 데이터 기입 (이름정보)
// LoggedInBuilder.swift
protocol LoggedInBuildable: Buildable {
func build(withListener listener: LoggedInListener,
player1Name: String,
player2Name: String) -> LoggedInRouting
}
- 변경내용 반영
// LoggedInBuilder.swift
func build(withListener listener: LoggedInListener, player1Name: String, player2Name: String) -> LoggedInRouting {
let component = LoggedInComponent(dependency: dependency,
player1Name: player1Name,
player2Name: player2Name)
- LoggedInComponent의 init, 프로퍼티 추가
// LoggedBuilder.swift
final class LoggedInComponent: Component<LoggedInDependency> {
fileprivate var loggedInViewController: LoggedInViewControllable {
return dependency.loggedInViewController
}
let player1Name: String
let player2Name: String
init(dependency: LoggedInDependency, player1Name: String, player2Name: String) {
self.player1Name = player1Name
self.player2Name = player2Name
super.init(dependency: dependency)
}
}
- RootRouter에 dependency내용의 build초기화 반영
// RootRouter.swift
func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) {
// Detach logged out.
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)
}
Component를 사용한 DI
Component란
- Dependency 프로토콜의 구현체
- 부모로 부터 필요한 종속성을 나열 (파일: ParentComponent+ChildComponent)
Component에 종속성 정의
- Dependency 프로토콜 정의:
// OffGameBuilder.swift
protocol OffGameDependency: Dependency {
var player1Name: String { get }
var player2Name: String { get }
}
- Component정의 (Dependency의 구현체)
- component의 접근 범위는 fileprivate: child scopes에 노출되지 않도록 하기 위함
- 단, LoggedInComponent에서는 OffGame 자식에게 데이터를 넘기기 위해 fileprivate 사용 x
// OffGameBuilder.swift
final class OffGameComponent: Component<OffGameDependency> {
fileprivate var player1Name: String {
return dependency.player1Name
}
fileprivate var player2Name: String {
return dependency.player2Name
}
}
OffGameViewController로 이름 넘기기
OffGameController에 이름 띄우기
- OffGameBuilder 생성자에서 DI: OffGameViewController를 만드는 build에 Component 적용
// OffGameBuilder.swift
final class OffGameBuilder: Builder<OffGameDependency>, OffGameBuildable {
override init(dependency: OffGameDependency) {
super.init(dependency: dependency)
}
func build(withListener listener: OffGameListener) -> OffGameRouting {
let component = OffGameComponent(dependency: dependency)
let viewController = OffGameViewController(player1Name: component.player1Name,
player2Name: component.player2Name)
let interactor = OffGameInteractor(presenter: viewController)
interactor.listener = listener
return OffGameRouter(interactor: interactor, viewController: viewController)
}
}
- 초기화 적용
// OffGameViewController.swift
...
private let player1Name: String
private let player2Name: String
init(player1Name: String, player2Name: String) {
self.player1Name = player1Name
self.player2Name = player2Name
super.init(nibName: nil, bundle: nil)
}
...
- OffGameViewController UI: raw.githubusercontent.com/uber/ribs/assets/tutorial_assets/ios/tutorial3-rib-di-and-communication/source/source1.swift
Rx stream
부모 -> 자식 하향 데이터 전달 시나리오
- 사용처: 게임이 끝난 경우 시작화면으로 이동
- TicTacToe에서 점수를 LoggedIn과 OffGame에서 받아서 데이터를 UI에 표출해야 하는 상황
- Listener interface로 부모에게 전달: TicTacToe -> LoggedIn
- Rx stream로 자식에게 전달: LoggedIn -> OffGame
- score stream을 생성하여 해결
구현
- LoggedIn 그룹에 ScoreStream.swift 추가
import RxSwift
import RxCocoa
struct Score {
let player1Score: Int
let player2Score: Int
static func equals(lhs: Score, rhs: Score) -> Bool {
return lhs.player1Score == rhs.player1Score && lhs.player2Score == rhs.player2Score
}
}
protocol ScoreStream: class {
var score: Observable<Score> { get }
}
protocol MutableScoreStream: ScoreStream {
func updateScore(withWinner winner: PlayerType)
}
class ScoreStreamImpl: MutableScoreStream {
var score: Observable<Score> {
return variable
.asObservable()
.distinctUntilChanged { (lhs: Score, rhs: Score) -> Bool in
Score.equals(lhs: lhs, rhs: rhs)
}
}
func updateScore(withWinner winner: PlayerType) {
let newScore: Score = {
let currentScore = variable.value
switch winner {
case .player1:
return Score(player1Score: currentScore.player1Score + 1, player2Score: currentScore.player2Score)
case .player2:
return Score(player1Score: currentScore.player1Score, player2Score: currentScore.player2Score + 1)
}
}()
variable.accept(newScore)
}
// MARK: - Private
private let variable = BehaviorRelay<Score>(value: Score(player1Score: 0, player2Score: 0))
}
- Stream 버전
- Read-only 버전: ScoreStream
- 변경할 수 있는 버전: MutableScoreStream
- mutableScoreStream 추가
- shared: 싱글톤
- stream들은 일반적으로 싱글톤
- 접근은 fileprivate이 아닌 internal: LoggedIn의 child에서 접근이 가능해야 하므로 internal
(Component는 대부분 fileprivate, stream은 대부분 internal)
// LoggedInBuilder.swift
var mutableScoreStream: MutableScoreStream {
return shared { ScoreStreamImpl() }
}
- LoggedInBuilder에서 LoggedInInteractor를 만들때 mutableScoreStream을 넘기는 코드 적용
// LoggedInInteractor.swift
private let mutableScoreStream: MutableScoreStream
init(mutableScoreStream: MutableScoreStream) {
self.mutableScoreStream = mutableScoreStream
}
- builder에서 Interactor를 만드는 코드 적용
// LoggedInBuilder.swift
func build(withListener listener: LoggedInListener, player1Name: String, player2Name: String) -> LoggedInRouting {
let component = LoggedInComponent(dependency: dependency,
player1Name: player1Name,
player2Name: player2Name)
let interactor = LoggedInInteractor(mutableScoreStream: component.mutableScoreStream)
OffGame에 scoreStream 전달
- OffGameBuilder를 만들 때 scoreStream을 넘기도록 적용
// OffGameBuilder.swift
protocol OffGameDependency: Dependency {
var player1Name: String { get }
var player2Name: String { get }
var scoreStream: ScoreStream { get }
}
final class OffGameComponent: Component<OffGameDependency> {
fileprivate var player1Name: String {
return dependency.player1Name
}
fileprivate var player2Name: String {
return dependency.player2Name
}
fileprivate var scoreStream: ScoreStream {
return dependency.scoreStream
}
}
- LoggedIn -> OffGame 의존성에 scoreStream 적용
- OffGameBuilder가 아닌 LoggedInComponent+OffGame.swift파일에 위치시키는 이유: LoggedIn RIB에서는 사용되지 않고 OffGame RIB에서만 사용되게 하기 위함
// LoggedInComponent+OffGame.swift
extension LoggedInComponent: OffGameDependency {
var scoreStream: ScoreStream {
return mutableScoreStream
}
}
- OffGameInteractor에 scoreStream 프로퍼티 적용
// OffGameInteractor.swift
private let scoreStream: ScoreStream
init(presenter: OffGamePresentable,
scoreStream: ScoreStream) {
self.scoreStream = scoreStream
super.init(presenter: presenter)
presenter.listener = self
}
- Builder에서, OffGameInteractor 초기화 적용
// OffGameBuilder.swift
final class OffGameBuilder: Builder<OffGameDependency>, OffGameBuildable {
override init(dependency: OffGameDependency) {
super.init(dependency: dependency)
}
func build(withListener listener: OffGameListener) -> OffGameRouting {
let component = OffGameComponent(dependency: dependency)
let viewController = OffGameViewController(player1Name: component.player1Name,
player2Name: component.player2Name)
let interactor = OffGameInteractor(presenter: viewController, scoreStream: component.scoreStream) // <- scoreStream 초기화 추가
interactor.listener = listener
return OffGameRouter(interactor: interactor, viewController: viewController)
}
}
subscribe Rx Stream
- OffGame의 Interactor -> ViewController 데이터 전송
- OffGameInteractor -> OffGamePresentable
// OffGameInteractor.swift
protocol OffGamePresentable: Presentable {
weak var listener: OffGamePresentableListener? { get set }
func set(score: Score) // <- 추가
}
- subscribe
- disposeOnDeactivate(interactor:) 의미:
// OffGameInteractor.swift
override func didBecomeActive() {
super.didBecomeActive()
updateScore() // <- 추가
}
private func updateScore() {
scoreStream.score
.subscribe(
onNext: { (score: Score) in
self.presenter.set(score: score)
}
)
.disposeOnDeactivate(interactor: self)
}
- offGamePresentableListener UI: 코드
게임이 끝난 경우 부모에게 전달
- 게임이 끝난 경우, 정의 TicTacToe -> LoggedIn
// TicTacToeInteractor.swift
protocol TicTacToeListener: class {
func gameDidEnd(withWinner winner: PlayerType?)
}
- LoggedInInteractor에 해당 함수 구현
// LoggedInInteractor.swift
func gameDidEnd(withWinner winner: PlayerType?) {
if let winner = winner {
mutableScoreStream.updateScore(withWinner: winner)
}
router?.routeToOffGame()
}
- listener수정: func closeGame() 삭제
protocol TicTacToePresentableListener: class {
func placeCurrentPlayerMark(atRow row: Int, col: Int)
}
- 게임이 끝난 경우 Interactor에서 이동하는 로직을 넣기 위해서 completionHandler로 구현
- 이유: 만약 ViewController에 화면전환 로직이 들어간다면, 비지니스 로직이 ViewController에 종속되는 문제가 발생하므로
// TicTacToeInteractor.swift
protocol TicTacToePresentable: Presentable {
var listener: TicTacToePresentableListener? { get set }
func setCell(atRow row: Int, col: Int, withPlayerType playerType: PlayerType)
func announce(winner: PlayerType?, withCompletionHandler handler: @escaping () -> ())
}
// TicTacToeViewController.swift
func announce(winner: PlayerType?, withCompletionHandler handler: @escaping () -> ()) {
let winnerString: String = {
if let winner = winner {
switch winner {
case .player1:
return "Red won!"
case .player2:
return "Blue won!"
}
} else {
return "It's a draw!"
}
}()
let alert = UIAlertController(title: winnerString, message: nil, preferredStyle: .alert)
let closeAction = UIAlertAction(title: "Close Game", style: UIAlertActionStyle.default) { _ in
handler()
}
alert.addAction(closeAction)
present(alert, animated: true, completion: nil)
}
- completion handler로 화면 전환 적용
// TicTacToeInteractor.swift
func placeCurrentPlayerMark(atRow row: Int, col: Int) {
guard board[row][col] == nil else {
return
}
let currentPlayer = getAndFlipCurrentPlayer()
board[row][col] = currentPlayer
presenter.setCell(atRow: row, col: col, withPlayerType: currentPlayer)
if let winner = checkWinner() {
presenter.announce(winner: winner) {
self.listener?.gameDidEnd(withWinner: winner)
}
}
}
'Architecture (swift) > RIBs' 카테고리의 다른 글
[iOS - swift] 5. RIBs 프로젝트 초기 세팅 (with cocoapod) (0) | 2021.04.28 |
---|---|
[iOS - swift] 4-6) RIBs 튜토리얼 (Deeplinking, Workflows, Actionable item) (0) | 2021.04.26 |
[iOS - swift] 4-4) RIBs 튜토리얼 (LoggedIn에 OffGame 붙이기) (0) | 2021.04.21 |
[iOS - swift] 4-3) RIBs 튜토리얼 (viewless RIB) (0) | 2021.04.20 |
[iOS - swift] 4-2) RIBs 튜토리얼 (Listener Interface) (0) | 2021.04.20 |
Comments