ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [iOS/Swift] Combine 알아보기 vii
    야매 iOS 2023. 3. 26. 17:10

    비동기 프로세스를 Combine의 Future로 감싸기

    지금까지 모든 예시들은 Combine의 내장된 publishers, operators를 사용했다.

    하지만 현실에서는 커스텀한 비동기 연산들을 구현해 사용한다.

    다행히도 Combine은 커스텀 publishers를 생성할 수 있도록 CurrentValueSubject와 PassthroughSubject 등 Subject 객체를 지원한다.

    Subject뿐만 아니라 Future를 사용해서 커스텀한 비동기 연산을 구현할 수 있다.

    Future

    Future는 Combine의 특별한 종류의 publisher로 task를 진행하고 task를 끝내면 promise를 사용해 subscriber에게 종료됐음을 알린다.

    • 일반적인 Publisher와의 동작이 비슷해 보이지만 다르다.

    Future는 어떻게 동작하는가?

    Future의 이벤트 방출

    Combine에서 Future는 하나의 값 만 방출하고 바로 종료한다.

    • 구현으로 강제되어 있다.

    Future는 publisher이기 때문에 AnyPublisher로 변환해 사용자들에게 Future라는 사실을 감출 수 있다.

    Future 구현

    var cancellables = Set<AnyCancellable>()
    
    func createFuture() -> Future<Int, Never> {
        return Future { promise in
            promise(.success(Int.random(in: 0..<Int.max)))
        }
    }
    
    createFuture()
        .sink(receiveCompletion: {
            print($0)
        }, receiveValue: {
            print("value received \\($0)")
        })
        .store(in: &cancellables)
    

    위 코드를 보면 createFuture 함수의 반환값과 Future 클로저 내부의 타입이 일치해야 한다는 사실을 알 수 있다.

    • Future의 타입과 Future 클로저 내부의 타입이 동일해야 함

    모든 작업이 끝나면 개발자가 Promise 클로저를 호출해야 한다.

    • Promise 클로저를 호출해야 Future가 결과를 방출한다.

    Future와 Publisher의 차이점

    var cancellables = Set()
    
    func fetchURL(_ url: URL) -> Future<(data: Data, response: URLResponse), URLError> {
        return Future { promise in
            URLSession.shared.dataTask(with: url) { data, response, error in
                if let error = error as? URLError {
                    promise(.failure(error))
                }
    
                if let data, let response {
                    promise(.success((data: data, response: response)))
                }
    
            }
            .resume()
        }
    }
    
    let publisher = fetchURL(URL(string: "<https://google.com>")!)
    
    publisher
        .sink(
            receiveCompletion: {
                print($0)
            },
            receiveValue: {
                print($0)
            }
        )
        .store(in: &cancellables)
    
    publisher
        .sink(
            receiveCompletion: {
                print($0)
            },
            receiveValue: {
                print($0)
            }
        )
        .store(in: &cancellables)
    

    위 코드는 네트워킹을 진행하는 Future

    이전 챕터에서 publisher에 두번 이상 구독하면 네트워크 요청이 두 번 발생하는 케이스를 확인했었다.

    • 문제를 해결하기 위해 share 연산자 사용

    이는 왜냐하면 publishers는 구독자가 있어야 값을 방출하기 때문

    Subscriber와 연산

    Future는 구독자가 없어도 연산을 실행한다.

    import PlaygroundSupport
    
    PlaygroundPage.current.needsIndefiniteExecution = true
    let myURL = URL(string: "<https://practicalcombine.com>")!
    
    func fetchURL(_ url: URL) -> Future<(data: Data, response: URLResponse), URLError> {
        return Future { promise in
            URLSession.shared.dataTask(with: url) { data, response, error in
                if let error = error as? URLError {
                    promise(.failure(error))
                }
                if let data, let response {
                    promise(.success((data: data, response: response)))
                }
    
                print("RECEIVED RESPONSE")
            }
            .resume()
        }
    }
    
    let publisher = fetchURL(myURL)
    
    /* Output
    RECEIVED RESPONSE
    */
    

    위 코드를 보면 publisher에 대해 subscriber가 없지만 Future 내부에서 모든 동작이 끝난 후 출력되는 문자열이 출력되는 것을 확인할 수 있다.

    Subscriber와 실행

    Future는 subscriber가 2개 이상이어도 오직 한 번만 실행한다.

    • Future가 종료되면 새로운 Future가 생성될 때까지 이전에 얻어온 값을 새로운 subscriber에게 전달한다.
    publisher.sink(
        receiveCompletion: {
            print($0)
        }, receiveValue: {
            print($0.data)
        })
    .store(in: &cancellables)
    
    publisher.sink(
        receiveCompletion: {
            print($0)
        }, receiveValue: {
            print($0.data)
        })
    .store(in: &cancellables)
    
    /*
    14692 bytes
    finished
    14692 bytes
    finished
    RECEIVED RESPONSE
    */
    

    Future는 Publisher와 유사한 것 같지만 Future는 subscriber가 없어도 바로 실행한다.

    subscriber가 많아도 한번만 실행하고 그 값을 전달한다.

    Future를 Publisher처럼 사용하기

    Future는 한번만 실행하고 생성된 직후 바로 실행하기 때문에 조심하지 않으면 예상과 다른 결과물을 얻을 수 있다.

    Future에 eraseToAnyPublisher()를 호출하면 AnyPublisher가 된다.

    • 생성한 Future의 사용자들은 이것이 Future인지 정확하게 알지 못한다.

    Deferred publisher를 사용해 subscriber가 생기기 전까지 실행하지 않도록 Future를 강제할 수 있다.

    Deferred Publisher

    func createfuture() -> Future<Int, Never> {
        return Future { promise in
            promise(.success(Int.random(in: (1...1000))))
            print("promise Finished")
        }
    }
    
    func createDeferredFuture() -> Deferred<Future<Int, Never>> {
        return Deferred {
            return Future { promise in
                promise(.success(Int.random(in: (1...1000))))
                print("promise Finished in Deferred")
            }
        }
    }
    

    타입이 Deferred<Future<Type, Error>>라면

    • subscriber가 생기기 전까지 publisher는 실행되지 않는다.
    • 같은 publisher 인스턴스에 2번 이상 구독하면, 작업은 반복된다.
    let plainFuture = createfuture()
    
    plainFuture.sink(receiveValue: {
        print("plain1", $0)
    })
    
    plainFuture.sink(receiveValue: {
        print("plain2", $0)
    })
    
    let deferred = createDeferredFuture()
    
    deferred.sink(receiveValue: {
        print("deferred1", $0)
    })
    
    deferred.sink(receiveValue: {
        print("deferred2", $0)
    })
    
    /*
    promise Finished
    plain1 823
    plain2 823
    promise Finished in Deferred
    deferred1 211
    promise Finished in Deferred
    deferred2 62
    */
    

    Future는 생성되자마자 바로 실행하고 실행한 결과를 subscriber에게 전달하기 때문에 plain1과 plain2는 같은 값을 갖는다.

    Deferred publisher는 subscribe될 때마다 받은 closure를 실행한다.

    • 새로운 Future를 생성한다.

    Deferred를 사용해 Future를 wrapping하면 다른 publisher처럼 사용할 수 있다.

    Deferred Future를 사용한다면 publisher 타입을 AnyPublisher로 변경하는 것이 낫다.

    • 사용자들에게 일반적인 Publisher와 동일하게 동작할 것이라는 인식
    • 구현부 detail을 숨길 수 있기 때문에 opeator의 변경, 수정, 삭제가 있어도 같은 반환값을 사용할 수 있다.

    Combine을 사용해서 푸시 Permission 얻기

    Future는 한 번만 실행하고 추가적인 업데이트가 필요없는 비동기 작업을 wrapping 하기 좋은 방법이다.

    Combine을 사용하지 않고 유저 푸시 노티피케이션 permission을 확인하고 그것을 바탕으로 어떤 동작을 해야 한다면 코드는 아래와 같다.

    UNUserNotificationCenter.current().getNotificationSettings { settings in
        switch settings.authorizationStatus {
        case .denied:
            DispatchQueue.main.async {
                // update UI to point user to settings
            }
        case .notDetermined:
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { result, error in
                if result == true && error == nil {
                    // notification permission granted
                } else {
                    DispatchQueue.main.async {
                        // do not have permssions
                    }
                }
            }
        default:
            break
        }
    
    }
    

    위 코드는 읽기 매우 복잡하다.

    위 코드는 두 단게로 나뉜다.

    1. 유저의 현재 notification setting을 받아와 closure 실행 (비동기 작업)
    2. 유저의 동의를 받지 않은 상태라면 유저에게 permission을 받기 위해 요청하고 그 결과를 받아와 closure 실행 (비동기 작업)

    이 두 비동기 처리를 Future로 구현한다.

    extension UNUserNotificationCenter {
        func getNotificationSettings() -> Future<UNNotificationSettings, Never> {
            return Future { promise in
                self.getNotificationSettings { settings in
                    promise(.success(settings))
                }
            }
        }
    
        func requestAuthorization(options: UNAuthorizationOptions) -> Future<Bool, Error> {
            return Future { promise in
                self.requestAuthorization(options: options) { result, error in
                    if let error {
                        promise(.failure(error))
                    } else {
                        promise(.success(result))
                    }
    
                }
    
            }
        }
    }
    

    위 코드를 실제로 적용한 코드는 아래와 같다.

    var cancellables = Set<AnyCancellable>()
    
    UNUserNotificationCenter.current().getNotificationSettings()
        .flatMap({ settings -> AnyPublisher<Bool, Never> in
            switch settings.authorizationStatus {
            case .notDetermined:
                return UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])
                    .replaceError(with: false)
                    .eraseToAnyPublisher()
            case .denied:
                return Just(false).eraseToAnyPublisher()
            default:
                return Just(true).eraseToAnyPublisher()
            }
        })
        .receive(on: DispatchQueue.main)
        .sink(receiveValue: { hasPermissions in
            if hasPermissions == false {
                // user setting
            } else {
                // permission granted
            }
    
        })
        .store(in: &cancellables)
    

     

    '야매 iOS' 카테고리의 다른 글

    [iOS/Swift] Combine 알아보기 ix  (0) 2023.03.26
    [iOS/Swift] Combine 알아보기 viii  (0) 2023.03.26
    [iOS/Swift] Combine 알아보기 vi  (0) 2023.03.26
    [iOS/Swift] Combine 알아보기 v  (0) 2023.03.05
    [iOS/Swift] Combine 알아보기 iv  (0) 2023.02.27
Designed by Tistory.