Lecture 13: Publisher More Persistence

sun·2021년 11월 24일
0

유튜브 링크

# Publisher, Subscriber, Cancellable

  • Publisher 는 값 혹은 에러를 발송하는 객체고, SubscriberPublisher 가 보낸 값을 받는 객체다!

  • sink(receiveCompletion:receiveValue:)Subsciber 의 하나로, Publisher 의 값들이 발송되는 곳인데, 값이 나올 때마다 특정 클로저( receiveValue )를 실행하게 할 수 있고, Publisher 가 할 일을 다 했거나 에러가 발생해서 작업을 끝내게 될 때 또한 특정 클로저( reciebeCompletion )를 실행하도록 지정할 수 있다.

  • 이때, sink 가 리턴하는 게 Cancellable 인데, sink 의 수명은 Cancellable 의 존재가 결정한다. 따라서 sink 가 동작하기를 원하는 동안은 반드시 Cancellable 이 존재하도록 해줘야 한다!
    • e.g. Cancellable
      • 옵셔널이라면 nil 이 될 때
      • 함수 내부의 변수라면 해당 함수가 종료될 때
      • 객체의 프로퍼티라면 해당 객체가 할당 해제될 때, sink 또한 작동을 멈춤

URL로부터 이미지 가져오기, 근데 이제 Combine을 곁들인

1. URLSession을 사용해서 Publisher 만들기

  • Combine 프레임워크를 사용하면 이전에 EmojiArtDocument 에서 기존에는 직접 GCD를 써서 드래그 & 드랍으로 받은 url 로부터 데이터를 받아와서 UIImage 로 변환하는 과정: fetchBackgroundImageDataIfNecessary() 을 좀 더 간단하고 가독성 있는 코드로 바꿔줄 수 있다.

  • 먼저 URL 로부터 데이터를 받아올 Publisher 를 생성해야 하는데, URL 엔드포인트와의 데이터 송수신을 쉽게 해주는 API인 URLSession 을 사용한다. 단순히 데이터를 받아오는 작업을 할 것이므로 shared session 을 사용할 건데, shared session 의 경우 싱글톤으로, 우리가 매번 인스턴스를 생성하는 게 아니라 이미 존재하는 프로퍼티에 접근하는 것. 따라서 별도의 설정을 할 수 없지만 간단한 작업에 적합하다! URLSessiondataTaskPublisher(for:) 메서드를 지원하는데, 이 메서드를 사용하면 data task 가 완료되었거나 에러로 인해 실패했을 때 결과를 발송하는 Publsher 를 생성할 수 있다.

    • Data Task : NSData 객체들을 사용해 데이터를 주고받는다.
    • dataTaskPublisher(for:) 는 성공 시 (받아온 데이터, URLResponse ) 를, 실패 시 에러를 리턴한다
    • dataTaskPublisher(for:) 를 쓰면 단순히 dataTask(with:completionHandler:) 에서 completionHandler 가 처리하던 작업의 상당수를 Combine operators 가 처리해준다!
  • 그리고 좀 더 깔끔한 코드를 위해 애초에 PublisherSubsriber 에게 값을 전송할 때 데이터가 아니라 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)
        ...
        }
        
    }
}

2. sink(receiveValue:) : 구독~ 좋아요~ 알림 설정~

  • Publisher 는 해결됐으니 이제 Subscriber 를 생성해서 Publihser 로부터 값을 받아와 우리의 목적대로 활용해주면 된다. 이를 위해서 sink(receiveValue:) 를 사용한다.
    • assign(to: \EmojiArtDocument.backgroudImage, on: self) 를 사용해서 바로 원하는 변수에 Publisher 로부터 받아온 값을 넣어줄 수도 있지만, 이렇게 하는 경우 다른 작업을 수행할 수 없다. 하지만 우리는 backgroundImage 변수 설정 외에도 backgroundImageFetchStatus 를 업데이트 하는 등 다른 작업을 수행해야하므로 sink(receiveValue:) 를 사용해야 한다!
  • 먼저 함수 밖에 private var backgroundImageFetchCancellable: AnyCancellable? 을 따로 선언해줘야 하는데, Subscriber 의 수명이 Cancellable 에 달려 있어 만약 함수 내에 선언한다면 함수가 끝남과 동시에 Cancellable 이 사라져서 SubscriberPublisher 가 값을 보내주기 전에 사라지게 되기 때문이다. 따라서 함수밖, 객체 내부에 별도로 Cacellable 을 선언해주면 이러한 문제를 방지할 수 있다.
  • 그리고 나서 sink 가 인자로 받는 클로저에서 우리가 원하는 작업(배경 이미지 설정, FetchStatus 설정)을 수행하면 된다. 이때 클로저에서 [weak self] 를 붙여주는 건 예전에 처음 [weak self] 에 대해 배웠을 때와 같은 이유인데, 유효하지 않은 링크라 불러올 데이터가 없어 무한 대기 상태에 있는 경우 유저가 기다리다가 지쳐서 해당 document 를 꺼버릴 수도 있는데 이럴 때 메모리에 더 이상 해당 EmojiArtDocument 가 남아있기를 원하지 않기 때문! weak 으로 선언해주면 이 document 가 모종의 이유로 이미 닫혔을 때 뒤늦은 sink 시도가 모두 무시된다.
  • 또 하나 필요한 작업은, 유저가 a 이미지를 드랍했는데 너무 오래 걸려서 로딩이 완료되기 전에 다른 이미지 b를 드랍하고 b는 바로 로딩이 된 경우, a 로딩 작업을 취소해주지 않으면 뒤늦게 a가 나타날 수 있다. 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 는 비슷하게 생겼지만 documentObservedObject 이므로 바인딩이라는 점에 주의할 것!
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)
            }
        }
    }
    ...
}

# CloudKit

  • 개략적으로 설명만 들어서 사실 감이 잘 안온다...뭘 정리해야될 지 모르겠음...내가 DB 알못이기도 하고 그래서 DB 공부도 해보고 CloudKit 을 나중에 직접 써보면서 별도로 포스팅할 생각이다. 일단 여기서는 슬라이드만 링크로 걸고 마무리!

# CoreData

  • 마찬가지로 이번 강의에서는 설명만 들었는데 CoreData 의 경우 2020 강의에 데모가 있다고 해서 해당 강의 + 이전에 봤던 유튜브를 보면서 한 번 정리해볼 생각이다!
  • @Evironment(\.managedObjectContext) var context 는 DB에 접근할 수 있는 윈도우 창으로, CoreData 를 쓰려는 코드가 있다면 꼭 필요하다.

  • CoreData 는 객체와 매우 유사하다. 단지 우리가 만든 map 에 기반해서 relational database 에 변환되어 저장되었을 뿐이다!

  • CoreData 에 저장하는 것은 동기적이며, 우리 기기의 로컬에 저장되는 것. 에러 처리를 우리 대신 다 해준다.


☀️ 느낀점

  • 이전에 혼자 API 공부하면서 Combine 을 접한 적이 있었는데 공식문서를 봐도 잘 이해가 안가서 대체 뭘까...뭘 Subscribe 한다는 걸까...이러면서 그냥 복붙했었는데 원시적인? 과정을 Combine 을 사용해서 바꿔보니까 어떤 과정인지 훨씬 이해가 잘 돼서 좋았다. 그리고 개인적으로 근데 사실 Combine 을 써도 코드 줄 수는 별 차이가 없는데 이게 필요한가 하는 의문이 있었는데, 교수님이 딱 훨씬 더 readable 해졌다고 말씀해주셔서 수긍이 갔다. 내가 직접 코드로 작성해야했던 메서드들이 내장되어있다는 점이 가장 큰 것 같다. 꼭 공부할 필요가 있을까 했는데 Combine 을 잘 이해하고 싶어졌다...






Publisher

  • learn the COMBINE framework:)

Listening(subscribing to a Publisher

  • 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

onRecieve(_:perform:)

  • onChange usually does all the tricks...but it's for vars so you could use this for publishers...

Cloud Kit 약 41분부터

  • shared database: a permission spaced shared database where u can explicilty give permission to a certain other iCloud user to access that Database

Core Data

  • @Evironment(\.managedObjectContext) var context -> window into the database
  • 53분 : using Core Data is just like using normal objects but it's just that they're mapped into that relational database as per the map that u build
  • saving in CoreData is not asynchronus, it's local to our device
  • CoreData does all the error handling for you:)

    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)

  • 57분 : @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
    • this will continuously update as the database changes, and invalidate our View and rebuild its body
    • so this keeps our UI in sync with the database
profile
☀️

0개의 댓글

관련 채용 정보