I have a UITableViewController which allows multiple selection and it presents model of the data stored in the CoreData.
When user taps on the row we need to animate it to selected state (change layout, fade in some elements etc). The problem is that when user taps on row and makes it selected - model gets updated (because we store selected items in the model too). Because of the NSFetchedResultController aborts our fancy animations as whole table gets reloaded.
To make it more clear. Here is configureCell method i call in tableView:cellForRowAtindexPath. The reason to have setSelected method with animated: false there is to set already selected cells to a proper state when user scrolls the table.
func configureCell(cell: MealCell, indexPath: NSIndexPath) {
let menuItem = self.fetchedResultsController.objectAtIndexPath(indexPath) as! MenuItem
cell.name = menuItem.name
// some cell initialisation
//we have this code to draw cells selected when user scrolls our table view. We don't need animation here.
if menuItem.isSelected {
cell.setSelected(true, animated: false)
} else {
cell.setSelected(false, animated: false)
}
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
}
All animations happen in setSelected method of the MealCell class
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
extraView.hidden = !selected
let extraViewAlpha: CGFloat = selected ? 1.0 : 0.0
self.extraViewWidthConstraint.constant = selected ? 38 : 0
if animated {
UIView.animateWithDuration(0.5, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
self.layoutIfNeeded()
}, completion: { completed -> Void in
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.extraView.alpha = extraViewAlpha
})
})
}
else {
self.layoutIfNeeded()
self.extraView.alpha = extraViewAlpha
}
}
The best is to change your way of storing selectedItems to prevent the fetchedResultsController from notifying the delegate of the changes. a workaround is to temporary set the fetchedResultsController.delegate to nil and set it back after animation ends.
Related
For my CollectionView I have this animation inside willDisplay :
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// Add animations here
let animation = AnimationFactory.makeMoveUpWithFade(rowHeight: cell.frame.height, duration: 0.5, delayFactor: 0.1)
let animator = Animator(animation: animation)
animator.animate(cell: cell, at: indexPath, in: collectionView)
}
This is how the animation works (I implemented it for CollectionView) if you need it for more info.
Probelm:
Inside my project the user can create and delete an item.
Right now the collectionView is not animating after deleting even though I am calling reloadData:
extension MainViewController: DismissWishlistDelegate {
func dismissWishlistVC(dataArray: [Wishlist], dropDownArray: [DropDownOption]) {
self.dataSourceArray = dataArray
self.dropOptions = dropDownArray
self.makeWishView.dropDownButton.dropView.tableView.reloadData()
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: nil)
}
}
This is where I call the delegate inside my other ViewController:
func deleteTapped(){
let alertcontroller = UIAlertController(title: "Wishlist löschen", message: "Sicher, dass du diese Wishlist löschen möchtest?", preferredStyle: .alert)
let deleteAction = UIAlertAction(title: "Löschen", style: .default) { (alert) in
DataHandler.deleteWishlist(self.wishList.index)
self.dataSourceArray.remove(at: self.currentWishListIDX)
self.dropOptions.remove(at: self.currentWishListIDX)
// change heroID so wishlist image doesnt animate
self.wishlistImage.heroID = "delete"
self.dismiss(animated: true, completion: nil)
// update datasource array in MainVC
self.dismissWishlistDelegate?.dismissWishlistVC(dataArray: self.dataSourceArray, dropDownArray: self.dropOptions)
}
let cancelAction = UIAlertAction(title: "Abbrechen", style: .default) { (alert) in
print("abbrechen")
}
alertcontroller.addAction(cancelAction)
alertcontroller.addAction(deleteAction)
self.present(alertcontroller, animated: true)
}
When creating the animation works just fine. This is how my createDelegateFunction looks like this:
func createListTappedDelegate(listImage: UIImage, listImageIndex: Int, listName: String) {
// append created list to data source array
var textColor = UIColor.white
if Constants.Wishlist.darkTextColorIndexes.contains(listImageIndex) {
textColor = UIColor.darkGray
}
let newIndex = self.dataSourceArray.last!.index + 1
self.dataSourceArray.append(Wishlist(name: listName, image: listImage, wishData: [Wish](), color: Constants.Wishlist.customColors[listImageIndex], textColor: textColor, index: newIndex))
// append created list to drop down options
self.dropOptions.append(DropDownOption(name: listName, image: listImage))
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: {
(result) in
// scroll to make newly added row visible (if needed)
let i = self.theCollectionView.numberOfItems(inSection: 0) - 1
let idx = IndexPath(item: i, section: 0)
self.theCollectionView.scrollToItem(at: idx, at: .bottom, animated: true)
})
}
Animation of IUCollectionView elements insertion and deletion is correctly done using finalLayoutAttributesForDisappearingItem(at:) and initialLayoutAttributesForAppearingItem(at:)
Little excerpt from Apple's documentation on finalLayoutAttributesForDisappearingItem(at:)
This method is called after the prepare(forCollectionViewUpdates:) method and before the finalizeCollectionViewUpdates() method for any items that are about to be deleted. Your implementation should return the layout information that describes the final position and state of the item. The collection view uses this information as the end point for any animations. (The starting point of the animation is the item’s current location.) If you return nil, the layout object uses the same attributes for both the start and end points of the animation.
I solved the problem by removing performBatchUpdates ... I have no idea why it works now and what exactly performBatchUpdates does but it works so if anyone wants to explain it to me, feel free :D
The final function looks like this:
func dismissWishlistVC(dataArray: [Wishlist], dropDownArray: [DropDownOption], shouldDeleteWithAnimation: Bool, indexToDelete: Int) {
if shouldDeleteWithAnimation {
self.shouldAnimateCells = true
self.dataSourceArray.remove(at: self.currentWishListIDX)
self.dropOptions.remove(at: self.currentWishListIDX)
// reload the collection view
theCollectionView.reloadData()
} else {
self.shouldAnimateCells = false
self.dataSourceArray = dataArray
self.dropOptions = dropDownArray
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: nil)
}
self.makeWishView.dropDownButton.dropView.tableView.reloadData()
}
uploaded video (7 sec) for describing my problem
i have a textView embed in a tableView cell when we enter something on that textView the textview automatically increases its height according to entered content and also cell increases its size for doing this am using this approach:-
//tableViewCell
/// Custom setter so we can initialise the height of the text view
var textString: String {
get {
return textView?.text ?? ""
}
set {
if let textView = textView {
textView.text = newValue
textViewDidChange(textView)
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Disable scrolling inside the text view so we enlarge to fitted size
textView?.scrollEnabled = false
textView?.delegate = self
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
textView?.becomeFirstResponder()
} else {
textView?.resignFirstResponder()
}
}
}
extension MultiLineTextInputTableViewCell: UITextViewDelegate {
func textViewDidChange(textView: UITextView) {
let size = textView.bounds.size
let newSize = textView.sizeThatFits(CGSize(width: size.width, height: CGFloat.max))
// Resize the cell only when cell's size is changed
if size.height != newSize.height {
UIView.setAnimationsEnabled(false)
tableView?.beginUpdates()
tableView?.endUpdates()
UIView.setAnimationsEnabled(true)
if let thisIndexPath = tableView?.indexPathForCell(self) {
tableView?.scrollToRowAtIndexPath(thisIndexPath, atScrollPosition: .Bottom, animated: false)
}
}
}
here everything is fine but i added a function for Forwarding any content and for this am using this approach :-
forwardNoteAction = UITableViewRowAction(style: .Normal, title: "Forward"){ action, index in
let cell = tableView.cellForRowAtIndexPath(indexPath) as! NotesTableViewCell
if self.searchBar.text == "" {
let object: PFObject = self.noteObjects.objectAtIndex(indexPath.row) as! PFObject
let postsObjID = object.objectId!
let query = PFQuery(className: "Notes")
query.fromLocalDatastore()
query.getObjectInBackgroundWithId(postsObjID) {
(objectViewes, error) -> Void in
if error != nil {
print(error)
} else {
self.forwardObject = object
print(self.forwardObject)
//object.pinInBackground()
self.performSegueWithIdentifier("forwardNoteSegue", sender: nil)
self.tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
i am putting the content into a variable self.forwardObject and passing that variable with an segue forwardNoteSegue after passing self.forwardObject am using that content for filling my tableView (which is a form , consist of textViews and TextFields) but when am doing this my textView is not increasing its height according to content which is inside that textView ,
all i want is the textView should increase increase its height when we use "textView.text = copiedContent" same as when we enter anything into the textView manually
please for understanding my problem properly see the uploaded video (link is provided above)
if my question is not understandable than please let me know i'll fix it
thanks
You need to manually call textViewDidChange when you modify the content of you textview programatically.
This is documented in the UITextviewDelegate's description of the method:
The text view calls this method in response to user-initiated changes to the text. This method is not called in response to
programmatically initiated changes.
I have one tableView in my storyBoard where I added 4 static cell into it and my storyBoard look like:
I don't have any dataSource for this tableView because my cells are static.
And I use below code to drag a cell and it is working fine till I scroll a table.
import UIKit
class TableViewController: UITableViewController {
var sourceIndexPath: NSIndexPath = NSIndexPath()
var snapshot: UIView = UIView()
let longPress: UILongPressGestureRecognizer = {
let recognizer = UILongPressGestureRecognizer()
return recognizer
}()
override func viewDidLoad() {
super.viewDidLoad()
longPress.addTarget(self, action: "longPressGestureRecognized:")
self.tableView.addGestureRecognizer(longPress)
self.tableView.allowsSelection = false
}
override func viewWillAppear(animated: Bool) {
self.tableView.reloadData()
}
// MARK: UIGestureRecognizer
func longPressGestureRecognized(gesture: UILongPressGestureRecognizer){
let state: UIGestureRecognizerState = gesture.state
let location:CGPoint = gesture.locationInView(self.tableView)
if let indexPath: NSIndexPath = self.tableView.indexPathForRowAtPoint(location){
switch(state){
case UIGestureRecognizerState.Began:
sourceIndexPath = indexPath
let cell: UITableViewCell = self.tableView .cellForRowAtIndexPath(indexPath)!
//take a snapshot of the selected row using helper method
snapshot = customSnapshotFromView(cell)
//add snapshot as subview, centered at cell's center
var center: CGPoint = cell.center
snapshot.center = center
snapshot.alpha = 0.0
self.tableView.addSubview(snapshot)
UIView.animateWithDuration(0.25, animations: { () -> Void in
center.y = location.y
self.snapshot.center = center
self.snapshot.transform = CGAffineTransformMakeScale(1.05, 1.05)
self.snapshot.alpha = 0.98
cell.alpha = 0.0
}, completion: { (finished) in
cell.hidden = true
})
case UIGestureRecognizerState.Changed:
let cell: UITableViewCell = self.tableView.cellForRowAtIndexPath(indexPath)!
var center: CGPoint = snapshot.center
center.y = location.y
snapshot.center = center
print("location \(location.y)")
//is destination valid and is it different form source?
if indexPath != sourceIndexPath{
//update data source
//I have commented this part because I am not using any dataSource.
// self.customArray.exchangeObjectAtIndex(indexPath.row, withObjectAtIndex: sourceIndexPath.row)
//move the row
self.tableView.moveRowAtIndexPath(sourceIndexPath, toIndexPath: indexPath)
//and update source so it is in sync with UI changes
sourceIndexPath = indexPath
}
if (location.y < 68) || (location.y > 450) {
print("cancelled")
self.snapshot.alpha = 0.0
cell.hidden = false
UIView.animateWithDuration(0.10, animations: { () -> Void in
self.snapshot.center = cell.center
self.snapshot.transform = CGAffineTransformIdentity
self.snapshot.alpha = 0.0
//undo fade out
cell.alpha = 1.0
}, completion: { (finished) in
self.snapshot.removeFromSuperview()
})
}
case UIGestureRecognizerState.Ended:
//clean up
print("ended")
let cell: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
cell.hidden = false
UIView.animateWithDuration(0.25, animations: { () -> Void in
self.snapshot.center = cell.center
self.snapshot.transform = CGAffineTransformIdentity
self.snapshot.alpha = 0.0
//undo fade out
cell.alpha = 1.0
}, completion: { (finished) in
self.snapshot.removeFromSuperview()
})
break
default:
break
}
}else{
gesture.cancelsTouchesInView = true
}
}
func customSnapshotFromView(inputView: UIView) -> UIView {
// Make an image from the input view.
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0)
inputView.layer.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
// Create an image view.
let snapshot = UIImageView(image: image)
snapshot.layer.masksToBounds = false
snapshot.layer.cornerRadius = 0.0
snapshot.layer.shadowOffset = CGSize(width: -5.0, height: 0.0)
snapshot.layer.shadowRadius = 5.0
snapshot.layer.shadowOpacity = 0.4
return snapshot
}
}
When I scroll after dragging it looks like:
As you can see cell is not appearing again. I want to drag and drop static cell and I want to save it's position so I will not rearrange again when I scroll.
Sample project for more Info.
This is just a demo project But I have added many elements into my cell and every cell have different UI.
There is a library that does exactly what you are looking to do with a very similar approach. It's called FMMoveTableView but it's for cells with a datasource.
I think that what is causing your problem is that when you move the cells around and then you scroll the datasource from the storyboard is no longer in sync with the table and therefore your cell object can't be redrawn.
I think you should implement your table this way:
Make your 4 cells custom cells.
Subclass each one.
Create an Array with numbers 1 to 4
Reorder the array on long drag
Override cellForRowAtIndexPath to show the right cell for the right number
You can drag uitableview cell from uitableview delegates .......
1) set the table view editing style to none in its delegate.
2) implement table view delegate to enable dragging of cell i.e canMoveRowAtIndexPath methods...
You can create multiple dynamic cells.
You'll just have to dequeue cells with correct identifier.
Are you doing this for layout purposes only, maybe a UICollectionView or a custom made UIScrollView could do the job?
Never the less, I have a solution:
Create a IBOutlet collection holding all your static UITableViewCells
Create a index list to simulate a "data source"
Override the cellForRowAtIndexPath to draw using your own index list
When updating the list order, update the indexList so that the view "remembers" this change
This Table view controller explains it all:
class TableViewController: UITableViewController {
#IBOutlet var outletCells: [UITableViewCell]!
var indexList = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
// Prepare a index list.
// We will move positions in this list instead
// of trying to move the view's postions.
for (index, _) in outletCells.enumerate() {
indexList.append(index)
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Use dynamic count, not needed I guess but
// feels better this way.
return outletCells.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Use the index path to get the true index and return
// the view on that index in our IBOutlet collection
let realIndexForPos = indexList[indexPath.row]
return outletCells[realIndexForPos]
}
#IBAction func onTap(sender: AnyObject) {
// Simulating your drag n drop stuff here... :)
let swapThis = 1
let swapThat = 2
tableView.moveRowAtIndexPath(NSIndexPath(forItem: swapThis, inSection: 0), toIndexPath: NSIndexPath(forItem: swapThat, inSection: 0))
// Update the indexList as this is the "data source"
// Do no use moveRowAtIndexPath as this is never triggred
// This one line works: swap(&indexList[swapThis], &indexList[swapThat])
// But bellow is easier to understand
let tmpVal = indexList[swapThis]
indexList[swapThis] = indexList[swapThat]
indexList[swapThat] = tmpVal
}
}
To create the IBOutlet use the Interface Builder.
Use the Referencing Outlet Collection on each Table View Cell and drag, for each, to the same #IBOutlet in your controller code.
I'd like to visually scroll through my whole tableView. I tried the following, but it doesn't seem to perform the scrolling. Instead it just runs through the loops. I inserted a dispatch_async(dispatch_get_main_queue()) statement, thinking that that would ensure the view is refreshed before proceeding, but no luck.
What am I doing wrong?
func scrollThroughTable() {
for sectionNum in 0..<tableView.numberOfSections() {
for rowNum in 0..<tableView.numberOfRowsInSection(sectionNum) {
let indexPath = NSIndexPath(forRow: rowNum, inSection: sectionNum)
var cellTemp = self.tableView.cellForRowAtIndexPath(indexPath)
if cellTemp == nil {
dispatch_async(dispatch_get_main_queue()) {
self.tableView.scrollToRowAtIndexPath(indexPath!, atScrollPosition: .Top, animated: true)
self.tableView.reloadData()
}
}
}
}
}
I found a solution. Simply use scrollToRowAtIndexPath() with animation. To do so I had to create a getIndexPath() function to figure out where I want to scroll. Has more or less the same effect as scrolling through the whole table if I pass it the last element of my tableView.
If you want it to happen slower with more scrolling effect, wrap it inside UIView.animateWithDuration() and play with 'duration'. You can even do more animation if you want in its completion block. (No need to set an unreliable sleep timer, etc.)
func animateReminderInserted(toDoItem: ReminderWrapper) {
if let definiteIndexPath = indexPathDelegate.getIndexPath(toDoItem) {
self.tableView.scrollToRowAtIndexPath(definiteIndexPath, atScrollPosition: .Middle, animated: true)
}
}
I have UITableView with CustomCell. I want to rotate record image when a cell fully visible or visible more than half at least.
This code block in CustomTableViewCell.swift
override func setSelected(selected: Bool, animated: Bool)
{
super.setSelected(selected, animated: animated)
RotateImage()
}
The problem is that image is starting rotate immediately custom cell appeared like image below.
Sample Image:
There are 2 cells. First one is fully visible and rotating. And second one is partially visible / loaded but its also rotating.
Is it possible to check visibility in setSelected code block or need to check with UITableView functions?
The result should be like that:
override func setSelected(selected: Bool, animated: Bool)
{
super.setSelected(selected, animated: animated)
if fullyVisible == true
{
RotateImage()
}
}
Thanks.
If you want more control for the behavior of cells based on their visibility withing the bounds of your screen, you could use UIScrollViewDelegate functions
such as:
optional func scrollViewDidScroll(_ scrollView: UIScrollView)
Get an array of the tableView's visible cells.
you can check the bounds of each cell to see if it is on screen
Check if the cell's record is spinning and if not start the rotation.
I did something similar in one of my projects:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleArea = scrollView.contentOffset.y + scrollView.height
for cell in tableView.visibleCells {
guard let propertyGraphCell = cell as? PropertyGraphCell else { continue }
if cell.frame.midY <= visibleArea, let indexPath = tableView.indexPath(for: cell) {
switch visibleRows[indexPath.row] {
case .daysOnMarket:
if daysOnMarketAnimated {
propertyGraphCell.drawLine(animated: true)
daysOnMarketAnimated = false
}
case .priceTrend:
if priceTrendAnimated {
propertyGraphCell.drawLine(animated: true)
priceTrendAnimated = false
}
case .vendorDiscount:
if vendorDiscountAnimated {
propertyGraphCell.drawLine(animated: true)
vendorDiscountAnimated = false
}
case .marketInsights:
assert(false, "cell animation not supported")
}
}
}
}