-
[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 } }
위 코드는 읽기 매우 복잡하다.
위 코드는 두 단게로 나뉜다.
- 유저의 현재 notification setting을 받아와 closure 실행 (비동기 작업)
- 유저의 동의를 받지 않은 상태라면 유저에게 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