Visually improve iOS 11 leadingSwipeActions for async actions - ios

I'd like to show a loading indicator when the user swipes the cell to the right, which triggers an asynchronous action. While this action is ongoing, I'd like to hide the label inside, keep the cell "inlined", not "re-swipeable" and show the already mentioned indicator.
This is what I got so far, and it is already partially working:
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let action = UIContextualAction(style: .normal, title: "My Action") { (action, view, handler: #escaping (Bool) -> Void) in
// Getting UIButtonLabel, which is a subclass of UILabel
guard let label = view as? UILabel else {
return
}
// Not used right now
guard let superView = label.superview else {
return
}
// Try to hide the existing label
label.text = "" // Doesn't work
label.attributedText = nil // Doesn't work
label.textColor = UIColor.clear // Doesn't work
// Add a loading indicator at the position of the label, works!
let loadingIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.white)
loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
loadingIndicator.startAnimating()
label.addSubview(loadingIndicator)
NSLayoutConstraint.activate([
loadingIndicator.centerXAnchor.constraint(equalTo: label.centerXAnchor),
loadingIndicator.centerYAnchor.constraint(equalTo: label.centerYAnchor),
loadingIndicator.heightAnchor.constraint(equalTo: label.heightAnchor, multiplier: 0.8),
loadingIndicator.widthAnchor.constraint(equalTo: loadingIndicator.heightAnchor)
])
self.viewModel.doAsyncAction(completionHandler: { (success) in
loadingIndicator.stopAnimating()
loadingIndicator.removeFromSuperview()
handler(true)
})))
}
action.backgroundColor = .darkGray
return UISwipeActionsConfiguration(actions:[action])
}
I'm able to add a loading indicator at the right position. I'm still not able to hide the label. Removing it or settings it's alpha or hidden state will not work, as the loading indicator will have to be a subview of it (for UITableView's internal positioning).
Also, I'm able to stop the user from pulling the action again by calling the callback handler late, but I'm not able to make the cell persist in that "one-level inline" swipe position. This is especially ugly when the user does not swipe the cell and click the button, but decides to swipe the cell all over to trigger the action. In this case I'd like the cell to automatically move to the "one level inline" position.
Does someone know a trick for this? I've seen this in some apps already. Is there a common UIKit addition for this kind of stuff?

One way I to go for I found out is disabling the superview, which is a subclass of a UIButton.
You can get it by
guard let superView = label.superview as? UIButton else {
return
}
and then disable it by doing
superView.isEnabled = false
label.isEnabled = false
label.isUserInteractionEnabled = false // Probably not required
This way, the label inside the button appears slightly faded. Also, it prevents the user from clicking the button multiple times.
It will not look so nice when the user "swipes to action", as the action button will remain at the slided position. But maybe this is something that can be tweaked easily, too?!

Related

How to you activate drag and order mode immediately after you tap on a button

Based on https://github.com/pgpt10/DragAndDrop-CollectionView
By using
self.collectionView.dragInteractionEnabled = true
self.collectionView.dragDelegate = self
self.collectionView.dropDelegate = self
Once you long press anywhere within a collection view cell, the following function will be triggered
extension DragDropViewController : UICollectionViewDragDelegate
{
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]
{
let item = collectionView == collectionView1 ? self.items1[indexPath.row] : self.items2[indexPath.row]
let itemProvider = NSItemProvider(object: item as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
However, I have a different requirement.
I have a collection which looks as the following
I wish when user tap (not long press) on the right 3 horizontal lines icon, he can immediately perform drag and reorder. Tapping other region, or long press on the cell, will not have drag and reorder effect.
May I know how can I achieve so?
Some notable app in App Store which able to achieve such feature
I notice Google Keep in App Store, able to achieve such feature. By just tapping on the left most icon in their Todo list, we can immediately reorder the Todo list item.
Wondering how they did that?
Approach 1: Install long press gesture on Cell's reorder icon
I had tried
Install long UILongPressGestureRecognizer on cell's reorder icon.
Use gesture.minimumPressDuration = 0 to mimic tap behavior.
class TabInfoSettingsItemCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
...
let gesture = UILongPressGestureRecognizer(target:self, action: #selector(longPressGesture))
gesture.minimumPressDuration = 0
reorderImageView.addGestureRecognizer(gesture)
}
But the outcome isn't encouraging. The "move" action isn't working at all!
Complete code can be found here : https://github.com/yccheok/ios-tutorial/tree/gesture-on-cell/TabDemo
Approach 2: Install long press gesture on Collection View
I had tried
Install long UILongPressGestureRecognizer on Collection View
Use gesture.minimumPressDuration = 0 to mimic tap behavior.
Here's the code snippet
let gesture = UILongPressGestureRecognizer(target:self, action: #selector(longPressGesture))
// Mimic short tap. But this blocks the events for delete button and text field :-(
gesture.minimumPressDuration = 0
collectionView.addGestureRecognizer(gesture)
But the outcome isn't perfect.
How can we recognise the tap event only within the reorder icon (icon with 3 horizontal lines) boundary.
Delete button no longer work as UILongPressGestureRecognizer blocks it from receiving event.
Text field no longer work as UILongPressGestureRecognizer blocks it from receiving event.
Complete code can be found here : https://github.com/yccheok/ios-tutorial/tree/gesture-on-collection-view/TabDemo
remove UILongPressGestureRecognizer from UICollectionView, Remove gesture comment from TabInfoSettingsItemCell class.
Replace this method in TabInfoSettingsController:
func changed(_ gesture: UILongPressGestureRecognizer) {
print("==changed==")
collectionView?.updateInteractiveMovementTargetPosition(gesture.location(in: collectionView))
}
and try that works.

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]
}

Increase the height of UINavigationBarLargeTitleView

I want to add a banner to the navigation bar, but by increasing the height of it. I want to copy the design and behaviour of an artist page in the Apple Music app:
It behaves just like a normal Large Title would, except for that it has been moved down, it has a sticky UIImageView behind it and it returns its background when the user scrolls down far enough. You can fire up Apple Music, search for an artist and go to their page to try it out yourself.
I've tried a bunch of things like setting the frame on the UINavigationBarLargeTitleView, and the code from this answer: https://stackoverflow.com/a/49326161/5544222
I already got a hold of the UINavigationBarLargeTitleView and its UILabel using the following code:
func setLargeTitleHeight() {
if let largeTitleView = self.getLargeTitleView() {
if let largeTitleLabel = self.getLargeTitleLabel(largeTitleView: largeTitleView) {
// Set largeTitleView height.
}
}
}
func getLargeTitleView() -> UIView? {
for subview in self.navigationBar.subviews {
if NSStringFromClass(subview.classForCoder).contains("UINavigationBarLargeTitleView") {
return subview
}
}
return nil
}
func getLargeTitleLabel(largeTitleView: UIView) -> UILabel? {
for subview in largeTitleView.subviews {
if subview.isMember(of: UILabel.self) {
return (subview as! UILabel)
}
}
return nil
}
Initially put a view with image and label and play button. Then clear the navigation bar it will show the image below it by
self.navigationController!.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController!.navigationBar.shadowImage = UIImage()
self.navigationController!.navigationBar.isTranslucent = true
later when you scroll up you have to handle it manually and again show the
navigation with title.

Swift 4.2 Add/Remove buttons with UICollectionViewCell

I am trying to add an UIButton to every cell in my UICollectionView when a button (outside of the UICollectionView) is pressed and then remove them when it's not.
Basically, a boolean if true - show/add, else hide/remove. This is my cellForItemAt. I also tried adding it to willDisplay cell.
let btnItemDelete = UIButton()
btnItemDelete.tag = indexPath.row
btnItemDelete.addTarget(self, action: #selector(self.btnItemDeleteClick), for: .touchUpInside) //Selector works
btnItemDelete.frame = CGRect(x: cell.bounds.width-22, y: 2, width: 20, height: 20) //Creation works
btnItemDelete.setImage(deleteImage, for: .normal) //Image works
if (isEdit) {
//Add or Show
cell.addSubview(btnItemDelete)
}
else {
//Delete or Hide
btnItemDelete.removeFromSuperview()
}
When running this, isEdit is initially set to false and the buttons do not show up. After clicking the button to change the boolean, the buttons appear. When clicking the button to set the boolean back to false, the buttons stay. I am figuring it's something with btnItemDelete.removeFromSuperview() - is there a different approach to do this? I figured I can't hide/show them because it's just going to keep adding a new button to the cell on every reload.
First you need to add to
cell.contentView not the cell
By this
btnItemDelete.removeFromSuperview()
you remove a button on the fly that's not added , instead you need
cell.contentView.subviews.forEach {
if $0.tag == 12 {
$0.removeFromSuperview()
}
}
I think the best approach is to add the button previously on the cell layout then manage it's appearance like
cell.myButton.isHidden = !isEdit

Hide button in UICollectionView cell

I am programmatically creating cells and adding a delete button to each one of them. The problem is that I'd like to toggle their .hidden state. The idea is to have an edit button that toggles all of the button's state at the same time. Maybe I am going about this the wrong way?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("verticalCell", forIndexPath: indexPath) as! RACollectionViewCell
let slide = panelSlides[indexPath.row]
cell.slideData = slide
cell.slideImageView.setImageWithUrl(NSURL(string: IMAGE_URL + slide.imageName + ".jpg")!)
cell.setNeedsLayout()
let image = UIImage(named: "ic_close") as UIImage?
var deleteButton = UIButton(type: UIButtonType.Custom) as UIButton
deleteButton.frame = CGRectMake(-25, -25, 100, 100)
deleteButton.setImage(image, forState: .Normal)
deleteButton.addTarget(self,action:#selector(deleteCell), forControlEvents:.TouchUpInside)
deleteButton.hidden = editOn
cell.addSubview(deleteButton)
return cell
}
#IBAction func EditButtonTap(sender: AnyObject) {
editOn = !editOn
sidePanelCollectionView.reloadData()
}
I think what you want to do is iterate over all of your data by index and then call cellForItemAtIndexPath: on your UICollectionView for each index. Then you can take that existing cell, cast it to your specific type as? RACollectionViewCell an then set the button hidden values this way.
Example (apologies i'm not in xcode to verify this precisely right now but this is the gist):
for (index, data) in myDataArray.enumerated() {
let cell = collectionView.cellForRowAtIndexPath(NSIndexPath(row: index, section: 0)) as? RACollectionViewCell
cell?.deleteButton.hidden = false
}
You probably also need some sort of isEditing Boolean variable in your view controller that keeps track of the fact that you are in an editing state so that as you scroll, newly configured cells continue to display with/without the button. You are going to need your existing code above as well to make sure it continues to work as scrolling occurs. Instead of creating a new delete button every time, you should put the button in your storyboard and set up a reference too and then you can just use something like cell.deleteButton.hidden = !isEditing

Resources