This question is old but now it's easier to do it without any third party library. Since iOS 14, you can use UICollectionViewDiffableDataSource
on a UICollectionView
. You can also use SwiftUI.
UIKit
class ViewController: UIViewController {
enum Section { // We have one section
case main
}
let directory = URL(fileURLWithPath: "/") // The directory we want to browse
var dataSource: UICollectionViewDiffableDataSource<Section, URL>! // The data source
var collectionView: UICollectionView! // The collection view
override func viewDidLoad() {
super.viewDidLoad()
// Create the collection view with a list layout so it looks like a table view
collectionView = UICollectionView(frame: view.frame, collectionViewLayout: UICollectionViewCompositionalLayout { section, layoutEnvironment in
let config = UICollectionLayoutListConfiguration(appearance: .plain)
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
})
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
// Here is the code to create a cell. Replace `URL` by your own data type managed by your app
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, URL> { (cell, indexPath, url) in
var content = cell.defaultContentConfiguration()
content.text = url.lastPathComponent
var isDir: ObjCBool = false
if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir) && isDir.boolValue {
cell.accessories = [.outlineDisclosure(options: .init(style: .header))] // Add this to expandable cells
}
cell.contentConfiguration = content
}
// Create a data source. We pass our `Section` type that we created and `URL` since we are working with files here
dataSource = UICollectionViewDiffableDataSource<Section, URL>(collectionView: collectionView, cellProvider: { collectionView, indexPath, url in
// Create a cell with the block created above
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: url)
})
// Only expand directories
dataSource.sectionSnapshotHandlers.shouldExpandItem = {
var isDir: ObjCBool = false
if FileManager.default.fileExists(atPath: $0.path, isDirectory: &isDir) {
return isDir.boolValue
} else {
return false
}
}
// Only collapse directories
dataSource.sectionSnapshotHandlers.shouldCollapseItem = {
var isDir: ObjCBool = false
if FileManager.default.fileExists(atPath: $0.path, isDirectory: &isDir) {
return isDir.boolValue
} else {
return false
}
}
// When a directory will be expanded, fill the directory with its files
dataSource.sectionSnapshotHandlers.willExpandItem = { [weak self] url in
guard let self = self else {
return
}
var snapshot = self.dataSource.snapshot(for: .main)
snapshot.append((try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])) ?? [], to: url)
self.dataSource.apply(snapshot, to: .main, animatingDifferences: true, completion: nil)
}
// When a directory is collapsed, clear its content to free memory
dataSource.sectionSnapshotHandlers.willCollapseItem = { [weak self] url in
guard let self = self else {
return
}
var snapshot = self.dataSource.snapshot(for: .main)
var items = [URL]()
for item in snapshot.items { // Delete all files that are in the collapsed directory
if item.resolvingSymlinksInPath().path.hasPrefix(url.resolvingSymlinksInPath().path) && item.resolvingSymlinksInPath() != url.resolvingSymlinksInPath() {
items.append(item)
}
}
snapshot.delete(items)
self.dataSource.apply(snapshot, to: .main, animatingDifferences: true, completion: nil)
}
// Load the directory
loadDirectory()
}
// Fill the collection view with the content of the directory
func loadDirectory() {
var snapshot = NSDiffableDataSourceSectionSnapshot<URL>()
snapshot.append((try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [])) ?? [])
dataSource.apply(snapshot, to: .main, animatingDifferences: true, completion: nil)
}
}
SwiftUI
SwiftUI has an OutlineGroup
type that can make it very easy to make a tree structure. However it's not possible to lazy load with this method.
struct TreeView: View {
// We create a type that has a name and an array for children.
// These children must have the same type of the parent.
// The array must be optional. If the array is `nil`, the item will not be expandable.
// The type must also conform to `Identifiable`.
struct Item: Identifiable {
var id = UUID()
var name: String
var children: [Item]?
}
// Here we create our structure
let items = [
Item(name: "Food", children: [
Item(name: "Fruits", children: [
Item(name: "π"),
Item(name: "π"),
Item(name: "π₯"),
Item(name: "π")
])
]),
Item(name: "Objects", children: [
Item(name: "π₯"),
Item(name: "π»"),
Item(name: "βοΈ"),
Item(name: "π±")
]),
]
// Here we create a `List` containing an `OutlineGroup` initialized with our data and the path to find children
var body: some View {
List {
OutlineGroup(items, children: \.children) { item in
Text(item.name)
}
}.listStyle(.plain)
}
}
SwiftUI (Lazy Loading)
SwiftUI also has a DisclosureGroup
view that allows us to make expandable sections manually, so it's easy to create our own lazy loading list.
struct TreeView_LazyLoading: View {
// We create a view that contains a list of files inside a directory
struct DirectoryList: View {
var directory: URL
func isDirectory(_ item: URL) -> Bool {
var isDir: ObjCBool = false
return FileManager.default.fileExists(atPath: item.path, isDirectory: &isDir) && isDir.boolValue
}
var body: some View {
// This will only be called when the view appears, so we can lazy load content
ForEach((try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [])) ?? [], id: \.self) { url in
if isDirectory(url) {
// If it's a directory, show a disclosure group with another `DirectoryList` view initialized with the url
DisclosureGroup {
DirectoryList(directory: url)
} label: {
Text(url.lastPathComponent)
}
} else {
// If not, just show the file name
Text(url.lastPathComponent)
}
}
}
}
// The directory we want to browse
let directory = URL(fileURLWithPath: "/")
var body: some View {
List { // A list with the content of `directory`
DirectoryList(directory: directory)
}.listStyle(.plain)
}
}