I am using a tab bar (UITabBarController) on my app and I wish to customize the appearance of the table that appears when you click the more button.
I have worked out how to change the appearance of the Navigation bar that is on the more screen by setting
self.moreNavigationController.navigationBar.barStyle
in a subclass of UITabBarController and I have managed to change the background colour of the table by modifying
self.moreNavigationController.topViewController.view.backgroundColor
, but I cannot work out how to change the font colour in the cells that appear on the table.
I was hoping I could use
self.moreNavigationController.topViewController.view.visibleCells
but this always seems to be empty. I've tried doing this in viewDidLoad, viewWillAppear and viewDidAppear with no success. The object self.moreNavigationController.topViewController is of type UIMoreListController, which seems to be undocumented and I can't see anything obvious in the interface that will help me.
Any ideas?
Following on from Stephan's suggestion to replace the dataSource of the moreNavigationController, here is a quick over view of the code I implemented.
I created a new class called MoreTableViewDataSource which implements the UITableViewDataSource protocol. The controller which the more page actually uses to build the table is called the UIMoreListControllerModern, and this implements just the required parts of the UITableViewDataSource protocol. My implementation looks like this.
-(MoreTableViewDataSource *) initWithDataSource:(id<UITableViewDataSource>) dataSource
{
self = [super init];
if (self)
{
self.originalDataSource = dataSource;
}
return self;
}
- (void)dealloc
{
self.originalDataSource = nil;
[super dealloc];
}
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section
{
return [originalDataSource tableView:table numberOfRowsInSection:section];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [originalDataSource tableView:tableView cellForRowAtIndexPath:indexPath];
cell.textColor = [UIColor whiteColor];
return cell;
}
and then in my CustomTabBarController class I override viewDidLoad as follows:
- (void)viewDidLoad {
[super viewDidLoad];
UINavigationController *moreController = self.moreNavigationController;
moreController.navigationBar.barStyle = UIBarStyleBlackOpaque;
if ([moreController.topViewController.view isKindOfClass:[UITableView class]])
{
UITableView *view = (UITableView *)moreController.topViewController.view;
view.backgroundColor = [UIColor blackColor];
moreTableViewDataSource = [[MoreTableViewDataSource alloc] initWithDataSource:view.dataSource];
view.dataSource = moreTableViewDataSource;
}
}
As requested here are the header files
#interface MoreTableViewDataSource : NSObject <UITableViewDataSource>
{
id<UITableViewDataSource> originalDataSource;
}
#property (retain) id<UITableViewDataSource> originalDataSource;
-(MoreTableViewDataSource *) initWithDataSource:(id<UITableViewDataSource>) dataSource;
#end
and
#import "MoreTableViewDataSource.h"
#interface CustomTabBarController : UITabBarController
{
MoreTableViewDataSource *moreTableViewDataSource;
}
visibleCells is populated only after the moreNavigationController is displayed.
And the cells are created at runtime, so even if you change the content of the cells, they are replaced when they are displayed.
One thing to try would be to replace the datasource of the moreNavigationController tableView, call the cellForRowAtIndexPath of the original datasource and change its content before returning it.
Using the code below, after having displayed once the moreNavigationController to initialize it, you'll see that when you return to the moreNavigationController, the cells are red, but return immediately to white background.
UITableView *view = (UITableView *)self.tabBarController.moreNavigationController.topViewController.view;
if ([[view subviews] count]) {
for (UITableViewCell *cell in [view visibleCells]) {
cell.backgroundColor = [UIColor redColor];
}
}
Thanks to Unknown.
Following his solution, I will put his code in Swift.
Only what you should do more is create MoreTableViewCell class and just it. You don't have to use Storyboard. If you want to modify tableView you can do it in customizeMoreTableView method.
class TabBarMenuController: UITabBarController, UITableViewDelegate, UITableViewDataSource{
var tabBarItems: [UIViewController] = []
var areMessagesVisible: Bool = false
var titleForTabBars: [String] = ["resources", "events", "training", "my profile", "news", "contacts"]
var iconNames: [String] = ["film", "calendar", "classroom", "profile", "news", "Phone"]
var controllersStoryboardId: [String] = ["resourcesNavController", "eventsNavController", "enablementNavController", "profileNavController", "newsNavController", "contactsNavController"]
// to manage moreTableView
var moreTableView: UITableView = UITableView()
var currentTableViewDelegate: UITableViewDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.customizeMoreTableView()
//to REMOVE
areMessagesVisible = true
if !areMessagesVisible{
self.titleForTabBars.removeAtIndex(4)
self.controllersStoryboardId.removeAtIndex(4)
self.iconNames.removeAtIndex(4)
}
for i in 0 ..< controllersStoryboardId.count{
tabBarItems.append(UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier(controllersStoryboardId[i]) as? UINavigationController ?? UINavigationController())
}
self.moreNavigationController.navigationBar.tintColor = UIColor.blackColor()
}
override func viewWillAppear(animated: Bool) {
for i in 0 ..< tabBarItems.count{
tabBarItems[i].tabBarItem = UITabBarItem(title: titleForTabBars[i], image: UIImage(named: iconNames[i]), selectedImage: UIImage(named: iconNames[i]))
}
self.viewControllers = tabBarItems
}
func customizeMoreTableView(){
moreTableView = self.moreNavigationController.topViewController!.view as? UITableView ?? UITableView()
currentTableViewDelegate = moreTableView.delegate;
moreTableView.delegate = self
moreTableView.dataSource = self;
moreTableView.registerClass(MoreTableViewCell.self, forCellReuseIdentifier: "MoreTableViewCell")
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let moreCell = tableView.dequeueReusableCellWithIdentifier("MoreTableViewCell", forIndexPath: indexPath) as? MoreTableViewCell ?? MoreTableViewCell()
moreCell.textLabel?.text = titleForTabBars[indexPath.row + 4]
moreCell.imageView?.image = UIImage(named: iconNames[indexPath.row + 4])
/*let testLabel: UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
testLabel.backgroundColor = UIColor.yellowColor()
moreCell.addSubview(testLabel)
*/
return moreCell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return titleForTabBars.count - 4
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
currentTableViewDelegate?.tableView!(tableView, didSelectRowAtIndexPath: indexPath)
}
}
I followed Ian's implementation to customize the More menu, but I was having a problem retaining the customizations after a memory warning. didReceiveMemoryWarning seems to destroy the UITableView, and when it is regenerated it gets its old dataSource back. Here's my solution:
I replace viewDidLoad on the CustomTabBarController with this:
- (void)viewDidLoad {
[super viewDidLoad];
UINavigationController* moreController = self.moreNavigationController;
if ([moreController.topViewController.view isKindOfClass:[UITableView class]]) {
moreController.delegate = self;
self.moreControllerClass = [moreController.topViewController class];
UITableView* view = (UITableView*) moreController.topViewController.view;
self.newDataSource = [[[MoreDataSource alloc] initWithDataSource:view.dataSource] autorelease];
}
}
As you can see, I added a few properties for storing things I needed. Those have to be added to the header and synthesized. I also made CustomTabBarController a UINavigationControllerDelegate in the header. Here's the delegate function I added:
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if ([viewController isKindOfClass:self.moreControllerClass]) {
UIView* view = self.moreNavigationController.topViewController.view;
if ([view isKindOfClass:[UITableView class]]) {
UITableView* tview = (UITableView*) view;
tview.dataSource = self.newDataSource;
tview.rowHeight = 81.0;
}
}
}
This way I make sure my custom data source is always used, because I set it that way just prior to showing the UIMoreListController, every time it's shown.
#interface TabBarViewController () <UITableViewDelegate,UITableViewDataSource>
#property (nonatomic,strong) UITableView* tabBarTableView;
#property (nonatomic,weak) id <UITableViewDelegate> currentTableViewDelegate;
#end
#implementation TabBarViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self costumizeMoreTableView];
}
-(void)costumizeMoreTableView{
_tabBarTableView = (UITableView *)self.moreNavigationController.topViewController.view;
_currentTableViewDelegate = _tabBarTableView.delegate;
_tabBarTableView.delegate = self;
_tabBarTableView.dataSource = self;
[_tabBarTableView registerNib:[UINib nibWithNibName:#"MoreTabBarTableViewCell" bundle:nil] forCellReuseIdentifier:#"MoreTabBarTableViewCell"];
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 120;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 2;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
MoreTabBarTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MoreTabBarTableViewCell" forIndexPath:indexPath];
[cell setMoreTableValues];
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath{
[_currentTableViewDelegate tableView:tableView didSelectRowAtIndexPath:indexPath];
}
#end
This works for me in iOS 13, Swift 5.1:
extension MyTabBarController: UITabBarControllerDelegate {
// handle a select of the More tab
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
// style all the tab bar windows and the More tab bar tableview
if viewController == moreNavigationController,
let moreTableView = moreNavigationController.topViewController?.view as? UITableView {
view.tintColor = .systemOrange
moreNavigationController.navigationBar.tintColor = .systemOrange
moreTableView.tintColor = .systemOrange
moreTableView.backgroundColor = UIColor(named: "Your Color")
moreTableView.visibleCells.forEach {
$0.backgroundColor = UIColor(named: "Your Color")
}
}
}
}
I think I found an easier solution... Just drop this extension on your landing page of Tab Bar Controller. This function is setting the menu tableView after the menu is created and the cells are all in tableView.visibleCells
extension UINavigationController: UINavigationControllerDelegate {
open override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if (viewController.classForCoder.description().description == "UIMoreListController") {
if let moreTableView = tabBarController!.moreNavigationController.topViewController?.view as? UITableView {
for cell in moreTableView.visibleCells {
cell.backgroundColor = .red
}
}
}
}
}
Related
In table list using swift here i want to load the one class table-list in another class view controller and this concept is working in objective-c but come down to swift delegate methods are not calling my objective -c#swift codes below please help me some one else
BackGroundView.h:-
#import <UIKit/UIKit.h>
#interface BackGroundView
UIViewController<UITableViewDataSource,UITableViewDelegate>
{
UITableView *tableView;
}
#property(nonatomic, retain) UITableView *tableView;
-(void)tableList:(UIView *) view1;
#end
BackGroundView.m:-
#import "BackGroundView.h"
#interface BackGroundView ()
{
NSArray * Mainarray;
}
#end
#implementation BackGroundView
#synthesize tableView;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
-(void)tableList:(UIView *) view1
{
Mainarray = [[NSArray alloc]initWithObjects:#"india",#"australia",#"usa", nil];
tableView=[[UITableView alloc]init];
tableView.frame = CGRectMake(0,0,320,400);
tableView.dataSource=self;
tableView.delegate=self;
tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"Cell"];
[tableView reloadData];
tableView.separatorColor = [UIColor blackColor];
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
[view1 addSubview:tableView];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return Mainarray.count;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath] ;
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.textLabel.text= [Mainarray objectAtIndex:indexPath.row];
return cell;
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([cell respondsToSelector:#selector(setSeparatorInset:)]) {
[cell setSeparatorInset:UIEdgeInsetsZero];
}
if ([cell respondsToSelector:#selector(setPreservesSuperviewLayoutMargins:)]) {
[cell setPreservesSuperviewLayoutMargins:NO];
}
if ([cell respondsToSelector:#selector(setLayoutMargins:)]) {
[cell setLayoutMargins:UIEdgeInsetsZero];
}
}
#end
MaindView.h
#import <UIKit/UIKit.h>
#interface ViewController1 : UIViewController
#end
MaindView.m
#import "ViewController1.h"
#import "BackGroundView.h"
#interface MaindView ()
{
BackGroundView * v1;
}
#end
#implementation MaindView
- (void)viewDidLoad {
[super viewDidLoad];
v1 = [[BackGroundView alloc]init];
// Do any additional setup after loading the view.
[v1 tableList:self.view];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
Now it's working fine in objective-c and table list is loading fine
come down to swift ios:-
Now i am calling TableViewAdding from Mainview class to BackgroundView it's calling but delegate methods are not calling in background class i.e it is showing empty view controller in swift table list is not loading properly please help me and this is my swift code
BackGroundView.swift
import UIKit
class BackGroundView: UIViewController,UITableViewDelegate, UITableViewDataSource {
var tableView: UITableView = UITableView()
var items = NSArray ()
override func viewDidLoad() {
super.viewDidLoad()
}
func TableViewAdding(myview:UIView)
{
items = ["india","australia","usa"];
println(items)
tableView.frame = CGRectMake(0, 50, 320, 200);
tableView.delegate = self
tableView.dataSource = self
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
myview.addSubview(tableView)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
println("numberOfRowsInSection")
return self.items.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
println("cellForRowAtIndexPath")
var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
cell.textLabel?.text = self.items.objectAtIndex(indexPath.row) as NSString
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
println("in first")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
Mainview.swift
import UIKit
class Mainview: UIViewController{
#IBOutlet var myview1: UIView!
override func viewDidLoad() {
super.viewDidLoad()
var total = BackGroundView.alloc()
total.TableViewAdding(self.view)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
[UPDATED]
You're not initializing the BackGroundView in mainview.swift. You're just allocating the memory for the object. Note that you both alloc and init a class in Objective-C. In Swift, you have to do the same thing with Class.alloc().initialize(). However, swift has replaced that verbose line of code with a simple call: Class()
Objective-C:
Class *myInstance = [[Class alloc] init];
Swift:
var myInstance = Class()
Some other things:
It's always a good idea to call tableView.reloadData() after you
change the information in it (like in your TableViewAdding) method.
It's never a good idea to hard code numbers (like the table view size).
TableViewAdding looks like a class method. tableViewAdding would follow the camelCase convention more accurately
Documentation for why using var myInstance = Class.alloc() is not the same as using var myInstance = Class() can be found in Apple's NSObject documentation under Creating, Copying, and Deallocating Objects
https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Reference/Foundation/Classes/NSObject_Class/index.html#//apple_ref/occ/clm/NSObject/alloc
Using KVC to get delegate property.
Using NSInvocation to call delegate method
Declare total as Class variable in Mainview.swift. Don't use .alloc()
import UIKit
class Mainview: UIViewController{
var total = BackGroundView()
#IBOutlet var myview1: UIView!
override func viewDidLoad() {
super.viewDidLoad()
total.TableViewAdding(self.view)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
When I involve [tableView setEditing:YES animated:YES], delete control shows on every cell on the left,what I want to do is to get the event when I tap delete control,and directly delete cell but not to show delete button on the right.
I know apple's standard way to do this is to show delete button on the right, and when I tap it ,datasource's
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
gets involved, the reason I don't want to do like this is my cell is customised by scrollview which scroll horizontally so scroll to show delete button would made it a mess, so I wouldn't implement
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
in my datasource.
Have any idea?
one way u do is, customising the cell and put your own way of deleting the cell for example,
create a new custom cell by subclassing the UITableviewCell name it as something like CustomCellTableViewCell
in CustomCellTableViewCell.h define a delegate method for example,
#import <UIKit/UIKit.h>
#class CustomCellTableViewCell;
#protocol CellDelegate <NSObject>
- (void)deleteCell:(CustomCellTableViewCell *)cell;
#end
#interface CustomCellTableViewCell : UITableViewCell
+ (CustomCellTableViewCell *)createCell;
#property (weak, nonatomic) IBOutlet UIButton *deleteButton;
#property (weak, nonatomic) IBOutlet UILabel *descriptionLabel;
#property (weak,nonatomic) id<CellDelegate> cellDelegate;
- (IBAction)deleteAction:(id)sender;
- (void)showDeleteButton;
- (void)hideDeleteButton;
#end
and in CustomCellTableViewCell.xib add a button and set label connect to deleteButton and descriptionLabel
in CustomCellTableViewCell.m file
#import "CustomCellTableViewCell.h"
#implementation CustomCellTableViewCell
- (void)awakeFromNib {
// Initialization code
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if(self)
{
self = [CustomCellTableViewCell createCell];
}
return self;
}
+ (CustomCellTableViewCell *)createCell
{
NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:#"CustomCellTableViewCell" owner:nil options:nil];
if ([arrayOfViews count] < 1) {
return nil;
}
for (id item in arrayOfViews) {
if([item isKindOfClass:[UITableViewCell class]])
return item;
}
return nil;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (IBAction)deleteAction:(id)sender {
if([self.cellDelegate respondsToSelector:#selector(deleteCell:)])
{
[self.cellDelegate deleteCell:self];
}
}
- (void)showDeleteButton
{
CGRect destRect = self.descriptionLabel.frame;
destRect.origin.x += 80;
[UIView animateWithDuration:0.3 animations:^{
self.descriptionLabel.frame = destRect;
}];
}
- (void)hideDeleteButton
{
CGRect destRect = self.descriptionLabel.frame;
destRect.origin.x = 0;
[UIView animateWithDuration:0.3 animations:^{
self.descriptionLabel.frame = destRect;
}] ;
}
#end
and in controller .m file
- (void)viewDidLoad {
[super viewDidLoad];
stringsArray = [[NSMutableArray alloc]initWithObjects:#"apple",#"dell",#"windows",#"nokia",#"sony",#"hp",#"lenovo", nil];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [stringsArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomCellTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"SuggestionCell"];
if(cell == nil)
{
cell = [[CustomCellTableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"SuggestionCell"];
}
if(customEditTableView)
[cell showDeleteButton];
else
[cell hideDeleteButton];
cell.cellDelegate = self;
cell.descriptionLabel.text = [stringsArray objectAtIndex:indexPath.row];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 50.0f;
}
- (IBAction)deleteCellsAction:(id)sender
{
if(customEditTableView)
customEditTableView = NO;
else
customEditTableView = YES;
[self.aTableView reloadData];
}
- (void)deleteCell:(CustomCellTableViewCell *)cell
{
NSIndexPath *indexPath = [self.aTableView indexPathForCell:cell];
[stringsArray removeObjectAtIndex:indexPath.row];
[self.aTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
try out in new project u will get it
You can achive this thing using UIViewController
Add tableview and tableviewcell in UIViewController
I have achive this same thing using swift. It will give you idea how to do in Objective-C
Below is Code:
var data:[String] = ["One","Three","Four","Five","Six"]
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = self.editButtonItem()
// Do any additional setup after loading the view, typically from a nib.
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel.text = self.data[indexPath.row]
if editing
{
cell.imageView.image = UIImage(named: "Button-Delete-icon.png")
}
else
{
cell.imageView.image = UIImage(named: "")
}
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if editing
{
self.data.removeAtIndex(indexPath.row)
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
}
}
override func setEditing(editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
self.tableView.reloadData()
}
Hope It will Help
I have implemented an empty ViewController i.e SearchViewController with a SearchBar in it. Ans as i am searching from a web service, i want the search results to be displayed only when the user presses the search button. That has been implemented. Bt the problem is, the results appear in a weird manner as shown below:
Dont know what are they getting hidden. How do i bring them to front??
Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
self.api.delegate = self
activateSearch()
searchTableView.delegate = self
searchTableView.dataSource = self
searchBar.delegate = self
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(kCellIdentifier) as UITableViewCell
var rowData: NSDictionary = self.tableData[indexPath.row] as NSDictionary
cell.textLabel?.text = rowData["title"] as? String
return cell
}
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
func didReceiveAPIResults(results: NSDictionary) {
var resultsArr: NSArray = results["posts"] as NSArray
dispatch_async(dispatch_get_main_queue(), {
self.tableData = resultsArr
self.searchTableView!.reloadData()
})
}
func activateSearch() {
// self.navigationController?.navigationBarHidden = true
searchTableView.scrollRectToVisible(CGRectMake(0, 0, 1, 1), animated: false)
searchBar.becomeFirstResponder()
}
override func viewWillAppear(animated: Bool) {
var newBounds:CGRect = self.searchTableView.bounds;
newBounds.origin.y = newBounds.origin.y + self.searchBar.bounds.size.height;
self.searchTableView.bounds = newBounds;
self.navigationController?.navigationBarHidden = true
}
func searchBarSearchButtonClicked( searchBar: UISearchBar!)
{
api.searchItunesFor(searchBar.text)
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
self.viewWillAppear(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
I might hv done something silly. bt m not able to figure out what is it.. pls help
It looks like your search bar has been placed over your table view. Try to scale your table view down in the storyboard so the top of the table view is below the search bar element. The results should display correctly
You are changing the searchTableView frame inside the viewWillAppear method which will not get call when you are in the same view controller.
Try changing the searchTableView frame inside the searchBarSearchButtonClicked method.
Hope this will solve your problem. :)
Edit:
Also try adding the search bar to the searchTableView header.
Below is the objective-c code for adding the search bar to the tableView header.
self.searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 44.0f)] ;
self.searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
self.searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.searchBar.keyboardType = UIKeyboardTypeAlphabet;
self.searchBar.delegate = self;
self.tableView.tableHeaderView = self.searchBar;
Just posting the answer incase someone ends in a situation like mine.
I didn't connect the tableView to the SearchDisplayController.
The tableView should be the dataSource and Delegate for the SearchDisplayController.
We just need to control+Drag to connect.
PS. in XCODE 6.1 the SearchDisplayController is displayed as a button like thing in the header of ViewController.
#import <UIKit/UIKit.h>
#interface TableViewController : UITableViewController
#end
#import "TableViewController.h"
#interface TableViewController () {
NSInteger _rows;
}
#property (weak, nonatomic) IBOutlet UISearchBar *searchBar;
#end
#implementation TableViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
_rows = 3;
// [self hideSearchBar];
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self.tableView setContentOffset:CGPointMake(0,44) animated:NO];
// self.tableView.tableHeaderView = self.searchBar;
}
-(void)viewDidDisappear:(BOOL)animated{
//self.tableView.tableHeaderView = nil;
//[self.tableView.tableHeaderView removeFromSuperview];
[self.tableView setContentInset:UIEdgeInsetsMake(-0.3, 0, 0, 0)];
[super viewDidAppear:animated];
}
- (void)hideSearchBar {
// hide search bar
[self.tableView setContentOffset:CGPointMake(0,44) animated:NO];
}
- (IBAction)toggleCount:(UIBarButtonItem *)sender {
if (_rows == 20) {
_rows = 3;
} else {
_rows = 20;
}
[self.tableView reloadData];
}
- (IBAction)hideBar:(UIBarButtonItem *)sender {
[self hideSearchBar];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = #"cell";
return cell;
}
/*
// Override to support conditional editing of the table view.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
*/
/*
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
*/
/*
// Override to support rearranging the table view.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
}
*/
/*
// Override to support conditional rearranging of the table view.
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the item to be re-orderable.
return YES;
}
*/
/*
#pragma mark - Navigation
// In a story board-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
#end
I have a view controller with a table view and a separate nib for the table cell template. The cell template has some buttons. I want to access the button click along with the index of the cell clicked inside the view controller where I have defined the Table view.
So I have ViewController.h and ViewController.m where I have the UITableView and TableTemplate.h, TableTemplate.m and TableTemplate.xib where I have the nib defined. I want the button click event with cell index in ViewController.m.
Any help on how can I do that?
1) In your cellForRowAtIndexPath: method, assign button tag as index:
cell.yourbutton.tag = indexPath.row;
2) Add target and action for your button as below:
[cell.yourbutton addTarget:self action:#selector(yourButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
3) Code actions based on index as below in ViewControler:
-(void)yourButtonClicked:(UIButton*)sender
{
if (sender.tag == 0)
{
// Your code here
}
}
Updates for multiple Section:
You can check this link to detect button click in table view for multiple row and section.
Delegates are the way to go.
As seen with other answers using views might get outdated. Who knows tomorrow there might be another wrapper and may need to use cell superview]superview]superview]superview]. And if you use tags you would end up with n number of if else conditions to identify the cell. To avoid all of that set up delegates. (By doing so you will be creating a re usable cell class. You can use the same cell class as a base class and all you have to do is implement the delegate methods.)
First we need a interface (protocol) which will be used by cell to communicate(delegate) button clicks. (You can create a separate .h file for protocol and include in both table view controller and custom cell classes OR just add it in custom cell class which will anyway get included in table view controller)
#protocol CellDelegate <NSObject>
- (void)didClickOnCellAtIndex:(NSInteger)cellIndex withData:(id)data;
#end
Include this protocol in custom cell and table view controller. And make sure table view controller confirms to this protocol.
In custom cell create two properties :
#property (weak, nonatomic) id<CellDelegate>delegate;
#property (assign, nonatomic) NSInteger cellIndex;
In UIButton IBAction delegate click : (Same can be done for any action in custom cell class which needs to be delegated back to view controller)
- (IBAction)buttonClicked:(UIButton *)sender {
if (self.delegate && [self.delegate respondsToSelector:#selector(didClickOnCellAtIndex:withData:)]) {
[self.delegate didClickOnCellAtIndex:_cellIndex withData:#"any other cell data/property"];
}
}
In table view controller cellForRowAtIndexPath after dequeing the cell, set the above properties.
cell.delegate = self;
cell.cellIndex = indexPath.row; // Set indexpath if its a grouped table.
And implement the delegate in table view controller:
- (void)didClickOnCellAtIndex:(NSInteger)cellIndex withData:(id)data
{
// Do additional actions as required.
NSLog(#"Cell at Index: %d clicked.\n Data received : %#", cellIndex, data);
}
This would be the ideal approach to get custom cell button actions in table view controller.
Instead of playing with tags, I took different approach. Made delegate for my subclass of UITableViewCell(OptionButtonsCell) and added an indexPath var. From my button in storyboard I connected #IBAction to the OptionButtonsCell and there I send delegate method with the right indexPath to anyone interested. In cell for index path I set current indexPath and it works :)
Let the code speak for itself:
Swift 3 Xcode 8
OptionButtonsTableViewCell.swift
import UIKit
protocol OptionButtonsDelegate{
func closeFriendsTapped(at index:IndexPath)
}
class OptionButtonsTableViewCell: UITableViewCell {
var delegate:OptionButtonsDelegate!
#IBOutlet weak var closeFriendsBtn: UIButton!
var indexPath:IndexPath!
#IBAction func closeFriendsAction(_ sender: UIButton) {
self.delegate?.closeFriendsTapped(at: indexPath)
}
}
MyTableViewController.swift
class MyTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, OptionButtonsDelegate {...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "optionCell") as! OptionButtonsTableViewCell
cell.delegate = self
cell.indexPath = indexPath
return cell
}
func closeFriendsTapped(at index: IndexPath) {
print("button tapped at index:\(index)")
}
This should help :-
UITableViewCell* cell = (UITableViewCell*)[sender superview];
NSIndexPath* indexPath = [myTableView indexPathForCell:cell];
Here sender is the UIButton instance that is sending the event.
myTableView is the UITableView instance you're dealing with.
Just get the cell reference right and all the work is done.
You may need to remove the buttons from cell's contentView &
add them directly to UITableViewCell instance as it's subview.
Or
You can formulate a tag naming scheme for different UIButtons in cell.contentView.
Using this tag, later you can know the row & section information as needed.
Following code might Help you.
I have taken UITableView with custom prototype cell class named UITableViewCell inside UIViewController.
So i have ViewController.h, ViewController.m and TableViewCell.h,TableViewCell.m
Here is the code for that:
ViewController.h
#interface ViewController : UIViewController<UITableViewDataSource,UITableViewDelegate>
#property (strong, nonatomic) IBOutlet UITableView *tblView;
#end
ViewController.m
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return (YourNumberOfRows);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"cell";
__weak TableViewCell *cell = (TableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
if (indexPath.row==0) {
[cell setDidTapButtonBlock:^(id sender)
{
// Your code here
}];
}
return cell;
}
Custom cell class :
TableViewCell.h
#interface TableViewCell : UITableViewCell
#property (copy, nonatomic) void (^didTapButtonBlock)(id sender);
#property (strong, nonatomic) IBOutlet UILabel *lblTitle;
#property (strong, nonatomic) IBOutlet UIButton *btnAction;
- (void)setDidTapButtonBlock:(void (^)(id sender))didTapButtonBlock;
#end
and
UITableViewCell.m
#implementation TableViewCell
- (void)awakeFromNib {
// Initialization code
[self.btnAction addTarget:self action:#selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (void)didTapButton:(id)sender {
if (self.didTapButtonBlock)
{
self.didTapButtonBlock(sender);
}
}
Note: Here I have taken all UIControls using Storyboard.
Hope that can help you...!!!
Use Swift closures :
class TheCell: UITableViewCell {
var tapCallback: (() -> Void)?
#IBAction func didTap(_ sender: Any) {
tapCallback?()
}
}
extension TheController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TheCell.identifier, for: indexPath) as! TheCell {
cell.tapCallback = {
//do stuff
}
return cell
}
}
The reason i like below technique because it also help me to identify the section of table.
Add Button in cell cellForRowAtIndexPath:
UIButton *selectTaskBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[selectTaskBtn setFrame:CGRectMake(15, 5, 30, 30.0)];
[selectTaskBtn setTag:indexPath.section]; //Not required but may find useful if you need only section or row (indexpath.row) as suggested by MR.Tarun
[selectTaskBtn addTarget:self action:#selector(addTask:) forControlEvents:UIControlEventTouchDown];
[cell addsubview: selectTaskBtn];
Event addTask:
-(void)addTask:(UIButton*)btn
{
CGPoint buttonPosition = [btn convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
if (indexPath != nil)
{
int currentIndex = indexPath.row;
int tableSection = indexPath.section;
}
}
Hopes this help.
Tarun's code doesnt work on iOS7, since the UITableViewCell structure changed and now he would get "UITableViewCellScrollView" instead.
This post Getting UITableViewCell with superview in iOS 7 has a good solution creating a loop to find the correct parent view, regardless of any future changes in the structure. It boils down to creating a loop:
UIView *superView = [sender superview];
UIView *foundSuperView = nil;
while (nil != superView && nil == foundSuperView) {
if ([superView isKindOfClass:[UITableViewCell class]]) {
foundSuperView = superView;
} else {
superView = superView.superview;
}
}
The link has code for a more reusable solution, but this should work.
Its Work For me.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UIButton *Btn_Play = (UIButton *)[cell viewWithTag:101];
[Btn_Play addTarget:self action:#selector(ButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
}
-(void)ButtonClicked:(UIButton*)sender {
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.Tbl_Name];
NSIndexPath *indexPath = [self.Tbl_Name indexPathForRowAtPoint:buttonPosition];
}
Swift 2.2
You need to add target for that button.
myButton.addTarget(self, action: #selector(ClassName.FunctionName(_:), forControlEvents: .TouchUpInside)
FunctionName: connected // for example
And of course you need to set tag of that button since you are using it.
myButton.tag = indexPath.row
You can achieve this by subclassing UITableViewCell. Use it in interface builder, drop a button on that cell, connect it via outlet and there you go.
To get the tag in the connected function:
func connected(sender: UIButton) {
let buttonTag = sender.tag
// Do any additional setup
}
Swift 3 with a Closure
A nice solution is using a closure in a custom UITableViewCell to callback to the viewController for an action.
In cell:
final class YourCustomCell: UITableViewCell {
var callbackClosure: (() -> Void)?
// Configure the cell here
func configure(object: Object, callbackClosure: (() -> Void)?) {
self.callbackClosure = callbackClosure
}
// MARK: - IBAction
extension YourCustomCell {
#IBAction fileprivate func actionPressed(_ sender: Any) {
guard let closure = callbackClosure else { return }
closure()
}
}
In View Controller: Tableview Delegate
extension YourViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell: YourCustomCell = cell as? YourCustomCell else { return }
cell.configure(object: object, callbackClosure: { [weak self] in
self?.buttonAction()
})
}
}
fileprivate extension YourViewController {
func buttonAction() {
// do your actions here
}
}
I find it simplest to subclass the button inside your cell (Swift 3):
class MyCellInfoButton: UIButton {
var indexPath: IndexPath?
}
In your cell class:
class MyCell: UICollectionViewCell {
#IBOutlet weak var infoButton: MyCellInfoButton!
...
}
In the table view's or collection view's data source, when dequeueing the cell, give the button its index path:
cell.infoButton.indexPath = indexPath
So you can just put these code into your table view controller:
#IBAction func handleTapOnCellInfoButton(_ sender: MyCellInfoButton) {
print(sender.indexPath!) // Do whatever you want with the index path!
}
And don't forget to set the button's class in your Interface Builder and link it to the handleTapOnCellInfoButton function!
edited:
Using dependency injection. To set up calling a closure:
class MyCell: UICollectionViewCell {
var someFunction: (() -> Void)?
...
#IBAction func didTapInfoButton() {
someFunction?()
}
}
and inject the closure in the willDisplay method of the collection view's delegate:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
(cell as? MyCell)?.someFunction = {
print(indexPath) // Do something with the indexPath.
}
}
If you want to pass parameter value from cell to UIViewController using closure then
//Your Cell Class
class TheCell: UITableViewCell {
var callBackBlockWithParam: ((String) -> ()) = {_ in }
//Your Action on button
#IBAction func didTap(_ sender: Any) {
callBackBlockWithParam("Your Required Parameter like you can send button as sender or anything just change parameter type. Here I am passing string")
}
}
//Your Controller
extension TheController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TheCell.identifier, for: indexPath) as! TheCell {
cell.callBackBlockWithParam = { (passedParamter) in
//you will get string value from cell class
print(passedParamter)
}
return cell
}
}
// Add action in cell for row at index path -tableView
cell.buttonName.addTarget(self, action: #selector(ViewController.btnAction(_:)), for: .touchUpInside)
// Button Action
#objc func btnAction(_ sender: AnyObject) {
var position: CGPoint = sender.convert(.zero, to: self.tableView)
let indexPath = self.tableView.indexPathForRow(at: position)
let cell: UITableViewCell = tableView.cellForRow(at: indexPath!)! as
UITableViewCell
}
for swift 4:
inside the cellForItemAt ,
cell.chekbx.addTarget(self, action: #selector(methodname), for: .touchUpInside)
then outside of cellForItemAt
#objc func methodname()
{
//your function code
}
#Mani answer is good, however tags of views inside cell's contentView often are used for other purposes. You can use cell's tag instead (or cell's contentView tag):
1) In your cellForRowAtIndexPath: method, assign cell's tag as index:
cell.tag = indexPath.row; // or cell.contentView.tag...
2) Add target and action for your button as below:
[cell.yourbutton addTarget:self action:#selector(yourButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
3) Create method that returns row of the sender (thanks #Stenio Ferreira):
- (NSInteger)rowOfSender:(id)sender
{
UIView *superView = sender.superview;
while (superView) {
if ([superView isKindOfClass:[UITableViewCell class]])
break;
else
superView = superView.superview;
}
return superView.tag;
}
4) Code actions based on index:
-(void)yourButtonClicked:(UIButton*)sender
{
NSInteger index = [self rowOfSender:sender];
// Your code here
}
CustomTableCell.h is a UITableViewCell:
#property (weak, nonatomic) IBOutlet UIButton *action1Button;
#property (weak, nonatomic) IBOutlet UIButton *action2Button;
MyVC.m after imports:
#interface MYTapGestureRecognizer : UITapGestureRecognizer
#property (nonatomic) NSInteger dataint;
#end
Inside "cellForRowAtIndexPath" in MyVC.m:
//CustomTableCell
CustomTableCell *cell = (CustomTableCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
//Set title buttons
[cell.action1Button setTitle:[NSString stringWithString:NSLocalizedString(#"action1", nil)] forState:UIControlStateNormal];
[cell.action2Button setTitle:[NSString stringWithString:NSLocalizedString(#"action2", nil)] forState:UIControlStateNormal];
//Set visibility buttons
[cell.action1Button setHidden:FALSE];
[cell.action2Button setHidden:FALSE];
//Do 1 action
[cell.action1Button addTarget:self action:#selector(do1Action :) forControlEvents:UIControlEventTouchUpInside];
//Do 2 action
MYTapGestureRecognizer *action2Tap = [[MYTapGestureRecognizer alloc] initWithTarget:self action:#selector(do2Action :)];
cancelTap.numberOfTapsRequired = 1;
cancelTap.dataint = indexPath.row;
[cell.action2Button setUserInteractionEnabled:YES];
[cell.action2Button addGestureRecognizer:action2Tap];
MyVC.m:
-(void)do1Action :(id)sender{
//do some action that is not necessary fr data
}
-(void)do2Action :(UITapGestureRecognizer *)tapRecognizer{
MYTapGestureRecognizer *tap = (MYTapGestureRecognizer *)tapRecognizer;
numberTag = tap.dataint;
FriendRequest *fr = [_list objectAtIndex:numberTag];
//connect with a WS o do some action with fr data
//actualize list in tableView
[self.myTableView reloadData];
}
cell.show.tag=indexPath.row;
[cell.show addTarget:self action:#selector(showdata:) forControlEvents:UIControlEventTouchUpInside];
-(IBAction)showdata:(id)sender
{
UIButton *button = (UIButton *)sender;
UIStoryboard *storyBoard;
storyBoard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
SecondViewController *detailView = [storyBoard instantiateViewControllerWithIdentifier:#"SecondViewController"];
detailView.string=[NSString stringWithFormat:#"%#",[_array objectAtIndex:button.tag]];
[self presentViewController:detailView animated:YES completion:nil];
}
Could somebody tell me the way to perform UITableView expandable/collapsible animations in sections of UITableView as below?
or
You have to make your own custom header row and put that as the first row of each section. Subclassing the UITableView or the headers that are already there will be a pain. Based on the way they work now, I am not sure you can easily get actions out of them. You could set up a cell to LOOK like a header, and setup the tableView:didSelectRowAtIndexPath to manually expand or collapse the section it is in.
I'd store an array of booleans corresponding the the "expended" value of each of your sections. Then you could have the tableView:didSelectRowAtIndexPath on each of your custom header rows toggle this value and then reload that specific section.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0) {
///it's the first row of any section so it would be your custom section header
///put in your code to toggle your boolean value here
mybooleans[indexPath.section] = !mybooleans[indexPath.section];
///reload this section
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
}
}
Then set numberOfRowsInSection to check the mybooleans value and return 1 if the section isn't expanded, or 1+ the number of items in the section if it is expanded.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (mybooleans[section]) {
///we want the number of people plus the header cell
return [self numberOfPeopleInGroup:section] + 1;
} else {
///we just want the header cell
return 1;
}
}
Also, you will need to update cellForRowAtIndexPath to return a custom header cell for the first row in any section.
Some sample code for animating an expand/collapse action using a table view section header is provided by Apple here: Table View Animations and Gestures
The key to this approach is to implement - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section and return a custom UIView which includes a button (typically the same size as the header view itself). By subclassing UIView and using that for the header view (as this sample does), you can easily store additional data such as the section number.
I got a nice solution inspired by Apple's Table View Animations and Gestures. I deleted unnecessary parts from Apple's sample and translated it into swift.
I know the answer is quite long, but all the code is necessary. Fortunately, you can just copy and paste most of the code and just need to do a bit modification on step 1 and 3
1.create SectionHeaderView.swift and SectionHeaderView.xib
import UIKit
protocol SectionHeaderViewDelegate {
func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionOpened: Int)
func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionClosed: Int)
}
class SectionHeaderView: UITableViewHeaderFooterView {
var section: Int?
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var disclosureButton: UIButton!
#IBAction func toggleOpen() {
self.toggleOpenWithUserAction(true)
}
var delegate: SectionHeaderViewDelegate?
func toggleOpenWithUserAction(userAction: Bool) {
self.disclosureButton.selected = !self.disclosureButton.selected
if userAction {
if self.disclosureButton.selected {
self.delegate?.sectionHeaderView(self, sectionClosed: self.section!)
} else {
self.delegate?.sectionHeaderView(self, sectionOpened: self.section!)
}
}
}
override func awakeFromNib() {
var tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "toggleOpen")
self.addGestureRecognizer(tapGesture)
// change the button image here, you can also set image via IB.
self.disclosureButton.setImage(UIImage(named: "arrow_up"), forState: UIControlState.Selected)
self.disclosureButton.setImage(UIImage(named: "arrow_down"), forState: UIControlState.Normal)
}
}
the SectionHeaderView.xib(the view with gray background) should look something like this in a tableview(you can customize it according to your needs, of course):
note:
a) the toggleOpen action should be linked to disclosureButton
b) the disclosureButton and toggleOpen action are not necessary. You can delete these 2 things if you don't need the button.
2.create SectionInfo.swift
import UIKit
class SectionInfo: NSObject {
var open: Bool = true
var itemsInSection: NSMutableArray = []
var sectionTitle: String?
init(itemsInSection: NSMutableArray, sectionTitle: String) {
self.itemsInSection = itemsInSection
self.sectionTitle = sectionTitle
}
}
3.in your tableview
import UIKit
class TableViewController: UITableViewController, SectionHeaderViewDelegate {
let SectionHeaderViewIdentifier = "SectionHeaderViewIdentifier"
var sectionInfoArray: NSMutableArray = []
override func viewDidLoad() {
super.viewDidLoad()
let sectionHeaderNib: UINib = UINib(nibName: "SectionHeaderView", bundle: nil)
self.tableView.registerNib(sectionHeaderNib, forHeaderFooterViewReuseIdentifier: SectionHeaderViewIdentifier)
// you can change section height based on your needs
self.tableView.sectionHeaderHeight = 30
// You should set up your SectionInfo here
var firstSection: SectionInfo = SectionInfo(itemsInSection: ["1"], sectionTitle: "firstSection")
var secondSection: SectionInfo = SectionInfo(itemsInSection: ["2"], sectionTitle: "secondSection"))
sectionInfoArray.addObjectsFromArray([firstSection, secondSection])
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sectionInfoArray.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.sectionInfoArray.count > 0 {
var sectionInfo: SectionInfo = sectionInfoArray[section] as! SectionInfo
if sectionInfo.open {
return sectionInfo.open ? sectionInfo.itemsInSection.count : 0
}
}
return 0
}
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let sectionHeaderView: SectionHeaderView! = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier(SectionHeaderViewIdentifier) as! SectionHeaderView
var sectionInfo: SectionInfo = sectionInfoArray[section] as! SectionInfo
sectionHeaderView.titleLabel.text = sectionInfo.sectionTitle
sectionHeaderView.section = section
sectionHeaderView.delegate = self
let backGroundView = UIView()
// you can customize the background color of the header here
backGroundView.backgroundColor = UIColor(red:0.89, green:0.89, blue:0.89, alpha:1)
sectionHeaderView.backgroundView = backGroundView
return sectionHeaderView
}
func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionOpened: Int) {
var sectionInfo: SectionInfo = sectionInfoArray[sectionOpened] as! SectionInfo
var countOfRowsToInsert = sectionInfo.itemsInSection.count
sectionInfo.open = true
var indexPathToInsert: NSMutableArray = NSMutableArray()
for i in 0..<countOfRowsToInsert {
indexPathToInsert.addObject(NSIndexPath(forRow: i, inSection: sectionOpened))
}
self.tableView.insertRowsAtIndexPaths(indexPathToInsert as [AnyObject], withRowAnimation: .Top)
}
func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionClosed: Int) {
var sectionInfo: SectionInfo = sectionInfoArray[sectionClosed] as! SectionInfo
var countOfRowsToDelete = sectionInfo.itemsInSection.count
sectionInfo.open = false
if countOfRowsToDelete > 0 {
var indexPathToDelete: NSMutableArray = NSMutableArray()
for i in 0..<countOfRowsToDelete {
indexPathToDelete.addObject(NSIndexPath(forRow: i, inSection: sectionClosed))
}
self.tableView.deleteRowsAtIndexPaths(indexPathToDelete as [AnyObject], withRowAnimation: .Top)
}
}
}
To implement the collapsible table section in iOS, the magic is how to control the number of rows for each section, or we can manage the height of rows for each section.
Also, we need to customize the section header so that we can listen to the tap event from the header area (whether it's a button or the whole header).
How to deal with the header? It's very simple, we extend the UITableViewCell class and make a custom header cell like so:
import UIKit
class CollapsibleTableViewHeader: UITableViewCell {
#IBOutlet var titleLabel: UILabel!
#IBOutlet var toggleButton: UIButton!
}
then use the viewForHeaderInSection to hook up the header cell:
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableCellWithIdentifier("header") as! CollapsibleTableViewHeader
header.titleLabel.text = sections[section].name
header.toggleButton.tag = section
header.toggleButton.addTarget(self, action: #selector(CollapsibleTableViewController.toggleCollapse), forControlEvents: .TouchUpInside)
header.toggleButton.rotate(sections[section].collapsed! ? 0.0 : CGFloat(M_PI_2))
return header.contentView
}
remember we have to return the contentView because this function expects a UIView to be returned.
Now let's deal with the collapsible part, here is the toggle function that toggle the collapsible prop of each section:
func toggleCollapse(sender: UIButton) {
let section = sender.tag
let collapsed = sections[section].collapsed
// Toggle collapse
sections[section].collapsed = !collapsed
// Reload section
tableView.reloadSections(NSIndexSet(index: section), withRowAnimation: .Automatic)
}
depends on how you manage the section data, in this case, I have the section data something like this:
struct Section {
var name: String!
var items: [String]!
var collapsed: Bool!
init(name: String, items: [String]) {
self.name = name
self.items = items
self.collapsed = false
}
}
var sections = [Section]()
sections = [
Section(name: "Mac", items: ["MacBook", "MacBook Air", "MacBook Pro", "iMac", "Mac Pro", "Mac mini", "Accessories", "OS X El Capitan"]),
Section(name: "iPad", items: ["iPad Pro", "iPad Air 2", "iPad mini 4", "Accessories"]),
Section(name: "iPhone", items: ["iPhone 6s", "iPhone 6", "iPhone SE", "Accessories"])
]
at last, what we need to do is based on the collapsible prop of each section, control the number of rows of that section:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (sections[section].collapsed!) ? 0 : sections[section].items.count
}
I have a fully working demo on my Github: https://github.com/jeantimex/ios-swift-collapsible-table-section
If you want to implement the collapsible sections in a grouped-style table, I have another demo with source code here: https://github.com/jeantimex/ios-swift-collapsible-table-section-in-grouped-section
Hope that helps.
I have a better solution that you should add a UIButton into section header and set this button's size equal to section size, but make it hidden by clear background color, after that you are easily to check which section is clicked to expand or collapse
I ended up just creating a headerView that contained a button ( i saw Son Nguyen's solution above after the fact, but heres my code.. it looks like a lot but it's pretty simple):
declare a couple bools for you sections
bool customerIsCollapsed = NO;
bool siteIsCollapsed = NO;
...code
now in your tableview delegate methods...
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, _tblSearchResults.frame.size.width, 35)];
UILabel *lblSection = [UILabel new];
[lblSection setFrame:CGRectMake(0, 0, 300, 30)];
[lblSection setFont:[UIFont fontWithName:#"Helvetica-Bold" size:17]];
[lblSection setBackgroundColor:[UIColor clearColor]];
lblSection.alpha = 0.5;
if(section == 0)
{
if(!customerIsCollapsed)
[lblSection setText:#"Customers --touch to show--"];
else
[lblSection setText:#"Customers --touch to hide--"];
}
else
{
if(!siteIsCollapsed)
[lblSection setText:#"Sites --touch to show--"];
else
[lblSection setText:#"Sites --touch to hide--"]; }
UIButton *btnCollapse = [UIButton buttonWithType:UIButtonTypeCustom];
[btnCollapse setFrame:CGRectMake(0, 0, _tblSearchResults.frame.size.width, 35)];
[btnCollapse setBackgroundColor:[UIColor clearColor]];
[btnCollapse addTarget:self action:#selector(touchedSection:) forControlEvents:UIControlEventTouchUpInside];
btnCollapse.tag = section;
[headerView addSubview:lblSection];
[headerView addSubview:btnCollapse];
return headerView;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
if(section == 0)
{
if(customerIsCollapsed)
return 0;
else
return _customerArray.count;
}
else if (section == 1)
{
if(siteIsCollapsed)
return 0;
else
return _siteArray.count;
}
return 0;
}
and finally the function that gets called when you touch one of the section header buttons:
- (IBAction)touchedSection:(id)sender
{
UIButton *btnSection = (UIButton *)sender;
if(btnSection.tag == 0)
{
NSLog(#"Touched Customers header");
if(!customerIsCollapsed)
customerIsCollapsed = YES;
else
customerIsCollapsed = NO;
}
else if(btnSection.tag == 1)
{
NSLog(#"Touched Site header");
if(!siteIsCollapsed)
siteIsCollapsed = YES;
else
siteIsCollapsed = NO;
}
[_tblSearchResults reloadData];
}
This is the best way i found to create expandable table view cells
.h file
NSMutableIndexSet *expandedSections;
.m file
if (!expandedSections)
{
expandedSections = [[NSMutableIndexSet alloc] init];
}
UITableView *masterTable = [[UITableView alloc] initWithFrame:CGRectMake(0,100,1024,648) style:UITableViewStyleGrouped];
masterTable.delegate = self;
masterTable.dataSource = self;
[self.view addSubview:masterTable];
Table view delegate methods
- (BOOL)tableView:(UITableView *)tableView canCollapseSection:(NSInteger)section
{
// if (section>0) return YES;
return YES;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 4;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if ([self tableView:tableView canCollapseSection:section])
{
if ([expandedSections containsIndex:section])
{
return 5; // return rows when expanded
}
return 1; // only top row showing
}
// Return the number of rows in the section.
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] ;
}
// Configure the cell...
if ([self tableView:tableView canCollapseSection:indexPath.section])
{
if (!indexPath.row)
{
// first row
cell.textLabel.text = #"Expandable"; // only top row showing
if ([expandedSections containsIndex:indexPath.section])
{
UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"UITableContract"]];
cell.accessoryView = imView;
}
else
{
UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"UITableExpand"]];
cell.accessoryView = imView;
}
}
else
{
// all other rows
if (indexPath.section == 0) {
cell.textLabel.text = #"section one";
}else if (indexPath.section == 1) {
cell.textLabel.text = #"section 2";
}else if (indexPath.section == 2) {
cell.textLabel.text = #"3";
}else {
cell.textLabel.text = #"some other sections";
}
cell.accessoryView = nil;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
}
else
{
cell.accessoryView = nil;
cell.textLabel.text = #"Normal Cell";
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self tableView:tableView canCollapseSection:indexPath.section])
{
if (!indexPath.row)
{
// only first row toggles exapand/collapse
[tableView deselectRowAtIndexPath:indexPath animated:YES];
NSInteger section = indexPath.section;
BOOL currentlyExpanded = [expandedSections containsIndex:section];
NSInteger rows;
NSMutableArray *tmpArray = [NSMutableArray array];
if (currentlyExpanded)
{
rows = [self tableView:tableView numberOfRowsInSection:section];
[expandedSections removeIndex:section];
}
else
{
[expandedSections addIndex:section];
rows = [self tableView:tableView numberOfRowsInSection:section];
}
for (int i=1; i<rows; i++)
{
NSIndexPath *tmpIndexPath = [NSIndexPath indexPathForRow:i
inSection:section];
[tmpArray addObject:tmpIndexPath];
}
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (currentlyExpanded)
{
[tableView deleteRowsAtIndexPaths:tmpArray
withRowAnimation:UITableViewRowAnimationTop];
UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"UITableExpand"]];
cell.accessoryView = imView;
}
else
{
[tableView insertRowsAtIndexPaths:tmpArray
withRowAnimation:UITableViewRowAnimationTop];
UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"UITableContract"]];
cell.accessoryView = imView;
}
}
}
NSLog(#"section :%d,row:%d",indexPath.section,indexPath.row);
}
So, based on the 'button in header' solution, here is a clean and minimalist implementation:
you keep track of collapsed (or expanded) sections in a property
you tag the button with the section index
you set a selected state on that button to change the arrow direction (like △ and ▽)
Here is the code:
#interface MyTableViewController ()
#property (nonatomic, strong) NSMutableIndexSet *collapsedSections;
#end
...
#implementation MyTableViewController
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (!self)
return;
self.collapsedSections = [NSMutableIndexSet indexSet];
return self;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// if section is collapsed
if ([self.collapsedSections containsIndex:section])
return 0;
// if section is expanded
#warning incomplete implementation
return [super tableView:tableView numberOfRowsInSection:section];
}
- (IBAction)toggleSectionHeader:(UIView *)sender
{
UITableView *tableView = self.tableView;
NSInteger section = sender.tag;
MyTableViewHeaderFooterView *headerView = (MyTableViewHeaderFooterView *)[self tableView:tableView viewForHeaderInSection:section];
if ([self.collapsedSections containsIndex:section])
{
// section is collapsed
headerView.button.selected = YES;
[self.collapsedSections removeIndex:section];
}
else
{
// section is expanded
headerView.button.selected = NO;
[self.collapsedSections addIndex:section];
}
[tableView beginUpdates];
[tableView reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
}
#end
I found another relatively simple way to solve that problem. By using this method we will not required to alter our cell which is almost always related to data array index, potentially causing mess in our view controller.
First, we add this following properties to our controller class:
#property (strong, nonatomic) NSMutableArray* collapsedSections;
#property (strong, nonatomic) NSMutableArray* sectionViews;
collapsedSections will save collapsed section numbers.
sectionViews will store our custom section view.
Synthesize it:
#synthesize collapsedSections;
#synthesize sectionViews;
Initialize it:
- (void) viewDidLoad
{
[super viewDidLoad];
self.collapsedSections = [NSMutableArray array];
self.sectionViews = [NSMutableArray array];
}
After that, we must connect our UITableView so it can be accessed from within our view controller class:
#property (strong, nonatomic) IBOutlet UITableView *tblMain;
Connect it from XIB to view controller using ctrl + drag like usually.
Then we create view as custom section header for our table view by implementing this UITableView delegate:
- (UIView*) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
// Create View
CGRect frame = CGRectZero;
frame.origin = CGPointZero;
frame.size.height = 30.f;
frame.size.width = tableView.bounds.size.width;
UIView* view = [[UIView alloc] initWithFrame:frame];
[view setBackgroundColor:[UIColor blueColor]];
// Add label for title
NSArray* titles = #[#"Title 1", #"Title 2", #"Title 3"];
NSString* selectedTitle = [titles objectAtIndex:section];
CGRect labelFrame = frame;
labelFrame.size.height = 30.f;
labelFrame.size.width -= 20.f;
labelFrame.origin.x += 10.f;
UILabel* titleLabel = [[UILabel alloc] initWithFrame:labelFrame];
[titleLabel setText:selectedTitle];
[titleLabel setTextColor:[UIColor whiteColor]];
[view addSubview:titleLabel];
// Add touch gesture
[self attachTapGestureToView:view];
// Save created view to our class property array
[self saveSectionView:view inSection:section];
return view;
}
Next, we implement method to save our previously created custom section header in class property:
- (void) saveSectionView:(UIView*) view inSection:(NSInteger) section
{
NSInteger sectionCount = [self numberOfSectionsInTableView:[self tblMain]];
if(section < sectionCount)
{
if([[self sectionViews] indexOfObject:view] == NSNotFound)
{
[[self sectionViews] addObject:view];
}
}
}
Add UIGestureRecognizerDelegate to our view controller .h file:
#interface MyViewController : UIViewController<UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate>
Then we create method attachTapGestureToView:
- (void) attachTapGestureToView:(UIView*) view
{
UITapGestureRecognizer* tapAction = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(onTap:)];
[tapAction setDelegate:self];
[view addGestureRecognizer:tapAction];
}
Above method will add tap gesture recognizer to all of section view we created before. Next we should implement onTap: selector
- (void) onTap:(UITapGestureRecognizer*) gestureRecognizer
{
// Take view who attach current recognizer
UIView* sectionView = [gestureRecognizer view];
// [self sectionViews] is Array containing our custom section views
NSInteger section = [self sectionNumberOfView:sectionView];
// [self tblMain] is our connected IBOutlet table view
NSInteger sectionCount = [self numberOfSectionsInTableView:[self tblMain]];
// If section more than section count minus one set at last
section = section > (sectionCount - 1) ? 2 : section;
[self toggleCollapseSection:section];
}
Above method will invoked when user tap any of our table view section. This method search correct section number based on our sectionViews array we created before.
Also, we implement method to get wihch section of header view belongs to.
- (NSInteger) sectionNumberOfView:(UIView*) view
{
UILabel* label = [[view subviews] objectAtIndex:0];
NSInteger sectionNum = 0;
for(UIView* sectionView in [self sectionViews])
{
UILabel* sectionLabel = [[sectionView subviews] objectAtIndex:0];
//NSLog(#"Section: %d -> %# vs %#", sectionNum, [label text], [sectionLabel text]);
if([[label text] isEqualToString:[sectionLabel text]])
{
return sectionNum;
}
sectionNum++;
}
return NSNotFound;
}
Next, we must implement method toggleCollapseSection:
- (void) toggleCollapseSection:(NSInteger) section
{
if([self isCollapsedSection:section])
{
[self removeCollapsedSection:section];
}
else
{
[self addCollapsedSection:section];
}
[[self tblMain] reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationFade];
}
This method will insert/remove section number to our collapsedSections array we created before. When a section number inserted to that array, it means that the section should be collapsed and expanded if otherwise.
Next we implement removeCollapsedSection:, addCollapsedSection:section and isCollapsedSection:section
- (BOOL)isCollapsedSection:(NSInteger) section
{
for(NSNumber* existing in [self collapsedSections])
{
NSInteger current = [existing integerValue];
if(current == section)
{
return YES;
}
}
return NO;
}
- (void)removeCollapsedSection:(NSInteger) section
{
[[self collapsedSections] removeObjectIdenticalTo:[NSNumber numberWithInteger:section]];
}
- (void)addCollapsedSection:(NSInteger) section
{
[[self collapsedSections] addObject:[NSNumber numberWithInteger:section]];
}
This three method is just helpers to make us easier in accessing collapsedSections array.
Finally, implement this table view delegate so our custom section views looks nice.
- (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 30.f; // Same as each custom section view height
}
Hope it helps.
I've used a NSDictionary as datasource, this looks like a lot of code, but it's really simple and works very well!
how looks here
I created a enum for the sections
typedef NS_ENUM(NSUInteger, TableViewSection) {
TableViewSection0 = 0,
TableViewSection1,
TableViewSection2,
TableViewSectionCount
};
sections property:
#property (nonatomic, strong) NSMutableDictionary * sectionsDisctionary;
A method returning my sections:
-(NSArray <NSNumber *> * )sections{
return #[#(TableViewSection0), #(TableViewSection1), #(TableViewSection2)];
}
And then setup my data soruce:
-(void)loadAndSetupData{
self.sectionsDisctionary = [NSMutableDictionary dictionary];
NSArray * sections = [self sections];
for (NSNumber * section in sections) {
NSArray * sectionObjects = [self objectsForSection:section.integerValue];
[self.sectionsDisctionary setObject:[NSMutableDictionary dictionaryWithDictionary:#{#"visible" : #YES, #"objects" : sectionObjects}] forKey:section];
}
}
-(NSArray *)objectsForSection:(NSInteger)section{
NSArray * objects;
switch (section) {
case TableViewSection0:
objects = #[] // objects for section 0;
break;
case TableViewSection1:
objects = #[] // objects for section 1;
break;
case TableViewSection2:
objects = #[] // objects for section 2;
break;
default:
break;
}
return objects;
}
The next methods, will help you to know when a section is opened, and how to respond to tableview datasource:
Respond the section to datasource:
/**
* Asks the delegate for a view object to display in the header of the specified section of the table view.
*
* #param tableView The table-view object asking for the view object.
* #param section An index number identifying a section of tableView .
*
* #return A view object to be displayed in the header of section .
*/
- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
NSString * headerName = [self titleForSection:section];
YourCustomSectionHeaderClass * header = (YourCustomSectionHeaderClass *)[tableView dequeueReusableHeaderFooterViewWithIdentifier:YourCustomSectionHeaderClassIdentifier];
[header setTag:section];
[header addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapGesture:)]];
header.title = headerName;
header.collapsed = [self sectionIsOpened:section];
return header;
}
/**
* Asks the data source to return the number of sections in the table view
*
* #param An object representing the table view requesting this information.
* #return The number of sections in tableView.
*/
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
// Return the number of sections.
return self.sectionsDisctionary.count;
}
/**
* Tells the data source to return the number of rows in a given section of a table view
*
* #param tableView: The table-view object requesting this information.
* #param section: An index number identifying a section in tableView.
* #return The number of rows in section.
*/
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
BOOL sectionOpened = [self sectionIsOpened:section];
return sectionOpened ? [[self objectsForSection:section] count] : 0;
}
Tools:
/**
Return the section at the given index
#param index the index
#return The section in the given index
*/
-(NSMutableDictionary *)sectionAtIndex:(NSInteger)index{
NSString * asectionKey = [self.sectionsDisctionary.allKeys objectAtIndex:index];
return [self.sectionsDisctionary objectForKey:asectionKey];
}
/**
Check if a section is currently opened
#param section the section to check
#return YES if is opened
*/
-(BOOL)sectionIsOpened:(NSInteger)section{
NSDictionary * asection = [self sectionAtIndex:section];
BOOL sectionOpened = [[asection objectForKey:#"visible"] boolValue];
return sectionOpened;
}
/**
Handle the section tap
#param tap the UITapGestureRecognizer
*/
- (void)handleTapGesture:(UITapGestureRecognizer*)tap{
NSInteger index = tap.view.tag;
[self toggleSection:index];
}
Toggle section visibility
/**
Switch the state of the section at the given section number
#param section the section number
*/
-(void)toggleSection:(NSInteger)section{
if (index >= 0){
NSMutableDictionary * asection = [self sectionAtIndex:section];
[asection setObject:#(![self sectionIsOpened:section]) forKey:#"visible"];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationFade];
}
}
Expanding on this answer written in Objective C, I wrote the following for those writing in Swift
The idea is to use sections within the table and set the number of rows in the section to 1 (collapsed) and 3(expanded) when the first row in that section is tapped
The table decides how many rows to draw based on an array of Boolean values
You'll need to create two rows in storyboard and give them the reuse identifiers 'CollapsingRow' and 'GroupHeading'
import UIKit
class CollapsingTVC:UITableViewController{
var sectionVisibilityArray:[Bool]!// Array index corresponds to section in table
override func viewDidLoad(){
super.viewDidLoad()
sectionVisibilityArray = [false,false,false]
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func numberOfSections(in tableView: UITableView) -> Int{
return sectionVisibilityArray.count
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat{
return 0
}
// numberOfRowsInSection - Get count of entries
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var rowsToShow:Int = 0
if(sectionVisibilityArray[section]){
rowsToShow = 3 // Or however many rows should be displayed in that section
}else{
rowsToShow = 1
}
return rowsToShow
}// numberOfRowsInSection
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){
if(indexPath.row == 0){
if(sectionVisibilityArray[indexPath.section]){
sectionVisibilityArray[indexPath.section] = false
}else{
sectionVisibilityArray[indexPath.section] = true
}
self.tableView.reloadSections([indexPath.section], with: .automatic)
}
}
// cellForRowAtIndexPath - Get table cell corresponding to this IndexPath
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell:UITableViewCell
if(indexPath.row == 0){
cell = tableView.dequeueReusableCell(withIdentifier: "GroupHeading", for: indexPath as IndexPath)
}else{
cell = tableView.dequeueReusableCell(withIdentifier: "CollapsingRow", for: indexPath as IndexPath)
}
return cell
}// cellForRowAtIndexPath
}
// -------------------------------------------------------------------------------
// tableView:viewForHeaderInSection:
// -------------------------------------------------------------------------------
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *mView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 20, 20)];
[mView setBackgroundColor:[UIColor greenColor]];
UIImageView *logoView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 5, 20, 20)];
[logoView setImage:[UIImage imageNamed:#"carat.png"]];
[mView addSubview:logoView];
UIButton *bt = [UIButton buttonWithType:UIButtonTypeCustom];
[bt setFrame:CGRectMake(0, 0, 150, 30)];
[bt setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[bt setTag:section];
[bt.titleLabel setFont:[UIFont systemFontOfSize:20]];
[bt.titleLabel setTextAlignment:NSTextAlignmentCenter];
[bt.titleLabel setTextColor:[UIColor blackColor]];
[bt setTitle: #"More Info" forState: UIControlStateNormal];
[bt addTarget:self action:#selector(addCell:) forControlEvents:UIControlEventTouchUpInside];
[mView addSubview:bt];
return mView;
}
#pragma mark - Suppose you want to hide/show section 2... then
#pragma mark add or remove the section on toggle the section header for more info
- (void)addCell:(UIButton *)bt{
// If section of more information
if(bt.tag == 2) {
// Initially more info is close, if more info is open
if(ifOpen) {
DLog(#"close More info");
// Set height of section
heightOfSection = 0.0f;
// Reset the parameter that more info is closed now
ifOpen = NO;
}else {
// Set height of section
heightOfSection = 45.0f;
// Reset the parameter that more info is closed now
DLog(#"open more info again");
ifOpen = YES;
}
//[self.tableView reloadData];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:2] withRowAnimation:UITableViewRowAnimationFade];
}
}// end addCell
#pragma mark -
#pragma mark What will be the height of the section, Make it dynamic
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
if (indexPath.section == 2) {
return heightOfSection;
}else {
return 45.0f;
}
// vKj
This action will happen in your didSelectRowAtIndexPath, when you will try to hide or show number of cell in a section
first of all declare a global variable numberOfSectionInMoreInfo in .h file and in your viewDidLoad set suppose to numberOfSectionInMoreInfo = 4.
Now use following logic:
// More info link
if(row == 3) {
/*Logic: We are trying to hide/show the number of row into more information section */
NSString *log= [NSString stringWithFormat:#"Number of section in more %i",numberOfSectionInMoreInfo];
[objSpineCustomProtocol showAlertMessage:log];
// Check if the number of rows are open or close in view
if(numberOfSectionInMoreInfo > 4) {
// close the more info toggle
numberOfSectionInMoreInfo = 4;
}else {
// Open more info toggle
numberOfSectionInMoreInfo = 9;
}
//reload this section
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
//vKj
Some sample code for animating an expand/collapse action using a table view section header is provided by Apple at Table View Animations and Gestures.
The key to this approach is to implement
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
and return a custom UIView which includes a button (typically the same size as the header view itself). By subclassing UIView and using that for the header view (as this sample does), you can easily store additional data such as the section number.
I have done the same thing using multiple sections .
class SCTierBenefitsViewController: UIViewController {
#IBOutlet private weak var tblTierBenefits: UITableView!
private var selectedIndexPath: IndexPath?
private var isSelected:Bool = false
override func viewDidLoad() {
super.viewDidLoad()
tblTierBenefits.register(UINib(nibName:"TierBenefitsTableViewCell", bundle: nil), forCellReuseIdentifier:"TierBenefitsTableViewCell")
tblTierBenefits.register(UINib(nibName:"TierBenefitsDetailsCell", bundle: nil), forCellReuseIdentifier:"TierBenefitsDetailsCell")
tblTierBenefits.rowHeight = UITableViewAutomaticDimension;
tblTierBenefits.estimatedRowHeight = 44.0;
tblTierBenefits.tableFooterView = UIView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
extension SCTierBenefitsViewController : UITableViewDataSource{
func numberOfSections(in tableView: UITableView) -> Int {
return 7
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (isSelected && section == selectedIndexPath?.section) ? 2 : 1
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0.01
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
let cell:TierBenefitsTableViewCell = tableView.dequeueReusableCell(withIdentifier: "TierBenefitsTableViewCell")! as! TierBenefitsTableViewCell
cell.selectionStyle = .none
cell.contentView.setNeedsLayout()
cell.contentView.layoutIfNeeded()
return cell
case 1:
let cell:TierBenefitsDetailsCell = tableView.dequeueReusableCell(withIdentifier: "TierBenefitsDetailsCell")! as! TierBenefitsDetailsCell
cell.selectionStyle = .none
return cell
default:
break
}
return UITableViewCell()
}
}
extension SCTierBenefitsViewController : UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
if let _selectedIndexPath = selectedIndexPath ,selectedIndexPath?.section == indexPath.section {
tblTierBenefits.beginUpdates()
expandCollapse(indexPath: _selectedIndexPath, isExpand: false)
selectedIndexPath = nil
}
else{
tblTierBenefits.beginUpdates()
if selectedIndexPath != nil {
tblTierBenefits.reloadSections([(selectedIndexPath?.section)!], with: .none)
}
expandCollapse(indexPath: indexPath, isExpand: true)
}
}
}
private func expandCollapse(indexPath: IndexPath?,isExpand: Bool){
isSelected = isExpand
selectedIndexPath = indexPath
tblTierBenefits.reloadSections([(indexPath?.section)!], with: .none)
tblTierBenefits.endUpdates()
}
}
I am adding this solution for completeness and showing how to work with section headers.
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet var tableView: UITableView!
var headerButtons: [UIButton]!
var sections = [true, true, true]
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
let section0Button = UIButton(type: .detailDisclosure)
section0Button.setTitle("Section 0", for: .normal)
section0Button.addTarget(self, action: #selector(section0Tapped), for: .touchUpInside)
let section1Button = UIButton(type: .detailDisclosure)
section1Button.setTitle("Section 1", for: .normal)
section1Button.addTarget(self, action: #selector(section1Tapped), for: .touchUpInside)
let section2Button = UIButton(type: .detailDisclosure)
section2Button.setTitle("Section 2", for: .normal)
section2Button.addTarget(self, action: #selector(section2Tapped), for: .touchUpInside)
headerButtons = [UIButton]()
headerButtons.append(section0Button)
headerButtons.append(section1Button)
headerButtons.append(section2Button)
}
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section] ? 3 : 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellReuseId = "cellReuseId"
let cell = UITableViewCell(style: .default, reuseIdentifier: cellReuseId)
cell.textLabel?.text = "\(indexPath.section): \(indexPath.row)"
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return headerButtons[section]
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44
}
#objc func section0Tapped() {
sections[0] = !sections[0]
tableView.reloadSections([0], with: .fade)
}
#objc func section1Tapped() {
sections[1] = !sections[1]
tableView.reloadSections([1], with: .fade)
}
#objc func section2Tapped() {
sections[2] = !sections[2]
tableView.reloadSections([2], with: .fade)
}
}
Link to gist: https://gist.github.com/pawelkijowskizimperium/fe1e8511a7932a0d40486a2669316d2c
in support to #jean.timex solution, use below code if you want to open one section at any time. create a variable like: var expandedSection = -1;
func toggleSection(_ header: CollapsibleTableViewHeader, section: Int) {
let collapsed = !sections[section].collapsed
// Toggle collapse
sections[section].collapsed = collapsed
header.setCollapsed(collapsed)
tableView.reloadSections(NSIndexSet(index: section) as IndexSet, with: .automatic)
if (expandedSection >= 0 && expandedSection != section){
sections[expandedSection].collapsed = true
tableView.reloadSections(NSIndexSet(index: expandedSection) as IndexSet, with: .automatic)
}
expandedSection = section;
}