写真アプリに写真が保存されているGridviewを実装して、ユーザーが1枚の写真を選択してプロフィール写真として選択できるようにしようとしています。SwiftUI を使用する前は、コレクション ビューと Photos Kit を使用して画像を取得し、グリッドに表示していました。
SwiftUI に切り替えたので、LazyVGrid を使用してみました。ユーザーのすべての写真を取得して、グリッドに表示できます。ただし、大量のメモリを使用します。以前はメモリ リークがありましたが、Instruments でリークが表示されなくなりました。
ユーザーに見えなくなったときに、グリッドが表示された画像を実際にアンロードしていない可能性があると思いました。ただし、何度も上下にスクロールすると、以前よりも多くのメモリが使用されます。グリッドが常に新しいビューを作成しているように、古いビューは削除されません。LazyVGrid の原理を間違っているか誤解していますか?
class PhotoLibrary: ObservableObject {
@Published var assets = [PHAsset]()
@Published var imageCachingManager: PHCachingImageManager = PHCachingImageManager()
func requestAuthorization() {
PHPhotoLibrary.requestAuthorization { [weak self] (status) in
guard let self = self else { return }
switch status {
case .authorized:
self.getAllPhotos()
case .denied:
break
case .notDetermined:
break
case .restricted:
break
case .limited:
self.getAllPhotos()
@unknown default:
break
}
}
}
private func getAllPhotos() {
imageCachingManager.allowsCachingHighQualityImages = false
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.includeHiddenAssets = false
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let assets: PHFetchResult = PHAsset.fetchAssets(with: .image, options: allPhotosOptions)
var _assets = [PHAsset]()
assets.enumerateObjects({ asset, _, _ in
_assets.append(asset)
})
DispatchQueue.main.async { [weak self] in
self?.assets = _assets
}
}
func resetCache() {
imageCachingManager.stopCachingImagesForAllAssets()
assets = []
}
func preCacheImages(assets: [PHAsset], size: CGSize) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else {
return
}
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.resizeMode = .fast
options.isSynchronous = true
options.isNetworkAccessAllowed = true
self.imageCachingManager.startCachingImages(for: assets, targetSize: size, contentMode: .aspectFill, options: options)
}
}
func fetchImage(index: Int, size: CGSize?) async -> UIImage? {
guard let asset = assets[safe: index] else {
return nil
}
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.resizeMode = .fast
options.isSynchronous = true
options.isNetworkAccessAllowed = true
return await withCheckedContinuation({
[weak self] (continuation: CheckedContinuation<UIImage?, Never>) in
guard let self = self else {
return
}
self.imageCachingManager.requestImage(for: asset, targetSize: size ?? PHImageManagerMaximumSize, contentMode: .aspectFill, options: options) {(image, info) in
continuation.resume(returning: image)
}
})
}
func removeFromCache(index: Int, size: CGSize) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else {
return
}
guard let asset = self.assets[safe: index] else {
return
}
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.resizeMode = .fast
options.isSynchronous = true
options.isNetworkAccessAllowed = true
self.imageCachingManager.stopCachingImages(for: [asset], targetSize: size, contentMode: .aspectFill, options: options)
}
}
}
struct PhotoView: View {
var index: Int
let thumbnailSize: CGSize?
weak var photoLibrary: PhotoLibrary?
@State var image: UIImage? = nil
var body: some View {
HStack {
if let image = image {
Image(uiImage: image)
.resizable()
.square()
} else {
Color.label
.aspectRatio(1.0, contentMode: .fill)
}
}
.onDisappear(perform: {
unload()
})
.onAppear(perform: {
Task {
guard let photoLibrary = photoLibrary else {
return
}
let image = await photoLibrary.fetchImage(index: index, size: thumbnailSize ?? PHImageManagerMaximumSize)
DispatchQueue.main.async {
self.image = image
}
}
})
}
private func unload() {
self.image = nil
}
}
struct PickProfilePictureView: View {
@ObservedObject var photoLibrary = PhotoLibrary()
var body: some View {
VStack(alignment: .leading) {
GeometryReader { reader in
let width = reader.size.width / 4 - 6
ScrollView {
LazyVGrid(columns: [GridItem(), GridItem(), GridItem(), GridItem()]) {
ForEach(photoLibrary.assets.indices, id: \.self) { index in
PhotoView(index: index, thumbnailSize: CGSize(width: width, height: width), photoLibrary: photoLibrary)
.contentShape(Rectangle())
.onDisappear(perform: {
photoLibrary.removeFromCache(index: index, size: CGSize(width: width, height: width))
})
}
}
}
}
Spacer()
}
.ignoresSafeArea(.container, edges: .bottom)
.onAppear {
self.photoLibrary.requestAuthorization()
}
}
}