Clear memory when reloading TextView with new text - iOS Swift - ios

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?

Related

SwiftUI Memory leak Issue in ForEach

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

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.

Accessing URL and Array from within a Block of JSON Data

Let's say I have JSON data structured in the following way:
{ "fruits" : {
"apple": {
"name": "Gala"
"color": "red",
"picture": "//juliandance.org/wp-content/uploads/2016/01/RedApple.jpg",
"noOfFruit": [1, 2]
}
}
How would I access the picture and noOfFruit array using the iOS version of Firebase? I want to make a table view with a cell that lists the apple's name, the color, a picture of the apple, and then lists the number of fruit. I get how to obtain the "color" and "name" values but how would I access the array and turn it into a string and the image so that it shows the image in the table view? Any help is appreciated!
For the array, it's really simple. Wherever you have your function that listenes for the firebase changes, I'll imagine that you have the info under the apple key stored in a variable like let apple
Then, you could cast the value of noOfFruit to an array, like the following:
let apple = // what you had before
guard let noOfFruit = apple["noOfFruit"] as? [Int] else {
return
}
//Here you have the array of ints called noOfFruit
For the image, theres several options out there. The first (and bad one) is to synchrounsly fetch the data of the url and set it to an image view as the following:
let url = URL(string: picture)
let data = try? Data(contentsOf: url!) //this may break due to force unwrapping, make sure it exists
imageView.image = UIImage(data: data!)
The thing with this approach is that it's NOT OK. It will block the main thread while its making the request and dowloading the image, leaving the app unresponsive.
The better approach would be to go fetch it asynchronously.There are several libraries that really help, such as AlamofireImage, but it can be done with barebones Foundation really easy. To do that, you should use URLSession class as the following:
guard let url = URL(string: picture) else {
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print(error)
return
}
//Remember to do UI updates in the main thread
DispatchQueue.main.async {
self.myImageView.image = UIImage(data: data!)
}
}.resume()

TableViewController search results don't load properly because of a lag

So I have an app that pulls up movie results when I type in a search. (Like IMDb.) I use a free API from themoviedb.org to load the results. I load them in a TableViewController. I load the posters for the results using a mod on the .dataTaskWithRequest method. to make it synchronous. Other than that, it's just basic API sending and receiving for the titles, genres, and years of the movies or TV Shows.
Now my app lags when I type too fast, this isn't completely because of the synchronous loading, because it still happens when I don't load images at all, but image loading makes the app lag, too. Now this is an issue in and of itself, but the problem is that when the app loads the words on to the screen, and is done with the lag, the results are the results of part of the word I have on screen. For example, if I type "The Simpsons" too fast, I get results for "The Sim", but if I backspace once, and retype "The Simpsons", the results reload correctly. Something that complicates things even more, is that sometimes I get the top result only being one of the old, partial results, and the rest are normal and loaded underneath.
Here is a video explaining the situation. The first time i type down "the simpsons", you can see the lag. I typed it all really fast, but it lags past the word "the". When it is done loading, it loads up a beowulf result that shouldn't even be there. I have no idea what's going on and it's driving me nuts. Even when I don't load images, and the typing doesn't lag, the results still don't update.
Here are the relevant code snippets, if you want any more, feel free to ask. I just don't want to bombard you with too much code at once:
This updates search results when text is typed in search bar:
extension SearchTable : UISearchResultsUpdating {
func updateSearchResultsForSearchController(searchController: UISearchController) {
//To Handle nils
var searchBarText = searchController.searchBar.text
if (searchBarText == nil) {
searchBarText = ""
}
searchBarText! = searchBarText!.condenseWhitespace()
//To Handle Disallowed Characters
searchBarText = searchBarText!.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
//Find Results from themoviedb
let urlString = "https://api.themoviedb.org/3/search/multi?query=" + searchBarText! + "&api_key= (I can't post the api key publicly online, sorry)"
let results = NSURL(string: urlString)
if (results == nil) {
//Server Error
}
//Wire Up Results with matchingItems Array
let task = NSURLSession.sharedSession().dataTaskWithURL(results!) { (data, response, error) -> Void in
if let jsonData = data {
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers)
if var results = jsonData["results"] as? [NSDictionary] {
if results.count > 0 {
//Clean out non-english results:
//I wrote the function, it shouldn't be the source of the lag, but I can still provide it.
self.cleanArray(&results)
self.matchingItems = results
} else {
self.matchingItems = []
}
}
} catch {
//JSON Serialization Error
}
}
}
task.resume()
self.tableView.reloadData()
}
}
Then, after I get the results, I reload the table using the two required methods from a TableViewDataSource:
//Table Data Source
extension SearchTable {
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return matchingItems.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell")! as! CustomCell
//Safe-Guard. This shouldn't be needed if I understood what I was doing
if (indexPath.row < matchingItems.count) {
cell.entry = matchingItems[indexPath.row] //404
//Name & Type & Year
//This is only for TV Shows, I removed the rest for simplicity
cell.title.text = matchingItems[indexPath.row]["name"] as? String
cell.icon.image = UIImage(named: "tv.png")
let date = (matchingItems[indexPath.row]["first_air_date"] as? String)
cell.year.text = date == nil ? "" : "(" + date!.substringToIndex(date!.startIndex.advancedBy(4)) + ")"
//Genre
//Code here removed for simplicity
//Poster
cell.poster.image = UIImage(named: "Placeholder.jpg")
if let imagePath = matchingItems[indexPath.row]["poster_path"] as? String {
let url = NSURL(string: "http://image.tmdb.org/t/p/w185" + imagePath)
let urlRequest = NSURLRequest(URL: url!)
let session = NSURLSession.sharedSession()
//Synchronous Request
let semaphore = dispatch_semaphore_create(0)
let task = session.dataTaskWithRequest(urlRequest) { data, response, error in
if let poster = UIImage(data: data!) {
cell.poster.image = poster
}
dispatch_semaphore_signal(semaphore)
}
task.resume()
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
}
return cell
}
}
Thanks!
First of all, I strongly recommend you don't use synchronous request, mainly because it blocks your UI until it finish and this is a very bad responsiveness for an app.
In your case you can place a placeholder for the UIImage and when the request finish substitute it for the correct image.
Regarding your issue of typing faster, it's called throttle or debounce, Apple recommends:
Performance issues. If search operations can be carried out very rapidly, it is possible to update the search results as the user is typing by implementing the searchBar:textDidChange: method on the delegate object. However, if a search operation takes more time, you should wait until the user taps the Search button before beginning the search in the searchBarSearchButtonClicked: method. Always perform search operations a background thread to avoid blocking the main thread. This keeps your app responsive to the user while the search is running and provides a better user experience.
But if you until want it to handle yourself you can see this two good answers explaining how to handle it correctly:
How to throttle search (based on typing speed) in iOS UISearchBar?
How can I debounce a method call?
I recommend you handle it as Apple recommends or you can change your philosophy and adopt some libraries that handle it for your automatically like:
Bond
RxSwift
The first one in more easy to learn, the second one needs to learn Reactive Programming and concepts of Functional Programming, It's up to you.
I hope this help you.
Just for people who may be struggling in the future with this same issue. First of all, read my comment to Victor Sigler's answer.
Here were the issues:
1 - I searched for the results online using .dataTaskWithURL() This is an asynchronous method which ran in the background while the code continued. So on occasion, the table would reload before the new results were in. See this thread for more information. I highly recommend checking this tutorial on concurrency if you are serious about learning Swift:
www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1
2 - The images lagged because of the search being synchronous, as Victor said. His answer pretty much handles the rest, so read it!

Repeat code for whole array

I am using some Facebook IDs in my app, and I have an array of serveral ID's, the array can be 10 numbers but can also be 500 numbers..
Right now the numbers are displayed in a tableview, and I want all the results there too, so they need to be in an array.
let profileUrl = NSURL(string:"http://www.facebook.com/" + newArray[0])!
let task = NSURLSession.sharedSession().dataTaskWithURL(profileUrl) {
(data, response, error) -> Void in
// Will happen when task completes
if let urlContent = data {
let webContent = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
dispatch_async(dispatch_get_main_queue(),
{ () -> Void in
let websiteArray = webContent!.componentsSeparatedByString("pageTitle\">")
//print(websiteArray[1])
let secondArray = websiteArray[1].componentsSeparatedByString("</title>")
print(secondArray[0])
})
}
}
this code takes the first number of the array, goes to facebook.com/[the actual number], and then downloads the data and splits the data into pieces, so that the data that I want it in the secondArray[0]. I want to do this for every number of the array, take the result data and put it back into an array. I have no idea how to do this because you don't know how much numbers there are gonna be etc, does someone has a good solution for this?
Any help would be appreciated, really!
Thanks
You have several problems here, and you should take them one at at a time to build up to your solution.
First, forget the table for the moment. Don't worry at all about how you're going to display these results. Just focus on getting the results in a simple form, and then you'll go back and convert that simple form into something easy to display, and then you'll display it.
So first, we want this in a simple form. That's a little bit complicated because it's all asynchronous. But that's not too hard to fix.
func fetchTitle(identifier: String, completion: (title: String) -> Void) {
let profileUrl = NSURL(string:"http://www.facebook.com/" + identifier)!
let task = NSURLSession.sharedSession().dataTaskWithURL(profileUrl) {
(data, response, error) -> Void in
if let urlContent = data {
let webContent = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
let websiteArray = webContent!.componentsSeparatedByString("pageTitle\">")
let secondArray = websiteArray[1].componentsSeparatedByString("</title>")
let title = secondArray[0]
completion(title: title)
}
}
task.resume()
}
Now this is still pretty bad code because it doesn't handle errors at all, but it's a starting point, and the most important parts are here. A function that takes a string, and when it's done fetching things, calls some completion handler.
(Regarding error handling, note how many places this code would crash if it were returned surprising data. Maybe the data you get isn't a proper string. Maybe it's not formatted like you think it is. Every time you use ! or subscript an array, you run the risk of crashing. Try to minimize those.)
So you might then wrap it up in something like:
var titles = [String]()
let identifiers = ["1","2","3"]
let queue = dispatch_queue_create("titles", DISPATCH_QUEUE_SERIAL)
dispatch_apply(identifiers.count, queue) { index in
let identifier = identifiers[index]
fetchTitle(identifier) { title in
dispatch_async(queue) {
titles.append(title)
}
}
}
This is just code to get you on the right track and start studying the right things. It certainly would need work to be production quality (particularly to handle errors).
Once you have something that returns your titles correctly, you should be able to write a program that does nothing but take a list of identifiers and prints out the list of titles. Then you can add code to integrate that list into your tableview. Keep the parts separate. The titles are the Model. The table is the View. Read up on the Model-View-Controller paradigm, and you'll be in good shape.
To repeat code for whole array put your code in a loop and run that loop from 0 to array.count-1
You don't need to know how many items there will be an array. You can just get the count at run time array.count here array is your array.
I hope this is what you wanted to know, your question doesn't make much sense though.

Resources