Showing the Contents of Directories in iOS

September 01, 2021

Last time we made an app that allows the user to select multiple folders and save them using bookmarks. It displays those bookmarks in a list. And the user can delete them from the list by swiping. Now, let’s make it a bit more interesting by showing the user the contents of the folder when they tap on it. And we’ll show an icon depending on whether it’s a folder or a file.

Make the DetailView

First, let’s make the view that will display the details of a folder. Make a new SwiftUI view called DetailView.swift that takes in a URL. Add a list with a NavigationTitle. To show the navigation title in the preview, wrap DetailView in a NavigationView in the previews.

import SwiftUI

struct DetailView: View {
    var url: URL
	// TODO: get the files in the directory
    
    var body: some View {
        List {
            // TODO: loop over the actual files here
            Text("Work")
            Text("Personal")
            Text("Home")
        }
        .navigationTitle(url.lastPathComponent)
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            DetailView(url: URL(string: "path/to/Notes")!)
        }
    }
}

Make a FileController

We need to get all the files in this directory, so let’s make a file called FileController.swift with a function called getContentsOfDirectory that takes in a URL and returns an array of URLs.

import Foundation

class FileController: ObservableObject {
    func getContentsOfDirectory(url: URL) -> [URL] {
        do {
            return try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
        } catch {
            print(error)
            return []
        }
    }
}

To add this to the environment, go back to BookmarkDirectoriesApp.swift and add an instance of this class to the environment just like we did with the BookmarkController earlier:

import SwiftUI

@main
struct BookmarkDirectoriesApp: App {
    @StateObject var bookmarkController = BookmarkController()
    @StateObject var fileController = FileController()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(bookmarkController)
                .environmentObject(fileController)
        }
    }
}

Now, in DetailView.swift, we can use the FileController by adding it as an environment object. To call the FileController’s method everytime this view loads, we can call it with onAppear(). We can save the results of this in a state variable called urls. Then you can show the actual files in the list.

import SwiftUI

struct DetailView: View {
    var url: URL
    @State var urls: [URL] = []
    @EnvironmentObject var fileController: FileController
    
    var body: some View {
        List {
            ForEach(urls, id: \.self) { url in
                Text(url.lastPathComponent)
            }
        }
        .onAppear {
            urls = fileController.getContentsOfDirectory(url: url)
            print(urls)
        }
        .navigationTitle(url.lastPathComponent)      
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            DetailView(url: URL(string: "path/to/Notes")!)
			    .environmentObject(FileController())
        }
    }
}

Add Navigation Links

Now we can link to the detail view from ContentView.swift:

Instead of showing Text in the list, use a NavigationLink.

ForEach(bookmarkController.bookmarks, id: \.uuid) { uuid, url in
    NavigationLink(url.lastPathComponent, destination: DetailView(url: url))
}

Now, when you run the app and tap on a folder, it will show you the files and folders inside that folder. Nice!

Detail View

Add icons

But before we call this done, I would like to add a little folder icon next to each folder and a document icon next to each document. If you look at the documentation for contentsOfDirectory(at:includingPropertiesForKeys:options:), it says that the keys parameter lets you specify what kind of information you want about each URL. That sounds exactly like what I want. It takes an array of URLResourceKeys. Looking at the page on URLResourceKey]s, if you search for “directory”, you’ll find a key called isDirectoryKey.

So let’s modify our getContentsOfDirectory() method to return an array of tuples so that we have a URL and a boolean telling us if it’s a directory or not:

func getContentsOfDirectory(url: URL) -> [(url: URL, isDirectory: Bool)] {
    if isPreview {
       return [
            (URL(string: "Notes/Books")!, false),
            (URL(string: "Notes/Home")!, true)
       ]
    }
    
    do {
        let urls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey])
        return urls.map({ url -> (url: URL, isDirectory: Bool) in
            do {
                let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey])
                return (url, resourceValues.isDirectory!)
            } catch {
                return (url, false)
            }
        })
    } catch {
        print(error)
        return []
    }
}

In DetailView.swift change our state variable from urls to items and change the type to match the return type from the function.

@State var items: [(url: URL, isDirectory: Bool)] = []

And change the ForEach to show either a folder or a document icon next to each name.

ForEach(items, id: \.url) { item in
    HStack {
        item.isDirectory
            ? Image(systemName: "folder")
            : Image(systemName: "doc")
        Text(item.url.lastPathComponent)
    }
}

When you run the app and tap on the folder, we now have icons next to each item showing whether it’s a folder or a file.

detail view with icons

Fix the preview

The preview in Xcode looks empty because the function returns an empty array. Let’s add a flag in the FileController’s initializer that lets it know that we’re in preview mode so it can give us some example data.

var isPreview: Bool
    
init(isPreview: Bool = false) {
    self.isPreview = isPreview
}

And at the top of getContentsOfDirectory, add some code to return example data if isPreview is true.

if isPreview {
   return [
        (URL(string: "Notes/Books")!, false),
        (URL(string: "Notes/Home.md")!, true)
   ]
}

In DetailView.swift change the preview code to pass true to isPreview.

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            DetailView(url: URL(string: "path/to/Notes")!)
                .environmentObject(FileController(isPreview: true))
        }
    }
}

The final code for this step can be found on GitHub.


Profile picture

Written by Adam Garrett-Harris, a podcaster and software engineer in Utah. You should follow him on Twitter