I am using DKImagePickerController to select multiple images, displaying them on my VC and them when I press a button they upload to Parse.
I load 7 images, but only the ones visible on the screen (Which is 4, as I am using a collectionView with a horizontal scroll) upload at the begining then if I scroll across to the end and upload again It uploads all 7 images. And if I scroll back and then forwards again the images seem to add up 7 at a time, so if I scrolled 4 times back and forth I would end up uploading 28 images when I only selected 7!!
I'm guessing it something to do with the way I add the images I have selected to the image Array which I am doing in the cellForItemAtIndexPath.
Here is my code below, if anyone has incounted this before or knows the issue you help is very much appreciated!
#IBOutlet weak var previewOrShareButton: UIBarButtonItem!
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var cancelButton: UIBarButtonItem!
var assets: [DKAsset]?
var images = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func selectImage(sender: AnyObject) {
let pickerController = DKImagePickerController()
pickerController.didSelectAssets = { (assets: [DKAsset]) in
print("didSelectAssets")
self.assets = assets
self.collectionView.reloadData()
}
pickerController.defaultSelectedAssets = self.assets
pickerController.maxSelectableCount = 7
pickerController.allowMultipleTypes = false
self.presentViewController(pickerController, animated: true) {}
}
#IBAction func postButton(sender: AnyObject) {
let post = PFObject(className: "Post")
post["username"] = PFUser.currentUser()!.username
for i in self.images.indices {
let imageData = self.images[i].lowestQualityJPEGNSData
let imageFile = PFFile(name: "image.JPEG", data: imageData)
post["imageArray\(i)"] = imageFile
}
post.saveInBackgroundWithBlock ({(success:Bool, error:NSError?) -> Void in
if error == nil {
}})
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return self.assets?.count ?? 0
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! BlogCollectionViewCell
let asset = self.assets![indexPath.row]
asset.fetchOriginalImageWithCompleteBlock { (image, info) -> Void in
cell.image.image = image
self.images.append(image!)
}
print(images)
return cell
}
Anything in your cellForRow will run every time a cell is scrolled to so it's fetching too many images. Ideally add the following to your viewDidLoad unless you need to the user to download the images then add this to a button:
for object in assets {
object.fetchOriginalImageWithCompleteBlock { (image, info) -> Void in
self.images.append(image!)
pickerController.reloadData()
}
}
That will load all of the images into the array and then you can populate your cells with the images:
cell.image = images[indexPath.row]
Yes you are doing it in wrong way, as you are fetching the images in cellForItemAtIndexPath and then adding the images in array in the same method. So when you scroll your UICollectionView then images will fetched and added to the array as cellForItemAtIndexPath will called when you scroll or reload your UICollectionView.
I will make some changes in your code, hope it will help you.
#IBAction func selectImage(sender: AnyObject) {
let pickerController = DKImagePickerController()
pickerController.didSelectAssets = { (assets: [DKAsset]) in
print("didSelectAssets")
assets.fetchOriginalImageWithCompleteBlock { (image, info) -> Void in
self.images.append(image!)
self.collectionView.reloadData()
}
}
pickerController.defaultSelectedAssets = self.assets
pickerController.maxSelectableCount = 7
pickerController.allowMultipleTypes = false
self.presentViewController(pickerController, animated: true) {}
}
and then cellForItemAtIndexPath will be
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! BlogCollectionViewCell
let asset = self.assets![indexPath.row]
cell.image.image = image
return cell
}
Related
The gifs I fetch from the Giphy API returns correctly and in fact loads properly to the uicollectionview using SwiftGif.
The issue only surfaces when I scroll immediately, the uicollectionview loads either duplicate gifs or gifs that are in the incorrect index. I understand this is probably a timing issue with the delay in rendering the gif and loading the gif to the cell.
Any guidance would be appreciated as asynchronous operations are something I'm still unfamiliar with..
Also any best practices for handling gifs would be appreciated if there are any flags in the code below, specifically to support speed/memory usage.
I've tried placing various checks like seeing if the initially passed gif url is the same at the point it's loaded, and also setting the image to nil every time cellForItemAt is fired, but to no avail. Couldn't find existing threads that clearly resolved this issue as well.
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var gifCollectionView: UICollectionView!
var gifUrls: [String] = []
var gifImages: [String: UIImage] = [:]
func fetchGiphs() {
let op = GiphyCore.shared.search("dogs", media: .sticker) { (response, error) in
guard error == nil else {
print("Giphy Fetch Error: ", error)
return
}
if let response = response, let data = response.data, let pagination = response.pagination {
for result in data {
if let urlStr = result.images?.downsized?.gifUrl {
self.gifUrls.append(urlStr)
}
}
if !self.gifUrls.isEmpty {
DispatchQueue.main.async {
self.gifCollectionView.reloadData()
}
}
} else {
print("No Results Found")
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return gifUrls.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! GifCell
let passedUrlString = gifUrls[indexPath.item]
cell.imageView.image = nil
if let image = gifImages[gifUrls[indexPath.item]] {
DispatchQueue.main.async {
cell.imageView.image = image
cell.activityIndicator.isHidden = true
}
} else {
cell.activityIndicator.isHidden = false
cell.activityIndicator.startAnimating()
DispatchQueue.global(qos: .default).async {
let gifImage = UIImage.gif(url: self.gifUrls[indexPath.item])
DispatchQueue.main.async {
if passedUrlString == self.gifUrls[indexPath.item] {
cell.activityIndicator.stopAnimating()
cell.activityIndicator.isHidden = true
cell.imageView.image = gifImage
self.gifImages[self.gifUrls[indexPath.item]] = gifImage
}
}
}
}
return cell
}
}
class GifCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
}
As you know, the cell may be reused when image loading completed.
You need to check if it is reused or not. Your passedUrlString == self.gifUrls[indexPath.item] does not work for this purpose.
Maybe, giving a unique ID for each cell would work:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! GifCell
let uniqueId = Int.random(in: Int.min...Int.max) //<-practically unique
cell.tag = uniqueId //<-
cell.imageView.image = nil
if let image = gifImages[gifUrls[indexPath.item]] {
cell.imageView.image = image
cell.activityIndicator.isHidden = true
} else {
cell.activityIndicator.isHidden = false
cell.activityIndicator.startAnimating()
DispatchQueue.global(qos: .default).async {
let gifImage = UIImage.gif(url: self.gifUrls[indexPath.item])
DispatchQueue.main.async {
if cell.tag == uniqueId { //<- check `cell.tag` is not changed
cell.activityIndicator.stopAnimating()
cell.activityIndicator.isHidden = true
cell.imageView.image = gifImage
self.gifImages[self.gifUrls[indexPath.item]] = gifImage
}
}
}
}
return cell
}
Assuming you are not using tag for other purposes.
Please try.
I'm downloading images using DropBox's API and displaying them in a Collection View. When the user scrolls, the image either disappears from the cell and another is loaded or a new image is reloaded and replaces the image in the cell. How can I prevent this from happening? I've tried using SDWebImage, this keeps the images in the right order but still the images disappear and reload each time they are scrolled off screen. Also, I'm downloading the images directly, not from a URL, I'd prefer to not have to write a work-a-round to be able to use SDWebImage.
I'd post a gif as example but my reputation is too low.
Any help would be welcomed :)
var filenames = [String]()
var selectedFolder = ""
// image cache
var imageCache = NSCache<NSString, UIImage>()
override func viewDidLoad() {
super.viewDidLoad()
getFileNames { (names, error) in
self.filenames = names
if error == nil {
self.collectionView?.reloadData()
print("Gathered filenames")
}
}
collectionView?.collectionViewLayout = gridLayout
collectionView?.reloadData()
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
}
func getFileNames(completion: #escaping (_ names: [String], _ error: Error?) -> Void) {
let client = DropboxClientsManager.authorizedClient!
client.files.listFolder(path: "\(selectedFolder)", recursive: false, includeMediaInfo: true, includeDeleted: false, includeHasExplicitSharedMembers: false).response { response, error in
var names = [String]()
if let result = response {
for entry in result.entries {
if entry.name.hasSuffix("jpg") {
names.append(entry.name)
}
}
} else {
print(error!)
}
completion(names, error as? Error)
}
}
func checkForNewFiles() {
getFileNames { (names, error) in
if names.count != self.filenames.count {
self.filenames = names
self.collectionView?.reloadData()
}
}
}
func downloadFiles(fileName: String, completion:#escaping (_ image: UIImage?, _ error: Error?) -> Void) {
if let cachedImage = imageCache.object(forKey: fileName as NSString) as UIImage? {
print("using a cached image")
completion(cachedImage, nil)
} else {
let client = DropboxClientsManager.authorizedClient!
client.files.download(path: "\(selectedFolder)\(fileName)").response { response, error in
if let theResponse = response {
let fileContents = theResponse.1
if let image = UIImage(data: fileContents) {
// resize the image here and setObject the resized Image to save it to cache.
// use resized image for completion as well
self.imageCache.setObject(image, forKey: fileName as NSString)
completion(image, nil) // completion(resizedImage, nil)
}
else {
completion(nil, error as! Error?)
}
} else if let error = error {
completion(nil, error as? Error)
}
}
.progress { progressData in
}
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.filenames.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCell
cell.backgroundColor = UIColor.lightGray
let fileName = self.filenames[indexPath.item]
let cellIndex = indexPath.item
self.downloadFiles(fileName: fileName) { (image, error) in
if cellIndex == indexPath.item {
cell.imageCellView.image = image
print("image download complete")
}
}
return cell
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
gridLayout.invalidateLayout()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
imageCache.removeAllObjects()
}
Because TableView's and CollectionView's use the
dequeueReusableCell(withReuseIdentifier: for indexPath:) function when you configure a new cell, what swift does under the table is use a cell that is out of the screen to help the memory of your phone and probably that cell already has a image set and you have to handle this case.
I suggest you to look at the method "prepareCellForReuse" in this case what I think you have to do is set the imageView.image atribute to nil.
I have pretty sure that it will solve your problem or give you the right direction, but if it doesn't work please tell me and I will try to help you.
Best results.
I fixed it. It required setting the cell image = nil in the cellForItemAt func and canceling the image request if the user scrolled the cell off screen before it was finished downloading.
Here's the new cellForItemAt code:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let fileId = indexPath.item
let fileName = self.filenames[indexPath.item]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCell
cell.backgroundColor = UIColor.lightGray
if cell.request != nil {
print("request not nil; cancel ", fileName)
}
cell.request?.cancel()
cell.request = nil
cell.imageCellView.image = nil
print ("clear image ", fileId)
self.downloadFiles(fileId:fileId, fileName: fileName, cell:cell) { (image, error) in
guard let image = image else {
print("abort set image ", fileId)
return
}
cell.imageCellView.image = image
print ("download/cache: ", fileId)
}
return cell
}
Use SDWebImage and add a placeholder image :
cell.imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))
I post this in case it may help someone.
I have a collection view (displayed as a vertical list) whose items are collection views (displayed as horizontal single-line grids). Images in the child-collection views were repeated when the list was scrolled.
I solved it by placing this in the class of the cells of the parent collection view.
override func prepareForReuse() {
collectionView.reloadData()
super.prepareForReuse()
}
Main Controller
class MainController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {
#IBOutlet weak var collectionView: UICollectionView!
let reUseCellName = "imgCell"
var counter = 1
override func viewDidLoad() {
super.viewDidLoad()
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reUseCellName, forIndexPath:indexPath) as! CellClass
cell.imageView.image = UIImage(named: "\(counter)")
cell.imgName = counter
counter++
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let newView = segue.destinationViewController as! showImage
let cell = sender as! CellClass
newView.imgNo = cell.imgName
}
I think its the way how your using them, try to define an array with images:
var arrayOfImages = ["1","2","3","4"]
And use it in cellForItemAtIndexPath :
cell.imageView.image = UIImage(named: arrayOfImages[indexPath.row])
Be sure to connect DataSoruce and Delegate for collectionView from connection inspector into your view controller to have the data loaded, and be sure to place the right image names.
Use the following code . the reason for the image not being displayed is the you have not mentioned the file extension with the image as jpg or png ... your counter value is "1" so app cannnot load the image name 1 without extension png or jpg
Use this code :
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reUseCellName, forIndexPath:indexPath) as! CellClass
cell.imageView.image = UIImage(named: "Complete image name with .png/.jpg")
cell.imgName = counter
counter++
return cell
}
I'm working on an onboarding flow for my iOS App in Swift. I'd like to allow users to tap other users in a collection view and have it follow those users. I need the collection view to be able to allow multiple cells to be selected, store the cells in an array and run a function once the users taps the next button. Here's my controller code:
class FollowUsers: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var tableData: [SwiftyJSON.JSON] = []
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var loadingView: UIView!
private var selectedUsers: [SwiftyJSON.JSON] = []
override func viewDidLoad() {
super.viewDidLoad()
self.getCommunities()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func getUsers() {
Alamofire.request(.GET, "url", parameters: parameters)
.responseJSON {response in
if let json = response.result.value {
let jsonObj = SwiftyJSON.JSON(json)
if let data = jsonObj.arrayValue as [SwiftyJSON.JSON]? {
self.tableData = data
self.collectionView.reloadData()
self.loadingView.hidden = true
}
}
}
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.tableData.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: UserViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("userCell", forIndexPath: indexPath) as! UserViewCell
let rowData = tableData[indexPath.row]
if let userName = rowData["name"].string {
cell.userName.text = userName
}
if let userAvatar = rowData["background"].string {
let url = NSURL(string: userAvatar)
cell.userAvatar.clipsToBounds = true
cell.userAvatar.contentMode = .ScaleAspectFill
cell.userAvatar.hnk_setImageFromURL(url!)
}
cell.backgroundColor = UIColor.whiteColor()
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell: UserViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("userCell", forIndexPath: indexPath) as! UserViewCell
let rowData = tableData[indexPath.row]
let userName = rowData["name"].string
let userId = rowData["id"].int
selectedUsers.append(rowData[indexPath.row])
print("Cell \(userId) \(userName) selected")
}
}
override func viewDidLoad() {
super.viewDidLoad()
collection.dataSource = self
collection.delegate = self
collection.allowsMultipleSelection = true
self.getCommunities()
}
You should be able to make multiple selections with this.
in my app I need to display several images that I fetch from Parse and everything was great until I tested it on iPad mini and iPad 2...
To give you a better idea, my Home page is a tableview with 4 cells.
The first cell must display a "big" image (50-55KB), the second cell shows a table view of thumbnail images(5-8KB) - but no more than 5 collection view cells! - and the other 2 cells are used for text, buttons etc.
I'm using Parse as a backend and I started with its tools to work with images like PFImageView: this way to load an image from the server I was using these two methods:
theImageView.file = thePFObject["<PFFileEntity>"] as? PFFile
theImageView..loadInBackground()
Once the app finished to load the home page , used memory device was at 100-110Mb, really close to 125-130Mb at witch the app is closed by the system.
I replaced the lines above with:
let imageData = NSData(contentsOfURL: NSURL(string: (stringContainingTheURL))!)
dispatch_async(dispatch_get_main_queue()) {
self.recipeImageView.image = UIImage(data: imageData!) }
and the memory usage is now at 83-85Mb. Better but as I go into another view containing only an YTPlayerView (for embedding youTube video) and a collection view of 5 thumbnail images, it easily shut down.
My strategies to reduce memory impact are:
thumbnails images where possible;
avoid UIImage (named:) method;
avoid to work with PFFiles: this way then I store PFObjects into array, I work with strings instead of files;
call didReciveMemoryWarnings() where possible to release memory
I've checked that there aren't memory leaks but I don't know how to improve the behavior.
Any idea?
I'll leve you my code for the first two cells in order to compare with you.
class HomeTableViewController: UITableViewController, UICollectionViewDelegate, UICollectionViewDataSource {
//MARK: -outlets
var collectionView: UICollectionView! { didSet {collectionView.reloadData() } }
// MARK: - Model
let indexPathforcell0 = NSIndexPath(forRow: 0, inSection: 0)
let indexPathforcell1 = NSIndexPath(forRow: 1, inSection: 0)
var firstCellObject: PFObject? { didSet{ tableView.reloadRowsAtIndexPaths([indexPathforcell0], withRowAnimation: .Fade) } }
var secondCellArrayOfObjects = [PFObject]() { didSet { tableView.reloadRowsAtIndexPaths([indexPathforcell1], withRowAnimation: .Fade) } }
// MARK: - life cycle
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewDidLoad() {
super.viewDidLoad()
//fetch
fetchFirstCellObject()
fetchSecondCellArrayOfObjects()
}
private func fetchFirstCellObject() {
//At the end...
// self.firstCellObject = something
}
private func fetchSecondCellArrayOfObjects() {
//self.secondCellArrayOfObjects = something
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var mainCell = UITableViewCell()
switch indexPath.row {
case 0:
let cell = tableView.dequeueReusableCellWithIdentifier("Cell1", forIndexPath: indexPath) as! CustomTableViewCell
cell.object = firstCellObject
mainCell = cell
case 1:
var cell = self.tableView.dequeueReusableCellWithIdentifier("Cell2") as! UITableViewCell
collectionView = cell.viewWithTag(1) as! UICollectionView
collectionView.delegate = self
collectionView.dataSource = self
mainCell = cell
return mainCell
}
// MARK: - UICollectionViewDataSource
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CommentedRecipeCollectionViewCell
cell.object = secondCellArrayOfObjects[indexPath.row]
return cell
}
}
And here how I implement the UITableViewCell:
class CustomTableViewCell: UITableViewCell {
// MARK: - Model
var object: PFObject? { didSet { updateUI() } }
#IBOutlet weak var objectImageView: PFImageView!
#IBOutlet weak var objectLabel: UILabel!
private func updateUI() {
let qos = Int(QOS_CLASS_USER_INITIATED.value)
dispatch_async(dispatch_get_global_queue(qos, 0)) {
let URLToUse = // ....
if let stringToUse = theString {
let imageData = NSData(contentsOfURL: NSURL(string: (URLToUse))!)
dispatch_async(dispatch_get_main_queue()) {
self.homeImageView.image = UIImage(data: imageData!)
}
}
}
objectLabel.text = // ....
}
}