-
[사전캠프 TIL 9, 10일차] Combine 딥다이브 🔥iOS💖 2025. 2. 14. 19:04
학습 키워드
🗂️ Combine 🔴
<목차>
- Publisher
- Just, Future
- Subscriber
- assign(to:on:) , sink(receiveValue:)
- subscriptions
- cancel()
- store(in:)
- store(in:) 사용 이유
- AnyCancellable
- 손정리 이미지
- inout parameter
Combine
- Reactive Programming (Apple에서 제공)
- ex. Combine, Rx
- 함수형 프로그래밍
- iOS 13 이상
- 데이터를 비동기적 처리, 이벤트를 선언적으로 처리
Combine 주요 개념
(event stream : publisher ➡️ operator ➡️ subscriber)- Publisher : 데이터를 제공하는 주체
- Subscriber : 데이터를 수신 & 처리하는 주체
- Operators : 데이터를 변환(가공) / filtering 하는 중간 연산자
1. Publisher

protocol Publisher - 데이터 배출하는 역할
- Output & Failure 타입 정의
- Subscriber 가 요청한 갯수 만큼 데이터 제공
- built-in Publisher : Just, Future
- Just : 값을 다룸
- Future : function 을 다룸
// 직접 구현해보기 Step1.2 let justPublisher = Just(100) justPublisher .map { $0 +10 } .sink { print($0) } /* received value 110 */// 직접 구현해보기 Step1.1 let numbers = [10, 20, 30, 40, 50].publisher numbers .map { $0 * 2 } .sinkn { print($0) } /* received value 20 40 60 80 100 */- iOS 에서 Publisher 을 자동으로 제공
- Timer
- NotificationCenter
- URLSession.dataTask
import Combine import Foundation let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")! var cancellable: AnyCancellable? // subscription cancel 방식 1 // AnyCancellable 타입인 cancellable로 받는이유?🔴 >> sink로 구독시 AnyCancellable 이 리턴됨 cancellable = URLSession.shared.dataTaskPublisher(for: url) .map { $0.data } .decode(type: Todo.self, to: JSONDecode()) .sink( receiveCompletion: { print("completion \($0)") }, receiveValue: { print("value \($0.title)" } ) cancellable.cancel() // 🔴 // subscription cancel 방식 2 var cancellable2 = Set<AnyCancellable>() URLSession.shared.dataTaskPublisher(for: url) .map { $0.data } .decode(type: Todo.self, to: JSONDecode()) .sink( receiveCompletion: { print("completion \($0)") }, receiveValue: { print("value \($0.title)" } ).store(in: &cancellable2) // 🔴 /* Instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'JSONDecoder.Input' (aka 'Data') be equivalent */
2. Subscriber

protocol Subscriber 
assign(to: on:) 
sink(receiveValue: ) - Publisher에게 데이터 요청 (= 구독 = subscribe)
- Publisher 을 구독하면, 구독 티켓의 의미를 띄는 Subscription 이 발행됨
- Input & Failure 타입 정의 필요
- Publisher 구독 후, 갯수를 요청함
- Built-in Subscriber:
- sink
- Publisher 가 제공하는 데이터를 받는 Closure 를 제공
- sink로 구독시, return value로 AnyCancellable 얻어짐 🔴
- assign
- Publisher 가 제공한 데이터를, 특정 객체의 키패스(property name) 에 할당
- assign 으로 구독시, return value로 AnyCancellable 얻어짐 🔴
- sink
let numbers = [1, 2, 3, 4].publisher class NumberChange { var updateNum: Int = 0 { didSet { // updateNum에 값이 set 되었을때, 실행되는 함수 print("Did set property to \(updateNum)") } } } let numChangeObj = NumberChange() let subscription = numbers.assign(to: \.updateNum, on: numChangeObj) // 🔴 // to. : key-path, on: object name print("Value: \(numChangeObj.updateNum") // 출력 // Did set property to 1 // Did set property to 2 // Did set property to 3 // Did set property to 4 // Value: 42-1. Subscription
: publisher-subscriber 가 연결됨을 나타내는 존재- 🔴 Cancellable protocol 준수 🔴
- 따라서, subscription 을 통해 publisher-subscriber 연결을 cancellation 할 수 있다.
- = Combine 작업들을 취소할 수 있다는 의미를 가진 프로토콜
🔴 Cancellable Protocol 🔴
: activity / action (= 컴바인 event stream, = publisher-subscriber 연결) 의 취소를 지원하는 프로토콜
Cancellable protocol 🔴 activity / action (= 컴바인 event stream) 을 취소시키는 방법 🔴
1. cancel() 직접 사용을 통해
- Calling cancel() frees up any allocated resources. It also stops side effects such as timers, network access, or disk I/O. (공식문서 참조)
activity / action (= 컴파인 event stream) 의 cancellation 은, 할당된 리소스를 해제시켜
> Side effect 를 중단시킨다. (timers, network access, disk I/O, etc...)
- cancelled 되었는지 확인!
- sink 의 cancel() breakpoint 찍어보면 됨
- sink 의 cancel() breakpoint 찍어보면 됨
- cancelled 되었는지 확인!
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")! var cancellable: AnyCancellable? // subscription cancel 방식 1 // cancel() cancellable = URLSession.shared.dataTaskPublisher(for: url) .map { $0.data } .decode(type: Todo.self, to: JSONDecode()) .sink( receiveCompletion: { print("completion \($0)") }, receiveValue: { print("value \($0.title)" } ) cancellable.cancel() // 🔴2. store(in: )
: cancel 을 예약!- Instance Method 인 store(in: ) 안에 Cancellable instances 저장
➡️ ex. 특정 화면(UIViewController) 의 instance가 해제되면, 그 안의 구독들도 같이 날아가게끔 하고싶은 경우
- store(in: ) 종류
- store<C> (in: inout C)
: inout 제네릭 parameter 이므로, 사용시 &변수 를 사용 - store(in: inout Set<AnyCancellable>)
: inout Set<AnyCancellable> parameter, 사용시 &변수 를 사용
- store<C> (in: inout C)
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")! var cancellable: AnyCancellable? // subscription cancel 방식 2 // store(in: ) var cancellable2 = Set<AnyCancellable>() URLSession.shared.dataTaskPublisher(for: url) .map { $0.data } .decode(type: Todo.self, to: JSONDecode()) .sink( receiveCompletion: { print("completion \($0)") }, receiveValue: { print("value \($0.title)" } ).store(in: &cancellable2) // 🔴
Combine > Cancellable > store(in: ) 
store(in: inout ...) 2-1. store(in: ) 🔴 더 자세하게 🔴
1 ) store(in: ) 잘못된 사용
public class ViewController: UIViewController { let subject: PassthroughSubject<String, Never> = PassthroughSubject() let x = subject.sink { print($0) } // ❌ let y = subject.sink { print($0) } // ❌ public override func viewDidLoad() { super.viewDidLoad() } public required init?(coder: NSCoder) { super.init(coder: coder) } }➡️ 해당 클래스의 init을 통한 객체생성 전에는, self접근 불가
- subject.sink { }
2 ) 🔴🔴🔴 store(in: ) 사용 이유 🔴🔴🔴
# rc
# strong 참조
➡️ event stream 이 불린, ViewController 이 종료 될 때 까지 stream 유지됨!
public class ViewController: UIViewController { let subject: PassthroughSubject<String, Never> = PassthroughSubject() var subscriptionsBag = Set<AnyCancellable>() public override func viewDidLoad() { super.viewDidLoad() let x = subject.sink { print($0) } .store(in: &subscriptionsBag) let y = subject.sink { print($0) } .store(in: &subscriptionsBag) subject.send("hello") } }- AnyCancellable 타입의 instance (=x, y) 를 rc 를 1 늘려주는 Set에, store(in: ) 을 통해 넣어주면,
🔴 rc 가 각각 +1 🔴 된다.
- viewDidLoad 내부에 있는 combine event stream (=x, y) 는,
rc != 0 이기 때문에, viewDidLoad가 종료되어도, cancel 되지 않는다. - rc != 0 인 이유?
- viewDidLoad가 종료되더라도,
ViewController 자체 안에 있는 subscriptionsBag 에서, x,y를 strong 참조 하기 때문에 rc가 각각 1로 유지됨
- viewDidLoad가 종료되더라도,
- 🔴 stream 이 끊어지는 시점? 🔴
- ViewController 자체가 deinit 되는 시점!
➡️ subscriptionsBag 이 deinit
➡️ x, y의 rc 가 0이 되고
➡️ AnyCancellable 이 deinit 되면서, ➡️ cacel 호출, ➡️ event stream 끊어주게 됨!!
- ViewController 자체가 deinit 되는 시점!
- viewDidLoad 내부에 있는 combine event stream (=x, y) 는,
AnyCancellable
final public class AnyCancellable : Cancellable, Hashable
Combine > AnyCancellable - Overview (공식문서)
- A type-erasing cancellable object that executes a provided closure when canceled.
- Subscriber implementations can use this type to provide a “cancellation token” that makes it possible for a caller to cancel a publisher, but not to use the Subscription object to request items.
An AnyCancellable instance automatically calls cancel() when deinitialized.
- subscriber은, publisher에의 구독을 취소 할 수 있게 해주는 (= 취소토큰을 제공해주는) AnyCancellable 으로 구독을 취소 가능!
AnyCancellable 은 deinitialized 될 때, 자동으로 cancel() 호출!
- subscriber은, publisher에의 구독을 취소 할 수 있게 해주는 (= 취소토큰을 제공해주는) AnyCancellable 으로 구독을 취소 가능!
- sink 로 이벤트 구독시, return 값으로 AnyCancellable 이 얻어진다.

sink(receiveValue: ) returns AnyCancellable final class Person { @Published var name: String init(name: String) { self.name = name } } let mimi = Person("mimi") let anyCancellable = mimi.$name .sink { print("name changed: \($0)") } mimi.name = "mimi s" // name changed: mimi s anyCancellable.cancel() mimi.name = "mimi ssss" // print("name changed: \($0)") 실행 X
손 정리

🔥 한 페이지로 다시 정리 🔥 ② store(in: )의 더 자세하게 ☆는, 이 게시글 2 )🔴🔴🔴store(in: ) 사용 이유🔴🔴🔴 목차 아래의 코드 참조!
func sink()public func sink(receiveValue: @escaping (Output) -> Void) -> AnyCancellable { let subscriber = Subscribers.Sink<Output, Failure>( receiveCompletion: { _ in }, receiveValue: eceiveValue ) subscribe(subscriber) return AnyCancellable(subscriber) }Protocol Cancellable
pubilc protocol Cancellable { /// Cancel the activity. func cancel() }
inout paramater 에 관하여
https://hortenssiaa.tistory.com/70
(내배캠 TIL) 8일차 _ Generic
학습 키워드제네릭 제네릭 (Generic)사전적 의미 : 포괄적인, 통칭의코드의 유연성 / 재사용성 높이기 위해, 타입에 의존하지 않는 코드를 작성하는 방법같은 로직을, 다양한 타입에서 사용
hortenssiaa.tistory.com
출처
https://developer.apple.com/documentation/combine/cancellable
Cancellable | Apple Developer Documentation
A protocol indicating that an activity or action supports cancellation.
developer.apple.com
https://developer.apple.com/documentation/combine/anycancellable
AnyCancellable | Apple Developer Documentation
A type-erasing cancellable object that executes a provided closure when canceled.
developer.apple.com
https://ios-development.tistory.com/1116
[iOS - SwiftUI] Combine의 Cancellable (AnyCancellable) 사용 방법
목차) Combine - 목차 링크 Cancellable Combine 작업들을 취소할 수 있다는 의미를 가지고 있는 프로토콜 Combine에서는 이벤트 스트림을 action이라는 이름을 사용, action을 취소할 수 있는 프로토콜이 Cancel
ios-development.tistory.com
https://velog.io/@kimscastle/iOSCombine%EC%9D%98-Cancellable-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%951 ❤️❤️❤️
[iOS]Combine의 Cancellable 딥다이브(1)
그래서 아마도 지금 제목이 Cancellable이지만 combine에서 실제로 cancel이 어떤 로직으로 작동하는지에 대한 아주아주 깊은 내용이 될겁니다 물론 우리는 combine의 실제 내부코드는 모르기때문에 combi
velog.io
'iOS💖' 카테고리의 다른 글
[Project TIL 12일차] 랜덤 컬러 생성 앱 (feat. Clean Code 🎯) (0) 2025.02.20 [사전캠프 TIL 11일차] Codable (w/ CodingKey) (0) 2025.02.17 [사전캠프 TIL 8일차] Generic (0) 2025.02.13 [사전캠프 TIL 7일차] GCD (DispatchQueue 종류 & 특성) (0) 2025.02.13 [사전캠프 TIL 6일차] GCD (Serial vs. Concurrent) (0) 2025.02.12 - Reactive Programming (Apple에서 제공)