From e6bda11caedf4f3aa0f24539d7d8cc39d944e847 Mon Sep 17 00:00:00 2001 From: Serhii Londar Date: Tue, 26 Nov 2024 01:58:56 +0100 Subject: [PATCH] Add preparation method to screenshot uploader. Update screenshot feature to screenshot uploader before adding screenshots. (Download mapping) Update crowdin mapping manager to support download completions. Adjust captureAndUpdateScreenshot method - add result parameter to completion. Update screenshots documentation to adjust captureAndUpdateScreenshot method update. --- .../AppleReminders/Controllers/MainVC.swift | 11 +++++ .../CrowdinSDK/CrowdinAPI/CrowdinAPI.swift | 30 +++++++----- .../Extensions/CrowdinSDK+Screenshots.swift | 8 +-- .../ScreenshotFeature/ScreenshotFeature.swift | 21 ++++++-- .../ScreenshotUploader.swift | 49 +++++++++++++++++-- .../CrowdinMappingManager.swift | 7 ++- .../docs/advanced-features/screenshots.mdx | 9 ++-- 7 files changed, 108 insertions(+), 27 deletions(-) diff --git a/Example/AppleReminders/Controllers/MainVC.swift b/Example/AppleReminders/Controllers/MainVC.swift index 70468f3d..68ed334c 100644 --- a/Example/AppleReminders/Controllers/MainVC.swift +++ b/Example/AppleReminders/Controllers/MainVC.swift @@ -9,6 +9,7 @@ import RealmSwift import SwiftUI import UIKit +import CrowdinSDK final class MainVC: UIViewController { @@ -101,6 +102,16 @@ final class MainVC: UIViewController { setupNavBar() setupSearch() + + CrowdinSDK.captureAndUpdateScreenshot(name: "{screenshot_name}") { result in + switch result { + case .new: print("New screenshot captured") + case .udpated: print("Screenshot updated") + } + } errorHandler: { error in + print("Error: \(error)") + } + } deinit { diff --git a/Sources/CrowdinSDK/CrowdinAPI/CrowdinAPI.swift b/Sources/CrowdinSDK/CrowdinAPI/CrowdinAPI.swift index 6255eb8c..a740e5b4 100644 --- a/Sources/CrowdinSDK/CrowdinAPI/CrowdinAPI.swift +++ b/Sources/CrowdinSDK/CrowdinAPI/CrowdinAPI.swift @@ -48,9 +48,12 @@ class CrowdinAPI: BaseAPI { func cw_post(url: String, parameters: [String: String]? = nil, headers: [String: String]? = nil, body: Data?, completion: @escaping (T?, Error?) -> Swift.Void) { self.post(url: url, parameters: parameters, headers: addDefaultHeaders(to: headers), body: body, completion: { data, response, error in + + CrowdinAPILog.logRequest(method: RequestMethod.POST.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), body: body, responseData: data) + if self.isUnautorized(response: response) { - CrowdinAPILog.logRequest(method: RequestMethod.POST.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), body: body, responseData: data) NotificationCenter.default.post(name: .CrowdinAPIUnautorizedNotification, object: nil) + completion(nil, NSError(domain: "CrowdinAPI Unautorized", code: 401, userInfo: nil)) return } guard let data = data else { @@ -58,8 +61,6 @@ class CrowdinAPI: BaseAPI { return } - CrowdinAPILog.logRequest(method: RequestMethod.POST.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), body: body, responseData: data) - do { let response = try JSONDecoder().decode(T.self, from: data) completion(response, error) @@ -76,7 +77,7 @@ class CrowdinAPI: BaseAPI { if self.isUnautorized(response: result.response) { NotificationCenter.default.post(name: .CrowdinAPIUnautorizedNotification, object: nil) - return (nil, nil); + return(nil, NSError(domain: "CrowdinAPI Unautorized", code: 401, userInfo: nil)) } guard let data = result.data else { return (nil, result.error) @@ -93,9 +94,12 @@ class CrowdinAPI: BaseAPI { func cw_put(url: String, parameters: [String: String]? = nil, headers: [String: String]? = nil, body: Data?, completion: @escaping (T?, Error?) -> Swift.Void) { self.put(url: url, parameters: parameters, headers: addDefaultHeaders(to: headers), body: body, completion: { data, response, error in + + CrowdinAPILog.logRequest(method: RequestMethod.POST.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), body: body, responseData: data) + if self.isUnautorized(response: response) { - CrowdinAPILog.logRequest(method: RequestMethod.POST.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), body: body, responseData: data) NotificationCenter.default.post(name: .CrowdinAPIUnautorizedNotification, object: nil) + completion(nil, NSError(domain: "CrowdinAPI Unautorized", code: 401, userInfo: nil)) return } guard let data = data else { @@ -103,8 +107,6 @@ class CrowdinAPI: BaseAPI { return } - CrowdinAPILog.logRequest(method: RequestMethod.POST.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), body: body, responseData: data) - do { let response = try JSONDecoder().decode(T.self, from: data) completion(response, error) @@ -121,7 +123,7 @@ class CrowdinAPI: BaseAPI { if self.isUnautorized(response: result.response) { NotificationCenter.default.post(name: .CrowdinAPIUnautorizedNotification, object: nil) - return (nil, nil); + return (nil, NSError(domain: "CrowdinAPI Unautorized", code: 401, userInfo: nil)) } guard let data = result.data else { return (nil, result.error) @@ -138,9 +140,12 @@ class CrowdinAPI: BaseAPI { func cw_get(url: String, parameters: [String: String]? = nil, headers: [String: String]? = nil, completion: @escaping (T?, Error?) -> Swift.Void) { self.get(url: url, parameters: parameters, headers: addDefaultHeaders(to: headers), completion: { data, response, error in + + CrowdinAPILog.logRequest(method: RequestMethod.GET.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), responseData: data) + if self.isUnautorized(response: response) { - CrowdinAPILog.logRequest(method: RequestMethod.GET.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), responseData: data) NotificationCenter.default.post(name: .CrowdinAPIUnautorizedNotification, object: nil) + completion(nil, NSError(domain: "CrowdinAPI Unautorized", code: 401, userInfo: nil)) return; } guard let data = data else { @@ -148,8 +153,6 @@ class CrowdinAPI: BaseAPI { return } - CrowdinAPILog.logRequest(method: RequestMethod.GET.rawValue, url: url, parameters: parameters, headers: self.addDefaultHeaders(to: headers), responseData: data) - do { let response = try JSONDecoder().decode(T.self, from: data) completion(response, error) @@ -162,11 +165,14 @@ class CrowdinAPI: BaseAPI { func cw_getSync(url: String, parameters: [String: String]? = nil, headers: [String: String]? = nil) -> (T?, Error?) { let result = self.get(url: url, parameters: parameters, headers: addDefaultHeaders(to: headers)) + CrowdinAPILog.logRequest(method: RequestMethod.GET.rawValue, url: url, parameters: parameters, headers: addDefaultHeaders(to: headers), responseData: result.data) + if isUnautorized(response: result.response) { NotificationCenter.default.post(name: .CrowdinAPIUnautorizedNotification, object: nil) - return (nil, nil) + return (nil, NSError(domain: "CrowdinAPI Unautorized", code: 401, userInfo: nil)) } + guard let data = result.data else { return (nil, result.error) } diff --git a/Sources/CrowdinSDK/Features/ScreenshotFeature/Extensions/CrowdinSDK+Screenshots.swift b/Sources/CrowdinSDK/Features/ScreenshotFeature/Extensions/CrowdinSDK+Screenshots.swift index 8d0123b8..d62772c7 100644 --- a/Sources/CrowdinSDK/Features/ScreenshotFeature/Extensions/CrowdinSDK+Screenshots.swift +++ b/Sources/CrowdinSDK/Features/ScreenshotFeature/Extensions/CrowdinSDK+Screenshots.swift @@ -61,12 +61,12 @@ extension CrowdinSDK { /// - success: A closure to be called when the screenshot is successfully captured and updated. /// - errorHandler: A closure to be called if an error occurs during the process. /// The closure receives an optional Error parameter indicating what went wrong. - public class func captureAndUpdateScreenshot(name: String, success: @escaping (() -> Void), errorHandler: @escaping ((Error?) -> Void)) { + public class func captureAndUpdateScreenshot(name: String, success: @escaping ((ScreenshotUploadResult) -> Void), errorHandler: @escaping ((Error?) -> Void)) { guard let screenshotFeature = ScreenshotFeature.shared else { errorHandler(NSError(domain: "Screenshots feature disabled", code: defaultCrowdinErrorCode, userInfo: nil)) return } - screenshotFeature.captureScreenshot(name: name, success: success, errorHandler: errorHandler) + screenshotFeature.updateOrUploadScreenshot(name: name, success: success, errorHandler: errorHandler) } /// Captures a screenshot of a specific view and updates it if it already exists in Crowdin. @@ -78,12 +78,12 @@ extension CrowdinSDK { /// - success: A closure to be called when the screenshot is successfully captured and updated. /// - errorHandler: A closure to be called if an error occurs during the process. /// The closure receives an optional Error parameter indicating what went wrong. - public class func captureAndUpdateScreenshot(view: View, name: String, success: @escaping (() -> Void), errorHandler: @escaping ((Error?) -> Void)) { + public class func captureAndUpdateScreenshot(view: View, name: String, success: @escaping ((ScreenshotUploadResult) -> Void), errorHandler: @escaping ((Error?) -> Void)) { guard let screenshotFeature = ScreenshotFeature.shared else { errorHandler(NSError(domain: "Screenshots feature disabled", code: defaultCrowdinErrorCode, userInfo: nil)) return } - screenshotFeature.captureScreenshot(view: view, name: name, success: success, errorHandler: errorHandler) + screenshotFeature.updateOrUploadScreenshot(view: view, name: name, success: success, errorHandler: errorHandler) } } diff --git a/Sources/CrowdinSDK/Features/ScreenshotFeature/ScreenshotFeature.swift b/Sources/CrowdinSDK/Features/ScreenshotFeature/ScreenshotFeature.swift index bbbc9316..2fae0c88 100644 --- a/Sources/CrowdinSDK/Features/ScreenshotFeature/ScreenshotFeature.swift +++ b/Sources/CrowdinSDK/Features/ScreenshotFeature/ScreenshotFeature.swift @@ -63,8 +63,15 @@ class ScreenshotFeature { errorHandler(NSError(domain: "Unable to create screenshot.", code: defaultCrowdinErrorCode, userInfo: nil)) return } - let controlsInformation = ScreenshotInformationCollector.getControlsInformation(from: view, rootView: view) - screenshotUploader.uploadScreenshot(screenshot: screenshot, controlsInformation: controlsInformation, name: name, success: success, errorHandler: errorHandler) + screenshotUploader.prepare { error in + if let error { + errorHandler(error) + return + } + + let controlsInformation = ScreenshotInformationCollector.getControlsInformation(from: view, rootView: view) + self.screenshotUploader.uploadScreenshot(screenshot: screenshot, controlsInformation: controlsInformation, name: name, success: success, errorHandler: errorHandler) + } } func updateOrUploadScreenshot(name: String, success: @escaping ((ScreenshotUploadResult) -> Void), errorHandler: @escaping ((Error?) -> Void)) { @@ -91,8 +98,14 @@ class ScreenshotFeature { errorHandler(NSError(domain: "Unable to create screenshot from view.", code: defaultCrowdinErrorCode, userInfo: nil)) return } - let controlsInformation = ScreenshotInformationCollector.getControlsInformation(from: view, rootView: view) - screenshotUploader.updateOrUploadScreenshot(screenshot: screenshot, controlsInformation: controlsInformation, name: name, success: success, errorHandler: errorHandler) + screenshotUploader.prepare { error in + if let error { + errorHandler(error) + return + } + let controlsInformation = ScreenshotInformationCollector.getControlsInformation(from: view, rootView: view) + self.screenshotUploader.updateOrUploadScreenshot(screenshot: screenshot, controlsInformation: controlsInformation, name: name, success: success, errorHandler: errorHandler) + } } /// Returns the top view controller of the application's key window. diff --git a/Sources/CrowdinSDK/Features/ScreenshotFeature/ScreenshotUploader.swift b/Sources/CrowdinSDK/Features/ScreenshotFeature/ScreenshotUploader.swift index 7bc84e7f..3e18c4aa 100644 --- a/Sources/CrowdinSDK/Features/ScreenshotFeature/ScreenshotUploader.swift +++ b/Sources/CrowdinSDK/Features/ScreenshotFeature/ScreenshotUploader.swift @@ -13,6 +13,8 @@ import CoreGraphics public protocol ScreenshotUploader { func uploadScreenshot(screenshot: Image, controlsInformation: [ControlInformation], name: String, success: (() -> Void)?, errorHandler: ((Error) -> Void)?) func updateOrUploadScreenshot(screenshot: Image, controlsInformation: [ControlInformation], name: String, success: ((ScreenshotUploadResult) -> Void)?, errorHandler: ((Error) -> Void)?) + + func prepare(completion: @escaping (Error?) -> Void) } public enum ScreenshotUploadResult { @@ -28,7 +30,7 @@ class CrowdinScreenshotUploader: ScreenshotUploader { let loginFeature: AnyLoginFeature? let storageAPI: StorageAPI - var mappingManager: CrowdinMappingManagerProtocol + var mappingManager: CrowdinMappingManager var projectId: Int? = nil enum Errors: String { @@ -64,8 +66,8 @@ class CrowdinScreenshotUploader: ScreenshotUploader { } func getProjectId(success: (() -> Void)? = nil, errorHandler: ((Error) -> Void)? = nil) { - let distrinbutionsAPI = DistributionsAPI(hashString: hash, organizationName: organizationName, auth: loginFeature) - distrinbutionsAPI.getDistribution { (response, error) in + let distributionsAPI = DistributionsAPI(hashString: hash, organizationName: organizationName, auth: loginFeature) + distributionsAPI.getDistribution { (response, error) in if let error = error { errorHandler?(error) } else if let id = response?.data.project.id, let projectId = Int(id) { @@ -78,6 +80,28 @@ class CrowdinScreenshotUploader: ScreenshotUploader { } } } + + func prepare(completion: @escaping (Error?) -> Void) { + downloadMappingIfNeeded(completion: { error in + DispatchQueue.main.async { + completion(error) + } + }) + } + + func downloadMappingIfNeeded(completion: @escaping (Error?) -> Void) { + if mappingManager.downloaded { + completion(nil) + } else { + mappingManager.downloadCompletions.append({ errors in + if let errors, let error = self.combineErrors(errors) { + completion(error) + return + } + completion(nil) + }) + } + } func uploadScreenshot(screenshot: Image, controlsInformation: [ControlInformation], name: String, success: (() -> Void)?, errorHandler: ((Error) -> Void)?) { guard let projectId = self.projectId else { @@ -201,6 +225,25 @@ class CrowdinScreenshotUploader: ScreenshotUploader { } return results } + + func combineErrors(_ errors: [Error]) -> Error? { + // If no errors, return nil + guard !errors.isEmpty else { return nil } + + // If only one error, return that error + guard errors.count > 1 else { return errors.first } + + // Custom error type to combine multiple errors + struct MultipleErrors: Error { + let errors: [Error] + + var localizedDescription: String { + return errors.map { $0.localizedDescription }.joined(separator: "; ") + } + } + + return MultipleErrors(errors: errors) + } } #endif diff --git a/Sources/CrowdinSDK/Providers/Crowdin/MappingDownloader/CrowdinMappingManager.swift b/Sources/CrowdinSDK/Providers/Crowdin/MappingDownloader/CrowdinMappingManager.swift index 6446cb83..ddefbec7 100644 --- a/Sources/CrowdinSDK/Providers/Crowdin/MappingDownloader/CrowdinMappingManager.swift +++ b/Sources/CrowdinSDK/Providers/Crowdin/MappingDownloader/CrowdinMappingManager.swift @@ -24,6 +24,8 @@ public class CrowdinMappingManager: CrowdinMappingManagerProtocol { var pluralsMapping: [String: String] = [:] var stringsMapping: [String: String] = [:] var plurals: [AnyHashable: Any] = [:] + var downloaded: Bool = false + var downloadCompletions: [([Error]?) -> Void] = [] init(hash: String, sourceLanguage: String, organizationName: String?) { self.manifestManager = ManifestManager.manifest(for: hash, sourceLanguage: sourceLanguage, organizationName: organizationName) @@ -43,10 +45,13 @@ public class CrowdinMappingManager: CrowdinMappingManagerProtocol { } func downloadMapping(hash: String, sourceLanguage: String) { - self.downloader.download(with: hash, for: sourceLanguage) { (strings, plurals, _) in + self.downloader.download(with: hash, for: sourceLanguage) { (strings, plurals, errors) in self.stringsMapping = strings ?? [:] self.plurals = plurals ?? [:] self.extractPluralsMapping() + self.downloaded = true + self.downloadCompletions.forEach({ $0(errors) }) + self.downloadCompletions.removeAll() } } diff --git a/website/docs/advanced-features/screenshots.mdx b/website/docs/advanced-features/screenshots.mdx index a65ea0a5..02891d83 100644 --- a/website/docs/advanced-features/screenshots.mdx +++ b/website/docs/advanced-features/screenshots.mdx @@ -133,10 +133,13 @@ CrowdinSDK.captureScreenshot(name: String(Date().timeIntervalSince1970)) { 2. Capture and update existing screenshot: ```swift -CrowdinSDK.captureAndUpdateScreenshot(name: "{screenshot_name}") { - print("Screenshot updated") +CrowdinSDK.captureAndUpdateScreenshot(name: "{screenshot_name}") { result in + switch result { + case .new: print("New screenshot captured") + case .udpated: print("Screenshot updated") + } } errorHandler: { error in - print("Screenshot update failed with error - " + (error?.localizedDescription ?? "")) + print("Error: \(error)") } ```