SwiftUI Memory leak Issue in ForEach - ios

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!!

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:
https://stackoverflow.com/a/25880106/11009631
https://swiftrocks.com/autoreleasepool-in-swift

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.

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.

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

Related

Mock Core Data object in SwiftUI Preview

Xcode's preview canvas keeps on crashing with no error message when I try to pass in a preview Core Data object like so:
import SwiftUI
import CoreData
struct BookView: View {
let book: Book
var body: some View {
Text("Hello, World!")
}
}
// ^^^ This stuff is fine ^^^
// vvv This stuff is not vvv
struct BookView_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let book = Book(context: moc)
book.title = "Test book"
book.author = "Test author"
book.genre = "Fantasy"
book.rating = 4
book.review = "This was a great book; I really enjoyed it."
return NavigationView {
BookView(book: book)
}
}
}
I'm following a Hacking with Swift tutorial on Core Data and SwiftUI and am at this step.
This appears to be the standard way to add preview objects into the SwiftUI canvas, but I'm unable to get it to work. FYI the app runs fine in the simulator, I'm just trying to get it to also work in the preview canvas. I'm using Xcode 13.2.1 on macOS 12.
Thank you!
Instead of creating a NSManagedObjectContext use
static let context = PersistenceController.preview.container.viewContext
That variable is provided in the standard Xcode project with Core Data.
Also, if you have been using the real store for preview it might be corrupted somehow so you might have to destroy.
Add the code below
do{
try container.persistentStoreCoordinator.destroyPersistentStore(at: container.persistentStoreDescriptions.first!.url!, type: .sqlite, options: nil)
}catch{
print(error)
}
Right under
container = NSPersistentCloudKitContainer(name: "YourAppName")
Before you load the store.
This destroys the store and then it gets recreated when you call loadPersistentStores be sure to remove that piece of code after you clear the preview device so you don't accidentally destroy another store you don't mean to destroy.

Inconsistent Widget Behaviour Between Devices in iOS 14

I recently launched an iOS 14 Widget for my app. In testing everything seemed a-okay but on launch I am having inconsistent behaviour. On some devices the widget loads as just black, on other devices it only loads the placeholder content and on some devices it works as intended. The behaviour exhibited, good or bad, is consistent in the Add Widget Screen and when it's added to the Home Screen. On the devices where my widget does not work, other widgets do work.
The content in the Widget only changes when something is changed in the app thus I call WidgetCenter.shared.reloadAllTimelines() in the SceneDelegate when sceneWillResignActive is called. The expected behaviour is that when the user backgrounds the application the widget will update. On the devices that show black or placeholder content this Widget Center Update does not work, on the devices where it does work the update function works as expected.
This is the code for my Widget:
struct ThisWeekProvider: TimelineProvider {
func placeholder(in context: Context) -> ThisWeekEntry {
return ThisWeekEntry(date: Date(), thisWeekJSON: getDefaultTimeSummaryJSON())
}
func getSnapshot(in context: Context, completion: #escaping (ThisWeekEntry) -> ()) {
var thisWeekData = TimeSummaryJSON()
if context.isPreview {
thisWeekData = getThisWeekData()
} else {
thisWeekData = getThisWeekData()
}
let entry = ThisWeekEntry(date: Date(), thisWeekJSON: thisWeekData)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let entries: [ThisWeekEntry] = [ThisWeekEntry(date: Date(), thisWeekJSON: getThisWeekData())]
let timeline = Timeline(entries: entries, policy: .after(entries[0].thisWeekJSON.endDate))
completion(timeline)
}
}
struct ThisWeekEntry: TimelineEntry {
let date: Date
let thisWeekJSON: TimeSummaryJSON
}
struct ThisWeekWidgetEntryView : View {
var entry: ThisWeekProvider.Entry
var body: some View {
// Generate View
// Use data from 'entry' to fill widget
}
struct ThisWeekWidget: Widget {
let kind: String = K.thisWeekWidget
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: ThisWeekProvider()) { entry in
ThisWeekWidgetEntryView(entry: entry)
}
.configurationDisplayName("this_week_widget".localized())
.description("this_week_description".localized())
.supportedFamilies([.systemSmall])
}
}
The custom data type 'TimeSummaryJSON' is as follows:
struct TimeSummaryJSON: Codable {
var labourIncome: String = "$0.00"
var equipmentIncome: String = "$0.00"
var days: String = "0"
var hours: String = "0"
var endDate: Date = Date()
var settingsFirstDayOfWeek: String = K.monday
var localeFirstDayOfWeek: Bool = false
}
The custom function that retrieves the data 'getThisWeekData()' is as follows:
private func getThisWeekData() -> TimeSummaryJSON {
if let encodedData = UserDefaults(suiteName: AppGroup.shared.rawValue)!.object(forKey: K.thisWeek) as? Data {
if let thisWeekJSON = try? JSONDecoder().decode(TimeSummaryJSON.self, from: encodedData) {
return checkExpiryForCurrentWeek(thisWeekJSON)
} else {
print("Decoding Error - Return Default This Week")
return getDefaultTimeSummaryJSON()
}
} else {
print("No Shared Data - Return Default This Week")
return getDefaultTimeSummaryJSON()
}
}
The process of saving and retrieving the data works like this:
SceneDelegate calls sceneWillResignActive
Data is pulled from the Local Realm Database, calculated and saved into a TimeSummaryJSON
TimeSummaryJSON is encoded and saved to a shared AppGroup
WidgetCenter.shared calls reloadAllTimelines()
Widget decodes JSON data from AppGroup
If the JSON Decode is successful the current user data is shown in the widget, if the JSON Decode fails a default TimeSummaryJSON is sent instead
I've looked over my code quite extensively and read countless forums and it seems that I am doing everything correctly. Have I missed something? Could anyone suggest why the behaviour is inconsistent between devices? I'm well and truly stuck and am not sure what to try next.
Any help you can offer would be kindly appreciated.
Thank you!
I finally got to the bottom of my issue thanks to some help on reddit. The problem was an image I was using was too large in resolution, the large file size capped out the 30MB Memory Limit of Widgets.
The reddit user that helped me had this to say:
'Black or redacted widget means that during loading you hit the 30 mb memory limit.
Also if you're reloading more than one widget at a time, it seems that there's a higher chance you'll hit the memory budget.'
'Forgot to mention that black/redacted widget can also happen if the widget crashes (not necessary due to memory budget). So maybe you have some threading problems or are force unwrapping something that is nil.'
This is the link to the full discussion:
https://www.reddit.com/r/iOSProgramming/comments/mmo5lf/inconsistent_widget_behaviour_between_devices_in/

Running out of memory in a for loop swift (4)

I'm looping through all pages in a PDFDocument (200+ pages) but app crashes with
Message from debugger: Terminated due to memory issue
The pdf is approx 4mb in size yet each iteration of the loop jumps the memory up approx 30mb. Which doesn't seem right to me. I have managed to locate where in my code the memory is being used just not sure how to claim it back. Tried setting variables to nil but no effect. Tried code in the for loop in an autoreleaspool{} but no effect.
#objc func scrapePDF(){
let documentURL = self.documentDisplayWebView!.url!
let document = PDFDocument(url: documentURL)
let numberOfPages = document!.pageCount
DispatchQueue.global().async {
for pageNumber in 1...numberOfPages {
print(document?.page(at: pageNumber)!.string!)
}
}
}
UPDATE: solved ..... kind of
Playing around a bit I found that rather than passing a reference to the PDFDocument inside the loop, if instead I create a new instance for each loop this strangely solves the memory issue. I don't quite understand why though. PDFDocument is a Class not a Struct so is passed by reference. Meaning it is only created once and then referenced to inside my loop. So why would it cause a memory issue?
#objc func scrapePDF(){
let documentURL = self.documentDisplayWebView!.url!
let document = PDFDocument(url: documentURL)
let numberOfPages = document!.pageCount
DispatchQueue.global().async {
for pageNumber in 1...numberOfPages {
let doc = PDFDocument(url: documentURL)
print(doc?.page(at: pageNumber)!.string!)
}
}
}
Though the above code clears the memory issue the problem with it is that its too slow. Each loop takes 0.5 seconds and with 300+ pages I can't accept that. Any tips on speeding it up? Or why it doesn't give the memory back if referencing the PDFDocument from outside the loop
Further UPDATE.
It seems that it’s calling the .string method of the PDFPage that is increases the memory to the point of crashing.

Clear memory when reloading TextView with new text - iOS Swift

I have a function below that changes the text of a text view by getting the contents of a file stored in the main bundle:
func setUpText() {
let path = Bundle.main.path(forResource: book, ofType: "txt")
var rawText = ""
do {
rawText = try String(contentsOfFile: path!, encoding: String.Encoding.utf8)
} catch {
print("Contents of File could not be retrieved")
}
myTextView.text = rawText
}
When the user changes the value of book, this function is called and the text view is populated with the new text. Everything works fine, but I've noticed in the Debug Navigator which shows CPU, Memory, Disk, and Network usage information, that Memory keeps going up every time I repopulate the text. How can I clear the memory before changing the text?
Here's how setUpText is called in viewDidLoad:
aiv.startAnimating()
myTextView.text = ""
DispatchQueue.main.async {
self.setUpText()
self.aiv.stopAnimating()
self.aiv.isHidden = true
}
I'm also calling setUpText in the function below in the dropdownMenu.didSelectItemAtIndexHandler closure. This is the source of my problem, when I add this, the memory climbs without ever going down as verified through Instrumenting.
func createDropdownMenu(title: String, items: [AnyObject]) -> BTNavigationDropdownMenu {
let dropdownMenu = BTNavigationDropdownMenu(navigationController: self.navigationController,
containerView: self.navigationController!.view,
title: title,
items: items)
dropdownMenu.didSelectItemAtIndexHandler = { (indexPath: Int) -> () in
self.chapterIndex = indexPath + 1
self.setUpText()
}
return dropdownMenu
}
I don't see anything that would cause a retain cycle...self doesn't have a strong reference to the closure, so you're fine there.
You can't 'clear memory', per se, under ARC. It won't allow you to manually deallocate an object (nor do you need to--that's the point). My best guess is that because you're reading a string from disk, which is a relatively slow/expensive operation, (emphasis on 'relatively') the system might be caching it at least temporarily. This happens in certain cases such [UIImage imageNamed:].
Is this actually causing a problem for you? Have you instrumented it at all?

ios swift - unreleased memory when appending uiimage to array

I'm appending uiimages from core data to an array in order to create a gif of them.
I'm creating the gif and then emptying the array; however, I still have an indefinite 50 mb memory allocation from the moment I create the gif.
I tried looking at instruments and this is what I'm getting in the call tree:
Call tree image
This is the code:
do{
let objects = try managedObjectContext.executeFetchRequest(request)
let results = objects
if results.count > 0 {
for var i = 0; i < results.count; i += 1{
let match = results[i] as! cFW
date.append(match.date)
let image = match.image
fetchedImage.append(UIImage(data: image)!)
}
} else {
}
}
catch{}
Even after deleting the gif and the array already being deleted, the app stays at 50mb of memory usage.
Thank you
edit: The issue that I'm having has to do with how I'm displaying the gif (in a webview). I will update the question with my solution asap
The issue was happening due to presenting the gif in a webview.
Clearing the cache of web view did not resolve the allocation of memory.
I ended up using a library to FLAnimatedImage to present my GIF.
No more unreleased memory.
Thank you guys

Resources