Post

Beer Diary version 1.4 released

appstore

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.

brewery autocomplete

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.

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