Deleting Bookmarks in iOS

August 23, 2021

Last time we made an app that allows the user to select multiple folders and save them using bookmarks. And it displays those bookmarks in a list. But the user can’t delete them from the list or tap on them to see the files inside the folder. Today we’ll tackle deleting bookmarks from the list.

Removing from the array

To delete the bookmark, we’ll need a new method in our BookmarkController. But first, let’s add the onDelete() modifier to our ForEach in the list and call the new method (which we haven’t created yet).

.onDelete(perform: { indexSet in
    bookmarkController.removeBookmark(at: indexSet)
})

As you can see in the onDelete modifier, the perform parameter is a function that receives an argument of type IndexSet. An IndexSet is a collection of unique integer values that represent the indexes of elements in another collection. In this case, it’s a collection of indexes that represent which bookmarks the user wants to delete. We can shorten our code to this:

.onDelete(perform: bookmarkController.removeBookmark)

We need to make a removeBookmark method in the BookmarkController that receives an IndexSet. It will need to remove those items from the urls array and delete the bookmark files so that they aren’t loaded up the next time the user opens the app.

func removeBookmark(at offsets: IndexSet) {
    // Remove bookmarks from urls array
    urls.remove(atOffsets: offsets)
	
    // Delete the bookmark file
}

Removing the items from the array is a matter of passing the offsets to the remove method.

To delete the bookmark file, we have to get the URL of the bookmark file. Remember that a bookmark is binary data and we save each bookmark in a separate plain text file in the app’s sandbox directory. The name of each file is a UUID. To get the URL of each file, we need to know the UUID associated with this bookmark. We don’t have that because when we load the bookmarks, the only thing we saved was the URL in the urls array. So, we need to go back and do some refactoring.

If you run the app at this point, you can swipe to delete bookmarks and they appear to be gone, but they come back when you restart the app.

Refactor Loading Bookmarks

I’m going to change the urls property by making it a tuple of UUIDs and URLs and I’m going to rename it to bookmarks since it will now store more than just URLs. To rename it everywhere at once, right-click on the name of the variable, and go to “Refactor”, “Rename…“. It should now look like this:

@Published var bookmarks: [(uuid: String, url: URL)] = []

I now have 4 issues I need to fix where the types don’t match.

In the init function, change the fake data to use tuples:

init(loadFakeData: Bool = false) {
    if loadFakeData {
        bookmarks = [
            ("123", URL(string: "some/path/Notes")!),
            ("124", URL(string: "some/path/Family%20Notes")!),
        ]
    } else {
        loadAllBookmarks()
    }
}

When we append to the array, append a tuple instead:

bookmarks.append((uuid, url))

In loadAllBookmarks, return a tuple:

return (file.lastPathComponent, url)

Finally, in ContentView’s ForEach we have to pull out the URL from the tuple and we can use uuid as the id instead of self.

ForEach(bookmarkController.bookmarks, id: \.uuid) { _, url in
    Text(url.lastPathComponent)
}

Now the refactor is complete and we have an array of tuples with the URL and UUID instead of an array of just URLs.

Delete the bookmark files

Let’s finish our removeBookmark method. First, get the array of UUIDS before removing them from the array. Then loop over the UUIDS and delete the bookmark files.

func removeBookmark(at offsets: IndexSet) {
    let uuids = offsets.map( { bookmarks[$0].uuid } )
    
    // Remove bookmarks from urls array
    bookmarks.remove(atOffsets: offsets)
    
    // Delete the bookmark file
    uuids.forEach({ uuid in
        let url = getAppSandboxDirectory().appendingPathComponent(uuid)
        try? FileManager.default.removeItem(at: url)
    })
}

Now when you run the app, you can swipe to delete bookmarks, and they’re still gone when you restart the app. The final code for this step can be found on GitHub.

Next time, we’ll display the content of the directories when you tap on them.


Profile picture

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