0

I have a struct for my model and it needs to conform to a protocol that is NSObject only.

I am looking for a viable alternative to converting the model to a class. The requirements are:

  1. Keeping the model a value type
  2. Updating the model when photoLibraryDidChange is called

This would be the ideal implementation if PHPhotoLibraryChangeObserver would not require the implementation to be an NSObject

struct Model: PHPhotoLibraryChangeObserver {

    var images:[UIImages] = []

    fileprivate var allPhotos:PHFetchResult<PHAsset>?

    mutating func photoLibraryDidChange(_ changeInstance: PHChange) {
        let changeResults = changeInstance.changeDetails(for: allPhotos)
        allPhotos = changeResults?.fetchResultAfterChanges
        updateImages()
    }

    mutating func updateImages() { 
        // update self.images
        ...
    }

}

I cannot pass the model to an external class implementing the observer protocol as then all the changes happen on the copy (its a value type...)

Any ideas? Best practices?

EDIT: Reformulated the question

EDIT 2: Progress

I have implemented a delegate as a reference type var of my model and pushed the data inside. Now photoLibraryDidChangeis not being called anymore.

This is the stripped down implementation:


class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver {
    
    var allPhotos: PHFetchResult<PHAsset>?
    
    var images:[UIImage] = []
    
    override init(){
        super.init()
    }
    
    func photoLibraryDidChange(_ changeInstance: PHChange) {
        DispatchQueue.main.async {
            if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
                self.allPhotos = changeResults.fetchResultAfterChanges
                //this neve gets executed. It used to provide high quality images
                self.updateImages()
            }
        }
    }
    
    func startFetching(){
        let allPhotosOptions = PHFetchOptions()
        allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
        PHPhotoLibrary.shared().register(self)
        //this gets executed and fetches the thumbnails
        self.updateImages()
    }
    
    fileprivate func appendImage(_ p: PHAsset) {
        let pm = PHImageManager.default()
        if p.mediaType == .image {
            pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
                image, _  in
                if let im = image {
                    self.images.append(im)
                }
                
            }
        }
    }
    
    fileprivate func updateImages() {
        self.images = []
        if let ap = allPhotos {
            for index in 0..<min(ap.count, 10) {
                let p = ap[index]
                appendImage(p)
            }
        }
    }
    
}


struct Model {
    
    private var pkAdapter = PhotoKitAdapter()
    
    var images:[UIImage] {
        pkAdapter.images
    }
    
    func startFetching(){
        pkAdapter.startFetching()
    }
    
    // example model implementation
    mutating func select(_ image:UIImage){
        // read directly from pkAdapter.images and change other local variables
    }
    

}


I have put a breakpoint in photoLibraryDidChange and it just does not go there. I also checked that pkAdapter is always the same object and does not get reinitialised on "copy on change".

**EDIT: adding the model view **

This is the relevant part of the modelview responsible for the model management


class ModelView:ObservableObject {
    
    @Published var model =  Model()
  
    init() {
        self.model.startFetching()
    }
    
    var images:[UIImage] {
        self.model.images
    }
    
    ...
    
}

EDIT: solved the update problem

It was a bug in the simulator ... on a real device it works

Daniele Bernardini
  • 1,516
  • 1
  • 12
  • 29
  • I am experimenting with pushing the only mutable data that PhotoKit has to change to a reference type property that acts as delegate. The rest of the model can work on it asynchronously so it can access the data inside the delegate without passing self (the model) to PhotoKit. – Daniele Bernardini Aug 31 '20 at 17:43
  • I wrote up the results so far. Seems viable except that `photoLibraryDidChange` does not get called... There is for sure a stupid error, I just can't see it – Daniele Bernardini Aug 31 '20 at 18:34
  • Might depend on how your struct gets passed around? Remember, it's a struct, so every assignment makes a copy. Can you show more about how Model is maintained and so forth? – matt Aug 31 '20 at 21:09
  • I debugged and checked that the adapter class was not instanced more than once. Swift when it does copy on change should transfer the pointer to the reference type properties to the new value. That seems to be the case. I also explicitly requested the id of the adapter object, after several mutation on the model and it was the same. So that exclude multiple copies and lost reference. – Daniele Bernardini Sep 01 '20 at 09:11
  • Simulator bug. On a real device it works... what a waste of time – Daniele Bernardini Sep 01 '20 at 11:26
  • Please don’t put the answer in the question. Put it as an answer so you can accept it and show the problem as solved. Thanks. – matt Sep 01 '20 at 12:55
  • I was planning to do it – Daniele Bernardini Sep 01 '20 at 13:06

1 Answers1

0

I ended up with 2 possible designs:

  1. decouple the PhotoKit interface completely from the model, let the modelview manage both and connect them, as the model view has access to the model instance.
  2. create a PhotoKit interface as var of the model and push the mutable data that is generated by the PhotoKit interface inside it, so it can be changed with an escaping closure. The model is never called from the interface but just exposes the data inside the PhotoKit through a computer property.

I will show two sample implementation below. They are naive in many respect, they ignore performance problems by refreshing all pictures every time the PhotoLibrary is updated. Implementing proper delta updates (and other optimisation) would just clutter up the code and offer no extra insight on the solution to the original problem.

Decouple the PhotoKit interface

ModelView

class ModelView:ObservableObject {
    
    var pkSubscription:AnyCancellable?
    
    private var pkAdapter = PhotoKitAdapter()
    
    @Published var model =  Model()
  
    init() {
        pkSubscription = self.pkAdapter.objectWillChange.sink{ _ in
            DispatchQueue.main.async {
                self.model.reset()
                for img in self.pkAdapter.images {
                    self.model.append(uiimage: img)
                }
            }
        }
        self.pkAdapter.startFetching()
    }
}

Model

struct Model {
    
    private(set) var images:[UIImage] = []
    
    mutating func append(uiimage:UIImage){
        images.append(uiimage)
    }
    
    mutating func reset(){
        images = []
    }
}

PhotoKit interface

class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver, ObservableObject {
    
    var allPhotos: PHFetchResult<PHAsset>?
    var images:[UIImage] = []
    
    func photoLibraryDidChange(_ changeInstance: PHChange) {
        DispatchQueue.main.async {
            if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
                self.allPhotos = changeResults.fetchResultAfterChanges
                self.updateImages()
                self.objectWillChange.send()
            }
        }
    }
    
    func startFetching(){
        PHPhotoLibrary.requestAuthorization{status in
            if status == .authorized {
                let allPhotosOptions = PHFetchOptions()
                allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
                self.allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
                PHPhotoLibrary.shared().register(self)
                self.updateImages()
                
            }
        }

        
    }
    
    fileprivate func appendImage(_ p: PHAsset) {
        // This actually appends multiple copies of the image because 
        // it gets called multiple times for the same asset. 
        // Proper tracking of the asset needs to be implemented
        let pm = PHImageManager.default()
        if p.mediaType == .image {
            pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
                image, _  in
                if let im = image {
                    self.images.append(im)
                    self.objectWillChange.send()
                }
                
            }
        }
    }
    
    fileprivate func updateImages() {
        self.images = []
        if let ap = allPhotos {
            for index in 0..<min(ap.count, 10) {
                let p = ap[index]
                appendImage(p)
            }
        }
    }

PhotoKit interface as property of the model

ModelView

class ModelView:ObservableObject {
    
    @Published var model =  Model()
  
}

Model

struct Model {
    
    private var pkAdapter = PhotoKitAdapter()
    
    var images:[UIImage] { pkAdapter.images }
    
}

PhotoKit interface

class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver{
    
    var allPhotos: PHFetchResult<PHAsset>?
    var images:[UIImage] = []

    override init(){
        super.init()
        startFetching()
    }
    
    // the rest of the implementation is the same as before
    ...
Daniele Bernardini
  • 1,516
  • 1
  • 12
  • 29