[SwiftUI] Combine Publishers & Subscribers

Junyoung Park·2022년 8월 24일
0

SwiftUI

목록 보기
47/136
post-thumbnail

Advanced Combine Publishers and Subscribers in SwiftUI | Advanced Learning #19

Combine Publishers & Subscribers

구현 목표

  • Combine 프레임워크를 적용할 때 사용 가능한 메소드 파악하기
  • Publish를 사용하는 다양한 방법 파악하기
  • 에러를 throw하고 catch하는 방법을 Combine 프레임워크에서 파악하기

구현 태스크

핵심 코드

소스 코드

import SwiftUI
import Combine

class AdvancedCombineDataService {
    // @Published var basicPublisher: String = "First Publish"
//    let currentValuePublisher = CurrentValueSubject<String, Error>("First Publish")
    let passThroughPublisher = PassthroughSubject<Int, Error>()
    // more memory efficient than CurrentValueSubject (all of values hold)
    let boolPublisher = PassthroughSubject<Bool, Error>()
    let intPublisher = PassthroughSubject<Int, Error>()
    
    init() {
        publishFakeData()
    }
    
    private func publishFakeData() {
        // Mock Fake API Request
        let items = [1, 1, 1, 1, 1, 2, 3, 5, 6, 7, 8, 9, 11, 10, 1, 1, 1]
        for index in items.indices {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.3) {
                self.passThroughPublisher.send(items[index])
                if index >= 4 && index < 8 {
                    self.boolPublisher.send(true)
                    self.intPublisher.send(999)
                } else {
                    self.boolPublisher.send(false)
                }
                if index == items.indices.last {
                    self.passThroughPublisher.send(completion: .finished)
                }
            }
        }
    }
}
  • 서버 리퀘스트를 통해 데이터를 받아오는 과정을 가데이터를 통해 구현한 데이터 서비스 클래스
  • 현재 Publisher@Publisher로도, CurrentValueSubject로도, PassthroughSubject로도 구현 가능
  • 초깃값을 가지고 있느냐의 문제, 계속 값을 가지고 있느냐에 따라 선택
class AdvancedCombineBootCampViewModel: ObservableObject {
    @Published var data: [String] = []
    @Published var dataBools: [Bool] = []
    @Published var error: String = ""
    let dataService: AdvancedCombineDataService
    let multicastSubject = PassthroughSubject<Int, Error>()
    var cancellables = Set<AnyCancellable>()
    init(dataService: AdvancedCombineDataService) {
        self.dataService = dataService
        addSubscriber()
    }
    
    private func addSubscriber() {
        // Sequence Operations
        //    .first()
        //    .first(where: {$0 > 4})
        //    .filter{$0 > 4}
//            .tryFirst(where: { int in
//                if int == 3 {
//                    throw URLError(.badServerResponse)
//                }
//                return int > 4
//            })
        //    .last()
        //  Need to know When to Finish this publisher -> sink "finished"
       //     .last(where: {$0 < 4})
//            .tryLast(where: { int in
//                if int == 13 {
//                    throw URLError(.badServerResponse)
//                    // if no error at all, can sink
//                }
//                return int > 1
//                // int = 1, 2 -> Success but not checked
//            })
        //    .dropFirst()
        // drop first published item
        // if currentValuePublisher - default value and does not want to show that value, use dropFirst()
        //    .dropFirst(3)
        // dropFirst three items from the first place
//            .drop(while: { $0 > 5})
//            .tryDrop(while: { int in
//                if int == 15 {
//                    throw URLError(.badServerResponse)
//                }
//                return int < 6
//            })
         //   .prefix(4)
        // first four items of the stream
//            .prefix(while: { $0 < 5})
        // publish finishes when fails ($0 > 5 -> fails at the first moment)
//            .tryPrefix(while: { int in
//                if int > 15 {
//                    throw URLError(.badServerResponse)
//                }
//                return int < 5
//            })
        //    .output(at: 1)
        // output of item indices
        //    .output(in: 2..<4)
        // output items between those range
            
        // Mathematic Operations
            // .max()
        // need to waif for publisher to finish
//            .max(by: { int1, int2 in
//                return int1 < int2
//            })
        // maximum -> 10
//            .tryMax(by: { int1, int2 in
//                return int1 > int2
//            })
//            .max()
        
        // Filter // Reducing Operations
//            .tryMap({ int -> String in
//                if int == 5 {
//                    throw URLError(.badServerResponse)
//                }
//                return String(int)
//            })
        //            .compactMap({ int -> String? in
        //                if int == 5 {
        //                    return nil
        //                }
        //                return "\(int)"
        //            })
        //            .tryCompactMap({ int -> String in
        //                if int == 5 {
        //                    throw URLError(.badServerResponse)
        //                }
        //                return "\(int)"
        //            })
//            .filter{$0 > 3 && $0 < 9}
//            .removeDuplicates(by: { int1, int2 in
//                return int1 == int2
//            })
//            .replaceNil(with: 100)
        // nil replaced by with value
//            .replaceEmpty(with: [])
//            .replaceError(with: "100")
//            .scan(0, { existingValue, newValue in
//                return existingValue + newValue
//            })
//            .scan(0, {$0 + $1} )
//            .scan(0, +)
//            .reduce(0, +)
        // One Element returned by reduce
//            .allSatisfy({$0 < 200})
        // Bool return: true / false
//            .tryAllSatisfy({ int in
//                if int < 4 {
//                    return true
//                } else {
//                    throw URLError(.badServerResponse)
//                }
//            })
            
        // Timing Operations
        /*
//            .debounce(for: 0.75, scheduler: DispatchQueue.main)
            // at least 1 second between each publishers
//            .delay(for: 2, scheduler: DispatchQueue.main)
        // arrived late for 2 seconds in the pipeline
//            .measureInterval(using: DispatchQueue.main)
//            .map({ stride in
//                return "\(stride.timeInterval)"
//            })
//            .throttle(for: 5, scheduler: DispatchQueue.main, latest: true)
        // bottle neck effect
//            .retry(3)
        // try but redonwload # times after error occurs.
//            .timeout(0.2, scheduler: DispatchQueue.main)
        */
        
        // Multiple Publishers / Subscribers
/*
//            .combineLatest(dataService.boolPublisher, dataService.intPublisher)
//            .map{String($0)}
//            .compactMap({ (int, bool) -> String? in
//                if bool {
//                    return String(int)
//                } else {
//                    return nil
//                }
//            })
//            .compactMap{ $1 ? String($0) : "n/a"}
//            .compactMap({ (int1, bool, int2) -> String in
//                // at least all of three publisher items must be arrived
//                if bool {
//                    return String(int1)
//                } else {
//                    return "n/a"
//                }
//            })
//            .merge(with: dataService.intPublisher)
//            .zip(dataService.boolPublisher, dataService.intPublisher)
//            .map { tuple in
//                // at least three publishers must arrive before using tuple
//                return String(tuple.0) + tuple.1.description + String(tuple.2)
//            }
//            .tryMap({ int in
//                if int == 5 {
//                    throw URLError(.badServerResponse)
//                }
//                return int
//            })
//            .catch({ error in
//                return self.dataService.intPublisher
//            })
//            .removeDuplicates()
        // Multiple Publishers make multiple items */
        
        let sharedPublisher = dataService.passThroughPublisher
            .share()
//            .multicast {
//                PassthroughSubject<Int, Error>()
//                // Make this publisher as "Auto Connected" Publisher
//            }
            .multicast(subject: multicastSubject)
        
        sharedPublisher
//        dataService.passThroughPublisher
            .map{String($0)}
            .sink { completion in
                switch completion {
                case .finished:
                    print("SUCCESS")
                    break
                case .failure(let error):
                    print("ERROR: \(error.localizedDescription)")
                    self.error = error.localizedDescription
                    break
                }
            } receiveValue: { [weak self] returnedData in
                guard let self = self else { return }
                self.data.append(returnedData)
//                self.data = returnedData
            }
            .store(in: &cancellables)
  
        sharedPublisher
//        dataService.passThroughPublisher
            .map{$0 > 5 ? true : false}
            .sink { completion in
                switch completion {
                case .finished: break
                case .failure(let error): break
                }
            } receiveValue: { [weak self] returnedData in
                guard let self = self else { return }
                self.dataBools.append(returnedData)
            }
            .store(in: &cancellables)
        // Subscribe multiple times at One Publisher
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            sharedPublisher
                .connect()
                .store(in: &self.cancellables)
            // delay and connect publisher's collection
        }
    }
}
  • 시퀀스 처리, 타이밍 처리, 수학 연산 처리, 필터링 처리, 여러 개의 Publisher를 연결하여 하는 처리 등 다양한 종류의 처리 메소드
  • dropFirst()를 통해 구독한 곳에서 들어오는 처음 데이터를 받아들이지 않을 수 있음
  • removeDuplicate를 통해 구독한 곳에서 연속된 데이터가 중복일 때 제거 가능
  • try~ 메소드를 통해 에러를 throw할 수 있음 → sinkcompletion 핸들러가 해당 에러를 처리
  • 여러 개의 Publisher를 동시에 구독하거나, 한 개의 Publisher를 여러 군데에서 구독할 수 있음
  • sharedPublisher, 즉 여러 군데에서 구독하는 하나의 공통된 Publisher를 설정할 수도 있음 → share(), multicast() 등도 적용 가능
  • 구독한 데이터에서 받아오는 타이밍 이슈, 에러 이슈 등이 CombineSubscriber가 주의해야 할 가장 중요한 부분
struct AdvancedCombineBootCamp: View {
    @StateObject private var viewModel: AdvancedCombineBootCampViewModel
    
    init(dataService: AdvancedCombineDataService) {
        _viewModel = StateObject(wrappedValue: AdvancedCombineBootCampViewModel(dataService: dataService))
    }
    var body: some View {
        ScrollView {
            VStack {
                ForEach(viewModel.data, id:\.self) { data in
                    Text(data)
                        .font(.largeTitle)
                        .fontWeight(.semibold)
                        .foregroundColor(.pink)
                }
                
                if !viewModel.error.isEmpty {
                    Text(viewModel.error)
                }
            }
            .padding()
        }
    }
}
  • 가데이터를 받아오는 데이터 서비스를 구독하고 있는 뷰 모델과 연결된 UI 담당 뷰
  • 데이터 의존성을 줄이기 위한 의존성 주입을 통해 구현

구현 화면

profile
JUST DO IT

0개의 댓글