ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [사전캠프 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 얻어짐 🔴
    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: 4

     

     

     

     

     

    2-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 찍어보면 됨
           
    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, 사용시 &변수 를 사용

     

    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로 유지됨

      • 🔴 stream 이 끊어지는 시점? 🔴
        • ViewController 자체가 deinit 되는 시점!
          ➡️ subscriptionsBag 이 deinit
          ➡️ x, y의 rc 가 0이 되고
          ➡️ AnyCancellable 이 deinit 되면서, ➡️  cacel 호출, ➡️  event stream 끊어주게 됨!!

     

     

     

     

     

     

     


     

    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() 호출! 
     

     

     

     

    • 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

     

    댓글

Designed by black7375.