Files
purchases-ios/Sources/Networking/Operations/NetworkOperation.swift
T
Mark Villacampa 6f8e53ac40 Fix Xcode 16 warnings (#4334)
When compiling with Xcode 16, we started throwing a bunch of new warnings. Here's a list and explanation of each:

- The most common one was needing to explicitly restate the @unchecked Sendable conformance from a parent class.
- There were a couple switches where a new entry was added in tvOS but only for iOS 18, and needed to add it to make it exhaustive.
- There we a couple of places where we were adding a protocol conformance to a class from a different module e.g. extension DispatchTimeInterval: Comparable. This is considered unsafe now because the owners of the module could add the conformance later. Solved by removing the conformance which wasn't being used widely in the codebase, only in tests.
- Finally there was a class where we were capturing SKRequest and SKProductsRequest instances in a sendable block, but only to either log something, or use them as dictionary key, so I just made the classes Sendable (with the new @retroactice attribute because they belong to a different module)
2024-10-02 11:53:45 +02:00

209 lines
5.8 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// NetworkOperation.swift
//
// Created by Joshua Liebowitz on 11/18/21.
import Foundation
/// A type that can construct a `CacheableNetworkOperation` and pre-compute a cache key.
final class CacheableNetworkOperationFactory<T: CacheableNetworkOperation> {
let creator: (_ cacheKey: String) -> T
let cacheKey: String
var operationType: T.Type { T.self }
/**
- Parameter individualizedCacheKeyPart: The part of the cacheKey that makes it unique from other operations of the
same type. Example: If you posted receipts two times in a row you'd have 2 operations. The cache key would be
PostOperation + individualizedCacheKeyPart where individualizedCacheKeyPart is whatever you determine to be unique.
*/
init(_ creator: @escaping (_ cacheKey: String) -> T, individualizedCacheKeyPart: String) {
self.creator = creator
self.cacheKey = T.cacheKey(with: individualizedCacheKeyPart)
}
func create() -> T {
return self.creator(self.cacheKey)
}
}
class CacheableNetworkOperation: NetworkOperation, CacheKeyProviding {
let cacheKey: String
init(configuration: NetworkConfiguration, cacheKey: String) {
self.cacheKey = cacheKey
super.init(configuration: configuration)
}
fileprivate static func cacheKey(with individualizedCacheKeyPart: String) -> String {
return "\(Self.self) \(individualizedCacheKeyPart)"
}
}
// Restating inherited @unchecked Sendable from Foundation's Operation
extension CacheableNetworkOperation: @unchecked Sendable {}
class NetworkOperation: Operation {
let httpClient: HTTPClient
private let _didStart: Atomic<Bool> = false
private var didStart: Bool { return self._didStart.value }
// Note: implementing asynchronousy `Operations` needs KVO.
// We're not using Swift's `KeyPath` verison (`willChangeValue(for:)`)
// due to it crashing on iOS 12. See https://github.com/RevenueCat/purchases-ios/pull/2008.
private let _isExecuting: Atomic<Bool> = false
private(set) override final var isExecuting: Bool {
get {
return self._isExecuting.value
}
set {
self.willChangeValue(forKey: #keyPath(NetworkOperation.isExecuting))
self._isExecuting.value = newValue
self.didChangeValue(forKey: #keyPath(NetworkOperation.isExecuting))
}
}
private let _isFinished: Atomic<Bool> = false
private(set) override final var isFinished: Bool {
get {
return self._isFinished.value
}
set {
self.willChangeValue(forKey: #keyPath(NetworkOperation.isFinished))
self._isFinished.value = newValue
self.didChangeValue(forKey: #keyPath(NetworkOperation.isFinished))
}
}
private let _isCancelled: Atomic<Bool> = false
private(set) override final var isCancelled: Bool {
get {
return self._isCancelled.value
}
set {
self.willChangeValue(forKey: #keyPath(NetworkOperation.isCancelled))
self._isCancelled.value = newValue
self.didChangeValue(forKey: #keyPath(NetworkOperation.isCancelled))
}
}
init(configuration: NetworkConfiguration) {
self.httpClient = configuration.httpClient
super.init()
}
deinit {
RCTestAssert(
self.didStart,
"\(type(of: self)) was deallocated but it never started. Did it need to be created?"
)
RCTestAssert(
self.isFinished,
"\(type(of: self)) started but never finished. " +
"Did the operation not call `completion` in its `begin` implementation?"
)
}
override final func main() {
self._didStart.value = true
if self.isCancelled {
self.isFinished = true
return
}
self.isExecuting = true
self.log("Started")
self.begin {
self.finish()
}
}
override final func cancel() {
self.isCancelled = true
self.isExecuting = false
self.isFinished = true
self.log("Cancelled")
}
/// Overriden by subclasses to define the body of the operation
/// - Important: this method may be called from any thread so it must be thread-safe.
/// - Parameter completion: must be called when the operation has finished.
func begin(completion: @escaping () -> Void) {
fatalError("Subclasses must override this method")
}
private final func finish() {
assert(!self.isFinished, "Operation \(type(of: self)) (\(self)) was already finished")
self.log("Finished")
self.isExecuting = false
self.isFinished = true
}
// MARK: -
final override var isAsynchronous: Bool {
return true
}
// MARK: -
internal func log(_ message: CustomStringConvertible) {
Logger.debug(Strings.network.operation_state(type(of: self),
state: message.description))
}
// MARK: -
struct Configuration: NetworkConfiguration {
let httpClient: HTTPClient
}
struct UserSpecificConfiguration: AppUserConfiguration, NetworkConfiguration {
let httpClient: HTTPClient
let appUserID: String
}
}
// Restating inherited @unchecked Sendable from Foundation's Operation
extension NetworkOperation: @unchecked Sendable {}
protocol AppUserConfiguration {
var appUserID: String { get }
}
protocol NetworkConfiguration {
var httpClient: HTTPClient { get }
}