8

I have a vertical list in the screen to show the images category wise and each category/list contains list of images which is shown horizontally. (Attached image for reference)

Now when I am scrolling horizontally or vertically then application is crashing due to memory leaking. I guess lots of people facing this issue in the ForEach loop.

I have also try with List instead of ForEach and ScrollView for both vertical/horizontal scrolling but unfortunately facing same issue.

Below code is the main view which create the vertical list :

@ObservedObject var mainCatData = DataFetcher.sharedInstance

var body: some View {
    
    NavigationView {
        VStack {
            ScrollView(showsIndicators: false) {
                LazyVStack(spacing: 20) {
                    ForEach(0..<self.mainCatData.arrCatData.count, id: \.self) { index in
                        self.horizontalImgListView(index: index)
                    }
                }
            }
        }.padding(.top, 5)
        .navigationBarTitle("Navigation Title", displayMode: .inline)
    }
}

I am using below code to create the horizontal list inside each category, I have used LazyHStack, ForEach loop and ScrollView

@ViewBuilder
func horizontalImgListView(index : Int) -> some View {
    
    let dataContent = self.mainCatData.arrCatData[index]

    VStack {
     
        HStack {
            Spacer().frame(width : 20)
            Text("Category \(index + 1)").systemFontWithStyle(style: .headline, design: .rounded, weight: .bold)
            Spacer()
        }
        
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 20) {
                ForEach(0..<dataContent.catData.count, id: \.self) { count in
                                                                                    
                    VStack(spacing : 0) {
                        VStack(spacing : 0) {
                            
                            if let arrImgNames = themeContent.catData[count].previewImgName {

                                // Use dynamic image name and it occurs app crash & memory issue and it reached above 1.0 gb memory
                                Image(arrImgNames.first!).resizable().aspectRatio(contentMode: .fit)
                                                                                                                                                                
                               // If I will use the same image name then there is no memory issue and it consumes only 75 mb
                               // Image("Category_Image_1").resizable().aspectRatio(contentMode: .fit)                              
                            }
                        }.frame(width: 150, height: 325).cornerRadius(8.0)
                    }
                }
            }
        }
    }
}

Below is the data model which I am using to fetch images from json file and shows it in the list

class DataFetcher: ObservableObject {
    
    static let sharedInstance = DataFetcher()
    @Published var arrCatData = [CategoryModel]()
     
    init() {
                
        do {
            if let bundlePath = Bundle.main.url(forResource: FileName.CategoryData, withExtension: "json"),
               
               let jsonData = try? Data(contentsOf: bundlePath) {
                
                let decodedData = try JSONDecoder().decode([CategoryModel].self, from: jsonData)
                DispatchQueue.main.async { [weak self] in
                    self?.arrCatData = decodedData
                }
            }
        } catch {
            print("Could not load \(FileName.CategoryData).json data : \(error)")
        }
    }
}

struct CategoryModel : Codable , Identifiable {
    let id: Int
    let catName: String
    var catData: [CategoryContentDataModel]
}

struct CategoryContentDataModel : Codable {
    var catId : Int
    var previewImgName : [String]
}

Crash logs :

malloc: can't allocate region
:*** mach_vm_map(size=311296, flags: 100) failed (error code=3)
(82620,0x106177880) malloc: *** set a breakpoint in malloc_error_break to debug
2021-07-01 18:33:06.934519+0530 [82620:5793991] [framework] CoreUI: vImageDeepmap2Decode() returned 0.
2021-07-01 18:33:06.934781+0530 [82620:5793991] [framework] CoreUI: CUIUncompressDeepmap2ImageData() fails [version 1].
2021-07-01 18:33:06.934814+0530 [82620:5793991] [framework] CoreUI: Unable to decompress 2.0 stream for CSI image block data. 'deepmap2'
(82620,0x106177880) malloc: can't allocate region
:*** mach_vm_map(size=311296, flags: 100) failed (error code=3)
(82620,0x106177880) malloc: *** set a breakpoint in malloc_error_break to debug

Note: All images of category are loading from the assets only and If I will use the static name of the image in the loop then there is no memory pressure and it will consume only 75 mb.

I think there is a image caching issue. Does I have to manage image caching even if I am loading images from assets?

Can anyone assist me to resolve this issue? Any help will be much appreciated. Thanks!!

enter image description here

Hardik Shekhat
  • 1,680
  • 12
  • 21
  • Instead of function *horizontalImgListView* (which generates and renders content each refresh) try to use separated view then SwiftUI could handle it in more robust way. – Asperi Jun 29 '21 at 05:34
  • Does not @ViewBuilder and separated view (struct) have same effect? – Hardik Shekhat Jun 29 '21 at 06:35
  • 2
    upload crash log with it. – Abu Ul Hassan Jun 30 '21 at 13:47
  • Can you make a small demo app and push to github? This way we can have a better look. For example, I created a basic demo app, but I dont know what kind of images we are dealing with here. – Sardorbek Ruzmatov Jun 30 '21 at 17:56
  • Apps rarely crash that quick due to memory leaking. Like it takes something crazy and substantial for it to crash. Can you edit the question and include the crash log? – mfaani Jun 30 '21 at 22:09
  • @Honey it taking the nearly 100 images which is high resolution and approx. 500 kb size of each image. – Hardik Shekhat Jul 01 '21 at 04:48
  • What do you mean by 'High Resolution'? Is the 500kb the size of the file or the size that the image will use in memory? If you had truly massive images then you could just be using all available RAM in just a few images. Is this on a simulator or a real device? – Upholder Of Truth Jul 01 '21 at 08:21
  • @UpholderOfTruth 500 kb is the size of the each image And its occurring in real device.. – Hardik Shekhat Jul 01 '21 at 08:54
  • 'Size of each image'. Does that mean the size of the image file or the total size of the image once it is being displayed as the two can be very different. For example I have an image file here that is 398Kb in size but when displayed requires 16Mb of memory. – Upholder Of Truth Jul 01 '21 at 11:08
  • Downvoted. Waiting for the crash log that you get in Xcode. FWIW you may not have memory leak. Only that you are using too much memory. Like if you leave the screen and come back, do you see the memory footprint of your app increasing? If that’s the case then yeah you have a memory leak. Otherwise you just have too many images in memory that will go away once you leave that screen. Have you profiled your app? – mfaani Jul 01 '21 at 11:50
  • @UpholderOfTruth 'Size of each image' means size of the image file, not memory size of what being displayed – Hardik Shekhat Jul 01 '21 at 12:55
  • @Honey I have updated question with crash logs. if I leave the screen then memory isn't increasing or decreasing but my main issue is app crashing when I am scrolling vertically or horizontally. So my main question how can I manage memory for images? – Hardik Shekhat Jul 01 '21 at 13:13
  • Then it's impossible to know how big the images are actually going to be in memory. So if they were large resolution ARGB type images even a few could start to use up all the RAM. – Upholder Of Truth Jul 01 '21 at 15:31

5 Answers5

1

Try LazyVGrid with only one column instead of using Foreach.

let columns = [GridItem(.flexible(minimum: Device.SCREEN_WIDTH - "Your horizontal padding" , maximum: Device.SCREEN_WIDTH - "Your horizontal padding"))]

ScrollView(.vertical ,showsIndicators: false ){
   LazyVGrid(columns: columns,spacing: 25, content: {
      ForEach(0..< dataContent.catData.count, id: \.self) { index in
         "Your View"
      }
   }
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Feb 28 '22 at 08:05
1

I faced the same problem when building the app using the SwiftUI framework. I fetched ~600 items from the server (200 ms), then tried to show it in UI using ForEach. But it took 3 GBs of RAM. After research, I understand that it's not an issue of SwiftUI. Memory issue happens because of the loop (for-loop).

I found the following:

In the pre-ARC Obj-C days of manual memory management, retain() and release() had to be used to control the memory flow of an iOS app. As iOS's memory management works based on the retain count of an object, users could use these methods to signal how many times an object is being referenced so it can be safely deallocated if this value ever reaches zero.

The following code stays at a stable memory level even though it's looping millions of times.

for _ in 0...9999999 {
    let obj = getGiantSwiftClass()
}

However, it's a different story if your code deals with legacy Obj-C code, especially old Foundation classes in iOS. Consider the following code that loads a big image ton of time:

func run() {
    guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
        return
    }
    for i in 0..<1000000 {
        let url = URL(fileURLWithPath: file)
        let imageData = try! Data(contentsOf: url)
    }
}

Even though we're in Swift, this will result in the same absurd memory spike shown in the Obj-C example! The Data init is a bridge to the original Obj-C [NSData dataWithContentsOfURL] -- which unfortunately still calls autorelease somewhere inside of it. Just like in Obj-C, you can solve this with the Swift version of @autoreleasepool; autoreleasepool without the @:

autoreleasepool {
    let url = URL(fileURLWithPath: file)
    let imageData = try! Data(contentsOf: url)
}

In your case, use autoreleasepool inside ForEach:

ForEach(0..<dataContent.catData.count, id: \.self) { count in
    autoreleasepool {
        // Code
    }
}

References:

Bilal Bakhrom
  • 151
  • 11
1

I've had an issue with this all day. The problem with LazyVGrid/LazyHGrid is that while the View's inside the ForEach loop are not created until needed, there is no automatic deallocation of the View after they scroll offscreen. This is counter intuitive because we'd expect it to work like UICollectionView but presently it just doesn't. So if a cell draws an Image then it will retain that image offscreen. It will also preserve any @State the cell was in. So if you store image data in a @State variable then that's sticking around as well.

My solution was to use .onDisappear to change the state of the cell as it scrolls offscreen. This way the cell saves it's state as something with a smaller memory footprint.

It doesn't feel ideal and I may end up just falling back on a UICollectionView but it does work.

Here's some demo code. In my case I'm loading an array of urls from disk.


struct ImageGridView: View {
   
   let urls: [URL]
   let columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 10), count: 4)
   
   var body: some View {
       ScrollView {
           LazyVGrid(columns: columns, spacing: 20) {
               ForEach(urls, id: \.self) { url in
                   URLImage(url: url)
               }
           }
           .padding()
       }
   }
}

struct URLImage: View {
   
   let url: URL
       
   @State private var isCellVisible:Bool = false
   
   var body: some View {
       Group {
           
           if isCellVisible {
               if let imgdata = FileHelper.syncRead(from: url),
                  let IMG = Image(data: imgdata) {
                   IMG
                       .resizable()
                       .scaledToFit()
               } else {
                   Image(systemName: "circle")
                       .resizable()
                       .scaledToFit()
               }
           }
           else {
               Image(systemName: "circle.hexagonpath.fill")
                   .resizable()
                   .scaledToFit()
           }
       }
       .onAppear {
           isCellVisible = true
       }
       .onDisappear {
           isCellVisible = false
       }
   }
}

Hope this helps, or ideally I hope Apple fixes LazyGrids.

0

Try not using explicit self in your ForEach. I've had some weird leaks in my SwiftUI views and switching to implicit self seemed to get rid of them.

0

Your main problem is that you're using a ScrollView/VStack vs using a List. List is like UITableView which intelligently only maintains content for cells that are showing. ScrollView doesn't assume anything about the structure and so everything within it is retained. The VStack being lazy only means that it doesn't allocate everything immediately. But as it scrolls to the bottom (or HStack to the side), the memory accumulates because it doesn't release the non visible items

You say you tried List, but what did that code look like? You should have replaced both ScrollView and LazyVStack.

Unfortunately, there is no horizonal list at this moment, so you'll either need to roll your own (perhaps based on UICollectionView), or just minimize the memory footprint of your horizonal rows.

What is the size of your images? Image is smart enough to not need to reload duplicate content: the reason why a single image literal works. But if you're loading different images, they'll all be retained in memory. Having said that, you should be able to load many small preview images. But it sounds like your source images may not be that small.

Eric Shieh
  • 697
  • 5
  • 11
  • 1
    yes I have tried with `List` and replaced it with `ScrollView` & `LazyVStack` but it giving me same results. I have used `List` for both horizontal (by applying rotation to List & vertical scrolling but there is no expected output. – Hardik Shekhat Jul 12 '21 at 10:13
  • "the memory accumulates because it doesn't release the non visible items" this is wrong, if you add onAppear and onDisappear you will see the cells that are no more visible will disappear – Sorin Lica Apr 13 '22 at 14:33
  • Disappearing is not the same as being released. Disappear means hiding a view or setting it's visible flag to false. Release means that the data structures representing that view are effectively destroyed and would need to be recreated if that view were to be displayed again. – Eric Shieh Apr 13 '22 at 18:45