There are 4 actions that we need to code.
Each time we tap on one of the number buttons we need to append it or add it to the end of our total string.
However, it is currently set for 0 so we will need to do a conditional check first and if it is 0 we will just replace 0 with the number tapped, otherwise we will add to our total by appending the number to the end.
Inside our parent struct
view, create a function called addDigit
that will receive a number as a String
Note: By using an _ as the beginning of our parameter, it means that when we call the function we can simply pass in the string without specifying the argument name. i.e.
addNumber("7")
instead ofaddNumber(number: "7")
xxxxxxxxxx
func addDigit(_ number: String) {
}
We can create a condition using an if...else
clause and then in the case that the comparison of the number to 0 is true, we assign number to the total, lease we assign to total the previous total plus the number. Since number and total are both strings, the appended string just gets added after the existing string so "7" + "8" - "78"
xxxxxxxxxx
if number == "0" {
total = number
} else {
total = total + number
}
This can be simplified using a ternary operator
newValue = WTF (What ? True : False)
xxxxxxxxxx
total = total == "0" ? number : total + number
Then we can add this function then as the action for our numberButton
button's action, passing in the number that we get as the function argument.
xxxxxxxxxx
func numberButton(number: String) -> some View {
Button {
addDigit(number)
} label: {
Text(number)
.font(.largeTitle)
.bold()
.frame(width: 80, height: 80)
.background(.purple)
.foregroundColor(.white)
.clipShape(Circle())
}
}
func addDigit(_ number: String) {
total = total == "0" ? number : total + number
}
If we test, we see it works, but if you go beyond 6 digits we are on to two lines, not good.
We can set the line limit on our Text
view to 1
xxxxxxxxxx
.lineLimit(1)
This limits the line, but it also truncates the number. Not good.
To accommodate this we do what the iPhone Calculator app does, it reduces the text size using a minimumScaleFactor
of our choosing
xxxxxxxxxx
.minimumScaleFactor(0.5)
Now the bill will have to be awfully large before it truncates.
Right now there are two issues with the decimal.
We can add more than 2 digits after the decimal.
We can add a second decimal point and this makes no sense.
To handle this, we can create 2 computed
properties
that will return a Boolean
value (true or false) that we can use to determine if we can add more digits or a decimal point.
Create a computed property called canAddDecimal
that is a Bool
type
xxxxxxxxxx
var canAddDecimal: Bool {
}
This means we need to provide a check that will be either true
or false
. What we want to do is to count the number of decimal points our total already has and if the count is 0 we will return true
, otherwise, return false
We can use a filter
function on a string because a String is essentially an array of characters so if we filter and look only for "." we will get an array of only "." and then we can count them.
xxxxxxxxxx
var canAddDecimal: Bool {
let periods = total.filter({$0 == "."})
if periods.count == 0 {
return true
} else {
return false
}
}
These 4 lines can be simplified to a single line that returns the truthfulness of comparing the count of the filter to 0. It will be either true or false
xxxxxxxxxx
return total.filter({$0 == "."}).count == 0
And, since there is only one line in the body of our computation, we can omit the word return
xxxxxxxxxx
var canAddDecimal: Bool {
total.filter({$0 == "."}).count == 0
}
For a video on filtering and other higher order functions, see:
We need a similar function for canAddDigit
Again, we start with a variable called canAddDigit
that is a computed variable that is a Bool
type
xxxxxxxxxx
var canAddDigit: Bool {
}
We need to be able to compute the index of the decimal point and find out what that distance is from the start and then subtract it from the total to see that we have no more than 2.
First we can use a guard check to see if the decimal index even exists. If it doesn't we return true.
If it does we can calculate the index a the distance from the start to the found index.
hen we can return that count truthfulness if it is less than 2
How did I figure this computation out? I GOOGLED it.
xxxxxxxxxx
var canAddDigit: Bool {
guard let decIndex = total.firstIndex(of: ".") else { return true }
let index = total.distance(from: total.startIndex, to: decIndex)
return (total.count - index - 1) < 2
}
With these to properties now available I can make some changes to my addDigitFunction
first, we check to see if we can actually add a digit, because if we already have a decimal and 2 digits, we can't
xxxxxxxxxx
if canAddDigit {
}
then we check to see if what we have tapped is a decimal and if so, we can see if we are allowed to add it and if so, we add it onto the total .
Note:
total += number
is the same astotal = total + number
if it is not a decimal. We use what we already had in place
xxxxxxxxxx
func addDigit(_ number: String) {
if canAddDigit {
if number == "." {
if canAddDecimal {
total += number
}
} else {
total = total == "0" ? number : total + number
}
}
}
We still have to code the action for the backspace button
For this, we check first to see how many digits we have already in our total by using the count
if it is equal to 1 digit, when we backspace we will set it back to the string "0"
otherwise we simply remove the last digit
xxxxxxxxxx
if total.count == 1 {
total = "0"
} else {
total.removeLast()
}
For the clear
button, it is going to be quite simple,
We can simply set the total back to "0" and leave the numPeople
and tipPct
the same.
xxxxxxxxxx
Button("Clear") {
total = "0"
}
The calculate button is not going to actually do any calculations. Instead it is going to present a second view that will handle the calculations and display them to the user. The presentation of that view is going to be done by way of a Modal Sheet that pops up part way from the bottom of the screen.
In order to do that, the calculate
button is going to toggle that action by simply setting a state variable to true.
This means we need to create another state property on our view that will be initialized as false.
xxxxxxxxxx
@State private var calculate = false
Then when someone taps on the button it will be set to true
.
xxxxxxxxxx
Button("Calculate") {
calculate = true
}
There is one more thing, if our total is 0 then there is nothing to clear or calculate. So we can disable both of those buttons if that is the case. We can apply a .disabled(Bool)
modifier to the HStack
containing both of those buttons and that will then get applied to those buttons. The argument for the disabled modifier is a Boolean and it will be true whenever total == "0" or "0."
We can convert the total which is a string representing a number using Double(total)
and compare it to the number 0
xxxxxxxxxx
.disabled(Double(total) == 0)