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
Related
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
}
Here is my code in cell for row method:-
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeue(.journalListingCell, for: indexPath) as! JournalListViewCell
cell.delegate = self
//fetching the category model as per the category id
if let category = categories?.first(where: { $0.categoryID == dreamJournalArray[indexPath.row].categoryID }) {
cell.configureCell(dreamJournalArray[indexPath.row], category)
}
return cell
}
This code is just matching a category id from a array of categories to the category id in the main data model and passing along that category to the configure cell model along with the data model.
The configure cell method is as follows:-
typealias Model = DreamJournalModel
func configureCell(_ model: Model, _ category: CategoryModel) {
//setting the gradient colors
verticalView.backgroundColor = category.categoryGradientColor(style: .topToBottom, frame: verticalView.frame, colors: [UIColor.init(hexString: category.initialGradientColor)!, UIColor.init(hexString: category.lastGradientColor)!])
horizontalVIew.backgroundColor = category.categoryGradientColor(style: .leftToRight, frame: self.frame, colors: [ UIColor.init(hexString: category.lastGradientColor)!, UIColor.init(hexString: category.initialGradientColor)!])
timelineCircleView.backgroundColor = category.categoryGradientColor(style: .topToBottom, frame: timelineCircleView.frame, colors: [UIColor.init(hexString: category.initialGradientColor)!, UIColor.init(hexString: category.lastGradientColor)!])
// setting the intention matching image as per the rating
intentionImageView.image = UIImage.init(named: "Match\(model.intentionMatchRating)")
// setting up the date of the journal recorded
let date = Date(unixTimestamp: model.createdAt!)
dateMonthLabel.text = (date.monthName(ofStyle: .threeLetters)).uppercased()
dateNumberLabel.text = String(date.day)
//setting the titles and labels
dreamTitleLabel.text = model.title
dreamTagsLabel.text = model.tags
// setting the lucid icon
gotLucidImageview.isHidden = !(model.isLucid!)
//setting the buttons text or image
if model.recordingPath != nil {
addRecordButton.setTitle(nil, for: .normal)
addRecordButton.backgroundColor = UIColor.clear
addRecordButton.layer.cornerRadius = 0
addRecordButton.setImage(LRAsset.cardRecording.image, for: .normal)
}
else{
addRecordButton.setTitle(" + RECORD ", for: .normal)
addRecordButton.backgroundColor = UIColor.blackColorWithOpacity()
addRecordButton.layer.cornerRadius = addRecordButton.bounds.height/2
addRecordButton.setImage(nil, for: .normal)
}
if model.note != nil {
addNoteButton.setTitle(nil, for: .normal)
addNoteButton.backgroundColor = UIColor.clear
addNoteButton.layer.cornerRadius = 0
addNoteButton.setImage(LRAsset.cardNote.image, for: .normal)
}
else{
addNoteButton.setTitle(" + NOTE ", for: .normal)
addNoteButton.backgroundColor = UIColor.blackColorWithOpacity()
addNoteButton.layer.cornerRadius = addRecordButton.bounds.height/2
addNoteButton.setImage(nil, for: .normal)
}
if model.sketchPath != nil {
addSketchButton.setTitle(nil, for: .normal)
addSketchButton.backgroundColor = UIColor.clear
addSketchButton.layer.cornerRadius = 0
addSketchButton.setImage(LRAsset.CardSketch.image, for: .normal)
}
else{
addSketchButton.setTitle(" + SKETCH ", for: .normal)
addSketchButton.backgroundColor = UIColor.blackColorWithOpacity()
addSketchButton.layer.cornerRadius = addSketchButton.bounds.height/2
addSketchButton.setImage(nil, for: .normal)
}
}
In this method I am setting the gradient colors in the cell as per the category. And then I am setting the button states at the bottom according to the mentioned conditions.
Here's what my tableview looks like:-
The 3 buttons at the bottom of the cell are in a Stackview and their appearance is changing dynamically as per the conditions.
The tableview is not scrolling smooth and the lag is very visible to the naked eye. The height for the cell is fixed at 205.0 and is defined in the height for the row index method.
I don't know where my fetching and feeding the data to the cell logic is mistaken.
Please guide if the scrolling can be improved here.
Thanks in advance.
You are writing too much code inside func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell which should not be there Examples can be setting corner radius, background colors, gradients. These are called each time. You can implement these when the cell is created inside awakeFromNib method.
But this is not that much of a reason your tableview is having scrolling issues. It is because of images that you are adding inside that method. You can use iOS 10 new tableView(_:prefetchRowsAt:) method which will be a better place to load images or preparing data for your cells before being displayed taking things like images there which is called prior to dequeue method.
Here is the link form Apple documentation:
https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching/1771764-tableview
The table view calls this method as the user scrolls, providing the index paths for cells it is likely to display in the near future. Your implementation of this method is responsible for starting any expensive data loading processes. The data loading must be performed asynchronously, and the results made available to the tableView(_:cellForRowAt:) method on the table view’s data source.The collection view does not call this method for cells it requires immediately, so your code must not rely on this method to load data. The order of the index paths provided represents the priority.
https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching
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.
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.^=^
This question already has answers here:
How to clear font cache filled with emoji characters?
(6 answers)
Closed 7 years ago.
My current assignment is a iOS keyboard extension, which among other things offers all iOS-supported Emoji's (yes, I know iOS has a builtin Emoji keyboard, but the goal is to have one included in the keyboard extension).
For this Emoji Layout, which is basically supposed to be a scroll view with all emojis in it in a grid order, I decided to use an UICollectionView, as it only creates a limited number of cells and reuses them. (There are quite a lot of emojis, over 1'000.) These cells simply contain a UILabel, which holds the emoji as its text, with a GestureRecognizer to insert the tapped Emoji.
However, as I scroll through the list, I can see the memory usage exploding for somewhere around 16-18MB to over 33MB. While this doesn't trigger a memory warning on my iPhone 5s yet, it may as well on other devices, as app extensions are only dedicated a very sparse amount of resources.
EDIT: Sometimes I do receive a memory warning, mostly when switching back to the 'normal' keyboard layout. Most times, the memory usage drops below 20MB when switching back, but not always.
How can I reduce the amount of memory used by this Emoji Layout?
class EmojiView: UICollectionViewCell {
//...
override init(frame: CGRect) {
super.init(frame: frame)
self.userInteractionEnabled = true
let l = UILabel(frame: self.contentView.frame)
l.textAlignment = .Center
self.contentView.addSubview(l)
let tapper = UITapGestureRecognizer(target: self, action: "tap:")
self.addGestureRecognizer(tapper)
}
override func prepareForReuse() {
super.prepareForReuse()
//We know that there only is one subview of type UILabel
(self.contentView.subviews[0] as! UILabel).text = nil
}
}
//...
class EmojiViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
//...
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//The reuse id "emojiCell" is registered in the view's init.
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("emojiCell", forIndexPath: indexPath)
//Get recently used emojis
if indexPath.section == 0 {
(cell.contentView.subviews[0] as! UILabel).text = recent.keys[recent.startIndex.advancedBy(indexPath.item)]
//Get emoji from full, hardcoded list
} else if indexPath.section == 1 {
(cell.contentView.subviews[0] as! UILabel).text = emojiList[indexPath.item]
}
return cell
}
//Two sections: recently used and complete list
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 2
}
}
let emojiList: [String] = [
"\u{1F600}",
"\u{1F601}",
"\u{1F602}",
//...
// I can't loop over a range, there are
// unused values and gaps in between.
]
Please let me know if you need more code and/or information.
Edit: My guess is that iOS keeps the rendered emojis somewhere in the memory, despite setting the text to nil before reuse. But I may be completely wrong...
EDIT: As suggested by JasonNam, I ran the keyboard using Xcode's Leaks tool. There I noticed two things:
VM: CoreAnimation goes up to about 6-7MB when scrolling, but I guess this may be normal when scrolling through a collection view.
Malloc 16.00KB, starting at a value in the kilobytes, shoots up to 17MB when scrolling through the whole list, so there is a lot of memory being allocated, but I can't see anything else actually using it.
But no leaks were reported.
EDIT2: I just checked with CFGetRetainCount (which still works when using ARC) that the String objects do not have any references left once the nil value in prepareForReuse is set.
I'm testing on an iPhone 5s with iOS 9.2, but the problem also appears in the simulator using a iPhone 6s Plus.
EDIT3: Someone had the exact same problem here, but due to the strange title, I didn't find it up to now. It seems the only solution is to use UIImageViews with UIImages in the list, as UIImages in UICollectionView's are properly released on cell reuse.
it's pretty interesting, in my testing project, i commented out the prepareForReuse part in the EmojiView, and the memory usage became steady, project started at 19MB and never goes above 21MB, the (self.contentView.subviews[0] as! UILabel).text = nil is causing the issues in my test.
I think you don't use storyboard to design the collection view. I searched around and found out that you need to register the class with identifier before you populate the collection view cell. Try to call the following method on viewDidLoad or something.
collectionView.registerClass(UICollectionViewCell.self , forCellWithReuseIdentifier: "emojiCell")
Since you have memory issues you should try lazy loading your labels.
// Define an emojiLabel property in EmojiView.h
var emojiLabel: UILabel!
// Lazy load your views in your EmojiView.m
lazy var emojiLabel: UILabel = {
var tempLabel: UIImageView = UILabel(frame: self.contentView.frame)
tempLabel.textAlignment = .Center
tempLabel.userInteractionEnabled = true
contentView.addSubview(tempLabel)
return tempLabel;
}()
override func prepareForReuse() {
super.prepareForReuse()
emojiLabel.removeFromSuperview()
emojiLabel = nil
}
//...
class EmojiViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
//...
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//The reuse id "emojiCell" is registered in the view's init.
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("emojiCell", forIndexPath: indexPath) as! EmojiView
//Get recently used emojis
if indexPath.section == 0 {
cell.emojiLabel.text = recent.keys[recent.startIndex.advancedBy(indexPath.item)]
//Get emoji from full, hardcoded list
} else if indexPath.section == 1 {
cell.emojiLabel.text = emojiList[indexPath.item]
}
return cell
}
That way you're certain that the label is released when you scroll.
Now I have one question. Why do you add a gesture recognizer to your EmojiViews ? UICollectionView already implements this functionality with its didSelectItemAtIndexPath: delegate. Allocating extra gestureRecognizers for each loaded cell is pretty heavy.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath){
let cell : UICollectionViewCell = collectionView.cellForItemAtIndexPath(indexPath) as! EmojiView
// Do stuff here
}
To sum up, I would recommand to get rid of your whole init function in EmojiViews.m, use lazy loading for the labels and didSelectItemAtIndexPath: delegate for the selection events.
NB : I'm not used to swift so my code might contain a few mistakes.