Uitableview: Nested Section Headers

UITableView : Nested Section Swift 5

This type of TableView is generally called Accordion TableView. If you run google search for this you will find a lot of tutorials and sample codes. 2 links are mentioned below

  1. Making Simple Accordion TableView in swift?
  2. https://medium.com/ios-os-x-development/ios-how-to-build-a-table-view-with-collapsible-sections-96badf3387d0

iPhone UITableView Nested Sections

You got it. A UITableView really isn't designed to show more than two levels of a hierarchy, as sections and rows. If you want to show more than two levels, a "drill-down" approach used in most (all?) iOS apps, where tapping a row presents another UITableView on the navigation stack. (As you say.)

There are lots of Apple sample code projects that use this design pattern.

Edit: just checked and DrillDownSave is a good example, as is SimpleDrillDown.

Multi Header Sections in UITableView Swift

You can do this fairly easily with a single Cell Prototype:

Sample Image

I've added a single label, constrained on all 4 sides (use margins).

You'll notice one of the constraints is not like the others - Label Leading - because I connected that as an @IBOutlet. When I set the cell data, I change the label background color, the .constant of the leading constraint, and the .selectionStyle based on it being a "Brand" row or a "Model" row:

enum VehicleType {
case car, truck
}

struct Vehicle {
var type: VehicleType = .car
var brand: String = ""
var model: String = ""
}

class MyCustomCell: UITableViewCell {

@IBOutlet var theLabel: UILabel!
@IBOutlet var labelLeading: NSLayoutConstraint!

func setData(_ v: Vehicle) -> Void {

if v.model == "" {
theLabel.text = v.brand
theLabel.textColor = .darkGray
theLabel.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
labelLeading.constant = 0
selectionStyle = .none
} else {
theLabel.text = v.model
theLabel.textColor = .black
theLabel.backgroundColor = .clear
labelLeading.constant = 16
selectionStyle = .default
}

}

}

In this example, I determine if it's a "Brand" row/cell if the "Model" name is an empty string.

Here's how it can look:

Sample Image

and after scrolling down to the Trucks section:

Sample Image

If you want other appearance differences between the Brand and Model rows, you can handle those in the same .setData() function.

Here is a complete example:

enum VehicleType {
case car, truck
}

struct Vehicle {
var type: VehicleType = .car
var brand: String = ""
var model: String = ""
}

class MyCustomCell: UITableViewCell {

@IBOutlet var theLabel: UILabel!
@IBOutlet var labelLeading: NSLayoutConstraint!

func setData(_ v: Vehicle) -> Void {

if v.model == "" {
theLabel.text = v.brand
theLabel.textColor = .darkGray
theLabel.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
labelLeading.constant = 0
selectionStyle = .none
} else {
theLabel.text = v.model
theLabel.textColor = .black
theLabel.backgroundColor = .clear
labelLeading.constant = 16
selectionStyle = .default
}

}

}

class MultiSectionViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

@IBOutlet var tableView: UITableView!

// for simulating getting the data
let activityView = UIActivityIndicatorView(style: .large)

// will contain an array of Cars and an array of Trucks
var dataArray: [[Vehicle]] = []

override func viewDidLoad() {
super.viewDidLoad()

tableView.delegate = self
tableView.dataSource = self

// empty view as footer so we don't see blank rows
tableView.tableFooterView = UIView()

}

override func viewDidAppear(_ animated: Bool) {
self.simulateGetData()
}

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let viewContainer = UIView()
viewContainer.backgroundColor = UIColor.lightGray
let labelHeader = UILabel()

labelHeader.textColor = UIColor.white
if section == 0 {
labelHeader.text = "Cars "
}
if section == 1 {
labelHeader.text = "Trucks"
}
viewContainer.addSubview(labelHeader)
labelHeader.autoresizingMask = [.flexibleWidth, .flexibleHeight]
labelHeader.frame = viewContainer.frame

return viewContainer
}

func numberOfSections(in tableView: UITableView) -> Int {
return dataArray.count
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray[section].count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCustomCell", for: indexPath) as! MyCustomCell

let vehicle: Vehicle = dataArray[indexPath.section][indexPath.row]
cell.setData(vehicle)

return cell
}

func simulateGetData() -> Void {

// show the "spinner"
view.addSubview(activityView)
activityView.center = CGPoint(x: tableView.center.x, y: tableView.frame.origin.y + 80)
activityView.startAnimating()

// simulate it taking 2 seconds to get the data
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.parseData(self.remoteData)
}
}

func parseData(_ str: String) -> Void {

var fullList: [Vehicle] = []

// split retrieved string into lines
let linesArray: [String] = str.components(separatedBy: "\n")
linesArray.forEach { line in
// split this line
let a: [String] = line.components(separatedBy: ",")
fullList.append(Vehicle(type: a[0] == "car" ? .car : .truck, brand: a[1], model: a[2]))
}

// get the cars
var cars: [Vehicle] = fullList.filter { $0.type == .car }

// get list of car brands
let carBrands = Set((cars).compactMap { $0.brand })
// for each brand, append a Vehicle with Brand but no Model
carBrands.forEach { brand in
cars.append(Vehicle(type: .car, brand: brand, model: ""))
}

// sort cars by brand / model
cars.sort {
($0.brand, $0.model) <
($1.brand, $1.model)
}

// get the trucks and sort by brand / model
var trucks: [Vehicle] = fullList.filter { $0.type == .truck }

// get list of trueck brands
let truckBrands = Set((trucks).compactMap { $0.brand })
// for each brand, append a Vehicle with Brand but no Model
truckBrands.forEach { brand in
trucks.append(Vehicle(type: .truck, brand: brand, model: ""))
}

// sort trucks by brand / model
trucks.sort {
($0.brand, $0.model) <
($1.brand, $1.model)
}

// fill our dataArray
dataArray.append(cars)
dataArray.append(trucks)

// remove the spinner
activityView.stopAnimating()
activityView.removeFromSuperview()

// reload the table
tableView.reloadData()
}

let remoteData: String = """
car,Chevrolet,Camaro
car,Chevrolet,Corvette
car,Chevrolet,Impala
car,Chevrolet,Malibu
car,Chevrolet,Sonic
truck,Chevrolet,Colorado
truck,Chevrolet,Silverado
car,Ford,EcoSport
car,Ford,Edge
car,Ford,Escape
car,Ford,Expedition
car,Ford,Fusion
car,Ford,Mustang
truck,Ford,F-150
truck,Ford,F-250
truck,Ford,F-350
car,Toyota,4Runner
car,Toyota,Avalon
car,Toyota,Camry
car,Toyota,Corolla
car,Toyota,Highlander
car,Toyota,Prius
car,Toyota,Rav4
truck,Toyota,Tacoma
truck,Toyota,Tundra
"""

}

and the source for the Storyboard I used with the Prototype cell:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wBJ-BC-ngb">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Multi Section View Controller-->
<scene sceneID="vJm-85-LPr">
<objects>
<viewController id="wBJ-BC-ngb" customClass="MultiSectionViewController" customModule="MiniScratch" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="pxy-Ko-DBo">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="UEv-mW-XVy">
<rect key="frame" x="40" y="100" width="295" height="527"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="myCustomCell" id="rtS-G4-74c" customClass="MyCustomCell" customModule="MiniScratch" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="295" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="rtS-G4-74c" id="39C-jc-tSh">
<rect key="frame" x="0.0" y="0.0" width="295" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="KlT-QG-nQC">
<rect key="frame" x="15" y="11" width="265" height="21.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="KlT-QG-nQC" secondAttribute="trailing" id="EpN-7X-Ue5"/>
<constraint firstAttribute="bottomMargin" secondItem="KlT-QG-nQC" secondAttribute="bottom" id="WK9-gS-0S3"/>
<constraint firstItem="KlT-QG-nQC" firstAttribute="top" secondItem="39C-jc-tSh" secondAttribute="topMargin" id="hkl-1J-cH5"/>
<constraint firstItem="KlT-QG-nQC" firstAttribute="leading" secondItem="39C-jc-tSh" secondAttribute="leadingMargin" id="zza-OX-VlC"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="labelLeading" destination="zza-OX-VlC" id="FIW-Qy-k9n"/>
<outlet property="theLabel" destination="KlT-QG-nQC" id="CX2-4G-IlT"/>
</connections>
</tableViewCell>
</prototypes>
</tableView>
</subviews>
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="nNy-q0-wea" firstAttribute="bottom" secondItem="UEv-mW-XVy" secondAttribute="bottom" constant="40" id="Je2-cL-xF0"/>
<constraint firstItem="UEv-mW-XVy" firstAttribute="top" secondItem="nNy-q0-wea" secondAttribute="top" constant="100" id="Wne-7o-FQB"/>
<constraint firstItem="nNy-q0-wea" firstAttribute="trailing" secondItem="UEv-mW-XVy" secondAttribute="trailing" constant="40" id="h4R-46-NsF"/>
<constraint firstItem="UEv-mW-XVy" firstAttribute="leading" secondItem="nNy-q0-wea" secondAttribute="leading" constant="40" id="hUZ-2G-yQI"/>
</constraints>
<viewLayoutGuide key="safeArea" id="nNy-q0-wea"/>
</view>
<connections>
<outlet property="tableView" destination="UEv-mW-XVy" id="pMr-41-iIc"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="mYD-E0-bHz" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="52" y="-68"/>
</scene>
</scenes>
</document>

Different section headers in UITableView of a nested NSArray

In your case, it's better to store your arrays in an NSDictionary. For example, if you declare and synthesize an NSDictionary variable called tableContents and an NSArray called titleOfSections, you can do something like this:

 - (void)viewDidLoad {
[super viewDidLoad];

//These will automatically be released. You won't be needing them anymore (You'll be accessing your data through the NSDictionary variable)
NSArray *firstSection = [NSArray arrayWithObjects:@"Red", @"Blue", nil];
NSArray *secondSection = [NSArray arrayWithObjects:@"Orange", @"Green", @"Purple", nil];
NSArray *thirdSection = [NSArray arrayWithObject:@"Yellow"];

//These are the names that will appear in the section header
self.titleOfSections = [NSArray arrayWithObjects:@"Name of your first section",@"Name of your second section",@"Name of your third section", nil];

NSDictionary *temporaryDictionary = [[NSDictionary alloc]initWithObjectsAndKeys:firstSection,@"0",secondSection,@"1",thirdSection,@"2",nil];
self.tableContents = temporaryDictionary;
[temporaryDictionary release];
}

Then in the table view controller methods:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return [self.titleOfSections count];
}
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
return [[self.tableContents objectForKey:[NSString stringWithFormat:@"%d",section]] count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
//Setting the name of your section
return [self.titleOfSections objectAtIndex:section];
}

Then to access the contents of each array in your cellForRowAtIndexPath method:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *SimpleTableIdentifier = @"SimpleTableIdentifier";

NSArray *arrayForCurrentSection = [self.tableContents objectForKey:[NSString stringWithFormat:@"%d",indexPath.section]];

UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:SimpleTableIdentifier];

if (cell == nil) {
cell = [[[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:SimpleTableIdentifier] autorelease];
}
cell.textLabel.text = [arrayForCurrentSection objectAtIndex:indexPath.row];

return cell;
}


Related Topics



Leave a reply



Submit