Add Firebase Crashlytics for error reporting

This commit is contained in:
William Brawner 2022-09-09 18:15:51 -06:00
parent 6896b7d9e6
commit 8c1239926b
6 changed files with 267 additions and 6 deletions

View file

@ -39,6 +39,10 @@
801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */; };
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */; };
802161D0277647920075761A /* AsyncObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 802161CF277647920075761A /* AsyncObservableObject.swift */; };
8043704028CBEC6800F229F9 /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 8043703F28CBEC6800F229F9 /* FirebaseAnalyticsWithoutAdIdSupport */; };
8043704228CBEC6800F229F9 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 8043704128CBEC6800F229F9 /* FirebaseCrashlytics */; };
8043704528CBEDF400F229F9 /* ErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8043704428CBEDF400F229F9 /* ErrorReporter.swift */; };
8043704728CBF16600F229F9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8043704628CBF16600F229F9 /* GoogleService-Info.plist */; };
8043EB84271F26ED00498E73 /* CategoryDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */; };
8044BA3927828E9D009A78D4 /* CategoryDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8044BA3827828E9D009A78D4 /* CategoryDataStore.swift */; };
8044BA3B2784B659009A78D4 /* TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8044BA3A2784B659009A78D4 /* TransactionDetails.swift */; };
@ -136,6 +140,8 @@
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDetailsView.swift; sourceTree = "<group>"; };
802161CF277647920075761A /* AsyncObservableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncObservableObject.swift; sourceTree = "<group>"; };
8021EFAB280A0FA100043F18 /* TwigsCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TwigsCore; path = ../TwigsCore; sourceTree = "<group>"; };
8043704428CBEDF400F229F9 /* ErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorReporter.swift; sourceTree = "<group>"; };
8043704628CBF16600F229F9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../Downloads/GoogleService-Info.plist"; sourceTree = "<group>"; };
8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailsView.swift; sourceTree = "<group>"; };
8044BA3827828E9D009A78D4 /* CategoryDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDataStore.swift; sourceTree = "<group>"; };
8044BA3A2784B659009A78D4 /* TransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetails.swift; sourceTree = "<group>"; };
@ -168,7 +174,9 @@
files = (
80D06DD1288C817800B50467 /* TwigsCore in Frameworks */,
8076A8522809FE99006B9DC9 /* Collections in Frameworks */,
8043704228CBEC6800F229F9 /* FirebaseCrashlytics in Frameworks */,
8076A84F2809FE8E006B9DC9 /* ArgumentParser in Frameworks */,
8043704028CBEC6800F229F9 /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -251,13 +259,13 @@
28AC94E1233C373900BFB70A = {
isa = PBXGroup;
children = (
8005FD59277E623900E48B23 /* Frameworks */,
80DBED432774AE4F00CB0A88 /* Packages */,
80A419EB2787C0A00090C515 /* twigs-cli */,
28AC94EB233C373900BFB70A /* Products */,
28AC94EC233C373900BFB70A /* Twigs */,
80A419EB2787C0A00090C515 /* twigs-cli */,
28AC9503233C373A00BFB70A /* TwigsTests */,
28AC950E233C373A00BFB70A /* TwigsUITests */,
8005FD59277E623900E48B23 /* Frameworks */,
);
sourceTree = "<group>";
};
@ -275,6 +283,7 @@
28AC94EC233C373900BFB70A /* Twigs */ = {
isa = PBXGroup;
children = (
8043704628CBF16600F229F9 /* GoogleService-Info.plist */,
282126A4235BCB7500072D52 /* Twigs.entitlements */,
28AC94FB233C373A00BFB70A /* Info.plist */,
28CE8B9423525F990072BC4C /* Extensions.swift */,
@ -298,6 +307,7 @@
802161CF277647920075761A /* AsyncObservableObject.swift */,
800DFC2B277FF47A00EDCE9B /* AsyncData.swift */,
80D2CE192833448500EDD6C2 /* DataStore.swift */,
8043704428CBEDF400F229F9 /* ErrorReporter.swift */,
);
path = Twigs;
sourceTree = "<group>";
@ -404,6 +414,7 @@
28AC94E6233C373900BFB70A /* Sources */,
28AC94E7233C373900BFB70A /* Frameworks */,
28AC94E8233C373900BFB70A /* Resources */,
8043704328CBED4000F229F9 /* ShellScript */,
);
buildRules = (
);
@ -414,6 +425,8 @@
8076A84E2809FE8E006B9DC9 /* ArgumentParser */,
8076A8512809FE99006B9DC9 /* Collections */,
80D06DD0288C817800B50467 /* TwigsCore */,
8043703F28CBEC6800F229F9 /* FirebaseAnalyticsWithoutAdIdSupport */,
8043704128CBEC6800F229F9 /* FirebaseCrashlytics */,
);
productName = Budget;
productReference = 28AC94EA233C373900BFB70A /* Twigs.app */;
@ -517,6 +530,7 @@
8076A84A2809FE56006B9DC9 /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
8076A84D2809FE8D006B9DC9 /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
8076A8502809FE99006B9DC9 /* XCRemoteSwiftPackageReference "swift-collections" */,
8043703E28CBEC6800F229F9 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
productRefGroup = 28AC94EB233C373900BFB70A /* Products */;
projectDirPath = "";
@ -537,6 +551,7 @@
files = (
28AC94FA233C373A00BFB70A /* LaunchScreen.storyboard in Resources */,
28AC951F233C381C00BFB70A /* Localizable.strings in Resources */,
8043704728CBF16600F229F9 /* GoogleService-Info.plist in Resources */,
28AC94F7233C373A00BFB70A /* Preview Assets.xcassets in Resources */,
28AC94F4233C373A00BFB70A /* Assets.xcassets in Resources */,
);
@ -558,6 +573,28 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
8043704328CBED4000F229F9 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}",
"$(SRCROOT)/newInputFile",
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
28AC94E6233C373900BFB70A /* Sources */ = {
isa = PBXSourcesBuildPhase;
@ -604,6 +641,7 @@
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */,
282126BD235CDE1400072D52 /* ProgressView.swift in Sources */,
806C7850272B700B00FA1375 /* TwigsApp.swift in Sources */,
8043704528CBEDF400F229F9 /* ErrorReporter.swift in Sources */,
808CA1A928355B30002EDD59 /* BudgetFormView.swift in Sources */,
80AF7A982835ED3B009565C6 /* RecurringTransactionFormView.swift in Sources */,
28CE8B9523525F990072BC4C /* Extensions.swift in Sources */,
@ -800,7 +838,8 @@
CODE_SIGN_ENTITLEMENTS = Twigs/Twigs.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
CURRENT_PROJECT_VERSION = 19;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_ASSET_PATHS = "\"Twigs/Preview Content\"";
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
@ -829,7 +868,7 @@
CODE_SIGN_ENTITLEMENTS = Twigs/Twigs.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
CURRENT_PROJECT_VERSION = 19;
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
DEVELOPMENT_ASSET_PATHS = "\"Twigs/Preview Content\"";
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
@ -1014,6 +1053,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
8043703E28CBEC6800F229F9 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 9.0.0;
};
};
8076A84A2809FE56006B9DC9 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-argument-parser.git";
@ -1057,6 +1104,16 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
8043703F28CBEC6800F229F9 /* FirebaseAnalyticsWithoutAdIdSupport */ = {
isa = XCSwiftPackageProductDependency;
package = 8043703E28CBEC6800F229F9 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseAnalyticsWithoutAdIdSupport;
};
8043704128CBEC6800F229F9 /* FirebaseCrashlytics */ = {
isa = XCSwiftPackageProductDependency;
package = 8043703E28CBEC6800F229F9 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseCrashlytics;
};
8076A84E2809FE8E006B9DC9 /* ArgumentParser */ = {
isa = XCSwiftPackageProductDependency;
package = 80A419F12787C13E0090C515 /* XCRemoteSwiftPackageReference "swift-argument-parser" */;

View file

@ -1,6 +1,105 @@
{
"object": {
"pins": [
{
"package": "abseil",
"repositoryURL": "https://github.com/firebase/abseil-cpp-SwiftPM.git",
"state": {
"branch": null,
"revision": "583de9bd60f66b40e78d08599cc92036c2e7e4e1",
"version": "0.20220203.2"
}
},
{
"package": "BoringSSL-GRPC",
"repositoryURL": "https://github.com/firebase/boringssl-SwiftPM.git",
"state": {
"branch": null,
"revision": "dd3eda2b05a3f459fc3073695ad1b28659066eab",
"version": "0.9.1"
}
},
{
"package": "Firebase",
"repositoryURL": "https://github.com/firebase/firebase-ios-sdk",
"state": {
"branch": null,
"revision": "7f31a43f8c49bd4a1723bc9fecdfaa4411dd9f36",
"version": "9.5.0"
}
},
{
"package": "GoogleAppMeasurement",
"repositoryURL": "https://github.com/google/GoogleAppMeasurement.git",
"state": {
"branch": null,
"revision": "f54f60d0164d887e1174fa51ab2efe48a8e9d178",
"version": "9.3.0"
}
},
{
"package": "GoogleDataTransport",
"repositoryURL": "https://github.com/google/GoogleDataTransport.git",
"state": {
"branch": null,
"revision": "5056b15c5acbb90cd214fe4d6138bdf5a740e5a8",
"version": "9.2.0"
}
},
{
"package": "GoogleUtilities",
"repositoryURL": "https://github.com/google/GoogleUtilities.git",
"state": {
"branch": null,
"revision": "f4abe56ce62a779e64b525eb133c8fc2a84bbc1f",
"version": "7.7.1"
}
},
{
"package": "gRPC",
"repositoryURL": "https://github.com/grpc/grpc-ios.git",
"state": {
"branch": null,
"revision": "8440b914756e0d26d4f4d054a1c1581daedfc5b6",
"version": "1.44.3-grpc"
}
},
{
"package": "GTMSessionFetcher",
"repositoryURL": "https://github.com/google/gtm-session-fetcher.git",
"state": {
"branch": null,
"revision": "19605024d59eaefdb1f6a2cb11ebe75df4421126",
"version": "2.0.0"
}
},
{
"package": "leveldb",
"repositoryURL": "https://github.com/firebase/leveldb.git",
"state": {
"branch": null,
"revision": "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
"version": "1.22.2"
}
},
{
"package": "nanopb",
"repositoryURL": "https://github.com/firebase/nanopb.git",
"state": {
"branch": null,
"revision": "819d0a2173aff699fb8c364b6fb906f7cdb1a692",
"version": "2.30909.0"
}
},
{
"package": "Promises",
"repositoryURL": "https://github.com/google/promises.git",
"state": {
"branch": null,
"revision": "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb",
"version": "2.1.1"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
@ -18,6 +117,15 @@
"revision": "48254824bb4248676bf7ce56014ff57b142b77eb",
"version": "1.0.2"
}
},
{
"package": "SwiftProtobuf",
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": {
"branch": null,
"revision": "b8230909dedc640294d7324d37f4c91ad3dcf177",
"version": "1.20.1"
}
}
]
},

View file

@ -16,6 +16,7 @@ private let LAST_BUDGET = "LAST_BUDGET"
@MainActor
class DataStore : ObservableObject {
let apiService: TwigsApiService
let errorReporter: ErrorReporter
@Published var budgets: AsyncData<[Budget]> = .empty
@Published var budget: AsyncData<Budget> = .empty {
didSet {
@ -66,9 +67,11 @@ class DataStore : ObservableObject {
}
init(
_ apiService: TwigsApiService
_ apiService: TwigsApiService,
errorReporter: ErrorReporter = LoggingErrorReporter()
) {
self.apiService = apiService
self.errorReporter = errorReporter
self.baseUrl = UserDefaults.standard.string(forKey: KEY_BASE_URL) ?? ""
self.apiService.baseUrl = baseUrl
self.token = UserDefaults.standard.string(forKey: KEY_TOKEN)
@ -95,6 +98,7 @@ class DataStore : ObservableObject {
}
}
} catch {
self.errorReporter.reportError(error: error)
self.budgets = .error(error)
showBudgetSelection = true
}
@ -120,6 +124,7 @@ class DataStore : ObservableObject {
self.budgets = .success(updatedBudgets.sorted(by: { $0.name < $1.name }))
}
} catch {
self.errorReporter.reportError(error: error)
self.budget = .error(error, budget)
}
}
@ -136,6 +141,7 @@ class DataStore : ObservableObject {
self.budgets = .success(budgets.filter(withoutId: budget.id))
}
} catch {
self.errorReporter.reportError(error: error)
self.budget = .error(error, budget)
}
}
@ -188,6 +194,7 @@ class DataStore : ObservableObject {
}
self.overview = .success(budgetOverview)
} catch {
self.errorReporter.reportError(error: error)
self.overview = .error(error)
}
}
@ -241,6 +248,7 @@ class DataStore : ObservableObject {
let categories = try await apiService.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page)
self.categories = .success(categories)
} catch {
self.errorReporter.reportError(error: error)
self.categories = .error(error)
}
}
@ -262,6 +270,7 @@ class DataStore : ObservableObject {
self.categories = .success(updatedCategories.sorted(by: { $0.title < $1.title }))
}
} catch {
self.errorReporter.reportError(error: error)
self.category = .error(error, category)
}
}
@ -276,6 +285,7 @@ class DataStore : ObservableObject {
self.categories = .success(categories.filter(withoutId: category.id))
}
} catch {
self.errorReporter.reportError(error: error)
self.category = .error(error, category)
}
}
@ -336,6 +346,7 @@ class DataStore : ObservableObject {
}
self.recurringTransactions = .success(recurringTransactions)
} catch {
self.errorReporter.reportError(error: error)
self.recurringTransactions = .error(error)
}
}
@ -381,6 +392,7 @@ class DataStore : ObservableObject {
}
await self.getRecurringTransactions()
} catch {
self.errorReporter.reportError(error: error)
self.recurringTransactions = .error(error)
}
}
@ -392,6 +404,7 @@ class DataStore : ObservableObject {
self.recurringTransaction = .empty
await self.getRecurringTransactions()
} catch {
self.errorReporter.reportError(error: error)
self.recurringTransaction = .error(error, transaction)
}
}
@ -436,6 +449,7 @@ class DataStore : ObservableObject {
let groupedTransactions = OrderedDictionary<String,[Transaction]>(grouping: transactions, by: { $0.date.toLocaleString() })
self.transactions = .success(groupedTransactions)
} catch {
self.errorReporter.reportError(error: error)
self.transactions = .error(error)
}
}
@ -456,6 +470,7 @@ class DataStore : ObservableObject {
}
await getTransactions()
} catch {
self.errorReporter.reportError(error: error)
self.transaction = .error(error, transaction)
}
}
@ -467,6 +482,7 @@ class DataStore : ObservableObject {
self.transaction = .empty
await getTransactions()
} catch {
self.errorReporter.reportError(error: error)
self.transaction = .error(error, transaction)
}
}
@ -555,6 +571,7 @@ class DataStore : ObservableObject {
self.userId = response.userId
await self.loadProfile()
} catch {
self.errorReporter.reportError(error: error)
switch error {
case NetworkError.jsonParsingFailed(let jsonError):
print(jsonError.localizedDescription)
@ -589,6 +606,7 @@ class DataStore : ObservableObject {
do {
_ = try await apiService.register(username: username, email: email, password: password)
} catch {
self.errorReporter.reportError(error: error)
switch error {
case NetworkError.jsonParsingFailed(let jsonError):
print(jsonError.localizedDescription)
@ -630,6 +648,7 @@ class DataStore : ObservableObject {
self.currentUser = .success(user)
await getBudgets()
} catch {
self.errorReporter.reportError(error: error)
self.currentUser = .error(error)
}
}
@ -644,6 +663,7 @@ class DataStore : ObservableObject {
self.currentUser = .success(updated)
return nil
} catch {
self.errorReporter.reportError(error: error)
self.currentUser = .error(error, current)
return .unavailable
}
@ -662,6 +682,7 @@ class DataStore : ObservableObject {
self.currentUser = .success(updated)
return nil
} catch {
self.errorReporter.reportError(error: error)
self.currentUser = .error(error, current)
return .unavailable
}
@ -680,6 +701,7 @@ class DataStore : ObservableObject {
self.currentUser = .success(updated)
return nil
} catch {
self.errorReporter.reportError(error: error)
self.currentUser = .error(error, current)
return .unknown
}
@ -695,6 +717,7 @@ class DataStore : ObservableObject {
self.currentUser = .success(updated)
return true
} catch {
self.errorReporter.reportError(error: error)
self.currentUser = .error(error, current)
return false
}
@ -707,6 +730,7 @@ class DataStore : ObservableObject {
let user = try await self.apiService.getUser(id)
self.user = .success(user)
} catch {
self.errorReporter.reportError(error: error)
self.currentUser = .error(error)
}
}

26
Twigs/ErrorReporter.swift Normal file
View file

@ -0,0 +1,26 @@
//
// ErrorReporter.swift
// Twigs
//
// Created by William Brawner on 9/9/22.
// Copyright © 2022 William Brawner. All rights reserved.
//
import Foundation
import FirebaseCrashlytics
protocol ErrorReporter {
func reportError(error: Error)
}
class LoggingErrorReporter: ErrorReporter {
func reportError(error: Error) {
print(error)
}
}
class FirebaseErrorReporter: ErrorReporter {
func reportError(error: Error) {
Crashlytics.crashlytics().record(error: error)
}
}

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>527070722499-8qtkq1oqkveapng949atmdt7l38r75ps.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.527070722499-8qtkq1oqkveapng949atmdt7l38r75ps</string>
<key>API_KEY</key>
<string>AIzaSyDzEcz1sz65JXpujOfFrRqUG2kD_tooA50</string>
<key>GCM_SENDER_ID</key>
<string>527070722499</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.wbrawner.projects.budget.ios</string>
<key>PROJECT_ID</key>
<string>budget-c7da5</string>
<key>STORAGE_BUCKET</key>
<string>budget-c7da5.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:527070722499:ios:0e0c4e20d30697df73e576</string>
<key>DATABASE_URL</key>
<string>https://budget-c7da5.firebaseio.com</string>
</dict>
</plist>

View file

@ -6,16 +6,26 @@
// Copyright © 2021 William Brawner. All rights reserved.
//
import FirebaseCore
import SwiftUI
import TwigsCore
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
struct MainView: View {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@StateObject var dataStore: DataStore
let apiService: TwigsApiService
init(_ apiService: TwigsApiService) {
self.apiService = apiService
self._dataStore = StateObject(wrappedValue: DataStore(apiService))
self._dataStore = StateObject(wrappedValue: DataStore(apiService, errorReporter: FirebaseErrorReporter()))
}
@ViewBuilder