iOS Dev/ReactorKit

[ReactorKit#2] Example : Counter 코드작성 (스토리보드 없이)

Mosu(정종인) 2020. 6. 7. 02:42
반응형

https://github.com/chongin12/Counter-Prac << 전체 소스코드 

우선 ReactorKit, RxSwift, RxCocoa, SnapKit 을 pod을 이용하여 설치를 할 것이다. .xcodeproj가 있는 폴더에서 pod init 명령어를 실행시켜주고, 생성된 Podfile에 다음과 같이 적어준다.

pod 'ReactorKit'
pod 'RxSwift'
pod 'RxCocoa'
pod 'SnapKit'

pod install 명령어로 설치를 한 다음 생성된 .xcworkspace 파일을 열어준다.

 

우선 스토리보드 없이 코드를 작성하려면 스토리보드를 삭제해야 한다. Main.storyboard, Launch.storyboard 다 휴지통으로 보내주고, 프로젝트 정보 General - Deployment Info - Main Interface를 비워준다.

Info.plist에 들어가서 Information Property List - Application Scene Manifest - Scene Configuration - Application Session Role - Item 0 (Default Configuration) 에 들어가서 Storyboard Name 부분 행을 아예 지워준다. (마우스 오버레이 후 -(빼기)버튼 눌러도 된다.)

그 다음 SceneDelegate.swift로 가서 func scene() 부분을 고쳐주어야 한다.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    guard let _ = (scene as? UIWindowScene) else { return }
}

이렇게 생긴 거를

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    guard let windowScene = (scene as? UIWindowScene) else { return }
    window = UIWindow(frame: windowScene.coordinateSpace.bounds)
    window?.windowScene = windowScene
    window?.rootViewController = ViewController()
    window?.makeKeyAndVisible()
}

이렇게 바꿔준다.

커밋 추가 (message : storyboard 제거)

이제 뷰를 하나 만들어야 한다.
CounterView.swift 파일 하나 만들어주고, 다음 내용을 집어넣는다.

//
//  CounterView.swift
//  Counter-Prac
//
//  Created by 정종인 on 2020/06/06.
//  Copyright © 2020 swmaestro10th. All rights reserved.
//

import Foundation
import UIKit
import SnapKit

class CounterView: UIView {
    private let backgroundView: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        return v;
    }()
    
    private let valueLabel: UILabel = {
        let v = UILabel()
        v.text = "0"
        v.textAlignment = .center
        return v
    }()
    
    private let increaseButton: UIButton = {
        let v = UIButton(type: .roundedRect)
        v.backgroundColor = .brown
        return v
    }()
    
    private let decreaseButton: UIButton = {
        let v = UIButton(type: .roundedRect)
        v.backgroundColor = .brown
        return v
    }()
    
    private let activityIndicatorView: UIActivityIndicatorView = {
        let v = UIActivityIndicatorView(style: .medium)
        return v
    }()
    
    func setup() {
        setupUI()
        setBind()
    }
    
}

extension CounterView {
    private func setupUI() {
        addSubviews()
        setLayout()
    }
    
    private func setBind() {
        //delegate, datasource
    }
    
    private func addSubviews() {
        self.addSubview(self.backgroundView)
        self.backgroundView.addSubview(self.valueLabel)
        self.backgroundView.addSubview(self.increaseButton)
        self.backgroundView.addSubview(self.decreaseButton)
        self.backgroundView.addSubview(self.activityIndicatorView)
    }
    
    private func setLayout() {
        self.backgroundView.snp.makeConstraints {
            $0.top.left.bottom.right.equalTo(safeAreaLayoutGuide)
        }
        self.valueLabel.snp.makeConstraints {
            $0.center.equalToSuperview()
            $0.width.height.equalTo(45)
        }
        self.increaseButton.snp.makeConstraints {
            $0.centerY.equalToSuperview()
            $0.trailing.equalTo(-30)
            $0.width.height.equalTo(45)
        }
        self.decreaseButton.snp.makeConstraints {
            $0.centerY.equalToSuperview()
            $0.leading.equalTo(30)
            $0.width.height.equalTo(45)
        }
        self.activityIndicatorView.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.top.equalTo(valueLabel).offset(30)
            $0.width.height.equalTo(45)
        }
    }
}

 

이걸 보여지게 하려면 뷰 컨트롤러에서 이 뷰를 불러와야 한다.

ViewController의 내용을 다음과 같이 바꾼다.

//
//  ViewController.swift
//  Counter-Prac
//
//  Created by 정종인 on 2020/06/05.
//  Copyright © 2020 swmaestro10th. All rights reserved.
//

import UIKit

class ViewController: UIViewController {
    private var counterView = CounterView()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        counterView.setup()
    }
    
    override func loadView() {
        self.view = counterView
    }

}

 

커밋 추가 (message : view 구성)

 이제 본격적으로 ReactorKit을 활용해본다.

CounterViewReactor.swift파일을 하나 만들고, Reactor 프로토콜을 추가해본다.

Action, Mutation, State, mutate(), reduce()를 구현한다.

코드 : 

//
//  CounterViewReactor.swift
//  Counter-Prac
//
//  Created by 정종인 on 2020/06/06.
//  Copyright © 2020 swmaestro10th. All rights reserved.
//

import Foundation
import ReactorKit

class CounterViewReactor: Reactor {
    
    enum Action {
        case increase
        case decrease
    }
    
    enum Mutation {
        case increaseValue
        case decreaseValue
        case isLoading(Bool)
    }
    
    struct State {
        var value: Int
        var isLoading: Bool
    }
    
    var initialState: State
    
    init() {
        self.initialState = State(value: 0, isLoading: false)
    }
    
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .increase:
            return Observable.concat([
                Observable.just(Mutation.isLoading(true)),
                Observable.just(Mutation.increaseValue),
                Observable.just(Mutation.isLoading(false))
            ])
        case .decrease:
            return Observable.concat([
                Observable.just(Mutation.isLoading(true)),
                Observable.just(Mutation.decreaseValue),
                Observable.just(Mutation.isLoading(false))
            ])
        }
    }
    
    func reduce(state: State, mutation: Mutation) -> State {
        var state = state
        switch mutation {
        case .increaseValue:
            state.value += 1
        case .decreaseValue:
            state.value -= 1
        case .isLoading(let isLoading):
            state.isLoading = isLoading
        }
        return state
    }
}

 

커밋 추가(message : reactor 추가)

이제 View 부분을 작성해야 한다. 다시 CounterView로 와서, disposeBag 변수를 하나 추가 해주고, View 프로토콜을 추가한 코드를 extension에 짜준다.

//
//  CounterView.swift
//  Counter-Prac
//
//  Created by 정종인 on 2020/06/06.
//  Copyright © 2020 swmaestro10th. All rights reserved.
//

import Foundation
import UIKit
import SnapKit
import ReactorKit
import RxCocoa

class CounterView: UIView {
    weak var vc: CounterViewController?
    
    init(controlBy viewController: CounterViewController) {
        self.vc = viewController
        super.init(frame: UIScreen.main.bounds)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private let baseView: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        return v
    }()
    
    private let backgroundView: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        return v
    }()
    
    private let valueLabel: UILabel = {
        let v = UILabel()
        v.text = "0"
        v.textAlignment = .center
        return v
    }()
    
    private let increaseButton: UIButton = {
        let v = UIButton(type: .roundedRect)
        v.setTitle("+", for: .normal)
        return v
    }()
    
    private let decreaseButton: UIButton = {
        let v = UIButton(type: .roundedRect)
        v.setTitle("-", for: .normal)
        return v
    }()
    
    private let activityIndicatorView: UIActivityIndicatorView = {
        let v = UIActivityIndicatorView(style: .medium)
        return v
    }()
    
    var disposeBag: DisposeBag = DisposeBag()
    
    func setup() {
        setupUI()
        setBind()
    }
    
}

extension CounterView {
    private func setupUI() {
        addSubviews()
        setLayout()
    }
    
    private func setBind() {
        //delegate, datasource
    }
    
    private func addSubviews() {
        self.addSubview(self.baseView)
        self.baseView.addSubview(self.backgroundView)
        self.backgroundView.addSubview(self.valueLabel)
        self.backgroundView.addSubview(self.increaseButton)
        self.backgroundView.addSubview(self.decreaseButton)
        self.backgroundView.addSubview(self.activityIndicatorView)
    }
    
    private func setLayout() {
        self.baseView.snp.makeConstraints {
            $0.top.left.bottom.right.equalToSuperview()
        }
        self.backgroundView.snp.makeConstraints {
            $0.top.left.bottom.right.equalTo(safeAreaLayoutGuide)
        }
        self.valueLabel.snp.makeConstraints {
            $0.center.equalToSuperview()
            $0.width.height.equalTo(45)
        }
        self.increaseButton.snp.makeConstraints {
            $0.centerY.equalToSuperview()
            $0.trailing.equalTo(-30)
            $0.width.height.equalTo(45)
        }
        self.decreaseButton.snp.makeConstraints {
            $0.centerY.equalToSuperview()
            $0.leading.equalTo(30)
            $0.width.height.equalTo(45)
        }
        self.activityIndicatorView.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.top.equalTo(valueLabel).offset(30)
            $0.width.height.equalTo(45)
        }
    }
}

extension CounterView: View {
    func bind(reactor: CounterViewReactor) {
        increaseButton.rx.tap
            .map { Reactor.Action.increase }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        decreaseButton.rx.tap
            .map { Reactor.Action.decrease }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        reactor.state
            .map { $0.value }
            .distinctUntilChanged()
            .map { "\($0)" }
            .bind(to: valueLabel.rx.text)
            .disposed(by: disposeBag)
        
        reactor.state
            .map { $0.isLoading }
            .distinctUntilChanged()
            .bind(to: activityIndicatorView.rx.isAnimating)
            .disposed(by: disposeBag)
        
        
    }
}

 

새로운 CounterViewController.swift를 작성하고, view를 연결하고, counterView의 reactor도 정의를 해준다.

//
//  CounterViewController.swift
//  Counter-Prac
//
//  Created by 정종인 on 2020/06/07.
//  Copyright © 2020 swmaestro10th. All rights reserved.
//

import Foundation
import UIKit

class CounterViewController: UIViewController {
    private lazy var counterView = CounterView(controlBy: self)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        counterView.setup()
        counterView.reactor = CounterViewReactor()
    }
    
    override func loadView() {
        self.view = counterView
    }
}

그 다음, SceneDelegate에 ViewController()부분도 CounterViewcontroller부분으로 바꿔주어야 한다.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    guard let windowScene = (scene as? UIWindowScene) else { return }
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.windowScene = windowScene
    window?.rootViewController = CounterViewController()
    window?.makeKeyAndVisible()
}

그밖에도 delay추가 등 세부적인 코드 추가 후 빌드.

커밋 추가(message: finish)

https://github.com/chongin12/Counter-Prac

 

chongin12/Counter-Prac

ReactorKit Example Counter Practice. Contribute to chongin12/Counter-Prac development by creating an account on GitHub.

github.com

 

반응형