Description:
I have been working on an app which displays images and caption in a CollectionView. I have made a custom Cell and CustomFlowLayout for the cell where the width of the cell, image and caption is equal to width of screen and height will change according to aspect ratio of the image height and height needed for the caption. The images which are displayed are stored on FirebaseStorage and I have also saved imagewidth and imageheight in firebaseDatabase. I Use SD_Web images to load and cache images. When the app loads for the first time the layout works perfectly fine as expected. The cells are arranged according to image height and caption height , with images being in aspect ratio.
The main problem occurs when a new post is done. I insert new post on the top of collectionview, and when the post is received the layout breaks, images becomes messed up, the caption goes on top image, lot of white space between image or caption. when I terminate the app from background and run it again , this time the layout is perfectly fine with new post present. I have to terminate app after everyone post to make the layout work.
What I feel the problem is, when I post the image. The new post is added on the top cell and the item on top cell pushed below without having the old height. how can I take this problem. I tried searching a lot but still of no use.
PS:
Using Autolayout for cell in IB.
FactsFeverLayout Class
import UIKit
protocol FactsFeverLayoutDelegate: class {
func collectionView(CollectionView: UICollectionView, heightForThePhotoAt indexPath: IndexPath, with width: CGFloat) -> CGFloat
func collectionView(CollectionView: UICollectionView, heightForCaptionAt indexPath: IndexPath, with width: CGFloat) -> CGFloat
}
class FactsFeverLayout: UICollectionViewLayout {
var cellPadding : CGFloat = 5.0
var delegate: FactsFeverLayoutDelegate?
private var contentHeight : CGFloat = 0.0
private var contentWidth : CGFloat {
let insets = collectionView!.contentInset
return (collectionView!.bounds.width - insets.left + insets.right)
}
private var attributeCache = [FactsFeverLayoutAttributes]()
override func prepare() {
if attributeCache.isEmpty {
let containerWidth = contentWidth
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
for item in 0 ..< collectionView!.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let width = containerWidth - cellPadding * 2
let photoHeight:CGFloat = (delegate?.collectionView(CollectionView: collectionView!, heightForThePhotoAt: indexPath, with: width))!
let captionHeight: CGFloat = (delegate?.collectionView(CollectionView: collectionView!, heightForCaptionAt: indexPath, with: width))!
let height: CGFloat = cellPadding + photoHeight + captionHeight + cellPadding
let frame = CGRect(x: xOffset, y: yOffset, width: containerWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// Create CEll Layout Atributes
let attributes = FactsFeverLayoutAttributes(forCellWith: indexPath)
attributes.photoHeight = photoHeight
attributes.frame = insetFrame
attributeCache.append(attributes)
// Update The Colunm any Y axis
contentHeight = max(contentHeight, frame.maxY)
yOffset = yOffset + height
}
}
}
override var collectionViewContentSize: CGSize{
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in attributeCache {
if attributes.frame.intersects(rect){
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}
// UICollectionView FlowLayout
// Abstract
class FactsFeverLayoutAttributes: UICollectionViewLayoutAttributes {
var photoHeight : CGFloat = 0.0
override func copy(with zone: NSZone? = nil) -> Any {
let copy = super.copy(with: zone) as! FactsFeverLayoutAttributes
copy.photoHeight = photoHeight
return copy
}
override func isEqual(_ object: Any?) -> Bool {
if let attributes = object as? FactsFeverLayoutAttributes {
if attributes.photoHeight == photoHeight {
super.isEqual(object)
}
}
return false
}
}
CollectionViewCell Class
class NewCellCollectionViewCell: UICollectionViewCell {
var facts: Facts!
var currentUser = Auth.auth().currentUser?.uid
// IBOutlets
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var imageHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var likeLable: UILabel!
#IBOutlet weak var likeButton: UIButton!
#IBOutlet weak var infoButton: UIButton!
#IBOutlet weak var buttonView: UIView!
#IBOutlet weak var captionTextView: UITextView!
override func awakeFromNib() {
super.awakeFromNib()
likeButton.setImage(UIImage(named: "noLike"), for: .normal)
likeButton.setImage(UIImage(named: "like"), for: .selected)
setupLayout()
}
func configureCell(fact: Facts){
facts = fact
imageView.sd_setImage(with: URL(string: fact.factsLink))
likeLable.text = String(fact.factsLikes.count)
captionTextView.text = fact.captionText
let factsRef = Database.database().reference().child("Facts").child(facts.factsId).child("likes")
factsRef.observeSingleEvent(of: .value) { (snapshot) in
if fact.factsLikes.contains(self.currentUser!){
self.likeButton.isSelected = true
} else {
self.likeButton.isSelected = false
}
}
}
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
if let attributes = layoutAttributes as? FactsFeverLayoutAttributes {
imageHeightConstraint.constant = attributes.photoHeight
}
}
}
ViewController Class
class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
//MARK: Outlets
#IBOutlet weak var uploadButtonOutlet: UIBarButtonItem!
#IBOutlet weak var collectionView: UICollectionView!
//MARK:- Properties
var images: [UIImage] = []
var factsArray:[Facts] = [Facts]()
var likeUsers:[String] = []
let currentUser = Auth.auth().currentUser?.uid
private let refreshControl = UIRefreshControl()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if #available(iOS 10.0, *) {
collectionView.refreshControl = refreshControl
} else {
collectionView.addSubview(refreshControl)
}
refreshControl.addTarget(self, action: #selector(refreshView), for: .valueChanged)
refreshControl.tintColor = UIColor.white
if let layout = collectionView?.collectionViewLayout as? FactsFeverLayout {
layout.delegate = self
}
collectionView.backgroundColor = UIColor.black
observeFactsFromFirebase()
}
#objc func refreshView(){
observeFactsFromFirebase()
}
//MARK:- Upload Facts
#IBAction func uploadButtonPressed(_ sender: Any) {
self.selectPhoto()
(deleted the function of selectPhoto but it works, UIImagePicker is used)
}
private func uploadImageToFirebaseStorage(image: UIImage, completion: #escaping (_ imageUrl: String) -> ()){
let imageName = NSUUID().uuidString + ".jpg"
let ref = Storage.storage().reference().child("message_images").child(imageName)
if let uploadData = image.jpegData(compressionQuality: 0.2){
ref.putData(uploadData, metadata: nil, completion: { (metadata, error) in
if error != nil {
print(" Failed to upload Image", error)
}
ref.downloadURL(completion: { (url, err) in
if let err = err {
print("Unable to upload image into storage due to \(err)")
}
let messageImageURL = url?.absoluteString
completion(messageImageURL!)
})
})
}
}
func addToDatabase(imageUrl:String, caption: String, image: UIImage){
let Id = NSUUID().uuidString
likeUsers.append(currentUser!)
let timeStamp = NSNumber(value: Int(NSDate().timeIntervalSince1970))
let factsDB = Database.database().reference().child("Facts")
let factsDictionary = ["factsLink": imageUrl, "likes": likeUsers, "factsId": Id, "timeStamp": timeStamp, "captionText": caption, "imageWidth": image.size.width, "imageHeight": image.size.height] as [String : Any]
factsDB.child(Id).setValue(factsDictionary){
(error, reference) in
if error != nil {
print(error)
ProgressHUD.showError("Image Upload Failed")
self.uploadButtonOutlet.isEnabled = true
return
} else{
print("Message Saved In DB")
ProgressHUD.showSuccess("image Uploded Successfully")
self.uploadButtonOutlet.isEnabled = true
self.observeFactsFromFirebase()
}
}
}
var imageUrl: [String] = []
func observeFactsFromFirebase(){
let factsDB = Database.database().reference().child("Facts").queryOrdered(byChild: "timeStamp")
factsDB.observe(.value){ (snapshot) in
print("Observer Data snapshot \(snapshot.value)")
self.factsArray = []
self.imageUrl = []
self.likeUsers = []
if let snapshot = snapshot.children.allObjects as? [DataSnapshot] {
for snap in snapshot {
if let postDictionary = snap.value as? Dictionary<String, AnyObject> {
let id = snap.key
let facts = Facts(dictionary: postDictionary)
self.factsArray.insert(facts, at: 0)
self.imageUrl.insert(facts.factsLink, at: 0)
}
}
}
self.collectionView.reloadData()
self.refreshControl.endRefreshing()
}
collectionView.reloadData()
}
// Download Image From Database
//-> Here I download image from firebase and store it locally and append it to images array (Deleted the code to remove unwanted clutter)
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
//MARK: Data Source
extension ViewController: UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return factsArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let facts = factsArray[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "newCellTrial", for: indexPath) as? NewCellCollectionViewCell
cell?.configureCell(fact: facts)
cell?.infoButton.addTarget(self, action: #selector(reportButtonPressed), for: .touchUpInside)
return cell!
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
let photos = IDMPhoto.photos(withURLs: imageUrl)
let browser = IDMPhotoBrowser(photos: photos)
browser?.setInitialPageIndex(UInt(indexPath.row))
self.present(browser!, animated: true, completion: nil)
}
}
extension ViewController: FactsFeverLayoutDelegate {
func collectionView(CollectionView: UICollectionView, heightForThePhotoAt indexPath: IndexPath, with width: CGFloat) -> CGFloat {
let facts = factsArray[indexPath.item]
let imageSize = CGSize(width: CGFloat(facts.imageWidht), height: CGFloat(facts.imageHeight))
let boundingRect = CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
let rect = AVMakeRect(aspectRatio: imageSize, insideRect: boundingRect)
return rect.size.height
}
func collectionView(CollectionView: UICollectionView, heightForCaptionAt indexPath: IndexPath, with width: CGFloat) -> CGFloat {
let fact = factsArray[indexPath.item]
let topPadding = CGFloat(8)
let bottomPadding = CGFloat(8)
let captionFont = UIFont.systemFont(ofSize: 15)
let viewHeight = CGFloat(40) //-> There is view below caption which holds like button and info button its height is constant (40)
let captionHeight = self.height(for: fact.captionText, with: captionFont, width: width)
let height = topPadding + captionHeight + topPadding + viewHeight + bottomPadding + topPadding + 10
return height
}
func height(for text: String, with font: UIFont, width: CGFloat) -> CGFloat {
let nsstring = NSString(string: text)
let maxHeight = CGFloat(1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let textAttributes = [NSAttributedString.Key.font: font]
let boundingRect = nsstring.boundingRect(with: CGSize(width: width, height: maxHeight), options: options, attributes: textAttributes, context: nil)
return ceil(boundingRect.height)
}
}
as you have used attributeCache, layoutAttribute of First Cell is Already Stored in cache.when you add images at first place and reload your collectionView , old Attribute for First cell will be retired from Cache and applying to your collectionView layout.
so you have to remove Element From Cache before reloading
override func prepare() {
attributeCache.RemoveAll()
if attributeCache.isEmpty {
let containerWidth = contentWidth
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
for item in 0 ..< collectionView!.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let width = containerWidth - cellPadding * 2
let photoHeight:CGFloat = (delegate?.collectionView(CollectionView: collectionView!, heightForThePhotoAt: indexPath, with: width))!
let captionHeight: CGFloat = (delegate?.collectionView(CollectionView: collectionView!, heightForCaptionAt: indexPath, with: width))!
let height: CGFloat = cellPadding + photoHeight + captionHeight + cellPadding
let frame = CGRect(x: xOffset, y: yOffset, width: containerWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// Create CEll Layout Atributes
let attributes = FactsFeverLayoutAttributes(forCellWith: indexPath)
attributes.photoHeight = photoHeight
attributes.frame = insetFrame
attributeCache.append(attributes)
// Update The Colunm any Y axis
contentHeight = max(contentHeight, frame.maxY)
yOffset = yOffset + height
}
}
}
Related
I have a scroll vie and i want to display the images selected by user in horizontal scroll manner. The following is my code. but, I am unable to achieve that. please guide me.
var xPosition: CGFloat = 0
var scrollViewContentWidth: CGFloat = 398
func handlePickedImage(image: UIImage){
let imageView = UIImageView(image: image)
imageView.frame = CGRect(x: 0, y: 0, width: 398, height: 188)
imageView.contentMode = UIView.ContentMode.scaleAspectFit
imageView.frame.origin.x = xPosition
imageView.frame.origin.y = 10
let spacer: CGFloat = 10
xPosition = 398 + spacer
scrollViewContentWidth = scrollViewContentWidth + spacer
imageScrollView.contentSize = CGSize(width: scrollViewContentWidth, height: 188)
imageScrollView.addSubview(imageView)
}
I have created just that with this code, it also has pagination implemented in it:
import UIKit
class ImagePagerViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var data: [FoodPostImageObject] = []
var userId: Int?
var indexPath: IndexPath?
var page: Int = 1
var alreadyFetchingData = false
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: self.view.frame.width, height: self.view.frame.height)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.scrollDirection = .horizontal
collectionView.collectionViewLayout = layout
if userId != nil {
getUserImages()
}
}
override func viewDidLayoutSubviews() {
guard self.indexPath != nil else {return}
self.collectionView.scrollToItem(at: self.indexPath!, at: .right, animated: false)
}
}
extension ImagePagerViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImagePagerCell", for: indexPath) as! ImagePagerCollectionViewCell
cell.image.loadImageUsingUrlString(urlString: data[indexPath.row].food_photo)
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
}
extension ImagePagerViewController {
func fetchMoreData(){
if !alreadyFetchingData {
if userId != nil {
getUserImages()
}
}
}
func getUserImages(){
guard let userId = self.userId else {return}
alreadyFetchingData = true
Server.get("/user_images/\(userId)/\(page)/"){ data, response, error in
self.alreadyFetchingData = false
guard let data = data else {return}
do {
self.data.append(contentsOf: try JSONDecoder().decode([FoodPostImageObject].self, from: data))
DispatchQueue.main.async {
self.collectionView.reloadData()
self.collectionView.scrollToItem(at: self.indexPath!, at: .right, animated: false)
self.page += 1
}
} catch {}
}
}
}
and this UICollectionViewCell:
import UIKit
class ImagePagerCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var image: CellImageView!
}
In the storyboard I just have a collectionView and ImagePagerCollectionViewCell inside.
Hope this helps
We are displaying a collection view with two cells in a row and displaying the label (dynamic with text) in each collectionviewcell.
We are able to achieve this by
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
The two cells in a row data are aligned as centered. can we make this to top-aligned?
Example:
Assume there are two cells with 2 lines of text in one cell and 4 lines of text in 2nd cell.
In this case, these two labels are centering aligned with each other. Can we make them top aligned?
Thanks in advance,
Ram
I have used AlignedCollectionViewFlowLayout for the quick solution.
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout
I have aprx same functionality so I am sharing what I have done in working condition. If your requirement is slightly different then you need to do more customization.
For code explanation, I suggest you to go through references links mentioned in last section of answer.
Create Custom FlowLayout Class
class CustomLayout: UICollectionViewFlowLayout {
private var computedContentSize: CGSize = .zero
private var cellAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
var array = [String]()
override var collectionViewContentSize: CGSize {
return computedContentSize
}
override func prepare() {
computedContentSize = .zero
cellAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
var preY = 10
var highY = 10
for section in 0 ..< collectionView!.numberOfSections {
var i = 0
for item in 0 ..< collectionView!.numberOfItems(inSection: section) {
let awidth = Int((self.collectionView?.bounds.width)!) / 3 - 15
let aheight = self.array[item].height(withConstrainedWidth: CGFloat(awidth), font: UIFont.systemFont(ofSize: 16)) + 20
var aX = 10
if item % 3 == 1 {
aX = aX + 10 + awidth
}
else if item % 3 == 2 {
aX = aX + 20 + 2 * awidth
}
else {
aX = 10
preY = highY
}
if highY < preY + Int(aheight) {
highY = preY + Int(aheight) + 5
}
let itemFrame = CGRect(x: aX, y: preY, width: awidth, height: Int(aheight))
let indexPath = IndexPath(item: item, section: section)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = itemFrame
cellAttributes[indexPath] = attributes
i += 1
}
}
computedContentSize = CGSize(width: (self.collectionView?.bounds.width)!,
height: CGFloat(collectionView!.numberOfItems(inSection: 0) * 70))
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributeList = [UICollectionViewLayoutAttributes]()
for (_, attributes) in cellAttributes {
if attributes.frame.intersects(rect) {
attributeList.append(attributes)
}
}
return attributeList
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cellAttributes[indexPath]
}
}
MainViewController
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var myColView: UICollectionView!
var array = ["Hello", "Look Behind", "Gotta get there", "Hey buddy", "Earth is rotating around the sun", "Sky is blue", "Kill yourself", "Humble docters", "Lets make party tonight Lets make party tonight", "Lets play PUB-G", "Where are you?", "Love you Iron Man."]
override func viewDidLoad() {
super.viewDidLoad()
let myLayout = CustomLayout()
myLayout.array = self.array
myColView.collectionViewLayout = myLayout
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return array.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell
cell.myLabel.text = array[indexPath.row]
cell.myLabel.layer.borderColor = UIColor.lightGray.cgColor
cell.myLabel.layer.borderWidth = 1
return cell
}
}
Used extensions
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height)
}
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.width)
}
}
Output:
Reference:
https://www.raywenderlich.com/4829472-uicollectionview-custom-layout-tutorial-pinterest
https://www.netguru.com/codestories/practical-introduction-to-custom-uicollectionview-layouts
https://developer.apple.com/documentation/uikit/uicollectionview/customizing_collection_view_layouts
I am trying to do a custom Flow Layout similar to the Apple News app. The flowLayout.delegate = self is in the viewDidLoad() method while my networking code is in in an async method in the viewDidAppear().
The problem is that the methods for the custom flow Layout get called before I can retrieve all the data from the server, therefore the app crashes.
Any ideas on how I could make it work? Here's my ViewController implementation:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, AppleNewsFlowLayoutDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var flowLayout: AppleNewsFlowLayout!
var newsArray = [News]()
var getFromDb = GetFromDb()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if getFromDb.news.isEmpty {
loadStore()
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.delegate = self
}
func loadStore() {
let urlString = "https://url"
self.getFromDb.getBreaksFromDb(url: urlString) { (breaksDataCell) in
if !breaksDataCell.isEmpty {
DispatchQueue.main.async(execute: {
self.collectionView.reloadData()
})
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return getFromDb.news.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Filling the cells with the correct info...
return cell
}
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType {
return getFromDb.news[indexPath.row].cellType
}
}
Here below the struct for News:
struct News {
let image: String
let provider: String
let title: String
let cellType: NewsCellType
init(image: String, provider: String, title: String, cellType: NewsCellType) {
self.image = image
self.provider = provider
self.title = title
self.cellType = cellType
}}
The Flow Layout class:
protocol AppleNewsFlowLayoutDelegate: class {
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType
}
class AppleNewsFlowLayout: UICollectionViewFlowLayout {
var maxY: CGFloat = 0.0
var isVSetOnce = false
weak var delegate: AppleNewsFlowLayoutDelegate?
var attributesArray: [UICollectionViewLayoutAttributes]?
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
for item in 0 ..< numberOfItems{
super.prepare()
minimumLineSpacing = 10
minimumInteritemSpacing = 16
sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let newsType: NewsCellType = delegate?.AppleNewsFlowLayout(self, cellTypeForItemAt: indexPath) else {
fatalError("AppleNewsFlowLayoutDelegate method is required.")
}
let screenWidth = UIScreen.main.bounds.width
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
var x = sectionInset.left
maxY = maxY + minimumLineSpacing
switch newsType {
case .big:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: width * 1.2)
maxY += width * 1.2
case .horizontal:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: 150)
maxY += 150
case .vertical:
let width = (screenWidth - minimumInteritemSpacing - sectionInset.left - sectionInset.right) / 2
x = isVSetOnce ? x + width + minimumInteritemSpacing : x
maxY = isVSetOnce ? maxY-10 : maxY
attributes.frame = CGRect(x: x, y: maxY, width: width, height: screenWidth * 0.8)
if isVSetOnce {
maxY += screenWidth * 0.8
}
isVSetOnce = !isVSetOnce
}
return attributes
}
override var collectionViewContentSize: CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: maxY)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if attributesArray == nil {
attributesArray = [UICollectionViewLayoutAttributes]()
print(collectionView!.numberOfItems(inSection: 0) - 1)
for i in 0 ... collectionView!.numberOfItems(inSection: 0) - 1
{
let attributes = self.layoutAttributesForItem(at: IndexPath(item: i, section: 0))
attributesArray!.append(attributes!)
}
}
return attributesArray
}
}
Two things which you need to do when we deal with custom collection view flow layouts.
changes in prepare() method in custom flow layout. you will start preparing the layout only if the number of items more than 0.
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
for item in 0 ..< numberOfItems{ }
}
use numberOfItems property whenever you wanted to do something with collectionView items in the customFlowLayout to avoid crashes.
Here below the functioning clean code if you are looking to implement a Custom Collection View Flow Layout similar to the Apple News app with networking:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, AppleNewsFlowLayoutDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var flowLayout: AppleNewsFlowLayout!
var getFromDb = GetFromDb()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if getFromDb.news.isEmpty {
loadStore()
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.delegate = self
}
func loadStore() {
let urlString = "https://yourURL"
self.getFromDb.getBreaksFromDb(url: urlString) { (breaksDataCell) in
if !breaksDataCell.isEmpty {
DispatchQueue.main.async(execute: {
self.collectionView.reloadData()
print("RELOAD")
})
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return getFromDb.news.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let news = getFromDb.news[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: news.cellType.rawValue, for: indexPath) as! NewCell
cell.image.image = UIImage(named: "news")!
cell.provider.text = news.provider
cell.title.text = news.title
return cell
}
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType {
print(getFromDb.news[indexPath.row].cellType)
return getFromDb.news[indexPath.row].cellType
}
}
This is the Flow Layout class:
protocol AppleNewsFlowLayoutDelegate: class {
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType
}
class AppleNewsFlowLayout: UICollectionViewFlowLayout {
var maxY: CGFloat = 0.0
var isVSetOnce = false
weak var delegate: AppleNewsFlowLayoutDelegate?
var attributesArray: [UICollectionViewLayoutAttributes]?
private var cache = [UICollectionViewLayoutAttributes]()
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
super.prepare()
minimumLineSpacing = 10
minimumInteritemSpacing = 16
sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
let screenWidth = UIScreen.main.bounds.width
var x = sectionInset.left
maxY = maxY + minimumLineSpacing
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
guard let newsType: NewsCellType = delegate?.AppleNewsFlowLayout(self, cellTypeForItemAt: indexPath) else {
fatalError("AppleNewsFlowLayoutDelegate method is required.")
}
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
switch newsType {
case .big:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: width * 1.2)
maxY += width * 1.2
case .horizontal:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: 150)
maxY += 150
case .vertical:
let width = (screenWidth - minimumInteritemSpacing - sectionInset.left - sectionInset.right) / 2
x = isVSetOnce ? x + width + minimumInteritemSpacing : x
maxY = isVSetOnce ? maxY-10 : maxY
attributes.frame = CGRect(x: x, y: maxY, width: width, height: screenWidth * 0.8)
if isVSetOnce {
maxY += screenWidth * 0.8
}
isVSetOnce = !isVSetOnce
}
cache.append(attributes)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attribute in cache{
if attribute.frame.intersects(rect){
layoutAttributes.append(attribute)
}
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
override var collectionViewContentSize: CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: maxY)
}
}
Really I have one issue about set UITableView Height Dynamically correct. I am not using AutoLayout. First time when table Loaded, It contains lots of space top and bottom of the cell and some time images are overlapping with other cells. I am unable to calculate the right height of cell due to images (image can be large in height )Below is code.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeight
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:Templategt4 = tableView.dequeueReusableCell(withIdentifier: "Templategt4Identify", for: indexPath) as! Templategt4
cell.delegate = self;
cell.configure( self.storyData[0], row: indexPath, screenSize: screenSize,
gt4tableview: self.storyTableView);
//self.secondWebViewLable.scrollView.contentSize.height
cellHeight = cell.firstWebViewLable.frame.height + cell.firstImage.frame.height + cell.secondWebViewLable.frame.height;
//cellHeight = cellHeight + 200
print("cellForRowAt cellHeight", cellHeight);
return cell
}
I am using Custom Cell. The cell contains to UiWebView, UIImageView ( Height can be too much long ). Below is the code of cell.
class Templategt4: UITableViewCell, UIWebViewDelegate{
#IBOutlet weak var firstWebViewLable: UIWebView!
#IBOutlet weak var firstImage: UIImageView!
#IBOutlet weak var secondWebViewLable: UIWebView!
#IBOutlet weak var playiconimage: UIImageView!
let padding = 24;
var contentHeights1 : [CGFloat] = [0.0]
var contentHeights2 : [CGFloat] = [0.0]
var imageHeights : [CGFloat] = [0.0]
var gt4tableview: UITableView? = nil
var indexpath: IndexPath? = nil
var delegate:ImageClickedForStoryDetails?
class MyTapGestureRecognizer: UITapGestureRecognizer {
var youTubeCode: String?
}
func configure(_ data: StoryDetailsRequest, row: IndexPath, screenSize: CGRect, gt4tableview: UITableView) {
self.gt4tableview = gt4tableview;
self.indexpath = row;
let callData = data.content[(row as NSIndexPath).row];
let url = Constants.TEMP_IMAGE_API_URL + callData.image;
// this is first webview data.
self.firstWebViewLable.frame.size.height = 1
self.firstWebViewLable.frame.size = firstWebViewLable.sizeThatFits(.zero)
let htmlString1:String! = callData.text1
self.firstWebViewLable.delegate = self;
self.firstWebViewLable.loadHTMLString(htmlString1, baseURL: nil)
// this is second webview data.
self.secondWebViewLable.frame.size.height = 1
self.secondWebViewLable.frame.size = secondWebViewLable.sizeThatFits(.zero)
let htmlString2:String! = callData.text2
self.secondWebViewLable.loadHTMLString(htmlString2, baseURL: nil)
self.secondWebViewLable.delegate = self;
if( !callData.image.isEmpty ) {
let range = url.range(of: "?", options: .backwards)?.lowerBound
var u:URL!
if(url.contains("?")) {
u = URL(string: url.substring(to: range!))
} else {
u = URL(string: url)
}
self.firstImage.image = nil
self.firstImage.kf.setImage(with: u, placeholder: nil,
options: nil, progressBlock: nil, completionHandler: { image, error, cacheType, imageURL in
if( image != nil) {
let imageW = (image?.size.width)!;
let imageheight = (image?.size.height)!;
self.firstImage.image = image;
self.firstImage.contentMode = UIViewContentMode.scaleAspectFit
self.firstImage.clipsToBounds = false
let ratio = self.firstImage.frame.size.width/imageW;
let scaledHeight = imageheight * ratio;
if(scaledHeight < imageheight)
{
//update height of your imageView frame with scaledHeight
self.firstImage.frame = CGRect(x: 0,
y: Int(self.firstWebViewLable.frame.origin.y + self.firstWebViewLable.frame.height),
width: Int(screenSize.width)-self.padding,
height: Int(scaledHeight) )
} else {
self.firstImage.frame = CGRect(x: 0,
y: Int(self.firstWebViewLable.frame.origin.y + self.firstWebViewLable.frame.height),
width: Int(screenSize.width)-self.padding,
height: Int(imageheight) )
}
} else {
self.firstImage.image = UIImage(named: "defaultimg")
}
self.secondWebViewLable.frame = CGRect(x: 0,
y: Int(self.firstImage.frame.origin.y + self.firstImage.frame.height),
width: Int(screenSize.width)-self.padding,
height: Int(self.secondWebViewLable.frame.height) )
if (self.imageHeights[0] == 0.0)
{
self.imageHeights[0] = self.firstImage.frame.height
UIView.performWithoutAnimation {
let ipath = IndexPath(item: (self.indexpath?.row)!, section: (self.indexpath?.section)!)
self.gt4tableview?.reloadRows(at: [ipath], with: UITableViewRowAnimation.none)
}
}
})
}
}
func webViewDidStartLoad(_ webView: UIWebView)
{
//myActivityIndicator.startAnimating()
}
func webViewDidFinishLoad(_ webView: UIWebView)
{
if(webView == self.firstWebViewLable) {
if (contentHeights1[0] != 0.0)
{
// we already know height, no need to reload cell
return
}
contentHeights1[0] = self.firstWebViewLable.scrollView.contentSize.height
UIView.performWithoutAnimation {
let ipath = IndexPath(item: (self.indexpath?.row)!, section: (self.indexpath?.section)!)
self.gt4tableview?.reloadRows(at: [ipath], with: UITableViewRowAnimation.none)
}
} else if(webView == self.secondWebViewLable) {
if (contentHeights2[0] != 0.0)
{
// we already know height, no need to reload cell
return
}
contentHeights2[0] = self.secondWebViewLable.scrollView.contentSize.height
UIView.performWithoutAnimation {
let ipath = IndexPath(item: (self.indexpath?.row)!, section: (self.indexpath?.section)!)
self.gt4tableview?.reloadRows(at: [ipath], with:UITableViewRowAnimation.none)
}
}
}
You should not calculate the cell height in cellForRow method.
and instead of heightForRowAt: use this method
TableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
and in your viewDidLoad call this.
tableView.rowHeight = UITableViewAutomaticDimension
In my application there is a collection view, that must update (load new posts at bottom), when i reach last - 10 item.
I am using Alamofire to get new posts.
Also, my collection view has custom layout.
Strange thing: when application loads up for the first time, collection view updates properly and I can see first portion of posts, but when I scroll and call a method to load more, it loads posts, appends it to data source, but collection view doesn't get update, even if I try to update it on main queue.
My code:
FeedViewController.swift:
private let reuseIdentifier = "Cell"
class FeedViewController: UICollectionViewController {
var posts : [GPost] = []
var offsetForPosts = 0
let limitForPosts = 50
override func viewDidLoad() {
super.viewDidLoad()
if let layout = collectionView?.collectionViewLayout as? FeedLayout {
layout.delegate = self
}
self.collectionView?.registerNib(UINib(nibName: "GCell", bundle: nil), forCellWithReuseIdentifier: reuseIdentifier)
let initialPath : URLStringConvertible = "\(DEFAULT_PATH)api_key=\(API_KEY)&offset=\(offsetForPosts)&limit=\(self.limitForPosts)"
self.loadPostsForPath(initialPath) { (_posts) in
self.posts.appendContentsOf(_posts)
print(self.posts.count)
dispatch_async(dispatch_get_main_queue(), {
self.collectionView?.reloadData()
})
}
}
func loadPostsForPath(path: URLStringConvertible, completion: (posts: [GPost]) -> ()) {
Alamofire.request(.GET, path, parameters: nil, encoding: .JSON, headers: nil).responseJSON { (response) in
switch response.result {
case .Success(let data):
let JSONData = JSON(data)
var result = [GPost]()
for (_, value) in JSONData["data"] {
let post = GPost(id: value["id"].stringValue,
url: NSURL(string: value["images"]["fixed_width_downsampled"]["url"].stringValue)!,
slug: value["slug"].stringValue,
imageWidth: CGFloat(value["images"]["original"]["width"].floatValue),
imageHeight: CGFloat(value["images"]["original"]["height"].floatValue))
result.append(post)
}
completion(posts: result)
break
case .Failure(let error):
print("Error loading posts: \(error)")
break
}
}
}
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if indexPath.item == posts.count - 10 {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), {
self.offsetForPosts += 50
let path : URLStringConvertible = "\(DEFAULT_PATH)api_key=\(API_KEY)&offset=\(self.offsetForPosts)&limit=\(self.limitForPosts)"
self.loadPostsForPath(path, completion: { (_posts) in
self.posts.appendContentsOf(_posts)
print(self.posts.count)
dispatch_async(dispatch_get_main_queue(), {
self.collectionView?.reloadData()
self.collectionView?.collectionViewLayout.invalidateLayout()
})
})
})
}
}
}
extension FeedViewController : FeedLayoutDelegate {
func collectionView(collectionView: UICollectionView, heightForPostAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
let post = posts[indexPath.item]
return width * post.getProportion() + 30
}
}
FeedLayout.swift:
protocol FeedLayoutDelegate {
func collectionView(collectionView: UICollectionView, heightForPostAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat
}
class FeedLayout: UICollectionViewLayout {
var delegate : FeedLayoutDelegate!
var numberOfColumns = 2
var cellPadding : CGFloat = 0.0
private var cache = [UICollectionViewLayoutAttributes]()
private var contentHeight : CGFloat = 0.0
private var contentWidth : CGFloat {
let insets = collectionView!.contentInset
return CGRectGetWidth(collectionView!.bounds) - (insets.left + insets.right)
}
override func prepareLayout() {
if cache.isEmpty {
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset = [CGFloat]()
for column in 0..<numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset = [CGFloat](count: numberOfColumns, repeatedValue: 0)
for item in 0..<collectionView!.numberOfItemsInSection(0) {
let indexPath = NSIndexPath(forItem: item, inSection: 0)
let width = columnWidth - cellPadding * 2
let height = cellPadding + delegate.collectionView(collectionView!, heightForPostAtIndexPath: indexPath, withWidth: width) + cellPadding
let frame = CGRectMake(xOffset[column], yOffset[column], columnWidth, height)
let insetFrame = CGRectInset(frame, cellPadding, cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, CGRectGetMaxY(frame))
yOffset[column] = yOffset[column] + height
column = column >= (numberOfColumns - 1) ? 0 : column + 1
}
}
}
override func collectionViewContentSize() -> CGSize {
return CGSizeMake(contentWidth, contentHeight)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if CGRectIntersectsRect(attributes.frame, rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}