Beer Diary version 2.0 released
Version 2.0 of the BeerDiary app is now live on the iOS AppStore! It is available for iPhone and iPad and 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
iPad support
I’m happy to announce that the iPad is now fully supported in the app! 🎉
Because all your data is stored in iCloud, any data entered on the iPhone version of the app will automatically show up on the iPad. When you initially start up the app, just give it some time to complete the sync. Afterwards, any edits/additions on any of your devices will show up on the other devices as well. Implementing this feature took quite a bit of time and effort and I sincerely hope you enjoy it!
Other changes
- Searching now also includes the ‘comments’ field text so you can more easily find the right beer.
- When using the auto complete functionality, some times tapping on the suggestion would not work correctly. The ‘tappable area’ has been increased and this should hopefully fix this.
- When you delete more than a few beers at a time, a nice overlay with an indicator is shown. Previously the UI would just stop responding for a while.
- On iPad, entries are saved automatically when you navigate to another entry or put the app in the background. There is also still a manual ‘Save’ button if you really want to be in control of things. Until an entry is saved, Undo is still available as usual.
- As part of the iPad release, I redesigned the App Store screenshots a bit. I think the new blue back ground looks a bit more professional than the previous yellow one. Let me know what you think!
- The minimum supported iOS/iPadOS version is now 17.5.
Implementation notes
Feel free to skip this section if you’re not that technical 🤓
iPad support
As part of the iPad support I made the following changes to the app:
- I started using
NavigationSplitView(see section below). - On iPad, changes are auto-saved when you navigate to a different entry or when you put the app in the background. On iPhone it was easy to explicitly save changes because any edits are performed in a modal sheet that blocks the rest of the app. For the iPad version this was a bit more difficult because at any time you can just click to another entry in the list. In my opinion it would be bad to ask the user at that point if they want to save/discard so the app tries to make the smart choice here.
- Following the previous item, on iPad this means that Undo functionality will work until you navigate to another entry or put the app in the background. I might rework this later but for now it felt like the pragmatic thing to do. Perhaps I will look into UndoManager in a later version.
- If a user is editing the same entry on device A and B at the same time, the logic has been changed so that if you save on device A these changes immediately show up on the edit screen of device B. The only exception is that if you already edited a field on device B the local change will not be overridden. The merge process is relatively smart so for instance the list of photos will automatically combine all photos from device A and B. It feels like a bit of an edge case but I thought it was nice to get this working properly.
In general, iPad support was quite a bit of work (more than I expected) but in the end the code base became a bit more structured as a result.
NavigationSplitView
In this version I moved to NavigationSplitView. See also this post where I first mentioned this. Coming from the ‘old’ navigation patterns, this was quite a refactor.
I usually prefer to make a small prototype app to really learn new functionality in detail. In this case I made a project called NavigationSplitViewtest and published it on Github. Feel free to check it out, it is pretty much the solution I have in the BeerDiary app.
The biggest win is that there is almost no platform-specific code. On iPhone it shows as a regular List-Detail screen and on iPad it shows the familiar column layout. The platform-specific code that I needed is mostly about different toolbar button layout and some other minor UI details. The biggest iPad-only thing is that when the app starts up, I want the first entry in the list to be automatically selected. I think this looks nicer; most sample code I found just handles this by showing an empty screen with a text ‘Please select an entry’ and this feels a bit cheap. Getting this right took quite a bit of time and it’s one of the areas where I think NavigationSplitView could improve in future.
On a side note: whatever I tried, the first entry selection code results in a grey background of the list entry. When the user selects it, it gets a blue background. This seems to be the standard platform behaviour so there is not much I can do. But I still think it looks a bit strange. Investigating solutions took a long time and in the end it’s probably not really that important.
My conclusion is that NavigationSplitView is good enough for now but I feel there are some rough edges that hopefully will be addressed in future iOS versions. Check out the sample project, there are some workarounds necessary that I would not prefer to do it if this thing was implemented a bit more user-friendly in the first place.
Observable (iOS 17)
This is actually one of the biggest changes made on a technical level. Previously I was using ObservableObject and I replaced it as much as possible with the new iOS 17+ Observation framework. The biggest advantage of this is that SwiftUI can now more efficiently determine if a view needs to be updated when an observed object has changed. With the old solution, any change in the observed data would trigger a view update even though it might be possible that the view was not even consuming this data. This is especially an issue where you have MVVM view models where only one property changes. The new framework is a lot smarter and considers things on a per-property basis. In my opinion it’s also easier to use and a bit more clean so this is a big improvement.
The one thing I’m missing still is a modern equivalent to the @StateObject because that allows a view to retain its own specific parameterized view model that was passed in init. For instance currently in a List-Detail scenario my code works like this:
- List view creates a detail view and passes some parameters to its constructor
- Detail view creates a detail view model using the parameters passed
- Detail view stores the view model as a
@StateObject. - Whenever list view updates and updates detail view, the view model from step 3 is re-used.
With the new Observation framework this is not really possible because Apple advises not to init @State properties in init.
In the Apple sample code / documentation it is explained that the list view should ‘own’ all data of the detail view (or better yet, some model object in the environment) and inject the view model when the detail view is created. If you really want to use a view model you’re forced to do something like this:
- List view owns the data for all detail views (an array of entries for instance)
- List view creates a parameterized detail view model and passes it to the detail view when it is created
But this will cause the detail view model to be recreated each time the list view is updated. It does not really seem very efficient to me. This forces you in the direction of putting the parameters directly into the view as @State (instead of a view model), which will work nicely but it will become messy when the view becomes complicated.
For now it seems the only in-between solution is to have a generic view model like this in the detail view: @State private var viewModel = ViewModel() and store the parameters in the view itself using @State. Then, whenever you need one of the @State values, you pass a reference to these parameters to the method in the view model.In some scenarios I chose this solution, in one place I’m still stuck with @StateObject so this is definitely an area I will come back to.
Some more details can be found in this nice article.
MV versus MVVM
As part of this refactor I partially reverted my decision to completely switch to MV-architecture (where everything is in the view or model objects in the environment) and I re-introduced MVVM in places where it made sense (complex views). Some views just had too much code in them and splitting them into smaller views did not resolve this. So in the end I just decided to be pragmatic. I guess these kind of changes are part of the ‘gold plating’ you’re allowed to do when it’s your own app 🤓.
Sheets
The app uses .sheet quite a bit and this ported over to iPad quite well in most scenarios. The biggest issue I had was with the Add new sheet, this is something I needed to make a bit bigger to allow for easy entry. Sizing this sheet to the exact size of the underlying view was a headache so I tried some other solutions like .fullscreenCover and .popOver and that one almost did what I wanted except that it’s hard to position and the arrow cannot be hidden. So in the end I adapted the underlying view a bit and forced the width of the .sheet on iPad so it looks OK. When I have some time I will revisit this topic.
CoreData main versus child view context
In previous versions both the ‘add’ and ‘edit’ use cases would be performed in a child view context that is isolated from the main view context. However, this yields interesting results on iPad when the same entry is edited from another device. In this case the list view already updates (because it’s the main view context) but the detail view doesn’t update because the child context does not see the changes yet.
I’ve experimented with the mechanism of listening for NSManagedObjectContextDidSave and manually merging changes. I also tried setting automaticallyMergesChangesFromParent to true on the child context. Both solutions did not work really well for me somehow.
So in the end, the most straightforward solution was to start editing entries on the main view context. This solved a lot of merge issues and to be honest also it simplifies the code a bit more. Undoing any changes is still a case of rolling back the changes to the context. Adding entries is still done in a child context because there is no risk of merge issues.

