Saturday, November 19, 2016

How To: Create Swipeable Tableview Cell with Any Objects

Here is what we're going to accomplish:


(May take a while to show: 3.3MB O_O)





But first... digressing (as always)

AW YISSS!


Be The Bird - Happy With Breadcrumbs XD


Hey guys what's going on. Been a while since I last wrote any tutorial for you wonderful programmers. So today I found some time to write it. Something broke in my blog - the source code part doesn't appear as they should! I apologize but as I checked the code formatting widget sourcecode that was hosted on googledrive was removed by the owner! (WTF dude).

Anyhow, I needed to change my blog layout design too - as you can see now my blog theme has changed. Many things on the blog didn't work well. For example, you need to open a blog post first before you can read/post comments and so on. So after hours of searching I found an okay-ish blog theme from the blogger standard themes. I feel simplest theme is the best. I still need to find a replacement for the code formatting widget tho. T_T

Today I want to teach you how to make your UITableView cells swipeable. In iOS7 (or was it iOS8?), Apple introduce a new API to create swipeable tableview cells. But only limited to buttons (or "actions" as they call it). What if you want to have an image there? Well, if you want images, you still can assign image as the button's backgroundImage. But, what if you want a Switch there? Or TextView? Or thingamabob? (I love this word - THINGAMABOB).


The only way to achieve that at the moment, is to CUSTOM CLASS the UITableViewCell. OR!!! OR YOU COULD USE LIBRARY! Yep, there are multiple awesome libraries for swipeable tableview cells created by awesome, generous, developers out there over at GITHUB and other places. However, if you use library, you will not learn so much. This is why I prefer to do things my own, because I want to learn how it works. And you learn best, by doing.

So lets go.

First, create a single view application. Then you add a UITableView and set all the necessary constraints (sides and bottom to superview margin=0, and top margin to Top Layout guide). Then add a Prototype UITableViewCell on it. This is the cell that we're going to subclass.


1. HOW TO SUBCLASS OBJECT?

If you don't know how to subclass an iOS object, tough luck! HAHAHAH! Ok jk, I will teach you here. It is quite an easy task. First, you decide what is the object that you want to subclass (ie customize)? Here, we are going to subclass UITableviewCell. So, Go to XCode and right click at your Project Navigator and select New File... then select "Cocoa Touch Class" under the iOS group. Then click Next. 

Then, in Subclass drop down menu, select UITableViewCell. Give a name of your new class - "SwipeTableViewCell". As you can see from the drop down menu, all iOS objects are listed here. You can subclass anything from here. Make sure you uncheck the "Also create XIB". We won't be needing a XIB since we already have the object in our storyboard.


Click Next and a dialog window will show requiring you to specify location to add the new class. Just click Create and then you will find 2 .m and .h files added to your project.

The next step is Critical - assigning your tableviewcell in the storyboard to your newly created class:


Observe the arrows. First, select your tableviewcell by clicking on the left panel (don't click on the storyboard object, because sometimes it will select tableview or the contentview instead). Then goto Identity Inspector on the right side panel, and enter your class name here. It should auto-complete by itself (if it doesn't autocomplete, it means you did not select a UITableViewCell object).

And... that is all there is to it! You are done subclassing a UITableViewCell! 

2. HOW TO CUSTOMIZE THE UITABLEVIEWCELL

If you already know how to subclass a tableviewcell, you may skip this section and go to section 3 below.

Now, lets customize this cell. As of now, all you did was subclass it but it is still the same old tableview. You need to add objects and codes to modify/customize the cell now. The basic uitableview cell provided by apple already can display items, but it is limited to a few texts - depending on the style of cell that we created.


By default, it is "Custom" (and you need to subclass tableviewcell). But if you don't want to subclass the cell, you can still use the default cell XCode provided - you can use Basic (where there is one default label in it), or Subtitle (where there are 2 labels in it). But these styles are fixed. You can't add images to the cell. Or other objects.

Now, lets make a decision - what to put in our table. For simplicity sake, lets create a fixed data system that resides in the app bundle (hence is read-only). I created a small database in database.plist file in the following format:




[
   {
       Brand = "Honda"
       Model = "Fit"
       Price = "$10,000"
       Specs = "......"
    }
...
]
[ ] denotes array. and { } denotes dictionary.

I added 5 entries with various car types. And then I search for these car images and change their name to match their Model (the "Model" field in the data above), and added them to the project's Images.xcasset. The purpose is so that we can load the image with command like:



[UIImage imageNamed:[NSString stringWithFormat:@"%@.jpeg",[carDictionary objectForKey:@"Model"]]];


The plan is this: Display photo of car in each cells, with brand and model and price. And then user can swipe the cell to left side to reveal the car specs.

First, lets add the necessary objects in our customcell and create all the necessary constraints (Yes! all objects need constraints even inside a tableview cell!). Then connect them to IBOutlets in our customcell header file.



Now to load our database into the tableviewcell, we need to tell the Viewcontroller that our table is using it to load data. So lets do that - select our tableview and connect the delegate and datasource to our viewcontroller. We also connect tableview to a local property carTable so we can access it easily.



Now, lets create datasource for this table. A datasource is typically a NSMutableArray. NSMutableArray is synonym with tableview because both have indexes. Indexes are important because that is how data is ordered and arranged.

Declare a NSMutableArray *carDataArr and lets code loading our local database into this array.



    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"database" ofType:@"plist"];
    carDataArr = [[NSMutableArray alloc] initWithContentsOfFile:filePath];


Next, we need to implement the tableView delegates so we can return this carDataArr as the table's datasource.




-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row % 2==0) {
        cell.contentView.backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:1.0 alpha:1];
    } else {
        cell.contentView.backgroundColor = [UIColor colorWithHue:0 saturation:0 brightness:0.9 alpha:1];
    
    }
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    return [carDataArr count];
    
}

-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    SwipeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"swipeCellID"];
    if (cell==nil) {
     
        cell = [[SwipeTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"swipeCellID"];
    }
    
    NSDictionary *eachCarData = [carDataArr objectAtIndex:indexPath.row];
    cell.carBrand.text = [eachCarData objectForKey:@"Brand"];
    cell.carModel.text = [eachCarData objectForKey:@"Model"];
    cell.carPrice.text = [eachCarData objectForKey:@"Price"];
    cell.carPic.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@.jpeg",[eachCarData objectForKey:@"Model"]]];
    
    return cell;
}

And take a look at what we've accomplished so far!



Now we have all the data nicely displayed in the table. Congratulations! :D But wait, we have one more data to display: The car specs! As the tutorial intended: we need to make the cell swipable to left side and display the car spec behind the cell.

3. SWIPEABLE CELL!

To visualize how to make a cell swipable, take a look at the following diagram. In a UITableview (grey outline), there are many cells (blue outline) which conforms to its prototype. Inside each cells, there is a contentView (orange rectangle). What we can do is add subviews (green rectangle) behind this contentview and move the contentview to any direction and thus revealing the subview below it.



To achieve as the diagram, we need to add some codes to our custom cell class - SwipeTableViewCell.m/.h. Inside the awakeFromNib, enter the following codes: (read the comments to understand what each portion does).



- (void)awakeFromNib {
    [super awakeFromNib];
    // Initialization code
    
    CGRect containerFrame = self.contentView.frame;
    
    // create the textview and add to the contentview of the cell
    if (self.carSpecs==nil)
        self.carSpecs = [[UITextView alloc] initWithFrame:CGRectMake(containerFrame.size.width/2.0, containerFrame.origin.y,
                                                                       containerFrame.size.width/2.0, containerFrame.size.height)];
    
    [self.carSpecs setBackgroundColor:[UIColor clearColor]];
    [self setBackgroundColor:[UIColor colorWithRed:0.50 green:0.50 blue:0.50 alpha:1.00]];
    
    [self addSubview:self.carSpecs];
    [self sendSubviewToBack:self.carSpecs];
    
    // add the shadow to contentview to make it nicer
    self.contentView.layer.shadowColor = [[UIColor blackColor] CGColor];
    self.contentView.layer.shadowRadius = 5.0;
    self.contentView.layer.shadowOpacity = 1.0;
    self.contentView.layer.masksToBounds = NO;
    
    // CGFloat variable to track our cell movements
    _oriX = self.contentView.center.x;
    _xOffset =  self.contentView.center.x;
    
    // gesture recognizer to detect "swipe" which is actually panning
    UIPanGestureRecognizer *panLeft = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panLeftHandler:)];
    panLeft.delegate = self;
    
    self.gestureRecognizers = @[panLeft];
    
}


And then, we need to implement the gesture handler (ie what happens when user swipe our cell). Remember to add <UIGestureRecognizerDelegate> at SwipeTableViewCell.h. Then implement these 2 gesture recognizer delegate methods: (Again, read the comments to see what the code does)




- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
    
    _oriX = _xOffset; // reset the starting location of cell
    
    return YES;
}

-(void)panLeftHandler:(UIPanGestureRecognizer*)panGesture {
    
    UIView *cell = [panGesture view];
    CGPoint translation = [panGesture translationInView:[cell superview]];
    
    // this is called when user lift finger from screen
    if ([panGesture state]==UIGestureRecognizerStateEnded) {
        if (_xOffset<self.frame.size.width/4.0) {
            // if offset is below treshold, move cell to "open" open
            [UIView animateWithDuration:0.35 animations:^(void) {
                
                self.contentView.center = CGPointMake(0, self.contentView.center.y);
                
            }completion:^(BOOL finished) {
                
                _xOffset = self.contentView.center.x;
                
            }];
            
        } else {
            // if offset is above treshold, move cell to "close" position
            [UIView animateWithDuration:0.35 animations:^(void) {
                
                self.contentView.center = CGPointMake(self.center.x, self.contentView.center.y);
                
            }completion:^(BOOL finished) {
                
                _xOffset = self.contentView.center.x;
            }];
        }
        
    // the following is called when user keeps on panning finger on the cell
    } else {
        
        // the 50 pixels buffer is to prevent accidental swipes during tableview scrolling
        
        if ((translation.x>50)) {
            // here we update the offset and move the cell's contentview following user's finger
            
            _xOffset = _oriX+translation.x-50;
            self.contentView.center = CGPointMake(_xOffset, self.contentView.center.y);
            
        }
        
        if ((translation.x<-50)) {
            // here we update the offset and move the cell's contentview following user's finger
            
            _xOffset = _oriX+translation.x+50;
            self.contentView.center = CGPointMake(_xOffset, self.contentView.center.y);
            
        }
    }
}


Finally, go back to our ViewController and now we can populate our "Specs" data into the carSpecs UITextView. Add the following line to the cellForRow delegate of our table.



cell.carSpecs.text = [eachCarData objectForKey:@"Specs"];


And that's all there is to it. You can replace the UITextView with ANY KIND OF OBJECT as you like. Just be sure to create them programatically properly. You can put multiple objects too, and objects like UISwitch, UIImageView, heck even a ContainerView.




3 comments:

  1. Nice tutorial, but it stops the tableview from scrolling. You need to implement the: shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer method and return true.

    ReplyDelete
    Replies
    1. That's weird used to work ok in iOS9. I think something changed in iOS10. Anyway thanks for your comment.

      Delete
  2. I definitely enjoying every little bit of it. It is a great website and nice share. I want to thank you. Good job! You guys do a great blog, and have some great contents. Keep up the good work.
    bwakochon

    ReplyDelete