Swift - present/dismiss ViewController with UIImage animation - ios

I would like to have an animation when pushing/dismissing my ViewController.
The best way to show you what I mean is to look at the N26: Animation
In my case I have a CollectionView where the user can click on a cell to get to the next ViewController which I handle with a tapCallback:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// if indexPath.item is less than data count, return a "Content" cell
if indexPath.item < dataSourceArray.count {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ContentCell", for: indexPath) as! ContentCell
// set cell label
cell.cellLabel.text = dataSourceArray[indexPath.item].name
// set cell image
cell.wishlistImage.image = dataSourceArray[indexPath.item].image
if cell.wishlistImage.image == images[2] || cell.wishlistImage.image == images[3] {
cell.priceLabel.textColor = .gray
cell.priceEuroLabel.textColor = .gray
cell.wishCounterLabel.textColor = .gray
cell.wünscheLabel.textColor = .gray
}
// set background color
cell.imageView.backgroundColor = dataSourceArray[indexPath.item].color
cell.wishCounterView.backgroundColor = dataSourceArray[indexPath.item].color
cell.priceView.backgroundColor = dataSourceArray[indexPath.item].color
cell.customWishlistTapCallback = {
// track selected index
self.currentWishListIDX = indexPath.item
let vc = self.storyboard?.instantiateViewController(withIdentifier: "WishlistVC") as! WishlistViewController
vc.wishList = self.dataSourceArray[self.currentWishListIDX]
vc.theTableView.tableView.reloadData()
self.present(vc, animated: true, completion: nil)
}
return cell
}
To dismiss the ViewController the user simply has to tap a button:
#objc private func dismissView(){
self.dismiss(animated: true, completion: nil)
}
This is a screenshot of my CollectionView and the ViewController I present/dismiss:
Like I said I would like to have the exact same animation as in the video (also be able to "drag-dismiss" the ViewController but that is probably another question?).
If you anything is unclear feel free to ask.
I tried searching for that but I couldn't really find anything so I am grateful for any help :)

There is a really good library that does this functionality, it’s called Hero.
If you want to make your own implementation you will need to use your own UIViewControllerTransitioningDelegate

Related

Collection view button background image getting repeated on scroll

I have a collection view.I have a button called furnitureSelectionBtn inside the collection view cell.
When i tap the button inside the collection view,it sets the button image as selected.png.When i tap it again it gets unselected and i set with unselected.png image.
When i tap the button cardselection method gets triggered and i save the index clicked in allFurnituresArray.
Now when i scroll this collection view ,few of the cells are getting selected image even if i havent selected them.This generally happens for the cells outside of the view .Cells getting reused I beleive.
For example if i have selected the 1st button and i start scrolling,I see 4th button also in selected state
So i tried to check in cellforRow method against the allFurnituresArray indexes which are selected and even thats not working.
I also tried to see if i can use prepareForReuse() but we dont get information about index I have scrolled to there.
Can someone please help
CardCollectionViewCell.swift
class CardCollectionViewCell: SwipingCarouselCollectionViewCell {
#IBOutlet weak var furnitureSelectionBtn: UIButton!
static let reuseIdentifier = "CardCollectionViewCell"
static var nib: UINib {
get {
return UINib(nibName: "CardCollectionViewCell", bundle: nil)
}
}
override func awakeFromNib() {
super.awakeFromNib()
furnitureSelectionBtn.setBackgroundImage(image, for: UIControl.State.normal)
}
CardViewController.swift
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CardCollectionViewCell.reuseIdentifier, for: indexPath) as! CardCollectionViewCell
// Configure the cell
cell.delegate = self
cell.furnitureSelectionBtn.tag = (indexPath as NSIndexPath).row
for element in allFurnituresArray {
print(element)
if (indexPath.row) == element as! Int {
let image1 = UIImage(named: "selected.png")
cell.furnitureSelectionBtn.setBackgroundImage(image1, for: UIControl.State.selected)
}
else
{
let image1 = UIImage(named: "unselected.png")
cell.furnitureSelectionBtn.setBackgroundImage(image1, for: UIControl.State.normal)
}
}
cell.furnitureSelectionBtn.addTarget(self, action: #selector(CardViewController.cardselection(_:)), for: .touchUpInside)
return cell
}
#objc func cardselection(_ sender:UIButton!)
{
if sender.isSelected == true {
sender.isSelected = false
let image1 = UIImage(named: "unselected.png")
sender.setBackgroundImage(image1, for: UIControl.State.normal)
allFurnituresArray.remove(sender.tag)
}else {
sender.isSelected = true
let image1 = UIImage(named: "selected.png")
allFurnituresArray.add(sender.tag)
sender.setBackgroundImage(image1, for: UIControl.State.selected)
}
print("Button tapped")
print(sender.tag)
}
The best way to save the selection status is to add this data to your item in the datasource array, e.g,
(1) datasource array
var items = [Item]()
(2) didSelectItem...
items[indexPath.row].isSelected = !(items[indexPath.row].isSelected)
and handle selection status in your cell.

Single CollectionView with multiple CollectionViewLayout.why Layout is mess up?

I have Single UICollectionView , and I want to Apply Two different layout dynamically.
UICollectionViewFlowLayout : A Layout with same size cell and circle image.
var flowLayout: UICollectionViewFlowLayout {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: UIScreen.main.bounds.width/3, height: 140)
flowLayout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0)
flowLayout.scrollDirection = UICollectionViewScrollDirection.vertical
flowLayout.minimumInteritemSpacing = 0.0
return flowLayout
}
Pintrest Layout :
https://www.raywenderlich.com/392-uicollectionview-custom-layout-tutorial-pinterest
For Example : when user Click on Profile Button FlowLayout will be Apllied and Cell Appear with image in Circle Shape. when user click on Picture button pintrest layout will be Applied and cell Appear with image in Rectangle shape with dynamic height.
intially CollectionView have 1.flowLayout and it appears perfectly.but when I click on Picture button Pintrest layout is messed up with previous layout as shown in above image.
Following is Code For changing Layout.
if isGrid {
let horizontal = flowLayout
recentCollectionView.setCollectionViewLayout(horizontal, animated: true)
recentCollectionView.reloadData()
}
else {
let horizontal = PinterestLayout()
horizontal.delegate = self
recentCollectionView.setCollectionViewLayout(horizontal, animated: true)
recentCollectionView.reloadData()
}
ViewHiarchy:
I have main Collection-view that Contain header and one bottom cell.cell contain other Collection-view to which I am Applying multiple layout.I have Two Different cell for each layout.I want bottom cell size equal to content Collection-view Content size so user can Scroll entire main collection-view vertically.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell : UICollectionViewCell!
switch isGrid {
case true:
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchProfileCell", for: indexPath)
if let annotateCell = cell as? SearchProfileCell {
annotateCell.photo = photos[indexPath.item]
}
case false:
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AnnotatedPhotoCell", for: indexPath)
if let annotateCell = cell as? AnnotatedPhotoCell {
annotateCell.cellwidth = collectionView.contentSize.width/3
annotateCell.photo = photos[indexPath.item]
}
}
cell.contentView.layer.cornerRadius = 0
return cell
}
Code of profile and picture button Action.
#IBAction func pictureClick(sender:UIButton) {
isGrid = false
self.searchCollectionView.reloadData()
}
#IBAction func profilClick(sender:UIButton) {
isGrid = true
self.searchCollectionView.reloadData()
}
I think problem is not inside layout but might be inside cellForItemAt. if you are using different cell for both layout then do not compare bool at cellForItemAt method. you should compare layout class type
like below code :
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView.collectionViewLayout.isKind(of: PinterestLayout.self) {
// return cell for PinterestLayout
guard let annotateCell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchProfileCell", for: indexPath) as? SearchProfileCell else {
fatalError("SearchProfileCell Not Found")
}
annotateCell.photo = photos[indexPath.item]
return annotateCell
} else {
// return cell for flowLayout
guard let annotateCell = collectionView.dequeueReusableCell(withReuseIdentifier: "AnnotatedPhotoCell", for: indexPath) as? AnnotatedPhotoCell else {
fatalError("AnnotatedPhotoCell Not Found")
}
annotateCell.cellwidth = collectionView.contentSize.width/3
annotateCell.photo = photos[indexPath.item]
return annotateCell
}
}
Also need to update layout change action methods like:
#IBAction func pictureClick(sender:UIButton) {
isGrid = false
self.collectionView?.collectionViewLayout.invalidateLayout()
self.collectionView?.setCollectionViewLayout(PinterestLayout(),
animated: false, completion: { [weak self] (complite) in
guard let strongSelf = self else {
return
}
strongSelf.searchCollectionView?.reloadData()
})
}
#IBAction func profilClick(sender:UIButton) {
isGrid = true
self.collectionView?.collectionViewLayout.invalidateLayout()
self.collectionView?.setCollectionViewLayout(flowLayout,
animated: false, completion: { [weak self] (complite) in
guard let strongSelf = self else {
return
}
strongSelf.searchCollectionView?.reloadData()
})
}
Why you are using two different layout even though you can achieve same Result with pintrestLayout. https://www.raywenderlich.com/392-uicollectionview-custom-layout-tutorial-pinterest.
Check pintrestLayout carefully , it have Delegate for Dynamic height.
let photoHeight = delegate.collectionView(collectionView, heightForPhotoAtIndexPath: indexPath)
if you return static height here , your pintrest layout become GridLayout(your First Layout).
if you want pintrest layout as work for both layout , you need to declare same Boolean(isGrid) in pintrest layout.and use this boolean to return UICollectionViewLayoutAttributes
more important raywenderlich pintrest layout uses cache to store layout attribute.you have to remove cache object before applying other layout.
Check in this tutorial , how same layout used for grid,list and linear.
https://benoitpasquier.com/optimise-uicollectionview-swift/
what you need in your layout.
var isGrid : Bool = true {
didSet {
if isGrid != oldValue {
cache.removeAll()
self.invalidateLayout()
}
}
}

Iterating over UITableViewCell for AVSpeechSynthesizer

I've seen everywhere that it's bad practice to iterate through a tableView but I'm afraid I have no other alternative. I want to have the AVSpeechSynthesizer read the content of the labels that are inside my custom UITableViewCells.
Of course, this could be done directly using the source array of the tableView but the problem is that I want to have the synthesiser read the labels and at the same time highlight the words that are being spoken.
I have done some tests but the problem is that as the cells are being reused by the table view, I end up with empty cells...
What is the right way of ensuring my cells are not empty ?
I've tried scrolling the tableView to ensure the cells aren't empty but it doesn't work either.
Here is my code so far ...
func scrollToRow(rowToGo: Int) {
self.collectionView.scrollToItem(at: self.indexPath as IndexPath, at: .centeredVertically, animated: true)
let colorCell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "ColorCell", for:indexPath as IndexPath ) as! ColorCell
let myString = colorCell.colorLabel.text!
print(myString)
self.speakString(sender: self, str:(myString))
}
#IBAction func playPauseButton(_ sender: UIButton) {
if !self.speechSynthesizer.isSpeaking{
let btnImage = UIImage(named: "pausedButton.png")
self.playPauseButton.setImage(btnImage , for: UIControlState.normal)
self.isPaused = false
// var indexPath:Double = 0
self.indexPath = NSIndexPath(row: 0, section: 0)
for i in 0...36 {
print(i)
self.indexPath = NSIndexPath(row: i, section:0)
self.scrollToRow(rowToGo:i)
// self.indexPath = NSIndexPath(index:i)
// self.collectionView.selectItem(at: indexPath as IndexPath, animated: true, scrollPosition: .centeredVertically)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt
indexPath: IndexPath) -> UICollectionViewCell {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier:
"ColorCell", for: indexPath) as! ColorCell
myCell.colorLabel.text = self.wordList[indexPath.row]
myCell.colorImage.image = self.images[indexPath.row]
return myCell }
My collectionView is displayed correctly and all my labels inside the UICollectionViewCells contain text. However, when I press the play button, the first time, nothing gets spoken. If I scroll back and press again, only 3 or 4 cells actually exist and get their label.text printed and spoken while all the other indexes are empty.
Any suggestion would be welcome

UICollectionView how to deselect all

I have a FollowVC and FollowCell Setup with collection View. I can display all the datas correctly into my uIcollection view cell using the following code with no problem.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCellWithReuseIdentifier("FollowCell", forIndexPath: indexPath) as? FollowCell {
let post = posts[indexPath.row]
cell.configureCell(post, img: img)
if cell.selected == true {
cell.checkImg.hidden = false
} else {
cell.checkImg.hidden = true
}
return cell
}
}
Note that I could also select and deselect multiple images using the following code
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
if deletePressed == true {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! FollowCell
cell.checkImg.hidden = false
} else {
let post = posts[indexPath.row]
performSegueWithIdentifier(SEGUE_FOLLOW_TO_COMMENTVC, sender: post)
}
}
func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! FollowCell
cell.checkImg.hidden = true
}
When In "Select" mode, I can perform the selction of each cell and a check mark will be displayed on the cell. However, what I want to do is to have a cancel buttom to disable all the selected cell and removing the checkImg.
I have tried
func clearSelection() {
print("ClearSelection posts.count = \(posts.count)")
for item in 0...posts.count - 1 {
let indexP = NSIndexPath(forItem: item, inSection: 0)
followCollectionView.deselectItemAtIndexPath(indexP, animated: true)
let cell = followCollectionView.cellForItemAtIndexPath(indexP) as! FollowCell
cell.checkImg.hidden = true
}
}
The program crashes here giving me a fatal error: Unexpectedly found nil while unwrapping an optional error at
let cell = followCollectionView.cellForItemAtIndexPath(indexP) as! FollowCell
I dont know why it is having trouble unwrapping the cell to be my FollowCell which contains an instance of the checkImg. I already used it before in a similar situation in didSelectItemAtIndexPath and it seems to work?
Thanks,
Not all of the selected cells may be on screen at the point when you are clearing the selection status, so collectionView.cellForItemAtIndexPath(indexPath) may return nil. Since you have a force downcast you will get an exception in this case.
You need to modify your code to handle the potential nil condition but you can also make your code more efficient by using the indexPathsForSelectedItems property of UICollectionView
let selectedItems = followCollectionView.indexPathsForSelectedItems
for (indexPath in selectedItems) {
followCollectionView.deselectItemAtIndexPath(indexPath, animated:true)
if let cell = followCollectionView.cellForItemAtIndexPath(indexPath) as? FollowCell {
cell.checkImg.hidden = true
}
}
Using Extension in Swift 4
extension UICollectionView {
func deselectAllItems(animated: Bool) {
guard let selectedItems = indexPathsForSelectedItems else { return }
for indexPath in selectedItems { deselectItem(at: indexPath, animated: animated) }
}
}
To simplify further, you could just do
followCollectionView.allowsSelection = false
followCollectionView.allowsSelection = true
This will in fact correctly clear your followCollectionView.indexPathsForSelectedItems even though it feels very wrong.
collectionView.indexPathsForSelectedItems?
.forEach { collectionView.deselectItem(at: $0, animated: false) }
This answer may be useful in swift 4.2
let selectedItems = followCollectionView.indexPathsForSelectedItems
for (value in selectedItems) {
followCollectionView.deselectItemAtIndexPath(value, animated:true)
if let cell = followCollectionView.cellForItemAtIndexPath(value) as? FollowCell {
cell.checkImg.hidden = true
}
}
I got it solved easier by doing this:
tableView.selectRow(at: nil, animated: true, scrollPosition: UITableView.ScrollPosition.top)

How to update the button tag which is part of UICollectionViewCell after a cell is deleted in UICollectionView?

Here's a problem which I have been stuck at for quite some time now.
Here's the code
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({() -> Void in
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
self.collectionViewLove?.reloadData()}, completion: nil)}
I have a button inside each UICollectionViewCell which deletes it on clicking. The only way for me to retrieve the indexPath is through the button tag. I have initialized the button tag in
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
However every time I delete, the first time it deletes the corresponding cell whereas the next time it deletes the cell follwing the one I clicked. The reason is that my button tag is not getting updated when I call the function reloadData().
Ideally, when I call the reloadData() ,
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
should get called and update the button tag for each cell. But that is not happening. Solution anyone?
EDIT:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: UIImage(named: "preloader"))
let item = self.wishlist?.results[indexPath.row]
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.text = item?.title
cell.itemName.numberOfLines = 1
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.price.adjustsFontSizeToFitWidth = true
cell.deleteButton.tag = indexPath.row
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = indexPath.row
cell.buyButton.backgroundColor = UIColor.blackColor()
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
A couple of things:
You're doing too much work in cellForItemAtIndexPath--you really want that to be as speedy as possible. For example, you only need to register the nib once for the collectionView--viewDidLoad() is a good place for that. Also, you should set initial state of the cell in the cell's prepareForReuse() method, and then only use cellForItemAtIndexPath to update with the custom state from the item.
You shouldn't reload the data until the deletion is complete. Move reloadData into your completion block so the delete method is complete and the view has had time to update its indexes.
However, it would be better if you didn't have to call reloadData in the first place. Your implementation ties the button's tag to an indexPath, but these mutate at different times. What about tying the button's tag to, say, the wishlist item ID. Then you can look up the appropriate indexPath based on the ID.
Revised code would look something like this (untested and not syntax-checked):
// In LoveListCollectionViewCell
override func prepareForReuse() {
// You could also set these in the cell's initializer if they're not going to change
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.numberOfLines = 1
cell.price.adjustsFontSizeToFitWidth = true
cell.buyButton.backgroundColor = UIColor.blackColor()
}
// In your UICollectionView class
// Cache placeholder image since it doesn't change
private let placeholderImage = UIImage(named: "preloader")
override func viewDidLoad() {
super.viewDidLoad()
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: placeholderImage)
let item = self.wishlist?.results[indexPath.row]
cell.itemName.text = item?.title
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.deleteButton.tag = item?.id
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = item?.id
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
func removeFromLoveList(sender: AnyObject?) {
let id = sender.tag
let index = wishlist?.results.indexOf { $0.id == id }
let indexPath = NSIndexPath(forRow: index, inSection: 0)
collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
wishlist?.results.removeAtIndex(index)
}
It's probably not a good idea to be storing data in the cell unless it is needed to display the cell. Instead your could rely on the UICollectionView to give you the correct indexPath then use that for the deleting from your data source and updating the collectionview.
To do this use a delegate pattern with cells.
1.Define a protocol that your controller/datasource should conform to.
protocol DeleteButtonProtocol {
func deleteButtonTappedFromCell(cell: UICollectionViewCell) -> Void
}
2.Add a delegate property to your custom cell which would call back to the controller on the delete action. The important thing is to pass the cell in to that call as self.
class CustomCell: UICollectionViewCell {
var deleteButtonDelegate: DeleteButtonProtocol!
// Other cell configuration
func buttonTapped(sender: UIButton){
self.deleteButtonDelegate.deleteButtonTappedFromCell(self)
}
}
3.Then back in the controller implement the protocol function to handle the delete action. Here you could get the indexPath for the item from the collectionView which could be used to delete the data and remove the cell from the collectionView.
class CollectionViewController: UICollectionViewController, DeleteButtonProtocol {
// Other CollectionView Stuff
func deleteButtonTappedFromCell(cell: UICollectionViewCell) {
let deleteIndexPath = self.collectionView!.indexPathForCell(cell)!
self.wishList.removeAtIndex(deleteIndexPath.row)
self.collectionView?.performBatchUpdates({ () -> Void in
self.collectionView?.deleteItemsAtIndexPaths([deleteIndexPath])
}, completion: nil)
}
}
4.Make sure you set the delegate for the cell when configuring it so the delegate calls back to somewhere.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//Other cell configuring here
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("identifier", forIndexPath: indexPath)
(cell as! CustomCell).deleteButtonDelegate = self
return cell
}
}
I was facing the similar issue and I found the answer by just reloading collection view in the completion block.
Just update your code like.
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
}, completion: {
self.collectionViewLove?.reloadData()
})
which is mentioned in UICollectionView Performing Updates using performBatchUpdates by Nik

Resources