-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial changes for saving product images upload statuses in UserDefaults #15196
base: trunk
Are you sure you want to change the base?
Changes from all commits
358daa8
992cc5c
b7b2078
a804b1e
e3c87b8
66c6694
6c678cf
d506946
51a1cfd
70e0fa9
27a1146
1f96482
c6be542
561582a
309d947
c84cef6
1344637
f5803bb
1d85fd2
e012096
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import Foundation | ||
import Photos | ||
import UIKit | ||
|
||
/// The status of a Product image. | ||
/// | ||
|
||
public enum ProductImageStatus: Equatable, Codable { | ||
/// An image asset is being uploaded. | ||
/// | ||
case uploading(asset: ProductImageAssetType, siteID: Int64, productID: ProductOrVariationID) | ||
|
||
/// The Product image exists remotely. | ||
/// | ||
case remote(image: ProductImage, siteID: Int64, productID: ProductOrVariationID) | ||
|
||
/// An image asset upload failed. | ||
/// | ||
case uploadFailure(asset: ProductImageAssetType, error: Error, siteID: Int64, productID: ProductOrVariationID) | ||
|
||
private enum CodingKeys: String, CodingKey { | ||
case type | ||
case asset | ||
case image | ||
case error | ||
case siteID | ||
case productID | ||
} | ||
|
||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
let typeString = try container.decode(String.self, forKey: .type) | ||
switch typeString { | ||
case "uploading": | ||
let asset = try container.decode(ProductImageAssetType.self, forKey: .asset) | ||
let sID = try container.decode(Int64.self, forKey: .siteID) | ||
let pID = try container.decode(ProductOrVariationID.self, forKey: .productID) | ||
self = .uploading(asset: asset, siteID: sID, productID: pID) | ||
case "remote": | ||
let image = try container.decode(ProductImage.self, forKey: .image) | ||
let sID = try container.decode(Int64.self, forKey: .siteID) | ||
let pID = try container.decode(ProductOrVariationID.self, forKey: .productID) | ||
self = .remote(image: image, siteID: sID, productID: pID) | ||
case "uploadFailure": | ||
let asset = try container.decode(ProductImageAssetType.self, forKey: .asset) | ||
let errorMessage = try container.decode(String.self, forKey: .error) | ||
let error = NSError(domain: "ProductImageStatus", code: 0, userInfo: [NSLocalizedDescriptionKey: errorMessage]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like we'll lose information about the error here. The information is necessary because we're showing details about the upload failures. Please also retain the domain and error code while encoding/decoding. |
||
let sID = try container.decode(Int64.self, forKey: .siteID) | ||
let pID = try container.decode(ProductOrVariationID.self, forKey: .productID) | ||
self = .uploadFailure(asset: asset, error: error, siteID: sID, productID: pID) | ||
default: | ||
throw DecodingError.dataCorruptedError(forKey: .type, | ||
in: container, | ||
debugDescription: "Invalid type value: \(typeString)") | ||
} | ||
} | ||
|
||
public func encode(to encoder: Encoder) throws { | ||
var container = encoder.container(keyedBy: CodingKeys.self) | ||
switch self { | ||
case .uploading(let asset, let siteID, let productID): | ||
try container.encode("uploading", forKey: .type) | ||
try container.encode(asset, forKey: .asset) | ||
try container.encode(siteID, forKey: .siteID) | ||
try container.encodeIfPresent(productID, forKey: .productID) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason why |
||
case .remote(let image, let siteID, let productID): | ||
try container.encode("remote", forKey: .type) | ||
try container.encode(image, forKey: .image) | ||
try container.encode(siteID, forKey: .siteID) | ||
try container.encode(productID, forKey: .productID) | ||
case .uploadFailure(let asset, let error, let siteID, let productID): | ||
try container.encode("uploadFailure", forKey: .type) | ||
try container.encode(asset, forKey: .asset) | ||
let errorMessage = (error as NSError).localizedDescription | ||
try container.encode(errorMessage, forKey: .error) | ||
try container.encode(siteID, forKey: .siteID) | ||
try container.encodeIfPresent(productID, forKey: .productID) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment about |
||
} | ||
} | ||
|
||
public static func == (lhs: Self, rhs: Self) -> Bool { | ||
switch (lhs, rhs) { | ||
case let (.uploading(lAsset, lSiteID, lProductID), .uploading(rAsset, rSiteID, rProductID)): | ||
return lAsset == rAsset && lSiteID == rSiteID && lProductID == rProductID | ||
case let (.remote(lImage, lSiteID, lProductID), .remote(rImage, rSiteID, rProductID)): | ||
return lImage == rImage && lSiteID == rSiteID && lProductID == rProductID | ||
case let (.uploadFailure(lAsset, lError, lSiteID, lProductID), .uploadFailure(rAsset, rError, rSiteID, rProductID)): | ||
return lAsset == rAsset && | ||
(lError as NSError) == (rError as NSError) && | ||
lSiteID == rSiteID && | ||
lProductID == rProductID | ||
default: | ||
return false | ||
} | ||
} | ||
} | ||
|
||
/// The type of product image asset. | ||
public enum ProductImageAssetType: Equatable, Codable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you added Codable conformance to |
||
/// `PHAsset` from device photo library or camera capture. | ||
case phAsset(asset: PHAsset) | ||
|
||
/// `UIImage` from image processing. The filename and alt text need to be provided separately. | ||
case uiImage(image: UIImage, filename: String?, altText: String?) | ||
|
||
private enum CodingKeys: String, CodingKey { | ||
case type | ||
case asset // For phAsset: localIdentifier string | ||
case imageData // For uiImage: base64 encoded image data | ||
case filename | ||
case altText | ||
} | ||
|
||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
let typeString = try container.decode(String.self, forKey: .type) | ||
switch typeString { | ||
case "phAsset": | ||
let localIdentifier = try container.decode(String.self, forKey: .asset) | ||
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil) | ||
guard let asset = fetchResult.firstObject else { | ||
throw DecodingError.dataCorruptedError(forKey: .asset, | ||
in: container, | ||
debugDescription: "No PHAsset found with localIdentifier \(localIdentifier)") | ||
} | ||
self = .phAsset(asset: asset) | ||
case "uiImage": | ||
let base64String = try container.decode(String.self, forKey: .imageData) | ||
guard let imageData = Data(base64Encoded: base64String), | ||
let image = UIImage(data: imageData) else { | ||
throw DecodingError.dataCorruptedError(forKey: .imageData, | ||
in: container, | ||
debugDescription: "Invalid image data") | ||
} | ||
let filename = try container.decodeIfPresent(String.self, forKey: .filename) | ||
let altText = try container.decodeIfPresent(String.self, forKey: .altText) | ||
self = .uiImage(image: image, filename: filename, altText: altText) | ||
default: | ||
throw DecodingError.dataCorruptedError(forKey: .type, | ||
in: container, | ||
debugDescription: "Unknown type \(typeString)") | ||
} | ||
} | ||
|
||
public func encode(to encoder: Encoder) throws { | ||
var container = encoder.container(keyedBy: CodingKeys.self) | ||
switch self { | ||
case .phAsset(let asset): | ||
try container.encode("phAsset", forKey: .type) | ||
try container.encode(asset.localIdentifier, forKey: .asset) | ||
case .uiImage(let image, let filename, let altText): | ||
try container.encode("uiImage", forKey: .type) | ||
guard let imageData = image.pngData() else { | ||
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "Unable to encode UIImage as PNG") | ||
throw EncodingError.invalidValue(image, context) | ||
} | ||
let base64String = imageData.base64EncodedString() | ||
try container.encode(base64String, forKey: .imageData) | ||
try container.encode(filename, forKey: .filename) | ||
try container.encode(altText, forKey: .altText) | ||
} | ||
} | ||
|
||
public static func == (lhs: ProductImageAssetType, rhs: ProductImageAssetType) -> Bool { | ||
switch (lhs, rhs) { | ||
case (.phAsset(let lAsset), .phAsset(let rAsset)): | ||
return lAsset.localIdentifier == rAsset.localIdentifier | ||
case (.uiImage(let lImage, let lFilename, let lAltText), | ||
.uiImage(let rImage, let rFilename, let rAltText)): | ||
return lImage.pngData() == rImage.pngData() && | ||
lFilename == rFilename && | ||
lAltText == rAltText | ||
default: | ||
return false | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import Foundation | ||
|
||
/// Save product image upload statuses in User Defaults. | ||
/// This class is declared in the Networking layer because it will also be accessed by the background URLSession operations. | ||
/// | ||
final class ProductImagesUserDefaultsStatuses { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please consider renaming this to |
||
private static let key = "savedProductUploadImageStatuses" | ||
|
||
static func addStatus(_ status: ProductImageStatus) { | ||
var statuses = getAllStatuses() | ||
statuses.append(status) | ||
saveAllStatuses(statuses) | ||
} | ||
|
||
static func removeStatus(_ status: ProductImageStatus) { | ||
var statuses = getAllStatuses() | ||
statuses.removeAll(where: { $0 == status }) | ||
saveAllStatuses(statuses) | ||
} | ||
|
||
static func findStatus(where predicate: (ProductImageStatus) -> Bool) -> ProductImageStatus? { | ||
return getAllStatuses().first(where: predicate) | ||
} | ||
|
||
static func getAllStatuses() -> [ProductImageStatus] { | ||
guard let data = UserDefaults.standard.data(forKey: key) else { | ||
return [] | ||
} | ||
do { | ||
let statuses = try JSONDecoder().decode([ProductImageStatus].self, from: data) | ||
return statuses | ||
} catch { | ||
DDLogError("Error decoding saved product image statuses: \(error)") | ||
return [] | ||
} | ||
} | ||
|
||
static func getAllStatuses(for siteID: Int64, productID: ProductOrVariationID?) -> [ProductImageStatus] { | ||
return getAllStatuses().filter { status in | ||
switch status { | ||
case .remote(_, let sID, let pID): | ||
if let filterProductID = productID { | ||
return sID == siteID && pID == filterProductID | ||
} else { | ||
return false | ||
} | ||
case .uploading(_, let sID, let pID), | ||
.uploadFailure(_, _, let sID, let pID): | ||
if let filterProductID = productID { | ||
return sID == siteID && pID == filterProductID | ||
} else { | ||
return sID == siteID && pID == nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Product ID is non-optional in image statuses so this check will likely fail. |
||
} | ||
} | ||
} | ||
} | ||
|
||
static func clearAllStatuses() { | ||
UserDefaults.standard.removeObject(forKey: key) | ||
} | ||
|
||
private static func saveAllStatuses(_ statuses: [ProductImageStatus]) { | ||
do { | ||
let data = try JSONEncoder().encode(statuses) | ||
UserDefaults.standard.set(data, forKey: key) | ||
} catch { | ||
DDLogError("Error encoding saved product image statuses: \(error)") | ||
} | ||
} | ||
} | ||
|
||
extension ProductImagesUserDefaultsStatuses { | ||
static func setAllStatuses(_ statuses: [ProductImageStatus]) { | ||
saveAllStatuses(statuses) | ||
} | ||
|
||
static func setAllStatuses(_ statuses: [ProductImageStatus], for siteID: Int64, productID: ProductOrVariationID?) { | ||
// Merge with existing, removing any old statuses for this site/product | ||
var all = getAllStatuses().filter { st in | ||
switch st { | ||
case .remote(_, let sID, let pID): | ||
if let filterProductID = productID { | ||
return !(sID == siteID && pID == filterProductID) | ||
} else { | ||
return true | ||
} | ||
case .uploading(_, let sID, let pID), | ||
.uploadFailure(_, _, let sID, let pID): | ||
if let filterProductID = productID { | ||
return !(sID == siteID && pID == filterProductID) | ||
} else { | ||
return !(sID == siteID && pID == nil) | ||
} | ||
} | ||
} | ||
all.append(contentsOf: statuses) | ||
saveAllStatuses(all) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you added Codable conformance to
ProductImageStatus
, please also add tests to confirm that both the encoding and decoding work as expected.