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)
Related
I met warning like this "Cyclomatic Complexity Violation: Function should have complexity 10 or less: currently complexity equals 14 (cyclomatic_complexity)" when I used RxDataSource.
My code structure like this:
struct ItemDetailDataSource {
typealias DataSource = RxTableViewSectionedReloadDataSource
static func dataSource() -> DataSource<ItemDetailTableViewSection> {
return .init(configureCell: { (dataSource, tableView, indexPath, _) -> UITableViewCell in
switch dataSource[indexPath] {
case .itemInfoTopItem(let info):
guard let cell = tableView.dequeueReusableCell(withIdentifier: ConstantsForCell.infoTopTableViewCell,
for: indexPath)
as? InfoTopTableViewCell else {
return UITableViewCell()
}
cell.configure(info)
return cell
case .itemHintItem(let hint):
...
case .itemManaColdownItem(let manacd):
case .itemNotesItem(let notes):
case .itemAttribItem(let attrib):
case .itemLoreItem(let lore):
case .itemComponentsItem(let components):
}
Can anyone help me fix this? Thanks very much.
The solution here is to not use an enum for your cell items. A possible solution is as follows:
struct DisplayableItem {
let makeCell: (UITableView, IndexPath) -> UITableViewCell
}
struct ItemDetailDataSource {
typealias DataSource = RxTableViewSectionedReloadDataSource
static func dataSource() -> DataSource<ItemDetailTableViewSection> {
.init { _, tableView, indexPath, item in
item.makeCell(tableView, indexPath)
}
}
}
Each DisplayableItem is given the means for making a UITableViewCell. You could do it with a closure like above, or with a protocol and a bunch of sub-classes.
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.)
I'm looking at DiffableDataSource available in iOS13 (or backported here: https://github.com/ra1028/DiffableDataSources) and cannot figure out how one would support multiple cell types in your collection or tableview.
Apple's sample code1 has:
var dataSource: UICollectionViewDiffableDataSource<Section, OutlineItem>! = nil
which seems to force a data source to be a single cell type. If I create a separate data source for another cell type - then there is no guarantee that both data sources don't have apply called on them at the same time - which would lead to the dreaded NSInternalInconsistencyException - which is familiar to anyone who has attempted to animate cell insertion/deletion manually with performBatchUpdates.
Am I missing something obvious?
I wrapped my different data in an enum with associated values. In my case, my data source was of type UICollectionViewDiffableDataSource<Section, Item>, where Item was
enum Item: Hashable {
case firstSection(DataModel1)
case secondSection(DataModel2)
}
then in your closure passed into the data source's initialization, you get an Item, and you can test and unwrap the data, as needed.
(I'd add that you should ensure that your backing associated values are Hashable, or else you'll need to implement that. That is what the diff'ing algorithm uses to identify each cell, and resolve movements, etc)
You definitely need to have a single data source.
The key is to use a more generic type. Swift's AnyHashable works well here. And you just need to cast the instance of AnyHashable to a more specific class.
lazy var dataSource = CollectionViewDiffableDataSource<Section, AnyHashable> (collectionView: collectionView) { collectionView, indexPath, item in
if let article = item as? Article, let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Section.articles.cellIdentifier, for: indexPath) as? ArticleCell {
cell.article = article
return cell
}
if let image = item as? ArticleImage, let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Section.trends.cellIdentifier, for: indexPath) as? ImageCell {
cell.image = image
return cell
}
fatalError()
}
And the Section enum looks like this:
enum Section: Int, CaseIterable {
case articles
case articleImages
var cellIdentifier: String {
switch self {
case .articles:
return "articleCell"
case .articleImages:
return "imagesCell"
}
}
}
One way of achieving this could be taking advantage your Section enum to identify the section with indexPath.section. It will be something like this:
lazy var dataSource = UICollectionViewDiffableDataSource<Section, Item> (collectionView: collectionView) { collectionView, indexPath, item in
let section = Section(rawValue: indexPath.section)
switch section {
case .firstSection:
let cell = ... Your dequeue code here for first section ...
return cell
case .secondSection:
let cell = ... Your dequeue code here for second section ...
return cell
default:
fatalError() // Here is handling the unmapped case that should not happen
}
}
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)
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.