最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - withObservationTracking not detecting property changes in a @Observable model stored within a UIKit Cell and modified fr

programmeradmin7浏览0评论

I am building an app that is mostly not a grid view. However there is one screen that is a grid view and because I need the extra preloading performance in order to prioritize fast scrolling image loading properly.

As such my app is mostly implemented in SwiftUI.

This is just about all you need to see of the view model

@MainActor
@Observable
class WeakImage {
    weak var image: UIImage?
    init(image: UIImage? = nil) {
        self.image = image
    }
}

@MainActor
@Observable
final class LibraryViewModel {
    
    let libraryMetadata: LibraryMetadata = LibraryMetadata()
    @ObservationIgnored let imageCache: ImageCache
    private var loadedImages: [ImageAtLevel: WeakImage] = [:]

    func bestAvailableImage(for id: ImageId) -> WeakImage? {
        if let grid = loadedImages[ImageAtLevel(id: id, level: .grid)] {
            return grid
        }
        if let thumbnail = loadedImages[ImageAtLevel(id: id, level: .thumbnail)] {
            return thumbnail
        }
        return nil
    }

    // Called during preload
    func loadImage(id: ImageId, level: ImageCache.ArtifactLevel) async {
        let image = try await imageCache.get(for: id, level: level)
        loadedImages[imageAtLevel] = WeakImage(image: image)
    }
}

The intention is that every view shares this LibraryViewModel. When images need preloading loadImage is called. The cache is free to free up images at will so I am using WeakImage.

The problem that I am having is in the reusable cell of my UIKit view. I cannot seem to get it to observe these @Observable objects correctly.

struct PhotosGrid: UIViewControllerRepresentable {
    let libraryViewModel: LibraryViewModel
    
    func updateUIViewController(_ photosGridViewController: PhotosGridUIViewController, context: Context) {
        photosGridViewController.libraryViewModel = libraryViewModel
        photosGridViewController.order = libraryViewModel.libraryMetadata.order
    }
    
    func makeUIViewController(context: Context) -> PhotosGridUIViewController {
        let viewController = PhotosGridUIViewController()
        viewController.libraryViewModel = libraryViewModel
        return viewController
    }
}

class PhotosGridUIViewController: UIViewController, UICollectionViewDataSource, ... {
    var libraryViewModel: LibraryViewModel?
    var order: [ImageId] = [] {
        didSet {
            DispatchQueue.main.async { [weak self] in
                self?.collectionView.reloadData()
            }
        }
    }
    
    lazy var collectionView: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        ...
        collectionView.dataSource = self
        collectionView.delegate = self
        return collectionView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(collectionView)
        ... (pinned to superview bounds)
        collectionView.register(UIPhotoGridCell.self, forCellWithReuseIdentifier: PhotosGridUIViewController.reuseId)
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return order.count
    }
        
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotosGridUIViewController.reuseId, for: indexPath) as? UIPhotoGridCell else {
            fatalError("Could not dequeue cell")
        }
        cell.model.libraryViewModel = libraryViewModel
        cell.model.id = order[indexPath.row]
        return cell
    }
}

// I dont really need this object. Its just here to make this observable
@Observable
class UIPhotoGridCellModel {
    var id: ImageId? = nil
    var libraryViewModel: LibraryViewModel? = nil
}

class UIPhotoGridCell: UICollectionViewCell {
    
    var model: UIPhotoGridCellModel = UIPhotoGridCellModel()
    
    let imageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFit
        return imageView
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.addSubview(imageView)

        ... (Pin to superview)
        
        withObservationTracking {
            // Never notices that model.id changed!
            if let id = model.id {
                _ = model.libraryViewModel?.bestAvailableImage(for: id)?.image
            }
        } onChange: {
            DispatchQueue.main.async { [weak self] in
                guard let self else { return }
                if let id = self.model.id {
                    imageView.image = model.libraryViewModel?.bestAvailableImage(for: id)?.image
                }
            }
        }
    }
}

And then of course the SwiftUI view that uses it

struct ContentView: View {
    @State var libraryViewModel = LibraryViewModel()
    var body: some View {
        VStack {
            PhotosGrid(libraryViewModel: libraryViewModel)
                .task {
                    await libraryViewModel.refresh()
                }
        }
    }
}

That is quite a bit of code but essentially at a high level:

  1. LibraryViewModel holds the libraryMetadata and the imageCache and the pertinent bestAvailableImage method being used.
  2. ContentView holds the LibraryViewModel as @State
  3. PhotosGrid is a UIViewControllerRepresentable holding the PhotosGridUIViewController. It passes in LibraryViewModel in its makeUIViewController and updateUIViewController.
  4. PhotosGridUIViewController uses reusable UIPhotoGridCell's. When the cells are set up in cellForItemAt they get their id and libraryViewModel passed into them.
  5. The UIPhotoGridCell attempts to use withObservationTracking to track changes to the bestAvailableImage result in LibraryViewModel.

This unfortunately does not work. None of the images on the first screen notice they even got a thumbnail (although when you scroll they do) and none of them update when a better version is available. I put a breakpoint in withObservationTracking and the withObservationTracking never calls to notice that model.id changed. I dont understand why it wouldnt.

If you use a SwiftUI view it does update as these images update properly so it is something about the way I am hosting a UIKit view here.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论