Skip to content

Commit

Permalink
Merge pull request #795 from kean/swift-6
Browse files Browse the repository at this point in the history
Swift 6
  • Loading branch information
kean authored Jul 13, 2024
2 parents 311016d + e18df74 commit 63105e0
Show file tree
Hide file tree
Showing 38 changed files with 181 additions and 86 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,17 @@ jobs:
# Scripts/test.sh -s "NukeUI" -d "OS=9.1,name=Apple Watch Series 8 (45mm)"
# Scripts/test.sh -s "Nuke Extensions" -d "OS=9.1,name=Apple Watch Series 8 (45mm)"
ios-xcode-14-3-1:
name: Unit Tests (iOS 16.4, Xcode 14.3.1)
name: Unit Tests (iOS 17.0, Xcode 15.0)
runs-on: macOS-13
env:
DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer
steps:
- uses: actions/checkout@v2
- name: Run Tests
run: |
Scripts/test.sh -s "Nuke" -d "OS=16.4,name=iPhone 14 Pro"
Scripts/test.sh -s "NukeUI" -d "OS=16.4,name=iPhone 14 Pro"
Scripts/test.sh -s "NukeExtensions" -d "OS=16.4,name=iPhone 14 Pro"
Scripts/test.sh -s "Nuke" -d "OS=17.0,name=iPhone 15 Pro"
Scripts/test.sh -s "NukeUI" -d "OS=17.0,name=iPhone 15 Pro"
Scripts/test.sh -s "NukeExtensions" -d "OS=17.0,name=iPhone 15 Pro"
ios-thread-safety:
name: Thread Safety Tests (TSan Enabled)
runs-on: macOS-14
Expand Down
8 changes: 8 additions & 0 deletions Nuke.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1836,6 +1836,7 @@
OTHER_SWIFT_FLAGS = "-D DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukeui;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
Expand All @@ -1855,6 +1856,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukeui;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Release;
};
Expand Down Expand Up @@ -1939,6 +1941,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nuke-extensions";
PRODUCT_NAME = NukeExtensions;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
Expand All @@ -1958,6 +1961,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nuke-extensions";
PRODUCT_NAME = NukeExtensions;
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Release;
};
Expand Down Expand Up @@ -2019,6 +2023,7 @@
OTHER_SWIFT_FLAGS = "-D DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukevideo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
Expand All @@ -2038,6 +2043,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukevideo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Release;
};
Expand Down Expand Up @@ -2298,6 +2304,7 @@
OTHER_SWIFT_FLAGS = "-D DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nuke;
PRODUCT_NAME = Nuke;
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Debug;
};
Expand All @@ -2317,6 +2324,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nuke;
PRODUCT_NAME = Nuke;
SWIFT_STRICT_CONCURRENCY = complete;
};
name = Release;
};
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ The image pipeline is easy to customize and extend. Check out the following firs
| Nuke | Date | Swift | Xcode | Platforms |
|------------|--------------|-------------|------------|-----------------------------------------------|
| Nuke 12.0 | Mar 4, 2023 | Swift 5.7 | Xcode 14.1 | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0 |
| Nuke 12.0 | Mar 4, 2023 | Swift 5.7 | Xcode 15.0 | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0 |
| Nuke 11.0 | Jul 20, 2022 | Swift 5.6 | Xcode 13.3 | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0 |
| Nuke 10.0 | Jun 1, 2021 | Swift 5.3 | Xcode 12.0 | iOS 11.0, watchOS 4.0, macOS 10.13, tvOS 11.0 |

Expand Down
3 changes: 1 addition & 2 deletions Sources/Nuke/ImageRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,7 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri
/// ```
public static let imageIdKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/imageId"

/// The image scale to be used. By default, the scale matches the scale
/// of the current display.
/// The image scale to be used. By default, the scale is `1`.
public static let scaleKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/scale"

/// Specifies whether the pipeline should retrieve or generate a thumbnail
Expand Down
4 changes: 2 additions & 2 deletions Sources/Nuke/ImageTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).

import Foundation
import Combine
@preconcurrency import Combine

#if canImport(UIKit)
import UIKit
Expand Down Expand Up @@ -283,7 +283,7 @@ public typealias AsyncImageTask = ImageTask
// MARK: - ImageTask (Private)

extension ImageTask {
private func makeStream<T>(of closure: @escaping (Event) -> T?) -> AsyncStream<T> {
private func makeStream<T>(of closure: @Sendable @escaping (Event) -> T?) -> AsyncStream<T> {
AsyncStream { continuation in
self.queue.async {
guard let events = self._makeEventsSubject() else {
Expand Down
10 changes: 8 additions & 2 deletions Sources/Nuke/Internal/DataPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ final class DataPublisher {
private func publisher(from closure: @Sendable @escaping () async throws -> Data) -> AnyPublisher<Data, Error> {
Deferred {
Future { promise in
let promise = UncheckedSendableBox(value: promise)
Task {
do {
let data = try await closure()
promise(.success(data))
promise.value(.success(data))
} catch {
promise(.failure(error))
promise.value(.failure(error))
}
}
}
Expand All @@ -52,3 +53,8 @@ enum PublisherCompletion {
case finished
case failure(Error)
}

/// - warning: Avoid using it!
struct UncheckedSendableBox<Value>: @unchecked Sendable {
let value: Value
}
1 change: 0 additions & 1 deletion Sources/Nuke/Internal/ImagePublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ private final class ImageSubscription<S>: Subscription where S: Subscriber, S: S

task = pipeline.loadImage(
with: request,
queue: nil,
progress: { response, _, _ in
if let response {
// Send progressively decoded image (if enabled and if any)
Expand Down
4 changes: 1 addition & 3 deletions Sources/Nuke/Internal/Log.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,12 @@ func signpost<T>(_ name: StaticString, _ work: () throws -> T) rethrows -> T {

private let log = Atomic(value: OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading"))

private let byteFormatter = ByteCountFormatter()

enum Formatter {
static func bytes(_ count: Int) -> String {
bytes(Int64(count))
}

static func bytes(_ count: Int64) -> String {
byteFormatter.string(fromByteCount: count)
ByteCountFormatter().string(fromByteCount: count)
}
}
2 changes: 1 addition & 1 deletion Sources/Nuke/Internal/Operation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import Foundation

final class Operation: Foundation.Operation {
final class Operation: Foundation.Operation, @unchecked Sendable {
override var isExecuting: Bool {
get {
os_unfair_lock_lock(lock)
Expand Down
25 changes: 14 additions & 11 deletions Sources/Nuke/Loading/DataLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation
/// Provides basic networking using `URLSession`.
public final class DataLoader: DataLoading, @unchecked Sendable {
public let session: URLSession
private let impl = _DataLoader()
private let impl: _DataLoader

/// Determines whether to deliver a partial response body in increments. By
/// default, `false`.
Expand Down Expand Up @@ -41,12 +41,12 @@ public final class DataLoader: DataLoading, @unchecked Sendable {
/// - validate: Validates the response. By default, check if the status
/// code is in the acceptable range (`200..<300`).
public init(configuration: URLSessionConfiguration = DataLoader.defaultConfiguration,
validate: @escaping (URLResponse) -> Swift.Error? = DataLoader.validate) {
validate: @Sendable @escaping (URLResponse) -> Swift.Error? = DataLoader.validate) {
self.impl = _DataLoader(validate: validate)
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
self.session = URLSession(configuration: configuration, delegate: impl, delegateQueue: queue)
self.session.sessionDescription = "Nuke URLSession"
self.impl.validate = validate
}

/// Returns a default configuration which has a `sharedUrlCache` set
Expand All @@ -59,7 +59,7 @@ public final class DataLoader: DataLoading, @unchecked Sendable {

/// Validates `HTTP` responses by checking that the status code is 2xx. If
/// it's not returns ``DataLoader/Error/statusCodeUnacceptable(_:)``.
public static func validate(response: URLResponse) -> Swift.Error? {
@Sendable public static func validate(response: URLResponse) -> Swift.Error? {
guard let response = response as? HTTPURLResponse else {
return nil
}
Expand Down Expand Up @@ -117,11 +117,15 @@ public final class DataLoader: DataLoading, @unchecked Sendable {
// Actual data loader implementation. Hide NSObject inheritance, hide
// URLSessionDataDelegate conformance, and break retain cycle between URLSession
// and URLSessionDataDelegate.
private final class _DataLoader: NSObject, URLSessionDataDelegate {
var validate: (URLResponse) -> Swift.Error? = DataLoader.validate
private final class _DataLoader: NSObject, URLSessionDataDelegate, @unchecked Sendable {
let validate: @Sendable (URLResponse) -> Swift.Error?
private var handlers = [URLSessionTask: _Handler]()
var delegate: URLSessionDelegate?

init(validate: @Sendable @escaping (URLResponse) -> Swift.Error?) {
self.validate = validate
}

/// Loads data with the given request.
func loadData(with task: URLSessionDataTask,
session: URLSession,
Expand Down Expand Up @@ -179,21 +183,20 @@ private final class _DataLoader: NSObject, URLSessionDataDelegate {
(delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, didFinishCollecting: metrics)
}

func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
(delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler) ??
completionHandler(request)
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @Sendable @escaping (URLRequest?) -> Void) {
(delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler) ?? completionHandler(request)
}

func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
(delegate as? URLSessionTaskDelegate)?.urlSession?(session, taskIsWaitingForConnectivity: task)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @Sendable @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
(delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, didReceive: challenge, completionHandler: completionHandler) ??
completionHandler(.performDefaultHandling, nil)
}

func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {
func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @Sendable @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {
(delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, willBeginDelayedRequest: request, completionHandler: completionHandler) ??
completionHandler(.continueLoading, nil)
}
Expand Down
8 changes: 7 additions & 1 deletion Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,13 @@ extension ImagePipeline {

/// A queue on which all callbacks, like `progress` and `completion`
/// callbacks are called. `.main` by default.
public var callbackQueue = DispatchQueue.main
@available(*, deprecated, message: "`ImagePipeline` no longer supports changing the callback queue")
public var callbackQueue: DispatchQueue {
get { _callbackQueue }
set { _callbackQueue = newValue }
}

var _callbackQueue = DispatchQueue.main

// MARK: - Options (Shared)

Expand Down
39 changes: 29 additions & 10 deletions Sources/Nuke/Pipeline/ImagePipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public final class ImagePipeline: @unchecked Sendable {
with url: URL,
completion: @escaping (_ result: Result<ImageResponse, Error>) -> Void
) -> ImageTask {
loadImage(with: ImageRequest(url: url), queue: nil, progress: nil, completion: completion)
_loadImage(with: ImageRequest(url: url), progress: nil, completion: completion)
}

/// Loads an image for the given request.
Expand All @@ -177,15 +177,13 @@ public final class ImagePipeline: @unchecked Sendable {
with request: ImageRequest,
completion: @escaping (_ result: Result<ImageResponse, Error>) -> Void
) -> ImageTask {
loadImage(with: request, queue: nil, progress: nil, completion: completion)
_loadImage(with: request, progress: nil, completion: completion)
}

/// Loads an image for the given request.
///
/// - parameters:
/// - request: An image request.
/// - queue: A queue on which to execute `progress` and `completion` callbacks.
/// By default, the pipeline uses `.main` queue.
/// - progress: A closure to be called periodically on the main thread when
/// the progress is updated.
/// - completion: A closure to be called on the main thread when the request
Expand All @@ -196,15 +194,15 @@ public final class ImagePipeline: @unchecked Sendable {
progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?,
completion: @escaping (_ result: Result<ImageResponse, Error>) -> Void
) -> ImageTask {
loadImage(with: request, queue: queue, progress: {
_loadImage(with: request, queue: queue, progress: {
progress?($0, $1.completed, $1.total)
}, completion: completion)
}

func loadImage(
func _loadImage(
with request: ImageRequest,
isDataTask: Bool = false,
queue callbackQueue: DispatchQueue?,
queue callbackQueue: DispatchQueue? = nil,
progress: ((ImageResponse?, ImageTask.Progress) -> Void)?,
completion: @escaping (Result<ImageResponse, Error>) -> Void
) -> ImageTask {
Expand All @@ -225,11 +223,15 @@ public final class ImagePipeline: @unchecked Sendable {
}
}

// nuke-13: requires callbacks to be @MainActor @Sendable or deprecate this entire API
private func dispatchCallback(to callbackQueue: DispatchQueue?, _ closure: @escaping () -> Void) {
let box = UncheckedSendableBox(value: closure)
if callbackQueue === self.queue {
closure()
} else {
(callbackQueue ?? self.configuration.callbackQueue).async(execute: closure)
(callbackQueue ?? self.configuration._callbackQueue).async {
box.value()
}
}
}

Expand All @@ -238,7 +240,24 @@ public final class ImagePipeline: @unchecked Sendable {
/// Loads image data for the given request. The data doesn't get decoded
/// or processed in any other way.
@discardableResult public func loadData(with request: ImageRequest, completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask {
loadData(with: request, queue: nil, progress: nil, completion: completion)
_loadData(with: request, queue: nil, progress: nil, completion: completion)
}

private func _loadData(
with request: ImageRequest,
queue: DispatchQueue?,
progress progressHandler: ((_ completed: Int64, _ total: Int64) -> Void)?,
completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void
) -> ImageTask {
_loadImage(with: request, isDataTask: true, queue: queue) { _, progress in
progressHandler?(progress.completed, progress.total)
} completion: { result in
let result = result.map { response in
// Data should never be empty
(data: response.container.data ?? Data(), response: response.urlResponse)
}
completion(result)
}
}

/// Loads the image data for the given request. The data doesn't get decoded
Expand All @@ -260,7 +279,7 @@ public final class ImagePipeline: @unchecked Sendable {
progress progressHandler: ((_ completed: Int64, _ total: Int64) -> Void)?,
completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void
) -> ImageTask {
loadImage(with: request, isDataTask: true, queue: queue) { _, progress in
_loadImage(with: request, isDataTask: true, queue: queue) { _, progress in
progressHandler?(progress.completed, progress.total)
} completion: { result in
let result = result.map { response in
Expand Down
4 changes: 2 additions & 2 deletions Sources/Nuke/Prefetching/ImagePrefetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public final class ImagePrefetcher: @unchecked Sendable {
/// regardless of whether the requests succeed or some fail.
///
/// - note: The closure is called on the main queue.
public var didComplete: (() -> Void)?
public var didComplete: (@MainActor @Sendable () -> Void)?

private let pipeline: ImagePipeline
private var tasks = [TaskLoadImageKey: Task]()
Expand Down Expand Up @@ -136,7 +136,7 @@ public final class ImagePrefetcher: @unchecked Sendable {
}

private func loadImage(task: Task, finish: @escaping () -> Void) {
task.imageTask = pipeline.loadImage(with: task.request, isDataTask: destination == .diskCache, queue: pipeline.queue, progress: nil) { [weak self] _ in
task.imageTask = pipeline._loadImage(with: task.request, isDataTask: destination == .diskCache, queue: pipeline.queue, progress: nil) { [weak self] _ in
self?._remove(task)
finish()
}
Expand Down
Loading

0 comments on commit 63105e0

Please sign in to comment.