Use Swift Charts for budget overview actual vs expected

This commit is contained in:
William Brawner 2023-02-08 20:17:54 -07:00
parent eb50266745
commit 6af0fda86f
7 changed files with 100 additions and 96 deletions

View file

@ -492,7 +492,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1340;
LastUpgradeCheck = 1420;
ORGANIZATIONNAME = "William Brawner";
TargetAttributes = {
28AC94E9233C373900BFB70A = {
@ -757,7 +757,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -814,7 +814,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "";
@ -834,22 +834,27 @@
CODE_SIGN_ENTITLEMENTS = Twigs/Twigs.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 19;
CURRENT_PROJECT_VERSION = 22;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_ASSET_PATHS = "\"Twigs/Preview Content\"";
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Twigs/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 15.0;
INFOPLIST_KEY_CFBundleDisplayName = Twigs;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.wbrawner.projects.budget.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
};
@ -864,21 +869,26 @@
CODE_SIGN_ENTITLEMENTS = Twigs/Twigs.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 19;
CURRENT_PROJECT_VERSION = 22;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_ASSET_PATHS = "\"Twigs/Preview Content\"";
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Twigs/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 15.0;
INFOPLIST_KEY_CFBundleDisplayName = Twigs;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.wbrawner.projects.budget.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6";
};
@ -892,7 +902,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
INFOPLIST_FILE = TwigsTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -902,7 +912,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Twigs.app/Twigs";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Twigs.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Twigs";
};
name = Debug;
};
@ -914,7 +924,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
INFOPLIST_FILE = TwigsTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -924,7 +934,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Twigs.app/Twigs";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Twigs.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Twigs";
};
name = Release;
};
@ -942,9 +952,10 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.wbrawner.BudgetUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = Budget;
TEST_TARGET_NAME = Twigs;
};
name = Debug;
};
@ -962,9 +973,10 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.wbrawner.BudgetUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = Budget;
TEST_TARGET_NAME = Twigs;
};
name = Release;
};
@ -974,7 +986,8 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = VJ33S6H7W7;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 12.1;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -989,7 +1002,8 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = VJ33S6H7W7;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
ENABLE_HARDENED_RUNTIME = YES;
MACOSX_DEPLOYMENT_TARGET = 12.1;
PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -6,6 +6,7 @@
// Copyright © 2019 William Brawner. All rights reserved.
//
import Charts
import SwiftUI
import TwigsCore
@ -21,14 +22,50 @@ struct BudgetDetailsView: View {
errorTextLocalizedStringKey: "budgets_load_failure"
) { overview in
List {
if let description = overview.budget.description {
Section(overview.budget.name) {
DescriptionOverview(overview: overview)
Text(description)
}
Section("income") {
IncomeOverview(overview: overview)
}
Section("expenses") {
ExpensesOverview(overview: overview)
Section("stats") {
HStack {
VStack(alignment: .center) {
Text("cash_flow")
.font(.caption)
Text(verbatim: overview.balance.toCurrencyString())
.foregroundColor(overview.balance < 0 ? .red : .green)
.font(.title2)
}
Spacer()
VStack(alignment: .center) {
Text("transactions")
.font(.caption)
Text(verbatim: String(overview.transactionCount))
.font(.title2)
}
}
}
Section("expected_vs_actual") {
Chart {
BarMark(
x: .value("Amount", overview.expectedIncome),
y: .value("Label", Bundle.main.localizedString(forKey: "expected_income", value: nil, table: nil))
).foregroundStyle(Color.gray)
BarMark(
x: .value("Amount", overview.actualIncome),
y: .value("Label", Bundle.main.localizedString(forKey: "actual_income", value: nil, table: nil))
).foregroundStyle(Color.green)
BarMark(
x: .value("Amount", overview.expectedExpenses),
y: .value("Label", Bundle.main.localizedString(forKey: "expected_expenses", value: nil, table: nil))
).foregroundStyle(Color.gray)
BarMark(
x: .value("Amount", overview.actualExpenses),
y: .value("Label", Bundle.main.localizedString(forKey: "actual_expenses", value: nil, table: nil))
).foregroundStyle(Color.red)
}
}
}
.listStyle(.insetGrouped)
@ -61,63 +98,6 @@ struct BudgetDetailsView: View {
}
}
struct DescriptionOverview: View {
let overview: BudgetOverview
var body: some View {
VStack(alignment: .leading) {
if let description = overview.budget.description {
Text(description)
}
HStack {
Text("current_balance")
Text(verbatim: overview.balance.toCurrencyString())
.foregroundColor(overview.balance < 0 ? .red : .green)
}
}
}
}
struct IncomeOverview: View {
let overview: BudgetOverview
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("expected")
Text(verbatim: overview.expectedIncome.toCurrencyString())
}
ProgressView(value: Float(overview.expectedIncome), maxValue: overview.maxValue, progressTintColor: .gray, progressBarHeight: 10.0, progressBarCornerRadius: 4.0)
HStack {
Text("actual")
Text(verbatim: overview.actualIncome.toCurrencyString())
.foregroundColor(.green)
}
ProgressView(value: Float(overview.actualIncome), maxValue: overview.maxValue, progressTintColor: .green, progressBarHeight: 10.0, progressBarCornerRadius: 4.0)
}
}
}
struct ExpensesOverview: View {
let overview: BudgetOverview
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("expected")
Text(verbatim: overview.expectedExpenses.toCurrencyString())
}
ProgressView(value: Float(overview.expectedExpenses), maxValue: overview.maxValue, progressTintColor: .gray, progressBarHeight: 10.0, progressBarCornerRadius: 4.0)
HStack {
Text("actual")
Text(verbatim: overview.actualExpenses.toCurrencyString())
.foregroundColor(.red)
}
ProgressView(value: Float(overview.actualExpenses), maxValue: overview.maxValue, progressTintColor: .red, progressBarHeight: 10.0, progressBarCornerRadius: 4.0)
}
}
}
#if DEBUG
struct BudgetDetailsView_Previews: PreviewProvider {
static var previews: some View {

View file

@ -178,7 +178,12 @@ class DataStore : ObservableObject {
do {
let budgetBalance = try await self.apiService.sumTransactions(budgetId: budget.id, categoryId: nil, from: nil, to: nil)
let categories = try await self.apiService.getCategories(budgetId: budget.id, expense: nil, archived: false, count: nil, page: nil)
var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance)
await self.getTransactions(showLoader: false)
var transactionCount = 0
if case let .success(transactions) = self.transactions {
transactionCount = transactions.count
}
var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance, categories: categories, transactionCount: transactionCount)
try await withThrowingTaskGroup(of: (TwigsCore.Category, BalanceResponse).self) { group in
for category in categories {
group.addTask {
@ -188,15 +193,15 @@ class DataStore : ObservableObject {
for try await (category, response) in group {
if category.expense {
budgetOverview.expectedExpenses += category.amount
budgetOverview.expectedExpenses += Float(category.amount) / 100.0
} else {
budgetOverview.expectedIncome += category.amount
budgetOverview.expectedIncome += Float(category.amount) / 100.0
}
if category.expense {
budgetOverview.actualExpenses += abs(response.balance)
budgetOverview.actualExpenses += Float(abs(response.balance)) / 100.0
} else {
budgetOverview.actualIncome += response.balance
budgetOverview.actualIncome += Float(response.balance) / 100.0
}
}
}

View file

@ -66,10 +66,13 @@
"budgets" = "Budgets";
"new_budget" = "New Budget";
"current_balance" = "Current Balance:";
"expected" = "Expected:";
"actual" = "Actual:";
"income" = "Income:";
"expenses" = "Expenses:";
"stats" = "Stats";
"cash_flow" = "Cash Flow";
"expected_vs_actual" = "Expected vs Actual";
"expected_income" = "Expected Income";
"expected_expenses" = "Expected Expenses";
"actual_income" = "Actual Income";
"actual_expenses" = "Actual Expenses";
// MARK: Profile
"profile" = "Profile";

View file

@ -65,11 +65,13 @@
"budget" = "Presupuesto";
"budgets" = "Presupuestos";
"new_budget" = "Nuevo Presupuesto";
"current_balance" = "Saldo Actual:";
"income" = "Ingresos:";
"expenses" = "Gastos:";
"expected" = "Planeados:";
"actual" = "Actuales:";
"stats" = "Estadísticas";
"cash_flow" = "Flujo de dinero";
"expected_vs_actual" = "Planeado vs Actual";
"expected_income" = "Ingresos Planeados";
"expected_expenses" = "Gastos Planeados";
"actual_income" = "Ingresos Actuales";
"actual_expenses" = "Gastos Actuales";
// MARK: Profile