[SwiftUI] AsyncPublisher

Junyoung Park·2022년 8월 29일
0

SwiftUI

목록 보기
65/136
post-thumbnail

How to use AsyncPublisher to convert @Published to Async / Await | Swift Concurrency #12

AsyncPublisher

AsyncPublisher의 정의

  • 비동기적으로 데이터를 리턴받는 곳에서 해당 퍼블리셔에 구독을 추가하지 않고도 value 프로퍼티를 관찰하는 방법

AsyncPublisher의 사용 방법

  • for in 구문을 통해 해당 퍼블리셔가 리턴하는 데이터 타입의 특정 값을 관찰 → 해당 값을 바인딩

핵심 코드

    private func addSubscriber2() {
        Task {
            for await value in dataService.$dataArray.values {
                // Executed Async
                await MainActor.run(body: {
                    self.dataArray = value
                })
            }
        }
    }
  • 기존에 데이터 서비스 클래스인 dataService$dataArray를 구독하던 것과 달리 $dataArray의 값을 직접 관찰 가능 → Combine 프레임워크의 sink 등 구독 추가 방법과는 또 다른 방법으로 특정 값의 변화를 감지 가능

소스 코드

import SwiftUI
import Combine

class AsyncPublisherBootCampDataService {
    @Published var dataArray: [String] = []
    
    func getData() async {
        dataArray.append("MockData1")
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        dataArray.append("MockData2")
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        dataArray.append("MockData3")
        try? await Task.sleep(nanoseconds: 2_000_000_000)
        dataArray.append("MockData4")
    }
}
  • 데이터 서비스 클래스의 데이터 패치가 비동기적으로 이루어질 때 (2초 간격으로 데이터 추가) 해당 퍼블리셔를 관찰할 수 있도록 뷰 모델 단에서 구독을 추가해야 함
class AsyncPublisherBootCampViewModel: ObservableObject {
    @MainActor @Published var dataArray: [String] = []
    let dataService = AsyncPublisherBootCampDataService()
    var cancellables = Set<AnyCancellable>()
    
    init() {
        addSubscriber3()
    }
    
    private func addSubsriber() {
        dataService.$dataArray
            .receive(on: DispatchQueue.main)
            .sink { [weak self] dataArray in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    self.dataArray = dataArray
                }
            }
            .store(in: &cancellables)
    }
    
    private func addSubscriber2() {
        Task {
            for await value in dataService.$dataArray.values {
                // Executed Async
                await MainActor.run(body: {
                    self.dataArray = value
                })
            }
        }
    }
    
    private func addSubscriber3() {
        Task {
            await MainActor.run(body: {
                self.dataArray = ["One"]
            })
            for await value in dataService.$dataArray.values {
                // Executed Async
                await MainActor.run(body: {
                    self.dataArray = value
                })
                break
                // using break -> escape those for-ever waiting loop
            }
            await MainActor.run(body: {
                self.dataArray = ["Two"]
            })
        }
    }
    
    func getData() async {
        await dataService.getData()
    }
}
  • 기존의 Combine 프레임워크의 구독 추가 방식을 사용하고 있는 addSubscriber 함수
  • values를 통해 AsyncPublisher 사용하고 있는 addSubscriber2 함수
  • 여러 개의 데이터 서비스를 동시에 구독해야 한다면 for await value in... 문을 브레이크를 통해 탈출해야 함 → 해당 구독자는 해당 퍼블리셔가 언제 끝나는지 알 수 없기 때문에 별도의 추가적인 탈출 방법이 필요
struct AsyncPublisherBootCamp: View {
    @StateObject private var viewModel = AsyncPublisherBootCampViewModel()
    var body: some View {
        ScrollView {
            VStack {
                ForEach(viewModel.dataArray, id:\.self) { data in
                    // Executed immediately
                    Text(data)
                        .font(.headline)
                        .fontWeight(.semibold)
                }
            }
        }
        .task {
            await viewModel.getData()
        }
    }
}
  • viewModelgetData 함수를 통해 데이터 서비스 클래스의 데이터 패치가 비동기적으로 실행, 해당 퍼블리셔의 데이터 추가에 따라 (기존 뷰 모델이 이니셜라이즈될 때 해당 데이터 서비스를 구독하고 있기 때문에) 데이터 패치에 따른 뷰 모델의 관찰이 실시간 연동 가능
profile
JUST DO IT

0개의 댓글