[UIKit] Combine: CombineCocoa

Junyoung Park·2022년 10월 1일
0

UIKit

목록 보기
44/142
post-thumbnail

CombineCocoa & FlatMap in Combine iOS Reactive Programming

Combine: CombineCocoa

구현 목표

  • CombineCocoa 라이브러리

구현 태스크

  • Combine 프레임워크를 사용한 반응형 프로그래밍
  • CombineCocoa 라이브러리를 통한 UX 데이터 바인딩

핵심 코드

    private var destinationPublisher: AnyPublisher<Destination, Never> {
        return destinationSegment.selectedSegmentIndexPublisher.flatMap { index in
            return Just(Destination.allCases[index]).eraseToAnyPublisher()
        }.eraseToAnyPublisher()
    }
    private func observe() {
        Publishers.CombineLatest3(destinationPublisher, passengerPublisher, seatPublisher)
            .sink { [weak self] destination, passenger, seat in
                guard let self = self else { return }
                let price = destination.costPerPassenger * passenger.doubleValue + seat.priceMultiplier
                self.priceLabel.text = price.formatted(.currency(code: "USD"))
            }
            .store(in: &cancellables)
        button
            .tapPublisher
            .sink { _ in
            print("Book Button Tapping Without AddTarget")
            }.store(in: &cancellables)
    }
  • 세그멘트 컨트롤 선택지에 따른 현재 인덱스 값을 관찰하기 위한 퍼블리셔
  • 퍼블리셔를 관찰하는 observe 함수 → UI 관찰값에 따라 패치
  • tapPublishertarget을 추가하는 형식을 따르지 않고도 설계 가능(CombineCocoa)

소스 코드

import UIKit
import Combine
import CombineCocoa

class BookingOptionViewController: UIViewController {
    private let priceTextLabel: UILabel = {
        let label = UILabel()
        label.text = "Fianl Price"
        label.textColor = .black
        label.font = .preferredFont(forTextStyle: .headline)
        return label
    }()
    private let priceLabel: UILabel = {
        let label = UILabel()
        label.textColor = .black
        label.font = .preferredFont(forTextStyle: .title1)
        return label
    }()
    private let priceStack: UIStackView = {
        let stack = UIStackView()
        stack.axis = .vertical
        stack.alignment = .fill
        stack.distribution = .equalSpacing
        stack.spacing = 15
        stack.backgroundColor = .white
        stack.isLayoutMarginsRelativeArrangement = true
        stack.layoutMargins = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
        stack.layer.cornerRadius = 12
        return stack
    }()
    private let optionStack: UIStackView = {
        let stack = UIStackView()
        stack.axis = .vertical
        stack.alignment = .fill
        stack.distribution = .equalSpacing
        stack.spacing = 10
        stack.backgroundColor = UIColor.white.withAlphaComponent(0.8)
        stack.isLayoutMarginsRelativeArrangement = true
        stack.layoutMargins = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
        stack.layer.cornerRadius = 12
        return stack
    }()
    private let destinationTextLabel: UILabel = {
        let label = UILabel()
        label.text = "Destination"
        label.font = .preferredFont(forTextStyle: .body)
        label.textColor = .black
        return label
    }()
    private let destinationSegment: UISegmentedControl = {
        let segment = UISegmentedControl(items: Destination.allCases.map{$0.rawValue.uppercased()})
        segment.selectedSegmentIndex = 0
        return segment
    }()
    private let passengerTextLabel: UILabel = {
        let label = UILabel()
        label.text = "Number of Passengers"
        label.font = .preferredFont(forTextStyle: .body)
        label.textColor = .black
        return label
    }()
    private let passengerSement: UISegmentedControl = {
        let segment = UISegmentedControl(items: Passenger.allCases.map{$0.rawValue.uppercased()})
        segment.selectedSegmentIndex = 0
        return segment
    }()
    private let seatTextLabel: UILabel = {
        let label = UILabel()
        label.text = "Seat Type"
        label.font = .preferredFont(forTextStyle: .body)
        label.textColor = .black
        return label
    }()
    private let seatSegment: UISegmentedControl = {
        let segment = UISegmentedControl(items: Seat.allCases.map{$0.rawValue.uppercased()})
        segment.selectedSegmentIndex = 0
        return segment
    }()
    private let button: UIButton = {
        let button = UIButton()
        var config = UIButton.Configuration.filled()
        config.background.backgroundColor = .systemBlue
        button.configuration = config
        button.setTitle("Book", for: .normal)
        return button
    }()
    private var destinationPublisher: AnyPublisher<Destination, Never> {
        return destinationSegment.selectedSegmentIndexPublisher.flatMap { index in
            return Just(Destination.allCases[index]).eraseToAnyPublisher()
        }.eraseToAnyPublisher()
    }
    private var passengerPublisher: AnyPublisher<Passenger, Never> {
        return passengerSement.selectedSegmentIndexPublisher
            .flatMap { index in
                return Just(Passenger.allCases[index]).eraseToAnyPublisher()
            }.eraseToAnyPublisher()
    }
    private var seatPublisher: AnyPublisher<Seat, Never> {
        return seatSegment.selectedSegmentIndexPublisher.flatMap { index in
            return Just(Seat.allCases[index]).eraseToAnyPublisher()
        }.eraseToAnyPublisher()
    }
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        observe()
    }
    
    private func setUI() {
        view.backgroundColor = .secondarySystemBackground
        title = "Booking Options"
        navigationController?.navigationBar.prefersLargeTitles = true
        priceStack.translatesAutoresizingMaskIntoConstraints = false
        priceLabel.translatesAutoresizingMaskIntoConstraints = false
        priceTextLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(priceStack)
        priceStack.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        priceStack.topAnchor.constraint(equalTo: view.topAnchor, constant: 200).isActive = true
        priceStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30).isActive = true
        priceStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30).isActive = true
        priceStack.addArrangedSubview(priceTextLabel)
        priceStack.addArrangedSubview(priceLabel)
        optionStack.translatesAutoresizingMaskIntoConstraints = false
        destinationTextLabel.translatesAutoresizingMaskIntoConstraints = false
        destinationSegment.translatesAutoresizingMaskIntoConstraints = false
        passengerTextLabel.translatesAutoresizingMaskIntoConstraints = false
        passengerSement.translatesAutoresizingMaskIntoConstraints = false
        seatTextLabel.translatesAutoresizingMaskIntoConstraints = false
        seatSegment.translatesAutoresizingMaskIntoConstraints = false
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(optionStack)
        optionStack.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        optionStack.topAnchor.constraint(equalTo: priceStack.bottomAnchor, constant: 30).isActive = true
        optionStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30).isActive = true
        optionStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30).isActive = true
        button.heightAnchor.constraint(equalToConstant: 50).isActive = true
        optionStack.addArrangedSubview(destinationTextLabel)
        optionStack.addArrangedSubview(destinationSegment)
        optionStack.addArrangedSubview(passengerTextLabel)
        optionStack.addArrangedSubview(passengerSement)
        optionStack.addArrangedSubview(seatTextLabel)
        optionStack.addArrangedSubview(seatSegment)
        optionStack.addArrangedSubview(button)
    }
    
    private func observe() {
        Publishers.CombineLatest3(destinationPublisher, passengerPublisher, seatPublisher)
            .sink { [weak self] destination, passenger, seat in
                guard let self = self else { return }
                let price = destination.costPerPassenger * passenger.doubleValue + seat.priceMultiplier
                self.priceLabel.text = price.formatted(.currency(code: "USD"))
            }
            .store(in: &cancellables)
        button
            .tapPublisher
            .sink { _ in
            print("Book Button Tapping Without AddTarget")
            }.store(in: &cancellables)
    }
}
import Foundation

enum Destination: String, CaseIterable {
    case tokyo
    case newyork
    case incheon
    
    var costPerPassenger: Double {
        switch self {
        case .tokyo:
            return 150
        case .newyork:
            return 300
        case .incheon:
            return 200
        }
    }
}
  • UI 뷰 구현
  • 세그멘트 컨트롤의 선택지를 관찰하는 퍼블리셔를 발행
  • 해당 퍼블리셔를 관찰하는 observe 함수 → 결과적으로 UI에 표현될 가격 계속해서 체크
  • 버튼 탭 버튼 함수 구현
import Foundation

enum Passenger: String, CaseIterable {
    case single
    case double
    
    var doubleValue: Double {
        switch self {
        case .single: return 1.0
        case .double: return 2.0
        }
    }
}
import Foundation

enum Seat: String, CaseIterable {
    case economy
    case business
    
    var priceMultiplier: Double {
        switch self {
        case .economy:
            return 1.0
        case .business:
            return 1.5
        }
    }
}

구현 화면

  • target-action 형식의 UIKit 구현이 아니라 Combine을 사용하여 반응형 프로그래밍을 통해 구현한게 가장 큰 특징
profile
JUST DO IT

0개의 댓글