From f49d189fe0b3acc04706d50425c80ba4581a8e28 Mon Sep 17 00:00:00 2001 From: Max Tharr Date: Fri, 12 Nov 2021 14:39:38 +0100 Subject: [PATCH 01/11] Update package dependecies --- PayForMe.xcodeproj/project.pbxproj | 16 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- PayForMe/Info.plist | 2 +- PayForMeTests/Info.plist | 2 +- PayForMeUITests/Info.plist | 2 +- fastlane/README.md | 2 +- fastlane/SnapshotHelper.swift | 36 +- fastlane/screenshots/screenshots.html | 319 ------------------ 8 files changed, 38 insertions(+), 345 deletions(-) diff --git a/PayForMe.xcodeproj/project.pbxproj b/PayForMe.xcodeproj/project.pbxproj index 7320ed8..76e172b 100644 --- a/PayForMe.xcodeproj/project.pbxproj +++ b/PayForMe.xcodeproj/project.pbxproj @@ -716,7 +716,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -771,7 +771,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.7; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -787,17 +787,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = PayForMe/PayForMe.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 89; + CURRENT_PROJECT_VERSION = 92; DEVELOPMENT_ASSET_PATHS = "\"PayForMe/Preview Content\""; DEVELOPMENT_TEAM = L79BTFY6FV; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = PayForMe/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.7; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; PRODUCT_BUNDLE_IDENTIFIER = de.mayflower.PayForMe; PRODUCT_NAME = PayForMe; SWIFT_VERSION = 5.0; @@ -811,17 +811,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = PayForMe/PayForMe.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 89; + CURRENT_PROJECT_VERSION = 92; DEVELOPMENT_ASSET_PATHS = "\"PayForMe/Preview Content\""; DEVELOPMENT_TEAM = L79BTFY6FV; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = PayForMe/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.7; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.1; + MARKETING_VERSION = 2.3.2; PRODUCT_BUNDLE_IDENTIFIER = de.mayflower.PayForMe; PRODUCT_NAME = PayForMe; SWIFT_VERSION = 5.0; diff --git a/PayForMe.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PayForMe.xcworkspace/xcshareddata/swiftpm/Package.resolved index aa583ca..6552439 100644 --- a/PayForMe.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PayForMe.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -2,7 +2,7 @@ "object": { "pins": [ { - "package": "CarBode-Barcode-Scanner-For-SwiftUI", + "package": "CarBode", "repositoryURL": "https://github.com/heart/CarBode-Barcode-Scanner-For-SwiftUI", "state": { "branch": null, @@ -11,7 +11,7 @@ } }, { - "package": "GRDB.swift", + "package": "GRDB", "repositoryURL": "https://github.com/groue/GRDB.swift.git", "state": { "branch": null, diff --git a/PayForMe/Info.plist b/PayForMe/Info.plist index aea01dc..26ae0bb 100644 --- a/PayForMe/Info.plist +++ b/PayForMe/Info.plist @@ -32,7 +32,7 @@ CFBundleVersion - 89 + 92 LSRequiresIPhoneOS NSCameraUsageDescription diff --git a/PayForMeTests/Info.plist b/PayForMeTests/Info.plist index 141aa66..1ab6e24 100644 --- a/PayForMeTests/Info.plist +++ b/PayForMeTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 89 + 92 diff --git a/PayForMeUITests/Info.plist b/PayForMeUITests/Info.plist index 172842f..df85f93 100644 --- a/PayForMeUITests/Info.plist +++ b/PayForMeUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 89 + 92 diff --git a/fastlane/README.md b/fastlane/README.md index 36f37b7..c02819e 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -49,6 +49,6 @@ fastlane ios upload ---- -This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/SnapshotHelper.swift b/fastlane/SnapshotHelper.swift index 1f12573..015cfea 100644 --- a/fastlane/SnapshotHelper.swift +++ b/fastlane/SnapshotHelper.swift @@ -165,7 +165,11 @@ open class Snapshot: NSObject { } let screenshot = XCUIScreen.main.screenshot() + #if os(iOS) let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image + #else + let image = screenshot.image + #endif guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } @@ -176,7 +180,11 @@ open class Snapshot: NSObject { simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") - try image.pngData()?.write(to: path, options: .atomic) + #if swift(<5.0) + UIImagePNGRepresentation(image)?.write(to: path, options: .atomic) + #else + try image.pngData()?.write(to: path, options: .atomic) + #endif } catch let error { NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") NSLog(error.localizedDescription) @@ -185,16 +193,20 @@ open class Snapshot: NSObject { } class func fixLandscapeOrientation(image: UIImage) -> UIImage { - if #available(iOS 10.0, *) { - let format = UIGraphicsImageRendererFormat() - format.scale = image.scale - let renderer = UIGraphicsImageRenderer(size: image.size, format: format) - return renderer.image { context in - image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) - } - } else { + #if os(watchOS) return image - } + #else + if #available(iOS 10.0, *) { + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + let renderer = UIGraphicsImageRenderer(size: image.size, format: format) + return renderer.image { context in + image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + } + } else { + return image + } + #endif } class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { @@ -219,7 +231,7 @@ open class Snapshot: NSObject { #if os(OSX) let homeDir = URL(fileURLWithPath: NSHomeDirectory()) return homeDir.appendingPathComponent(cachePath) - #elseif arch(i386) || arch(x86_64) + #elseif arch(i386) || arch(x86_64) || arch(arm64) guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { throw SnapshotError.cannotFindSimulatorHomeDirectory } @@ -294,4 +306,4 @@ private extension CGFloat { // Please don't remove the lines below // They are used to detect outdated configuration files -// SnapshotHelperVersion [1.24] +// SnapshotHelperVersion [1.27] diff --git a/fastlane/screenshots/screenshots.html b/fastlane/screenshots/screenshots.html index 5de11fa..aba7df9 100644 --- a/fastlane/screenshots/screenshots.html +++ b/fastlane/screenshots/screenshots.html @@ -96,327 +96,8 @@

By Language:

-

de-DE

-
- - - - - - - - - - - -
- iPhone 8 Plus -
- - de-DE iPhone 8 Plus - - - - de-DE iPhone 8 Plus - - - - de-DE iPhone 8 Plus - - - - de-DE iPhone 8 Plus - - - - de-DE iPhone 8 Plus - -
-

en-US

-
- - - - - - - - - - - -
- iPhone 8 Plus -
- - en-US iPhone 8 Plus - - - - en-US iPhone 8 Plus - - - - en-US iPhone 8 Plus - - - - en-US iPhone 8 Plus - - - - en-US iPhone 8 Plus - -
-

es-ES

-
- - - - - - - - - - - -
- iPhone 8 Plus -
- - es-ES iPhone 8 Plus - - - - es-ES iPhone 8 Plus - - - - es-ES iPhone 8 Plus - - - - es-ES iPhone 8 Plus - - - - es-ES iPhone 8 Plus - -
-

fr-FR

-
- - - - - - - - - - - -
- iPhone 8 Plus -
- - fr-FR iPhone 8 Plus - - - - fr-FR iPhone 8 Plus - - - - fr-FR iPhone 8 Plus - - - - fr-FR iPhone 8 Plus - - - - fr-FR iPhone 8 Plus - -

By Screen:

-

Add Bill

-
- - - - - - - - - - -
- iPhone 8 Plus -
- - de-DE iPhone 8 Plus - -
de-DE
-
- - en-US iPhone 8 Plus - -
en-US
-
- - es-ES iPhone 8 Plus - -
es-ES
-
- - fr-FR iPhone 8 Plus - -
fr-FR
-
-

Balance List

-
- - - - - - - - - - -
- iPhone 8 Plus -
- - de-DE iPhone 8 Plus - -
de-DE
-
- - en-US iPhone 8 Plus - -
en-US
-
- - es-ES iPhone 8 Plus - -
es-ES
-
- - fr-FR iPhone 8 Plus - -
fr-FR
-
-

Bill List

-
- - - - - - - - - - -
- iPhone 8 Plus -
- - de-DE iPhone 8 Plus - -
de-DE
-
- - en-US iPhone 8 Plus - -
en-US
-
- - es-ES iPhone 8 Plus - -
es-ES
-
- - fr-FR iPhone 8 Plus - -
fr-FR
-
-

Known Projects

-
- - - - - - - - - - -
- iPhone 8 Plus -
- - de-DE iPhone 8 Plus - -
de-DE
-
- - en-US iPhone 8 Plus - -
en-US
-
- - es-ES iPhone 8 Plus - -
es-ES
-
- - fr-FR iPhone 8 Plus - -
fr-FR
-
-

Onboarding

-
- - - - - - - - - - -
- iPhone 8 Plus -
- - de-DE iPhone 8 Plus - -
de-DE
-
- - en-US iPhone 8 Plus - -
en-US
-
- - es-ES iPhone 8 Plus - -
es-ES
-
- - fr-FR iPhone 8 Plus - -
fr-FR
-
From d399d75ea70a728e04d233b07fec23225acc1417 Mon Sep 17 00:00:00 2001 From: Max Tharr Date: Fri, 12 Nov 2021 14:40:04 +0100 Subject: [PATCH 02/11] Bump to iOS 15 --- PayForMe.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PayForMe.xcodeproj/project.pbxproj b/PayForMe.xcodeproj/project.pbxproj index 76e172b..268bee3 100644 --- a/PayForMe.xcodeproj/project.pbxproj +++ b/PayForMe.xcodeproj/project.pbxproj @@ -716,7 +716,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.7; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -771,7 +771,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.7; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; From fdf9cebc97add3ec6c23eedd965344d43a17624b Mon Sep 17 00:00:00 2001 From: Max Tharr Date: Fri, 12 Nov 2021 17:35:27 +0100 Subject: [PATCH 03/11] Use async/await for sending bills --- PayForMe.xcodeproj/project.pbxproj | 4 +- PayForMe/Services/NetworkService.swift | 61 +++++++++++-------- PayForMe/Services/ProjectManager.swift | 50 +++++++-------- .../Views/BillDetail/BillDetailView.swift | 17 +++--- PayForMe/Views/FancyLoadingButton.swift | 17 +++--- .../Manual/AddProjectManualViewModel.swift | 2 +- .../QRCodes/AddProjectQRViewModel.swift | 4 +- 7 files changed, 81 insertions(+), 74 deletions(-) diff --git a/PayForMe.xcodeproj/project.pbxproj b/PayForMe.xcodeproj/project.pbxproj index 268bee3..a4d09fc 100644 --- a/PayForMe.xcodeproj/project.pbxproj +++ b/PayForMe.xcodeproj/project.pbxproj @@ -792,7 +792,7 @@ DEVELOPMENT_TEAM = L79BTFY6FV; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = PayForMe/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.7; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -816,7 +816,7 @@ DEVELOPMENT_TEAM = L79BTFY6FV; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = PayForMe/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.7; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/PayForMe/Services/NetworkService.swift b/PayForMe/Services/NetworkService.swift index d2d1789..39a8646 100644 --- a/PayForMe/Services/NetworkService.swift +++ b/PayForMe/Services/NetworkService.swift @@ -75,7 +75,7 @@ class NetworkService { .eraseToAnyPublisher() } - func testProject(_ project: Project) -> AnyPublisher<(Project, Int), Never> { + func foundProjectStatusCode(_ project: Project) -> AnyPublisher<(Project, Int), Never> { let request = buildURLRequest("members", params: [:], project: project) let requestPub = URLSession.shared.dataTaskPublisher(for: request) .tryMap { data, response -> Int in @@ -93,7 +93,7 @@ class NetworkService { return URLSession.shared.dataTaskPublisher(for: request) .tryMap { data, response -> Bool in guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode / 100 == 2 else { - throw HTTPError.statuscode + throw HTTPError.generalFailure } guard let responseString = String(data: data, encoding: .utf8) else { return false @@ -105,21 +105,6 @@ class NetworkService { .eraseToAnyPublisher() } - func postBillPublisher(bill: Bill) -> AnyPublisher { - let request = buildURLRequest("bills", params: bill.paramsFor(currentProject.backend), project: currentProject, httpMethod: "POST") - return sendBillPublisher(request: request) - } - - func updateBillPublisher(bill: Bill) -> AnyPublisher { - let request = buildURLRequest("bills/\(bill.id)", params: bill.paramsFor(currentProject.backend), project: currentProject, httpMethod: "PUT") - return sendBillPublisher(request: request) - } - - func deleteBillPublisher(bill: Bill) -> AnyPublisher { - let request = buildURLRequest("bills/\(bill.id)", params: [:], project: currentProject, httpMethod: "DELETE") - return sendBillPublisher(request: request) - } - private func sendBillPublisher(request: URLRequest) -> AnyPublisher { return URLSession.shared.dataTaskPublisher(for: request) @@ -133,6 +118,31 @@ class NetworkService { .eraseToAnyPublisher() } + func post(bill: Bill) async throws { + let request = buildURLRequest("bills", params: bill.paramsFor(currentProject.backend), project: currentProject, httpMethod: "POST") + try await sendWithOutResponseData(request: request) + } + + func update(bill: Bill) async throws { + let request = buildURLRequest("bills/\(bill.id)", params: bill.paramsFor(currentProject.backend), project: currentProject, httpMethod: "PUT") + try await sendWithOutResponseData(request: request) + } + + private func sendWithOutResponseData(request: URLRequest) async throws { + let (_, response) = try await URLSession.shared.data(for: request) + guard let response = response as? HTTPURLResponse else { + throw HTTPError.generalFailure + } + if response.statusCode / 100 == 2 { + throw HTTPError.statuscode(code: response.statusCode) + } + } + + func deleteBillPublisher(bill: Bill) -> AnyPublisher { + let request = buildURLRequest("bills/\(bill.id)", params: [:], project: currentProject, httpMethod: "DELETE") + return sendBillPublisher(request: request) + } + func createMemberPublisher(name: String) -> AnyPublisher { let request = buildURLRequest("members", params: ["name": name], project: currentProject, httpMethod: "POST") return sendMemberPublisher(request: request) @@ -218,12 +228,13 @@ class NetworkService { return request } - - enum HTTPError: LocalizedError { - case statuscode - } - - enum ServerError: LocalizedError { - case noIdReturned - } +} + +enum HTTPError: LocalizedError { + case statuscode(code: Int) + case generalFailure +} + +enum ServerError: LocalizedError { + case noIdReturned } diff --git a/PayForMe/Services/ProjectManager.swift b/PayForMe/Services/ProjectManager.swift index 41077e5..8bef295 100644 --- a/PayForMe/Services/ProjectManager.swift +++ b/PayForMe/Services/ProjectManager.swift @@ -86,33 +86,27 @@ class ProjectManager: ObservableObject { .assign(to: &$currentProject) } - private func sendBillToServer(bill: Bill, update: Bool, completion: @escaping () -> Void) { - cancellable?.cancel() - cancellable = nil - - if update { - cancellable = NetworkService.shared.updateBillPublisher(bill: bill) - .sink { success in - if success { - print("Bill id\(bill.id) updated") - } else { - print("error updating bill id\(bill.id)") - } - completion() - } - } else { - cancellable = NetworkService.shared.postBillPublisher(bill: bill) - .sink { success in - if success { - print("Bill posted") - } else { - print("Error posting bill") - } - completion() - } + private func updateBill(bill: Bill) async { + do { + try await NetworkService.shared.update(bill: bill) + } catch { + // TODO + print("Error posting bill") + } } + private func createBill(bill: Bill) async { + do { + try await NetworkService.shared.post(bill: bill) + } catch { + // TODO + print("Error posting bill") + } + } + + + private func deleteBillFromServer(bill: Bill, completion: @escaping () -> Void) { cancellable?.cancel() cancellable = nil @@ -218,13 +212,13 @@ extension ProjectManager { addProject(demoProject) } - func saveBill(_ bill: Bill, completion: @escaping () -> Void) { - if bill.id != -1, let _ = self.currentProject.bills.firstIndex(where: { + func saveBill(_ bill: Bill) async { + if bill.id != -1 && self.currentProject.bills.contains(where: { $0.id == bill.id }) { - sendBillToServer(bill: bill, update: true, completion: completion) + await createBill(bill: bill) } else { - sendBillToServer(bill: bill, update: false, completion: completion) + await updateBill(bill: bill) } } diff --git a/PayForMe/Views/BillDetail/BillDetailView.swift b/PayForMe/Views/BillDetail/BillDetailView.swift index 073bf91..b2dfbf9 100644 --- a/PayForMe/Views/BillDetail/BillDetailView.swift +++ b/PayForMe/Views/BillDetail/BillDetailView.swift @@ -63,20 +63,19 @@ struct BillDetailView: View { .navigationBarTitle(navBarTitle, displayMode: .inline) } - func sendBillToServer() { + func sendBillToServer() async { guard let newBill = self.viewModel.createBill() else { print("Could not create bill") return } sendingInProgress = .connecting - ProjectManager.shared.saveBill(newBill, completion: { - self.sendingInProgress = .success - ProjectManager.shared.loadBillsAndMembers() - self.showModal.toggle() - DispatchQueue.main.async { - self.presentationMode.wrappedValue.dismiss() - } - }) + await ProjectManager.shared.saveBill(newBill) + self.sendingInProgress = .success + ProjectManager.shared.loadBillsAndMembers() + self.showModal.toggle() + DispatchQueue.main.async { + self.presentationMode.wrappedValue.dismiss() + } } } diff --git a/PayForMe/Views/FancyLoadingButton.swift b/PayForMe/Views/FancyLoadingButton.swift index 38e4337..38fce12 100644 --- a/PayForMe/Views/FancyLoadingButton.swift +++ b/PayForMe/Views/FancyLoadingButton.swift @@ -17,13 +17,16 @@ struct FancyLoadingButton: View { var add: Bool - var action: () -> Void + var action: () async -> Void var text: String var body: some View { switch isLoading { - case .notStarted: - return Button(action: action) { + case .notStarted: + return Button(action: { + Task { + await action() + }}) { if add { Image(systemName: "plus") } else { @@ -33,10 +36,10 @@ struct FancyLoadingButton: View { .fancyStyle(active: self.isEnabled) .disabled(!isEnabled) .eraseToAnyView() - default: - return SlickLoadingSpinner(connectionState: isLoading) - .frame(width: 50, height: 50) - .eraseToAnyView() + default: + return SlickLoadingSpinner(connectionState: isLoading) + .frame(width: 50, height: 50) + .eraseToAnyView() } } } diff --git a/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift b/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift index bc244dd..907f17f 100644 --- a/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift +++ b/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift @@ -148,7 +148,7 @@ class AddProjectManualViewModel: ObservableObject { private var validatedServer: AnyPublisher { validatedInput.flatMap { project in - return NetworkService.shared.testProject(project) + return NetworkService.shared.foundProjectStatusCode(project) } .map {project, code in self.lastProjectTestedSuccessfully = project diff --git a/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift b/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift index 363b4b6..fc872ec 100644 --- a/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift +++ b/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift @@ -54,7 +54,7 @@ class AddProjectQRViewModel: ObservableObject { return Project(name: name, password: password, backend: .cospend, url: url) } .flatMap { project in - NetworkService.shared.testProject(project) + NetworkService.shared.foundProjectStatusCode(project) } .map { project, statusCode in if statusCode == 200 { @@ -89,7 +89,7 @@ class AddProjectQRViewModel: ObservableObject { if let password = projectData.passwd { self.isTestingSubject.send(.connecting) let project = Project(name: name, password: password, backend: .cospend, url: url) - NetworkService.shared.testProject(project) + NetworkService.shared.foundProjectStatusCode(project) .asUIPublisher .sink(receiveValue: { project, code in From d9dc118ee6783db41b9e0c15358f126760e9fb1f Mon Sep 17 00:00:00 2001 From: Max Tharr Date: Fri, 12 Nov 2021 17:38:29 +0100 Subject: [PATCH 04/11] Swiftformat --- PayForMe/AppDelegate.swift | 22 +-- PayForMe/Model/Bill.swift | 42 ++--- PayForMe/Model/Person.swift | 16 +- PayForMe/Model/Project.swift | 14 +- PayForMe/Model/Server.swift | 6 +- PayForMe/SceneDelegate.swift | 31 ++-- PayForMe/Services/NetworkService.swift | 174 +++++++++--------- PayForMe/Services/ProjectManager.swift | 124 ++++++------- PayForMe/Services/StorageService.swift | 41 ++--- PayForMe/Util/Combine.swift | 4 +- .../Util/FloatingAddButtonViewModifier.swift | 32 ++-- PayForMe/Util/Util.swift | 102 +++++----- PayForMe/Util/Views+Extensions.swift | 11 +- PayForMe/Views/Balance/AddMemberView.swift | 18 +- PayForMe/Views/Balance/BalanceList.swift | 46 +++-- PayForMe/Views/Balance/BalanceViewModel.swift | 28 +-- PayForMe/Views/BillDetail/AddBillView.swift | 7 +- .../Views/BillDetail/BillDetailView.swift | 29 ++- .../BillDetail/BillDetailViewModel.swift | 57 +++--- .../BillDetail/CommunicationIndicator.swift | 21 +-- .../Views/BillDetail/PotentialOwersView.swift | 3 +- .../BillDetail/PotentialOwersViewModel.swift | 40 ++-- PayForMe/Views/BillDetail/WhoPaidView.swift | 11 +- PayForMe/Views/BillList/BillCell.swift | 23 ++- PayForMe/Views/BillList/BillList.swift | 65 ++++--- .../Views/BillList/BillListViewModel.swift | 25 ++- PayForMe/Views/BillList/PersonsView.swift | 20 +- PayForMe/Views/ContentView.swift | 31 ++-- PayForMe/Views/FancyButton.swift | 7 +- PayForMe/Views/FancyLoadingButton.swift | 32 ++-- PayForMe/Views/LoadingDots.swift | 29 ++- PayForMe/Views/PersonText.swift | 9 +- PayForMe/Views/Projects/AddPasswordView.swift | 9 +- .../Manual/AddProjectManualView.swift | 30 +-- .../Manual/AddProjectManualViewModel.swift | 90 +++++---- PayForMe/Views/Projects/OnboardingView.swift | 8 +- PayForMe/Views/Projects/ProjectList.swift | 61 +++--- .../Projects/QRCodes/AddFromURLView.swift | 5 +- .../Projects/QRCodes/AddProjectQRView.swift | 47 +++-- .../QRCodes/AddProjectQRViewModel.swift | 63 ++++--- .../ProjectQRPermissionCheckerView.swift | 26 +-- .../Views/Projects/ShareProjectQRCode.swift | 4 +- 42 files changed, 700 insertions(+), 763 deletions(-) diff --git a/PayForMe/AppDelegate.swift b/PayForMe/AppDelegate.swift index b638ab7..c3d15ef 100644 --- a/PayForMe/AppDelegate.swift +++ b/PayForMe/AppDelegate.swift @@ -10,19 +10,14 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - static var isUITestingEnabled: Bool { - get { - return ProcessInfo.processInfo.arguments.contains("UI-Testing") - } + return ProcessInfo.processInfo.arguments.contains("UI-Testing") } - + static var isUITestingOnboarding: Bool { - get { - return ProcessInfo.processInfo.arguments.contains("Onboarding") - } + return ProcessInfo.processInfo.arguments.contains("Onboarding") } - + private func setStateForUITesting() { if AppDelegate.isUITestingEnabled { if AppDelegate.isUITestingOnboarding { @@ -33,7 +28,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. setStateForUITesting() return true @@ -41,18 +36,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + func application(_: UIApplication, didDiscardSceneSessions _: Set) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - - } - diff --git a/PayForMe/Model/Bill.swift b/PayForMe/Model/Bill.swift index 0c90b07..2fe1fc0 100644 --- a/PayForMe/Model/Bill.swift +++ b/PayForMe/Model/Bill.swift @@ -9,7 +9,6 @@ import Foundation struct Bill: Codable, Identifiable, Hashable { - var id: Int var amount: Double var what: String @@ -18,19 +17,19 @@ struct Bill: Codable, Identifiable, Hashable { var owers: [Person] var `repeat`: String? var lastchanged: Int? - + func paramsFor(_ backend: ProjectBackend) -> [String: Any] { var dict: [String: Any] = [ - "date": DateFormatter.cospend.string(from: self.date), - "what": self.what, - "payer": self.payer_id.description, - "amount": self.amount.description, + "date": DateFormatter.cospend.string(from: date), + "what": what, + "payer": payer_id.description, + "amount": amount.description, ] if backend == .cospend { - dict["payed_for"] = self.owers.map{$0.id.description}.joined(separator: ",") + dict["payed_for"] = owers.map { $0.id.description }.joined(separator: ",") dict["paymentmode"] = "n" dict["categoryid"] = "0" - + if let rep = self.repeat { dict["repeat"] = rep } else { @@ -38,12 +37,12 @@ struct Bill: Codable, Identifiable, Hashable { } } if backend == .iHateMoney { - dict["payed_for"] = self.owers.map{$0.id.description} + dict["payed_for"] = owers.map { $0.id.description } } - + return dict } - + static func newBill() -> Bill { Bill(id: -1, amount: 0, what: "", date: Date(), payer_id: 0, owers: [], repeat: "n") } @@ -55,25 +54,24 @@ let previewBills = [ Bill(id: 1, amount: 5, what: "Erdnüsse", date: date, payer_id: 1, owers: [ Person(id: 1, weight: 1, name: "Pikachu", activated: true), Person(id: 2, weight: 1, name: "Schiggy", activated: true), - Person(id: 3, weight: 1, name: "Bisasam", activated: true) - ], repeat: "n", lastchanged: 1231234), + Person(id: 3, weight: 1, name: "Bisasam", activated: true), + ], repeat: "n", lastchanged: 1_231_234), Bill(id: 2, amount: 5, what: "Nochmal Erdnüsse", date: date, payer_id: 1, owers: [ Person(id: 1, weight: 1, name: "Pikachu", activated: true), Person(id: 2, weight: 1, name: "Schiggy", activated: true), - Person(id: 3, weight: 1, name: "Bisasam", activated: true) - ], repeat: "n", lastchanged: 1231234), + Person(id: 3, weight: 1, name: "Bisasam", activated: true), + ], repeat: "n", lastchanged: 1_231_234), Bill(id: 3, amount: 5, what: "Nochmal Erdnüsse", date: date, payer_id: 2, owers: [ Person(id: 1, weight: 1, name: "Pikachu", activated: true), - Person(id: 2, weight: 1, name: "Schiggy", activated: true) - ], repeat: "n", lastchanged: 1231234), + Person(id: 2, weight: 1, name: "Schiggy", activated: true), + ], repeat: "n", lastchanged: 1_231_234), Bill(id: 4, amount: 5, what: "Nochmal Erdnüsse", date: date, payer_id: 3, owers: [ Person(id: 1, weight: 1, name: "Pikachu", activated: true), Person(id: 2, weight: 1, name: "Schiggy", activated: true), Person(id: 3, weight: 1, name: "Bisasam", activated: true), - Person(id: 4, weight: 1, name: "Glumanda", activated: true) - ], repeat: "n", lastchanged: 1231234), + Person(id: 4, weight: 1, name: "Glumanda", activated: true), + ], repeat: "n", lastchanged: 1_231_234), Bill(id: 5, amount: 5, what: "Nochmal Erdnüsse", date: date, payer_id: 1, owers: [ - Person(id: 3, weight: 1, name: "Bisasam", activated: true) - ], repeat: "n", lastchanged: 1231234), - + Person(id: 3, weight: 1, name: "Bisasam", activated: true), + ], repeat: "n", lastchanged: 1_231_234), ] diff --git a/PayForMe/Model/Person.swift b/PayForMe/Model/Person.swift index 35eb7a5..116d5e9 100644 --- a/PayForMe/Model/Person.swift +++ b/PayForMe/Model/Person.swift @@ -9,33 +9,31 @@ import Foundation struct Person: Hashable, Codable, Identifiable { - var id: Int var weight: Int var name: String var activated: Bool var color: PersonColor? - + static func == (lhs: Person, rhs: Person) -> Bool { return lhs.id == rhs.id } - + func hash(into hasher: inout Hasher) { hasher.combine(id) } } + struct PersonColor: Codable { var r: Int var g: Int var b: Int } - let previewPerson = Person(id: 1, weight: 1, name: "Pikachu", activated: true, color: PersonColor(r: 60, g: 110, b: 186)) let previewPersons = [ - 1:previewPerson, - 2:Person(id: 2, weight: 1, name: "Schiggy", activated: true, color: PersonColor(r: 60, g: 110, b: 186)), - 3:Person(id: 3, weight: 1, name: "Bisasam", activated: true), - 4:Person(id: 4, weight: 1, name: "Glumanda", activated: true), + 1: previewPerson, + 2: Person(id: 2, weight: 1, name: "Schiggy", activated: true, color: PersonColor(r: 60, g: 110, b: 186)), + 3: Person(id: 3, weight: 1, name: "Bisasam", activated: true), + 4: Person(id: 4, weight: 1, name: "Glumanda", activated: true), ] - diff --git a/PayForMe/Model/Project.swift b/PayForMe/Model/Project.swift index 0578719..edd4ab4 100644 --- a/PayForMe/Model/Project.swift +++ b/PayForMe/Model/Project.swift @@ -14,14 +14,14 @@ class Project: Codable, Identifiable { let url: URL let id: Int? let backend: ProjectBackend - - var members: [Int : Person] + + var members: [Int: Person] var bills: [Bill] - + convenience init(name: String, password: String, backend: ProjectBackend, url: URL) { self.init(name: name, password: password, backend: backend, url: url, id: nil) } - + fileprivate init(name: String, password: String, backend: ProjectBackend, url: URL, id: Int?) { self.name = name self.password = password @@ -39,7 +39,7 @@ struct StoredProject: Codable { let url: URL let backend: ProjectBackend var id: Int? - + init(name: String, password: String, url: URL, backend: ProjectBackend) { self.name = name self.password = password @@ -47,7 +47,7 @@ struct StoredProject: Codable { self.backend = backend id = nil } - + init(project: Project) { name = project.name password = project.password @@ -55,7 +55,7 @@ struct StoredProject: Codable { backend = project.backend id = project.id } - + func toProject() -> Project { Project(name: name, password: password, backend: backend, url: url, id: id!) } diff --git a/PayForMe/Model/Server.swift b/PayForMe/Model/Server.swift index 7634477..d9606ab 100644 --- a/PayForMe/Model/Server.swift +++ b/PayForMe/Model/Server.swift @@ -8,14 +8,14 @@ import Foundation -//class Server: Codable { +// class Server: Codable { // let name: String // let url: String // var projects: [Project] -// +// // init(name: String, url: String, projects: [Project]) { // self.name = name // self.url = url // self.projects = projects // } -//} +// } diff --git a/PayForMe/SceneDelegate.swift b/PayForMe/SceneDelegate.swift index 07a9e43..709c438 100644 --- a/PayForMe/SceneDelegate.swift +++ b/PayForMe/SceneDelegate.swift @@ -6,14 +6,13 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import UIKit import SwiftUI +import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). @@ -25,62 +24,60 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) - + let tapGesture = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing)) tapGesture.requiresExclusiveTouchType = false tapGesture.cancelsTouchesInView = false tapGesture.delegate = self - + window.addGestureRecognizer(tapGesture) - + self.window = window window.makeKeyAndVisible() } } - func sceneDidDisconnect(_ scene: UIScene) { + func sceneDidDisconnect(_: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). } - func sceneDidBecomeActive(_ scene: UIScene) { + func sceneDidBecomeActive(_: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } - func sceneWillResignActive(_ scene: UIScene) { + func sceneWillResignActive(_: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } - func sceneWillEnterForeground(_ scene: UIScene) { + func sceneWillEnterForeground(_: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } - func sceneDidEnterBackground(_ scene: UIScene) { + func sceneDidEnterBackground(_: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } - - func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + + func scene(_: UIScene, openURLContexts URLContexts: Set) { guard let context = URLContexts.first, let scheme = context.url.scheme, scheme.localizedCaseInsensitiveContains("cospend") - else { + else { return } ProjectManager.shared.openedByURL(url: context.url) } - - } extension SceneDelegate: UIGestureRecognizerDelegate { - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool { return true } } diff --git a/PayForMe/Services/NetworkService.swift b/PayForMe/Services/NetworkService.swift index 39a8646..6a8e105 100644 --- a/PayForMe/Services/NetworkService.swift +++ b/PayForMe/Services/NetworkService.swift @@ -6,90 +6,88 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import Foundation import Combine +import Foundation class NetworkService { - static let shared = NetworkService() - + private let decoder: JSONDecoder - - private init(){ + + private init() { decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(DateFormatter.cospend) } - + private let cospendStaticPath = "/index.php/apps/cospend/api/projects" private let iHateMoneyStaticPath = "/api/projects" - + static let iHateMoneyURLString = "https://ihatemoney.org" - + private var currentProject: Project { - get { - return ProjectManager.shared.currentProject - } + return ProjectManager.shared.currentProject } - + let networkActivityPublisher = PassthroughSubject() - + func loadBillsPublisher(_ project: Project) -> AnyPublisher<[Bill], Never> { - let request = self.buildURLRequest("bills", params: [:], project: project) + let request = buildURLRequest("bills", params: [:], project: project) return URLSession.shared.dataTaskPublisher(for: request) .compactMap { data, response -> Data? in guard let httpResponse = response as? HTTPURLResponse else { print("Network Error"); return nil } guard httpResponse.statusCode == 200 else { print("Network Error: Status code: \(httpResponse.statusCode) \(httpResponse.description)"); return nil } return data - } - .decode(type: [Bill].self, decoder: decoder) - .replaceError(with: []) - .map { - return $0.sorted { - if let l1 = $0.lastchanged, - let l2 = $1.lastchanged { - return l1 > l2 + } + .decode(type: [Bill].self, decoder: decoder) + .replaceError(with: []) + .map { + $0.sorted { + if let l1 = $0.lastchanged, + let l2 = $1.lastchanged + { + return l1 > l2 + } + return $0.date > $1.date } - return $0.date > $1.date } - } - .eraseToAnyPublisher() + .eraseToAnyPublisher() } - - func loadMembersPublisher(_ project: Project) -> AnyPublisher<[Int:Person], Never> { + + func loadMembersPublisher(_ project: Project) -> AnyPublisher<[Int: Person], Never> { let request = buildURLRequest("members", params: [:], project: project) return URLSession.shared.dataTaskPublisher(for: request) .compactMap { data, response -> Data? in guard let httpResponse = response as? HTTPURLResponse else { print("Network Error"); return nil } guard httpResponse.statusCode == 200 else { print("Network Error: Status code: \(httpResponse.statusCode) \(httpResponse.description)"); return nil } return data - } - .decode(type: [Person].self, decoder: decoder) - .replaceError(with: []) - .map { - members in - let filtered = members.filter { - $0.activated } - return Dictionary(filtered.map {($0.id, $0)}) {a,_ in a } - } - .eraseToAnyPublisher() + .decode(type: [Person].self, decoder: decoder) + .replaceError(with: []) + .map { + members in + let filtered = members.filter { + $0.activated + } + return Dictionary(filtered.map { ($0.id, $0) }) { a, _ in a } + } + .eraseToAnyPublisher() } - + func foundProjectStatusCode(_ project: Project) -> AnyPublisher<(Project, Int), Never> { let request = buildURLRequest("members", params: [:], project: project) let requestPub = URLSession.shared.dataTaskPublisher(for: request) - .tryMap { data, response -> Int in - guard let httpResponse = response as? HTTPURLResponse else { print("Network Error"); return -1} - return httpResponse.statusCode - } - .replaceError(with: -1) - return Publishers.CombineLatest(Just(project),requestPub).eraseToAnyPublisher() + .tryMap { _, response -> Int in + guard let httpResponse = response as? HTTPURLResponse else { print("Network Error"); return -1 } + return httpResponse.statusCode + } + .replaceError(with: -1) + return Publishers.CombineLatest(Just(project), requestPub).eraseToAnyPublisher() } - + func createProjectPublisher(_ project: Project, email: String) -> AnyPublisher { let params = ["name": project.name, "id": project.name, "password": project.password, "contact_email": email] let request = buildURLRequest("", params: params, project: project, httpMethod: "POST") - + return URLSession.shared.dataTaskPublisher(for: request) .tryMap { data, response -> Bool in guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode / 100 == 2 else { @@ -98,66 +96,65 @@ class NetworkService { guard let responseString = String(data: data, encoding: .utf8) else { return false } - + return responseString.contains(project.name) - } - .replaceError(with: false) - .eraseToAnyPublisher() + } + .replaceError(with: false) + .eraseToAnyPublisher() } - + private func sendBillPublisher(request: URLRequest) -> AnyPublisher { - return URLSession.shared.dataTaskPublisher(for: request) .tryMap { output in guard let response = output.response as? HTTPURLResponse, response.statusCode / 100 == 2 else { return false } return true - } - .replaceError(with: false) - .eraseToAnyPublisher() + } + .replaceError(with: false) + .eraseToAnyPublisher() } - + func post(bill: Bill) async throws { let request = buildURLRequest("bills", params: bill.paramsFor(currentProject.backend), project: currentProject, httpMethod: "POST") try await sendWithOutResponseData(request: request) } - + func update(bill: Bill) async throws { let request = buildURLRequest("bills/\(bill.id)", params: bill.paramsFor(currentProject.backend), project: currentProject, httpMethod: "PUT") try await sendWithOutResponseData(request: request) } - + private func sendWithOutResponseData(request: URLRequest) async throws { let (_, response) = try await URLSession.shared.data(for: request) guard let response = response as? HTTPURLResponse else { throw HTTPError.generalFailure - } + } if response.statusCode / 100 == 2 { throw HTTPError.statuscode(code: response.statusCode) } } - + func deleteBillPublisher(bill: Bill) -> AnyPublisher { let request = buildURLRequest("bills/\(bill.id)", params: [:], project: currentProject, httpMethod: "DELETE") return sendBillPublisher(request: request) } - + func createMemberPublisher(name: String) -> AnyPublisher { let request = buildURLRequest("members", params: ["name": name], project: currentProject, httpMethod: "POST") return sendMemberPublisher(request: request) } - + func updateMemberPublisher(member: Person) -> AnyPublisher { let request = buildURLRequest("members/\(member.id)", params: ["name": member.name], project: currentProject, httpMethod: "PUT") return sendMemberPublisher(request: request) } - + func deleteMemberPublisher(member: Person) -> AnyPublisher { let request = buildURLRequest("members/\(member.id)", params: [:], project: currentProject, httpMethod: "DELETE") return sendMemberPublisher(request: request) } - + private func sendMemberPublisher(request: URLRequest) -> AnyPublisher { return URLSession.shared.dataTaskPublisher(for: request) .map { output in @@ -165,67 +162,66 @@ class NetworkService { return false } return true - } - .replaceError(with: false) - .eraseToAnyPublisher() + } + .replaceError(with: false) + .eraseToAnyPublisher() } - - private func baseURLFor(_ project: Project) -> URL { + + private func baseURLFor(_ project: Project) -> URL { switch project.backend { - case .cospend: - return project.url.appendingPathComponent("\(cospendStaticPath)/") - case .iHateMoney: - return project.url.appendingPathComponent("\(iHateMoneyStaticPath)") + case .cospend: + return project.url.appendingPathComponent("\(cospendStaticPath)/") + case .iHateMoney: + return project.url.appendingPathComponent("\(iHateMoneyStaticPath)") } } - + private func baseURLFor(_ project: Project, suffix: String) -> URL { switch project.backend { - case .cospend: - return baseURLFor(project).appendingPathComponent("\(project.name.lowercased())/\(project.password)/\(suffix)") - case .iHateMoney: - return baseURLFor(project).appendingPathComponent("\(project.name.lowercased())/\(suffix)") + case .cospend: + return baseURLFor(project).appendingPathComponent("\(project.name.lowercased())/\(project.password)/\(suffix)") + case .iHateMoney: + return baseURLFor(project).appendingPathComponent("\(project.name.lowercased())/\(suffix)") } } - + private func buildURLRequest(_ suffix: String, params: [String: Any] = [:], project: Project = ProjectManager.shared.currentProject, httpMethod: String = "GET") -> URLRequest { - let baseURL: URL let requestURL: URL var request: URLRequest - + if !suffix.isEmpty { baseURL = baseURLFor(project, suffix: suffix) } else { baseURL = baseURLFor(project) } - - if let cospendParams = params as? [String: String], project.backend == .cospend && !params.isEmpty { + + if let cospendParams = params as? [String: String], project.backend == .cospend, !params.isEmpty { var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) urlComponents?.queryItems = cospendParams.map { URLQueryItem(name: $0, value: $1) } - + requestURL = urlComponents!.url! } else { requestURL = baseURL } - + request = URLRequest(url: requestURL) - + if project.backend == .iHateMoney { if !suffix.isEmpty { guard let authString = "\(project.name):\(project.password)".data(using: .utf8)?.base64EncodedString() else { fatalError("error generating authString. THIS SHOULD NOT HAPPEN") } request.setValue("Basic \(authString)", forHTTPHeaderField: "Authorization") } - + if !params.isEmpty { request.httpBody = try? JSONSerialization.data(withJSONObject: params) request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Accept") } } - + request.httpMethod = httpMethod - + return request } } diff --git a/PayForMe/Services/ProjectManager.swift b/PayForMe/Services/ProjectManager.swift index 8bef295..81c9262 100644 --- a/PayForMe/Services/ProjectManager.swift +++ b/PayForMe/Services/ProjectManager.swift @@ -6,36 +6,35 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import Foundation import Combine +import Foundation class ProjectManager: ObservableObject { - private let defaults = UserDefaults.standard - + private var cancellable: Cancellable? - + @Published private(set) var projects = [Project]() - + @Published var currentProject: Project = demoProject - + let storageService = StorageService() - + static let shared = ProjectManager() - + @Published var openedByURL: URL? - + private init() { print("init") projects = storageService.loadProjects() - + let id = defaults.integer(forKey: "projectID") if let project = projects.first(where: { - $0.id == id - }){ - self.currentProject = project + $0.id == id + }) { + currentProject = project loadBillsAndMembers() } else { if !projects.isEmpty { @@ -43,22 +42,23 @@ class ProjectManager: ObservableObject { } } } - + func openedByURL(url: URL) { let data = url.decodeCospendString() guard let _ = data.server, - let _ = data.project else { + let _ = data.project + else { return } openedByURL = url } - + // MARK: Server Communication - + private func createProjectOnServer(_ project: Project, email: String, completion: @escaping () -> Void) { cancellable?.cancel() cancellable = nil - + cancellable = NetworkService.shared.createProjectPublisher(project, email: email) .sink { success in if success { @@ -66,51 +66,48 @@ class ProjectManager: ObservableObject { } else { print("Error creating project \(project.name)") } - completion() + completion() } } - + func loadBillsAndMembers() { let project = currentProject - + let billsPublisher = NetworkService.shared.loadBillsPublisher(project) let membersPublisher = NetworkService.shared.loadMembersPublisher(project) - + Publishers.Zip(billsPublisher, membersPublisher) .map { bills, members in project.bills = bills project.members = members return project } - .receive(on: DispatchQueue.main) - .assign(to: &$currentProject) + .receive(on: DispatchQueue.main) + .assign(to: &$currentProject) } - + private func updateBill(bill: Bill) async { do { try await NetworkService.shared.update(bill: bill) } catch { - // TODO + // TODO: print("Error posting bill") - } } - + private func createBill(bill: Bill) async { do { try await NetworkService.shared.post(bill: bill) } catch { - // TODO + // TODO: print("Error posting bill") } } - - - + private func deleteBillFromServer(bill: Bill, completion: @escaping () -> Void) { cancellable?.cancel() cancellable = nil - + cancellable = NetworkService.shared.deleteBillPublisher(bill: bill) .sink { success in if success { @@ -119,13 +116,13 @@ class ProjectManager: ObservableObject { print("Error deleting bill") } completion() - } + } } - + private func sendMemberToServer(_ member: Person, update: Bool, completion: @escaping () -> Void) { cancellable?.cancel() cancellable = nil - + if update { cancellable = NetworkService.shared.updateMemberPublisher(member: member) .sink { success in @@ -135,7 +132,7 @@ class ProjectManager: ObservableObject { print("Error updating Member") } completion() - } + } } else { cancellable = NetworkService.shared.createMemberPublisher(name: member.name) .sink { success in @@ -145,14 +142,14 @@ class ProjectManager: ObservableObject { print("Error creating member") } completion() - } + } } } - + private func deleteMemberFromServer(_ member: Person, completion: @escaping () -> Void) { cancellable?.cancel() cancellable = nil - + cancellable = NetworkService.shared.deleteMemberPublisher(member: member) .sink { success in if success { @@ -161,27 +158,27 @@ class ProjectManager: ObservableObject { print("Error deleting member") } completion() - } + } } } extension ProjectManager { func createProject(_ project: Project, email: String, completion: @escaping () -> Void) { - guard !projects.contains(project) else { print("project duplicate") ; return } + guard !projects.contains(project) else { print("project duplicate"); return } let inceptedCompletion = { self.addProject(project) completion() } - - self.createProjectOnServer(project, email: email, completion: inceptedCompletion) + + createProjectOnServer(project, email: email, completion: inceptedCompletion) } - + func addProject(_ project: Project) -> Bool { guard storageService.saveProject(project: project) else { return false } projects = storageService.loadProjects() - + if projects.count == 1 { setCurrentProject(project) } @@ -189,7 +186,7 @@ extension ProjectManager { print("project added") return true } - + func deleteProject(_ project: Project) { storageService.removeProject(project: project) projects = storageService.loadProjects() @@ -202,18 +199,18 @@ extension ProjectManager { } } } - + func prepareUITestOnboarding() { projects.forEach { deleteProject($0) } } - + func prepareUITest() { projects.forEach { deleteProject($0) } addProject(demoProject) } - + func saveBill(_ bill: Bill) async { - if bill.id != -1 && self.currentProject.bills.contains(where: { + if bill.id != -1, currentProject.bills.contains(where: { $0.id == bill.id }) { await createBill(bill: bill) @@ -221,36 +218,35 @@ extension ProjectManager { await updateBill(bill: bill) } } - + func deleteBill(_ bill: Bill, completion: @escaping () -> Void) { - self.currentProject.bills.removeAll { + currentProject.bills.removeAll { $0.id == bill.id } - self.deleteBillFromServer(bill: bill, completion: completion) + deleteBillFromServer(bill: bill, completion: completion) } - + func addMember(_ name: String, completion: @escaping () -> Void) { let newMember = Person(id: -1, weight: -1, name: name, activated: true, color: nil) - self.sendMemberToServer(newMember, update: false, completion: completion) + sendMemberToServer(newMember, update: false, completion: completion) } - + func updateMember(_ member: Person, completion: @escaping () -> Void) { - self.sendMemberToServer(member, update: true, completion: completion) + sendMemberToServer(member, update: true, completion: completion) } - + func deleteMember(_ member: Person, completion: @escaping () -> Void) { - self.deleteMemberFromServer(member, completion: completion) + deleteMemberFromServer(member, completion: completion) } - + func setCurrentProject(_ project: Project) { - guard let project = projects.first (where: { + guard let project = projects.first(where: { $0 == project }) else { return } - self.currentProject = project + currentProject = project loadBillsAndMembers() defaults.set(project.id, forKey: "projectID") } - } diff --git a/PayForMe/Services/StorageService.swift b/PayForMe/Services/StorageService.swift index 7d60aba..8cb6787 100644 --- a/PayForMe/Services/StorageService.swift +++ b/PayForMe/Services/StorageService.swift @@ -10,18 +10,16 @@ import Foundation import GRDB class StorageService { - let encoder = JSONEncoder() let decoder = JSONDecoder() - + private let databasePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] private var legacyFilePath: URL { return databasePath.appendingPathComponent("Projects.json") } - private let dbQueue: DatabaseQueue - + init() { do { dbQueue = try DatabaseQueue(path: databasePath.appendingPathComponent("payforme.sqlite").path) @@ -34,17 +32,17 @@ class StorageService { table.column("backend") } } - } catch let error { + } catch { print("Storage couldn't be initialized \(error.localizedDescription)") fatalError() } #if targetEnvironment(simulator) - print("Database file at \(databasePath.appendingPathComponent("payforme.sqlite").path)") + print("Database file at \(databasePath.appendingPathComponent("payforme.sqlite").path)") #endif testLegacy() print("Storage service initialized") } - + func saveProject(project: Project) -> Bool { let storedProject = StoredProject(project: project) do { @@ -55,34 +53,34 @@ class StorageService { } return false } - } catch let error { + } catch { print("Couldn't store projects \(error.localizedDescription)") return false } } - + func loadProjects() -> [Project] { do { return try dbQueue.read { db in try StoredProject.fetchAll(db).map { $0.toProject() } } - } catch let error { + } catch { print("Catched \(error.localizedDescription) while loading projects") return [] } } - + func removeProject(project: Project) { let storedProject = StoredProject(project: project) do { try dbQueue.write { db in try storedProject.delete(db) } - } catch let error { + } catch { print("Couldn't remove projects \(error.localizedDescription)") } } - + private func testLegacy() { guard let data = try? Data(contentsOf: legacyFilePath) else { return @@ -93,35 +91,34 @@ class StorageService { storeProjects(projects: projects.map { $0.toProject() }) try FileManager.default.removeItem(at: legacyFilePath) print("data loaded legacy, will save as new") - } catch let error { + } catch { print("\(error.localizedDescription)") } } - + private func storeProjects(projects: [StoredProject]) { do { try dbQueue.write { db in try projects.forEach { try $0.save(db) } } - } catch let error { + } catch { print("Couldn't store projects \(error.localizedDescription)") } } } -extension StoredProject: FetchableRecord, PersistableRecord { -} +extension StoredProject: FetchableRecord, PersistableRecord {} -fileprivate class OldProject: Codable, Identifiable { +private class OldProject: Codable, Identifiable { let name: String let password: String let url: URL let id: UUID let backend: ProjectBackend - - var members: [Int : Person] + + var members: [Int: Person] var bills: [Bill] - + func toProject() -> StoredProject { return StoredProject(name: name, password: password, url: url, backend: backend) } diff --git a/PayForMe/Util/Combine.swift b/PayForMe/Util/Combine.swift index 0ef696d..ef7948e 100644 --- a/PayForMe/Util/Combine.swift +++ b/PayForMe/Util/Combine.swift @@ -10,7 +10,7 @@ import Combine import Foundation extension Publisher where Failure == Never { - var asUIPublisher: AnyPublisher { - self.receive(on: RunLoop.main).eraseToAnyPublisher() + var asUIPublisher: AnyPublisher { + receive(on: RunLoop.main).eraseToAnyPublisher() } } diff --git a/PayForMe/Util/FloatingAddButtonViewModifier.swift b/PayForMe/Util/FloatingAddButtonViewModifier.swift index b78a0f7..3faa039 100644 --- a/PayForMe/Util/FloatingAddButtonViewModifier.swift +++ b/PayForMe/Util/FloatingAddButtonViewModifier.swift @@ -10,31 +10,31 @@ import SwiftUI struct FloatingAddButtonViewModifier: ViewModifier { @State private var sheetToggle = false - + func body(content: Content) -> some View { content .sheet(isPresented: $sheetToggle) { AddBillView(showModal: $sheetToggle) } - .overlay( FloatingAddButtonView(sheetToggle: $sheetToggle).padding(32), alignment: .bottom) + .overlay(FloatingAddButtonView(sheetToggle: $sheetToggle).padding(32), alignment: .bottom) } } private struct FloatingAddButtonView: View { @Binding var sheetToggle: Bool - + var body: some View { - Button(action: { - sheetToggle.toggle() - }) { - Image(systemName: "plus") - .resizable() - .frame(width: 50, height: 50) - .foregroundColor(Color.blue) - .shadow(color: Color(red: 0.53, green: 0.53, blue: 0.53), radius: 3, x: 2, y: 2) - } - .accessibility(identifier: "Add Bill") - .accessibility(label: Text("Add Bill")) + Button(action: { + sheetToggle.toggle() + }) { + Image(systemName: "plus") + .resizable() + .frame(width: 50, height: 50) + .foregroundColor(Color.blue) + .shadow(color: Color(red: 0.53, green: 0.53, blue: 0.53), radius: 3, x: 2, y: 2) + } + .accessibility(identifier: "Add Bill") + .accessibility(label: Text("Add Bill")) } } @@ -44,11 +44,11 @@ struct FloatingAddButtonViewModifier_Previews: PreviewProvider { previewProject.bills = [previewBills, previewBills, previewBills].flatMap { $0 } previewProject.members = previewPersons viewModel.currentProject = previewProject - + return NavigationView { BillList(viewModel: viewModel) } - //.modifier(FloatingAddButtonViewModifier()) + // .modifier(FloatingAddButtonViewModifier()) .navigationViewStyle(StackNavigationViewStyle()) .preferredColorScheme(.dark) } diff --git a/PayForMe/Util/Util.swift b/PayForMe/Util/Util.swift index aa87607..30a3dd1 100644 --- a/PayForMe/Util/Util.swift +++ b/PayForMe/Util/Util.swift @@ -7,57 +7,57 @@ // import Foundation -import SwiftUI import SlickLoadingSpinner +import SwiftUI extension Collection { /// Returns the element at the specified index iff it is within bounds, otherwise nil. - subscript (safe index: Index) -> Element? { + subscript(safe index: Index) -> Element? { return indices.contains(index) ? self[index] : nil } } extension Color { init(_ pc: PersonColor) { - self.init(red: Double(pc.r)/255, green: Double(pc.g)/255, blue: Double(pc.b)/255, opacity: 1) + self.init(red: Double(pc.r) / 255, green: Double(pc.g) / 255, blue: Double(pc.b) / 255, opacity: 1) } - + static var PFMBackground: Color { if UIScreen.main.traitCollection.userInterfaceStyle == .dark { return Color.black } else { - return Color(UIColor(red:0.95, green:0.95, blue:0.97, alpha:1.0)) + return Color(UIColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.0)) } } - + static func standardColorById(id: Int) -> Color { let colors = [ - rgb(88,86,214), - rgb(52,170,220), - rgb(90,200,250), - rgb(76,217,100), - rgb(255,59,48), - rgb(255,59,48), - rgb(255,149,0), - rgb(255,204,0) + rgb(88, 86, 214), + rgb(52, 170, 220), + rgb(90, 200, 250), + rgb(76, 217, 100), + rgb(255, 59, 48), + rgb(255, 59, 48), + rgb(255, 149, 0), + rgb(255, 204, 0), ] return colors[id % colors.count] } - + private static func rgb(_ r: Int, _ g: Int, _ b: Int) -> Color { - Color(red: Double(r)/255.0, green: Double(g)/255.0, blue: Double(b)/255.0, opacity: 1) + Color(red: Double(r) / 255.0, green: Double(g) / 255.0, blue: Double(b) / 255.0, opacity: 1) } } extension String { var isValidURL: Bool { let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) { - return (match.range.length == self.utf16.count) && (self.contains("https://") || self.contains("http://")) + if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: utf16.count)) { + return (match.range.length == utf16.count) && (contains("https://") || contains("http://")) } return false } - + var isValidEmail: Bool { // here, `try!` will always succeed because the pattern is valid let regex = try! NSRegularExpression(pattern: "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", options: .caseInsensitive) @@ -68,14 +68,14 @@ extension String { extension JSONDecoder { convenience init(dateFormatter: DateFormatter) { self.init() - self.dateDecodingStrategy = .formatted(dateFormatter) + dateDecodingStrategy = .formatted(dateFormatter) } } extension JSONEncoder { convenience init(dateFormatter: DateFormatter) { self.init() - self.dateEncodingStrategy = .formatted(dateFormatter) + dateEncodingStrategy = .formatted(dateFormatter) } } @@ -83,26 +83,25 @@ extension DateFormatter { static let cospend: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" - + return formatter }() } struct TextFieldContainer: UIViewRepresentable { - private var placeholder : String - private var text : Binding - - init(_ placeholder:String, text:Binding) { + private var placeholder: String + private var text: Binding + + init(_ placeholder: String, text: Binding) { self.placeholder = placeholder self.text = text } - + func makeCoordinator() -> TextFieldContainer.Coordinator { Coordinator(self) } - + func makeUIView(context: UIViewRepresentableContext) -> UITextField { - let innertTextField = UITextField(frame: .zero) innertTextField.keyboardType = .URL innertTextField.autocorrectionType = .no @@ -110,60 +109,66 @@ struct TextFieldContainer: UIViewRepresentable { innertTextField.placeholder = placeholder innertTextField.text = text.wrappedValue innertTextField.delegate = context.coordinator - + context.coordinator.setup(innertTextField) - + return innertTextField } - - func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext) { - uiView.text = self.text.wrappedValue + + func updateUIView(_ uiView: UITextField, context _: UIViewRepresentableContext) { + uiView.text = text.wrappedValue } - + class Coordinator: NSObject, UITextFieldDelegate { var parent: TextFieldContainer - + init(_ textFieldContainer: TextFieldContainer) { - self.parent = textFieldContainer + parent = textFieldContainer } - - func setup(_ textField:UITextField) { + + func setup(_ textField: UITextField) { textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) } - + @objc func textFieldDidChange(_ textField: UITextField) { - self.parent.text.wrappedValue = textField.text ?? "" - + parent.text.wrappedValue = textField.text ?? "" + let newPosition = textField.endOfDocument textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) } } } + extension StringProtocol { func index(of string: S, options: String.CompareOptions = []) -> Index? { range(of: string, options: options)?.lowerBound } + func endIndex(of string: S, options: String.CompareOptions = []) -> Index? { range(of: string, options: options)?.upperBound } + func indices(of string: S, options: String.CompareOptions = []) -> [Index] { var indices: [Index] = [] var startIndex = self.startIndex while startIndex < endIndex, let range = self[startIndex...] - .range(of: string, options: options) { + .range(of: string, options: options) + { indices.append(range.lowerBound) startIndex = range.lowerBound < range.upperBound ? range.upperBound : index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex } return indices } + func ranges(of string: S, options: String.CompareOptions = []) -> [Range] { var result: [Range] = [] var startIndex = self.startIndex while startIndex < endIndex, let range = self[startIndex...] - .range(of: string, options: options) { + .range(of: string, options: options) + { result.append(range) startIndex = range.lowerBound < range.upperBound ? range.upperBound : index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex @@ -177,8 +182,8 @@ typealias ProjectData = (server: URL?, project: String?, passwd: String?) extension URL { func decodeMoneyBusterString() -> ProjectData { guard absoluteString.hasPrefix("https://net.eneiluj.moneybuster.cospend/"), - pathComponents.count >= 3, pathComponents.count <= 4 else { return (nil,nil,nil)} - return (URL(string: "https://" + pathComponents[1]),pathComponents[2],pathComponents[safe: 3]) + pathComponents.count >= 3, pathComponents.count <= 4 else { return (nil, nil, nil) } + return (URL(string: "https://" + pathComponents[1]), pathComponents[2], pathComponents[safe: 3]) } } @@ -188,8 +193,9 @@ extension URL { let scheme = scheme, scheme.localizedCaseInsensitiveContains("cospend"), pathComponents.count >= 2, - pathComponents.count <= 3 else { - return (nil,nil,nil) + pathComponents.count <= 3 + else { + return (nil, nil, nil) } return (URL(string: "https://\(host)"), pathComponents[1], diff --git a/PayForMe/Util/Views+Extensions.swift b/PayForMe/Util/Views+Extensions.swift index 8611d69..6e0d426 100644 --- a/PayForMe/Util/Views+Extensions.swift +++ b/PayForMe/Util/Views+Extensions.swift @@ -11,20 +11,19 @@ import SwiftUI extension View { func fancyStyle(active: Bool = true) -> some View { - self - .padding(10) + padding(10) .background(active ? Color.blue : Color.gray) .foregroundColor(.white) - + .cornerRadius(10) .shadow(color: (active ? Color.blue : Color.gray).opacity(0.5), radius: 4, x: 2, y: 2) } - + func eraseToAnyView() -> AnyView { AnyView(self) } - + func addFloatingAddButton() -> some View { - self.modifier(FloatingAddButtonViewModifier()) + modifier(FloatingAddButtonViewModifier()) } } diff --git a/PayForMe/Views/Balance/AddMemberView.swift b/PayForMe/Views/Balance/AddMemberView.swift index 9615056..8238d69 100644 --- a/PayForMe/Views/Balance/AddMemberView.swift +++ b/PayForMe/Views/Balance/AddMemberView.swift @@ -9,23 +9,22 @@ import SwiftUI struct AddMemberView: View { - @Binding var memberName: String - - var addMemberAction: () -> () - var cancelButtonAction: () -> () - + + var addMemberAction: () -> Void + var cancelButtonAction: () -> Void + var cancelButtonWidth: CGFloat = 10 - + var body: some View { VStack(alignment: .center) { HStack(alignment: .center) { EmptyView() Spacer() Text("Add member") - .font(.headline) - .multilineTextAlignment(.center) + .font(.headline) + .multilineTextAlignment(.center) .padding(.trailing, -cancelButtonWidth) Spacer() Button(action: cancelButtonAction, label: { Image(systemName: "xmark.circle.fill") }) @@ -33,13 +32,12 @@ struct AddMemberView: View { } TextField("Member name", text: $memberName) .multilineTextAlignment(.center) - .textFieldStyle(RoundedBorderTextFieldStyle()) + .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() FancyButton(add: false, action: addMemberAction, text: "Submit") } .padding() } - } struct AddMemberView_Previews: PreviewProvider { diff --git a/PayForMe/Views/Balance/BalanceList.swift b/PayForMe/Views/Balance/BalanceList.swift index c056e06..0c96527 100644 --- a/PayForMe/Views/Balance/BalanceList.swift +++ b/PayForMe/Views/Balance/BalanceList.swift @@ -9,37 +9,36 @@ import SwiftUI struct BalanceList: View { - @ObservedObject var viewModel: BalanceViewModel - + @State var addingUser = false - + @State var memberName = "" - + var body: some View { NavigationView { mainView - .navigationBarItems(trailing: !addingUser ? FancyButton(add: true, action: showAddUser, text: "") : nil) - .navigationBarTitle("Members") - .onAppear { - ProjectManager.shared.loadBillsAndMembers() - } + .navigationBarItems(trailing: !addingUser ? FancyButton(add: true, action: showAddUser, text: "") : nil) + .navigationBarTitle("Members") + .onAppear { + ProjectManager.shared.loadBillsAndMembers() + } }.navigationViewStyle(StackNavigationViewStyle()) } - + var mainView: some View { VStack(alignment: .center) { if addingUser { AddMemberView(memberName: $memberName, addMemberAction: submitUser, cancelButtonAction: cancelAddUser) } list - .addFloatingAddButton() + .addFloatingAddButton() } } - + var list: some View { List { ForEach(viewModel.balances.sorted(by: balanceSort(_:_:))) { @@ -54,15 +53,15 @@ struct BalanceList: View { } } } - + func balanceSort(_ a: Balance, _ b: Balance) -> Bool { (a.amount > b.amount) || ((a.amount == b.amount) && (a.person.name < b.person.name)) } - + func showAddUser() { addingUser = true } - + func submitUser() { ProjectManager.shared.addMember(memberName) { self.addingUser = false @@ -70,14 +69,14 @@ struct BalanceList: View { ProjectManager.shared.loadBillsAndMembers() } } - + func cancelAddUser() { - self.memberName = "" - self.addingUser = false + memberName = "" + addingUser = false } - + func createSettlingBill(balance: Balance) -> Bill { - let ower = viewModel.balances.sorted(by: {$0.amount > $1.amount})[0] + let ower = viewModel.balances.sorted(by: { $0.amount > $1.amount })[0] let payer = balance.person let topic = "Settling balance for \(balance.person.name)" let amount = ower.amount.magnitude < balance.amount.magnitude ? ower.amount : balance.amount.magnitude @@ -85,7 +84,6 @@ struct BalanceList: View { } } - struct BalanceList_Previews: PreviewProvider { static var previews: some View { let vm = BalanceViewModel() @@ -98,14 +96,14 @@ struct BalanceList_Previews: PreviewProvider { struct BalanceCell: View { @State var balance: Balance - + var body: some View { HStack { PersonText(person: balance.person) Spacer() - Text(" \(String(format:"%.2f",balance.amount))") + Text(" \(String(format: "%.2f", balance.amount))") .font(.headline) - .foregroundColor( balance.amount >= 0 ? Color.primary : Color.red) + .foregroundColor(balance.amount >= 0 ? Color.primary : Color.red) }.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)) } } diff --git a/PayForMe/Views/Balance/BalanceViewModel.swift b/PayForMe/Views/Balance/BalanceViewModel.swift index 51ee718..fd6d6a7 100644 --- a/PayForMe/Views/Balance/BalanceViewModel.swift +++ b/PayForMe/Views/Balance/BalanceViewModel.swift @@ -6,28 +6,27 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import Foundation import Combine +import Foundation import SwiftUI class BalanceViewModel: ObservableObject { - var manager = ProjectManager.shared var cancellable: Cancellable? - + @Published var currentProject: Project - + @Published var balances = [Balance]() - + init() { - self.currentProject = manager.currentProject - self.setBalances() - - self.cancellable = currentProjectChanged + currentProject = manager.currentProject + setBalances() + + cancellable = currentProjectChanged } - + var currentProjectChanged: AnyCancellable { manager.$currentProject .sink { @@ -35,15 +34,16 @@ class BalanceViewModel: ObservableObject { self.setBalances() } } - + func setBalances() { balances = currentProject.members.values.map { member in let paid = currentProject.bills.filter { $0.payer_id == member.id }.map { $0.amount }.reduce(0.0, +) let owes = currentProject.bills.compactMap { bill in - bill.owers.first { ower in ower.id == member.id } == nil ? nil : bill.amount / Double( bill.owers.count) } - .reduce(0.0, -) - + bill.owers.first { ower in ower.id == member.id } == nil ? nil : bill.amount / Double(bill.owers.count) + } + .reduce(0.0, -) + return Balance(id: member.id, amount: paid + owes, person: member) } } diff --git a/PayForMe/Views/BillDetail/AddBillView.swift b/PayForMe/Views/BillDetail/AddBillView.swift index aa76d33..0f6f634 100644 --- a/PayForMe/Views/BillDetail/AddBillView.swift +++ b/PayForMe/Views/BillDetail/AddBillView.swift @@ -9,10 +9,9 @@ import SwiftUI struct AddBillView: View { - @Binding var showModal: Bool - + var body: some View { NavigationView { BillDetailView(showModal: $showModal, viewModel: BillDetailViewModel(currentBill: Bill.newBill()), navBarTitle: "Add Bill") @@ -20,8 +19,8 @@ struct AddBillView: View { } } -//struct AddBillView_Previews: PreviewProvider { +// struct AddBillView_Previews: PreviewProvider { // static var previews: some View { // AddBillView() // } -//} +// } diff --git a/PayForMe/Views/BillDetail/BillDetailView.swift b/PayForMe/Views/BillDetail/BillDetailView.swift index b2dfbf9..e1b72dc 100644 --- a/PayForMe/Views/BillDetail/BillDetailView.swift +++ b/PayForMe/Views/BillDetail/BillDetailView.swift @@ -6,34 +6,33 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI -import Foundation import Combine +import Foundation import SlickLoadingSpinner +import SwiftUI struct BillDetailView: View { - @Environment(\.presentationMode) var presentationMode: Binding - + @Binding var showModal: Bool - + @ObservedObject var viewModel: BillDetailViewModel - + var navBarTitle = LocalizedStringKey("Add Bill") var sendButtonTitle = LocalizedStringKey("Create Bill") - + @State var noneAllToggle = 1 - + @State var sendBillButtonDisabled = true - + @State var sendingInProgress = LoadingState.notStarted - + var body: some View { VStack { Form { @@ -50,7 +49,6 @@ struct BillDetailView: View { Section(header: Text("Owers")) { PotentialOwersView(vm: viewModel.povm) } - } FancyLoadingButton(isLoading: sendingInProgress, add: false, action: self.sendBillToServer, text: showModal ? "Create Bill" : "Update Bill") .disabled(sendBillButtonDisabled) @@ -62,17 +60,17 @@ struct BillDetailView: View { .background(Color.PFMBackground) .navigationBarTitle(navBarTitle, displayMode: .inline) } - + func sendBillToServer() async { - guard let newBill = self.viewModel.createBill() else { + guard let newBill = viewModel.createBill() else { print("Could not create bill") return } sendingInProgress = .connecting await ProjectManager.shared.saveBill(newBill) - self.sendingInProgress = .success + sendingInProgress = .success ProjectManager.shared.loadBillsAndMembers() - self.showModal.toggle() + showModal.toggle() DispatchQueue.main.async { self.presentationMode.wrappedValue.dismiss() } @@ -86,4 +84,3 @@ struct BillDetailView_Previews: PreviewProvider { return BillDetailView(showModal: .constant(true), viewModel: vm) } } - diff --git a/PayForMe/Views/BillDetail/BillDetailViewModel.swift b/PayForMe/Views/BillDetail/BillDetailViewModel.swift index 360e591..6e3d58d 100644 --- a/PayForMe/Views/BillDetail/BillDetailViewModel.swift +++ b/PayForMe/Views/BillDetail/BillDetailViewModel.swift @@ -6,48 +6,47 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import Foundation import Combine +import Foundation class BillDetailViewModel: ObservableObject { - var manager = ProjectManager.shared var cancellable: Cancellable? - + @Published var topic = "" - + @Published var amount = "" - + @Published var selectedPayer = 1 - + @Published var currentProject: Project = demoProject - + @Published var currentBill: Bill - + var povm: PotentialOwersViewModel - + init(currentBill: Bill) { self.currentBill = currentBill - self.povm = PotentialOwersViewModel(members: ProjectManager.shared.currentProject.members) - + povm = PotentialOwersViewModel(members: ProjectManager.shared.currentProject.members) + manager.$currentProject.assign(to: &$currentProject) - + prefillData() } - + var validatedInput: AnyPublisher { return Publishers.CombineLatest3($topic, validatedAmount, povm.anyOwers) .map { topic, validatedAmount, anyOwers in - return !topic.isEmpty && validatedAmount && anyOwers - } - .eraseToAnyPublisher() + !topic.isEmpty && validatedAmount && anyOwers + } + .eraseToAnyPublisher() } - + var validatedAmount: AnyPublisher { return $amount.map { amount in let safeAmount = amount.replacingOccurrences(of: ",", with: ".") @@ -55,33 +54,29 @@ class BillDetailViewModel: ObservableObject { } .eraseToAnyPublisher() } - + func createBill() -> Bill? { let safeAmount = amount.replacingOccurrences(of: ",", with: ".") guard let doubleAmount = Double(safeAmount) else { return nil } - + let billID = currentBill.id let date = currentBill.date - + let actualOwers = povm.actualOwers() - - + return Bill(id: billID, amount: doubleAmount, what: topic, date: date, payer_id: selectedPayer, owers: actualOwers, repeat: currentProject.backend == .cospend ? "n" : nil, lastchanged: 0) - } - - + func prefillData() { - - self.topic = currentBill.what + topic = currentBill.what if currentBill.amount != 0 { - self.amount = String(currentBill.amount) + amount = String(currentBill.amount) } - - self.selectedPayer = currentBill.payer_id - currentBill.owers.forEach { (person) in + + selectedPayer = currentBill.payer_id + currentBill.owers.forEach { person in if let index = povm.members.firstIndex(of: person) { povm.isOwing[index] = true } diff --git a/PayForMe/Views/BillDetail/CommunicationIndicator.swift b/PayForMe/Views/BillDetail/CommunicationIndicator.swift index 536772b..8094d53 100644 --- a/PayForMe/Views/BillDetail/CommunicationIndicator.swift +++ b/PayForMe/Views/BillDetail/CommunicationIndicator.swift @@ -11,15 +11,11 @@ import SwiftUI struct CommunicationIndicator: View { var body: some View { ZStack { - RadialGradient(gradient: Gradient(colors: [Color.white, Color.gray]), center: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, startRadius: /*@START_MENU_TOKEN@*/5/*@END_MENU_TOKEN@*/, endRadius: /*@START_MENU_TOKEN@*/500/*@END_MENU_TOKEN@*/) + RadialGradient(gradient: Gradient(colors: [Color.white, Color.gray]), center: /*@START_MENU_TOKEN@*/ .center/*@END_MENU_TOKEN@*/, startRadius: /*@START_MENU_TOKEN@*/5/*@END_MENU_TOKEN@*/, endRadius: /*@START_MENU_TOKEN@*/500/*@END_MENU_TOKEN@*/) .scaleEffect(1.5) .opacity(0.5) LoadingRings() - } - - - } } @@ -30,11 +26,10 @@ struct CommunicationIndicator_Previews: PreviewProvider { } struct LoadingRings: View { - @State var spin3D_x = false @State var spin3D_y = false @State var spin3D_xy = false - + var body: some View { ZStack { Circle() // Large circle @@ -43,27 +38,27 @@ struct LoadingRings: View { .foregroundColor(.red) .rotation3DEffect(.degrees(spin3D_x ? 180 : 1), axis: (x: spin3D_x ? 1 : 0, y: 0, z: 0)) .animation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) - .onAppear(){ + .onAppear { self.spin3D_x.toggle() - } + } Circle() // Middle circle .stroke(lineWidth: 5) .frame(width: 60, height: 60) .foregroundColor(.green) .rotation3DEffect(.degrees(spin3D_y ? 360 : 1), axis: (x: 0, y: spin3D_y ? 1 : 0, z: 0)) .animation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) - .onAppear(){ + .onAppear { self.spin3D_y.toggle() - } + } Circle() // Inner Circle .stroke(lineWidth: 5) .frame(width: 20, height: 20) .foregroundColor(.blue) .rotation3DEffect(.degrees(spin3D_xy ? 180 : 1), axis: (x: spin3D_xy ? 0 : 1, y: spin3D_xy ? 0 : 1, z: 0)) .animation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) - .onAppear(){ + .onAppear { self.spin3D_xy.toggle() - } + } } } } diff --git a/PayForMe/Views/BillDetail/PotentialOwersView.swift b/PayForMe/Views/BillDetail/PotentialOwersView.swift index 67dfc9b..523f0c4 100644 --- a/PayForMe/Views/BillDetail/PotentialOwersView.swift +++ b/PayForMe/Views/BillDetail/PotentialOwersView.swift @@ -9,7 +9,6 @@ import SwiftUI struct PotentialOwersView: View { - @ObservedObject var vm: PotentialOwersViewModel @@ -23,7 +22,7 @@ struct PotentialOwersView: View { ForEach(vm.isOwing.indices, id: \.self) { index in Toggle(isOn: self.$vm.isOwing[index]) { - Text(self.vm.members[index].name ) + Text(self.vm.members[index].name) } } } diff --git a/PayForMe/Views/BillDetail/PotentialOwersViewModel.swift b/PayForMe/Views/BillDetail/PotentialOwersViewModel.swift index 31bf391..3de4201 100644 --- a/PayForMe/Views/BillDetail/PotentialOwersViewModel.swift +++ b/PayForMe/Views/BillDetail/PotentialOwersViewModel.swift @@ -6,50 +6,48 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import Foundation import Combine +import Foundation class PotentialOwersViewModel: ObservableObject { - @Published var members: [Person] - + @Published - var isOwing: [Bool]{ + var isOwing: [Bool] { didSet { owingStatus = 0 } } - + @Published var owingStatus = 0 - - + var owersCancellable: AnyCancellable? - - var anyOwers: AnyPublisher { + + var anyOwers: AnyPublisher { return Publishers.Map(upstream: $isOwing) { isOwing in isOwing.contains(true) }.eraseToAnyPublisher() } - - init(members: [Int:Person]) { - self.members = Array(members.values).sorted(by: {$0.name.lowercased() < $1.name.lowercased()}) + + init(members: [Int: Person]) { + self.members = Array(members.values).sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) isOwing = [Bool].init(repeating: false, count: members.count) - - owersCancellable = $owingStatus.sink { (newValue) in + + owersCancellable = $owingStatus.sink { newValue in switch newValue { - case 1: - self.isOwing = self.isOwing.map {_ in false} - case 2: - self.isOwing = self.isOwing.map {_ in true} - default: - break + case 1: + self.isOwing = self.isOwing.map { _ in false } + case 2: + self.isOwing = self.isOwing.map { _ in true } + default: + break } } } - + func actualOwers() -> [Person] { var persons = [Person]() for index in isOwing.indices { diff --git a/PayForMe/Views/BillDetail/WhoPaidView.swift b/PayForMe/Views/BillDetail/WhoPaidView.swift index 953df7e..a96ddc8 100644 --- a/PayForMe/Views/BillDetail/WhoPaidView.swift +++ b/PayForMe/Views/BillDetail/WhoPaidView.swift @@ -11,12 +11,11 @@ import SwiftUI struct WhoPaidView: View { @State var members: [Person] - + @Binding var selectedPayer: Int - + var body: some View { - if members.count <= 4 { return AnyView(Picker(selection: $selectedPayer, label: Text("Payer")) { ForEach(members) { @@ -28,10 +27,10 @@ struct WhoPaidView: View { return AnyView( Picker(selection: $selectedPayer, label: Text("Payer")) { ForEach(members) { - member in + member in PersonText(person: member) - } - }.pickerStyle(DefaultPickerStyle())) + } + }.pickerStyle(DefaultPickerStyle())) } } } diff --git a/PayForMe/Views/BillList/BillCell.swift b/PayForMe/Views/BillList/BillCell.swift index b0c6945..f5b16b4 100644 --- a/PayForMe/Views/BillList/BillCell.swift +++ b/PayForMe/Views/BillList/BillCell.swift @@ -9,27 +9,26 @@ import SwiftUI struct BillCell: View { - @ObservedObject var viewModel: BillListViewModel - + @State var bill: Bill - + var body: some View { HStack(alignment: .top) { - VStack(alignment: .leading, spacing: 10) { - Text(bill.what).font(.headline) - PersonsView(bill: $bill, members: $viewModel.currentProject.members) - } - Spacer() - VStack(alignment: .trailing, spacing: 10) { - Text(amountString()).font(.headline) - Text(DateFormatter.cospend.string(from: bill.date)).font(.subheadline) + VStack(alignment: .leading, spacing: 10) { + Text(bill.what).font(.headline) + PersonsView(bill: $bill, members: $viewModel.currentProject.members) + } + Spacer() + VStack(alignment: .trailing, spacing: 10) { + Text(amountString()).font(.headline) + Text(DateFormatter.cospend.string(from: bill.date)).font(.subheadline) } } } - + func amountString() -> String { return "\(String(format: "%.2f", bill.amount))" } diff --git a/PayForMe/Views/BillList/BillList.swift b/PayForMe/Views/BillList/BillList.swift index 388eb40..77d5f0e 100644 --- a/PayForMe/Views/BillList/BillList.swift +++ b/PayForMe/Views/BillList/BillList.swift @@ -9,20 +9,19 @@ import SwiftUI struct BillList: View { - @ObservedObject var viewModel: BillListViewModel - + @State var deleteAlert: IndexSet? - + var body: some View { NavigationView { List { if #available(iOS 15, *) { iOS15ListContent } else { - iOS14ListContent + iOS14ListContent } } .addFloatingAddButton() @@ -32,8 +31,8 @@ struct BillList: View { Alert(title: Text("Delete Bill"), message: Text("Do you really want to erase the bill from the server?"), primaryButton: .destructive(Text("Sure")) { - self.deleteBill(at: index) - }, + self.deleteBill(at: index) + }, secondaryButton: .cancel()) } .listStyle(InsetGroupedListStyle()) @@ -42,45 +41,45 @@ struct BillList: View { ProjectManager.shared.loadBillsAndMembers() } } - + @ViewBuilder var iOS15ListContent: some View { - Section(header: Picker("Sort by", selection: $viewModel.sortBy) { - Text("Expense date").tag(BillListViewModel.SortedBy.expenseDate) - Text("Changed date").tag(BillListViewModel.SortedBy.changedDate) - }.pickerStyle(SegmentedPickerStyle())) { - ForEach(viewModel.sortedBills) { bill in - NavigationLink(destination: - BillDetailView(showModal: .constant(false), - viewModel: BillDetailViewModel(currentBill: bill), - navBarTitle: "Edit Bill", - sendButtonTitle: "Update Bill")) { - BillCell(viewModel: self.viewModel, bill: bill) + Section(header: Picker("Sort by", selection: $viewModel.sortBy) { + Text("Expense date").tag(BillListViewModel.SortedBy.expenseDate) + Text("Changed date").tag(BillListViewModel.SortedBy.changedDate) + }.pickerStyle(SegmentedPickerStyle())) { + ForEach(viewModel.sortedBills) { bill in + NavigationLink(destination: + BillDetailView(showModal: .constant(false), + viewModel: BillDetailViewModel(currentBill: bill), + navBarTitle: "Edit Bill", + sendButtonTitle: "Update Bill")) { + BillCell(viewModel: self.viewModel, bill: bill) + } } + .onDelete(perform: { + offset in + self.deleteAlert = offset + }) } - .onDelete(perform: { - offset in - self.deleteAlert = offset - }) - } } - + @ViewBuilder var iOS14ListContent: some View { Section { Picker("Sort by", selection: $viewModel.sortBy) { - Text("Expense date").tag(BillListViewModel.SortedBy.expenseDate) - Text("Changed date").tag(BillListViewModel.SortedBy.changedDate) - }.pickerStyle(SegmentedPickerStyle()) + Text("Expense date").tag(BillListViewModel.SortedBy.expenseDate) + Text("Changed date").tag(BillListViewModel.SortedBy.changedDate) + }.pickerStyle(SegmentedPickerStyle()) } Section { ForEach(viewModel.sortedBills) { bill in NavigationLink(destination: - BillDetailView(showModal: .constant(false), - viewModel: BillDetailViewModel(currentBill: bill), - navBarTitle: "Edit Bill", - sendButtonTitle: "Update Bill")) { - BillCell(viewModel: self.viewModel, bill: bill) + BillDetailView(showModal: .constant(false), + viewModel: BillDetailViewModel(currentBill: bill), + navBarTitle: "Edit Bill", + sendButtonTitle: "Update Bill")) { + BillCell(viewModel: self.viewModel, bill: bill) } } .onDelete(perform: { @@ -89,7 +88,7 @@ struct BillList: View { }) } } - + func deleteBill(at offsets: IndexSet) { for offset in offsets { guard let bill = viewModel.sortedBills[safe: offset] else { diff --git a/PayForMe/Views/BillList/BillListViewModel.swift b/PayForMe/Views/BillList/BillListViewModel.swift index 16f6fa6..453091a 100644 --- a/PayForMe/Views/BillList/BillListViewModel.swift +++ b/PayForMe/Views/BillList/BillListViewModel.swift @@ -6,47 +6,46 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import Foundation import Combine +import Foundation class BillListViewModel: ObservableObject { - var manager = ProjectManager.shared var cancellable: Cancellable? - + @Published var currentProject: Project - + @Published var sortBy = SortedBy.expenseDate - + @Published var sorter = "" - + @Published var sortedBills = [Bill]() - + init() { - self.currentProject = manager.currentProject - self.cancellable = currentProjectChanged + currentProject = manager.currentProject + cancellable = currentProjectChanged $sortBy .map { $0.sort(bills: self.currentProject.bills) } .assign(to: &$sortedBills) } - + var currentProjectChanged: AnyCancellable { manager.$currentProject .assign(to: \.currentProject, on: self) } - + enum SortedBy: String { case expenseDate case changedDate - + func sort(bills: [Bill]) -> [Bill] { - switch(self) { + switch self { case .expenseDate: return bills.sorted { a, b in a.date > b.date diff --git a/PayForMe/Views/BillList/PersonsView.swift b/PayForMe/Views/BillList/PersonsView.swift index 75c8dce..5278fa3 100644 --- a/PayForMe/Views/BillList/PersonsView.swift +++ b/PayForMe/Views/BillList/PersonsView.swift @@ -11,10 +11,10 @@ import SwiftUI struct PersonsView: View { @Binding var bill: Bill - + @Binding - var members: [Int:Person] - + var members: [Int: Person] + var body: some View { HStack(spacing: 5) { payerText() @@ -22,11 +22,11 @@ struct PersonsView: View { owersTexts() }.font(.headline) } - + func payerText() -> some View { PersonText(person: payer) } - + func owersTexts() -> some View { HStack(spacing: 1) { ForEach(bill.owers) { @@ -38,20 +38,16 @@ struct PersonsView: View { } }.padding(0) } - + func realPerson(_ ower: Person) -> Person { guard let person = members[ower.id] else { return ower } return person } - - - + var payer: Person { - get { - members[bill.payer_id] ?? Person(id: 1, weight: 1, name: "Unknown", activated: true) - } + members[bill.payer_id] ?? Person(id: 1, weight: 1, name: "Unknown", activated: true) } } diff --git a/PayForMe/Views/ContentView.swift b/PayForMe/Views/ContentView.swift index e16287a..0dfb88f 100644 --- a/PayForMe/Views/ContentView.swift +++ b/PayForMe/Views/ContentView.swift @@ -6,26 +6,25 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI import Foundation +import SwiftUI struct ContentView: View { - @ObservedObject var manager = ProjectManager.shared - + @State var tabBarIndex = tabBarItems.BillList - + @State var showModal = false - + @State var hidePlusButton = false - + var body: some View { ZStack { - if !manager.projects.isEmpty { + if !manager.projects.isEmpty { tabBar } else { OnboardingView() @@ -35,23 +34,23 @@ struct ContentView: View { AddFromURLView(viewmodel: AddProjectQRViewModel(openedByURL: url)) } } - + var tabBar: some View { - TabView(selection: $tabBarIndex){ + TabView(selection: $tabBarIndex) { BillList(viewModel: BillListViewModel()) - .tabItem({ + .tabItem { Image(systemName: "rectangle.stack") - }) + } .tag(tabBarItems.BillList) BalanceList(viewModel: BalanceViewModel()) - .tabItem({ + .tabItem { Image(systemName: "arrow.right.arrow.left") - }) + } .tag(tabBarItems.Balance) ProjectList() - .tabItem({ + .tabItem { Image(systemName: "gear") - }) + } .tag(tabBarItems.ServerList) } } @@ -65,7 +64,7 @@ enum tabBarItems: Int { struct ContentView_Previews: PreviewProvider { static var previews: some View { - previewProjects.forEach{ + previewProjects.forEach { ProjectManager.shared.addProject($0) } ProjectManager.shared.currentProject = previewProject diff --git a/PayForMe/Views/FancyButton.swift b/PayForMe/Views/FancyButton.swift index e7fa94e..afe26ae 100644 --- a/PayForMe/Views/FancyButton.swift +++ b/PayForMe/Views/FancyButton.swift @@ -9,14 +9,13 @@ import SwiftUI struct FancyButton: View { - @Environment(\.isEnabled) private var isEnabled: Bool - + var add: Bool - + var action: () -> Void var text: String - + var body: some View { return Button(action: action) { if add { diff --git a/PayForMe/Views/FancyLoadingButton.swift b/PayForMe/Views/FancyLoadingButton.swift index 38fce12..9a214bc 100644 --- a/PayForMe/Views/FancyLoadingButton.swift +++ b/PayForMe/Views/FancyLoadingButton.swift @@ -6,36 +6,36 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI import SlickLoadingSpinner +import SwiftUI struct FancyLoadingButton: View { - @Environment(\.isEnabled) private var isEnabled: Bool - + let isLoading: LoadingState - + var add: Bool - + var action: () async -> Void var text: String - + var body: some View { switch isLoading { case .notStarted: return Button(action: { Task { await action() - }}) { - if add { - Image(systemName: "plus") - } else { - Text(LocalizedStringKey(text)) - } } - .fancyStyle(active: self.isEnabled) - .disabled(!isEnabled) - .eraseToAnyView() + }) { + if add { + Image(systemName: "plus") + } else { + Text(LocalizedStringKey(text)) + } + } + .fancyStyle(active: self.isEnabled) + .disabled(!isEnabled) + .eraseToAnyView() default: return SlickLoadingSpinner(connectionState: isLoading) .frame(width: 50, height: 50) @@ -46,6 +46,6 @@ struct FancyLoadingButton: View { struct FancyBotton_Previews: PreviewProvider { static var previews: some View { - FancyLoadingButton(isLoading: .connecting, add: false, action: ({ return }), text: "Add Project").environment(\.locale, .init(identifier: "de")) + FancyLoadingButton(isLoading: .connecting, add: false, action: ({}), text: "Add Project").environment(\.locale, .init(identifier: "de")) } } diff --git a/PayForMe/Views/LoadingDots.swift b/PayForMe/Views/LoadingDots.swift index 8d9817d..f197d95 100644 --- a/PayForMe/Views/LoadingDots.swift +++ b/PayForMe/Views/LoadingDots.swift @@ -12,57 +12,54 @@ struct LoadingDots: View { @State private var leftAnimates = false @State private var middleAnimates = false @State private var rightAnimates = false - + var body: some View { - - // Middle HStack { - // Left Circle() .stroke(lineWidth: 3) .frame(width: 12, height: 12) .scaleEffect(leftAnimates ? 1 : 0) .animation(Animation.spring().repeatForever(autoreverses: false).speed(0.5)) - .onAppear() { + .onAppear { self.leftAnimates.toggle() - } - + } + Circle() .stroke(lineWidth: 3) .frame(width: 12, height: 12) .scaleEffect(middleAnimates ? 1 : 0) .animation(Animation.spring().repeatForever(autoreverses: false).speed(0.5).delay(0.15)) - .onAppear() { + .onAppear { self.middleAnimates.toggle() - } - + } + // Right Circle() .stroke(lineWidth: 3) .frame(width: 12, height: 12) .scaleEffect(rightAnimates ? 1 : 0) .animation(Animation.spring().repeatForever(autoreverses: false).speed(0.5).delay(0.25)) - .onAppear() { + .onAppear { self.rightAnimates.toggle() - } + } Circle() .stroke(lineWidth: 3) .frame(width: 12, height: 12) .scaleEffect(rightAnimates ? 1 : 0) .animation(Animation.spring().repeatForever(autoreverses: false).speed(0.5).delay(0.35)) - .onAppear() { + .onAppear { self.rightAnimates.toggle() - } + } Circle() .stroke(lineWidth: 3) .frame(width: 12, height: 12) .scaleEffect(rightAnimates ? 1 : 0) .animation(Animation.spring().repeatForever(autoreverses: false).speed(0.5).delay(0.45)) - .onAppear() { + .onAppear { self.rightAnimates.toggle() - } + } } } } diff --git a/PayForMe/Views/PersonText.swift b/PayForMe/Views/PersonText.swift index 1983a5f..f6e3561 100644 --- a/PayForMe/Views/PersonText.swift +++ b/PayForMe/Views/PersonText.swift @@ -9,10 +9,9 @@ import SwiftUI struct PersonText: View { - @State var person: Person - + var body: some View { Text(person.name) .padding(2) @@ -21,12 +20,12 @@ struct PersonText: View { .cornerRadius(5) .lineLimit(1) } - + func colorOfPerson(_ person: Person) -> Color { guard let color = person.color else { - return Color.standardColorById(id: person.id) + return Color.standardColorById(id: person.id) } - + return Color(color) } } diff --git a/PayForMe/Views/Projects/AddPasswordView.swift b/PayForMe/Views/Projects/AddPasswordView.swift index 46f24b2..145c4e4 100644 --- a/PayForMe/Views/Projects/AddPasswordView.swift +++ b/PayForMe/Views/Projects/AddPasswordView.swift @@ -6,18 +6,18 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI import SlickLoadingSpinner +import SwiftUI struct AddPasswordView: View { @Binding var password: String let connectionState: LoadingState - + let name: String let urlString: String - + var body: some View { - VStack(spacing:10) { + VStack(spacing: 10) { Text(urlString).font(.title) Text(name).font(.title) SecureField("Type password here", text: $password) @@ -32,6 +32,5 @@ struct AddPasswordView: View { struct AddPasswordView_Previews: PreviewProvider { static var previews: some View { AddPasswordView(password: .constant(""), connectionState: .connecting, name: "Test", urlString: "myserver.de") - } } diff --git a/PayForMe/Views/Projects/Manual/AddProjectManualView.swift b/PayForMe/Views/Projects/Manual/AddProjectManualView.swift index 758f4af..3e4310e 100644 --- a/PayForMe/Views/Projects/Manual/AddProjectManualView.swift +++ b/PayForMe/Views/Projects/Manual/AddProjectManualView.swift @@ -6,18 +6,17 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI import Combine import SlickLoadingSpinner +import SwiftUI struct AddProjectManualView: View { - @Environment(\.presentationMode) var presentationMode: Binding - + @StateObject private var viewmodel = AddProjectManualViewModel() - + var body: some View { VStack { Text("Add project").font(.title) @@ -35,19 +34,20 @@ struct AddProjectManualView: View { } Form { Section( - header: Text( (viewmodel.projectType == .iHateMoney ? "Server Address (Optional)" : "Server Address")) + header: Text(viewmodel.projectType == .iHateMoney ? "Server Address (Optional)" : "Server Address") ) { TextFieldContainer( viewmodel.projectType == .cospend ? "https://mynextcloud.org" : "https://ihatemoney.org", - text: self.$viewmodel.serverAddress) + text: self.$viewmodel.serverAddress + ) } Section(header: Text("Project ID & Password")) { TextField("Enter project id", text: self.$viewmodel.projectName) .autocapitalization(.none) - + SecureField("Enter project password", text: self.$viewmodel.projectPassword) } } @@ -56,26 +56,26 @@ struct AddProjectManualView: View { SlickLoadingSpinner(connectionState: viewmodel.validationProgress) .frame(width: 50, height: 50) FancyButton( - add: false, - action: addButton, - text: "Add Project") - .disabled(viewmodel.validationProgress != .success) + add: false, + action: addButton, + text: "Add Project" + ) + .disabled(viewmodel.validationProgress != .success) if !viewmodel.errorText.isEmpty { Text(viewmodel.errorText) } Spacer() - } .padding(.horizontal, 20) .padding(.vertical, 50) .background(Color.PFMBackground) } - + func addButton() { viewmodel.addProject() - self.presentationMode.wrappedValue.dismiss() + presentationMode.wrappedValue.dismiss() } - + private func pasteLink() { if let pasteString = UIPasteboard.general.string { print(pasteString) diff --git a/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift b/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift index 907f17f..a05c974 100644 --- a/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift +++ b/PayForMe/Views/Projects/Manual/AddProjectManualViewModel.swift @@ -6,58 +6,56 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import Foundation -import UIKit import Combine +import Foundation import SlickLoadingSpinner +import UIKit class AddProjectManualViewModel: ObservableObject { - - @Published var projectType = ProjectBackend.cospend - + @Published var serverAddress = "" - + @Published var projectName = "" - + @Published var projectPassword = "" - + @Published var validationProgress = LoadingState.notStarted - + @Published var errorText = "" - + private var lastProjectTestedSuccessfully: Project? - + init() { validatedInput.map { _ in LoadingState.connecting }.assign(to: &$validationProgress) validatedServer.map { $0 == 200 ? LoadingState.success : LoadingState.failure }.assign(to: &$validationProgress) errorTextPublisher.assign(to: &$errorText) serverCheckUnsupportedPorts.assign(to: &$errorText) } - + func reset() { - self.serverAddress = "" - self.projectName = "" - self.projectPassword = "" + serverAddress = "" + projectName = "" + projectPassword = "" } - + func addProject() { guard let project = lastProjectTestedSuccessfully else { return } if !ProjectManager.shared.addProject(project) { errorText = "Project already exists!" } } - + func pasteAddress(address: String) { let trimmedAddress = address.trimmingCharacters(in: .whitespacesAndNewlines) guard let url = URL(string: trimmedAddress) else { return } - + // If it is a moneybuster URL let (mUrl, mName, mPassword) = url.decodeQRCode() if let url = mUrl, let name = mName { @@ -69,7 +67,7 @@ class AddProjectManualViewModel: ObservableObject { return } // If it is another url - + let pathComponents = url.pathComponents let pureUrl = url.deletingPathExtension().absoluteString let trimmIndices = url.absoluteString.indices(of: "/") @@ -81,21 +79,21 @@ class AddProjectManualViewModel: ObservableObject { } fillFieldsFromComponents(components: pathComponents) } - + var serverAddressFormatted: AnyPublisher { $serverAddress .map { $0.hasPrefix("https://") ? $0 : "https://\($0)" } .map { unformatted in - if let index = unformatted.index(of: "/index.php") { - if let url = URL(string: unformatted) { - self.fillFieldsFromComponents(components: url.pathComponents) + if let index = unformatted.index(of: "/index.php") { + if let url = URL(string: unformatted) { + self.fillFieldsFromComponents(components: url.pathComponents) + } + return String(unformatted[.. { serverAddressFormatted .map { @@ -106,22 +104,22 @@ class AddProjectManualViewModel: ObservableObject { .removeDuplicates() .eraseToAnyPublisher() } - + private func fillFieldsFromComponents(components: [String]) { if components.count == 6 { - self.projectPassword = components[5] - self.projectName = components[4] + projectPassword = components[5] + projectName = components[4] } if components.count == 5 { - self.projectName = components[4] + projectName = components[4] } } - - private var validatedAddress: AnyPublisher<(type: ProjectBackend, address: String?), Never> { + + private var validatedAddress: AnyPublisher<(type: ProjectBackend, address: String?), Never> { return Publishers.CombineLatest($projectType, serverAddressFormatted) .map { type, serverAddress in - if type == .iHateMoney && serverAddress == "https://" { + if type == .iHateMoney, serverAddress == "https://" { return (type, NetworkService.iHateMoneyURLString) } else { return (type, serverAddress) @@ -129,12 +127,12 @@ class AddProjectManualViewModel: ObservableObject { } .eraseToAnyPublisher() } - + var validatedInput: AnyPublisher { return Publishers.CombineLatest3(validatedAddress, $projectName, $projectPassword) .debounce(for: 1, scheduler: DispatchQueue.main) .compactMap { server, name, password in - if let address = server.address, address.isValidURL && !name.isEmpty && !password.isEmpty { + if let address = server.address, address.isValidURL, !name.isEmpty, !password.isEmpty { guard let url = URL(string: address) else { return nil } return Project(name: name.lowercased(), password: password, backend: server.0, url: url) } else { @@ -144,13 +142,13 @@ class AddProjectManualViewModel: ObservableObject { .removeDuplicates() .eraseToAnyPublisher() } - + private var validatedServer: AnyPublisher { validatedInput.flatMap { project in - return NetworkService.shared.foundProjectStatusCode(project) + NetworkService.shared.foundProjectStatusCode(project) } - .map {project, code in + .map { project, code in self.lastProjectTestedSuccessfully = project return code } @@ -158,12 +156,12 @@ class AddProjectManualViewModel: ObservableObject { .receive(on: RunLoop.main) .eraseToAnyPublisher() } - + private var errorTextPublisher: AnyPublisher { validatedServer .map { - statusCode in - switch statusCode { + statusCode in + switch statusCode { case 200: return "" case -1: @@ -172,9 +170,9 @@ class AddProjectManualViewModel: ObservableObject { return "Unauthorized: Wrong project id/pw" default: return "Server error: \(statusCode)" + } } - } - .removeDuplicates() - .eraseToAnyPublisher() + .removeDuplicates() + .eraseToAnyPublisher() } } diff --git a/PayForMe/Views/Projects/OnboardingView.swift b/PayForMe/Views/Projects/OnboardingView.swift index 9aad9ce..6175a06 100644 --- a/PayForMe/Views/Projects/OnboardingView.swift +++ b/PayForMe/Views/Projects/OnboardingView.swift @@ -9,9 +9,8 @@ import SwiftUI struct OnboardingView: View { - @State private var moreInfo = false - + var body: some View { NavigationView { VStack(spacing: 32) { @@ -29,7 +28,8 @@ struct OnboardingView: View { Image(systemName: "square.and.pencil") .resizable() .aspectRatio(contentMode: .fit) - }) + } + ) }.padding(.horizontal, 30) if moreInfo { Button(action: { @@ -38,7 +38,7 @@ struct OnboardingView: View { } }, label: { Image(systemName: "chevron.compact.up") - .resizable().aspectRatio(contentMode: .fit).frame(width:30) + .resizable().aspectRatio(contentMode: .fit).frame(width: 30) }) HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 5) { diff --git a/PayForMe/Views/Projects/ProjectList.swift b/PayForMe/Views/Projects/ProjectList.swift index a506044..5455688 100644 --- a/PayForMe/Views/Projects/ProjectList.swift +++ b/PayForMe/Views/Projects/ProjectList.swift @@ -6,17 +6,16 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI import AVFoundation +import SwiftUI struct ProjectList: View { - @ObservedObject var manager = ProjectManager.shared - + @State private var addProject: AddingProjectMethod? @State private var shareProject: Project? - + var body: some View { NavigationView { VStack { @@ -32,31 +31,31 @@ struct ProjectList: View { } .navigationBarTitle("Projects") .navigationBarItems(trailing: - HStack { - Button(action: { - self.addProject = .manual - }) { - Image(systemName: "plus").fancyStyle() - } - Button(action: { - self.addProject = .qrCode - }) { - Image(systemName: "qrcode").fancyStyle() - } - } - .sheet(item: $addProject, content: { method -> AnyView in - switch method { - case .qrCode: - return destination.eraseToAnyView() - case .manual: - return AddProjectManualView().eraseToAnyView() - } - }) + HStack { + Button(action: { + self.addProject = .manual + }) { + Image(systemName: "plus").fancyStyle() + } + Button(action: { + self.addProject = .qrCode + }) { + Image(systemName: "qrcode").fancyStyle() + } + } + .sheet(item: $addProject, content: { method -> AnyView in + switch method { + case .qrCode: + return destination.eraseToAnyView() + case .manual: + return AddProjectManualView().eraseToAnyView() + } + }) ) } .navigationViewStyle(StackNavigationViewStyle()) } - + private func listRow(project: Project) -> some View { Button(action: { self.manager.setCurrentProject(project) @@ -83,25 +82,25 @@ struct ProjectList: View { } }) } - + private enum AddingProjectMethod: Int, CaseIterable, Identifiable { var id: Int { switch self { - case .qrCode: return 0 - case .manual: return 1 + case .qrCode: return 0 + case .manual: return 1 } } - + case qrCode case manual } - + func deleteProject(at offsets: IndexSet) { for index in offsets { manager.deleteProject(manager.projects[index]) } } - + var destination: some View { ProjectQRPermissionCheckerView() } diff --git a/PayForMe/Views/Projects/QRCodes/AddFromURLView.swift b/PayForMe/Views/Projects/QRCodes/AddFromURLView.swift index 3515959..67a9b34 100644 --- a/PayForMe/Views/Projects/QRCodes/AddFromURLView.swift +++ b/PayForMe/Views/Projects/QRCodes/AddFromURLView.swift @@ -6,18 +6,17 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI import SlickLoadingSpinner +import SwiftUI struct AddFromURLView: View { - @ObservedObject var viewmodel: AddProjectQRViewModel var body: some View { viewmodel.askForPassword ? AddPasswordView(password: $viewmodel.passwordText, connectionState: viewmodel.isProject, name: viewmodel.name, urlString: viewmodel.urlString).eraseToAnyView() : loadingView.eraseToAnyView() } - + var loadingView: some View { VStack { SlickLoadingSpinner(connectionState: viewmodel.isProject) diff --git a/PayForMe/Views/Projects/QRCodes/AddProjectQRView.swift b/PayForMe/Views/Projects/QRCodes/AddProjectQRView.swift index 69ab341..c27b887 100644 --- a/PayForMe/Views/Projects/QRCodes/AddProjectQRView.swift +++ b/PayForMe/Views/Projects/QRCodes/AddProjectQRView.swift @@ -6,18 +6,18 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI -import CarBode import AVFoundation +import CarBode import SlickLoadingSpinner +import SwiftUI struct AddProjectQRView: View { @StateObject var viewmodel = AddProjectQRViewModel() - + @Environment(\.presentationMode) var presentationMode - + @State var scanningCode: [AVMetadataObject.ObjectType] = [.qr] - + var body: some View { VStack { if viewmodel.askForPassword { @@ -28,46 +28,45 @@ struct AddProjectQRView: View { } .onReceive(viewmodel.$isProject, perform: { status in switch status { - case .success: - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now().advanced(by: .seconds(1)), execute: { - withAnimation { - presentationMode.wrappedValue.dismiss() - } - }) - break - case .failure: - scanningCode = [.qr] - default: - break + case .success: + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now().advanced(by: .seconds(1))) { + withAnimation { + presentationMode.wrappedValue.dismiss() + } + } + case .failure: + scanningCode = [.qr] + default: + break } }) } - + var passwordView: some View { AddPasswordView(password: $viewmodel.passwordText, connectionState: viewmodel.isProject, name: viewmodel.name, urlString: viewmodel.urlString) } - + var connectIndicator: some View { SlickLoadingSpinner(connectionState: viewmodel.isProject) .frame(width: 100, height: 100, alignment: .center) } - + var qrCodeScanner: some View { CBScanner( - supportBarcode: $scanningCode, //Set type of barcode you want to scan - scanInterval: .constant(5.0) //Event will trigger every 5 seconds, - ){ code in + supportBarcode: $scanningCode, // Set type of barcode you want to scan + scanInterval: .constant(5.0) // Event will trigger every 5 seconds, + ) { code in // If we find a QR code which is an url with at least 3 components, it can be a Cospend link guard let url = URL(string: code.value), url.pathComponents.count >= 3 else { return } - + scanningCode = [] self.viewmodel.scannedCode = url } .overlay(footer, alignment: .bottom) .edgesIgnoringSafeArea(.all) } - + var footer: some View { Group { if viewmodel.isProject == .notStarted { diff --git a/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift b/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift index fc872ec..b7f38f1 100644 --- a/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift +++ b/PayForMe/Views/Projects/QRCodes/AddProjectQRViewModel.swift @@ -6,38 +6,38 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import Foundation import Combine -import SwiftUI +import Foundation import SlickLoadingSpinner +import SwiftUI class AddProjectQRViewModel: ObservableObject { @Published var scannedCode: URL? @Published var text = "" @Published var askForPassword = false @Published var passwordText = "" - + @Published var url: URL? @Published var name = "" - + typealias ProjectConnectState = LoadingState @Published var isProject = ProjectConnectState.notStarted - + private var subscriptions = Set() - + init() { foundCodeSink.store(in: &subscriptions) passwordCorrect.assign(to: &$isProject) isTestingSubject.assign(to: &$isProject) } - + convenience init(openedByURL: URL?) { self.init() scannedCode = openedByURL } - - var isTestingSubject = PassthroughSubject() - + + var isTestingSubject = PassthroughSubject() + var passwordCorrect: AnyPublisher { Publishers.CombineLatest3( $url @@ -45,42 +45,41 @@ class AddProjectQRViewModel: ObservableObject { $name, $passwordText .debounce(for: 0.5, scheduler: RunLoop.main) - .compactMap { $0.isEmpty ? nil : $0} + .compactMap { $0.isEmpty ? nil : $0 } .removeDuplicates() ) - .map { url, name, password in - self.isTestingSubject.send(.connecting) - print("\(url) \(name) \(password)") - return Project(name: name, password: password, backend: .cospend, url: url) - } - .flatMap { project in - NetworkService.shared.foundProjectStatusCode(project) - } - .map { project, statusCode in - if statusCode == 200 { - ProjectManager.shared.addProject(project) - return withAnimation { - .success - } - } + .map { url, name, password in + self.isTestingSubject.send(.connecting) + print("\(url) \(name) \(password)") + return Project(name: name, password: password, backend: .cospend, url: url) + } + .flatMap { project in + NetworkService.shared.foundProjectStatusCode(project) + } + .map { project, statusCode in + if statusCode == 200 { + ProjectManager.shared.addProject(project) return withAnimation { - .failure + .success } } - .eraseToAnyPublisher() - + return withAnimation { + .failure + } + } + .eraseToAnyPublisher() } - + var urlString: String { url?.absoluteString ?? "URL wrong, please scan right barcode" } - + var foundCode: AnyPublisher { $scannedCode .compactMap { $0 } .eraseToAnyPublisher() } - + var foundCodeSink: AnyCancellable { foundCode .sink { codedUrl in diff --git a/PayForMe/Views/Projects/QRCodes/ProjectQRPermissionCheckerView.swift b/PayForMe/Views/Projects/QRCodes/ProjectQRPermissionCheckerView.swift index d74ec16..e4e57e3 100644 --- a/PayForMe/Views/Projects/QRCodes/ProjectQRPermissionCheckerView.swift +++ b/PayForMe/Views/Projects/QRCodes/ProjectQRPermissionCheckerView.swift @@ -6,34 +6,34 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI import AVFoundation +import SwiftUI struct ProjectQRPermissionCheckerView: View { @State var cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video) - + var body: some View { switch cameraAuthStatus { - case .authorized: - return AddProjectQRView().eraseToAnyView() - case .denied: - return permissionDeniedView.eraseToAnyView() - default: - AVCaptureDevice.requestAccess(for: .video) { _ in - cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video) - } - return Text("Please allow us to use the camera in order to scan the Cospend QR code").padding(20).eraseToAnyView() + case .authorized: + return AddProjectQRView().eraseToAnyView() + case .denied: + return permissionDeniedView.eraseToAnyView() + default: + AVCaptureDevice.requestAccess(for: .video) { _ in + cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video) + } + return Text("Please allow us to use the camera in order to scan the Cospend QR code").padding(20).eraseToAnyView() } } - + var permissionDeniedView: some View { VStack(spacing: 20) { Text("If you want to scan your project as a QR code, you need to allow this app to use your camera. Otherwise please navigate back and fill out the information manually.") Button("Go to settings") { if let url = URL(string: UIApplication.openSettingsURLString) { if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: {_ in + UIApplication.shared.open(url, options: [:], completionHandler: { _ in cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video) }) } diff --git a/PayForMe/Views/Projects/ShareProjectQRCode.swift b/PayForMe/Views/Projects/ShareProjectQRCode.swift index 4da0565..6f37a09 100644 --- a/PayForMe/Views/Projects/ShareProjectQRCode.swift +++ b/PayForMe/Views/Projects/ShareProjectQRCode.swift @@ -6,9 +6,9 @@ // Copyright © 2020 Mayflower GmbH. All rights reserved. // -import SwiftUI import AVFoundation import CarBode +import SwiftUI struct ShareProjectQRCode: View { let project: Project @@ -20,7 +20,7 @@ struct ShareProjectQRCode: View { } .padding() } - + var path: String { let server = project.url.relativeString.replacingOccurrences(of: "https://", with: "") return "cospend://\(server)/\(project.name.lowercased())/\(project.password)" From 8d5089ea29a0f961821348cdb60454876e21b677 Mon Sep 17 00:00:00 2001 From: Camille Mainz Date: Fri, 10 Dec 2021 13:31:39 +0100 Subject: [PATCH 05/11] fixes cospend url decoding for subdirectory nextclouds --- PayForMe/Util/Util.swift | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/PayForMe/Util/Util.swift b/PayForMe/Util/Util.swift index 30a3dd1..60639ba 100644 --- a/PayForMe/Util/Util.swift +++ b/PayForMe/Util/Util.swift @@ -191,15 +191,23 @@ extension URL { func decodeCospendString() -> ProjectData { guard let host = host, let scheme = scheme, - scheme.localizedCaseInsensitiveContains("cospend"), - pathComponents.count >= 2, - pathComponents.count <= 3 + scheme.localizedCaseInsensitiveContains("cospend") else { return (nil, nil, nil) } - return (URL(string: "https://\(host)"), - pathComponents[1], - pathComponents[safe: 2]) + + var hostString = "https://\(host)" + + if pathComponents.count > 3 { + for i in 1.. Date: Fri, 23 May 2025 02:03:39 +0200 Subject: [PATCH 06/11] add back port component to cospend path --- PayForMe.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- PayForMe/Util/Util.swift | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/PayForMe.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PayForMe.xcworkspace/xcshareddata/swiftpm/Package.resolved index 224c859..fb409cb 100644 --- a/PayForMe.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PayForMe.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift.git", "state" : { - "revision" : "04e73c26c4ce8218ab85aaf791942bb0b204f330", - "version" : "7.4.1" + "revision" : "a5a1be26b4513dc7ec360eb56bc08a345bac6649", + "version" : "7.5.0" } }, { diff --git a/PayForMe/Util/Util.swift b/PayForMe/Util/Util.swift index 60639ba..97d7800 100644 --- a/PayForMe/Util/Util.swift +++ b/PayForMe/Util/Util.swift @@ -198,6 +198,8 @@ extension URL { var hostString = "https://\(host)" + if let port = port {hostString += ":\(port)"} + if pathComponents.count > 3 { for i in 1.. Date: Fri, 23 May 2025 02:09:26 +0200 Subject: [PATCH 07/11] removed copyright claims to match the main branch --- PayForMe/Util/Util.swift | 3 +-- PayForMe/Views/FancyLoadingButton.swift | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/PayForMe/Util/Util.swift b/PayForMe/Util/Util.swift index 97d7800..1ec6422 100644 --- a/PayForMe/Util/Util.swift +++ b/PayForMe/Util/Util.swift @@ -1,9 +1,8 @@ // // Util.swift -// iWontPayAnyway +// PayForMe // // Created by Camille Mainz on 28.01.20. -// Copyright © 2020 Mayflower GmbH. All rights reserved. // import Foundation diff --git a/PayForMe/Views/FancyLoadingButton.swift b/PayForMe/Views/FancyLoadingButton.swift index 9a214bc..a3cf0eb 100644 --- a/PayForMe/Views/FancyLoadingButton.swift +++ b/PayForMe/Views/FancyLoadingButton.swift @@ -3,7 +3,6 @@ // PayForMe // // Created by Camille Mainz on 24.02.20. -// Copyright © 2020 Mayflower GmbH. All rights reserved. // import SlickLoadingSpinner From 283f789bfcb47f6b9e683d967e2592a4ff76f615 Mon Sep 17 00:00:00 2001 From: InteractionEngineer Date: Fri, 23 May 2025 02:24:51 +0200 Subject: [PATCH 08/11] outsourced async-await features to its own feature branch --- .../Views/BillDetail/BillDetailView.swift | 19 ++++++++++--------- PayForMe/Views/FancyLoadingButton.swift | 10 +++------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/PayForMe/Views/BillDetail/BillDetailView.swift b/PayForMe/Views/BillDetail/BillDetailView.swift index ac3d02b..092b974 100644 --- a/PayForMe/Views/BillDetail/BillDetailView.swift +++ b/PayForMe/Views/BillDetail/BillDetailView.swift @@ -64,19 +64,20 @@ struct BillDetailView: View { .navigationBarTitle(navBarTitle, displayMode: .inline) } - func sendBillToServer() async { + func sendBillToServer() { guard let newBill = viewModel.createBill() else { print("Could not create bill") return } sendingInProgress = .connecting - await ProjectManager.shared.saveBill(newBill) - sendingInProgress = .success - ProjectManager.shared.loadBillsAndMembers() - showModal.toggle() - DispatchQueue.main.async { - self.presentationMode.wrappedValue.dismiss() - } + ProjectManager.shared.saveBill(newBill, completion: { + self.sendingInProgress = .success + ProjectManager.shared.loadBillsAndMembers() + self.showModal.toggle() + DispatchQueue.main.async { + self.presentationMode.wrappedValue.dismiss() + } + }) } } @@ -86,4 +87,4 @@ struct BillDetailView_Previews: PreviewProvider { vm.currentProject = previewProject return BillDetailView(showModal: .constant(true), viewModel: vm) } -} +} \ No newline at end of file diff --git a/PayForMe/Views/FancyLoadingButton.swift b/PayForMe/Views/FancyLoadingButton.swift index a3cf0eb..3c0c0f5 100644 --- a/PayForMe/Views/FancyLoadingButton.swift +++ b/PayForMe/Views/FancyLoadingButton.swift @@ -15,17 +15,13 @@ struct FancyLoadingButton: View { var add: Bool - var action: () async -> Void + var action: () -> Void var text: String var body: some View { switch isLoading { case .notStarted: - return Button(action: { - Task { - await action() - } - }) { + return Button(action: action) { if add { Image(systemName: "plus") } else { @@ -47,4 +43,4 @@ struct FancyBotton_Previews: PreviewProvider { static var previews: some View { FancyLoadingButton(isLoading: .connecting, add: false, action: ({}), text: "Add Project").environment(\.locale, .init(identifier: "de")) } -} +} \ No newline at end of file From a00701300e89a9e8bc0181c936050edf5bd69a38 Mon Sep 17 00:00:00 2001 From: InteractionEngineer Date: Fri, 23 May 2025 02:52:26 +0200 Subject: [PATCH 09/11] fixed: No newline at end of file --- PayForMe/Views/BillDetail/BillDetailView.swift | 2 +- PayForMe/Views/FancyLoadingButton.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PayForMe/Views/BillDetail/BillDetailView.swift b/PayForMe/Views/BillDetail/BillDetailView.swift index 092b974..55720d3 100644 --- a/PayForMe/Views/BillDetail/BillDetailView.swift +++ b/PayForMe/Views/BillDetail/BillDetailView.swift @@ -87,4 +87,4 @@ struct BillDetailView_Previews: PreviewProvider { vm.currentProject = previewProject return BillDetailView(showModal: .constant(true), viewModel: vm) } -} \ No newline at end of file +} diff --git a/PayForMe/Views/FancyLoadingButton.swift b/PayForMe/Views/FancyLoadingButton.swift index 3c0c0f5..4de4d3d 100644 --- a/PayForMe/Views/FancyLoadingButton.swift +++ b/PayForMe/Views/FancyLoadingButton.swift @@ -43,4 +43,4 @@ struct FancyBotton_Previews: PreviewProvider { static var previews: some View { FancyLoadingButton(isLoading: .connecting, add: false, action: ({}), text: "Add Project").environment(\.locale, .init(identifier: "de")) } -} \ No newline at end of file +} From e19fd80d185de1b4f87f105037f3caa3a803b6e2 Mon Sep 17 00:00:00 2001 From: InteractionEngineer Date: Fri, 23 May 2025 03:03:53 +0200 Subject: [PATCH 10/11] version-bump for AppStore release --- PayForMe.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PayForMe.xcodeproj/project.pbxproj b/PayForMe.xcodeproj/project.pbxproj index 795c50f..16ef506 100644 --- a/PayForMe.xcodeproj/project.pbxproj +++ b/PayForMe.xcodeproj/project.pbxproj @@ -804,7 +804,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = de.mayflower.PayForMe; PRODUCT_NAME = PayForMe; SWIFT_VERSION = 5.0; @@ -829,7 +829,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = de.mayflower.PayForMe; PRODUCT_NAME = PayForMe; SWIFT_VERSION = 5.0; From af70d14558635041171d7e306b69d0db5b2c728e Mon Sep 17 00:00:00 2001 From: Jona Date: Sat, 24 May 2025 00:26:56 +0200 Subject: [PATCH 11/11] simplified loop Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- PayForMe/Util/Util.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/PayForMe/Util/Util.swift b/PayForMe/Util/Util.swift index 1ec6422..a191874 100644 --- a/PayForMe/Util/Util.swift +++ b/PayForMe/Util/Util.swift @@ -200,10 +200,7 @@ extension URL { if let port = port {hostString += ":\(port)"} if pathComponents.count > 3 { - for i in 1..