Unable to display thumbnails in UICollectionView - ios

I am trying to recreate this thing. I've created in Storyboard skeleton. Here's the idea of my code:
Fetch images from URL's array with help of the function getThumbnailFromImage
Add UIImage's with my thumbnails in array webImages
Add in ViewController reusable cell MyCollectionView
...
But here I am with this))) (Don't mind absence of Auto Layout). What am I doing wrong? I think that the problem is with reloadData() but I don't know where to put it.
ViewController:
//
// ViewController.swift
// youtube-clone
//
// Created by мас on 16.08.2022.
//
import Foundation
import UIKit
import YouTubePlayer
import AVFoundation
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var url: [URL?] = [
URL(string: "https://www.youtube.com/watch?v=KhebpuFBD14"),
URL(string: "https://www.youtube.com/watch?v=UfNdNrRHpUw"),
URL(string: "https://www.youtube.com/watch?v=CX-BdDHW0Ho"),
URL(string: "https://www.youtube.com/watch?v=NIOMtSzfpck")
]
var webImages: [UIImage] = []
var currentPage: Int = 0
#IBOutlet var myPage: UIPageControl!
#IBOutlet weak var buttonInfo: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
myPage.currentPage = 0
myPage.numberOfPages = webImages.count
}
// MARK: - Collection View Setup
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return webImages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
getThumbnailFromImage(url: url[indexPath.row]!, completion: { image in
self.webImages.append(image!)
})
cell.myWebImage.image = webImages[indexPath.row]
cell.myWebImage.layer.cornerRadius = 20
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
myPage.currentPage = indexPath.row
}
// MARK: - Layout Setup // IGNORE IT
func setupLayout() {
buttonInfo.layer.cornerRadius = 25
buttonInfo.imageView!.transform = CGAffineTransform(rotationAngle: 180 * .pi / 180)
self.navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
}
// MARK: - Videos Thumbnail Fetcher
func getThumbnailFromImage(url: URL, completion: #escaping ((_ image: UIImage?) -> Void)) {
DispatchQueue.global().async {
let asset = AVAsset(url: url)
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
avAssetImageGenerator.appliesPreferredTrackTransform = true
let thumbnailTime = CMTimeMake(value: 7, timescale: 1)
do {
let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumbnailTime, actualTime: nil)
let thumbImage = UIImage(cgImage: cgThumbImage)
DispatchQueue.main.async {
completion(thumbImage)
}
}
catch {
print(error.localizedDescription)
}
}
}
}
Reusable Cell AKA MyCollectionCell:
import UIKit
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
}
P.s.: YouTubePlayer is custom pod from GitHub, it's not currently used.

You do NOT have to use AVAssetImageGenerator, Simply you can use Youtube API to fetch the thumbnail images as .jpg image by video id,
and each YouTube video has four generated images.
https://img.youtube.com/vi/{id}/0.jpg
https://img.youtube.com/vi/{id}/1.jpg
https://img.youtube.com/vi/{id}/2.jpg
https://img.youtube.com/vi/{id}/3.jpg
Example
https://img.youtube.com/vi/KhebpuFBD14/0.jpg
And then it is preferred to use a third party to load this image as its displayed in a list, like https://github.com/SDWebImage/SDWebImage or https://github.com/onevcat/Kingfisher and you will NOT be worry about Concurrency or caching.

A couple of thoughts:
#matt is right in the comment - getThumbnailFromImage will likely not have called the completion block by the time cellForItemAt returns.
From what is visible in the code you posted, webImages.count will still be 0 when your collection view checks numberOfItemsInSection. If the number of items is 0, cellForItemAt may never get called so the call to getThumbnailFromImage wouldn't even be reached. (I'm not sure if the white box in your screenshot is part of a cell or another view element. If a cell is being displayed, I'm assuming you're populating webImages somewhere else before the collection view gets laid out).
One way you could work around these issues is by giving each cell a URL rather than a thumbnail. That way the cell can be displayed while the image is still loading. The cell could look something like this:
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
func configure(urlString: String) {
guard let self = self, let url = URL(string: urlString) else {
return
}
getThumbnailFromImage(url: url, completion: { [weak self] image in
self?.myWebImage.image = image
})
}
// Move `getThumbnailForImage` function to here, or give the cell a delegate to call back to the VC with if you don't want any networking in the view itself
}
The cellForItemAt function in the VC would need to be changed to something like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
cell.configure(urlString: url[indexPath.row])
cell.myWebImage.layer.cornerRadius = 20 // This should probably live in the cell since the parent doesn't actually need to know about it!
return cell
}
An added benefit of this approach is that you're not referencing a separate array of images that could theoretically end up being in the wrong order if there's a mistake somewhere in the code. You could get rid of the webImages array entirely and use urls.count in numberOfItemsInSection instead - or eventually the number of elements returned from an API somewhere.
Side note - make sure you add [weak self] at the beginning of any closure that references self to avoid trying to access it after it's been deallocated! Currently the call to getThumbnailFromImage doesn't have that :)
Also, note that I changed to a guard statement for checking that the URL exists. This is much safer than force unwrapping a URL(string:) value, especially if you ever end up getting the strings from a dynamic source.

Related

Collection View reusable cell issue while scrolling

I am using the collection view to show the gif's on the list.
Now facing the cell reusable issue while scrolling the cells up or down of collection view.
Like itemA is on first place in the list and itemB is on the second place in the list.
but when I scroll the data in the collection view. the places of items got misplaced. like some time itemA gone on 5th place or sometimes anywhere in the list.
i know i think this is the use with reusable cell, but don't know how to salve this.
Plss help.
Collection view cellForItemAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GifCell", for: indexPath as IndexPath) as? GifCell else {
fatalError()
}
if gifArr.count > 0 {
let urlString = self.gifArr[indexPath.row]
let url = URL(string: urlString)!
DispatchQueue.global().async {
let imageData = try? Data(contentsOf: url)
let imageData3 = FLAnimatedImage(animatedGIFData: imageData) // this is the 3rd pary library to show the gifs on UIimageview's
DispatchQueue.main.async {
cell.imageView.animatedImage = imageData3
cell.textLabel.text = String(indexPath.row)
}
}
}
return cell
}
In GifCell you could implement prepareForReuse() method:
Performs any clean up necessary to prepare the view for use again.
override func prepareForReuse() {
super.prepareForReuse()
imageView.animatedImage = nil
textLabel.text = ""
}
Note:
at this point, each time cellForItemAt method gets called, the url will be reloaded, so later, you might want find a way to cache the images instead of keep reloading them.
First solution: You can cache data and every time check if there is, use your cache.
you can use this link, but replace UIImage with gift type!
or
try this, I did not test it
if let giftAny = UserDefaults.standard.value(forKey: "giftUrl") {
//cast giftAny to Data
// use cached gift
} else {
// cache gift
let giftData = try? Data(contentsOf: url)
UserDefaults.standard.setValue(giftData, forKeyPath: "giftUrl")
//use gift
}
Second Solution: Don't reuse cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = UICollectionViewCell(style: .default, reuseIdentifier:"Cell")
return cell
}
but in this case, if you have many cells, memory leak is unavoidable.

Variables in UICollectionViewCells change to different cells

I have a shop menu for my game in swift. I am using a UICollectionView to hold the views for the items. There is a black glass covering over them before they are bought, and it turns clear when they buy it. I am storing data for owning the certain items in the cells' classes. When I scroll down in the scrollView and then come back up after clicking a cell. A different cell than collected has the clear class and the one I previously selected is black again.
import UIKit
class Shop1CollectionViewCell: UICollectionViewCell {
var owned = Bool(false)
var price = Int()
var texture = String()
#IBOutlet weak var glass: UIImageView!
#IBOutlet weak var ball: UIImageView!
func initiate(texture: String, price: Int){//called to set up the cell
ball.image = UIImage(named: texture)
if owned{//change the glass color if it is owned or not
glass.image = UIImage(named: "glass")
}else{
glass.image = UIImage(named: "test")
}
}
func clickedOn(){
owned = true//when selected, change the glass color
glass.image = UIImage(named: "glass")
}
}
Then I have the UICollectionView class
import UIKit
class ShopViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
struct ball{
var price = Int()
var texture = String()
var owned = Bool()
}
var balls = Array<ball>()//This is assigned values, just taken off of the code because it is really long
override func viewDidLoad() {
super.viewDidLoad()
balls = makeBalls(
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return (balls.count - 1)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Shop1CollectionViewCell
cell.initiate(texture: balls[indexPath.item].texture, price: 1)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! Shop1CollectionViewCell
cell.clickedOn()
}
}
I am wondering why the variables stored in one class of a cell are being switched to another.
Please ask if you need me to add any additional information or code.
You should definitely implement prepareForReuse, and set the cell back to its default state - which in this case would be with the black glass, and unowned.
You can still change the glass in the same way as you're doing in didSelectItemAt, but I'd recommend having the view controller track the state of each ball.
For example, when didSelectItemAt gets called - the view controller would update the ball stored in self.balls[indexPath.row] to have owned = true.
That way, the next time cellForItemAt gets called, you'll know what colour the glass should be by checking the value in self.balls[indexPath.row].owned.
Lastly, you're returning balls.count - 1 in numberOfItems, is this intentional?
In the case where have 10 balls, you're only going to have 9 cells. If you want a cell for each of your objects you should always return the count as is.
Sometimes, you may have to override the following function
override func prepareForReuse(){
super.prepareForReuse()
// reset to default value.
}
in class Shop1CollectionViewCell to guarantee cells will behave correctly. Hope you got it.

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)

Swift - AlamofireImage download multiple URLs and display in CollectionView

I would like to download multiple URLs from string in array, and display images in collectionView. I succeed to display four images (4 visible cells on my screen) and when I scroll, that begins the download of the 2 other images.
I want that these 6 images are downloaded in the same time (and I don't have to scroll for beginning the other download). After this, I want to display the total time spent to download and display images in my collectionView.
What I am doing wrong ?
Here is my code :
import UIKit
import AlamofireImage
import Alamofire
class ViewController: UIViewController,UICollectionViewDelegate,UICollectionViewDataSource {
var players = [
["image_Name":"https://images.unsplash.com/photo-1420768255295-e871cbf6eb81?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=4b94ef340bd7f6cac580fbc76af326af"],
["image_Name":"https://images.unsplash.com/photo-1465411801898-f1a78de00afc?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=149d27223217c0fa63c7dd8f1e8d23f6"],
["image_Name":"https://images.unsplash.com/photo-1466853817435-05b43fe45b39?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=a3b629b7e0c4f710ce119f219ae5b874"],
["image_Name":"https://images.unsplash.com/photo-1467404899198-ccadbcd96b91?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=66eef8db7a0aa4119c6c8d7ba211f79f"],
["image_Name":"https://images.unsplash.com/photo-1470322346096-ecab3914cab7?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=83863ba23662871baf6434c6000e00bd"],
["image_Name":"https://images.unsplash.com/photo-1473624566182-509e437512f4?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&s=54c3ec6d1fee824d62e6fa76676ddf17"]
]
var methodStart = Date()
#IBOutlet weak var mycall: UICollectionView!
var selectedIndex=[Int]()
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.players.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell:playerCell = collectionView.dequeueReusableCell(withReuseIdentifier: "playerCell", for: indexPath) as! playerCell
let url = URL(string:self.players[indexPath.row]["image_Name"]!)
if indexPath.row == 0 {
methodStart = Date()
}
Alamofire.request(url!).responseImage { response in
// debugPrint(response)
// print(response.request)
// print(response.response)
// debugPrint(response.result)
if let image = response.result.value {
cell.playerImage.image = image
}
}
if indexPath.row == self.players.count - 1 {
let methodFinish = NSDate()
let executionTime = methodFinish.timeIntervalSince(methodStart as Date)
print("Execution time: \(executionTime)")
}
return cell
}
}
Problem in your code -
you said you want those 6 images to be downloaded at the same time and when user scrolls the 5th and 6th image should display immediately. but this is not happening because the cellForItemAtIndexPath function for the 5th and 6th cell called when you scroll. this is the default behavior and you cannot invoke this before user scrolled.
Quick fix solution -
Download all the images outside cellForItemAtIndexPath function, like in either viewDidLoad or viewDidAppear by looping the array of players and then reload the collection view after completion of download of the last one.
Permanent and best solution -
Implement lazy loading and caching of images using either Heneke or SDWebImage to display image in cells. And use prefetch option in those libraries to fetch images before they were shown in cells. To use prefetch option you need to loop through the array and pass the urls to the fetching and storing functions of the library and just load image in cell according to the syntax of that library and it will manage the displaying of image when it downloaded and you don't have to worried about that.
When you are dealing with network calls, you should update UI in main queue. One more valid point i want to add here is Alamofire has a special library for image download/resize. Please have a look at AlamofireImage Library
Alamofire.request(url!).responseImage { response in
if let image = response.result.value {
DispatchQueue.main.async {
cell.playerImage.image = image
cell.playerImage.setNeedsLayout()
}
}
}

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