Post

Beer Diary version 1.7 released

appstore

Version 1.7 of my BeerDiary app is now live on the iOS AppStore! You can download it here.

I would love to receive your feedback. If you have questions or remarks don’t hesitate to send me a message.

New features

Edit Mode

This version introduces an Edit button that allows you to select multiple beers at once and delete them.

edit mode

Faster exports

The compression step of the export process is now 10 times faster on average. The image files are now just stored in the zip file instead of being compressed. JPG files are already heavily compressed so there was no point in trying to recompress them.

When searching for a partial beer name and subsequently selecting one of the matching beers, the search field would autocorrect the input to the first suggestion from your iOS keyboard. This would result in the detail view quickly disappearing again. This has now been fixed.

Cancel prompt when adding new beer

When adding a new beer and trying to exit the detail screen, the app will now always ask for confirmation to prevent unintended data loss.

Implementation notes

Feel free to skip this section if you’re not that technical 😁

Edit Mode

This release enables the SwiftUI built-in \.environmentMode functionality from the @Environment. Basically you just start observing this environment value and add an EditButton() somewhere on your toolbar. Also you need to modify your List to accept a binding to the currently selected rows like this: List(selection: $selectedBeers).

Lesson learned: Selection binding type

The variable of the selection binding needs to match whatever type you use as \.id in the List. In my case I’m using UUID so it had to be @State var selectedBeers = Set<UUID>(). When this variable has the wrong type (for example Set<Beer>) the edit mode selection will not work. It would be nice to get a warning here from Xcode… 😬

Image versus text

I’ve considered using a graphical image for the Edit button but it would make things less clear, the textual button is pretty standard for iOS.

Swipe to delete

Because each row in the main list already supported ‘swipe to delete’ (via the onDelete modifier), this also meant that in edit mode each row would have a red ‘delete’ marker and a checkbox to select the row like this:

red marker in edit mode

When pressing this delete marker the row shows the swipe action:

swipe action in edit mode

I think this looked quite ugly so I wanted to remove the marker. There is a way to disable onDelete with the .deleteDisabled() modifier depending on edit mode being active, but I could not get this to work reliably. This was a general pattern with \.editMode, I also wanted to communicate it between a parent and child view but that also did not work as expected. For the second case, I solved it using a separate isEditing boolean binding.

In the end, I removed onDelete and switched to .swipeActions(edge: .trailing) to manually implement ‘swipe to delete’. This got rid of the red markers in edit mode:

only checkbox in edit mode

And and swipe to delete still works in non-edit mode:

swipe

Reduce CoreData memory usage when looping

After my adventures with CoreData migration for the the 1.6 release it occurred to me that the ‘export beers’ process was probably running into the same memory behaviour as the migration. In short, all entities processed in a for loop will be kept in memory until the loop finishes and the resulting memory usage is pretty big. This is the basic code:

1
2
3
4
5
6
7
8
9
10
11
var exported = [ExportedBeer]()
for beer in beers {
    do {
        let exportedBeer = try beer.export(compressionQuality: compressionQuality,
                                           photosURL: photosURL)
        logDebug("Exported: \(beer.wrappedName)")
        exported.append(exportedBeer)
    } catch {
        logError("Error exporting object: \(error)")
    }
}

With 500 test beer instances, the loop causes multiple gigabytes of used memory. Just like the migration 🤔. Well, at least CoreData / Swift are consistent.

I did some more digging and it turns out adding our old friend autoreleasepool solves the problem pretty well. This is the new code:

1
2
3
4
5
6
7
8
9
10
11
12
13
var exported = [ExportedBeer]()
for beer in beers {
    autoreleasepool {
        do {
            let exportedBeer = try beer.export(compressionQuality: compressionQuality,
                                               photosURL: photosURL)
            logDebug("Exported: \(beer.wrappedName)")
            exported.append(exportedBeer)
        } catch {
            logError("Error exporting object: \(error)")
        }
    }
}

The difference? The memory usage when running this loop went from multiple gigabytes of memory to just 200mb. If I have some time I will re-run the migration scenario with autoreleasepool and see if it finally helps solve that puzzle was well.

This post is licensed under CC BY 4.0 by the author.