I have a UITextView which is embedded in a UIView amongst a number of other UIViews which are all in a UIScrollView (a form essentially). Instead of the textView automatically expanding with the content, I have a button underneath which I would like the user to be able to click and expand/colapse the textView.
Here is what I have:
var textViewIsExpanded: Bool = false {
didSet {
if self.textViewIsExpanded {
self.expandTextViewButton.isSelected = true
guard self.myTextView.contentSize.height > 70 else { return }
self.myTextView.isScrollEnabled = false
self.myTextView.translatesAutoresizingMaskIntoConstraints = true
self.myTextView.sizeThatFits(CGSize(width: self.scrollView.width - 24, height: CGFloat.greatestFiniteMagnitude))
} else {
self.expandTextViewButton.isSelected = false
self.myTextView.isScrollEnabled = true
self.myTextView.translatesAutoresizingMaskIntoConstraints = false
}
}
}
#IBAction func expandTextViewButtonTapped(_ sender: UIButton) {
textViewIsExpanded.toggle()
}
I have tried .sizeToFit() in place of .sizeThatFits(...) which sort of worked but it resized the width along with the height and I am only looking to expand/colapse the height. I'm guessing it is a matter of correctly implementing the CGSize and/or IB constraints but I am not able to land on a solution that works how I want.
First, it's a bad idea to toggle .translatesAutoresizingMaskIntoConstraints between true and false.
What you probably want is to give your text view a Height constraint of 70... connect it to an #IBOutlet... and then toggle .isActive on that constraint.
Second, if you have only one line of text, so the content size height is, maybe, 30, and then you call
textViewIsExpanded = true
your code as-is will set textViewIsExpanded to true but will leave .isScrollEnabled true --- so it won't really be "expanded".
Third, you need to let auto-layout know that you're changing the sizing behavior of the text view by calling:
self.myTextView.invalidateIntrinsicContentSize()
after toggling .isScrollEnabled.
So, add and connect a property for your text view's height constraint:
#IBOutlet var textViewHeightConstraint: NSLayoutConstraint!
and try changing your code to this:
var textViewIsExpanded: Bool = false {
didSet {
if self.textViewIsExpanded {
// if contentSize.height is less-than 71
// reset to false
if self.myTextView.contentSize.height < 71 {
self.textViewIsExpanded = false
return
} else {
self.expandTextViewButton.isSelected = true
self.myTextView.isScrollEnabled = false
self.textViewHeightConstraint.isActive = false
}
} else {
self.expandTextViewButton.isSelected = false
self.myTextView.isScrollEnabled = true
self.textViewHeightConstraint.isActive = true
}
self.myTextView.invalidateIntrinsicContentSize()
}
}
I want to get the reference of all applied constraint using storyboard without any reference:
I had tried many ways but could not able to find the exact solution:
My Approach is as follows:
if let constraint = (self.constraints.filter{$0.firstAttribute == .height}.first) {
}
using the above approach, I am able to find out the height only.
if let topConstraint = (self.constraints.filter{$0.firstAttribute == .top}.first) {
topConstraint.constant = 150//topMargin
}
if let leadingConstraint = (self.constraints.filter{$0.firstAttribute == .leading}.first) {
leadingConstraint.constant = 60 //leadingMargin
}
For topConstraint and leadingConstraint i am getting nil.
self.constraints
self.constraints is giving only one reference that is height only even I had applied leading, trailing and bottom constraint on the same view.
Note: I don't want to take reference from storyboard so please don't suggest that solution. I want reference dynamically.
I am looking for the approach something like below:
if let topConstraint = (self.constraints.filter{$0.firstAttribute == .top}.first) {
topConstraint.constant = 150//topMargin
}
if let leadingConstraint = (self.constraints.filter{$0.firstAttribute == .leading}.first) {
leadingConstraint.constant = 60 //leadingMargin
}
if let trailingConstraint = (self.constraints.filter{$0.firstAttribute == .trailing}.first) {
trailingConstraint.constant = 70//leadingMargin
}
if let bottomConstraint = (self.constraints.filter{$0.firstAttribute == .bottom}.first) {
bottomConstraint.constant = 150//49 + bottomMargin
}
But unfortunately above one is not working for me :(
For a single view you can easily get all the constraints related to it
for constraint in view.constraints {
print(constraint.constant)
}
And for all the subviews of a particular view, you can get like this
func getAllTheConstraintConstantsFor(view:UIView) {
for constraint in view.constraints {
print(constraint.constant)
}
for subview in view.subviews {
self.getAllTheConstraintConstantsFor(view: subview)
}
}
Here you can pass self.view and you will get all the constraints.
With reference to this answer
For a view like UIButton you can find top constraint by using this code.
extension UIButton {
func findTopConstraint() -> NSLayoutConstraint? {
for constraint in (self.superview?.constraints)! {
if isTopConstraint(constraint: constraint) {
return constraint
}
}
return nil
}
func isTopConstraint(constraint: NSLayoutConstraint) -> Bool {
return (firstItemMatchesTopConstraint(constraint: constraint) || secondItemMatchesTopConstraint(constraint: constraint))
}
func firstItemMatchesTopConstraint(constraint: NSLayoutConstraint) -> Bool {
return (constraint.firstItem as? UIButton == self && constraint.firstAttribute == .top )
}
func secondItemMatchesTopConstraint(constraint: NSLayoutConstraint) -> Bool {
return (constraint.secondItem as? UIButton == self && constraint.secondAttribute == .top)
}
}
To get top constaint on UIButton just use this code
button.findTopConstraint()!
Similarly you can find any constraint on any view.
Note : You need to manage nil case yourself.
Say I have a UIView,
class CleverView: UIView
In the custom class, I want to do this:
func changeWidth() {
let c = ... find my own layout constraint, for "width"
c.constant = 70 * Gameinfo.ImportanceOfEnemyFactor
}
Similarly I wanna be able to "find" like that, the constraint (or I guess, all constraints, there could be more than one) attached to one of the four edges.
So, to look through all the constraints attached to me, and find any width/height ones, or indeed any relevant to a given (say, "left") edge.
Any ideas?
It's perhaps worth noting this question
Please, note that (obviously) I am asking how to do this dynamically/programmatically.
(Yes, you can say "link to the constraint" or "use an ID" - the whole point of the QA is how to find them on the fly and work dynamically.)
If you are new to constraints, note that .constraints just gives you the ends stored "there".
There are really two cases:
Constraints regarding a view's size or relations to descendant views are saved in itself
Constraints between two views are saved in the views' lowest common ancestor
To repeat. For constraints which are between two views. iOS does, in fact, always store them in the lowest common ancestor. Thus, a constraint of a view can always be found by searching all ancestors of the view.
Thus, we need to check the view itself and all its superviews for constraints. One approach could be:
extension UIView {
// retrieves all constraints that mention the view
func getAllConstraints() -> [NSLayoutConstraint] {
// array will contain self and all superviews
var views = [self]
// get all superviews
var view = self
while let superview = view.superview {
views.append(superview)
view = superview
}
// transform views to constraints and filter only those
// constraints that include the view itself
return views.flatMap({ $0.constraints }).filter { constraint in
return constraint.firstItem as? UIView == self ||
constraint.secondItem as? UIView == self
}
}
}
You can apply all kinds of filters after getting all constraints about a view, and I guess that's the most difficult part. Some examples:
extension UIView {
// Example 1: Get all width constraints involving this view
// We could have multiple constraints involving width, e.g.:
// - two different width constraints with the exact same value
// - this view's width equal to another view's width
// - another view's height equal to this view's width (this view mentioned 2nd)
func getWidthConstraints() -> [NSLayoutConstraint] {
return getAllConstraints().filter( {
($0.firstAttribute == .width && $0.firstItem as? UIView == self) ||
($0.secondAttribute == .width && $0.secondItem as? UIView == self)
} )
}
// Example 2: Change width constraint(s) of this view to a specific value
// Make sure that we are looking at an equality constraint (not inequality)
// and that the constraint is not against another view
func changeWidth(to value: CGFloat) {
getAllConstraints().filter( {
$0.firstAttribute == .width &&
$0.relation == .equal &&
$0.secondAttribute == .notAnAttribute
} ).forEach( {$0.constant = value })
}
// Example 3: Change leading constraints only where this view is
// mentioned first. We could also filter leadingMargin, left, or leftMargin
func changeLeading(to value: CGFloat) {
getAllConstraints().filter( {
$0.firstAttribute == .leading &&
$0.firstItem as? UIView == self
}).forEach({$0.constant = value})
}
}
// edit: Enhanced examples and clarified their explanations in comments
I guess you can work with constraints property of UIView. constraints basically returns an array of constraint directly assigned to UIView. It will not be able to get you the constraints held by superview such as leading, trailing, top or bottom but width and height constraints are held by View itself. For superview's constraints, you can loop through superview's constraints. Lets say the clever view has these constraints:
class CleverView: UIView {
func printSuperViewConstriantsCount() {
var c = 0
self.superview?.constraints.forEach({ (constraint) in
guard constraint.secondItem is CleverView || constraint.firstItem is CleverView else {
return
}
c += 1
print(constraint.firstAttribute.toString())
})
print("superview constraints:\(c)")
}
func printSelfConstriantsCount() {
self.constraints.forEach { (constraint) in
return print(constraint.firstAttribute.toString())
}
print("self constraints:\(self.constraints.count)")
}
}
Output:
top
leading
trailing
superview constraints:3
height
self constraints:1
Basically, you can look at NSLayoutConstraint class to get the info out about a particular constraint.
To print the name of constraints, we can use this extension
extension NSLayoutAttribute {
func toString() -> String {
switch self {
case .left:
return "left"
case .right:
return "right"
case .top:
return "top"
case .bottom:
return "bottom"
case .leading:
return "leading"
case .trailing:
return "trailing"
case .width:
return "width"
case .height:
return "height"
case .centerX:
return "centerX"
case .centerY:
return "centerY"
case .lastBaseline:
return "lastBaseline"
case .firstBaseline:
return "firstBaseline"
case .leftMargin:
return "leftMargin"
case .rightMargin:
return "rightMargin"
case .topMargin:
return "topMargin"
case .bottomMargin:
return "bottomMargin"
case .leadingMargin:
return "leadingMargin"
case .trailingMargin:
return "trailingMargin"
case .centerXWithinMargins:
return "centerXWithinMargins"
case .centerYWithinMargins:
return "centerYWithinMargins"
case .notAnAttribute:
return "notAnAttribute"
}
}
}
stakri's answer is OK, but we can do better by using
sequence(first:next:):
extension UIView {
var allConstraints: [NSLayoutConstraint] {
sequence(first: self, next: \.superview)
.flatMap(\.constraints)
.lazy
.filter { constraint in
constraint.firstItem as? UIView == self || constraint.secondItem as? UIView == self
}
}
}
Then, if we check both implementations by swift-benchmark by Google we can see that Sequence implementation is much faster (almost +50k iterations for the ±same time).
running Find All Constraints: Stakri... done! (1778.86 ms)
running Find All Constraints: Sequence... done! (1875.20 ms)
name time std iterations
---------------------------------------------------------------
Find All Constraints.Stakri 3756.000 ns ± 96.67 % 291183
Find All Constraints.Sequence 3727.000 ns ± 117.42 % 342261
Might save someone some typing.......
Based on stakri's bounty-winning answer, here is exactly how to get
all constraints of the type "fractional width of another view"
all constraints of the type "fixed point width"
all constraints of the type "your x position"
So ..
fileprivate extension UIView {
func widthAsPointsConstraints()->[NSLayoutConstraint] {}
func widthAsFractionOfAnotherViewConstraints()->[NSLayoutConstraint] {}
func xPositionConstraints()->[NSLayoutConstraint]
}
Full code below. Of course, you can do "height" the same way.
So, use them like this...
let cc = someView.widthAsFractionOfAnotherViewConstraints()
for c in cc {
c.changeToNewConstraintWith(multiplier: 0.25)
}
or
let cc = someView.widthAsPointsConstraints()
for c in cc {
c.constant = 150.0
}
Also, at the bottom I pasted in a simple demo code, example output...
Here's the code. V2 ...
fileprivate extension UIView { // experimental
func allConstraints()->[NSLayoutConstraint] {
var views = [self]
var view = self
while let superview = view.superview {
views.append(superview)
view = superview
}
return views.flatMap({ $0.constraints }).filter { constraint in
return constraint.firstItem as? UIView == self ||
constraint.secondItem as? UIView == self
}
}
func widthAsPointsConstraints()->[NSLayoutConstraint] {
return self.allConstraints()
.filter({
( $0.firstItem as? UIView == self && $0.secondItem == nil )
})
.filter({
$0.firstAttribute == .width && $0.secondAttribute == .notAnAttribute
})
}
func widthAsFractionOfAnotherViewConstraints()->[NSLayoutConstraint] {
func _bothviews(_ c: NSLayoutConstraint)->Bool {
if c.firstItem == nil { return false }
if c.secondItem == nil { return false }
if !c.firstItem!.isKind(of: UIView.self) { return false }
if !c.secondItem!.isKind(of: UIView.self) { return false }
return true
}
func _ab(_ c: NSLayoutConstraint)->Bool {
return _bothviews(c)
&& c.firstItem as? UIView == self
&& c.secondItem as? UIView != self
&& c.firstAttribute == .width
}
func _ba(_ c: NSLayoutConstraint)->Bool {
return _bothviews(c)
&& c.firstItem as? UIView != self
&& c.secondItem as? UIView == self
&& c.secondAttribute == .width
}
// note that .relation could be anything: and we don't mind that
return self.allConstraints()
.filter({ _ab($0) || _ba($0) })
}
func xPositionConstraints()->[NSLayoutConstraint] {
return self.allConstraints()
.filter({
return $0.firstAttribute == .centerX || $0.secondAttribute == .centerX
})
}
}
extension NSLayoutConstraint {
// typical routine to "change" multiplier fraction...
#discardableResult
func changeToNewConstraintWith(multiplier:CGFloat) -> NSLayoutConstraint {
//NSLayoutConstraint.deactivate([self])
self.isActive = false
let nc = NSLayoutConstraint(
item: firstItem as Any,
attribute: firstAttribute,
relatedBy: relation,
toItem: secondItem,
attribute: secondAttribute,
multiplier: multiplier,
constant: constant)
nc.priority = priority
nc.shouldBeArchived = self.shouldBeArchived
nc.identifier = self.identifier
//NSLayoutConstraint.activate([nc])
nc.isActive = true
return nc
}
}
Just an example demo...
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
_teste()
delay(5) {
print("changing any 'fraction fo another view' style widths ...\n\n")
let cc = self.animeHolder.widthAsFractionOfAnotherViewConstraints()
for c in cc {
c.changeToNewConstraintWith(multiplier: 0.25)
}
self._teste()
}
delay(10) {
print("changing any 'points' style widths ...\n\n")
let cc = self.animeHolder.widthAsPointsConstraints()
for c in cc {
c.constant = 150.0
}
self._teste()
}
}
func _teste() {
print("\n---- allConstraints")
for c in animeHolder.allConstraints() {
print("\n \(c)")
}
print("\n---- widthAsPointsConstraints")
for c in animeHolder.widthAsPointsConstraints() {
print("\n \(c)\n \(c.multiplier) \(c.constant)")
}
print("\n---- widthAsFractionOfAnotherViewConstraints")
for c in animeHolder.widthAsFractionOfAnotherViewConstraints() {
print("\n \(c)\n \(c.multiplier) \(c.constant)")
}
print("\n----\n")
}
i want to know detect if the user touched the screen until rect is in a specific x or y position. If not stop the game here's my code:
func didTouchedIntime() -> Bool{
let screenRect = UIScreen.mainScreen().bounds
let viewHeight = screenRect.size.height
let viewWidth = screenRect.size.width
if rect.center.x == 0 {
return false
} else if rect.center.x == viewWidth {
return false
} else if rect.center.y == viewHeight {
return false
} else if rect.center.y == 0 {
return false
}
return true
}
This can be most easily done with a GestureRecognizer. There are, I believe, 3 or 4 different types of GestureRecognizers. You would drag this item onto your storyboard just as you would a label or a TextField. Once this is done, you can configure the GestureRecognizer to do exactly what you want to. Finally you should connect it to an IBAction method and write all the code of pausing the game and resuming the game in there.
Try this link for more information!
Also let me know if you have any other questions.
I just like to ask how can I implement the same behavior of UITableView`s swipe to delete in UICollectionView. I am trying to find a tutorial but I cannot find any.
Also, I am using PSTCollectionView wrapper to support iOS 5.
Thank you!
Edit:
The swipe recognizer is already good.
What I need now is the same functionality as UITableView's when cancelling the Delete mode, e.g. when user taps on a cell or on a blank space in the table view (that is, when user taps outside of the Delete button).
UITapGestureRecognizer won't work, since it only detects taps on release of a touch.
UITableView detects a touch on begin of the gesture (and not on release), and immediately cancels the Delete mode.
There is a simpler solution to your problem that avoids using gesture recognizers. The solution is based on UIScrollView in combination with UIStackView.
First, you need to create 2 container views - one for the visible part of the cell and one for the hidden part. You’ll add these views to a UIStackView. The stackView will act as a content view. Make sure that the views have equal widths with stackView.distribution = .fillEqually.
You’ll embed the stackView inside a UIScrollView that has paging enabled. The scrollView should be constrained to the edges of the cell. Then you’ll set the stackView’s width to be 2 times the scrollView’s width so each of the container views will have the width of the cell.
With this simple implementation, you have created the base cell with a visible and hidden view. Use the visible view to add content to the cell and in the hidden view you can add a delete button. This way you can achieve this:
I've set up an example project on GitHub. You can also read more about this solution here.
The biggest advantage of this solution is the simplicity and that you don't have to deal with constraints and gesture recognizers.
Its very simple..You need to add a customContentView and customBackgroundView behind the customContentView.
After that and you need to shift the customContentViewto the left as user swipes from right to left. Shifting the view makes visible to the customBackgroundView.
Lets Code:
First of all you need to add panGesture to your UICollectionView as
override func viewDidLoad() {
super.viewDidLoad()
self.panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.panThisCell))
panGesture.delegate = self
self.collectionView.addGestureRecognizer(panGesture)
}
Now implement the selector as
func panThisCell(_ recognizer:UIPanGestureRecognizer){
if recognizer != panGesture{ return }
let point = recognizer.location(in: self.collectionView)
let indexpath = self.collectionView.indexPathForItem(at: point)
if indexpath == nil{ return }
guard let cell = self.collectionView.cellForItem(at: indexpath!) as? CustomCollectionViewCell else{
return
}
switch recognizer.state {
case .began:
cell.startPoint = self.collectionView.convert(point, to: cell)
cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant
if swipeActiveCell != cell && swipeActiveCell != nil{
self.resetConstraintToZero(swipeActiveCell!,animate: true, notifyDelegateDidClose: false)
}
swipeActiveCell = cell
case .changed:
let currentPoint = self.collectionView.convert(point, to: cell)
let deltaX = currentPoint.x - cell.startPoint.x
var panningleft = false
if currentPoint.x < cell.startPoint.x{
panningleft = true
}
if cell.startingRightLayoutConstraintConstant == 0{
if !panningleft{
let constant = max(-deltaX,0)
if constant == 0{
self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: false)
}else{
cell.contentViewRightConstraint.constant = constant
}
}else{
let constant = min(-deltaX,self.getButtonTotalWidth(cell))
if constant == self.getButtonTotalWidth(cell){
self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: false)
}else{
cell.contentViewRightConstraint.constant = constant
cell.contentViewLeftConstraint.constant = -constant
}
}
}else{
let adjustment = cell.startingRightLayoutConstraintConstant - deltaX;
if (!panningleft) {
let constant = max(adjustment, 0);
if (constant == 0) {
self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: false)
} else {
cell.contentViewRightConstraint.constant = constant;
}
} else {
let constant = min(adjustment, self.getButtonTotalWidth(cell));
if (constant == self.getButtonTotalWidth(cell)) {
self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: false)
} else {
cell.contentViewRightConstraint.constant = constant;
}
}
cell.contentViewLeftConstraint.constant = -cell.contentViewRightConstraint.constant;
}
cell.layoutIfNeeded()
case .cancelled:
if (cell.startingRightLayoutConstraintConstant == 0) {
self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: true)
} else {
self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: true)
}
case .ended:
if (cell.startingRightLayoutConstraintConstant == 0) {
//Cell was opening
let halfOfButtonOne = (cell.swipeView.frame).width / 2;
if (cell.contentViewRightConstraint.constant >= halfOfButtonOne) {
//Open all the way
self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: true)
} else {
//Re-close
self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: true)
}
} else {
//Cell was closing
let buttonOnePlusHalfOfButton2 = (cell.swipeView.frame).width
if (cell.contentViewRightConstraint.constant >= buttonOnePlusHalfOfButton2) {
//Re-open all the way
self.setConstraintsToShowAllButtons(cell,animate: true, notifyDelegateDidOpen: true)
} else {
//Close
self.resetConstraintToZero(cell,animate: true, notifyDelegateDidClose: true)
}
}
default:
print("default")
}
}
Helper methods to update constraints
func getButtonTotalWidth(_ cell:CustomCollectionViewCell)->CGFloat{
let width = cell.frame.width - cell.swipeView.frame.minX
return width
}
func resetConstraintToZero(_ cell:CustomCollectionViewCell, animate:Bool,notifyDelegateDidClose:Bool){
if (cell.startingRightLayoutConstraintConstant == 0 &&
cell.contentViewRightConstraint.constant == 0) {
//Already all the way closed, no bounce necessary
return;
}
cell.contentViewRightConstraint.constant = -kBounceValue;
cell.contentViewLeftConstraint.constant = kBounceValue;
self.updateConstraintsIfNeeded(cell,animated: animate) {
cell.contentViewRightConstraint.constant = 0;
cell.contentViewLeftConstraint.constant = 0;
self.updateConstraintsIfNeeded(cell,animated: animate, completionHandler: {
cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant;
})
}
cell.startPoint = CGPoint()
swipeActiveCell = nil
}
func setConstraintsToShowAllButtons(_ cell:CustomCollectionViewCell, animate:Bool,notifyDelegateDidOpen:Bool){
if (cell.startingRightLayoutConstraintConstant == self.getButtonTotalWidth(cell) &&
cell.contentViewRightConstraint.constant == self.getButtonTotalWidth(cell)) {
return;
}
cell.contentViewLeftConstraint.constant = -self.getButtonTotalWidth(cell) - kBounceValue;
cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell) + kBounceValue;
self.updateConstraintsIfNeeded(cell,animated: animate) {
cell.contentViewLeftConstraint.constant = -(self.getButtonTotalWidth(cell))
cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell)
self.updateConstraintsIfNeeded(cell,animated: animate, completionHandler: {(check) in
cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant;
})
}
}
func setConstraintsAsSwipe(_ cell:CustomCollectionViewCell, animate:Bool,notifyDelegateDidOpen:Bool){
if (cell.startingRightLayoutConstraintConstant == self.getButtonTotalWidth(cell) &&
cell.contentViewRightConstraint.constant == self.getButtonTotalWidth(cell)) {
return;
}
cell.contentViewLeftConstraint.constant = -self.getButtonTotalWidth(cell) - kBounceValue;
cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell) + kBounceValue;
self.updateConstraintsIfNeeded(cell,animated: animate) {
cell.contentViewLeftConstraint.constant = -(self.getButtonTotalWidth(cell))
cell.contentViewRightConstraint.constant = self.getButtonTotalWidth(cell)
self.updateConstraintsIfNeeded(cell,animated: animate, completionHandler: {(check) in
cell.startingRightLayoutConstraintConstant = cell.contentViewRightConstraint.constant;
})
}
}
func updateConstraintsIfNeeded(_ cell:CustomCollectionViewCell, animated:Bool,completionHandler:#escaping ()->()) {
var duration:Double = 0
if animated{
duration = 0.1
}
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: {
cell.layoutIfNeeded()
}, completion:{ value in
if value{ completionHandler() }
})
}
I have created a sample project here in Swift 3.
It is a modified version of this tutorial.
In the Collection View Programming Guide for iOS, in the section Incorporating Gesture Support, the docs read:
You should always attach your gesture recognizers to the collection view itself and not to a specific cell or view.
So, I think it's not a good practice to add recognizers to UICollectionViewCell.
I followed a similar approach to #JacekLampart, but decided to add the UISwipeGestureRecognizer in the UICollectionViewCell's awakeFromNib function so it is only added once.
UICollectionViewCell.m
- (void)awakeFromNib {
UISwipeGestureRecognizer* swipeGestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipeToDeleteGesture:)];
swipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
[self addGestureRecognizer:swipeGestureRecognizer];
}
- (void)swipeToDeleteGesture:(UISwipeGestureRecognizer *)swipeGestureRecognizer {
if (swipeGestureRecognizer.state == UIGestureRecognizerStateEnded) {
// update cell to display delete functionality
}
}
As for exiting delete mode, I created a custom UIGestureRecognizer with an NSArray of UIViews. I borrowed the idea from #iMS from this question: UITapGestureRecognizer - make it work on touch down, not touch up?
On touchesBegan, if the touch point isn't within any of the UIViews, the gesture succeeds and delete mode is exited.
In this way, I am able to pass the delete button within the cell (and any other views) to the UIGestureRecognizer and, if the touch point is within the button's frame, delete mode will not exit.
TouchDownExcludingViewsGestureRecognizer.h
#import <UIKit/UIKit.h>
#interface TouchDownExcludingViewsGestureRecognizer : UIGestureRecognizer
#property (nonatomic) NSArray *excludeViews;
#end
TouchDownExcludingViewsGestureRecognizer.m
#import "TouchDownExcludingViewsGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
#implementation TouchDownExcludingViewsGestureRecognizer
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.state == UIGestureRecognizerStatePossible) {
BOOL touchHandled = NO;
for (UIView *view in self.excludeViews) {
CGPoint touchLocation = [[touches anyObject] locationInView:view];
if (CGRectContainsPoint(view.bounds, touchLocation)) {
touchHandled = YES;
break;
}
}
self.state = (touchHandled ? UIGestureRecognizerStateFailed : UIGestureRecognizerStateRecognized);
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
self.state = UIGestureRecognizerStateFailed;
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
self.state = UIGestureRecognizerStateFailed;
}
#end
Implementation (in the UIViewController containing UICollectionView):
#import "TouchDownExcludingViewsGestureRecognizer.h"
TouchDownExcludingViewsGestureRecognizer *touchDownGestureRecognizer = [[TouchDownExcludingViewsGestureRecognizer alloc] initWithTarget:self action:#selector(exitDeleteMode:)];
touchDownGestureRecognizer.excludeViews = #[self.cellInDeleteMode.deleteButton];
[self.view addGestureRecognizer:touchDownGestureRecognizer];
- (void)exitDeleteMode:(TouchDownExcludingViewsGestureRecognizer *)touchDownGestureRecognizer {
// exit delete mode and disable or remove TouchDownExcludingViewsGestureRecognizer
}
You can try adding a UISwipeGestureRecognizer to each collection cell, like this:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CollectionViewCell *cell = ...
UISwipeGestureRecognizer* gestureRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(userDidSwipe:)];
[gestureRecognizer setDirection:UISwipeGestureRecognizerDirectionRight];
[cell addGestureRecognizer:gestureRecognizer];
}
followed by:
- (void)userDidSwipe:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//handle the gesture appropriately
}
}
With iOS 14, you can use UICollectionViewLayoutListConfiguration in conjunction with UICollectionViewCompositionalLayout to get this functionality natively for free, no custom cells or gesture recognizes needed.
If your minimum deploy target is >= iOS 14.x, this is probably the preferred method from now on, and it will also let you to adopt modern cell configuration with UIContentView and UIContentConfiguration to boot.
There is a more standard solution to implement this feature, having a behavior very similar to the one provided by UITableView.
For this, you will use a UIScrollView as the root view of the cell, and then position the cell content and the delete button inside the scroll view. The code in your cell class should be something like this:
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(scrollView)
scrollView.addSubview(viewWithCellContent)
scrollView.addSubview(deleteButton)
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
}
In this code we set the property isPagingEnabled to true to make the scroll view to stop scrolling only at the boundaries of its content. The layout subviews for this cell should be something like:
override func layoutSubviews() {
super.layoutSubviews()
scrollView.frame = bounds
// make the view with the content to fill the scroll view
viewWithCellContent.frame = scrollView.bounds
// position the delete button just at the right of the view with the content.
deleteButton.frame = CGRect(
x: label.frame.maxX,
y: 0,
width: 100,
height: scrollView.bounds.height
)
// update the size of the scrolleable content of the scroll view
scrollView.contentSize = CGSize(width: button.frame.maxX, height: scrollView.bounds.height)
}
With this code in place, if you run the app you will see that the swipe to delete is working as expected, however, we lost the ability to select the cell. The problem is that since the scroll view is filling the whole cell, all the touch events are processed by it, so the collection view will never have the opportunity to select the cell (this is similar to when we have a button inside a cell, since touches on that button don't trigger the selection process but are handled directly by the button.)
To fix this problem we just have to indicate the scroll view to ignore the touch events that are processed by it and not by one of its subviews. To achieve this just create a subclass of UIScrollView and override the following function:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
return result != self ? result : nil
}
Now in your cell you should use an instance of this new subclass instead of the standard UIScrollView.
If you run the app now you will see that we have the cell selection back, but this time the swipe isn't working 😳. Since we are ignoring touches that are handled directly by the scroll view, then its pan gesture recognizer won't be able to start recognizing touch events. However, this can be easily fixed by indicating to the scroll view that its pan gesture recognizer will be handled by the cell and not by the scroll. You do this adding the following line at the bottom of your cell's init(frame: CGRect):
addGestureRecognizer(scrollView.panGestureRecognizer)
This may look like a bit hacky, but it isn't. By design, the view that contains a gesture recognizer and the target of that recognizer don't have to be the same object.
After this change all should be working as expected. You can see a full implementation of this idea in this repo