100 Days of SwiftUI Day 38

100 Days of SwiftUI – Day 38

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!

Darryl

Hi! My name is Darryl and this is my personal blog where I write about my journey as I learn programming! You'll also find articles about other things that interest me including games, tech and anime.

Post navigation

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

100 Days of SwiftUI – Day 56

100 Days of SwiftUI – Day 8 – Functions Part 2

100 Days of SwiftUI – Day 58

100 Days of SwiftUI – Day 92