Beer Diary version 1.4 released
Version 1.4 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
Suggestions for Brewery field
When adding multiple beers from the same brewery, there now is a new suggestions functionality to quickly select an existing value.
Stripping of whitespace
It’s a small issue but when entering text in any text field, leading and trailing whitespace is now automatically trimmed to keep the data in your app just that little bit more tidy.
Implementation notes
Feel free to skip this section if you’re not that technical 😁
###
Suggestions for Brewery field
For this functionality, I started out with the code from Gabriele Cusimano. It was a good starting point but I have customized it quite heavily. For instance, I removed the ScrollView
because my suggestion view will show maximum 3 suggestions anyway. I’m not completely happy with the manual adding of Divider
and tried to switch to List
but that had its own issues. Maybe I will revisit this in a next version.
For highlighting the existing search text, I used the code from here.
I ended up with this combined solution. It just takes the suggestions
Array that I fill, deduplicate (case and diacritic insensitive) and truncate in another method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//
// TextFieldSuggestions.swift
// BeerDiary
//
// Created by Gabriele Cusimano on 26/08/21.
// Source: https://github.com/Gabro777/SuggestionTextFieldMenu-SwiftUI
// Adapted and refactored by Benjamin van den Hout on 28/01/24.
//
import SwiftUI
public struct TextFieldSuggestions: View {
/// Indicates if TextField is being edited
@Binding var editing: Bool
/// Input text to filter on
@Binding var inputText: String
/// Suggestions to show
@Binding var suggestions: [String]
/// Vertical offset, higher value means menu is shown further down
@State var verticalOffset: CGFloat
/// Horizontal offset, higher value means menu is shown further to the right
@State var horizontalOffset: CGFloat
/// Init
public init(editing: Binding<Bool>,
text: Binding<String>,
suggestions: Binding<[String]>,
verticalOffset: CGFloat = 0.0,
horizontalOffset: CGFloat = 0.0) {
self._editing = editing
self._inputText = text
self._suggestions = suggestions
self.verticalOffset = verticalOffset
self.horizontalOffset = horizontalOffset
}
public var body: some View {
LazyVStack(spacing: 0) {
ForEach($suggestions.wrappedValue, id: \.self) { suggestedText in
HightLightedText(suggestedText, matching: inputText, highlightColor: Constant.textHighlightColor)
.padding(.horizontal, Constant.horizontalTextPadding)
.padding(.vertical, Constant.verticalTextPadding)
.frame(minWidth: Constant.minTextFrameWidth,
maxWidth: Constant.maxTextFrameWidth,
minHeight: Constant.minTextHeight,
maxHeight: Constant.maxTextHeight,
alignment: Constant.textAlignment)
.contentShape(Rectangle())
.onTapGesture(perform: {
inputText = suggestedText
editing = false
self.hideKeyboard()
})
if needDivider(for: suggestedText) {
Divider()
.padding(.horizontal, Constant.dividerHorizontalPadding)
}
}
}
.foregroundColor(Constant.foregroundColor)
.background(Constant.backgroundColor)
.cornerRadius(Constant.cornerRadius)
.ignoresSafeArea()
.frame(maxWidth: Constant.viewMaxWidth)
.shadow(radius: Constant.shadowRadius)
.padding(.horizontal, Constant.horizontalPadding)
.offset(x: horizontalOffset, y: verticalOffset)
.isHidden(!editing, remove: !editing)
}
/// Determines if a Divider is needed
/// - Parameter suggestedText: Text to evaluate
/// - Returns: True if Divider should be added, false if not
private func needDivider(for suggestedText: String) -> Bool {
return suggestedText != suggestions.last
}
private struct Constant {
// Text
static let horizontalTextPadding = 25.0
static let verticalTextPadding = 25.0
static let minTextFrameWidth = 0.0
static let maxTextFrameWidth: CGFloat = .infinity
static let minTextHeight = 0.0
static let maxTextHeight = 50.0
static let textAlignment: Alignment = .leading
static let textHighlightColor: Color = .primary
// Divider
static let dividerHorizontalPadding = 10.0
// View
static let cornerRadius = 15.0
static let foregroundColor: Color = .primary
static let backgroundColor = Color(UIColor.systemBackground)
static let viewMaxWidth: CGFloat = .infinity
static let shadowRadius = 4.0
static let horizontalPadding = 0.0
}
}
Capitalization of section headers
I re-capitalize section headers in the main list when sorting by Brewery. This is to group values that just vary in case (like San miguel
and San Miguel
). This gave some interesting result for input like 't IJ
. It would end up as 'T Ij
which looks strange in Dutch.
I’m now using String.localizedCapitalized
which, for Dutch, ends up with 'T IJ
. Not completely pretty yet but at least it is better. One downside is that if you have the app set to English, it will still use the old behaviour. 🤓
This is an issue I’m still figuring out the solution for. I might just end up using the exact values the user entered and they will notice soon enough if spelling varies. With the new suggestions box I think this problem might become less of an issue. To be continued.
Xcode Cloud
In this version I started using Xcode Cloud. 25 hours of build time is included in every Apple developer account so that is plenty of time. I set up a job to just run unit tests whenever I push to main
or a feature branch. I also integrated it into Github but blocking a PR on unit tests passing in Xcode Cloud is a feature you need a paid Github account for so I left that out.
Overall I’m pretty happy with it, build times are pretty fast and it was easy to set up. I’m still manually uploading but if I ever go for Testflight it should be easy to automate.
Periphery
I integrated the excellent Periphery app into the project. It is similar to Swiftlint but it scans for unused code and variables. I created a separate target for it and run it once in a while. It’s also automatically run in Xcode Cloud.