본문 바로가기
Xcode/Swift - PlayGround

PlayGround) Combine 체험기#2

by 후르륵짭짭 2023. 1. 8.
728x90
반응형

일본 오사카 어딘가

안녕하세요! 후르륵짭짭입니다.

다시 Combine으로 돌아 왔습니다.

이번에는 Combine의 Operator들에 대해서 정리하고 주의할 점들에 대해 정리하고 마치려고 합니당!

 

** Operator ** 

- Map - 

Map은 특정 타입으로 내려온 값을 다른 타입으로 변형할 때 사용합니다.

더보기

 

func test_map(){
        let publishSubject = PassthroughSubject<[Int], Never>()

        publishSubject.flatMap { elementList -> AnyPublisher<Int,Never> in
            return elementList.publisher.eraseToAnyPublisher()
        }
        .map { element -> String in
            return "Hello \(element)"
        }
        .sink { result in
            print("\(result) World")
        }
        .store(in: &bag)

        publishSubject.send([1,2,2,3,3,4,5])
    }

 

- tryMap -

tryMap은 Map과 같습니다. 하지만 다른 점은 throw가 되어 있어서 Error Catch가 가능합니다.

더보기
func test_tryMap(){
        let publishSubject = PassthroughSubject<Int, CombineError>()

        publishSubject
            .tryMap({ number -> String in
                if number % 2 == 0 {
                    throw CombineError.fail
                }

                return "Hello \(number)"
            })
            .sink { comp in
                print(comp)
            } receiveValue: { result in
                print(result)
            }
            .store(in: &bag)

        publishSubject.send(3)
    }

 

- Scan - 

Scan은 초기값에 input으로 내려온 값을 원하는 연산을 한 후 방출합니다.

그리고 다음 Input이 내려오면 기존에 변경된 초기값에 연산을 처리해서 다시 방출합니다.

더보기
func test_scan(){
        let publishSubject = PassthroughSubject<Int, CombineError>()

        publishSubject
            .scan(2, { inital , input in
                return inital + input
            })
            .sink { comp in
                print(comp)
            } receiveValue: { result in
                print(result)
            }
            .store(in: &bag)

        publishSubject.send(3)
        publishSubject.send(10)
    }
    
//결과 
5
15
func test_tryScan(){
        let publishSubject = PassthroughSubject<Int, CombineError>()

        publishSubject
            .tryScan(2, { inital, input in
                if (inital + input) % 15 == 0 {
                    throw CombineError.fail
                }
                return inital + input
            })
            .sink { comp in
                print(comp)
            } receiveValue: { result in
                print(result)
            }
            .store(in: &bag)

        publishSubject.send(3)
        publishSubject.send(10)
    }

 

- Filter - 

Filter는 말 그대로 BOOL 값 상태로 해당 값을 분류해줍니다.

Input으로 내려온 값이 연산처리에서 True라면 DownStream으로 내려주고 그게 아니면 걸러 줍니다.

더보기
func test_tryFilter(){
        let publishSubject = PassthroughSubject<Int, CombineError>()

        publishSubject
            .tryFilter({ number in
                if number % 2 == 0 {
                    throw CombineError.fail
                }
                return true
            })
            .sink { comp in
                print(comp)
            } receiveValue: { result in
                print(result)
            }
            .store(in: &bag)

        publishSubject.send(3)
        publishSubject.send(10)
    }

 

- compactMap - 

CompactMap은 원래 알던 CompactMap 처럼 Nil 값에 대해서는 필터링 해서 반환 해줍니다.

더보기
func test_compactMap(){
        ["1","2","3",nil,"5"].publisher
            .compactMap { number -> Int? in
                return Int(number ?? "")
            }
            .sink { result in
                print(result)
            }
            .store(in: &bag)
    }
    
//결과
1
2
3
5

 

- removeDuplicate - 

Input으로 내려온 값이 이전에 이미 DownStream으로 내려간 Input과 동일하다면 걸러줍니다.

더보기
func test_removeDuplicate(){
        ["1","2","3","3","3"].publisher
            .removeDuplicates()
            .sink { result in
                print(result)
            }
            .store(in: &bag)

        ["4","4","4","5","6"].publisher
            .removeDuplicates(by: { before, after in
                return before == after
            })
            .sink { result in
                print(result)
            }
            .store(in: &bag)
    }
    
//결과 
1
2
3
4
5
6

 

- replaceError - 

ReplaceError는 에러가 발생했을 때 receiveCompletion 뿐만 아니라 receiveValue에도 값을 Return 해줍니다.

receiveCompletion -> Fail

receiveValue - > 값 

더보기
func test_replaceError(){
        let publishSubject = PassthroughSubject<Int, CombineError>()

        publishSubject
            .tryMap({ number -> String in
                if number % 2 == 0 {
                    throw CombineError.fail
                }

                return "Hello \(number)"
            })
            .replaceError(with: "Error Find")
            .sink { comp in
                print(comp)
            } receiveValue: { result in
                print(result)
            }
            .store(in: &bag)

        publishSubject.send(2)

    }
    
//결과
Error Find
finished

 

- Collect - 

Complete 되는 시점에 Input 값들을 모아하 한번에 반출해준다.

더보기
func test_Collect(){

        let publisher = PassthroughSubject<Int,Never>()

        publisher.collect()
            .sink(receiveValue: { item in
                print(item)
            }).store(in: &bag)

        publisher.send(10)
        publisher.send(10)
        publisher.send(10)
        publisher.send(10)

        publisher.send(completion: .finished) //마지막에 Complete 되는 시점에 반환

    }

 

- CollectByCount - 

Complete 되는 시점에 Input 값들을 모아서 한번에 다 하는 것이 아니라 Count 만큼 방출한다.

더보기
func test_CollectByCount(){
        let publisher = PassthroughSubject<Int,Never>()

        publisher.collect(3) //3개씩 나눠서 방출합니다.
            .sink(receiveValue: { item in
                print(item)
            }).store(in: &bag)

        publisher.send(10)
        publisher.send(10)
        publisher.send(10)
        publisher.send(10)

        publisher.send(completion: .finished) //마지막에 Complete 되는 시점에 반환
    }

 

- CollectByTime -

위 Complete 시점에 방출하는 Collect와 달리 시간에 따라 방출한다.

따라서 .byTime의 N 시간 마다 모아서 방출하게 된다.

** 여기서 자동으로 Connect 시켜주는 Timer Publiser를 사용해줘야한다. 따라서 autoConnect 메소드를 사용

더보기
func test_CollectByTime(){

        Timer.publish(every: 0.5, on: .main, in: .default)
            .autoconnect() //ConnectablePublisher는 Connect 메소드가 필요하다. 그때 autoconnect가 그 역할을 자동으로 해준다.
            .map({ _ -> Int in
                return Int.random(in: 0...100)
            })
            .collect(.byTime(DispatchQueue.main, .seconds(1))) //1초마다 방출 한다.
            .sink(receiveValue: { item in
                print(item)
            }).store(in: &bag)

    }

 

- CollectByTimeOrCount -

특정 시간 이전에 Count 갯수가 모이면 방출한다.

그리고 특정 시간에도 방출한다. 그러니 특정 시간에 도달했을 때는 3개 보다 작은 1 ~ 2개가 방출 될 수 있다.

더보기
func test_CollectByTimeOrCount(){

        Timer.publish(every: 0.5, on: .main, in: .default)
            .autoconnect() //ConnectablePublisher는 Connect 메소드가 필요하다. 그때 autoconnect가 그 역할을 자동으로 해준다.
            .map({ _ -> Int in
                return Int.random(in: 0...100)
            })
            .collect(.byTimeOrCount(DispatchQueue.main, .seconds(4), 3)) //4초 이전에 3개가 모이면 방출한다.
            .sink(receiveValue: { item in
                print(item)
            }).store(in: &bag)

    }

 

- reduce - 

들어 오는 Input 값을 reduce 연산에 맞게 처리하였다가 Complete 시점에 방출 

더보기
func test_reduce() {
        let publisher = PassthroughSubject<Int,Never>()

        publisher.reduce(0, {$0 + $1}) //초기값에서 새로운 값을 다 더해서 Complete 시점에 반환
            .sink(receiveValue: { item in
                print(item)
            }).store(in: &bag)

        publisher.send(10)
        publisher.send(20)
        publisher.send(30)
        publisher.send(40)

        publisher.send(completion: .finished) //마지막에 Complete 되는 시점에 반환
    }

 

func test_tryreduce() {
        let publisher = PassthroughSubject<Int,CombineError>()

        publisher.tryReduce(0, { total , input in
            if input % 2 == 0 {
                throw CombineError.fail
            }
            return total + input
        })
        .sink(receiveCompletion: { competion in
            switch competion {

            case .finished:
                break
            case .failure(let error):
                print(error.localizedDescription)
            }

        }, receiveValue: { total in
            print(total)
        }).store(in: &bag)

        publisher.send(10)
        publisher.send(20)
        publisher.send(30)
        publisher.send(40)

        publisher.send(completion: .finished) //마지막에 Complete 되는 시점에 반환
    }

 

** Sink를 Cancelable없이 사용하기 ** 

그런데 어느 순간 저의 호기심을 자극하는 코드를 봤습니다. 

Sink와 Subscribe 였습니다.

Rx에서 Subscribe 역할을 하는 것이 Sink 였는데, Combine에서 Subscribe가 따로 있는 겁니다.

Combine의 Subscribe는 Rx의 Bind와 비슷한 역할을 하는 걸로 생각했습니다. 

또한 단일 결과만 반납하고 Cancel 되는 기능도 구현이 가능하단 생각을 했습니다.

Subscribe 내부에 Subscriber를 구현하면 Cancleable에 전달할 필요가 없다는 장점이 있습니다.

 class SampleSubscriber : Subscriber {

        typealias Input = String
        typealias Failure = Never

        func receive(subscription: Subscription) {
            subscription.request(.unlimited) //무한으로 받을 수 있도록 설정
        }

        func receive(_ input: String) -> Subscribers.Demand {
            print("Get Data from Subscription", input) //결과값이 여기에 찍힘
            return .none
        }

        func receive(completion: Subscribers.Completion<Never>) {
            print("Subscriber End") // Complete 결과가 여기에 나옴
            print(completion)
        }

}
 
 func test_SingInSubscriber(){
        let publisher = PassthroughSubject<Int,Never>()

        let publisherOutput = PassthroughSubject<String,Never>()

        publisherOutput.sink(receiveValue: {print($0)}).store(in: &bag) 
        //결과값이 PublisherOutput Subejct에 전달되고 Sink로 결과값이 나옴

        publisher
            .map({String($0) + " to publisherOutput \n"})
            .subscribe(publisherOutput).store(in: &bag) //output Subject에 Bind을 걸어놈 ^

        publisher.map({String($0) + " to SampleSubscriber \n"})
            .subscribe(SampleSubscriber()) //Custom Subscribe에 결과 값이 전달

        publisher.map({String($0) + " to SampleSubscriber \n"})
            .subscribe(Subscribers.Sink(receiveCompletion: {print($0)}, receiveValue: {print($0)}))
            //Subscriber.Sink 객체를 subscribe 내부에 구현하여 Cancel시 바로 죽을 수 있도록 구현

        publisher.send(1)
        publisher.send(1)
        publisher.send(1)
        publisher.send(completion: .finished)
        publisher.send(2)

}

//결과 
1 to SampleSubscriber 

1 to publisherOutput 

Get Data from Subscription 1 to SampleSubscriber 

1 to SampleSubscriber 

1 to publisherOutput 

Get Data from Subscription 1 to SampleSubscriber 

1 to SampleSubscriber 

1 to publisherOutput 

Get Data from Subscription 1 to SampleSubscriber 

finished
Subscriber End
finished

 

** Assing(to) vs Assing(to,on) ** 

Assing은 일단 Sink와 같이 구독 기능을 가지고 있지만 실패 없는 구독 입니다. 

to 는 ios 14 이상 부터 지원이 가능한데, Published Property가 죽을 때 자동으로 Deinit 되는 것이고 

Assing(to, on)은 AnyCancelable이 죽을 대 Deinit 되는 것 입니다.

@Published var name : String = "" {
    didSet{
        print(self.name) //Assing(to,on)을 사용할 때만 호출 됨
    }
}

var studentName : String = "" {
    didSet{
        print(studentName)
    }
}
    
func test_Assign(){
        let publisher = PassthroughSubject<Int,Never>()
        let publisher2 = PassthroughSubject<Int,Never>()
        
        [1,2,3].publisher.map({String($0)})
            .assign(to: &self.$name)

        publisher.map({String($0)})
            .assign(to: &self.$name)
            //name이라는 PropertyWrapper 객체인 Published를 전달.

        publisher2.map({String($0) + "Assign origin "})
            .assign(to: \.name, on: self).store(in: &bag)
            //on에 들어가는 Object에서 Property가 name인 것을 찾는 것
            
        publisher2.map({String($0) + "Assign StudentName "})
        .assign(to: \.studentName, on: self).store(in: &bag)
        //PropertyWrapper 객체가 아닌 일단 Property에서도 가능함

        DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
            publisher.send(4)
            print(self.name)
        })

        DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
            publisher2.send(5)
        })

    }
    
//결과 
4
5Assign origin 
5Assign origin

 

** 참고 사이트  ** 

Sink vs Subscribe 

https://stackoverflow.com/questions/61216852/whats-the-difference-between-sink-and-subscribers-sink

 

What's the difference between .sink and Subscribers.Sink?

I want to do an asynchronous job with Future. But the below .sink() closures never get called. It seems that the instance of Future was released right after it was called. Future<Int, Never...

stackoverflow.com

 

Sink  vs Assign

https://sujinnaljin.medium.com/combine-sink-assign-3dc04b7b326f

 

[Combine] sink & assign

편하게 구독해보자✨

sujinnaljin.medium.com

https://developer.apple.com/documentation/combine/fail/assign(to:) 

 

Apple Developer Documentation

 

developer.apple.com

KeyPath

https://docs.swift.org/swift-book/ReferenceManual/Expressions.html#//apple_ref/doc/uid/TP40014097-CH32-ID563

 

Expressions — The Swift Programming Language (Swift 5.7)

Expressions In Swift, there are four kinds of expressions: prefix expressions, infix expressions, primary expressions, and postfix expressions. Evaluating an expression returns a value, causes a side effect, or both. Prefix and infix expressions let you ap

docs.swift.org

 

autoConnect

https://zeddios.tistory.com/1009

 

Combine ) ConnectablePublisher

안녕하세요 :) Zedd입니다. Combine도 계속 공부해야하는데..!!!! @_@ ConnectablePublisher는 그냥 눈에 띄길래...공부해보려고 합니다. ConnectablePublisher ConnectablePublisher은 프로토콜입니다! 아오 프로토콜 개

zeddios.tistory.com

 

Combine 공부 리스트 

https://icksw.tistory.com/category/iOS/Combine?page=1 

 

'iOS/Combine' 카테고리의 글 목록

iOS 개발자가 되기 위해 iOS 공부, Swift 공부, CS 공부를 하고 있습니다.

icksw.tistory.com

 

Combine Ext Lib

https://github.com/CombineCommunity/CombineExt

 

GitHub - CombineCommunity/CombineExt: CombineExt provides a collection of operators, publishers and utilities for Combine, that

CombineExt provides a collection of operators, publishers and utilities for Combine, that are not provided by Apple themselves, but are common in other Reactive Frameworks and standards. - GitHub -...

github.com

 

728x90
반응형

댓글