How to I have a Repeat vs Continual Animation? - uiview

Scenario: I have a '>' button:
that is supposed to animate +90 degrees upon initial tap:
However after returning to the 0 degree '>' position via tapping on the UITableViewCell again which returns to its original height; then tapping AGAIN I get a further rotation to the '<' position:
How do I freeze the max rotation to only the 90 degrees (pointing down); so that I have the single +/- 90 deg rotation toggle?
Here's my code:
func rotate2ImageView() {
UIView.animate(withDuration: 0.3) {
self.rightArrowImage.transform = self.rightArrowImage.transform.rotated(by: .pi / 2)
}
}
Here's a failed remedy (where I tried to remove all animations):
func rotateImageView(){
UIView.animate(withDuration: 0.3, animations: {() -> Void in
self.rightArrowImage.transform = self.rightArrowImage.transform.rotated(by: .pi / 2)
}, completion: {(_ finished: Bool) -> Void in
if finished {
self.rightArrowImage.layer.removeAllAnimations()
}
})
}

I needed to reset the '>' image's transformation prior to future taps to rotate the icon. I did this via setting its transformation to 'identity' per Matt's suggestion.
func rotateImageView() {
// Reset any pending transformation to original status:
rightArrowImage.transform = .identity
UIView.animate(withDuration: 0.3) {
self.rightArrowImage.transform = self.rightArrowImage.transform.rotated(by: .pi / 2)
}
}
This is fired every via the UITableViewDelete's willDisplay function:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let myCell = cell as? CommonFAQCell
if expandCell {
myCell?.rotateImageView()
}
}

Related

How can I make sure all cells (not only the visible ones) are loaded in a tableview before didSelectRowAtIndexPath?

I am coding a quiz app. As you can see in the storyboard, there are 2 tableviews. In the first one you select the type of the exam and in the second one you see the question. The way I designed the app is, there are 2 different kind of cells. Question and Multiple Choice.
The first cell of the 2nd tableView is the question cell and the remaining 4 are multipleChoice cells.(You will see 6 cells in the storyboard.In the last one I am trying to show ads.But that's irrelevant to this question.So think of it as 5 cells in total)
If the selected answer is right, I set the selected cell's contentView background color to green. If it is wrong, I set the selected cell's contentView background color to red and make the correct answer blink in green by using an extension.Here is the extension...
extension UIView{
func blink() {
self.alpha = 0.2
self.backgroundColor = .green
UIView.animate(withDuration: 0.15, delay: 0.0, options: [.curveLinear, .repeat, .autoreverse], animations: {self.alpha = 1.0}, completion: nil)
}
func stopBlink() {
self.backgroundColor = .clear
layer.removeAllAnimations()
alpha = 1
}
}
And here is the code that achieves this
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(indexPath.row)
if indexPath.row == dogruCevapInt && isCompleted == false && sonrakiVCyeAktarilacakIsTimeUp == false {
// The answer is right
print("Doğru Cevap")
self.tableView.allowsSelection = false
dogrular = dogrular + 1
updateNavigationItemTitle()
if let dogruCell = self.tableView.cellForRow(at: indexPath) as? SecenekTableViewCell {
let originalBGColor = dogruCell.contentView.backgroundColor
dogruCell.contentView.backgroundColor = .green
playSound(soundState: SettingsSingleton.shared.isSoundOn!,soundName: "correct", fileExtension: "mp3")
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false, block: { (newTimer) in
dogruCell.contentView.backgroundColor = originalBGColor
self.nextQuestion()
})
}
}else if indexPath.row != 0 && isCompleted == false && sonrakiVCyeAktarilacakIsTimeUp == false {
// The answer is wrong
print("Yanlış Cevap. Doğrusu \(dogruCevap)")
self.tableView.allowsSelection = false
yanlislar = yanlislar + 1
updateNavigationItemTitle()
if let yanlisCell = self.tableView.cellForRow(at: indexPath) as? SecenekTableViewCell {
let originalBGColor = yanlisCell.contentView.backgroundColor
yanlisCell.contentView.backgroundColor = .red
playSound(soundState: SettingsSingleton.shared.isSoundOn!,soundName: "wrong", fileExtension: "wav")
self.tableView.scrollToRow(at: IndexPath(row: self.dogruCevapInt, section: indexPath.section), at: .bottom, animated: true)
if let dogruCell = self.tableView.cellForRow(at: IndexPath(row: dogruCevapInt, section: indexPath.section)) {
dogruCell.contentView.backgroundColor = .green
dogruCell.blink()
timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false, block: { (newTimer) in
dogruCell.stopBlink()
yanlisCell.contentView.backgroundColor = originalBGColor
self.nextQuestion()
dogruCell.contentView.backgroundColor = .clear
})
}else {
//The right answer is currently not visible
//We cannot make it blink let's at least go to the next question
self.tableView.allowsSelection = false
timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false, block: { (newTimer) in
self.nextQuestion()
})
}
}
}else if isCompleted == true {
print("Is Completed true")
}else {
//If the user selects the question
self.tableView.deselectRow(at: indexPath, animated: false)
}
}
and now the problem...
Everything works perfect if all the multiple choice cells are currently visible(or at least some part of it is visible)on the screen. But if the cell that holds the right answer is not visible, I cannot make it blink no matter what I try. How do I do this?
I recorded a video and converted it into animated gif. Please see the 5th question to see what I mean.
Screen Recording of the problem

iOS - How To Give A Peek At The "Swipe To Delete" Action On A Table View Cell?

When a certain Table View Controller displays for the first time, how to briefly show that the red “swipe to delete” functionality exists in a table row?
The goal of programmatically playing peekaboo with this is to show the user that the functionality exists.
Environment: iOS 11+ and iPhone app.
Here's an image showing the cell slid partway with a basic "swipe to delete" red action button.
A fellow developer kindly mentioned SwipeCellKit, but there’s a lot to SwipeCellKit. All we want to do is briefly simulate a partial swipe to let the user know the "swipe to delete" exists. In other words, we want to provide a sneak peak at the delete action under the cell.
In case it helps, here's the link to the SwipeCellKit's showSwipe code Here is a link with an example of its use.
I looked at the SwipeCellKit source code. It's not clear to me how to do it without SwipeCellKit. Also, Using SwipeCellKit is not currently an option.
Googling hasn't helped. I keep running into how to add swipe actions, but not how to briefly show the Swipe Actions aka UITableViewRowAction items that are under the cell to the user.
How to briefly show this built in "swipe to delete" action to the user?
I've written this simple extension for UITableView. It searches through visible cells, finds first row that contains edit actions and then "slides" that cell for a bit to reveal action underneath. You can adjust parameters like width and duration of the hint.
It works great because it doesn't hardcode any values, so for example the hint's background color will always match the real action button.
import UIKit
extension UITableView {
/**
Shows a hint to the user indicating that cell can be swiped left.
- Parameters:
- width: Width of hint.
- duration: Duration of animation (in seconds)
*/
func presentTrailingSwipeHint(width: CGFloat = 20, duration: TimeInterval = 0.8) {
var actionPath: IndexPath?
var actionColor: UIColor?
guard let visibleIndexPaths = indexPathsForVisibleRows else {
return
}
if #available(iOS 13.0, *) {
// Use new API, UIContextualAction
for path in visibleIndexPaths {
if let config = delegate?.tableView?(self, trailingSwipeActionsConfigurationForRowAt: path), let action = config.actions.first {
actionPath = path
actionColor = action.backgroundColor
break
}
}
} else {
for path in visibleIndexPaths {
if let actions = delegate?.tableView?(self, editActionsForRowAt: path), let action = actions.first {
actionPath = path
actionColor = action.backgroundColor
break
}
}
}
guard let path = actionPath, let cell = cellForRow(at: path) else { return }
cell.presentTrailingSwipeHint(actionColor: actionColor ?? tintColor)
}
}
fileprivate extension UITableViewCell {
func presentTrailingSwipeHint(actionColor: UIColor, hintWidth: CGFloat = 20, hintDuration: TimeInterval = 0.8) {
// Create fake action view
let dummyView = UIView()
dummyView.backgroundColor = actionColor
dummyView.translatesAutoresizingMaskIntoConstraints = false
addSubview(dummyView)
// Set constraints
NSLayoutConstraint.activate([
dummyView.topAnchor.constraint(equalTo: topAnchor),
dummyView.leadingAnchor.constraint(equalTo: trailingAnchor),
dummyView.bottomAnchor.constraint(equalTo: bottomAnchor),
dummyView.widthAnchor.constraint(equalToConstant: hintWidth)
])
// This animator reverses back the transform.
let secondAnimator = UIViewPropertyAnimator(duration: hintDuration / 2, curve: .easeOut) {
self.transform = .identity
}
// Don't forget to remove the useless view.
secondAnimator.addCompletion { position in
dummyView.removeFromSuperview()
}
// We're moving the cell and since dummyView
// is pinned to cell's trailing anchor
// it will move as well.
let transform = CGAffineTransform(translationX: -hintWidth, y: 0)
let firstAnimator = UIViewPropertyAnimator(duration: hintDuration / 2, curve: .easeIn) {
self.transform = transform
}
firstAnimator.addCompletion { position in
secondAnimator.startAnimation()
}
// Do the magic.
firstAnimator.startAnimation()
}
}
Example usage:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
tableView.presentSwipeHint()
}
I'm pretty sure this can't be done. The swipe actions are contained in a UISwipeActionPullView, which contains UISwipeStandardAction subviews, both of which are private. They are also part of the table view, not the cell, and they're not added unless a gesture is happening, so you can't just bump the cell to one side and see them there.
Outside of UI automation tests, it isn't possible to simulate user gestures without using private API, so you can't "fake" a swipe and then show the results to the user.
However, why bother doing it "properly" when you can cheat? It shouldn't be too hard to bounce the cell's content view slightly to the left, and bounce in a red box (not so far that you can see text, to avoid localisation issues), then return to normal. Do this on the first load of the table view, and stop doing it after N times or after the user has proven that they know how this particular iOS convention works.
First : enable the editing
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
Then customise your swipe
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let conformeRowAction = UITableViewRowAction(style: UITableViewRowActionStyle.default, title: NSLocalizedString("C", comment: ""), handler:{action, indexpath in
print("C•ACTION")
})
conformeRowAction.backgroundColor = UIColor(red: 154.0/255, green: 202.0/255, blue: 136.0/255, alpha: 1.0)
let notConform = UITableViewRowAction(style: UITableViewRowActionStyle.default, title: NSLocalizedString("NC", comment: ""), handler:{action, indexpath in
print("NC•ACTION")
})
notConform.backgroundColor = UIColor(red: 252.0/255, green: 108.0/255, blue: 107.0/255, alpha: 1.0)
return [conformeRowAction,notConform]
}

Multi selection table view - Rotate image in custom header when section expands/collapses

Problem: When a section is expanded, the arrow in the section header rotates to point up. When I tap a cell, the arrow starts pointing down & rotates clockwise to point up (the arrow "twitches").
How do I get rid of the arrow's "twitch"? I want the arrow to point up when the section is expanded and point down when the section is collapsed.
CollapsibleFilterTableViewHeader
func setExpanded(expanded: Bool) {
//Get angle of arrow (0.0 for pointing up or 180.0 for pointing down)
let rad: Double = atan2( Double(arrowImageView.transform.b), Double(arrowImageView.transform.a))
let deg: CGFloat = CGFloat(rad) * (CGFloat(180) / CGFloat.pi )
if (expanded && deg == 0.0) {
arrowImageView.rotate(.pi)
}
}
FiltersViewController
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
header.setExpanded(expanded: filterSection.isExpanded)
header.delegate = self
return header
}
func toggleSection(header: CollapsibleFilterTableViewHeader, section: Int) {
let isExpanded = !filterSection.isExpanded
// Toggle isExpanded
filterSection.isExpanded = isExpanded
header.setExpanded(expanded: isExpanded)
filtersTableView.reloadSections(NSIndexSet(index: section) as IndexSet, with: .automatic)
}
Sources: Stackoverflow Question, Medium Article
Please try this code
func setExpanded(expanded: Bool) {
arrowImageView.transform = CGAffineTransform(rotationAngle: expanded ? CGFloat.pi : 0)
}
Try this out
func setExpanded(expanded: Bool) {
if (expanded) {
arrowImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
} else {
arrowImageView.transform = CGAffineTransform.identity
}
}
Set rotation when expanded otherwise provide CGAffineTransform.identity to reset
Hope it is helpful to you

How to drag a static cell into tableView swift?

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.

issue with animate with duration in swift

I got an error while using blocks in animateWithDuration:animations:completion: method
below is my code:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var cell=tableView.cellForRowAtIndexPath(indexPath)
var backGrndView:UIView?=cell?.contentView.viewWithTag(2) as UIView?
UIView.animateWithDuration(0.2,
animations: {
backGrndView?.backgroundColor=UIColor.redColor()
},
completion: { finished in
backGrndView?.backgroundColor=UIColor.redColor()
})
}
I tried the solutions at link.
But my problem is not solved.
below is screenshot:
Please help me out.
Thanks in advance
Seems that problem is the fact that swift auto returns if there is only one statement in closure. You can work around this by adding explicit returns:
UIView.animate(withDuration: 0.2,
animations: {
backGrndView?.backgroundColor = UIColor.red
return
},
completion: { finished in
backGrndView?.backgroundColor = UIColor.red
return
})
This statement:
backGrndView?.backgroundColor = UIColor.red
returns ()? that is same as Void?, but closure return type is Void.

Resources