Beer Diary version 1.7 released
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.
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.
Disabled auto correction in search
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:
When pressing this delete marker the row shows the swipe action:
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:
And and swipe to delete still works in non-edit mode:
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.