Here2.click

サブドメインを使用したい方は連絡ください。とかきつつ連絡先書いてないですけど…

Swift6、Actorで警告0、PhotoLibraryの写真一覧のスクロール追従 サムネ取得&解放

Swift6、Actorで警告0、PhotoLibraryの写真一覧のスクロール追従 サムネ取得&解放

こんな程度と思ったら滅茶苦茶苦労したので、知見の共有と自身の備忘録。
分かってる人には、なんだこの程度かって内容かもしれませんが…

import Foundation
import UIKit
import Photos
import CryptoKit
import CryptoSwift
import CoreLocation
import Combine

private class WeakContainer {
    weak var delegate: ActorSingletonDelegate?
    init(delegate: ActorSingletonDelegate) {
        self.delegate = delegate
    }
}

public protocol ActorSingletonDelegate: AnyObject, Sendable {
    func eachPhotoLibraryFetched(to index: Int, info: AssetInformation)
    func photoLibraryLoadComplete(photos: [AssetInformation], totalCount: Int)
}

public actor ActorSingleton {
    static let shared = ActorSingleton()

    var PLinfo: [AssetInformation] = []
    let height: Int = 72
    let width: Int = 96
    let preload: Int = 20
    public var cntPLinfo: Int = 0

    var fetchThumbTask: Task<Void, Never>?

    private var rangeThumbnail: Set<Int> = []

    private var delegates: [WeakContainer] = []
    public func addDelegate(_ delegate: ActorSingletonDelegate) {
        if delegates.contains(where: { $0.delegate === delegate }) {
            return
        }
        delegates.append(WeakContainer(delegate: delegate))
    }
    public func removeDelegate(_ delegate: ActorSingletonDelegate) async {
        delegates.removeAll { $0.delegate === delegate }
    }
    private func cleanupDelegates() async {
        delegates.removeAll { $0.delegate == nil }
    }

    public func updateThumbnail(for toFetch: Range<Int>, releaseThumb: Bool = false) async {
        fetchThumbTask?.cancel()
        fetchThumbTask = Task {
            guard !Task.isCancelled else { return }

            let allphoto = Set(0..<self.PLinfo.count)
            let toRelease = allphoto.subtracting(toFetch)

            if releaseThumb {
                for index in toRelease {
                    guard !Task.isCancelled else { return }
                    if PLinfo.indices.contains(index) {
                        var asset = self.PLinfo[index]
                        asset.thumbnail = nil
                        self.PLinfo[index] = asset
                    }
                }
            }

            await withTaskGroup(of: Void.self) { group in
                for index in toFetch {
                    guard !Task.isCancelled else { return }
                    group.addTask {
                        await self.fetchThumbnail(for: index)
                    }
                }
            }
        }
    }

    private func fetchThumbnail(for index: Int) async {
        guard PLinfo.indices.contains(index) else { return }
        guard PLinfo[index].thumbnail == nil else { return }
        guard PLinfo[index].icloud != true else { return }

        let assetId = self.PLinfo[index].id
        let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil)
        guard let phAsset = fetchResult.firstObject else { return }

        let manager = PHImageManager.default()
        let options = PHImageRequestOptions()
        options.deliveryMode = .fastFormat
        options.isSynchronous = true
        options.isNetworkAccessAllowed = false

        var thumbnail: UIImage?
        manager.requestImage(for: phAsset, targetSize: CGSize(width: self.width, height: self.height), contentMode: .aspectFill, options: options) { image, _ in
            thumbnail = image
        }

        guard self.PLinfo.indices.contains(index) else { return }
        var updated = self.PLinfo[index]
        updated.thumbnail = thumbnail
        //updated.thumbnail = encodeImage2PNG(image: thumbnail)
        updated.icloud = (thumbnail == nil) ? true : false
        self.PLinfo[index] = updated
        await self.notifyItemUpdate(at: index, info: updated)
    }
    public func notifyItemUpdate(at index: Int, info: AssetInformation) async {
        for container in delegates {
            if let delegate = container.delegate {
                await MainActor.run {
                    delegate.eachPhotoLibraryFetched(to: index, info: info)
                }
            }
        }
    }

    func loadPhotoLibraryAsset() async {
        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: “creationDate”, ascending: false)]
        let assets = PHAsset.fetchAssets(with: fetchOptions)
        assets.enumerateObjects { asset, _, _ in
            let PLinfoRet = CommonSingleton.shared.upsertPLinfoEnt(id: asset.localIdentifier, isLibrary: true)
            let info = AssetInformation(
                id: asset.localIdentifier,
                creationDate: asset.creationDate,
                mediaType: asset.mediaType == .video ? “video” : “image”,
                pixelWidth: asset.pixelWidth,
                pixelHeight: asset.pixelHeight,
                size: nil,
                duration: (asset.mediaType == .video) ? asset.duration : nil,
                latitude: asset.location?.coordinate.latitude,
                longitude: asset.location?.coordinate.longitude,
                altitude: asset.location?.altitude,
                address: PLinfoRet[“address”]!,
                comment: PLinfoRet[“comment”]!,
                thumbnail: nil,
                icloud: nil,
                distance: CommonSingleton.shared.haversineDistance(latitude: asset.location?.coordinate.latitude, longitude: asset.location?.coordinate.longitude, unitMile: false)
            )
            self.PLinfo.append(info)
        }
        cntPLinfo = self.PLinfo.count
        await self.notifyLoadCompletion()
    }
    public func notifyLoadCompletion() async {
        for container in delegates {
            if let delegate = container.delegate {
                let photos = self.PLinfo
                let cnt = self.PLinfo.count
                await MainActor.run {
                    delegate.photoLibraryLoadComplete(photos: photos, totalCount:cnt)
                }
            }
        }
    }
}

hide

コメントは受け付けていません。