We’ve arrived at day 38 of the 100 Days of SwiftUI! Yesterday, we created our iExpense app from scratch. Today, we’re wrapping up this project and face a few challenges. Let’s dive in!
iExpense SwiftUI challenge #1
Our first challenge is to use the user’s preferred currency, rather than always using US dollars. Looking at our code, we specify the users currency in two places, our ContentView
and AddView
. In order to use users default currency, we have to change this line:
TextField("Amount", value: $amount, format: .currency(code: "USD"))
To this:
TextField("Amount", value: $amount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
We try using the preferred currency and if it’s unavailable, we nil coalesce to USD. Easy does it, right? Well, yes! But, we can also make this a bit more elegant.
We can modify Swift’s FormatStyle
protocol and change how we use the currency formatter.
//
// FormatStyle-LocalCurrency.swift
// iExpense
//
import Foundation
extension FormatStyle where Self == FloatingPointFormatStyle<Double>.Currency {
static var localCurrency: Self {
.currency(code: Locale.current.currencyCode ?? "USD")
}
}
What we’re saying here is that whenever the FloatingPointFormatStyle
function is called with a Double
and Currency
, use this code.
To implement this in any of our views, all we have to do is call .localCurrency
when using the formatter
:
TextField("Amount", value: $amount, format: .localCurrency)
iExpense SwiftUI challenge #2
The second challenge is to modify the expense amounts in ContentView
to contain some styling depending on their value – expenses under $10 should have one style, expenses under $100 another, and expenses over $100 a third style.
There are a tons of modifications we could apply, but to keep our UI nice and clean, I just went with simple colors for our amounts.
Text(item.amount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
.foregroundColor(item.amount < 10 ? .green : item.amount < 100 ? .indigo : .red)
I find that the ternary operator is an elegant solution now that I have a good grasp on how to implement it.
However, we could also make a view extension.
struct ExpenseStyle: ViewModifier { // create a struct that follows the ViewModifier protocol
let expenseItem: ExpenseItem // create an instance of an ExpenseItem
func body(content: Content) -> some View {
switch expenseItem.amount { // Switch statement to control which modifiers are shown and when
case 0..<10:
content
.foregroundColor(.green)
case 10..<100:
content
.foregroundColor(.blue)
default:
content
.font(.headline)
.foregroundColor(.red)
}
}
}
extension View { // creates an extension to the View protocol
func expenseStyle(for expenseItem: ExpenseItem) -> some View {
modifier(ExpenseStyle(expenseItem: expenseItem))
}
}
iExpense SwiftUI challenge #3
Our third and final challenge was more difficult. We had to try splitting the expenses list into two sections: one for personal expenses, and one for business expenses. This is tricky for a few reasons, not least because it means being careful about how items are deleted!
// Expenses.swift
var personalItems: [ExpenseItem] {
items.filter { $0.type == "Personal" }
}
var businessItems: [ExpenseItem] {
items.filter { $0.type == "Business" }
}
// ExpenseItem.swift
struct ExpenseItem: Identifiable, Codable, Equatable { // Represents a single expense, Identifiable means that it can be identified
var id = UUID() // Generates a unique ID for each instance of ExpenseItem
let name: String
let type: String
let amount: Double
}
//
// ExpenseSection.swift
// iExpense
//
import SwiftUI
struct ExpenseSection: View {
let title: String
let expenses: [ExpenseItem]
let deleteItems: (IndexSet) -> Void
var body: some View {
Section(title) {
ForEach(expenses) { item in // Loop through our items in the expenses class instance
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.type)
}
Spacer()
Text(item.amount, format: .localCurrency)
.expenseStyle(for: item)
// .foregroundColor(item.amount < 10 ? .green : item.amount < 100 ? .indigo : .red)
// .font(item.amount < 10 ? .title3 : item.amount < 100 ? .title2 : .title)
}
}
.onDelete(perform: deleteItems) // Used to delete a row
}
}
}
struct ExpenseSection_Previews: PreviewProvider {
static var previews: some View {
ExpenseSection(title: "Example", expenses: [], deleteItems: { _ in })
}
}
//
// ContentView.swift
// iExpense
//
import SwiftUI
struct ExpenseStyle: ViewModifier { // create a struct that follows the ViewModifier protocol
let expenseItem: ExpenseItem // create an instance of an ExpenseItem
func body(content: Content) -> some View {
switch expenseItem.amount { // Switch statement to control which modifiers are shown and when
case 0..<10:
content
.foregroundColor(.green)
case 10..<100:
content
.foregroundColor(.blue)
default:
content
.font(.headline)
.foregroundColor(.red)
}
}
}
extension View { // creates an extension to the View protocol
func expenseStyle(for expenseItem: ExpenseItem) -> some View {
modifier(ExpenseStyle(expenseItem: expenseItem))
}
}
struct ContentView: View {
@StateObject var expenses = Expenses() // create a new instance of our Expenses class
@State private var showingAddExpense = false // Used to track whether our sheet containing our AddView view should be sown or not
var body: some View {
NavigationView {
List {
ExpenseSection(title: "Business", expenses: expenses.businessItems, deleteItems: removeBusinessItems)
ExpenseSection(title: "Personal", expenses: expenses.personalItems, deleteItems: removePersonalItems)
}
.navigationTitle("iExpense")
.toolbar{
Button {
showingAddExpense = true // If the button is pressed, a sheet is presented with our AddView view
} label: {
Image(systemName: "plus")
}
}
.sheet(isPresented: $showingAddExpense) { // presents a sheet when showingAddExpense = true
AddView(expenses: expenses) // shows our AddView view and passes in our instance of the expenses class
}
}
}
func removeItems(at offsets: IndexSet, in inputArray: [ExpenseItem]) { // function used to delete items in our list
var objectsToDelete = IndexSet()
for offset in offsets {
let item = inputArray[offset]
if let index = expenses.items.firstIndex(of: item) {
objectsToDelete.insert(index)
}
}
expenses.items.remove(atOffsets: objectsToDelete)
}
func removePersonalItems(at offsets: IndexSet) {
removeItems(at: offsets, in: expenses.personalItems)
}
func removeBusinessItems(at offsets: IndexSet) {
removeItems(at: offsets, in: expenses.businessItems)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Wrap up
That’s it for day 38 and the iExpense project! We’ll be starting the next project tomorrow, so it’s time to recharge!
100 Days of SwiftUI – Day 38