Publisher 는 값 혹은 에러를 발송하는 객체고, Subscriber 는 Publisher
가 보낸 값을 받는 객체다!
sink(receiveCompletion:receiveValue:)
는 Subsciber
의 하나로, Publisher
의 값들이 발송되는 곳인데, 값이 나올 때마다 특정 클로저( receiveValue
)를 실행하게 할 수 있고, Publisher
가 할 일을 다 했거나 에러가 발생해서 작업을 끝내게 될 때 또한 특정 클로저( reciebeCompletion
)를 실행하도록 지정할 수 있다.
sink
가 리턴하는 게 Cancellable 인데, sink
의 수명은 Cancellable
의 존재가 결정한다. 따라서 sink
가 동작하기를 원하는 동안은 반드시 Cancellable
이 존재하도록 해줘야 한다!Cancellable
이 nil
이 될 때sink
또한 작동을 멈춤Combine
프레임워크를 사용하면 이전에 EmojiArtDocument
에서 기존에는 직접 GCD를 써서 드래그 & 드랍으로 받은 url
로부터 데이터를 받아와서 UIImage
로 변환하는 과정: fetchBackgroundImageDataIfNecessary()
을 좀 더 간단하고 가독성 있는 코드로 바꿔줄 수 있다.
먼저 URL
로부터 데이터를 받아올 Publisher
를 생성해야 하는데, URL
엔드포인트와의 데이터 송수신을 쉽게 해주는 API인 URLSession
을 사용한다. 단순히 데이터를 받아오는 작업을 할 것이므로 shared session
을 사용할 건데, shared session
의 경우 싱글톤으로, 우리가 매번 인스턴스를 생성하는 게 아니라 이미 존재하는 프로퍼티에 접근하는 것. 따라서 별도의 설정을 할 수 없지만 간단한 작업에 적합하다! URLSession
은 dataTaskPublisher(for:)
메서드를 지원하는데, 이 메서드를 사용하면 data task
가 완료되었거나 에러로 인해 실패했을 때 결과를 발송하는 Publsher
를 생성할 수 있다.
Data Task
: NSData
객체들을 사용해 데이터를 주고받는다.dataTaskPublisher(for:)
는 성공 시 (받아온 데이터, URLResponse
) 를, 실패 시 에러를 리턴한다dataTaskPublisher(for:)
를 쓰면 단순히 dataTask(with:completionHandler:)
에서 completionHandler
가 처리하던 작업의 상당수를 Combine operators
가 처리해준다!Publisher
가 Subsriber
에게 값을 전송할 때 데이터가 아니라 UIImage
형태로 전송하도록 바꿔줄 건데, map(_:)
을 사용해서 URL
로부터 받아온 데이터를 UIImage
로 바꿔주면 된다!URL
로 인해 이미지를 불러오지 못하는 경우 에러를 출력하는 게 아니라 그냥 배경 이미지를 .blank
로 설정하고 싶으므로 replaceError(with:)
를 사용해서 에러가 발생하는 경우 그냥 Subscriber
에게 nil
을 전송하도록 한다!Subcriber
에서 배경 이미지를 설정해 줄건데 이는 UI
에 영향을 미치는 작업이므로 메인 큐에서 이루어져야 한다. receive(on:)
을 사용하면 아주 쉽게 Subscriber
가 어디서 값을 받을 지 지정해 줄 수 있다.class EmojiArtDocument: ObservableObject {
private func fetchBackgroundImageDataIfNecessary() {
backgroundImage = nil
switch emojiArt.background {
case .url(let url):
backgroundImageFetchStatus = .fetching
let session = URLSession.shared // created URLSession
let publisher = session.dataTaskPublisher(for: url) // created a Publisher
.map { (data, urlResponse) in UIImage(data: data)}
}
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
...
}
}
}
Publisher
는 해결됐으니 이제 Subscriber
를 생성해서 Publihser
로부터 값을 받아와 우리의 목적대로 활용해주면 된다. 이를 위해서 sink(receiveValue:)
를 사용한다. assign(to: \EmojiArtDocument.backgroudImage, on: self)
를 사용해서 바로 원하는 변수에 Publisher
로부터 받아온 값을 넣어줄 수도 있지만, 이렇게 하는 경우 다른 작업을 수행할 수 없다. 하지만 우리는 backgroundImage
변수 설정 외에도 backgroundImageFetchStatus
를 업데이트 하는 등 다른 작업을 수행해야하므로 sink(receiveValue:)
를 사용해야 한다! private var backgroundImageFetchCancellable: AnyCancellable?
을 따로 선언해줘야 하는데, Subscriber
의 수명이 Cancellable
에 달려 있어 만약 함수 내에 선언한다면 함수가 끝남과 동시에 Cancellable
이 사라져서 Subscriber
도 Publisher
가 값을 보내주기 전에 사라지게 되기 때문이다. 따라서 함수밖, 객체 내부에 별도로 Cacellable
을 선언해주면 이러한 문제를 방지할 수 있다.sink
가 인자로 받는 클로저에서 우리가 원하는 작업(배경 이미지 설정, FetchStatus
설정)을 수행하면 된다. 이때 클로저에서 [weak self]
를 붙여주는 건 예전에 처음 [weak self]
에 대해 배웠을 때와 같은 이유인데, 유효하지 않은 링크라 불러올 데이터가 없어 무한 대기 상태에 있는 경우 유저가 기다리다가 지쳐서 해당 document
를 꺼버릴 수도 있는데 이럴 때 메모리에 더 이상 해당 EmojiArtDocument
가 남아있기를 원하지 않기 때문! weak
으로 선언해주면 이 document
가 모종의 이유로 이미 닫혔을 때 뒤늦은 sink
시도가 모두 무시된다.Combine
프레임워크에서는 이를 아주 간단하게 처리할 수 있는데, 데이터를 받아오는 작업을 시작할 때마다 이전 작업이 있다면 backgroundImageCancellable?.cancel()
과 같이 취소해주면 끝! class EmojiArtDocument: ObservableObject {
private var backgroundImageFetchCancellable: AnyCancellable?
private func fetchBackgroundImageDataIfNecessary() {
backgroundImage = nil
switch emojiArt.background {
case .url(let url):
backgroundImageFetchStatus = .fetching
backgroundImageFetchCancellable?.cancel() // cancel prev fetch
let session = URLSession.shared
let publisher = session.dataTaskPublisher(for: url)
.map { (data, urlResponse) in UIImage(data: data)}
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
backgroundImageFetchCancellable = publisher
.sink { [weak self] image in
self?.backgroundImage = image
self?.backgroundImageFetchStatus = image != nil ? .idle : .failed(url)
}
...
}
}
}
onReceive(_:perform:)
를 사용할건데, 인자로 받은 Publisher
가 값을 뱉을 때마다 특정 액션을 수행하는 친구다. 사실 onChange(of:perform)
을 써도 같은 효과를 낼 수 있지만 수업때 교수님이 한번 보여주고 싶으셔서 이걸 쓴 것 같다. @Published
변수의 앞에 $
를 붙여주면 Publisher
가 된다! 따라서 아래와 같이 Publisher
가 값을 보낼 때마다 zoomToFit(_:in:)
을 호출하도록 해주면 backgroundImage
가 바뀔 때마다 이를 감지하는 Publisher
가 된다.$document.backgroundImage
는 비슷하게 생겼지만 document
는 ObservedObject
이므로 바인딩이라는 점에 주의할 것!struct EmojiArtDocumentView: View {
@ObservedObject var document: EmojiArtDocument
var documentBody: some View {
GeometryReader { geometry in
ZStack {
// some code
}
.onReceive(document.$backgroundImage) { image in
zoomToFit(image, in: geometry.size)
}
}
}
...
}
DB
알못이기도 하고 그래서 DB
공부도 해보고 CloudKit
을 나중에 직접 써보면서 별도로 포스팅할 생각이다. 일단 여기서는 슬라이드만 링크로 걸고 마무리!CoreData
의 경우 2020 강의에 데모가 있다고 해서 해당 강의 + 이전에 봤던 유튜브를 보면서 한 번 정리해볼 생각이다!@Evironment(\.managedObjectContext) var context
는 DB에 접근할 수 있는 윈도우 창으로, CoreData
를 쓰려는 코드가 있다면 꼭 필요하다.
CoreData
는 객체와 매우 유사하다. 단지 우리가 만든 map
에 기반해서 relational database
에 변환되어 저장되었을 뿐이다!
CoreData
에 저장하는 것은 동기적이며, 우리 기기의 로컬에 저장되는 것. 에러 처리를 우리 대신 다 해준다.
Combine
을 접한 적이 있었는데 공식문서를 봐도 잘 이해가 안가서 대체 뭘까...뭘 Subscribe
한다는 걸까...이러면서 그냥 복붙했었는데 원시적인? 과정을 Combine
을 사용해서 바꿔보니까 어떤 과정인지 훨씬 이해가 잘 돼서 좋았다. 그리고 개인적으로 근데 사실 Combine
을 써도 코드 줄 수는 별 차이가 없는데 이게 필요한가 하는 의문이 있었는데, 교수님이 딱 훨씬 더 readable
해졌다고 말씀해주셔서 수긍이 갔다. 내가 직접 코드로 작성해야했던 메서드들이 내장되어있다는 점이 가장 큰 것 같다. 꼭 공부할 필요가 있을까 했는데 Combine
을 잘 이해하고 싶어졌다... sink is a place where the values are coming out of a Publisher and u can excute some closure everytime a value comes out and also at the end when the publisher runs out of whatever it wanted to publish or there's an error u excute another closure
sink returns a cancellable and this cancellable keeps sink alive(i.e. the lifetime of sink is tied to the the existence of the cancellable
- e.g. if the cancellable is an optional and becomes nil at one point, the sink will stop
onChange
usually does all the tricks...but it's for vars so you could use this for publishers... @Evironment(\.managedObjectContext) var context
-> window into the database
async
: means that the funciton returns right away and the work is done in some background Queue, and alerts u (if required) in some way when the work is done(success/failure)
@FetchRequest(fetchRequest)
this property wrapper takes fetchRequest as the argument. this now becomes kinda like a standing fetch request and the var that goes along with the property wrapper is going to be the results of that fetch