I have a TableView with cells, and one cell is holding a CollectionView.
Inside the CollectionView, I have cells with UIImageViews.
If I add new elements to the datasource while the CollectionView is visible then it works fine.
But if I scroll down in the TableView, add the new elements then scroll up, then even though it adds the new cells, they are displaying the wrong image.
Video: https://youtu.be/QwvMv2xaaAI
Code:
MainViewController(Not the whole)
func addNewPhotos(newPhotosArray: [Photo]){
var collectionViewInserts : [IndexPath] = []
for (i in 0...newPhotosArray.count) {
// I add the new photos to the datasource
PhotosStore.shared.photos.insert(newPhotosArray[i], at: 0)
// Then save the indexPath what needs to be inserted
collectionViewInserts.insert(IndexPath(row: i, section: 0), at: 0)
}
if let cell = self.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? PhotosCell {
cell.photosCollectionView.performBatchUpdates({
cell.photosCollectionView.insertItems(at: collectionViewInserts)
}, completion: nil)
}
}
extension MainViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return PhotosStore.shared.photos.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell
cell.photoImageView.downloadedFrom(link: (appSettings.url + "/resources/img/wp/prev/" + PhotosStore.shared.photos[indexPath.item].fileName))
return cell
}
}
PhotosCell:
import UIKit
class PhotosCell : UITableViewCell{
#IBOutlet weak var photosCollectionView : UICollectionView!
}
extension PhotosCell {
func setCollectionViewDataSourceDelegate<D: UICollectionViewDataSource & UICollectionViewDelegate>(_ dataSourceDelegate: D, forRow row: Int) {
// IF I PLACE A .reloadData() HERE, THEN IT WORKS BUT THEN THE CELL FLICKERS/JUMPS WHEN APPEARING ON SCREEN
let itemSize = 70
photosCollectionView.delegate = dataSourceDelegate
photosCollectionView.dataSource = dataSourceDelegate
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = UICollectionViewScrollDirection.horizontal
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.itemSize = CGSize(width: itemSize, height: itemSize)
photosCollectionView.setCollectionViewLayout(layout, animated: true)
photosCollectionView.tag = row
photosCollectionView.setContentOffset(photosCollectionView.contentOffset, animated:false) // Stops collection view if it was scrolling.
photosCollectionView.reloadData()
}
var collectionViewOffset: CGFloat {
set { photosCollectionView.contentOffset.x = newValue }
get { return photosCollectionView.contentOffset.x }
}
}
What do I wrong? I do update the datasource correctly, I do perform batch updates on the collection view to insert the correct cells..
Updated details:
MainViewController:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Photos on top
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "PhotosCell", for: indexPath) as! PhotosCell
cell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row)
cell.collectionViewOffset = storedPhotosCollectionViewOffset[indexPath.row] ?? 0
return cell
}
... other cells ...
}
Extension to download images: (I'm sure that's not the problem but just in case)
extension UIImageView {
func downloadedFrom(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit) {
image = nil
if let cachedImage = ImageCache.shared.loadCachedImage(url: url) {
image = cachedImage
return
}
contentMode = mode
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else {
DispatchQueue.main.async {
self.image = UIImage(named: "imageMissing")
}
return
}
DispatchQueue.main.async {
self.image = image
ImageCache.shared.cacheImage(image: image, url: url)
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
return downloadedFrom(url: url, contentMode: mode)
}
}
First in cellForRowAt
cell.photosCollectionView.reloadData()
return cell
Second you have to note that the image is downloaded ( consider a dummy image for the imageView or set a background to it ) every scroll so use SDWebImage
Related
I'm having problem with didSelectItemAt in a UICollectionViewCell. I'm using a third party framework to select multiple photos from the library. The selected assets are then transferred and stored in a array of PHAssets. I use a small script for custom preview of the images when tapped on them. (It basically expands the image to full screen and put in and then you can swipe up or down to dismiss it). The problem is not with the images, they're properly displayed in my custom UICollectionViewCell. The problem is when I tap on them. For some reason it gives the wrong image and I'm not sure why.
var cameraPhotoUIImage: UIImage?
var assets = [PHAsset]()
lazy var assetsTurnedIntoImages =
{
return [UIImage]()
}()
lazy var imageManager = {
return PHCachingImageManager()
}()
extension PhotoVC : UICollectionViewDataSource, UICollectionViewDelegate
{
func setupCollectionView()
{
collectionView.dataSource = self
collectionView.delegate = self
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoPostCVCell", for: indexPath) as! PhotoPostCVCell
if let takenImage = cameraPhotoUIImage
{
cell.cellImage.image = takenImage
}
if assets.count > 0
{
let asset = assets[indexPath.row]
imageManager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil)
{ (image, info) in
cell.cellImage.contentMode = .scaleAspectFill
cell.cellImage.image = image!
self.assetsTurnedIntoImages.append(image!)
}
}
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
if assets.count > 0
{
return assets.count
}
else
{
return 1
}
}
// MARK: Preview Selected Image
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let postStoryboard = UIStoryboard(name: Storyboard_Post, bundle:nil)
if let destinationVC = postStoryboard.instantiateViewController(withIdentifier: "PreviewImageVC") as? PreviewImageVC
{
destinationVC.allowedDismissDirection = .both
destinationVC.maskType = .clear
if cameraPhotoUIImage != nil
{
destinationVC.transferedImageToPreview.image = cameraPhotoUIImage
destinationVC.showInteractive()
}
if assetsTurnedIntoImages.count > 0
{
print("Selected image to preview:",assetsTurnedIntoImages[indexPath.row] )
destinationVC.transferedImageToPreview.image = assetsTurnedIntoImages[indexPath.item]
destinationVC.showInteractive()
}
}
}
}
You should update the requestImage method. requestImage method looks like async. So that request does not wait block to complete. If you update your block like this, it should work.
imageManager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil)
{ (image, info) in
DispatchQueue.main.async {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoPostCVCell", for: indexPath) as! PhotoPostCVCell
cell.cellImage.contentMode = .scaleAspectFill
cell.cellImage.image = image!
self.assetsTurnedIntoImages.append(image!)
}
}
When I use collectionView without
let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.estimatedItemSize = CGSize (width: self.collectionView.frame.width, height: 100)
then all the cells are displayed but when I use flowLayout then cells are not displayed I've been sitting here for a week on this and can not understand why this is happening.
here my code
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.estimatedItemSize = CGSize(width: self.collectionView.frame.width, height: 100)
requestD(url: url)
}
// MARK: UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return modals.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if modals.count == 0 {
return UICollectionViewCell()
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCellAudio
cell.name?.text = modals[indexPath.row].name
let u = URL(string: "http://www.---.com" + (modals[indexPath.row].ImageViewURL))
cell.ImageView.sd_setImage(with: u, placeholderImage: UIImage(named: "white"),
options: [.continueInBackground,.scaleDownLargeImages]) { (image, error, cacheType, url) in
self.modals[indexPath.row].ImageView = cell.ImageView.image!
}
return cell
}
func requestD(url:String){
var urlRequest = URLRequest(url: URL(string: url)!)
urlRequest.timeoutInterval = 300
let task = URLSession.shared.dataTask(with: urlRequest) { (data,response,error) in
if error != nil{
print(error ?? 0)
return
}
do{
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! [String : Any]
if let main = json["LIBRARY"] as? [String:[String : Any]]{
for (_, data) in main {
let info = ModalAudio()
info.id = data["ID"] as? String
info.name = data["NAME"] as? String
info.AboutBook = data["DETAIL_TEXT"] as? String
info.ImageViewURL = data["PICTURE"] as! String
self.modals.append(info)
}
}
} catch let error {
print(error)
}
DispatchQueue.main.sync {
self.isMoreDataLoading = false
self.iNumPage += 1
}
}
self.collectionView?.reloadData()
task.resume()
}
In the viewDidLoad the frame of your collection view is undefined yet, so put this code in your viewDidLayoutSubView method, after reviewing your project your issues is related to use of flowLayout.estimatedItemSize as you can read in the property declaration comment
#available(iOS 8.0, *)
open var estimatedItemSize: CGSize // defaults to CGSizeZero - setting a non-zero size enables cells that self-size via -preferredLayoutAttributesFittingAttributes:
So use this code, it works!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
//flowLayout.estimatedItemSize = CGSize(width: self.collectionView.frame.width, height: 100) don't show your cells
flowLayout.itemSize = CGSize(width: self.collectionView.frame.width, height: 100)//show your cells
flowLayout.invalidateLayout()
}
Hope this helps
I have a custom UICollectionView cell with an imageView and a label, as listed below:
import UIKit
class PollCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var pollQuestion: UILabel!
}
I want to make each populated cell clickable and then pass information from that clicked cell into a new ViewController. Below is the ViewController that populates the custom cell.
import UIKit
import FirebaseDatabase
import FirebaseDatabaseUI
import FirebaseStorageUI
private let reuseIdentifier = "PollCell"
class TrendingViewController: UICollectionViewController {
var ref: FIRDatabaseReference!
var dataSource: FUICollectionViewDataSource!
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Register cell classes
self.dataSource = self.collectionView?.bind(to: self.ref.child("Polls")) { collectionView, indexPath, snap in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! PollCell
/* populate cell */
cell.pollQuestion.text = snap.childSnapshot(forPath: "question").value as! String?
let urlPollImage = snap.childSnapshot(forPath: "image_URL").value as! String?
cell.imageView.sd_setImage(with: URL(string: urlPollImage!), placeholderImage: UIImage(named: "Fan_Polls_Logo.png"))
//Comment
return cell
}
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath)
}
}
I also have some code to "invert" the cells that populate in my inverted ViewController:
import Foundation
import UIKit
class InvertedStackLayout: UICollectionViewLayout {
let cellHeight: CGFloat = 100.00 // Your cell height here...
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttrs = [UICollectionViewLayoutAttributes]()
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numberOfSectionItems = numberOfItemsInSection(section) {
for item in 0 ..< numberOfSectionItems {
let indexPath = IndexPath(item: item, section: section)
let layoutAttr = layoutAttributesForItem(at: indexPath)
if let layoutAttr = layoutAttr, layoutAttr.frame.intersects(rect) {
layoutAttrs.append(layoutAttr)
}
}
}
}
}
return layoutAttrs
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAttr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let contentSize = self.collectionViewContentSize
layoutAttr.frame = CGRect(
x: 0, y: contentSize.height - CGFloat(indexPath.item + 1) * cellHeight,
width: contentSize.width, height: cellHeight)
return layoutAttr
}
func numberOfItemsInSection(_ section: Int) -> Int? {
if let collectionView = self.collectionView,
let numSectionItems = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: section)
{
return numSectionItems
}
return 0
}
override var collectionViewContentSize: CGSize {
get {
var height: CGFloat = 0.0
var bounds = CGRect.zero
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numItems = numberOfItemsInSection(section) {
height += CGFloat(numItems) * cellHeight
}
}
bounds = collectionView.bounds
}
return CGSize(width: bounds.width, height: max(height, bounds.height))
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if let oldBounds = self.collectionView?.bounds,
oldBounds.width != newBounds.width || oldBounds.height != newBounds.height
{
return true
}
return false
}
}
You must to implement this method of UICollectionViewDelegate
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
debugPrint("this works")
}
I hope this helps you
This Function used to select the row of your collection view
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
}
Then you use function call. Whatever you need, just Defined the inside function. And easily called the function during the selected the row (inside the didSelectItemAt indexPath)
Make sure you have enabled UserIntraction for both UILabel and UIImageView, because both have default value as false.
self.dataSource = self.collectionView?.bind(to: self.ref.child("Polls")) { collectionView, indexPath, snap in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! PollCell
/* populate cell */
cell.pollQuestion.userInteractionEnabled = true;
cell.imageView.userInteractionEnabled = true;
cell.pollQuestion.text = snap.childSnapshot(forPath: "question").value as! String?
let urlPollImage = snap.childSnapshot(forPath: "image_URL").value as! String?
cell.imageView.sd_setImage(with: URL(string: urlPollImage!), placeholderImage: UIImage(named: "Fan_Polls_Logo.png"))
//Comment
return cell
}
Then you need to use of UICollectionViewDelegate method:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// handle tap events
}
Add a segue in your storyboard from the cell to the view controller you want to transition to. You can use the prepare for segue method, prepare(for:sender:), to pass your data. Using the segue (UIStoryboarySegue documentation) parameter, you can access the 'destinationViewController`.
Check out the View Controller for iOS Programming Guide: Using Segues for more information about using segues.
I did a grid (collectionView) inside a tableViewCell, the problem is loading different images per cell. Make a Json like this:
{
{
"name": "Vegetales"
"images": { imagesURLStrings }
},
{
"name": "Frutas"
"images": { imagesURLStrings }
},
}
I use this page for custom the view and this other to make the async download.
I think the problem is because, when I try to defined the quantity of cells for the collectionView inside the tableviewCell, the assignation its wrong, its not working, and I don't know how to fixed.
The code for download the images:
func loadImages() {
var section = 0
var row = 0
while (section < searchDataresults.count) {
for i in searchDataresults[section].images {
let key = section * 10 + row
let imageUrl = i
let url:URL! = URL(string: imageUrl)
task = session.downloadTask(with: url, completionHandler: { (location, response, error) -> Void in
if let data = try? Data(contentsOf: url){
// 4
DispatchQueue.main.async(execute: { () -> Void in
// 5
// Before we assign the image, check whether the current cell is visible
let img:UIImage! = UIImage(data: data)
saveImage(image: img, name: String(key))
})
}
})
task.resume()
row += 1
}
section += 1
row = 0
}
}
}
And the code were I put the images on the collectionView, remembering that it is inside a tableViewCell, so the quantity of cells have to change depending of the images.count of the json.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return searchDataresults[cellLoad].images.count
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellImage", for: indexPath as IndexPath)
let key = cellLoad * 10 + indexPath.row
if let img = loadImage(name: String(key)) {
let imageView = UIImageView(image: img)
imageView.frame = cell.frame
imageView.bounds = cell.bounds
imageView.center = cell.center
cell.contentView.addSubview(imageView)
print(key)
} else {
let imageView = UIImageView(image: UIImage(named: "emptyImg"))
imageView.frame = cell.frame
imageView.bounds = cell.bounds
imageView.center = cell.center
cell.contentView.addSubview(imageView)
}
return cell
}
I really appreciate your help!
subclass UICollectionviewCell and reset the content of your collection view cell
override func prepareForReuse() {
super.prepareForReuse()
self.customImageview.image = nil
}
I am learning how to create a UICollectionView programmatically. I want to create a grid of pictures collected from the user in another part of the app.
Will this sample code help me accomplish this? Also, how do I configure the data to emit the image I want? My source code is below.
UICollectionView:
class PhotosViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
let imageStore = ImageStore()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
layout.itemSize = CGSize(width: 100, height: 100)
let myCollectionView:UICollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
myCollectionView.dataSource = self
myCollectionView.delegate = self
myCollectionView.registerClass(RDCellCollectionViewCell.self, forCellWithReuseIdentifier: "MyCell")
myCollectionView.backgroundColor = UIColor.whiteColor()
self.view.addSubview(myCollectionView)
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
var images: [UIImage] = [
]
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let myCell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath) as! RDCellCollectionViewCell
myCell.imageView.image = images[indexPath.item]
myCell.backgroundColor = UIColor.grayColor()
return myCell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
print("User tapped on item \(indexPath.row)")
}
}
ImageStore.swift:
class ImageStore: NSObject {
let cache = NSCache()
func setImage(image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key)
let imageURL = imageURLForKey(key)
if let data = UIImageJPEGRepresentation(image, 0.5) {
data.writeToURL(imageURL, atomically: true)
}
}
func imageForKey(key: String) -> UIImage? {
if let existingImage = cache.objectForKey(key) as? UIImage {
return existingImage
}
let imageURL = imageURLForKey(key)
guard let imageFromDisk = UIImage(contentsOfFile: imageURL.path!) else {
return nil
}
cache.setObject(imageFromDisk, forKey: key)
return imageFromDisk
}
func deleteImageForKey(key: String) {
cache.removeObjectForKey(key)
let imageURL = imageURLForKey(key)
do {
try NSFileManager.defaultManager().removeItemAtURL(imageURL)
}
catch let deleteError {
print("Error removing the image from disk: \(deleteError)")
}
}
func imageURLForKey(key: String) -> NSURL {
let documentsDirectories =
NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let documentDirectory = documentsDirectories.first!
return documentDirectory.URLByAppendingPathComponent(key)
}
}
You're on the right track. You'll need to create a subclass of UICollectionViewCell that contains a UIImageView; this will let you plug the correct UIImage into it in cellForItemAtIndexPath.
This describes how to hook up your custom cell:
Create UICollectionViewCell programmatically without nib or storyboard
As for getting the correct image, you'll need to map the index path to your image store somehow, so that an item number corresponds to the correct image key.
If the task is to add an image, you should use something like this in cellForItemAtIndexPath:
let myCell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath)
myCell.backgroundColor = UIColor.blueColor()
let imageView = UIImageView(frame: cell.contentView.frame)
cell.contentView.addSubview(imageView)
imageView.image = //Here you should get right UIImage like ImageStore().imageForKey("YOUR_KEY")
return myCell
Or you can use custom UICollectionViewCell subclass as Joshua Kaden wrote.