Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(iExpense): add challenges #7

Merged
merged 12 commits into from
Feb 10, 2023
44 changes: 42 additions & 2 deletions 10-iExpense/iExpense/iExpense.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
2C51729A2993FA4F00461260 /* ExpenseItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5172992993FA4F00461260 /* ExpenseItem.swift */; };
2C51729C2993FAB600461260 /* Expenses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C51729B2993FAB600461260 /* Expenses.swift */; };
2C51729E2994114800461260 /* AddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C51729D2994114800461260 /* AddView.swift */; };
2C6389F82995458D00210EA9 /* HeaderFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6389F72995458D00210EA9 /* HeaderFont.swift */; };
2C9082992995792C003FEB23 /* Extension-LocalCurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9082982995792C003FEB23 /* Extension-LocalCurrency.swift */; };
2C90829B29957A9B003FEB23 /* Extension-ExpenseStyling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C90829A29957A9B003FEB23 /* Extension-ExpenseStyling.swift */; };
2C90829F29958298003FEB23 /* ExpenseSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C90829E29958298003FEB23 /* ExpenseSection.swift */; };
2C9082A129958F90003FEB23 /* Extension-ButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9082A029958F90003FEB23 /* Extension-ButtonStyle.swift */; };
2C9082A32995900A003FEB23 /* EmptyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9082A22995900A003FEB23 /* EmptyList.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -25,6 +31,12 @@
2C5172992993FA4F00461260 /* ExpenseItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpenseItem.swift; sourceTree = "<group>"; };
2C51729B2993FAB600461260 /* Expenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Expenses.swift; sourceTree = "<group>"; };
2C51729D2994114800461260 /* AddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddView.swift; sourceTree = "<group>"; };
2C6389F72995458D00210EA9 /* HeaderFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderFont.swift; sourceTree = "<group>"; };
2C9082982995792C003FEB23 /* Extension-LocalCurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension-LocalCurrency.swift"; sourceTree = "<group>"; };
2C90829A29957A9B003FEB23 /* Extension-ExpenseStyling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension-ExpenseStyling.swift"; sourceTree = "<group>"; };
2C90829E29958298003FEB23 /* ExpenseSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpenseSection.swift; sourceTree = "<group>"; };
2C9082A029958F90003FEB23 /* Extension-ButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Extension-ButtonStyle.swift"; sourceTree = "<group>"; };
2C9082A22995900A003FEB23 /* EmptyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyList.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -59,9 +71,12 @@
children = (
2C145CAF298BF29700813CBC /* iExpenseApp.swift */,
2C145CB1298BF29700813CBC /* ContentView.swift */,
2C9082A22995900A003FEB23 /* EmptyList.swift */,
2C90829E29958298003FEB23 /* ExpenseSection.swift */,
2C6389F72995458D00210EA9 /* HeaderFont.swift */,
2C51729D2994114800461260 /* AddView.swift */,
2C5172992993FA4F00461260 /* ExpenseItem.swift */,
2C51729B2993FAB600461260 /* Expenses.swift */,
2C90829C29957CA3003FEB23 /* Extensions */,
2C90829D29957CE1003FEB23 /* Model */,
2C145CB3298BF29800813CBC /* Assets.xcassets */,
2C145CB5298BF29800813CBC /* Preview Content */,
);
Expand All @@ -76,6 +91,25 @@
path = "Preview Content";
sourceTree = "<group>";
};
2C90829C29957CA3003FEB23 /* Extensions */ = {
isa = PBXGroup;
children = (
2C9082A029958F90003FEB23 /* Extension-ButtonStyle.swift */,
2C90829A29957A9B003FEB23 /* Extension-ExpenseStyling.swift */,
2C9082982995792C003FEB23 /* Extension-LocalCurrency.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
2C90829D29957CE1003FEB23 /* Model */ = {
isa = PBXGroup;
children = (
2C5172992993FA4F00461260 /* ExpenseItem.swift */,
2C51729B2993FAB600461260 /* Expenses.swift */,
);
path = Model;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -147,10 +181,16 @@
buildActionMask = 2147483647;
files = (
2C51729C2993FAB600461260 /* Expenses.swift in Sources */,
2C90829F29958298003FEB23 /* ExpenseSection.swift in Sources */,
2C145CB2298BF29700813CBC /* ContentView.swift in Sources */,
2C51729A2993FA4F00461260 /* ExpenseItem.swift in Sources */,
2C6389F82995458D00210EA9 /* HeaderFont.swift in Sources */,
2C51729E2994114800461260 /* AddView.swift in Sources */,
2C90829B29957A9B003FEB23 /* Extension-ExpenseStyling.swift in Sources */,
2C9082A129958F90003FEB23 /* Extension-ButtonStyle.swift in Sources */,
2C9082A32995900A003FEB23 /* EmptyList.swift in Sources */,
2C145CB0298BF29700813CBC /* iExpenseApp.swift in Sources */,
2C9082992995792C003FEB23 /* Extension-LocalCurrency.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
5 changes: 5 additions & 0 deletions 10-iExpense/iExpense/iExpense/AddView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct AddView: View {
NavigationView {
Form {
TextField("Name", text: $name)
.autocorrectionDisabled(true)

Picker("Type", selection: $type) {
ForEach(types, id: \.self) {
Expand All @@ -33,6 +34,10 @@ struct AddView: View {
.navigationTitle("Add new expense")
.toolbar {
Button("Save") {
if name.isEmpty {
name = "Unknown"
}

let item = ExpenseItem(name: name, type: type, amount: amount)
expenses.items.append(item)
dismiss()
Expand Down
66 changes: 44 additions & 22 deletions 10-iExpense/iExpense/iExpense/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,33 @@ struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(expenses.items) { item in
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.type)
}

Spacer()

Text(item.amount, format: .currency(code: "USD"))
}
}
.onDelete { IndexSet in
removeItems(at: IndexSet)
// challenge 3
ExpenseSection(title: "Business", expenses: expenses.businessItems, deleteItems: removeBusinessItems)
// challenge 3
ExpenseSection(title: "Personal", expenses: expenses.personalItems, deleteItems: removePersonalItems)

if expenses.isEmpty {
EmptyList()
}
}
.navigationTitle("iExpense")
.navigationTitle("iExpense 💸")
.toolbar {
Button {
showingAddExpense = true
} label: {
Image(systemName: "plus")
ToolbarItem(placement: .primaryAction) {
Button {
showingAddExpense = true
} label: {
Image(systemName: "plus")
}
}

if expenses.isEmpty {
// only show this when list is empty
ToolbarItem(placement: .bottomBar) {
Button("Add Expenses") {
showingAddExpense = true
}
.buttonStyle(.ghost)
}
}
}
.sheet(isPresented: $showingAddExpense) {
Expand All @@ -44,8 +48,26 @@ struct ContentView: View {
}
}

func removeItems(at offsets: IndexSet) {
expenses.items.remove(atOffsets: offsets)
func removeItems(at offsets: IndexSet, from inputArray: [ExpenseItem]) {
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, from: expenses.personalItems)
}

func removeBusinessItems(at offsets: IndexSet) {
removeItems(at: offsets, from: expenses.businessItems)
}
}

Expand Down
37 changes: 37 additions & 0 deletions 10-iExpense/iExpense/iExpense/EmptyList.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Date: 2/10/23
//
// Author: Zai Santillan
// Github: @plskz


import SwiftUI

struct EmptyList: View {
var body: some View {
VStack {
Image(systemName: "cart")
.font(.system(size: 128))

Text("Your Expenses is Empty!")
.font(.system(size: 22, weight: .heavy, design: .rounded))
.padding()

Text("Tap the button to add items")
.font(.system(size: 16, weight: .medium, design: .rounded))
.foregroundColor(.secondary)

Image(systemName: "arrow.down")
.font(.system(size: 64, weight: .ultraLight, design: .rounded))
.padding(.top, 80)
}
.listRowBackground(Color.clear)
}
}

struct EmptyList_Previews: PreviewProvider {
static var previews: some View {
List {
EmptyList()
}
}
}
61 changes: 61 additions & 0 deletions 10-iExpense/iExpense/iExpense/ExpenseSection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Date: 2/10/23
//
// Author: Zai Santillan
// Github: @plskz


import SwiftUI

/// challenge 3 - For a bigger challenge, 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!
struct ExpenseSection: View {
var title: String
var expenses: [ExpenseItem]
var deleteItems: (IndexSet) -> Void

var body: some View {
Section {
ForEach(expenses) { item in
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.type)
}

Spacer()

Text(item.amount, format: .localCurrency) // challenge 1
.style(for: item) // challenge 2
}
}
.onDelete(perform: deleteItems)
} header: {
if expenses.count != 0 {
HeaderFont(title)
}
}
}
}

struct ExpenseSection_Previews: PreviewProvider {
static var previews: some View {
List {
ExpenseSection(
title: "Business",
expenses: [
ExpenseItem(name: "Macbook Pro", type: "Business", amount: 1200),
ExpenseItem(name: "Uber", type: "Business", amount: 21.59)
]) { _ in }

ExpenseSection(
title: "Personal",
expenses: [
ExpenseItem(name: "Breakfast", type: "Personal", amount: 10),
ExpenseItem(name: "Lunch", type: "Personal", amount: 12),
ExpenseItem(name: "Gift", type: "Personal", amount: 500)
]) { _ in }
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Date: 2/10/23
//
// Author: Zai Santillan
// Github: @plskz


import SwiftUI

struct GhostButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.foregroundStyle(.tint)
.background(
RoundedRectangle(
cornerRadius: 20,
style: .continuous
)
.stroke(.tint, lineWidth: 2)
)
}
}

extension ButtonStyle where Self == GhostButtonStyle {
static var ghost: Self {
return .init()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Date: 2/10/23
//
// Author: Zai Santillan
// Github: @plskz


import SwiftUI

extension View {
/// challenge 2 - 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
/// - expenses over $100 a third style.
func style(for item: ExpenseItem) -> some View {
item.amount <= 10 ? self.foregroundColor(.green) : item.amount <= 100 ? self.foregroundColor(.yellow) : self.foregroundColor(.red)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Date: 2/10/23
//
// Author: Zai Santillan
// Github: @plskz


import Foundation

extension FormatStyle where Self == FloatingPointFormatStyle<Double>.Currency {
/// challenge 1: I'll use locale currency instead of user's preferred currency ^,..,^
static var localCurrency: Self {
.currency(code: Locale.current.currency?.identifier ?? "USD")
}
}
36 changes: 36 additions & 0 deletions 10-iExpense/iExpense/iExpense/HeaderFont.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Date: 2/9/23
//
// Author: Zai Santillan
// Github: @plskz


import SwiftUI

struct HeaderFont: View {
let types = ["Business", "Personal"]
var headerTitle: String

init(_ headerTitle: String) {
self.headerTitle = headerTitle
}

var body: some View {
Text("\(headerTitle) \(headerTitle == "Personal" ? "🛍️" : "💼")")
.font(.title2.bold())
.foregroundColor(.blue)
.textCase(nil)
}
}

struct HeaderFont_Previews: PreviewProvider {
static let types = ["Business", "Personal"]

static var previews: some View {
VStack {
ForEach(types, id: \.self) { type in
HeaderFont(type)
}
.padding()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import Foundation

struct ExpenseItem: Identifiable, Codable {
struct ExpenseItem: Identifiable, Codable, Equatable {
var id = UUID()
let name: String
let type: String
Expand Down
Loading