CombineCocoa & FlatMap in Combine iOS Reactive Programming
Combine: 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 관찰값에 따라 패치
tapPublisher
등 target
을 추가하는 형식을 따르지 않고도 설계 가능(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
을 사용하여 반응형 프로그래밍을 통해 구현한게 가장 큰 특징