I'm wondering if anyone has a link to a good tutorial or can point me in the right direction to recreate the 'drag to reorder' cells in a UITableView like Epic Win App. The basic idea is you tap and hold on a list item and then you drag to where you want the item to be. Any help would be greatly appreciated.
The simplest way, using the built-in methods, is as follows:
First, set your table cell to have shows reorder control on. Simplest example (using ARC):
This is assuming NSMutableArray *things has been created somewhere else in this class
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"do not leak me"];
if (!cell) {
cell = [[UITableViewCell alloc] init];
cell.showsReorderControl = YES;
}
cell.textLabel.text = [things objectAtIndex:indexPath.row];
return cell;
}
Implement your two UITableViewDelegate methods, like this:
This method tells the tableView it is allowed to be reordered
-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
This is where the tableView reordering actually happens
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
id thing = [things objectAtIndex:sourceIndexPath.row];
[things removeObjectAtIndex:sourceIndexPath.row];
[things insertObject:thing atIndex:destinationIndexPath.row];
}
and then somehow, somewhere, set editing to true. This is one example of how you can do this. The tableView must be in editing mode in order for it to be reordered.
- (IBAction)doSomething:(id)sender {
self.table.editing = YES;
}
Hope that serves as a concrete example.
It is pretty straight forward - which is probibly why there is no explicit tutorial on the matter.
Just create the UITableView normally, but set the showsReorderControl of each cell to TRUE. When you go into editing mode (normally by pressing the "edit" button and setting the "Editing" value of the UITableView to TRUE) - the reorder bars will appear in the cells.
Note:
If your data source implements tableView:canMoveRowAtIndexPath: - and it returns "NO" - the reorder bars will not appear.
You will also need to implement –tableView:moveRowAtIndexPath:toIndexPath: in the data source delegate.
This solution here will function similar to the Reminders app on OSX which allows you to reorder cells by holding and dragging anywhere.
I've created a subclass of UITableView that allows you to drag and reorder the cells without having to have editing on or use the standard reorder handles.
The basic idea is I use the touches began and touches ended to get which cell was tapped, I then create a fake view that is really just a screenshot of the view you tapped and move it back and forth, swapping the cells in the background. When the user lets go I then unhide the original view.
class ReorderTableView: UITableView {
var customView:UIImageView?
var oldIndexPath:NSIndexPath?
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let touch1 = touches.first else{
return
}
oldIndexPath = self.indexPathForRowAtPoint(touch1.locationInView(self))
guard (oldIndexPath != nil) else{
return
}
let oldCell = self.cellForRowAtIndexPath(self.oldIndexPath!)
customView = UIImageView(frame: CGRectMake(0, touch1.locationInView(self).y - 20, self.frame.width, 40))
customView?.image = screenShotView(oldCell!)
customView?.layer.shadowColor = UIColor.blackColor().CGColor
customView?.layer.shadowOpacity = 0.5
customView?.layer.shadowOffset = CGSizeMake(1, 1)
self.addSubview(customView!)
oldCell?.alpha = 0
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let touch1 = touches.first else{
return
}
let newIndexPath = self.indexPathForRowAtPoint(touch1.locationInView(self))
guard newIndexPath != nil else{
return
}
guard oldIndexPath != nil else{
return
}
if newIndexPath != oldIndexPath{
self.moveRowAtIndexPath(oldIndexPath!, toIndexPath: newIndexPath!)
oldIndexPath = newIndexPath
self.cellForRowAtIndexPath(self.oldIndexPath!)!.alpha = 0
}
self.customView!.frame.origin = CGPointMake(0, touch1.locationInView(self).y - 20)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.customView?.removeFromSuperview()
self.customView = nil
guard (oldIndexPath != nil) else{
return
}
self.cellForRowAtIndexPath(self.oldIndexPath!)!.alpha = 1
}
func screenShotView(view: UIView) -> UIImage? {
let rect = view.bounds
UIGraphicsBeginImageContextWithOptions(rect.size,true,0.0)
let context = UIGraphicsGetCurrentContext()
CGContextTranslateCTM(context, 0, -view.frame.origin.y);
self.layer.renderInContext(context!)
let capturedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return capturedImage
}
}
In the end you can reorder cells without having to use the handles. This can be extended for different gestures, or modes depending on the use case, but I left this as a vanilla version. Let me know if you have any questions.
Update:
Some caveats I found. It is very important you figure out how you want to use scrolling and modify this accordingly. For instance, when I mocked it out I set scrollingEnabled = false. In the long run I did want scrolling, so I changed the code to only run this functionality on a press longer than 0.25 (I used some timers to do this). I would then temporarily disable scrolling during this period, and re-enable upon completion.
Related
HI i have searched everywhere for a solution to my problem and cannot figure it out or found any other posts with the same problem.
I recently updated my Xcode project to Xcode 13 and my iPhone to iOS 15 and Apple made some significant changes to the navigation and tab bar structure which completely destroyed my app.
Need assistance on the correct structure on how to resolve this problem and am sure this will help lots of people who come across this same issue.
I have a ViewControllerA inside of a navigation controller which is nested inside of tab bar controller. User taps a button and segues to another ViewControllerB which is a modal VC that goes over full screen. User can then swipe down with a panGestureRecognizer.
As user swipes down there is a strange flicker effect which looks awful. The only solution I found is in storyboards to completely remove navigation bar and apply constraints to top of the view(I know this is possible but not scalable as I do not want to determine device size). But then view is covered by top bar.
I have attached video showing problem.
Please help as I do not know where to go from here.
THANK YOU
var initialTouchPoint: CGPoint = CGPoint(x: 0,y: 0)
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.estimatedRowHeight = 636
tableView.rowHeight = UITableView.automaticDimension
}
#IBAction func panGestureRecognizerHandler(_ sender: UIPanGestureRecognizer) {
let touchPoint = sender.location(in: self.view?.window)
if sender.state == UIGestureRecognizer.State.began {
initialTouchPoint = touchPoint
} else if sender.state == UIGestureRecognizer.State.changed {
if touchPoint.y - initialTouchPoint.y > 0 {
self.view.frame = CGRect(x: 0, y: touchPoint.y - initialTouchPoint.y, width: self.view.frame.size.width, height: self.view.frame.size.height)
}
} else if sender.state == UIGestureRecognizer.State.ended || sender.state == UIGestureRecognizer.State.cancelled {
if touchPoint.y - initialTouchPoint.y > 100 {
self.dismiss(animated: true)
} else {
UIView.animate(withDuration: 0.3, animations: {
self.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
})
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SwipeDownCell", for: indexPath) as! SwipeDownTableViewCell
cell.postImageView.image = UIImage(named: "food")
cell.postImageView.contentMode = .scaleAspectFill
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
https://streamable.com/924xoo
I have the same issue. It is caused by a combination of safeArea and the modalPresentationStyle of the parent view controller. In my case it is UIModalPresentationOverCurrentContext.
If you use other presentation styles, e.g. UIModalPresentationPopover, the flickering disappear.
As this is not appropriate in my case, I found an easy workaround.
You need to set the top constraint of your first item in the child view to refer the superview, not the safe area!
To better support devices without the notch, you can use following helper method implemented in AppDelegate.
- (UIEdgeInsets)getSafeAreaInsets {
if (#available(iOS 11.0, *)) {
UIEdgeInsets insets = self.window.safeAreaInsets;
return insets;
} else {
return UIEdgeInsetsZero;
}
Call this in the viewDidLoad of the child view and check if the bottom edge is 0.
//classic devices without notch
if ([(AppDelegate*)[[UIApplication sharedApplication] delegate] getSafeAreaInsets].bottom == 0) {
self.labelHeaderContraint.constant = 32;
}
I'm adapting my iPad app to Mac Catalyst and in the app I have a UITextView inside a UITableViewCell with some strange behavior. All of my textViews inside tableview cells are entering the return key. I just press on a textView and it's stuck pressing the return key making new lines (I'm not typing anything). I've tried using different keyboards and I'm getting the same outcome.
This doesn't happen on iPhone or iPad. This also doesn't happen all the time it's very random. Does anyone know how to fix this?
Here's my code:
class TextViewCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
textView.delegate = self
textView.isScrollEnabled = false
textView.returnKeyType = .done
}
}
// MARK: - textView functions
extension TextViewCell: UITextViewDelegate {
//grow textView as the user types
func textViewDidChange(_ textView: UITextView) {
let size = textView.bounds.size
let newSize = textView.sizeThatFits(CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude))
if size.height != newSize.height {
UIView.setAnimationsEnabled(false)
tableView?.beginUpdates()
tableView?.endUpdates()
UIView.setAnimationsEnabled(true)
if let thisIndexPath = tableView?.indexPath(for: self) {
tableView?.scrollToRow(at: thisIndexPath, at: .bottom, animated: false)
}
}
}
}
Has anyone else run into this issue and knows how to fix it???
Try performing any operation on the visible elements of the UITableView off the GUI thread. For some reason on Catalyst without this operations fail.
DispatchQueue.main.async{
// stop animations to prevent boucing
UIView.setAnimationsEnabled(false)
// cycle updates to allow frames to be recalculated
self.tableView.beginUpdates()
self.tableView.endUpdates()
// done with all the updates, reset back to normal
UIView.setAnimationsEnabled(true)
}
I have a custom inline date picker that expands and collapses just like the Apple Calendar events date pickers. The problem is that I need the Date Picker to in a UITableViewCell that is at the bottom of the UITableView, but when it expands, you cannot see the Picker without scrolling the view down manually. For a similar issue (UITextField disappears behind keyboard when it's being edited if it's below the height of the keyboard) I was able to fix it with an animation in the beginEditing and endEditing delegate functions:
let keyboardHeight: CGFloat = 216
UIView.animateWithDuration(0.25, delay: 0.25, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
self.view.frame = CGRectMake(0, (self.view.frame.origin.y - keyboardHeight), self.view.bounds.width, self.view.bounds.height)
}, completion: nil)
Does anyone know of a similar animation that would work for maybe placing in didSelectRowAtIndexPath, and if it's a date picker the screen will adjust by scrolling down to show the full date picker expanded, and then it will adjust back when the row is not selected anymore - or something along these lines?
I'm not sure if the fact that there's already an animation occurring to expand/collapse the date picker would conflict with this or if it's just a matter of precedence for the animations to occur. Anyways I will post some code below:
When the DatePicker is selected in the tableView:
/**
Used to notify the DatePickerCell that it was selected. The DatePickerCell will then run its selection animation and expand or collapse.
*/
public func selectedInTableView() {
expanded = !expanded
UIView.transitionWithView(rightLabel, duration: 0.25, options:UIViewAnimationOptions.TransitionCrossDissolve, animations: { () -> Void in
self.rightLabel.textColor = self.expanded ? self.tintColor : self.rightLabelTextColor
}, completion: nil)
}
didSelectRowAtIndexPath function:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Deselect automatically if the cell is a DatePickerCell.
let cell = self.tableView(tableView, cellForRowAtIndexPath: indexPath)
if (cell.isKindOfClass(DatePickerCell)) {
let datePickerTableViewCell = cell as! DatePickerCell
datePickerTableViewCell.selectedInTableView()
tableView.deselectRowAtIndexPath(indexPath, animated: true)
} else if(cell.isKindOfClass(PickerCell)) {
let pickerTableViewCell = cell as! PickerCell
pickerTableViewCell.selectedInTableView()
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
tableView.beginUpdates()
tableView.endUpdates()
}
I think I came up with a solution that seems to work as needed, I wrote this code in the didSelectRowAtIndexPath delegate function after the call to selectedInTablView():
if(datePickerTableViewCell.isExpanded() && datePickerTableViewCell.datePicker == billDatePicker.datePicker) {
tableView.contentOffset = CGPoint(x: 0, y: self.view.frame.height + datePickerTableViewCell.datePickerHeight())
}
I have created a table view and i am adding my custom uiview into the cell's content view.
The uiview has a uibutton. However i can add the uibutton into the content view when the cell is created.
I want to get the tap event on the tableview to perform some action. I also want tap event on the uibutton to perform a different action.
The problem is when i tap a row the button is also getting pressed and two events are triggered.
How can i get both the events discreetly? i.e tap on the tableview cell when portion the cell outside the uibuttons boundary is clicked.
You can add a Button in cell in as
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake( 13.0f, 13.0f, 90.0f, 90.0f)];
[button addTarget:self action:#selector(BtnPressed) forControlEvents:UIControlEventTouchUpInside];
[cellView.contentView addSubview:button];
then you can get events separately
Are you sure two different events are being triggered by the same tap? My understanding is that UIKit generates only one UIEvent and sends it to the top-most view in the hierarchy that responds to that type of gesture. In this case, if the button is higher in the view hierarchy, as it probably is, it should be receiving the event message. But, I may be wrong.
One solution to definitely avoid the possibility of two events being triggered, though possibly not the ideal, would be to deactivate row selection for the tableView, as follows:
self.tableView.allowsSelection = NO;
Then, add a view covering the remainder of the tableCell & add a gesture recognizer to that view. Since they don't cover the same area, there is no chance of conflicting events. Of course, to know what row was tapped, you'd have to add an instance variable for both the button and the new view to hold the indexPath. You would set the index path when you set up the tableCell in tableView:cellForRowAtIndexPath:
Hope this is helpful or gives you some new ideas.
You could subclass UITableViewCell and add a button, here is an example class:
import UIKit
private let DWWatchlistAddSymbolCellAddSymbolButtonLeftMargin: CGFloat = 15
class DWWatchlistAddSymbolCell: UITableViewCell {
private(set) var addSymbolButton:UIButton
init(reuseIdentifier:String?, primaryTextColor:UIColor) {
self.addSymbolButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
super.init(style:UITableViewCellStyle.Default, reuseIdentifier:reuseIdentifier)
selectionStyle = UITableViewCellSelectionStyle.None
backgroundColor = UIColor.clearColor()
imageView!.image = UIImage(named:"PlusIcon")!
addSymbolButton.setTitle("Add Symbol", forState:UIControlState.Normal)
addSymbolButton.setTitleColor(primaryTextColor, forState:UIControlState.Normal)
addSymbolButton.contentHorizontalAlignment = UIControlContentHorizontalAlignment.Left
addSymbolButton.contentVerticalAlignment = UIControlContentVerticalAlignment.Center
contentView.addSubview(addSymbolButton)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Layout
override func layoutSubviews() {
super.layoutSubviews()
addSymbolButton.frame = contentView.frame
let imageViewFrame = imageView!.frame
let imageViewMaxX = imageViewFrame.origin.x + imageViewFrame.size.width
let addSymbolButtonX = imageViewMaxX + DWWatchlistAddSymbolCellAddSymbolButtonLeftMargin
addSymbolButton.contentEdgeInsets = UIEdgeInsetsMake(0, addSymbolButtonX, 0, 0);
}
}
You can see the "Add Symbol" button at the bottom:
When you're returning the cell for the UITableViewDataSource, make sure to set the target for the button:
private func addButtonCell(tableView: UITableView) -> UITableViewCell {
var cell:DWWatchlistAddSymbolCell? = tableView.dequeueReusableCellWithIdentifier(kDWWatchlistViewControllerAddButtonCellReuseIdentifier) as? DWWatchlistAddSymbolCell
if (cell == nil) {
cell = DWWatchlistAddSymbolCell(reuseIdentifier:kDWWatchlistViewControllerAddButtonCellReuseIdentifier, primaryTextColor:primaryTextColor)
}
cell!.addSymbolButton.addTarget(self,
action:Selector("didPressAddSymbolButton"),
forControlEvents:UIControlEvents.TouchUpInside)
return cell!
}
Is there any way to know if a tableview cell is currently visible?
I have a tableview whose first cell(0) is a uisearchbar.
If a search is not active, then hide cell 0 via an offset.
When the table only has a few rows, the row 0 is visible.
How to determine if row 0 is visible or is the top row?
UITableView has an instance method called indexPathsForVisibleRows that will return an NSArray of NSIndexPath objects for each row in the table which are currently visible. You could check this method with whatever frequency you need to and check for the proper row. For instance, if tableView is a reference to your table, the following method would tell you whether or not row 0 is on screen:
-(BOOL)isRowZeroVisible {
NSArray *indexes = [tableView indexPathsForVisibleRows];
for (NSIndexPath *index in indexes) {
if (index.row == 0) {
return YES;
}
}
return NO;
}
Because the UITableView method returns the NSIndexPath, you can just as easily extend this to look for sections, or row/section combinations.
This is more useful to you than the visibleCells method, which returns an array of table cell objects. Table cell objects get recycled, so in large tables they will ultimately have no simple correlation back to your data source.
To checking tableview cell is visible or not use this code of line
if(![tableView.indexPathsForVisibleRows containsObject:newIndexPath])
{
// your code
}
here newIndexPath is IndexPath of checking cell.....
Swift 3.0
if !(tableView.indexPathsForVisibleRows?.contains(newIndexPath)) {
// Your code here
}
I use this in Swift 3.0
extension UITableView {
/// Check if cell at the specific section and row is visible
/// - Parameters:
/// - section: an Int reprenseting a UITableView section
/// - row: and Int representing a UITableView row
/// - Returns: True if cell at section and row is visible, False otherwise
func isCellVisible(section:Int, row: Int) -> Bool {
guard let indexes = self.indexPathsForVisibleRows else {
return false
}
return indexes.contains {$0.section == section && $0.row == row }
} }
Swift Version:
if let indices = tableView.indexPathsForVisibleRows {
for index in indices {
if index.row == 0 {
return true
}
}
}
return false
Another solution (which can also be used to check if a row is fully visible) would be to check if the frame for the row is inside the visible rect of the tableview.
In the following code, ip represents the NSIndexPath:
CGRect cellFrame = [tableView rectForRowAtIndexPath:ip];
if (cellFrame.origin.y<tableView.contentOffset.y) { // the row is above visible rect
[tableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
else if(cellFrame.origin.y+cellFrame.size.height>tableView.contentOffset.y+tableView.frame.size.height-tableView.contentInset.top-tableView.contentInset.bottom){ // the row is below visible rect
[tableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:NO];
}
Also using cellForRowAtIndexPath: should work, since it returns a nil object if the row is not visible:
if([tableView cellForRowAtIndexPath:ip]==nil){
// row is not visible
}
Note that "Somewhat visible" is "visible". Also, in viewWillAppear, you might get false positives from indexPathsForVisibleRows as layout is in progress, and if you're looking at the last row, even calling layoutIfNeeded() won't help you for tables. You'll want to check things in/after viewDidAppear.
This swift 3.1 code will disable scroll if the last row is fully visible. Call it in/after viewDidAppear.
let numRows = tableView.numberOfRows(inSection: 0) // this can't be in viewWillAppear because the table's frame isn't set to proper height even after layoutIfNeeded()
let lastRowRect = tableView.rectForRow(at: IndexPath.init(row: numRows-1, section: 0))
if lastRowRect.origin.y + lastRowRect.size.height > tableView.frame.size.height {
tableView.isScrollEnabled = true
} else {
tableView.isScrollEnabled = false
}
Swift:
Improved #Emil answer
extension UITableView {
func isCellVisible(indexPath: IndexPath) -> Bool {
guard let indexes = self.indexPathsForVisibleRows else {
return false
}
return indexes.contains(indexPath)
}
}
IOS 4:
NSArray *cellsVisible = [tableView indexPathsForVisibleRows];
NSUInteger idx = NSNotFound;
idx = [cellsVisible indexOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop)
{
return ([(NSIndexPath *)obj compare:indexPath] == NSOrderedSame);
}];
if (idx == NSNotFound)
{
Best to check by Frame
let rectOfCellInSuperview = yourTblView.convert(yourCell.frame, to: yourTblView.superview!) //make sure your tableview has Superview
if !yourTblView.frame.contains(rectOfCellInSuperview) {
//Your cell is not visible
}
Swift 2023 simple solution, for a given index path:
tableView.indexPathsForVisibleItems.contains(cellIndexPath)
For me the problem was that indexPathsForVisibleRows doesn't take tableView insets into account. So for example if you change insets on keyboard appearance, rows covered by keyboard still are considered visible. So I had to do this:
func isRowVisible(at indexPath: IndexPath, isVisibleFully: Bool = false) -> Bool {
let cellRect = tableView.rectForRow(at: indexPath)
let cellY = isVisibleFully ? cellRect.maxY : cellRect.minY
return cellY > tableView.minVisibleY && cellY < tableView.maxVisibleY
}
And this is UITableView extension:
extension UITableView {
var minVisibleY: CGFloat {
contentOffset.y + contentInset.top
}
var maxVisibleY: CGFloat {
contentOffset.y + bounds.height - contentInset.bottom
}
}