Comment créer ce type de table dans iOS ??
Ici, si nous tapons sur la 1ère ligne "Compte", puis automatiquement, il défile avec quelques lignes supplémentaires qui s'affichent dans l'image. Et si nous tapons à nouveau sur Compte, cette vue sera masquée.
Vous pouvez facilement configurer une cellule pour qu'elle ressemble à un en-tête et configurer le tableView: didSelectRowAtIndexPath
Pour développer ou réduire manuellement la section dans laquelle elle se trouve. Si je stockais un tableau de booléens correspondant à la valeur "dépensée" de chacune de vos sections. Vous pouvez alors avoir le tableView:didSelectRowAtIndexPath
Sur chacune de vos lignes d'en-tête personnalisées pour basculer cette valeur puis recharger cette section spécifique.
- (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];
}
}
Vous devez ensuite configurer votre nombre numberOfRowsInSection
pour vérifier la valeur mybooleans
et renvoyer soit 1 si la section n'est pas développée, soit 1+ le nombre d'éléments dans la section, si elle est développée .
- (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;
}
}
Vous devrez également mettre à jour votre cellForRowAtIndexPath
pour renvoyer une cellule d'en-tête personnalisée pour la première ligne de tout section
.
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
est le meilleur moyen de fournir votre "propre en-tête personnalisé", car c'est exactement ce pour quoi il est conçu.
Pour plus de détails, reportez-vous à cette Réponse ou à cette PKCollapsingTableViewSections .
En outre, vous pouvez obtenir ce type de vues de table en utilisant setIndentationLevel
. Veuillez vous référer à DemoCode pour cet exemple. Je pense que c'est la meilleure solution pour les vues de tables déroulantes.
Si vous souhaitez créer un simple en-tête et une liste déroulante de cellules, veuillez vous reporter à STCollapseTableView .
J'espère que c'est ce que vous recherchez. Toute inquiétude me revient. :)
Le moyen le plus simple et le plus naturel de l'implémenter via des cellules de vue tableau. Pas de vues de cellule en expansion, pas d'en-têtes de section, de cellules simples et simples (nous sommes après tout dans une vue de table).
La conception est la suivante:
CollapsableViewModel
qui contient les informations nécessaires pour configurer la cellule: label, imagechildren
, qui est un tableau d'objets CollapsableViewModel
, et isCollapsed
, qui contient l'état de la liste déroulanteCollapsableViewModel
, ainsi qu'une liste plate contenant les modèles de vue qui seront rendus à l'écran (la propriété displayedRows
)displayedRows
et dans la vue tableau, via les fonctions insertRowsAtIndexPaths()
et deleteRowsAtIndexPaths()
.Le code Swift est le suivant (notez que le code utilise uniquement la propriété label
du modèle de vue, pour le garder propre):
import UIKit
class CollapsableViewModel {
let label: String
let image: UIImage?
let children: [CollapsableViewModel]
var isCollapsed: Bool
init(label: String, image: UIImage? = nil, children: [CollapsableViewModel] = [], isCollapsed: Bool = true) {
self.label = label
self.image = image
self.children = children
self.isCollapsed = isCollapsed
}
}
class CollapsableTableViewController: UITableViewController {
let data = [
CollapsableViewModel(label: "Account", image: nil, children: [
CollapsableViewModel(label: "Profile"),
CollapsableViewModel(label: "Activate account"),
CollapsableViewModel(label: "Change password")]),
CollapsableViewModel(label: "Group"),
CollapsableViewModel(label: "Events", image: nil, children: [
CollapsableViewModel(label: "Nearby"),
CollapsableViewModel(label: "Global"),
]),
CollapsableViewModel(label: "Deals"),
]
var displayedRows: [CollapsableViewModel] = []
override func viewDidLoad() {
super.viewDidLoad()
displayedRows = data
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return displayedRows.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier") ?? UITableViewCell()
let viewModel = displayedRows[indexPath.row]
cell.textLabel!.text = viewModel.label
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
let viewModel = displayedRows[indexPath.row]
if viewModel.children.count > 0 {
let range = indexPath.row+1...indexPath.row+viewModel.children.count
let indexPaths = range.map { IndexPath(row: $0, section: indexPath.section) }
tableView.beginUpdates()
if viewModel.isCollapsed {
displayedRows.insert(contentsOf: viewModel.children, at: indexPath.row + 1)
tableView.insertRows(at: indexPaths, with: .automatic)
} else {
displayedRows.removeSubrange(range)
tableView.deleteRows(at: indexPaths, with: .automatic)
}
tableView.endUpdates()
}
viewModel.isCollapsed = !viewModel.isCollapsed
}
}
La contrepartie d'Objective-C est facile à traduire, j'ai ajouté la version Swift uniquement car elle est plus courte et plus lisible.
Avec quelques petites modifications, le code peut être utilisé pour générer des listes déroulantes de plusieurs niveaux.
Les gens m'ont posé des questions sur les séparateurs, cela peut être réalisé en ajoutant une classe personnalisée CollapsibleTableViewCell
qui est configurée avec un modèle de vue (enfin, déplacez la logique de configuration de la cellule du contrôleur vers son emplacement - la cellule). Les crédits pour la logique de séparation uniquement pour certaines cellules vont aux personnes répondant this SO question.
Tout d'abord, mettez à jour le modèle, ajoutez une propriété needsSeparator
qui indique à la cellule de vue de table de rendre ou non le séparateur:
class CollapsableViewModel {
let label: String
let image: UIImage?
let children: [CollapsableViewModel]
var isCollapsed: Bool
var needsSeparator: Bool = true
init(label: String, image: UIImage? = nil, children: [CollapsableViewModel] = [], isCollapsed: Bool = true) {
self.label = label
self.image = image
self.children = children
self.isCollapsed = isCollapsed
for child in self.children {
child.needsSeparator = false
}
self.children.last?.needsSeparator = true
}
}
Ensuite, ajoutez la classe de cellules:
class CollapsibleTableViewCell: UITableViewCell {
let separator = UIView(frame: .zero)
func configure(withViewModel viewModel: CollapsableViewModel) {
self.textLabel?.text = viewModel.label
if(viewModel.needsSeparator) {
separator.backgroundColor = .gray
contentView.addSubview(separator)
} else {
separator.removeFromSuperview()
}
}
override func layoutSubviews() {
super.layoutSubviews()
let separatorHeight = 1 / UIScreen.main.scale
separator.frame = CGRect(x: separatorInset.left,
y: contentView.bounds.height - separatorHeight,
width: contentView.bounds.width-separatorInset.left-separatorInset.right,
height: separatorHeight)
}
}
cellForRowAtIndexPath
devrait alors être modifié pour renvoyer ce type de cellules:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = (tableView.dequeueReusableCell(withIdentifier: "CollapsibleTableViewCell") as? CollapsibleTableViewCell) ?? CollapsibleTableViewCell(style: .default, reuseIdentifier: "CollapsibleTableViewCell")
cell.configure(withViewModel: displayedRows[indexPath.row])
return cell
}
Une dernière étape, supprimez les séparateurs de cellules de vue de tableau par défaut - soit de xib soit du code (tableView.separatorStyle = .none
).
Voici une solution basée sur [~ # ~] mvc [~ # ~] .
Créer une classe de modèle ClsMenuGroup pour vos sections
class ClsMenuGroup: NSObject {
// We can also add Menu group's name and other details here.
var isSelected:Bool = false
var arrMenus:[ClsMenu]!
}
Créer une classe de modèle ClsMenu pour vos lignes
class ClsMenu: NSObject {
var strMenuTitle:String!
var strImageNameSuffix:String!
var objSelector:Selector! // This is the selector method which will be called when this menu is selected.
var isSelected:Bool = false
init(pstrTitle:String, pstrImageName:String, pactionMehod:Selector) {
strMenuTitle = pstrTitle
strImageNameSuffix = pstrImageName
objSelector = pactionMehod
}
}
Créer un tableau de groupes dans votre ViewController
class YourViewController: UIViewController, UITableViewDelegate {
@IBOutlet var tblMenu: UITableView!
var objTableDataSource:HDTableDataSource!
var arrMenuGroups:[AnyObject]!
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
if arrMenuGroups == nil {
arrMenuGroups = Array()
}
let objMenuGroup = ClsMenuGroup()
objMenuGroup.arrMenus = Array()
var objMenu = ClsMenu(pstrTitle: "Manu1", pstrImageName: "Manu1.png", pactionMehod: "menuAction1")
objMenuGroup.arrMenus.append(objMenu)
objMenu = ClsMenu(pstrTitle: "Menu2", pstrImageName: "Menu2.png", pactionMehod: "menuAction2")
objMenuGroup.arrMenus.append(objMenu)
arrMenuGroups.append(objMenuGroup)
configureTable()
}
func configureTable(){
objTableDataSource = HDTableDataSource(items: nil, cellIdentifier: "SideMenuCell", configureCellBlock: { (cell, item, indexPath) -> Void in
let objTmpGroup = self.arrMenuGroups[indexPath.section] as! ClsMenuGroup
let objTmpMenu = objTmpGroup.arrMenus[indexPath.row]
let objCell:YourCell = cell as! YourCell
objCell.configureCell(objTmpMenu) // This method sets the IBOutlets of cell in YourCell.m file.
})
objTableDataSource.sectionItemBlock = {(objSection:AnyObject!) -> [AnyObject]! in
let objMenuGroup = objSection as! ClsMenuGroup
return (objMenuGroup.isSelected == true) ? objMenuGroup.arrMenus : 0
}
objTableDataSource.arrSections = self.arrMenuGroups
tblMenu.dataSource = objTableDataSource
tblMenu.reloadData()
}
// MARK: - Tableview Delegate
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let objTmpGroup = self.arrMenuGroups[indexPath.section] as! ClsMenuGroup
let objTmpMenu = objTmpGroup.arrMenus[indexPath.row]
if objTmpMenu.objSelector != nil && self.respondsToSelector(objTmpMenu.objSelector) == true {
self.performSelector(objTmpMenu.objSelector) // Call the method for the selected menu.
}
tableView.reloadData()
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let arrViews:[AnyObject] = NSBundle.mainBundle().loadNibNamed("YourCustomSectionView", owner: self, options: nil)
let objHeaderView = arrViews[0] as! UIView
objHeaderView.sectionToggleBlock = {(objSection:AnyObject!) -> Void in
let objMenuGroup = objSection as! ClsMenuGroup
objMenuGroup.isSelected = !objMenuGroup.isSelected
tableView.reloadData()
}
return objHeaderView
}
// MARK: - Menu methods
func menuAction1(){
}
func menuAction2(){
}
}
J'ai utilisé HDTableDataSource à la place des méthodes de source de données de Tableview. Vous pouvez trouver un exemple de HDTableDataSource de Github.
Les avantages du code ci-dessus sont
- Vous pouvez à tout moment modifier l'ordre de n'importe quel menu ou section ou changer de menu et de section, sans changer d'autres fonctions.
- Vous n'aurez pas besoin d'ajouter un long code else if ladder dans les méthodes de délégué de votre tableview
- Vous pouvez spécifier séparément l'icône, le titre ou un autre attribut pour votre élément de menu, comme l'ajout du nombre de badges, la modification de la couleur du menu sélectionné, etc.
- Vous pouvez également utiliser plusieurs cellules ou sections en appliquant des modifications mineures au code existant
le moyen le plus simple est d'utiliser l'en-tête de section UITableView comme cellule-> et le nombre de lignes défini est 0 et section.count pour réduire et développer l'état.
Il s'agit de l'en-tête TableViewSection, isExpand -> section.count sinon retourne 0.
-Cellule normale
-Cellule normale
-Cellule normale
Il s'agit de l'en-tête TableViewSection, isExpand -> section.count sinon retourne 0.
-Cellule normale
-Cellule normale
-Cellule normale
Habituellement, je le fais en définissant la hauteur de la ligne. Par exemple, vous disposez de deux éléments de menu avec des listes déroulantes:
Vous devez donc créer une vue de table avec 2 sections. La première section contient 4 lignes (Menu 1 et ses éléments) et la section seconde contient 3 lignes (Menu 2 et ses éléments).
Vous définissez toujours la hauteur uniquement pour la première ligne de la section. Et si l'utilisateur clique sur la première ligne, vous développez les lignes de cette section en définissant la hauteur et rechargez cette section.
Il n'y a pas de contrôle intégré pour les vues arborescentes comme dans le cadre iOS - UIKit . Comme cela a été souligné par d'autres utilisateurs, la solution la plus simple (sans utiliser de bibliothèques externes) est probablement d'ajouter une logique personnalisée au délégué et à la source de données de UITableView
pour imiter le comportement souhaité.
Heureusement, il existe des bibliothèques open source qui vous permettent d'implémenter l'arborescence souhaitée comme la vue sans vous soucier des détails des opérations de développement/réduction. Il y a deux d'entre eux disponibles pour la plate-forme iOS. Dans la plupart des cas, ces bibliothèques enveloppent simplement UITableView
et vous fournissent une interface conviviale pour le programmeur qui vous permet de vous concentrer sur votre problème et non sur les détails d'implémentation de l'arborescence.
Personnellement, je suis l'auteur de la bibliothèque RATreeView qui vise à minimiser le coût nécessaire pour créer une arborescence comme des vues sur iOS. Vous pouvez consulter des exemples de projets (disponibles dans Objective-c et Swift ) pour vérifier comment ce contrôle fonctionne et se comporte. En utilisant mon contrôle, il est vraiment simple de créer la vue que vous souhaitez:
DataObject
struct sera utilisé pour conserver les informations sur le nœud de l'arborescence - il sera responsable de conserver les informations sur le titre de la cellule, son image (si la cellule a une image) et ses enfants (si la cellule a des enfants) .class DataObject
{
let name : String
let imageURL : NSURL?
private(set) var children : [DataObject]
init(name : String, imageURL : NSURL?, children: [DataObject]) {
self.name = name
self.imageURL = imageURL
self.children = children
}
convenience init(name : String) {
self.init(name: name, imageURL: nil, children: [DataObject]())
}
}
TreeTableViewCell
et implémenterons deux cellules conformes à ce protocole. Une de ces cellules sera utilisée pour afficher les éléments racine et une autre sera utilisée pour afficher les enfants des éléments racine.protocol TreeTableViewCell {
func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool)
}
class ChildTreeTableViewCell : UITableViewCell, TreeTableViewCell {
func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool) {
//implementation goes here
}
}
class RootTreeTableViewCell : UITableViewCell, TreeTableViewCell {
func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool) {
//implementation goes here
}
}
let profileDataObject = DataObject(name: "Profile")
let privateAccountDataObject = DataObject(name: "Private Account")
let changePasswordDataObject = DataObject(name: "Change Password")
let accountDataObject = DataObject(name: "Account", imageURL: NSURL(string: "AccountImage"), children: [profileDataObject, privateAccountDataObject, changePasswordDataObject])
let groupDataObject = DataObject(name: "Group", imageURL: NSURL(string: "GroupImage"), children: [])
let eventDataObject = DataObject(name: "Event", imageURL: NSURL(string: "EventImage"), children: [])
let dealsDataObject = DataObject(name: "Deals", imageURL: NSURL(string: "DealsImage"), children: [])
data = [accountDataObject, groupDataObject, eventDataObject, dealsDataObject]
RATreeView
.func treeView(treeView: RATreeView, numberOfChildrenOfItem item: AnyObject?) -> Int {
if let item = item as? DataObject {
return item.children.count //return number of children of specified item
} else {
return self.data.count //return number of top level items here
}
}
func treeView(treeView: RATreeView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
if let item = item as? DataObject {
return item.children[index] //we return child of specified item here (using provided `index` variable)
} else {
return data[index] as AnyObject //we return root item here (using provided `index` variable)
}
}
func treeView(treeView: RATreeView, cellForItem item: AnyObject?) -> UITableViewCell {
let cellIdentifier = item ? “TreeTableViewChildCell” : “TreeTableViewCellRootCell”
let cell = treeView.dequeueReusableCellWithIdentifier(cellIdentifier) as! TreeTableViewCell
//TreeTableViewCell is a protocol which is implemented by two kinds of
//cells - the one responsible for root items in the tree view and another
//one responsible for children. As we use protocol we don't care
//which one is truly being used here. Both of them can be
//configured using data from `DataItem` object.
let item = item as! DataObject
let isExpanded = treeView.isCellForItemExpanded(item) //this variable can be used to adjust look of the cell by determining whether cell is expanded or not
cell.setup(withTitle: item.name, imageURL: item.imageURL, expanded: isExpanded)
return cell
}
Notez que l'utilisation de ma bibliothèque vous n'avez pas à vous soucier de développer et de réduire la cellule - elle est gérée par le RATreeView
. Vous n'êtes responsable que des données utilisées pour configurer les cellules - le reste est géré par le contrôle lui-même.
@interface TestTableViewController ()
{
BOOL showMenu;
}
@implementation TestTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"accountMenu"];
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"accountSubMenu"];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section == 0) {
// Account Menu
return 1;
}
if (showMenu) {
// Profile/Private Account/Change Password
return 3;
}
// Hidden Account Menu
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell;
if (indexPath.section == 0) {
cell = [tableView dequeueReusableCellWithIdentifier:@"accountMenu" forIndexPath:indexPath];
cell.textLabel.text = @"Account";
}
else
{
cell = [tableView dequeueReusableCellWithIdentifier:@"accountSubMenu" forIndexPath:indexPath];
switch (indexPath.row) {
case 0:
cell.textLabel.text = @"Profile";
break;
case 1:
cell.textLabel.text = @"Private Account";
break;
case 2:
cell.textLabel.text = @"Change Password";
break;
default:
break;
}
}
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
// Click on Account Menu
showMenu = !showMenu;
[tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
J'espère que cela vous aidera :)
Vous pouvez avoir un compte en tant que cellule qui se développe au toucher pour révéler trois boutons ("Profil", "Activer le compte", "Changer le mot de passe"), mais cela crée un problème: tapoter autour de chacun des trois boutons comptera comme "sélectionné par l'utilisateur" la cellule Compte "et déclenchez -tableView:didSelectRowAtIndexPath:
avec l'extension/l'effondrement de la cellule qui en résulte.
Ou vous pouvez faire de chacune des options masquées ("Profil", "Activer le compte", "Changer le mot de passe") une cellule de vue de table distincte. Mais je ne sais pas comment vous pourriez animer les trois cellules dans son ensemble en expansion et en contraction (au lieu de chaque expansion séparément de la hauteur zéro à la pleine expansion).
Donc, la meilleure solution est peut-être de:
Ce n'est pas l'utilisation la plus élégante d'UITableView, mais cela fera le travail.
Vous avez besoin d'une TableView réductible. Pour y parvenir, dans votre TableView, vous devez suivre les sections qui sont réduites (contractées) et celles qui sont développées. Pour cela, vous devez conserver un ensemble d'index de sections développées ou un tableau booléen où la valeur de chaque index indique si la section correspondante est développée ou non. Vérifiez les valeurs à l'index spécifique lors de l'attribution de la hauteur à une certaine ligne. Vérifiez ce lien pour plus d'aide.
Vous pouvez en apprendre davantage sur les vues de table sectionnelles ici .
Il existe des bibliothèques tierces disponibles sur Github qui peuvent vous sauver de l'hustel. Jetez un œil à CollapsableTableView ou CollapsableTable-Swift
Si vous n'aimez pas utiliser de bibliothèque externe, vous pouvez créer 2 cellules personnalisées. Un qui s'affiche avant l'expansion et l'autre après l'expansion (avec différents identifiants). Et lorsque vous cliquez sur la cellule, vérifiez si la cellule est développée ou non. Si ce n'est pas le cas, utilisez l'identifiant de cellule développé, sinon l'identifiant de cellule non développé.
C'est le moyen le plus efficace et le plus propre de créer une cellule de vue de tableau étendue.
Selon la réponse de @sticker, vous pouvez lier l'exécution
objc_setAssociatedObject
pour l'index de section, et utilisez sa logique. Et tout en utilisant tapgesture sur la vue d'en-tête, vous pouvez obtenir l'index de section comme
objc_getAssociatedObject.
UITapGestureRecognizer *singleTapRecogniser = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureHandler:)] autorelease];
[singleTapRecogniser setDelegate:self];
singleTapRecogniser.numberOfTouchesRequired = 1;
singleTapRecogniser.numberOfTapsRequired = 1;
[sectionHeaderView addGestureRecognizer:singleTapRecogniser];
Si vous voulez une bibliothèque tierce, vous pouvez essayer la solution this .
J'aime la solution @Cristik, il y a quelque temps, j'ai eu le même problème et ma solution suit en quelque sorte les mêmes principes; c'est donc ce que je propose en fonction des exigences que j'avais:
Pour le rendre plus général, les éléments de la table ne devraient pas hériter d'une classe spécialisée pour la fonctionnalité d'extension, mais il devrait y avoir un protocole qui définit les propriétés nécessaires
Il ne devrait pas y avoir de restriction sur le nombre de niveaux que nous pouvons étendre. Ainsi, la table peut avoir une option, une sous-option, une sous-option, etc.
La vue du tableau doit afficher ou masquer les cellules en utilisant l'une des animations habituelles (pas de reloadData
)
L'action d'expansion ne doit pas nécessairement être attachée à l'utilisateur qui sélectionne la cellule, la cellule peut avoir un UISwitch par exemple
La version simplifiée de l'implémentation ( https://github.com/JuanjoArreola/ExpandableCells ) est la suivante:
D'abord le protocole:
protocol CellDescriptor: class {
var count: Int { get }
var identifier: String! { get }
}
Une cellule non extensible a toujours un nombre de 1:
extension CellDescriptor {
var count: Int { return 1 }
}
Ensuite, le protocole de cellule extensible:
protocol ExpandableCellDescriptor: CellDescriptor {
var active: Bool { get set }
var children: [CellDescriptor] { get set }
subscript(index: Int) -> CellDescriptor? { get }
func indexOf(cellDescriptor: CellDescriptor) -> Int?
}
Une chose intéressante à propos de Swift est que nous pouvons écrire une partie de l'implémentation dans une extension de protocole et que toutes les classes conformes peuvent utiliser l'implémentation par défaut, nous pouvons donc écrire le count
subscript
et indexOf
implémentation et en plus quelques autres fonctions utiles comme ceci:
extension ExpandableCellDescriptor {
var count: Int {
var total = 1
if active {
children.forEach({ total += $0.count })
}
return total
}
var countIfActive: Int {
...
}
subscript(index: Int) -> CellDescriptor? {
...
}
func indexOf(cellDescriptor: CellDescriptor) -> Int? {
...
}
func append(cellDescriptor: CellDescriptor) {
children.append(cellDescriptor)
}
}
L'implémentation complète se trouve dans le fichier CellDescriptor.Swift
De plus, dans le même fichier, il existe une classe nommée CellDescriptionArray
qui implémente ExpandableCellDescriptor
et n'affiche pas une cellule seule
Maintenant, n'importe quelle classe peut se conformer aux protocoles précédents sans avoir besoin d'hériter d'une classe spécifique, pour l'exemple de code dans github j'ai créé quelques classes: Option
et ExpandableOption
, voici comment ExpandableOption
ressemble à:
class ExpandableOption: ExpandableCellDescriptor {
var delegate: ExpandableCellDelegate?
var identifier: String!
var active: Bool = false {
didSet {
delegate?.expandableCell(self, didChangeActive: active)
}
}
var children: [CellDescriptor] = []
var title: String?
}
Et c'est l'une des sous-classes UITableViewCell:
class SwitchTableViewCell: UITableViewCell, CellDescrptionConfigurable {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var switchControl: UISwitch!
var cellDescription: CellDescriptor! {
didSet {
if let option = cellDescription as? ExpandableOption {
titleLabel.text = option.title
switchControl.on = option.active
}
}
}
@IBAction func activeChanged(sender: UISwitch) {
let expandableCellDescriptor = cellDescription as! ExpandableCellDescriptor
expandableCellDescriptor.active = sender.on
}
}
Notez que vous pouvez configurer la cellule et sa classe comme vous le souhaitez, vous pouvez ajouter des images, des étiquettes, des commutateurs, etc.; aucune restriction et aucun changement aux protocoles nécessaires.
Enfin, dans TableViewController, nous créons l'arborescence des options:
var options = CellDescriptionArray()
override func viewDidLoad() {
super.viewDidLoad()
let account = ExpandableOption(identifier: "ExpandableCell", title: "Account")
let profile = Option(identifier: "SimpleCell", title: "Profile")
let isPublic = ExpandableOption(identifier: "SwitchCell", title: "Public")
let caption = Option(identifier: "SimpleCell", title: "Anyone can see this account")
isPublic.append(caption)
account.append(profile)
account.append(isPublic)
options.append(account)
let group = ExpandableOption(identifier: "ExpandableCell", title: "Group")
group.append(Option(identifier: "SimpleCell", title: "Group Settings"))
options.append(group)
...
}
Le reste de l'implémentation est désormais très simple:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return options.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let option = options[indexPath.row]!
let cell = tableView.dequeueReusableCellWithIdentifier(option.identifier, forIndexPath: indexPath)
(cell as! CellDescrptionConfigurable).cellDescription = option
(option as? ExpandCellInformer)?.delegate = self
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
guard let option = options[indexPath.row] else { return }
guard let expandableOption = option as? ExpandableOption else { return }
if expandableOption.identifier == "ExpandableCell" {
expandableOption.active = !expandableOption.active
}
}
func expandableCell(expandableCell: ExpandableCellDescriptor, didChangeActive active: Bool) {
guard let index = options.indexOf(expandableCell) else { return }
var indexPaths = [NSIndexPath]()
for row in 1..<expandableCell.countIfActive {
indexPaths.append(NSIndexPath(forRow: index + row, inSection: 0))
}
if active {
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Fade)
} else {
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Fade)
}
}
Cela peut ressembler à beaucoup de code, mais la plupart ne sont écrits qu'une seule fois, la plupart des informations nécessaires pour dessiner correctement la vue de table existent dans le fichier CellDescriptor.Swift, le code de configuration de cellule existe à l'intérieur des sous-classes UITableViewCell et il y a relativement peu de code dans le TableViewController lui-même.
J'espère que ça aide.