[UIKit] Combine: Publisher

Junyoung Park·2022년 10월 2일
0

UIKit

목록 보기
47/142
post-thumbnail

Combine Framework FREE course: write you first iOS app - use Subscriptions & Publishers like Subject

Combine: Publisher

Publisher

  • CurrentValueSubject: 초깃값이 존재하는 퍼블리셔
  • PassthroughSubject: 초깃값이 없는 퍼블리셔
  • @Published: 퍼블리시 프로토콜을 따른다는 의미
  • sink를 통해 해당 값의 변화를 스트리밍 가능
  • ObservableObject 등 프로토콜과 함께 사용할 때 해당 클래스 (즉 해당 프로토콜을 채택하고 내부에서 퍼블리셔를 통해 변화를 체크할 데이터를 가지고 있는 클래스) 자체의 변화 또한 전달 가능

소스 코드

struct UserModel {
    var id: Int
    var name: String
}

let currentUserId = CurrentValueSubject<Int, Never>(1000)

// get the value for currentUserId
print("currentUserId \(currentUserId.value)")

// subscribe to Subject
currentUserId
    .sink { value in
        print("Received value: \(value)")
    }

// passing down new values with Subject
currentUserId.send(999)
currentUserId.send(100)

// sending completion finished with Subject
currentUserId.send(completion: .finished)

// currentUserId.send(10) -> Finished data stream
  • 초깃값을 가지고 있는 CurrentValueSubjectsend 메소드를 통해 데이터 스트림을 시작, sink를 통해 마지막 단에서 데이터를 캐치
  • sendcompletion을 통해 해당 스트림 자체를 핸들링 가능
let newUserNameEntered = PassthroughSubject<String, Never>()

// get the value for newUserNameEntered

// does not hold a value newUserNameEntered.value

// subscribe to Subject
newUserNameEntered
    .sink { value in
        print("Received value: \(value)")
    }

// passing down new values with Subject
newUserNameEntered.send("Hello")
newUserNameEntered.send("World")
// sending completion finished with Subject
newUserNameEntered.send(completion: .finished)
  • 초깃값이 없는 PassthroughSubject
class ViewModel {
    private let userNamesSubject = CurrentValueSubject<[String], Never>(["Bill"])
    // Object with Read + Write -> Private
    var userNames: AnyPublisher<[String], Never>
    // Read Only
    let newUserNameEntered = PassthroughSubject<String, Never>()
    
    var cancellables = Set<AnyCancellable>()
    
    init() {
        // create publisher stream that updates whenever a newUserNameEntered has a new value
        userNames = userNamesSubject.eraseToAnyPublisher()
        
        newUserNameEntered
            .sink
        { [weak self] userName in
            guard let self = self else { return }
            self.userNamesSubject.send(self.userNamesSubject.value + [userName])
        }
        .store(in: &cancellables)
        
        userNames
            .sink { users in
            print("usernames changed to \(users)")
            }
            .store(in: &cancellables)
    }
}

let viewModel = ViewModel()

viewModel.userNames.sink { names in
    print(names)
}
// userNames -> Read Only

// add new User name
viewModel.newUserNameEntered.send("Junyeong")
viewModel.newUserNameEntered.send("Sujeong")

/*
 usernames changed to ["Bill"]
 ["Bill"]
 usernames changed to ["Bill", "Junyeong"]
 ["Bill", "Junyeong"]
 usernames changed to ["Bill", "Junyeong", "Sujeong"]
 ["Bill", "Junyeong", "Sujeong"]
 */
  • 뷰 모델 내부에서만 private으로 사용하는 퍼블리셔 및 외부에서 접근 가능한 읽기 전용 퍼블리셔 구별
  • newuserNameEntered 퍼블리셔는 외부에서 send를 통해 데이터를 인풋으로 줄 수 있음
  • userNamessend 불가능, sink만 가능한 단방향 스트림
class ViewModel {
    // use @Published to create Publisher
    
    @Published var userNames: [String] = ["Bill"] {
        willSet {
            print("\(newValue) has come!")
        }
    }
    // Read Only
    let newUserNameEntered = PassthroughSubject<String, Never>()
    var cancellables = Set<AnyCancellable>()
    
    init() {
        $userNames
            .sink { [weak self] names in
                guard let self = self else { return }
                print("Received value: \(names)")
            }
            .store(in: &cancellables)
        
        newUserNameEntered
            .sink { [weak self] name in
                guard let self = self else { return }
                self.userNames.append(name)
            }
            .store(in: &cancellables)
    }
}

let viewModel = ViewModel()
viewModel.newUserNameEntered.send("Susan")
// When the property changes, publishing occurs in the property's willSet block, meaning that subscribers receive the new value before it's actually set on the property

/*
 Received value: ["Bill"]
 ["Bill", "Susan"] has come!
 Received value: ["Bill", "Susan"]
 */
  • Published 프로토콜을 따르는 퍼블리셔
  • 프로퍼티 값이 바뀔 때 willSet 블럭 안에서 퍼블리싱이 발생
  • newUserNameEntered에 새로운 데이터 입력 → userNames 퍼블리셔의 willSet 블럭 동작 → userNames 퍼블리셔의 sink를 통해 데이터 스트림 말단부에서 해당 값을 sink로 받음
class ViewModel: ObservableObject {
    @Published var userNames = ["Bill", "Joe", "Jean"]
    let userNamesSubject = CurrentValueSubject<[String], Never>(["Bill", "Joe", "Jean"])
    var cancellables = Set<AnyCancellable>()
    
    init() {
        addSubscriber()
    }
    
    private func addSubscriber() {
        // subscribe to publisher
        $userNames
            .sink { [unowned self] names in
                print("Last: \(self.userNames) - Received: \(names)")
            }
            .store(in: &cancellables)
        userNamesSubject
            .sink { [unowned self] name in
                print("Last: \(self.userNamesSubject.value) - Received: \(name)")
                self.objectWillChange.send()
            }
            .store(in: &cancellables)
    }
}

let viewModel = ViewModel()
viewModel.objectWillChange.sink { _ in
    print("ObjectWillChange was sent")
}
viewModel.userNames.append("Moana")
viewModel.userNamesSubject.send(["Elsa"])

/*
 Last: ["Bill", "Joe", "Jean"] - Received: ["Bill", "Joe", "Jean"]
 Last: ["Bill", "Joe", "Jean"] - Received: ["Bill", "Joe", "Jean"]
 ObjectWillChange was sent
 Last: ["Bill", "Joe", "Jean"] - Received: ["Bill", "Joe", "Jean", "Moana"]
 Last: ["Elsa"] - Received: ["Elsa"]
 ObjectWillChange was sent
 */
  • SwiftUIObservableObject 프로토콜을 채택한 뷰 모델
  • Published된 값의 변화를 그대로 감지, objectWillChange 등 변화하기 이전에 특정한 블럭을 실행 가능
  • Published 프로토콜을 따르는 userNames 값이 변화할 때에는 자동으로 objectWillChange 감지 가능
  • CurrentValueSubject, 즉 Published 프로토콜이 아닌 다른 퍼블리셔의 새로운 값 변화를 감지해야 할 때 ObjectWillChange를 주기 위해서는 직접적으로 self.objectWillChange.send()sink 블럭 안에 작성해야 할 필요가 있음
profile
JUST DO IT

0개의 댓글