UITableViewCell containing a UICollectionView using RxSwift - ios

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.

Related

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

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
}

iOS tableViewCell is not updating after reloading the tableview even after setting the properties properly

I've been trying to figure out a problem in my code but no luck finding a solution.
I have a dropdown library which I'm using for dropdown the library is ios Dropdown
1: https://github.com/AssistoLab/DropDown here in this I've customised the the tableViewCell and I've added a radio button. But when I'm trying to reload it its not updating its state it still remains the same even after setting the state.
func updateCellBy(text: String, index: Int, isSelected: Bool) {
self.index = index
radioButton.isSelected = isSelected
titleLabel.text = text
checkboxButtonWidthConstraint.constant = 20
radioButtonLeadingConstraint.constant = 15
}
But when I scroll this dropdown/tableview dropdown buttons state gets updated. I'm still wodering whats wrong with this. Tested if the data coming in the above method is correct or not but that is correct.DroppDown image
Note this dropdown is inside a collectionViewCell which is indeed inside a view.
dropDown.cellNib = UINib(nibName: "TableViewCell", bundle: nil)
dropDown.customCellConfiguration = { [self] (index: Index, item: String, cell: DropDownCell) -> Void in
guard let cell = cell as? TableViewCell else { return }
cell.updateCellBy(text: DropdownArrays.shared.getData()[index], index: index, isSelected: self.cellIndexes.contains(index))
cell.delegate = self
}
dropDown.dataSource = DropdownArrays.shared.getData()
above is how I'm configuring that drop down. Its just a tableview with cells.
for reloadind the drop down I'm calling the below method
dropDown.reloadAllComponents()
which is indeed reloading the cell inside the DropDown library
Reloads all the cells.
It should not be necessary in most cases because each change to
`dataSource`, `textColor`, `textFont`, `selectionBackgroundColor`
and `cellConfiguration` implicitly calls `reloadAllComponents()`.
*/
public func reloadAllComponents() {
DispatchQueue.executeOnMainThread {
self.tableView.reloadData()
self.setNeedsUpdateConstraints()
}
}
but the cell data is not updating.

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
}

Need help setting UISwitch in custom cell (XIB, Swift 4, Xcode 9)

Successes so far: I have a remote data source. Data gets pulled dynamically into a View Controller. The data is used to name a .title and .subtitle on each of the reusable custom cells. Also, each custom cell has a UISwitch, which I have been able to get functional for sending out both a “subscribe” signal for push notifications (for a given group identified by the cell’s title/subtitle) and an “unsubscribe” signal as well.
My one remaining issue: Whenever the user "revisits" the settings VC, while my code is "resetting" the UISwitches, it causes the following warnings in Xcode 9.2:
UISwitch.on must be used from main thread
UISwitch.setOn(_:animated:) must be used from main thread only
-[UISwitch setOn:animated:notifyingVisualElement:] must be used from main thread
The code below "works" -- however the desired result happens rather slowly (the UISwitches that are indeed supposed to be "on" take a good while to finally flip to "on").
More details:
What is needed: Whenever the VC is either shown or "re-shown," I need to "reset" the custom cell’s UISwitch to "on" if the user is subscribed to the given group, and to "off" if the user is not subscribed. Ideally, each time the VC is displayed, something should reach out and touch the OneSignal server and find out that user’s “subscribe state” for each group, using the OneSignal.getTags() function. I have that part working. This code is in the VC. But I need to do it the right way, to suit proper protocols regarding threading.
VC file, “ViewController_13_Settings.swift” holds a Table View with the reusable custom cell.
Table View file is named "CustomTableViewCell.swift"
The custom cell is called "customCell" (I know, my names are all really creative).
The custom cell (designed in XIB) has only three items inside it:
Title – A displayed “friendly name” of a “group” to be subscribed to or unsubscribed from. Set from the remote data source
Subtitle – A hidden “database name” of the aforementioned group. Hidden from the user. Set from the remote data source.
UISwitch - named "switchMinistryGroupList"
How do I properly set the UISwitch programmatically?
Here is the code in ViewController_13_Settings.swift that seems pertinent:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomTableViewCell
// set cell's title and subtitle
cell.textLabelMinistryGroupList?.text = MinistryGroupArray[indexPath.row]
cell.textHiddenUserTagName?.text = OneSignalUserTagArray[indexPath.row]
// set the custom cell's UISwitch.
OneSignal.getTags({ tags in
print("tags - \(tags!)")
self.OneSignalUserTags = String(describing: tags)
print("OneSignalUserTags, from within the OneSignal func, = \(self.OneSignalUserTags)")
if self.OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
}, onFailure: { error in
print("Error getting tags - \(String(describing: error?.localizedDescription))")
// errorWithDomain - OneSignalError
// code - HTTP error code from the OneSignal server
// userInfo - JSON OneSignal responded with
})
viewWillAppear(true)
return cell
}
}
In the above portion of the VC code, this part (below) is what is functioning but apparently not in a way the uses threading properly:
if OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
It's not entirely clear what your code is doing, but there seems to be a few things that need sorting out, that will help you solve your problem.
1) Improve the naming of your objects. This helps others see what's going on when asking questions.
Don't call your cell CustomTableViewCell - call it, say, MinistryCell or something that represents the data its displaying. Rather than textLabelMinistryGroupList and textHiddenUserTagName tree ministryGroup and userTagName etc.
2) Let the cell populate itself. Make your IBOutlets in your cell private so you can't assign to them directly in your view controller. This is a bad habit!
3) Create an object (Ministry, say) that corresponds to the data you're assigning to the cell. Assign this to the cell and let the cell assign to its Outlets.
4) Never call viewWillAppear, or anything like it! These are called by the system.
You'll end up with something like this:
In your view controller
struct Ministry {
let group: String
let userTag: String
var tagExists: Bool?
}
You should create an array var ministries: [Ministry] and populate it at the start, rather than dealing with MinistryGroupArray and OneSignalUserTagArray separately.
In your cell
class MinistryCell: UITableViewCell {
#IBOutlet private weak var ministryGroup: UILabel!
#IBOutlet private weak var userTagName: UILabel!
#IBOutlet private weak var switch: UISwitch!
var ministry: Ministry? {
didSet {
ministryGroup.text = ministry?.group
userTagName.text = ministry?.userTag
if let tagExists = ministry?.tagExists {
switch.isEnabled = false
switch.isOn = tagExists
} else {
// We don't know the current state - disable the switch?
switch.isEnabled = false
}
}
}
}
Then you dataSource method will look like…
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! MinistryCell
let ministry = ministries[indexPath.row]
cell.ministry = ministry
if ministry.tagExists == nil {
OneSignal.getTags { tags in
// Success - so update the corresponding ministry.tagExists
// then reload the cell at this indexPath
}, onFailure: { error in
print("Error")
})
}
return cell
}

Resources