Skip to content

Commit

Permalink
Merge pull request #1590 from onevcat/feature/low-data-mode
Browse files Browse the repository at this point in the history
Feature Low Data Mode
  • Loading branch information
onevcat committed Jan 2, 2021
2 parents d880f03 + d53c35b commit 96610b8
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 21 deletions.
17 changes: 17 additions & 0 deletions Sources/General/KFOptionsSetter.swift
Expand Up @@ -320,6 +320,23 @@ extension KFOptionSetter {
options.retryStrategy = strategy
return self
}

/// Sets the `Source` should be loaded when user enables Low Data Mode and the original source fails with an
/// `NSURLErrorNetworkUnavailableReason.constrained` error.
/// - Parameter source: The `Source` will be loaded under low data mode.
/// - Returns: A `Self` value with changes applied.
///
/// When this option is set, the
/// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the
/// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a
/// low-resolution version of your image or a local image provider to display a placeholder.
///
/// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will
/// be loaded following the system default behavior, in a normal way.
public func lowDataModeSource(_ source: Source?) -> Self {
options.lowDataModeSource = source
return self
}
}

// MARK: - Request Modifier
Expand Down
12 changes: 12 additions & 0 deletions Sources/General/KingfisherError.swift
Expand Up @@ -256,6 +256,18 @@ public enum KingfisherError: Error {
}
return false
}

var isLowDataModeConstrained: Bool {
if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *),
case .responseError(reason: .URLSessionError(let sessionError)) = self,
let urlError = sessionError as? URLError,
urlError.networkUnavailableReason == .constrained
{
return true
}
return false
}

}

// MARK: - LocalizedError Conforming
Expand Down
34 changes: 13 additions & 21 deletions Sources/General/KingfisherManager.swift
Expand Up @@ -241,7 +241,19 @@ public class KingfisherManager {
completionHandler?(.failure(error))
return
}
// When low data mode constrained error, retry with the low data mode source instead of use alternative on fly.
guard !error.isLowDataModeConstrained else {
if let source = retrievingContext.options.lowDataModeSource {
retrievingContext.options.lowDataModeSource = nil
startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated)
} else {
// This should not happen.
completionHandler?(.failure(error))
}
return
}
if let nextSource = retrievingContext.popAlternativeSource() {
retrievingContext.appendError(error, to: source)
startNewRetrieveTask(with: nextSource, downloadTaskUpdated: downloadTaskUpdated)
} else {
// No other alternative source. Finish with error.
Expand Down Expand Up @@ -276,27 +288,7 @@ public class KingfisherManager {
}
}
} else {

// Skip alternative sources if the user cancelled it.
guard !error.isTaskCancelled else {
completionHandler?(.failure(error))
return
}
if let nextSource = retrievingContext.popAlternativeSource() {
retrievingContext.appendError(error, to: currentSource)
startNewRetrieveTask(with: nextSource, downloadTaskUpdated: downloadTaskUpdated)
} else {
// No other alternative source. Finish with error.
if retrievingContext.propagationErrors.isEmpty {
completionHandler?(.failure(error))
} else {
retrievingContext.appendError(error, to: currentSource)
let finalError = KingfisherError.imageSettingError(
reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors)
)
completionHandler?(.failure(finalError))
}
}
failCurrentSource(currentSource, with: error)
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/General/KingfisherOptionsInfo.swift
Expand Up @@ -248,6 +248,16 @@ public enum KingfisherOptionsInfoItem {
/// when pass to an `ImageDownloader` or `ImageCache`.
///
case retryStrategy(RetryStrategy)

/// The `Source` should be loaded when user enables Low Data Mode and the original source fails with an
/// `NSURLErrorNetworkUnavailableReason.constrained` error. When this option is set, the
/// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the
/// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a
/// low-resolution version of your image or a local image provider to display a placeholder.
///
/// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will
/// be loaded following the system default behavior, in a normal way.
case lowDataSource(Source?)
}

// Improve performance by parsing the input `KingfisherOptionsInfo` (self) first.
Expand Down Expand Up @@ -291,6 +301,7 @@ public struct KingfisherParsedOptionsInfo {
public var progressiveJPEG: ImageProgressive? = nil
public var alternativeSources: [Source]? = nil
public var retryStrategy: RetryStrategy? = nil
public var lowDataModeSource: Source? = nil

var onDataReceived: [DataReceivingSideEffect]? = nil

Expand Down Expand Up @@ -332,6 +343,7 @@ public struct KingfisherParsedOptionsInfo {
case .progressiveJPEG(let value): progressiveJPEG = value
case .alternativeSources(let sources): alternativeSources = sources
case .retryStrategy(let strategy): retryStrategy = strategy
case .lowDataSource(let source): lowDataModeSource = source
}
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/Networking/ImageDownloader.swift
Expand Up @@ -239,6 +239,9 @@ open class ImageDownloader {
// Creates default request.
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
request.httpShouldUsePipelining = requestsUsePipelining
if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil {
request.allowsConstrainedNetworkAccess = false
}

if let requestModifier = options.requestModifier {
// Modifies request before sending.
Expand Down
32 changes: 32 additions & 0 deletions Tests/KingfisherTests/ImageViewExtensionTests.swift
Expand Up @@ -580,6 +580,14 @@ class ImageViewExtensionTests: XCTestCase {
imageView.kf.setImage(with: url, options: [.onFailureImage(testImage)]) {
result in
XCTAssertNil(result.value)

if case KingfisherError.responseError(let reason) = result.error!,
case .URLSessionError(error: let nsError) = reason
{
XCTAssertEqual((nsError as NSError).code, 404)
} else {
XCTFail()
}
XCTAssertEqual(self.imageView.image, testImage)
exp.fulfill()
}
Expand Down Expand Up @@ -841,6 +849,30 @@ class ImageViewExtensionTests: XCTestCase {
waitForExpectations(timeout: 1, handler: nil)
}

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func testLowDataModeSource() {
let exp = expectation(description: #function)

let url = testURLs[0]
stub(url, data: testImageData)

// Stub a failure of `.constrained`. It is what happens when an image downloading fails when low data mode on.
let brokenURL = testURLs[1]
let error = URLError(
.notConnectedToInternet,
userInfo: [NSURLErrorNetworkUnavailableReasonKey: URLError.NetworkUnavailableReason.constrained.rawValue]
)
stub(brokenURL, error: error)

imageView.kf.setImage(with: .network(brokenURL), options: [.lowDataSource(.network(url))]) { result in
XCTAssertNotNil(result.value)
XCTAssertEqual(result.value?.source.url, url)
XCTAssertEqual(result.value?.originalSource.url, brokenURL)
exp.fulfill()
}
waitForExpectations(timeout: 1, handler: nil)
}

}

extension KFCrossPlatformView: Placeholder {}
4 changes: 4 additions & 0 deletions Tests/KingfisherTests/Utils/StubHelpers.swift
Expand Up @@ -42,5 +42,9 @@ func delayedStub(_ url: URL, data: Data, statusCode: Int = 200, length: Int? = n

func stub(_ url: URL, errorCode: Int) {
let error = NSError(domain: "stubError", code: errorCode, userInfo: nil)
stub(url, error: error)
}

func stub(_ url: URL, error: Error) {
return stubRequest("GET", url.absoluteString as NSString).andFailWithError(error)
}

0 comments on commit 96610b8

Please sign in to comment.