100 Days of Swift UI – Day 17
WeSplit – Part 2
Reading text from the user with TextField
Now that we have a clean slate to work on, add three @State
properties, an empty String called checkAmount
, and two Integer properties, numberOfPeople
and tipPercentage
initialised to 2. We need to use strings for TextField, we have no choice here. The tipPercentage
is set to 2 for reasons that will become clearer shortly. To show that add an array of integers as a constant called tipPercentages
containing 10, 15, 20, 25, 0.
Inside the body
property create a Form
, with a Section
inside and, inside that, a TextField
with a placeholder text of “Amount” and a binding text of $checkAmount
.
The nice thing of @State
is that it will reload the body
property every time something happens with the things marked with @State
.
Now add a second Section with a Text which reads "$\(checkAmount)"
.
The two sections are synchronised because we have two-way binding to the checkAmount
property and because it is marked with @State
, thing that reloads the UI by calling the body
property again.
To clean this up we should change the keyboardType()
to a .decimalPad
, adding a modifier to the TextField in the form of .keyboardType(.decimalPad)
. These modifiers should be written on a new line and indented by a tab inside. The user can still enter other characters via an external keyboard, but we’ll fix this later.
Creating pickers in a form
Pickers, like text fields, need a two-way binding to a property. For our picker, that property is numberOfPeople
. In the first Section create a Picker with a label of “Number of people”, a selection of $numberOfPeople
and closure that calls the ForEach struct with a range of 2 ..< 100
creating a Text with "\($0) people"
inside the parentheses. This will populate the picker’s options with a text that says n people where n is a number between 2 and 100.
The issue here is that even if we have a new row that says “Number of people” on the left and “4 people” on the right (where did this 4 come from?!) and even if this row has a disclosure indicator on the right edge (the thing that, in table view, that invoked a detail view controller), tapping on it doesn’t do anything.
The “4 people” is not a bug because the numberOfPeople
index brings us to the third row, which is 4. To solve the second and most important thing we need to wrap our view into a NavigationView. Then, outside of the Form, add a modifier .navigationBarTitle("WeSplit")
.
Right now, on Xcode 11.1 on macOS 10.14.6 the checkmark Paul is talking about is not appearing and a lot of errors and warnings in the console.
Adding a segmented control for tip percentages
Add a third Section between the other two with a Picker saying “Tip percentage”, with a selection of $tipPercentage
and a closure that calls ForEach between 0 up to and excluding tipPercentages.count
to create a Text that shows "\(self.tipPercentages[$0])%"
. At the end of this Picker add this modifier: .pickerStyle(SegmentedPickerStyle())
.
This is certainly working, but, on the Simulator at least, if the keyboard was not dismissed and we rotate the device in landscape, we are not going to see anything on the screen, not even scrolling. Also, the checkmark is not showing up when going into the detail view controller.
Next to this Section add a parentheses to invoke the extra options and write (header: Text("How much tip do you want to leave?"))
.
Calculating the total per person.
Our next task is to show how much each person should be paying. The problems we have to overcome are: the numberOfPeople
being off by 2, the tipPercentage
storing an index of an array instead of the real value and the checkAmount
which is a String, that is, it is not safe.
Create a Double
computed property called totalPerPerson
and make it return 0
by now. Above that, create a Double
constant called peopleCount
which converts into Double the numberOfPeople
amount after adding 2 to it. Next, create a new tipSelection
constant that converts into a Double the extracted value from the tipPercentages
array at the tipPercentage
index. Finally, add a orderAmount
constant that tries to converts the checkAmount
property to a Double and gives out 0
if an incompatible piece of data is found.
Now, create three new constants: a tipValue
equal to orderAmount
divided by 100 and multiplied by tipSelection
, a grandTotal
equal to the order amount plus the tip’s value and, finally, an amountPerPerson
equal to the grand total divided by the people’s count. Then return this last constant.
Last change before running the app: in the last Section replace the passed variable with totalPerPerson
. To prevent bad rounding, add a specifier: "%.2f"
at the end, which is a C-language modifier that allows us to cut the amount of digits for floating-point numbers.
Now, if we run it, this works, but only in portrait orientation. In landscape we just see a totally blank screen. Also, a full load of errors appear on the console, such as:
- ForEach<Range<Int>, Int, Text> count (98) != its initial count (3).
ForEach(_:content:)
should only be used for constant data. Instead conform data toIdentifiable
or useForEach(_:id:content:)
and provide an explicitid
! - 2019-10-11 11:55:24.355542+0200 WeSplit [4351:1017463]. Can’t find keyplane that supports type 8 for keyboard iPhone-PortraitTruffle-DecimalPad; using 25724PortraitTruffleiPhone-Simple-PadDefault
- 2019-10-11 11:55:57.462859+0200 WeSplit [4351:1017463] [TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: <TtC7SwiftUIP33BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView: 0x7fd4371bf200; baseClass = UITableView; frame = (0 0; 414 896); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x60000125d9e0>; layer = <CALayer: 0x600001c8f9e0>; contentOffset: [0, -140]; contentSize: [414, 4393.5]; adjustedContentInset: [140, 0, 34, 0]; dataSource: <TtGC7SwiftUIP13$7fff2c69bad819ListCoreCoordinatorGVS20SystemListDataSourceOs5NeverGOS19SelectionManagerBoxS2: 0x7fd436ee9580>>
I am not really sure I can do anything about that so I will just surrender and move on.
That’s it for today.
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).
If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s books from this Affiliate Link.
Anyways, thank you so much for reading!
Till the next one!