Skip to content

Commit 3e00311

Browse files
committed
Merge branch 'develop' into issue/1713-image-upload-ux
2 parents e5ac6f0 + 8c48997 commit 3e00311

34 files changed

+808
-183
lines changed

Networking/Networking/Model/Media/Media.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
/// WordPress Site Media
2-
/// Note: this is an `NSObject` class instead of a struct because it has to conform to `WPMediaAsset` protocol defined in
3-
/// Objective-C.
42
///
5-
public class Media: NSObject, Decodable {
3+
public struct Media {
64
public let mediaID: Int64
75
public let date: Date // gmt iso8601
86
public let fileExtension: String
@@ -37,10 +35,12 @@ public class Media: NSObject, Decodable {
3735
self.height = height
3836
self.width = width
3937
}
38+
}
4039

40+
extension Media: Decodable {
4141
/// Decodable Initializer.
4242
///
43-
required public convenience init(from decoder: Decoder) throws {
43+
public init(from decoder: Decoder) throws {
4444
let container = try decoder.container(keyedBy: CodingKeys.self)
4545

4646
let mediaID = try container.decode(Int64.self, forKey: .mediaID)

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
4.1
22
-----
33
- Fix an intermittent crash when downloading Orders
4+
- The Photo Library permission alert shouldn't be prompted when opening the readonly product details or edit product for simple products, which is reproducible on iOS 11 or 12 devices. (The permission is only triggered when uploading images in Zendesk support or in debug builds with Products M2 enabled.)
45
- [internal] Updated the empty search result views for Products and Orders. https://git.io/Jvdap
56

67

WooCommerce/Classes/Extensions/UINavigationController+Woo.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ extension UINavigationController {
3333

3434
// MARK: - Handle UINavigationBar's 'Back' button action
3535
//
36-
protocol UINavigationBarBackButtonHandler {
36+
protocol UINavigationBarBackButtonHandler {
3737

3838
/// Should block the 'Back' button action
3939
///
@@ -49,6 +49,9 @@ extension UIViewController: UINavigationBarBackButtonHandler {
4949
}
5050

5151
extension UINavigationController: UINavigationBarDelegate {
52+
53+
// This delegate method is not called on the simulator or device running iOS 13.4 from Xcode.
54+
// You need to use a release build.
5255
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
5356
guard let items = navigationBar.items else {
5457
return false

WooCommerce/Classes/Tools/Currency/CurrencyFormatter.swift

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ public class CurrencyFormatter {
1414
/// Returns a decimal value from a given string.
1515
/// - Parameters:
1616
/// - stringValue: the string received from the API
17+
/// - locale: the locale that the currency string is based on.
1718
///
18-
func convertToDecimal(from stringValue: String) -> NSDecimalNumber? {
19+
func convertToDecimal(from stringValue: String, locale: Locale = .current) -> NSDecimalNumber? {
1920

2021
// NSDecimalNumber use by default the local decimal separator to evaluate a decimal amount.
2122
// We substitute the current decimal separator with the locale decimal separator.
22-
let localeDecimalSeparator = Locale.current.decimalSeparator ?? currencySettings.decimalSeparator
23+
let localeDecimalSeparator = locale.decimalSeparator ?? currencySettings.decimalSeparator
2324
var newStringValue = stringValue.replacingOccurrences(of: ",", with: localeDecimalSeparator)
2425
newStringValue = newStringValue.replacingOccurrences(of: ".", with: localeDecimalSeparator)
2526

@@ -28,7 +29,7 @@ public class CurrencyFormatter {
2829
let unit = currencySettings.symbol(from: currencyCode)
2930
newStringValue = newStringValue.replacingOccurrences(of: unit, with: "")
3031

31-
let decimalValue = NSDecimalNumber(string: newStringValue, locale: Locale.current)
32+
let decimalValue = NSDecimalNumber(string: newStringValue, locale: locale)
3233

3334
guard decimalValue != NSDecimalNumber.notANumber else {
3435
DDLogError("Error: string input is not a number: \(stringValue)")
@@ -72,21 +73,22 @@ public class CurrencyFormatter {
7273

7374
/// Returns a string that displays the amount using all of the specified currency settings
7475
/// - Parameters:
75-
/// - amount: a formatted string, preferably converted using `localize(_:in:with:including:)`.
76+
/// - stringValue: a formatted string, preferably converted using `localize(_:in:with:including:)`.
7677
/// - position: the currency position enum, either right, left, right_space, or left_space.
7778
/// - symbol: the currency symbol as a string, to be used with the amount.
79+
/// - isNegative: whether the value is negative or not.
80+
/// - locale: the locale that is used to format the currency amount string.
7881
///
79-
func formatCurrency(using stringValue: String, at position: CurrencySettings.CurrencyPosition, with symbol: String, isNegative: Bool) -> String {
82+
func formatCurrency(using amount: String,
83+
at position: CurrencySettings.CurrencyPosition,
84+
with symbol: String,
85+
isNegative: Bool,
86+
locale: Locale = .current) -> String {
8087
let space = "\u{00a0}" // unicode equivalent of  
8188
let negative = isNegative ? "-" : ""
8289

8390
// We're relying on the phone's Locale to assist with language direction
84-
let current = Locale.current as NSLocale
85-
let languageCode = current.object(forKey: NSLocale.Key.languageCode) as? String
86-
87-
// Remove all occurences of the minus sign from the string amount.
88-
// We want to position the minus sign manually.
89-
let amount = stringValue.replacingOccurrences(of: "-", with: "")
91+
let languageCode = locale.languageCode
9092

9193
// Detect the language direction
9294
var languageDirection: Locale.LanguageDirection = .unknown
@@ -126,23 +128,25 @@ public class CurrencyFormatter {
126128
/// - Parameters:
127129
/// - amount: a raw string representation of the amount, from the API, with no formatting applied. e.g. "19.87"
128130
/// - currency: a 3-letter country code for currencies that are supported in the API. e.g. "USD"
131+
/// - locale: the locale that is used to format the currency amount string.
129132
///
130-
func formatAmount(_ stringAmount: String, with currency: String = CurrencySettings.shared.currencyCode.rawValue) -> String? {
131-
guard let decimalAmount = convertToDecimal(from: stringAmount) else {
133+
func formatAmount(_ amount: String, with currency: String? = nil, locale: Locale = .current) -> String? {
134+
guard let decimalAmount = convertToDecimal(from: amount, locale: locale) else {
132135
return nil
133136
}
134137

135-
return formatAmount(decimalAmount, with: currency)
138+
return formatAmount(decimalAmount, with: currency ?? currencySettings.currencyCode.rawValue, locale: locale)
136139
}
137140

138141

139142
/// Formats the provided `amount` param into a human readable value and applies the currency option
140143
/// settings for the given currency.
141144
///
142145
/// - Parameters:
143-
/// - amount: a raw string representation of the amount, from the API, with no formatting applied. e.g. "19.87"
146+
/// - stringAmount: a raw string representation of the amount, from the API, with no formatting applied. e.g. "19.87"
144147
/// - currency: a 3-letter country code for currencies that are supported in the API. e.g. "USD"
145148
/// - roundSmallNumbers: if `true`, small numbers are rounded, if `false`, no rounding occurs (defaults to true)
149+
/// - locale: the locale that is used to format the currency amount string.
146150
/// - Returns: a formatted amount string
147151
///
148152
/// For our purposes here, a "small number" is anything in-between -1000 and 1000 (exclusive).
@@ -167,17 +171,21 @@ public class CurrencyFormatter {
167171
/// - 5800199.56 becomes "$5.8m"
168172
///
169173
func formatHumanReadableAmount(_ stringAmount: String,
170-
with currency: String = CurrencySettings.shared.currencyCode.rawValue,
171-
roundSmallNumbers: Bool = true) -> String? {
172-
guard let amount = convertToDecimal(from: stringAmount) else {
174+
with currency: String? = nil,
175+
roundSmallNumbers: Bool = true,
176+
locale: Locale = .current) -> String? {
177+
guard let amount = convertToDecimal(from: stringAmount, locale: locale) else {
178+
assertionFailure("Cannot convert the amount \"\(stringAmount)\" to decimal value with locale \(locale.identifier)")
173179
return nil
174180
}
175181

176-
let humanReadableAmount = amount.humanReadableString(roundSmallNumbers: roundSmallNumbers)
182+
let currency = currency ?? currencySettings.currencyCode.rawValue
183+
184+
let humanReadableAmount = amount.abs().humanReadableString(roundSmallNumbers: roundSmallNumbers)
177185
if humanReadableAmount == amount.stringValue, roundSmallNumbers == false {
178186
// The human readable version of amount is the same as the converted param value which means this is a "small"
179187
// number — format it normally *without* rounding.
180-
return formatAmount(amount, with: currency)
188+
return formatAmount(amount, with: currency, locale: locale)
181189
}
182190

183191
// If we are here, the human readable version of the amount param is a "large" number *OR* a small number but rounding has been requested,
@@ -190,15 +198,18 @@ public class CurrencyFormatter {
190198
return formatCurrency(using: humanReadableAmount,
191199
at: position,
192200
with: symbol,
193-
isNegative: isNegative)
201+
isNegative: isNegative,
202+
locale: locale)
194203
}
195204

196205
/// Applies currency option settings to the amount for the given currency.
197206
/// - Parameters:
198207
/// - amount: a NSDecimalNumber representation of the amount, from the API, with no formatting applied. e.g. "19.87"
199208
/// - currency: a 3-letter country code for currencies that are supported in the API. e.g. "USD"
209+
/// - locale: the locale that is used to format the currency amount string.
200210
///
201-
func formatAmount(_ decimalAmount: NSDecimalNumber, with currency: String = CurrencySettings.shared.currencyCode.rawValue) -> String? {
211+
func formatAmount(_ amount: NSDecimalNumber, with currency: String? = nil, locale: Locale = .current) -> String? {
212+
let currency = currency ?? currencySettings.currencyCode.rawValue
202213
// Get the currency code
203214
let code = CurrencySettings.CurrencyCode(rawValue: currency) ?? currencySettings.currencyCode
204215
// Grab the read-only currency options. These are set by the user in Site > Settings.
@@ -210,7 +221,7 @@ public class CurrencyFormatter {
210221

211222
// Put all the pieces of user preferences on currency formatting together
212223
// and spit out a string that has the formatted amount.
213-
let localized = localize(decimalAmount,
224+
let localized = localize(amount,
214225
with: separator,
215226
in: numberOfDecimals,
216227
including: thousandSeparator)
@@ -224,7 +235,8 @@ public class CurrencyFormatter {
224235
let formattedAmount = formatCurrency(using: localizedAmount,
225236
at: position,
226237
with: symbol,
227-
isNegative: decimalAmount.isNegative())
238+
isNegative: amount.isNegative(),
239+
locale: locale)
228240

229241
return formattedAmount
230242
}

WooCommerce/Classes/Tools/ImageService/DefaultImageService.swift

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ struct DefaultImageService: ImageService {
1414
private let imageCache: ImageCache
1515

1616
private var defaultOptions: KingfisherOptionsInfo {
17-
return [.targetCache(imageCache), .downloader(imageDownloader)]
17+
if let imageDownloader = imageDownloader as? Kingfisher.ImageDownloader {
18+
return [.targetCache(imageCache), .downloader(imageDownloader)]
19+
}
20+
return [.targetCache(imageCache)]
1821
}
1922

2023
init(imageCache: ImageCache = ImageCache.default,
21-
imageDownloader: ImageDownloader = ImageDownloader.default) {
24+
imageDownloader: ImageDownloader = Kingfisher.ImageDownloader.default) {
2225
self.imageCache = imageCache
2326
self.imageDownloader = imageDownloader
2427
}
@@ -35,21 +38,18 @@ struct DefaultImageService: ImageService {
3538
}
3639
}
3740

38-
func downloadImage(with url: URL, shouldCacheImage: Bool, completion: ImageDownloadCompletion?) {
39-
imageDownloader.downloadImage(with: url,
40-
options: nil) { result in
41-
switch result {
42-
case .success(let imageResult):
43-
let image = imageResult.image
44-
45-
if shouldCacheImage {
46-
self.imageCache.store(image, forKey: url.imageCacheKey)
47-
}
41+
func downloadImage(with url: URL, shouldCacheImage: Bool, completion: ImageDownloadCompletion?) -> ImageDownloadTask? {
42+
return imageDownloader.downloadImage(with: url) { result in
43+
switch result {
44+
case .success(let image):
45+
if shouldCacheImage {
46+
self.imageCache.store(image, forKey: url.imageCacheKey)
47+
}
4848

49-
completion?(image, nil)
50-
case .failure(let kingfisherError):
51-
completion?(nil, .other(error: kingfisherError))
52-
}
49+
completion?(image, nil)
50+
case .failure(let kingfisherError):
51+
completion?(nil, .other(error: kingfisherError))
52+
}
5353
}
5454
}
5555

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import UIKit
2+
3+
/// Used for any activity or action that may be canceled.
4+
/// TODO: `Cancellable` is also available starting iOS 13, please delete when the project is iOS 13+.
5+
///
6+
protocol Cancellable {
7+
func cancel()
8+
}
9+
10+
/// A task that downloads an image asynchronously.
11+
///
12+
protocol ImageDownloadTask: Cancellable {}
13+
14+
/// Performs tasks related to downloading an image.
15+
///
16+
protocol ImageDownloader {
17+
func downloadImage(with url: URL,
18+
onCompletion: ((Result<UIImage, Error>) -> Void)?) -> ImageDownloadTask?
19+
}

WooCommerce/Classes/Tools/ImageService/ImageService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ protocol ImageService {
1919
/// - url: url of the image.
2020
/// - shouldCacheImage: whether the downloaded image should be stored in the cache for faster access in the future.
2121
/// - completion: called when the image download completes.
22-
func downloadImage(with url: URL, shouldCacheImage: Bool, completion: ImageDownloadCompletion?)
22+
func downloadImage(with url: URL, shouldCacheImage: Bool, completion: ImageDownloadCompletion?) -> ImageDownloadTask?
2323

2424
/// Downloads and caches an image for a `UIImageView` given a URL and a placeholder.
2525
/// - Parameters:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Kingfisher
2+
3+
extension Kingfisher.DownloadTask: ImageDownloadTask {}
4+
5+
extension Kingfisher.ImageDownloader: ImageDownloader {
6+
func downloadImage(with url: URL, onCompletion: ((Result<UIImage, Error>) -> Void)?) -> ImageDownloadTask? {
7+
return downloadImage(with: url, options: nil) { result in
8+
switch result {
9+
case .success(let imageResult):
10+
onCompletion?(.success(imageResult.image))
11+
case .failure(let kingfisherError):
12+
onCompletion?(.failure(kingfisherError))
13+
}
14+
}
15+
}
16+
}

WooCommerce/Classes/ViewRelated/Editor/AztecTextViewAttachmentHandler.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import Aztec
22

33
/// Implements Aztec's `TextViewAttachmentDelegate` without media support.
44
final class AztecTextViewAttachmentHandler: TextViewAttachmentDelegate {
5+
private var activeImageTasks = [ImageDownloadTask]()
6+
7+
deinit {
8+
activeImageTasks.forEach { $0.cancel() }
9+
activeImageTasks.removeAll()
10+
}
11+
512
func textView(_ textView: TextView,
613
attachment: NSTextAttachment,
714
imageAt url: URL,
@@ -17,13 +24,16 @@ final class AztecTextViewAttachmentHandler: TextViewAttachmentDelegate {
1724
}
1825
}
1926

20-
imageService.downloadImage(with: url, shouldCacheImage: true) { image, error in
27+
let task = imageService.downloadImage(with: url, shouldCacheImage: true) { image, error in
2128
guard let image = image else {
2229
failure()
2330
return
2431
}
2532
success(image)
2633
}
34+
if let task = task {
35+
activeImageTasks.append(task)
36+
}
2737
default:
2838
return
2939
}

WooCommerce/Classes/ViewRelated/Products/Cells/Product Images/Collection View Cells/ProductImageCollectionViewCell.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ final class ProductImageCollectionViewCell: UICollectionViewCell {
44

55
@IBOutlet weak var imageView: UIImageView!
66

7+
var cancellableTask: Cancellable?
8+
79
override func awakeFromNib() {
810
super.awakeFromNib()
911
configureBackground()
@@ -16,6 +18,11 @@ final class ProductImageCollectionViewCell: UICollectionViewCell {
1618
// Border color is not automatically updated on trait collection changes and thus manually updated here.
1719
contentView.layer.borderColor = Colors.borderColor.cgColor
1820
}
21+
22+
override func prepareForReuse() {
23+
cancellableTask?.cancel()
24+
cancellableTask = nil
25+
}
1926
}
2027

2128
/// Private Methods

0 commit comments

Comments
 (0)