Skip to content

Commit

Permalink
Add preparation method to screenshot uploader.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
serhii-londar committed Nov 26, 2024
1 parent 0999d76 commit e6bda11
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 27 deletions.
11 changes: 11 additions & 0 deletions Example/AppleReminders/Controllers/MainVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import RealmSwift
import SwiftUI
import UIKit
import CrowdinSDK

final class MainVC: UIViewController {

Expand Down Expand Up @@ -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 {
Expand Down
30 changes: 18 additions & 12 deletions Sources/CrowdinSDK/CrowdinAPI/CrowdinAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,19 @@ class CrowdinAPI: BaseAPI {

func cw_post<T: Decodable>(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 {
completion(nil, error)
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)
Expand All @@ -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)
Expand All @@ -93,18 +94,19 @@ class CrowdinAPI: BaseAPI {

func cw_put<T: Decodable>(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 {
completion(nil, error)
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)
Expand All @@ -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)
Expand All @@ -138,18 +140,19 @@ class CrowdinAPI: BaseAPI {

func cw_get<T: Decodable>(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 {
completion(nil, error)
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)
Expand All @@ -162,11 +165,14 @@ class CrowdinAPI: BaseAPI {

func cw_getSync<T: Decodable>(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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
}
}

Expand Down
9 changes: 6 additions & 3 deletions website/docs/advanced-features/screenshots.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
```

Expand Down

0 comments on commit e6bda11

Please sign in to comment.