DiffableDataSource CollectionView returning no items in section - ios

Here is my class:
class MediaViewController: UIViewController{
var collectionView: UICollectionView! = nil
private lazy var dataSource = makeDataSource()
fileprivate typealias DataSource = UICollectionViewDiffableDataSource<SectionLayoutKind, testRecord>
fileprivate typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot<SectionLayoutKind, testRecord>
override func viewDidLoad() {
super.viewDidLoad()
setRecordItems()
configureHierarchy()
configureDataSource()
applySnapshot()
}
func setRecordItems(){
for i in 0...3{
let record = testRecord(daysBack: i/2, progression: i/10)
records.append(record)
}
}
extension MediaViewController {
func configureHierarchy() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground
view.addSubview(collectionView)
collectionView.delegate = self
}
}
extension MediaViewController {
fileprivate enum SectionLayoutKind: Int, CaseIterable{
case records
case timeline
}
fileprivate func makeDataSource() -> DataSource {
let dataSource = DataSource(
collectionView: collectionView,
cellProvider: { (collectionView, indexPath, testRecord) ->
UICollectionViewCell? in
// 2
switch indexPath.section {
case 0:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RecordCollectionViewCell.identifier, for: indexPath) as? RecordCollectionViewCell
cell?.configure(with: testRecord)
return cell
case 1:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TimelineDayCell.identifier, for: indexPath) as? TimelineDayCell
cell?.configure(with: testRecord)
return cell
default:
return UICollectionViewCell()
}
})
return dataSource
}
func configureDataSource() {
collectionView.register(RecordCollectionViewCell.nib, forCellWithReuseIdentifier: RecordCollectionViewCell.identifier)
collectionView.register(TimelineDayCell.nib, forCellWithReuseIdentifier: TimelineDayCell.identifier)
}
func applySnapshot(animatingDifferences: Bool = true) {
// 2
var snapshot = DataSourceSnapshot()
SectionLayoutKind.allCases.forEach {
snapshot.appendSections([$0])
let records_copy = records
snapshot.appendItems(records_copy, toSection: $0)
}
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
}
So the setup is that there are two sections, records and timeline. These are both run by the same data - the records array. Currently I am copying this array from the class when I apply the snapshot each time - Im not sure its bad to use the same array for both for some reason..
Then when setting up the data source, for the cellProvider I have a switch statement that checks the section. If its section 0 Ill use a Record Cell and if its section 1 I will use a timeline cell.
Right now no record cells are being produced. When I check collectionView.numberOfItems(inSection:0) its 0.
collectionView.numberOfItems(inSection:1) is 4 (the amount of records)
Why isn't it 4 for both sections? How can I do this?

var snapshot = DataSourceSnapshot()
SectionLayoutKind.allCases.forEach {
snapshot.appendSections([$0])
let records_copy = records
snapshot.appendItems(records_copy, toSection: $0)
}
So, let's consider what happens in that code. There are two cases in SectionLayoutKind.allCases, so the forEach runs twice.
The first time, we append one section and then append four records to it.
The second time, we append another section that then append the same four records to it. This effectively removes the four records from the first section and puts them in the second section.
Im not sure its bad to use the same array for both for some reason
It's not exactly "bad" but it certainly isn't getting you where you want to go. Remember, all items — not all items of the same section, but all items — must be unique. Obviously if you use the same four records twice, that's not unique. Unique doesn't mean whether it's the same or a different object. Uniqueness is determined by the Hashable / Equatable implementation of your cell identifier type, which in this case is testRecord. Your copies are identical, in that sense, to the original set of objects, so they count as the same as far as the diffable data source is concerned.
(You have not shown the testRecord type so I can't make further observations. But please, please, never again write code where a type begins with a small letter.)

Related

How to handle different data models with collectionView compositional layout?

Im updating an app which had many collectionViews embedded inside tableview so I decided to use UICollectionViewCompositionalLayout instead. I find them very flexible and I was able to design Home page for my app using this layout.
I followed some tutorials which just showed same data types, like Integers in multiple sections or photos app showing photos in different sections. In those tutorials they will use dataSource like this:
var dataSource: UICollectionViewDiffableDataSource<Section, PhotosItem>! = nil
or
var dataSource: UICollectionViewDiffableDataSource<Section, MoviesEntity>!
As my homepage consisted of many types of data, I simply used:
var dataSource: UICollectionViewDiffableDataSource<Section, Int>! = nil
And simply used numberOfSections, numberOfItemsInSection and cellForItemAt methods. I used this approach as my homepage will get data from multiple api's and some sections are static.
In UICollectionView, I would simply hit an api and update my data model and reload collectionView and do same for different collectionViews handling different data models in same page but now there is only one CollectionView so how do I handle this?
Do I just hit 7-8 API's and reload collectionView everytime? What I would like to do is use snapshot feature of feeding CollectionView with data coming from multiple API's.
This is what I have done to create compositional layout:
func generateLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let isWideView = layoutEnvironment.traitCollection.horizontalSizeClass == .regular
let sectionLayoutKind = Section.allCases[sectionIndex]
switch (sectionLayoutKind) {
case .firstSection:
return self.generateFirstLayout(isWide: isWideView)
case .secondSection:
return self.generateCategoriesLayout(isWide: isWideView)
case .services:
return self.generateServicesLayout(isWide: isWideView)
case .spotlight:
return self.generateSpotlightLayout(isWide: isWideView)
case .offers:
return self.generateOffersLayout(isWide: isWideView)
case .reviews:
return self.generateReviewLayout(isWide: isWideView)
case .logo:
return self.generateLogoLayout(isWide: isWideView)
}
}
}
And im simply using these functions to generate sections and adding Dummy items in them:
func numberOfSections(in collectionView: UICollectionView) -> Int {
return Section.allCases.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
switch section {
case 0 :
return 1
case 1 :
return categories.count
case 2 :
return services.count
case 3:
return 2
case 4:
return 5
case 5:
return offers.count
case 6:
return 4
default:
return 10
}
}
So far this is all dummy data and now I want to feed live data and this is not same as what those tutorials did but my sections are different and I could not create something like "PhotosItem" to feed into dataSource method like in those tutorials.
Those tutorials used something like this:
func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource
<Section, AlbumItem>(collectionView: albumsCollectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, albumItem: AlbumItem) -> UICollectionViewCell? in
let sectionType = Section.allCases[indexPath.section]
switch sectionType {
case .featuredAlbums:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: FeaturedAlbumItemCell.reuseIdentifer,
for: indexPath) as? FeaturedAlbumItemCell else { fatalError("Could not create new cell") }
cell.featuredPhotoURL = albumItem.imageItems[0].thumbnailURL
cell.title = albumItem.albumTitle
cell.totalNumberOfImages = albumItem.imageItems.count
return cell
case .sharedAlbums:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: SharedAlbumItemCell.reuseIdentifer,
for: indexPath) as? SharedAlbumItemCell else { fatalError("Could not create new cell") }
cell.featuredPhotoURL = albumItem.imageItems[0].thumbnailURL
cell.title = albumItem.albumTitle
return cell
case .myAlbums:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: AlbumItemCell.reuseIdentifer,
for: indexPath) as? AlbumItemCell else { fatalError("Could not create new cell") }
cell.featuredPhotoURL = albumItem.imageItems[0].thumbnailURL
cell.title = albumItem.albumTitle
return cell
}
}
dataSource.supplementaryViewProvider = { (
collectionView: UICollectionView,
kind: String,
indexPath: IndexPath) -> UICollectionReusableView? in
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: HeaderView.reuseIdentifier,
for: indexPath) as? HeaderView else { fatalError("Cannot create header view") }
supplementaryView.label.text = Section.allCases[indexPath.section].rawValue
return supplementaryView
}
let snapshot = snapshotForCurrentState()
dataSource.apply(snapshot, animatingDifferences: false)
}
But i don't have one AlbumItem but like ReviewItem, OfferItem and many more. So I was wondering do I stick to my way and just call the api and reload my collectionView?
I have seen many apps that probably have same situation as me. So can anyone help me how to deal with this?
Im using SwiftyJSON to feed my data into my data models. Just telling this as I have seen in most examples they use Hashable.
I found answer by making my Model Hashable and had to switch from SwiftyJSON to Codable completely which I was avoiding for months. When your model is Hashable, your can pass it as item for your section. You can create different data models and add them to any section as required.
So for above, if I want to add PhotoItems or MovieItems or anything else, first I need to create Sections which can be done by creating an Enum like this:
enum Section {
case Section1
case SEction2
}
And then create another enum for items for each section like this:
enum Items {
case Section1Item(PhotoItem)
case Section2Item(MovieItem)
}
then define your dataSource like this:
var dataSource: UICollectionViewDiffableDataSource<Section, Items>!
As you can see now we can add items of any type as we please instead of defining Item type like in question.
Finally just create a function to define snapshot:
var snapshot = Snapshot()
snapshot.appendItems(ArrayOfItemType1.map(PhotosItem.photos), toSection: Section.first)
snapshot.appendItems(ArrayOfItemType2.map(MovieItem.movie), toSection: Section.first)
datasource.apply(snapshot, animatingDifferences: true)

sort tableViewCells by numbers inside a label

I try to sort the tableViewCells by numbers inside a label, so the cell which includes the highest number in a label should be last, and vice versa.
I tried it with different solutions like following, but it's simply not working, it also doesn't show any error code
I don't know if there is just a small mistake or if it is all completely wrong, but if so, I hope that you know a completely different way to solve it.
TableView:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// download jobs
jobsRef.observe(.value, with: { (snapshot) in
self.jobs.removeAll()
for child in snapshot.children {
let childSnapshot = child as! DataSnapshot
let job = Job(snapshot: childSnapshot)
print(job)
self.jobs.insert(job, at: 0)
}
filterLocation()
self.tableView.reloadData()
})
}
var jobArr = JobTableViewCell.jobDistance!.jobArr
func filterLocation() {
jobArr.sort() { $0.distance.text > $1.distance.text}
}
TableViewCell:
#IBOutlet weak var distance: UILabel!
static var jobDistance: JobTableViewCell?
var jobArr = [JobTableViewCell.jobDistance!.distance.text]
override func layoutSubviews() {
super.layoutSubviews()
JobTableViewCell.jobDistance = self
}
lets check out apple doc for the table view https://developer.apple.com/documentation/uikit/uitableviewdatasource
as it says there is method:
func tableView(UITableView, cellForRowAt: IndexPath) -> UITableViewCell
we can read it like "give me[UITableView] cell[-> UITableViewCell] for this index[cellForRowAt]"
so all we need is just map our data source to tableview indexes:
e.g.
we have datasource array of strings
var dataSource = ["String", "Very long string", "Str"]
sort...
> ["Str", "String", "Very long string"]
and then just provide our data to cell (your tableview must conform UITableViewDataSource protocol)
// Provide a cell object for each row.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Fetch a cell of the appropriate type.
let cell = tableView.dequeueReusableCell(withIdentifier: "cellTypeIdentifier", for: indexPath)
// Configure the cell’s contents.
cell.textLabel!.text = dataSource[indexPath]
return cell
}
The problem is you sort another array jobArr
jobArr.sort() { $0.distance.text > $1.distance.text}
and append values to another one jobs

Adding sections to UITableView in 'Edit' Mode

I'm working on a basic list workout app right now that keeps track of workouts and then the exercises for each workout. I want to extend the current 'editing' mode of a TableViewController to allow for more advanced editing options. Here is what I have so far:
As you can see, I am inserting a section at the top of the table view so that the title of the workout can be edited. The problem I am facing is twofold:
There is no animation when the edit button is tapped anymore.
When you try to swipe right on one of the exercises (Squat or Bench press) the entire section containing exercises disappears.
I start by triggering one of two different functions on the setEditing function, to either switch to read mode or edit mode based on whether the boolean editing returns true or false.
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: true)
tableView.setEditing(editing, animated: true)
if editing {
switchToEditMode()
} else {
switchToReadMode()
}
}
Then I either insert the "addTitle" section (the text field seen in the second image) to an array called tableSectionsKey which I use to determine how to display the table (seen further below), and then reload the table data.
func switchToEditMode(){
tableSectionsKey.insert("addTitle", at:0)
self.tableView.reloadData()
}
func switchToReadMode(){
tableSectionsKey.remove(at: 0)
self.tableView.reloadData()
}
Here is my tableView data method. Basically the gist of it is that I have the array called tableSectionsKey I mentioned above, and I add strings that relate to sections based on what mode I'm in and what information should be displayed. Initially it just has "addExercise", which related to the "Add exercise to routine" cell
class WorkoutRoutineTableViewController: UITableViewController, UITextFieldDelegate, UINavigationControllerDelegate {
var tableSectionsKey = ["addExercise"]
}
Then in viewDidLoad I add the "exercise" section (for list of exercises) if the current workout routine has any, and I add the addTitle section if it's in new mode, which is used to determine if the view controller is being accessed from an add new workout button or a from a list of preexisting workouts (so to determine if the page is being used to create a workout or update an existing one)
override func viewDidLoad() {
super.viewDidLoad()
if workoutRoutine.exercises.count > 0 {
tableSectionsKey.insert("exercise", at:0)
}
if mode == "new" {
tableSectionsKey.insert("addTitle", at: 0)
}
}
Then in the cellForRowAt function I determine how to style the cell based on how the section of the table relates with an index in the tableSectionsKey array
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let section = indexPath.section
let sectionKey = tableSectionsKey[section]
let cellIdentifier = sectionKey + "TableViewCell"
switch sectionKey {
case "addTitle":
guard let addTitleCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? AddTitleTableViewCell else {
fatalError("Was expecting cell of type AddTitleTableViewCell.")
}
setUpAddTitleTableViewCell(for: addTitleCell)
return addTitleCell
case "exercise":
guard let exerciseCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? ExerciseTableViewCell else {
fatalError("Was expecting cell of type ExerciseTableViewCell.")
}
let exercise = workoutRoutine.exercises[indexPath.row]
setUpExerciseTableViewCell(for: exerciseCell, with: exercise)
return exerciseCell
case "addExercise":
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
return cell
default:
fatalError("Couldn't find section: \(section), in WorkoutRoutineTableView" )
}
}
private func setUpExerciseTableViewCell(for cell: ExerciseTableViewCell, with exercise: Exercise) {
let titleText = exercise.name
let detailsText = "\(exercise.sets)x\(exercise.reps) - \(exercise.weight)lbs"
cell.titleLabel.text = titleText
cell.detailsLabel.text = detailsText
}
private func setUpAddTitleTableViewCell(for cell: AddTitleTableViewCell) {
cell.titleTextField.delegate = self
if (workoutRoutine.title != nil) {
cell.titleTextField.text = workoutRoutine.title
}
// Set the WorkoutRoutineTableViewController property 'titleTextField' to the 'titleTextField' found in the addTitleTableViewCell
self.titleTextField = cell.titleTextField
}
This isn't all of my code but I believe it is all of the code that could be relevant to this problem.
Your animation issue is due to the use of reloadData. You need to replace the uses of reloadData with calls to insert or delete just the one section.
func switchToEditMode(){
tableSectionsKey.insert("addTitle", at:0)
let section = IndexSet(integer: 0)
self.tableView.insertSections(section, with: .left) // use whatever animation you want
}
func switchToReadMode(){
tableSectionsKey.remove(at: 0)
let section = IndexSet(integer: 0)
self.tableView.deleteSections(section, with: .left) // use whatever animation you want
}

CollectionView in TableView displays false Data swift

I'm trying to combine a CollectionViewwith a TableView, so fare everything works except one problem, which I cant fix myself.
I have to load some data in the CollectionViews which are sorted with the header of the TableViewCell where the CollectionView is inside. For some reason, every time I start the app, the first three TableViewCells are identical. If I scroll a little bit vertically, they change to the right Data.
But it can also happen that while using it sometimes displays the same Data as in on TableViewCell another TableViewCell, here again the problem is solved if I scroll a little.
I think the problem are the reusableCells but I cant find the mistake myself. I tried to insert a colletionView.reloadData() and to set the cells to nil before reusing, sadly this didn`t work.
My TableViewController
import UIKit
import RealmSwift
import Alamofire
import SwiftyJSON
let myGroupLive = DispatchGroup()
let myGroupCommunity = DispatchGroup()
var channelTitle=""
class HomeVTwoTableViewController: UITableViewController {
var headers = ["LIVE","Channel1", "Channel2", "Channel3", "Channel4", "Channel5", "Channel6"]
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.navigationBar.isTranslucent = false
DataController().fetchDataLive(mode: "get")
DataController().fetchDataCommunity(mode: "get")
}
//MARK: Custom Tableview Headers
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return headers[section]
}
//MARK: DataSource Methods
override func numberOfSections(in tableView: UITableView) -> Int {
return headers.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
//Choosing the responsible PrototypCell for the Sections
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellBig", for: indexPath) as! HomeVTwoTableViewCell
print("TableViewreloadMain")
cell.collectionView.reloadData()
return cell
}
else if indexPath.section >= 1 {
// getting header Titel for reuse in cell
channelTitle = self.tableView(tableView, titleForHeaderInSection: indexPath.section)!
let cell = tableView.dequeueReusableCell(withIdentifier: "cellSmall", for: indexPath) as! HomeVTwoTableViewCellSmall
// anti Duplicate protection
cell.collectionView.reloadData()
return cell
}
else {
channelTitle = self.tableView(tableView, titleForHeaderInSection: indexPath.section)!
let cell = tableView.dequeueReusableCell(withIdentifier: "cellSmall", for: indexPath) as! HomeVTwoTableViewCellSmall
// anti Duplicate protection
cell.collectionView.reloadData()
return cell
}
}
}
}
My TableViewCell with `CollectionView
import UIKit
import RealmSwift
var communities: Results<Community>?
class HomeVTwoTableViewCellSmall: UITableViewCell{
//serves as a translator from ChannelName to the ChannelId
var channelOverview: [String:String] = ["Channel1": "399", "Channel2": "401", "Channel3": "360", "Channel4": "322", "Channel5": "385", "Channel6": "4"]
//Initiaize the CellChannel Container
var cellChannel: Results<Community>!
//Initialize the translated ChannelId
var channelId: String = ""
#IBOutlet weak var collectionView: UICollectionView!
}
extension HomeVTwoTableViewCellSmall: UICollectionViewDataSource,UICollectionViewDelegate {
//MARK: Datasource Methods
func numberOfSections(in collectionView: UICollectionView) -> Int
{
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return (cellChannel.count)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCellSmall", for: indexPath) as? HomeVTwoCollectionViewCellSmall else
{
fatalError("Cell has wrong type")
}
//removes the old image and Titel
cell.imageView.image = nil
cell.titleLbl.text = nil
//inserting the channel specific data
let url : String = (cellChannel[indexPath.row].pictureId)
let name :String = (cellChannel[indexPath.row].communityName)
cell.titleLbl.text = name
cell.imageView.downloadedFrom(link :"link")
return cell
}
//MARK: Delegate Methods
override func layoutSubviews() {
myGroupCommunity.notify(queue: DispatchQueue.main, execute: {
let realm = try! Realm()
//Getting the ChannelId from Dictionary
self.channelId = self.channelOverview[channelTitle]!
//load data from Realm into variables
self.cellChannel = realm.objects(Community.self).filter("channelId = \(String(describing: self.channelId)) ")
self.collectionView.dataSource = self
self.collectionView.delegate = self
print("collectionView layout Subviews")
self.collectionView.reloadData()
})
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCommunity = (cellChannel[indexPath.row].communityId)
let home = HomeViewController()
home.showCommunityDetail()
}
}
Thanks in advance.
tl;dr make channelTitle a variable on your cell and not a global variable. Also, clear it, and your other cell variables, on prepareForReuse
I may be mistaken here, but are you setting the channelTitle on the cells once you create them? As I see it, in your viewController you create cells based on your headers, and for each cell you set TableViewController's channelTitle to be the title at the given section.
If this is the case, then the TableViewCell actually isn't receiving any information about what it should be loading before you call reloadData().
In general, I would also recommend implementing prepareForReuse in your HomeVTwoTableViewCellSmall, since it will give you a chance to clean up any stale data. Likely you would want to do something like set cellChannel and channelId to empty strings or nil in that method, so when the cell is reused that old data is sticking around.
ALSO, I just reread the cell code you have, and it looks like you're doing some critical initial cell setup in layoutSubviews. That method is going to be potentially called a lot, but you really only need it to be called once (for the majority of what it does). Try this out:
override the init with reuse identifier on the cell
in that init, add self.collectionView.dataSource = self and self.collectionView.delegate = self
add a didSet on channelTitle
set channelTitle in the viewController
So the code would look like:
var channelTitle: String = "" {
didSet {
self.channelId = self.channelOverview[channelTitle]!
self.cellChannel = realm.objects(Community.self).filter("channelId = \(String(describing: self.channelId)) ")
self.collectionView.reloadData()
}
}
This way you're only reloading your data when the cell is updated with a new channel, rather than every layout of the cell's views.
Sorry... one more addition. I wasn't aware of how your channelTitle was actually being passed. As I see it, you're using channelTitle as a global variable rather than a local one. Don't do that! remove channelTitle from where it is currently before implementing the code above. You'll see some errors, because you're setting it in the ViewController and accessing it in the cell. What you want is to set the channelTitle on the cell from the ViewController (as I outlined above). That also explains why you were seeing the same data across all three cells. Basically you had set only ONE channelTitle and all three cells were looking to that global value to fetch their data.
Hope that helps a little!
(also, you should be able to remove your else if block in the cellForRowAtIndexPath method, since the else block that follows it covers the same code. You can also delete your viewDidLoad, since it isn't doing anything, and you should, as a rule, see if you can get rid of any !'s because they're unsafe. Use ? or guard or if let instead)

Realm/iOS: Bumpy scrolling performance for UICollectionView

I am using Realm as the alternative for coredata for the first time.
Sadly, I had this bumpy scrolling issue(It is not too bad, but quite obvious) for collectionView when I try Realm out. No data were downloaded blocking the main thread, I use local stored image instead.
Another issue is when I push to another collectionVC, if the current VC will pass data to the other one, the segue is also quite bumpy.
I am guessing it is because of the way I write this children property in the Realm Model. But I do not know what might be the good way to compute this array of array value (merging different types of list into one)
A big thank you in advance!!
Here is the main model I use for the collectionView
class STInstitution: STHierarchy, STContainer {
let boxes = List<STBox>()
let collections = List<STCollection>()
let volumes = List<STVolume>()
override dynamic var _type: ReamlEnum {
return ReamlEnum(value: ["rawValue": STHierarchyType.institution.rawValue])
}
var children: [[AnyObject]] {
var result = [[AnyObject]]()
var tempArr = [AnyObject]()
boxes.forEach{ tempArr.append($0) }
result.append(tempArr)
tempArr.removeAll()
collections.forEach{ tempArr.append($0) }
result.append(tempArr)
tempArr.removeAll()
volumes.forEach{ tempArr.append($0) }
result.append(tempArr)
return result
}
var hierarchyProperties: [String] {
return ["boxes", "collections", "volumes"]
}
}
Here is how I implement the UICollectionViewController:
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView?.alwaysBounceVertical = true
dataSource = STRealmDB.query(fromRealm: realm, ofType: STInstitution.self, query: "ownerId = '\(STUser.currentUserId)'")
}
// MARK: - datasource:
override func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
guard let dataSource = dataSource else { return 0 }
return dataSource.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! STArchiveCollectionViewCell
guard let dataSource = dataSource,
dataSource.count > indexPath.row else {
return cell
}
let item = dataSource[indexPath.row]
DispatchQueue.main.async {
cell.configureUI(withHierarchy: item)
}
return cell
}
// MARK: - Open Item
func pushToDetailView(dataSource: [[AnyObject]], titles: [String]) {
guard let vc = storyboard?.instantiateViewController(withIdentifier: STStoryboardIds.archiveDetailVC.rawValue) as? STArchiveDetailVC
else { return }
vc.dataSource = dataSource
vc.sectionTitles = titles
self.navigationController?.pushViewController(vc, animated: true)
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let dataSource = self.dataSource,
dataSource.count > indexPath.row else {
return
}
let item = dataSource[indexPath.row]
self.pushToDetailView(dataSource: item.children, titles: item.hierarchyProperties)
}
Modification(more codes on configureUI):
// configureUI
// data.type is an enum type
func configureUI<T: STHierarchy>(withHierarchy data: T) {
print("data", kHierarchyCoverImage + "\(data.type)")
titleLabel.text = data.title
let image = data.type.toUIImage()
self.imageView.image = image
}
// toUIImage of enum data.type
func toUIImage() -> UIImage {
let key = kHierarchyCoverImage + "\(self.rawValue)" as NSString
if let image = STCache.imageCache.object(forKey: key) {
return image
}else{
print("toUIImage")
let defaultImage = UIImage(named: "institution")
let image = UIImage(named: "\(self)") ?? defaultImage!
STCache.imageCache.setObject(image, forKey: key)
return image
}
}
If your UI is bumpy when you're scrolling, it simply means the operations you're performing in collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell are too heavy.
Realm itself is structured in such a way that reading data from objects is very fast, so you shouldn't be seeing substantial dropped frames if all you're doing is populating a cell with values from Realm.
A couple of considerations:
If you're calling item.children inside the cellForItem block method, since you're manually looping through and paging in every Realm object doing that, that will cause frame drops. If you are, it'd be best to either do that ahead of time, or re-desing the logic to only access those arrays when absolutely needed.
You mentioned you're including images. Even if the images are on disk, unless you force image decompression ahead of time, Core Animation will lazily decompress the image at draw time on the main thread which can severely kill scroll performance. See this question for more info.
The cellForItemAt method call should already be on the main thread, so configuring your cell in a DispatchQueue.main.async closure seems un-necessary, and given that it's not synchronous, may be causing additional issues by running out of order.
Collection views are notoriously hard for performance since entire rows of cells used to be created and configured in one run loop iteration. This behavior was changed in iOS 10 to spread cell creation out across multiple run loop iterations. See this WWDC video for tips on optimizing your collection view code to take advantage of this.
If you're still having trouble, please post up more of your sample code; most importantly, the contents of configureUI. Thanks!
Turned out I was focusing on the wrong side. My lack of experience with Realm made me feel that there must be something wrong I did with Realm. However, the true culprit was I forgot to define the path for shadow of my customed cell, which is really expensive to draw repeatedly. I did not find this until I used the time profile to check which methods are taking the most CPU, and I should have done it in the first place.

Resources