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)
}
}
}
}
}