I’m currently working on a SwiftUI application that displays media files in a grid with LazyVGrid on GalleryView
where there will be 3 media files per row. The media files are loaded from the app’s cache and not from the user’s Photos
app. When user scrolls down to see more media data, media files are lazily loaded into memory and the ones hidden from view are not deallocated from memory because LazyVGrid does not have a built-in memory deallocation mechanism. The memory issues become more apparent (n>10
where n
is the number of media files) when there exists a large number of images or videos to show on appear of GalleryView
:
struct GalleryView: View {
@Binding var mediaFiles: [Media]
var body: some View {
GeometryReader { geometry in
ScrollView {
let width = geometry.size.width / 3 - 10
let gridItemLayout = [GridItem(.fixed(width)), GridItem(.fixed(width)), GridItem(.fixed(width))]
LazyVGrid(columns: gridItemLayout, spacing: 10) {
ForEach(mediaFiles, id: .id) { media in
if let image = media.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: width)
} else if let videoURL = media.videoURL {
// Display video thumbnail here
}
}
}
}
}
}
}
struct Media: Identifiable {
let id = UUID()
let creationTime: Date
var image: UIImage?
var videoURL: URL?
}
I have tried a custom UIKit approach to handle the memory deallocation issue:
struct CollectionView: UIViewControllerRepresentable {
typealias UIViewControllerType = UICollectionViewController
@Binding var mediaFiles: [Media]
var width: CGFloat
func makeUIViewController(context: Context) -> UICollectionViewController {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: width / 3, height: width / 3)
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
let collectionViewController = UICollectionViewController(collectionViewLayout: layout)
collectionViewController.collectionView?.register(MediaCell.self, forCellWithReuseIdentifier: "cell")
collectionViewController.collectionView?.backgroundColor = .white
return collectionViewController
}
func updateUIViewController(_ uiViewController: UICollectionViewController, context: Context) {
uiViewController.collectionView?.dataSource = context.coordinator
uiViewController.collectionView?.delegate = context.coordinator
uiViewController.collectionView?.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
var parent: CollectionView
init(_ parent: CollectionView) {
self.parent = parent
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return parent.mediaFiles.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MediaCell
let media = parent.mediaFiles[indexPath.row]
if let image = media.image {
cell.imageView.image = image
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Handle selection here
}
}
}
so that I can use CollectionView
in place of LazyVGrid in GalleryView
like this:
CollectionView(mediaFiles: $mediaFiles, width: width)
.frame(width: geometry.size.width, height: geometry.size.height)
.onAppear {
model.loadMediaFiles()
if let lastMediaIndex = mediaFiles.indices.last {
scrollView.scrollTo(lastMediaIndex, anchor: .bottom)
}
}
But the custom UIKit code didn’t work as expected because GalleryView
now shows a blank white screen (on either light/dark mode) instead of showing 3 media items per row. I have also tried using List in place of LazyVGrid, or trying Chris C‘s code to optimize the memory complexity of the problem to O(1)
but neither approach worked. I’ve also tried to chunk the data into pieces and create rows with more than one image as suggested in another thread by @baronfac. However, @baronfac‘s suggestion only optimizes the memory issue by from n
to n/3
, which this linear reduction doesn’t really change the memory complexity of the problem since it’s still O(n)
.
Is there a simple and intuitive way to deal with this absence of memory deallocation issue with LazyVGrid? If a UIKit implementation is the best approach, could someone help identify what’s wrong with my current UIKit code that’s causing the GalleryView to display a blank screen?
2
Answers
UIViewRepresentable
instead of view controller variant, and use SwiftUI as parent navigation view, where you inject Collection view (UIView
)?AVAsset
,UIImage
,PHAsset
resulting in them getting overwritten every time new cell is resued. With this setup you will only have N realtime instances of assets, where N = number of visible cells.Now to do this in SwiftUI you can use onAppear(perform:) & onDisappear(perform:) view modifiers.
Here is how I would try:
then just replace this view in your
LazyVGrid
‘sForEach
like:Now this way assets are loaded and stay live in memory as long as the view is visible.
NOTE: Both onAppear and onDisappear were known to be buggy in earlier iterations of SwiftUI. Read here. But it should be more stable by iOS 14+.
I was just solving this problem a few days ago, and now it’s solved (completely SwiftUI)
***2. Manually release the original memory (this step is very important)
(Use @StateObject and @Published for real feedback)
Code: Local database load images, but the memory grows infinitely and memory overflow – SwiftUI