0. 서론
기존 @Published로 작성된 계산기 코드를 Combine의 Subject로 바꿔보면서 학습한 것을 적어본다.
1. 기존 코드
편안한 학습을 위해 간단한 정수 사칙 연산 로직을 가지고 있는 뷰모델을 짜보았다.
final class CalcViewModel: ObservableObject {
@Published var previewText = ""
@Published var resultNumberText = ""
private var operators: Set<Character> = ["+", "-", "*", "/"]
enum Action {
case numberTapped(String)
case operatorTapped(String)
case deleteTapped
case allClearTapped
case equalTapped
}
func send(_ action: Action) {
switch action {
case .numberTapped(let numberText):
handleNumber(numberText)
case .operatorTapped(let operatorText):
handleOperator(operatorText)
case .deleteTapped:
if !previewText.isEmpty { previewText.removeLast() }
case .allClearTapped:
resultNumberText = ""
previewText = ""
case .equalTapped:
handleEqual()
}
}
}
extension CalcViewModel {
private func handleNumber(_ numberText: String) {
if numberText == "0" {
if let lastText = previewText.last, !operators.contains(lastText) {
previewText += numberText
}
} else {
previewText += numberText
}
}
private func handleOperator(_ operatorText: String) {
if let lastText = previewText.last, !operators.contains(lastText) {
previewText += operatorText
}
}
private func handleEqual() {
guard let lastText = previewText.last, !operators.contains(lastText)
else { return }
if let expression = NSExpression(format: previewText) as NSExpression?,
let result = expression.expressionValue(with: nil, context: nil) as? NSNumber {
resultNumberText = result.stringValue
}
}
}
2. @Published를 Subject로 바꾸기
일단... @Published를 Subject로 바꿔보자
import Foundation
import Combine
final class CalcViewModel: ObservableObject {
let previewTextPublisher = CurrentValueSubject<String, Never>("")
let resultNumberPublisher = PassthroughSubject<Int, Never>()
// ... code
}
이때, Subject는 뭐하는 녀석일까? 공식문서를 살펴보면 아래와 같이 나와 있다.

위를 잘보면 Subject도 프로토콜이며 Publisher 프로토콜을 채택하고 있고, Summary에는 “stream에 send(_:) 메서드를 호출해서 값을 주입할 수 있는 Publisher이다.”라고 써져 있다.
아하 Subject는 스트림을 만들 수도 있고, 값을 방출할수도 있고, (publisher 프로토콜을 채택하니깐) send() 메서드를 통해 값을 주입할 수도 있구나!
(Combine을 사용하지 않던 코드에 Combine 모델을 적용하고 싶을 때 사용하면 좋다고함-참고1번)
Subject를 채택한 객체는 두 가지 종류가 있다.
- PassthroughSubject
이름에서 알 수 있다 시피 값을 스쳐보내는 쿨한 녀석 이라고 생각하면된다. 정의도 살펴보면 딱히 초기값도 필요 없으며 최신 값을 저장하기 위한 공간도 없다고 되어있다. - CurrentValueSubject
Passthroughsubject와 다르게 가장 최근에 published된 값의 버퍼를 유지한다고 한다.
내 코드에서
resultNumberText는 이전값 필요없기 때문에 Passthroughsubject를 previewText는 이전 값이 operator가 마지막에 있는지, empty인지 확인하기 위해서 필요하기 때문에 CurrentValueSubject를 썼다.
3. 이벤트 처리
extension CalcViewModel {
private func handleNumber(_ numberText: String) {
if numberText == "0" {
if let lastText = previewTextPublisher.value.last, !operators.contains(lastText) {
previewTextPublisher.value += numberText
}
} else {
previewTextPublisher.value += numberText
}
}
private func handleOperator(_ operatorText: String) {
if let lastText = previewTextPublisher.value.last, !operators.contains(lastText) {
previewTextPublisher.value += operatorText
}
}
private func handleEqual() {
guard let lastText = previewTextPublisher.value.last, !operators.contains(lastText) else { return }
if let expression = NSExpression(format: previewTextPublisher.value) as NSExpression?,
let result = expression.expressionValue(with: nil, context: nil) as? NSNumber {
resultNumberPublisher.send(result.intValue)
}
}
}
- resultNumberPublisher.send(result.stringValue)
요거는 위에서 설명했던 send 메서드 - previewTextPublisher.value += operatorText
CurrentValueSubject인 previewTextPublisher는 value를 바꿔주면 알아서 값이 방출된다고 한다.
4. 뷰에서 구독
import SwiftUI
import Combine
struct ContentView: View {
@StateObject private var viewModel = CalcViewModel()
@State private var resultNumberText = "0"
@State private var previewText = ""
@State private var cancellableBag = Set<AnyCancellable>()
// code ..
.onAppear {
// 뷰가 나타날때 State변수와 바인딩
viewModel.resultNumberPublisher
.map { String($0) }
.sink { resultNumberText = $0 }
.store(in: &cancellableBag)
viewModel.previewTextPublisher
.assign(to: \.previewText, on: self)
.store(in: &cancellableBag)
}
}
- .map { String($0) } → Int로 들어오는 값을 String으로 변환해주는 Operator
- .sink { previewText = $0 } → Publisher가 방출한 값을 구독, 값을 방출하면 넣어주는 역할
- .assign(to: \\.previewText, on: self) → 위 .sink와 다르게 값을 바로 할당할 수 있다
- .store(in: &cancellableBag) → 뷰가 사라질때 자동으로 구독을 해제
풀 코드는 여기에
https://github.com/Monfi98/ESTsoftCamp/tree/main/Study/CalculatorCombine
ESTsoftCamp/Study/CalculatorCombine at main · Monfi98/ESTsoftCamp
📚 이스트소프트 프론티어 iOS 부트캠프에서 매일매일 배운 내용을 기록하는 "Today I Learned" (TIL) 저장소입니다. - Monfi98/ESTsoftCamp
github.com
6. 참고
[Combine] Subject - Combine 공부 4
안녕하세요 Pingu입니다.🐧 지난 글에서는 Apple에서 미리 정의해둔 Publisher들을 알아봤었는데, 이번 글에서는 이어서 Publisher 프로토콜을 채택하는 또 다른 녀석들인 Subject들에 대해서 알아보려고
icksw.tistory.com
'iOS > Swift' 카테고리의 다른 글
| [Swift] ARC, weak, unowned (0) | 2025.07.01 |
|---|---|
| [SwiftUI] 렌더링 실험 (3) | 2025.06.25 |
| [Swift] SwiftData Concurrency 사용하기 (6) | 2025.06.20 |
| [Swift] 스위프트 기본문법#4 - 튜플(Tuple) (0) | 2023.05.11 |
| [Swift] 스위프트 기본문법#3 - 제어문 (0) | 2023.02.14 |
댓글