Swift Memory Leaks with UIDiffableDataSource, CFString, Malloc Blocks, and others - ios

I've been writing an application for a couple months now and just started checking for memory leaks - it turns out I have a lot of them - 25 unique types (purple exclamation point) and over 500 total if I navigate through my app enough.
The Memory Graph in Xcode is pointing mostly to 1.) different elements of UIDiffableDataSource, 2.) "CFString"/"CFString (Storage)", and 3.) "Malloc Blocks". The Leaks Instrument is giving me similar feedback, saying about 90% of my memory leaks are these "Malloc Blocks" and also says the Responsible Frame is either newJSONString or newJSONValue. In many of the leaks there is a "4-node-cycle" involved which contains a Swift Closure Context, My UIDiffableDataSource<Item, Section> object, and __UIDiffableDataSource. The CFString ones just have one object - CFString, and nothing else. I'll try to add images showing the 2 examples, but StackO is restricting my ability to add them.
This leads me to believe I'm creating some type of memory leak within my custom dataSource closure for the UICollectionViewDiffableDataSource. I've tried making the DataSource a weak var & I've tried using weak self and unowned self in each of my closures -- especially the closure creating my datasource, but it's had no impact.
What does it mean when Memory Graph/Leaks Instrument point to these generic "CFString" or "Malloc Block" objects?
Does anyone have an idea of what might actually be causing these memory leaks and how I can resolve them?
Do memory graph and Leaks instrument ever false report leaks/create unnecessary noise or are these legitimate?
any additional info or additional resources would be super helpful. So far I've found iOS Academy & Mark Moeykens YouTube videos helpful in understanding the basics, but having trouble applying it to my app. I can provide code blocks if that helps, but there is a lot of code that could be causing it and not really sure what all to dump in here.
overview of errors
4 node cycle (diffableDataSource)
CFString (Storage)
I was able to find some additional info after posting this. Based on this post I was able to use the Backtrace pane and 95% of the memory leaks are pointing back to my createDataSource() and my applySnapshotUsing(sectionIDs, itemsBySection) methods [dropped those below].
I feel like I've figured out WHERE the leaks are originating, but still stumped on HOW or IF I should fix the leaks... I've tried making closures 'weak self' and any possible variables as weak, to no avail. Any help would be appreciated :)
Backtrace1
Backtrace2
func applySnapshotUsing(sectionIDs: [SectionIdentifierType], itemsBySection: [SectionIdentifierType: [ItemIdentifierType]],animatingDifferences: Bool, sectionsRetainedIfEmpty: Set<SectionIdentifierType> = Set<SectionIdentifierType>()) {
var snapshot = NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>()
for sectionID in sectionIDs {
guard let sectionItems = itemsBySection[sectionID], sectionItems.count > 0 || sectionsRetainedIfEmpty.contains(sectionID) else { continue }
snapshot.appendSections([sectionID])
snapshot.appendItems(sectionItems, toSection: sectionID)
}
self.apply(snapshot, animatingDifferences: animatingDifferences)
}
func createDataSource() -> DataSourceType {
//use DataSourceType closure provided by UICollectionViewDiffableDataSource class to setup each collectionViewCell
let dataSource = DataSourceType(collectionView: collectionView) { [weak self] (collectionView, indexPath, item) in
//loops through each 'Item' in itemsBySection (provided to the DataSource snapshot) and expects a UICollectionView cell to be returned
//unwrap self
guard let self = self else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SeeMore", for: indexPath)
return cell
}
//figure out what type of ItemIdentifier we're dealing with
switch item {
case .placeBox(let place):
//most common cell -> will display an image and Place name in a square box
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Restaurant", for: indexPath) as! RestaurantBoxCollectionViewCell
//Place object stored in each instance of ViewModel.Item and can be used to configure the cell
cell.placeID = place.ID
cell.segment = self.model.segment
//fetch image with cells fetchImage function
//activity indicator stopped once the image is returned
cell.imageRequestTask?.cancel()
cell.imageRequestTask = nil
cell.restaurantPic.image = nil
cell.fetchImage(imageURL: place.imageURL)
//image task will take time so animate an activity indicator to show activity in progress
cell.activityIndicator.isHidden = false
cell.activityIndicator.startAnimating()
cell.restaurantNameLabel.text = place.name
//setup long press gesture recognizer - currently contains 1 context menu item (<3 restaurant)
let lpgr : UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress))
lpgr.minimumPressDuration = 0.5
lpgr.delegate = cell as? UIGestureRecognizerDelegate
lpgr.delaysTouchesBegan = true
self.collectionView?.addGestureRecognizer(lpgr)
//return cell for each item
return cell
case .header:
//top cell displaying the city's header image
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Header", for: indexPath) as! CityHeaderCollectionViewCell
self.imageRequestTask = Task {
if let image = try? await ImageRequest(path: self.city.headerImageURL).send() {
cell.cityHeaderImage.image = image
}
self.imageRequestTask = nil
}
return cell
}
}
dataSource.supplementaryViewProvider = { (collectionView, kind, indexPath) in
//Setup Section headders
let header = collectionView.dequeueReusableSupplementaryView(ofKind: "SectionHeader", withReuseIdentifier: "HeaderView", for: indexPath) as! NamedSectionHeaderView
//get section from IndexPath
let section = dataSource.snapshot().sectionIdentifiers[indexPath.section]
//find the section we're currently in & set the section header label text according
switch section {
case .genreBox(let genre):
header.nameLabel.text = genre
case .neighborhoodBox(let neighborhood):
header.nameLabel.text = "The best of \(neighborhood)"
case .genreList(let genre):
header.nameLabel.text = genre
case .neighborhoodList(let neighborhood):
header.nameLabel.text = "The best of \(neighborhood)"
default:
print(indexPath)
header.nameLabel.text = "favorites"
}
return header
}
return dataSource
}

Related

UITableViewCell - hiding unused views or not even adding them?

I have a simple question for which I haven't found anything related online.
What is more practical?
Having a TableViewCell that has more than 10+ subviews (inside of a stackview), but generally only 3-4 is shown, others are hidden.
Or having an empty stackview, and adding every view by code if needed.
For example for the second option:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell : PostCell
guard let post = PostsStore.shared.posts.getPost(index: indexPath.row) else {
return UITableViewCell()
}
// Remove reused cell's stackview's views
cell.stackView.subviews.forEach({ $0.removeFromSuperview() })
if post.voteAddon != nil {
if let voteView = Bundle.main.loadNibNamed("VoteView", owner: self, options: nil)?.first as? VoteView {
voteView.voteForLabel = "Vote for X or Y"
voteView.buttonX.setTitle("\(post.votes.x) votes", for: .normal)
voteView.buttonY.setTitle("\(post.votes.y) votes", for: .normal)
cell.stackView.addArrangedSubview(voteView)
}
}
if post.attachments != nil {
if let attachmentsView = Bundle.main.loadNibNamed("AttachmentsView", owner: self, options: nil)?.first as? AttachmentsView {
attachmentsView.attachmentImageView.image = UIImage()
cell.stackView.addArrangedSubview(attachmentsView)
}
}
cell.delegate = self
return cell
}
This is just an example, in reality those views are more complex.
What would be more practical? The first option is easier, but the second might be better for memory handling.. What are your thoughts about this?
Having a TableViewCell that has more than 10+ subviews (inside of a stackview), but generally only 3-4 is shown, others are hidden
3-4 shown means that 7+ - 6+ are hidden but still in memory , so i prefer instant processing load of the Add/Remove to the permanent memory existence when the cell is visible
It's also worth saying that this heavily depend on the number of visible cells at a time for e.x if it's full screen then option 1 is better than 2 because cells are dequeued as number of visible cells increase option 2 becomes more better

Data From Label in CollectionViewCell Sometimes Refreshes on Reload other times it Doesn't

First let me say this seems to be a common question on SO and I've read through every post I could find from Swift to Obj-C. I tried a bunch of different things over the last 9 hrs but my problem still exists.
I have a vc (vc1) with a collectionView in it. Inside the collectionView I have a custom cell with a label and an imageView inside of it. Inside cellForItem I have a property that is also inside the the custom cell and when the property gets set from datasource[indePath.item] there is a property observer inside the cell that sets data for the label and imageView.
There is a button in vc1 that pushes on vc2, if a user chooses something from vc2 it gets passed back to vc1 via a delegate. vc2 gets popped.
The correct data always gets passed back (I checked multiple times in the debugger).
The problem is if vc1 has an existing cell in it, when the new data is added to the data source, after I reload the collectionView, the label data from that first cell now shows on the label in new cell and the data from the new cell now shows on the label from old cell.
I've tried everything from prepareToReuse to removing the label but for some reason only the cell's label data gets confused. The odd thing is sometimes the label updates correctly and other times it doesn't? The imageView ALWAYS shows the correct image and I never have any problems even when the label data is incorrect. The 2 model objects that are inside the datasource are always in their correct index position with the correct information.
What could be the problem?
vc1: UIViewController, CollectionViewDataSource & Delegate {
var datasource = [MyModel]() // has 1 item in it from viewDidLoad
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}
// delegate method from vc2
func appendNewDataFromVC2(myModel: MyModel) {
// show spinner
datasource.append(myModel) // now has 2 items in it
// now that new data is added I have to make a dip to fb for some additional information
firebaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let dict = snapshot.value as? [String: Any] else { }
for myModel in self.datasource {
myModel.someValue = dict["someValue"] as? String
}
// I added the gcd timer just to give the loop time to finish just to see if it made a difference
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.datasource.sort { return $0.postDate > $1.postDate } // Even though this sorts correctly I also tried commenting this out but no difference
self.collectionView.reloadData()
// I also tried to update the layout
self.collectionView.layoutIfNeeded()
// remove spinner
}
})
}
}
CustomCell Below. This is a much more simplified version of what's inside the myModel property observer. The data that shows in the label is dependent on other data and there are a few conditionals that determine it. Adding all of that inside cellForItem would create a bunch of code that's why I didn't update the data it in there (or add it here) and choose to do it inside the cell instead. But as I said earlier, when I check the data it is always 100% correct. The property observer always works correctly.
CustomCell: UICollectionViewCell {
let imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
let priceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var someBoolProperty = false
var myModel: MyModel? {
didSet {
someBoolProperty = true
// I read an answer that said try to update the label on the main thread but no difference. I tried with and without the DispatchQueue
DispatchQueue.main.async { [weak self] in
self?.priceLabel.text = myModel.price!
self?.priceLabel.layoutIfNeeded() // tried with and without this
}
let url = URL(string: myModel.urlStr!)
imageView.sd_setImage(with: url!, placeholderImage: UIImage(named: "placeholder"))
// set imageView and priceLabel anchors
addSubview(imageView)
addSubview(priceLabel)
self.layoutIfNeeded() // tried with and without this
}
}
override func prepareForReuse() {
super.prepareForReuse()
// even though Apple recommends not to clean up ui elements in here, I still tried it to no success
priceLabel.text = ""
priceLabel.layoutIfNeeded() // tried with and without this
self.layoutIfNeeded() // tried with and without this
// I also tried removing the label with and without the 3 lines above
for view in self.subviews {
if view.isKind(of: UILabel.self) {
view.removeFromSuperview()
}
}
}
func cleanUpElements() {
priceLabel.text = ""
imageView.image = nil
}
}
I added 1 breakpoint for everywhere I added priceLabel.text = "" (3 total) and once the collectionView reloads the break points always get hit 6 times (3 times for the 2 objects in the datasource).The 1st time in prepareForReuse, the 2nd time in cellForItem, and the 3rd time in cleanUpElements()
Turns out I had to reset a property inside the cell. Even though the cells were being reused and the priceLabel.text was getting cleared, the property was still maintaining it's old bool value. Once I reset it via cellForItem the problem went away.
10 hrs for that, smh
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.someBoolProperty = false
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}

UITableViewCell containing a UICollectionView using RxSwift

I started using RxSwift in my iOS project and I have a UITableView with a custom UITableViewCell subclass. Within that subclass, I have a UICollectionView.
Populating the tableview using RxSwift works pretty flawlessly, I'm using another extension of RxSwift for that (RxDataSources)
Here's how I'm doing it:
self.dataSource = RxTableViewSectionedReloadDataSource<Section>(configureCell: {(section, tableView, indexPath, data) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCellWithCollectionView
switch indexPath.section {
case 0:
cell.collectionViewCellNibName = "ContactDataItemCollectionViewCell"
cell.collectionViewCellReuseIdentifier = "contactDataItemCellIdentifier"
case 1, 2:
cell.collectionViewCellNibName = "DebtCollectionViewCell"
cell.collectionViewCellReuseIdentifier = "debtCellIdentifier"
default:
break
}
cell.registerNibs(indexPath.section, nativePaging: false, layoutDirection: indexPath.section != 0 ? .horizontal : .vertical)
let cellCollectionView = cell.collectionView!
data.debts.asObservable().bind(to: cellCollectionView.rx.items(cellIdentifier: "debtCellIdentifier", cellType: DebtCollectionViewCell.self)) { row, data, cell in
cell.setup(debt: data)
}
return cell
})
This actually works. But the problem arises when a tableview cell gets scrolled off the screen and reappears. This triggers the code block from above and lets the app crash when
data.debts.asObservable().bind(to: cellCollectionView.rx.items(cellIdentifier: "debtCellIdentifier", cellType: DebtCollectionViewCell.self)) { row, data, cell in
cell.setup(debt: data)
}
is invoked twice on the same tableview cell (The funny thing is, even Xcode crashes without any trace).
What can I do to avoid this?
EDIT:
I have found a solution, but I must admit that I'm not really happy with it... Here's the idea (tested and works)
I define another Dictionary in my class:
var boundIndizes = [Int: Bool]()
and then I make an if around the binding like this:
if let bound = self.boundIndizes[indexPath.section], bound == true {
//Do nothing, content is already bound
} else {
data.debts.asObservable().bind(to: cellCollectionView.rx.items(cellIdentifier: "debtCellIdentifier", cellType: DebtCollectionViewCell.self)) { row, data, cell in
cell.setup(debt: data)
}.disposed(by: self.disposeBag)
self.boundIndizes[indexPath.section] = true
}
But I cannot believe there is no "cleaner" solution
The issue is you are binding your data.debts to the collectionView within the cell every time the cell is dequeued.
I would recommend you move the logic that is related to your data.debts to the cell itself and declare a var disposeBag: DisposeBag which you reinstantiate on prepareForReuse. See this answer for reference.

UITableView mess with the rows content

I tried to copy only the necessary code to show my problem. I have a tableview with dynamic content. I created a prototype cell and it has a user name and 10 stars (it's a rating page). People in the group are allowed to rate other people. Everything is working ok, but I have a problem when I scroll down. If I rate my first user with 8 stars, when I scroll down then some user that was in the bottom area of the tableview, appears with the rate that I gave to my first user. I know that tableview reuse cells. I tried many things but with no success. Hope someone can help me on that.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let model = users[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("RatingCell") as! RatingTableViewCell
cell.tag = indexPath.row
cell.playerLabel.text = model.name
cell.averageView.layer.borderWidth = 1
cell.averageView.layer.borderColor = Color.Gray1.CGColor
cell.averageView.layer.cornerRadius = 5
cell.starsView.userInteractionEnabled = true
cell.averageLabel.text = "\(user.grade)"
for i in 0...9 {
let star = cell.starsView.subviews[i] as! UIImageView
star.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(starTap)))
star.userInteractionEnabled = true
star.tag = i
star.image = UIImage(named: (i + 1 <= grade ? "star-selected" : "star-empty"))
}
return cell
}
func changeRating(sender: UIImageView) {
let selectedStarIndex = sender.tag
let cell = sender.superview?.superview?.superview as! RatingTableViewCell
let model = users[cell.tag]
let stars = sender.superview?.subviews as! [UIImageView]
cell.averageLabel.text = "\(selectedStarIndex + 1)"
for i in 0...9 {
let imgName = i <= selectedStarIndex ? "star-selected" : "star-empty"
stars[i].image = UIImage(named: imgName)
}
}
func starTap(gesture: UITapGestureRecognizer) {
changeRating(gesture.view as! UIImageView)
}
The way to solve this problem is by updating the model that holds all the information for the uitableviewcell. Whenever a rating is updated fora particular cell, make sure you reflect that update in the respective object / dictionary in an array. Furthermore, if you have a customuitableviewcell, it might be a good idea to reset the stars in the "prepareForUse" function, so that way when a cell is reused it doesn't use old data.
In your comments, you said that you have an array with selected rates.But you did not show that in your code.
In my opinion, you need record indexPath too, because indexPath.row is binding with your rate data(may be grade?).The best way to do so is that #Jay described up.And you should not write the code of configuring cell data and cell's logic in your view controller.If your business logic is complex, you will find that it is a nightmare.^=^

UITableViewCell button tags return desired IndexPath in UISearchController, not FetchedResultsController

I implemented an NSFetchedResultsController on a UITableView in a Core Data project in Swift 2.0. Additionally, I have a UISearchController implemented. Everything works perfectly with the exception of the behavior I'm encountering on my custom UITableViewCell buttons.
When UISearchController is active, the customTableViewCell's buttons work as they should. If I click the same button when the fetchedResultsController is displaying its results, the method thinks Index 0 is the sender, regardless of which button I click.
func playMP3File(sender: AnyObject) {
if resultsSearchController.active {
// ** THIS WORKS **
// get a hold of my song
// (self.filteredSounds is an Array)
let soundToPlay = self.filteredSounds[sender.tag]
// grab an attribute
let soundFilename = soundToPlay.soundFilename as String
// feed the attribute to an initializer of another class
mp3Player = MP3Player(fileName: soundFilename)
mp3Player.play()
} else {
// ** THIS ALWAYS GETS THE OBJECT AT INDEX 0 **
let soundToPlay = fetchedResultsController.objectAtIndexPath(NSIndexPath(forRow: sender.tag, inSection: (view.superview?.tag)!)) as! Sound
// OTHER THINGS I'VE TRIED
// let soundToPlay = fetchedResultsController.objectAtIndexPath(NSIndexPath(forRow: sender.indexPath.row, inSection: (view.superview?.tag)!)) as! Sound
// let soundToPlay: Sound = fetchedResultsController.objectAtIndexPath(NSIndexPath(index: sender.indexPath.row)) as! Sound
let soundFilename = soundToPlay.soundFilename as String
mp3Player = MP3Player(fileName: soundFilename)
mp3Player.play()
}
}
Here's an abbreviated version of my cellForRowAtIndexPath to show I'm setting up the cells' buttons:
let customCell: SoundTableViewCell = tableView.dequeueReusableCellWithIdentifier("customCell", forIndexPath: indexPath) as! SoundTableViewCell
if resultsSearchController.active {
let sound = soundArray[indexPath.row]
customCell.playButton.tag = indexPath.row
} else {
let sound = fetchedResultsController.objectAtIndexPath(indexPath) as! Sound
customCell.playButton.tag = indexPath.row
}
// add target actions for cells
customCell.playButton.addTarget(self, action: "playMP3file:", forControlEvents: UIControlEvents.TouchUpInside)
I've tried a few other approaches I've found here, such as translating CGPoints to IndexPaths, etc. without much luck. Everything that looked promising in the compiler crashed when I clicked the button in the simulator.
Thank you for reading.
Update
Installed Xcode 7.1, rebooted, cleaned caches, nuked derived data, did a cold boot.
Solution
Tags will get the job done in many cases (such as getting the location in an Array) and get lots of votes here, but as I've learned, they don't work all the time. Thank you to Mundi for pointing me towards a more robust solution.
// this gets the correct indexPath when resultsSearchController is not active
let button = sender as! UIButton
let view = button.superview
let cell = view?.superview as! SoundTableViewCell
let indexPath: NSIndexPath = self.tableView.indexPathForCell(cell)!
let soundToPlay = fetchedResultsController.objectAtIndexPath(indexPath) as! Sound
I've tried a few other approaches I've found here, such as translating CGPoints to IndexPaths, etc. without much luck.
Translating points is indeed the most robust solution. This answer contains the correct code.

Resources