Combine Framework FREE course: write you first iOS app - use Subscriptions & Publishers like Subject
sink
, assign
사용하는 게 주요 방법var subscription: Cancellable? = Timer.publish(every: 0.25, on: .main, in: .common)
.autoconnect()
.scan(0, { count, output in
return count + 1
})
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
.filter({ count in
return count < 20
})
.sink { completion in
print("Data Stream Completion: \(completion)")
} receiveValue: { output in
print("Received Value: \(output)")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
subscription = nil
}
scan
, throttle
, filter
등 '걸러 주는' 함수를 통과한 최종 데이터를 sink
를 통해 핸들링
let foodbank: Publishers.Sequence<[String], Never> = ["apple", "bread", "orange", "milk"].publisher
let foodSubscription = foodbank
.sink { foodItem in
print("Received Value: \(foodItem)")
}
var timer = Timer.publish(every: 0.5, on: .main, in: .common)
.autoconnect()
let calendar = Calendar.current
let endDate = calendar.date(byAdding: .second, value: 2, to: Date())
enum SubscriptionError: LocalizedError {
case TryMapLocalizedError
}
func throwAtEndDate(foodItem: String, date: Date) throws -> String {
guard
let endDate = endDate,
endDate >= date else {
throw SubscriptionError.TryMapLocalizedError
}
return "\(foodItem) at \(date)"
}
let timerSubscription = foodbank
.zip(timer)
.tryMap({ foodItem, timeStamp in
try throwAtEndDate(foodItem: foodItem, date: timeStamp)
})
.sink { completion in
switch completion {
case .finished: print("Success")
case .failure(let error):
print(error.localizedDescription)
}
} receiveValue: { result in
print(result)
}
zip
을 통해 타이머 퍼블리셔의 시간 주기와 함께 해당 퍼블리셔를 구독하기tryMap
등 에러를 throw
할 수 있는 방법을 sink
이전에 두고 특정 조건에 더 이상 구독하지 않도록 구현class MyClass {
var anInt: Int = 0 {
didSet {
print("anInt was set to: \(anInt)")
}
}
}
var myObject = MyClass()
let myRange = (0...2)
let subscription = myRange
.publisher
.map{$0 * 10}
.sink(receiveValue: { value in
myObject.anInt = value
})
sink
, assign(on: to:)
등 퍼블리셔 데이터를 핸들링하는 방법을 통해 특정 값을 변경할 수 있음myRange
의 데이터 시퀀스에 따라 anInt
값이 변화하는 것을 didSet
을 통해 추적 가능class TextFieldViewController: UIViewController {
private let label: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .title1)
label.textColor = .black
label.text = "Test Label"
label.numberOfLines = 0
return label
}()
private let textField: UITextField = {
let textField = UITextField()
textField.borderStyle = .roundedRect
return textField
}()
private var textMessage = CurrentValueSubject<String, Never>("Hello World!")
private var cancellables = Set<AnyCancellable>()
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
setUI()
}
private func setUI() {
label.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.addSubview(textField)
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 40).isActive = true
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30).isActive = true
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30).isActive = true
textField.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 30).isActive = true
textField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30).isActive = true
textField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30).isActive = true
textField.addTarget(self, action: #selector(updateText), for: .editingChanged)
textMessage
.compactMap{$0}
.map{"Typed: \($0)"}
.assign(to: \.text, on: label)
.store(in: &cancellables)
}
@objc private func updateText() {
textMessage.value = textField.text ?? ""
}
}
textMessage
는 문자열 타입의 퍼블리셔, 초깃값을 가지고 있기 때문에 CurrentValueSubject
로 구현testMessage
의 데이터를 구독하고 있는 label
의 텍스트assign
또는 sink
를 통해 구현 가능struct UserModel {
let name: String
let id: Int
}
class AssignViewModel {
var user = CurrentValueSubject<UserModel, Never>(UserModel(name: "Mock", id: 1))
var userId: Int = 1 {
didSet {
print("userId changed: \(userId)")
}
}
var cancellables = Set<AnyCancellable>()
init() {
// user.map{$0.id}
// .assign(to: \.userId, on: self)
// .store(in: &cancellables)
// user <- userId subscription as assigned
user.map{$0.id}
.sink { [weak self] value in
guard let self = self else { return }
self.userId = value
}
.store(in: &cancellables)
// user <- userId subscription using sink (user -> userId)
}
deinit {
print("AssignViewModel is deallocated")
}
}
var viewModel: AssignViewModel? = AssignViewModel()
viewModel?.user.send(UserModel(name: "New Data", id: 2))
// send: user (publisher model) has been changed via send event
viewModel = nil
send
를 통해 불러 일으킬 수 있음sink
부분은 약한 참조를 통해 강한 참조 사이클을 사전 방지 가능class MyModel: ObservableObject {
@Published var lastUpdated: Date = Date()
init() {
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.assign(to: &$lastUpdated)
}
}
struct ClockView: View {
@StateObject private var clockModel = MyModel()
var body: some View {
Text("\(clockModel.lastUpdated)")
.fixedSize()
.padding(50)
}
}
MyModel
이라는 ObservableObject
의 lastUpdated
가 Published
프로토콜을 따르고 있는 퍼블리셔 → ClockView
에서 UI를 그리는 직접적인 데이터Timer
퍼블리셔를 구독하고, 해당 퍼블리셔의 값 변화에 따라 값이 변하는 assign
적용let intSubject = PassthroughSubject<Int, Never>()
let subscription = intSubject
.map{$0}
.receive(on: DispatchQueue.main)
.sink { value in
print("Receive value \(value)")
print("Thread: \(Thread.current)")
}
intSubject.send(1)
DispatchQueue.global().async {
intSubject.send(2)
}
PassthroughSubject
를 통해 정수 형태 데이터를 담당하는 퍼블리셔receive
메소드를 통해 스레드 선택 가능var cancellables = Set<AnyCancellable>()
let intSubject = PassthroughSubject<Int, Never>()
intSubject
.subscribe(on: DispatchQueue.global())
.sink { value in
print("Value: \(value)")
print("Thread: \(Thread.current)")
}
.store(in: &cancellables)
for number in 1...10 {
intSubject.send(number)
print("Sending Even at: \(Thread.current)")
}
subscribe
메소드를 사용할 수도 있음let subscription = URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com")!)
.map { _ in
print("Thread is main at mapping ? : \(Thread.current.isMainThread)")
// false
}
.subscribe(on: DispatchQueue.main)
.receive(on: DispatchQueue.main)
.sink { completion in
} receiveValue: { value in
print("Thread is main at sinking ? : \(Thread.current.isMainThread)")
// true
}