Currently I have the following layout:

But I want to get this layout for the headers (expanded state):

So essentialy I need a custom view for the disclosure indicator because for the expanded state it should point to the bottom and for the collapsed state it should point at the top (see above picture). In addition the disclosure indicator should be on the left.
What I have done so far
With this code I get the first example:
class ViewController: UIViewController {
enum ListItem: Hashable {
case header(HeaderItem)
case symbol(SFSymbolItem)
}
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())
var dataSource: UICollectionViewDiffableDataSource<HeaderItem, ListItem>!
override func viewDidLoad() {
super.viewDidLoad()
configureCollectionView()
setupDataSource()
updateDate(items: HeaderItem.modelObjects)
}
private func configureCollectionView() {
let layoutConfig = UICollectionLayoutListConfiguration(appearance: .plain)
let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
collectionView.register(CustomListCell.self, forCellWithReuseIdentifier: CustomListCell.reuseID)
collectionView.collectionViewLayout = listLayout
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func setupDataSource() {
let headerCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, HeaderItem> {
(cell, indexPath, headerItem) in
var config = cell.defaultContentConfiguration()
config.text = headerItem.title
cell.contentConfiguration = config
let headerDisclosureOption = UICellAccessory.OutlineDisclosureOptions(style: .header)
cell.accessories = [.outlineDisclosure(options: headerDisclosureOption)]
//cell.accessories = [.customView(configuration: .init(customView: UIView(), placement: .trailing(displayed: .always, at: .center), isHidden: <#T##Bool?#>, reservedLayoutWidth: <#T##UICellAccessory.LayoutDimension?#>, tintColor: <#T##UIColor?#>, maintainsFixedSize: <#T##Bool?#>))]
}
let symbolCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SFSymbolItem> {
(cell, indexPath, symbolItem) in
var config = cell.defaultContentConfiguration()
config.text = symbolItem.name
cell.contentConfiguration = config
}
dataSource = UICollectionViewDiffableDataSource<HeaderItem, ListItem>(collectionView: collectionView) {
(collectionView, indexPath, listItem) -> UICollectionViewCell? in
switch listItem {
case .header(let headerItem):
// Dequeue header cell
let cell = collectionView.dequeueConfiguredReusableCell(using: headerCellRegistration,
for: indexPath,
item: headerItem)
return cell
case .symbol(let symbolItem):
// Dequeue symbol cell
let cell = collectionView.dequeueConfiguredReusableCell(using: symbolCellRegistration,
for: indexPath,
item: symbolItem)
return cell
}
}
}
private func updateDate(items: [HeaderItem]) {
var snapshot = NSDiffableDataSourceSnapshot<HeaderItem, ListItem>()
snapshot.appendSections(items)
for headerItem in items {
//section snapshot
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<ListItem>()
let headerListItem = ListItem.header(headerItem)
sectionSnapshot.append([headerListItem])
let symbolListItemArray = headerItem.symbols.map { ListItem.symbol($0) }
sectionSnapshot.append(symbolListItemArray, to: headerListItem)
dataSource.apply(sectionSnapshot, to: headerItem, animatingDifferences: false)
}
}
}
Model:
struct HeaderItem: Hashable {
let title: String
let symbols: [SFSymbolItem]
}
struct SFSymbolItem: Hashable {
let name: String
let image: UIImage
init(name: String) {
self.name = name
self.image = UIImage(systemName: name)!
}
}
extension HeaderItem {
static let modelObjects = [
HeaderItem(title: "Communication", symbols: [
SFSymbolItem(name: "mic"),
SFSymbolItem(name: "mic.fill"),
SFSymbolItem(name: "message"),
SFSymbolItem(name: "message.fill"),
]),
HeaderItem(title: "Weather", symbols: [
SFSymbolItem(name: "sun.min"),
SFSymbolItem(name: "sun.min.fill"),
SFSymbolItem(name: "sunset"),
SFSymbolItem(name: "sunset.fill"),
]),
HeaderItem(title: "Objects & Tools", symbols: [
SFSymbolItem(name: "pencil"),
SFSymbolItem(name: "pencil.circle"),
SFSymbolItem(name: "highlighter"),
SFSymbolItem(name: "pencil.and.outline"),
]),
]
}
Edit:
I was able to get closer to the desired result by using .customView(...) as accesory type.
let testAction = UIAction(image: UIImage(systemName: "chevron.up"), handler: { [weak self] _ in
//expand / collapse programmatically
})
let testBtn = UIButton(primaryAction: testAction)
let customAccessory = UICellAccessory.CustomViewConfiguration(
customView: testBtn,
placement: .leading(displayed: .always))
cell.accessories = [.customView(configuration: customAccessory)]

Is it possible to do the collapsing / expanding programmatically by clicking on the customView?
Here's a simple workaround, but it will fulfill your requirement. Instead of creating a custom accessory view simply use the
.imageproperty of theUIListContentConfigurationand change it based on the state. With that, your headerCellRegistration will look something like the below:Hope this solves your problem, cheers!