| | |
| | | PODS: |
| | | - Alamofire (5.9.1) |
| | | - Alamofire (5.10.2) |
| | | - AliyunOSSiOS (2.10.22) |
| | | - APNGKit (2.3.0): |
| | | - Delegate (~> 1.3) |
| | |
| | | - EmptyDataSet-Swift (5.0.0) |
| | | - FFPage (3.0.0) |
| | | - HandyJSON (5.0.2) |
| | | - IQKeyboardCore (1.0.5) |
| | | - IQKeyboardManager (6.5.19) |
| | | - IQKeyboardManagerSwift (7.1.1) |
| | | - IQKeyboardManagerSwift (8.0.0): |
| | | - IQKeyboardManagerSwift/Appearance (= 8.0.0) |
| | | - IQKeyboardManagerSwift/Core (= 8.0.0) |
| | | - IQKeyboardManagerSwift/IQKeyboardReturnManager (= 8.0.0) |
| | | - IQKeyboardManagerSwift/IQKeyboardToolbarManager (= 8.0.0) |
| | | - IQKeyboardManagerSwift/IQTextView (= 8.0.0) |
| | | - IQKeyboardManagerSwift/Resign (= 8.0.0) |
| | | - IQKeyboardManagerSwift/Appearance (8.0.0): |
| | | - IQKeyboardManagerSwift/Core |
| | | - IQKeyboardManagerSwift/Core (8.0.0): |
| | | - IQKeyboardNotification |
| | | - IQTextInputViewNotification |
| | | - IQKeyboardManagerSwift/IQKeyboardReturnManager (8.0.0): |
| | | - IQKeyboardReturnManager |
| | | - IQKeyboardManagerSwift/IQKeyboardToolbarManager (8.0.0): |
| | | - IQKeyboardManagerSwift/Core |
| | | - IQKeyboardToolbarManager |
| | | - IQKeyboardManagerSwift/IQTextView (8.0.0): |
| | | - IQTextView |
| | | - IQKeyboardManagerSwift/Resign (8.0.0): |
| | | - IQKeyboardManagerSwift/Core |
| | | - IQKeyboardNotification (1.0.3) |
| | | - IQKeyboardReturnManager (1.0.4): |
| | | - IQKeyboardCore (= 1.0.5) |
| | | - IQKeyboardToolbar (1.1.1): |
| | | - IQKeyboardCore |
| | | - IQKeyboardToolbar/Core (= 1.1.1) |
| | | - IQKeyboardToolbar/Core (1.1.1): |
| | | - IQKeyboardCore |
| | | - IQKeyboardToolbar/Placeholderable |
| | | - IQKeyboardToolbar/Placeholderable (1.1.1): |
| | | - IQKeyboardCore |
| | | - IQKeyboardToolbarManager (1.1.3): |
| | | - IQKeyboardToolbar |
| | | - IQTextInputViewNotification |
| | | - IQTextInputViewNotification (1.0.8): |
| | | - IQKeyboardCore |
| | | - IQTextView (1.0.5): |
| | | - IQKeyboardToolbar/Placeholderable |
| | | - JQTools (0.1.5): |
| | | - EmptyDataSet-Swift |
| | | - HandyJSON |
| | |
| | | - MJRefresh |
| | | - ObjectMapper |
| | | - QMUIKit (~> 4.7.0) |
| | | - RxCocoa |
| | | - RxDataSources |
| | | - RxSwift |
| | | - RxCocoa (~> 6.9.0) |
| | | - RxDataSources (~> 5.0.0) |
| | | - RxSwift (~> 6.9.0) |
| | | - SDWebImage |
| | | - SnapKit |
| | | - SVProgressHUD |
| | |
| | | - QMUIKit/QMUILog |
| | | - QMUIKit/QMUIResources (4.7.0) |
| | | - QMUIKit/QMUIWeakObjectContainer (4.7.0) |
| | | - RxCocoa (6.7.1): |
| | | - RxRelay (= 6.7.1) |
| | | - RxSwift (= 6.7.1) |
| | | - RxCocoa (6.9.0): |
| | | - RxRelay (= 6.9.0) |
| | | - RxSwift (= 6.9.0) |
| | | - RxDataSources (5.0.0): |
| | | - Differentiator (~> 5.0) |
| | | - RxCocoa (~> 6.0) |
| | | - RxSwift (~> 6.0) |
| | | - RxRelay (6.7.1): |
| | | - RxSwift (= 6.7.1) |
| | | - RxSwift (6.7.1) |
| | | - SDWebImage (5.19.6): |
| | | - SDWebImage/Core (= 5.19.6) |
| | | - SDWebImage/Core (5.19.6) |
| | | - RxRelay (6.9.0): |
| | | - RxSwift (= 6.9.0) |
| | | - RxSwift (6.9.0) |
| | | - SDWebImage (5.20.0): |
| | | - SDWebImage/Core (= 5.20.0) |
| | | - SDWebImage/Core (5.20.0) |
| | | - SnapKit (5.7.1) |
| | | - SPPageMenu (3.5.0) |
| | | - SVProgressHUD (2.3.1): |
| | | - SVProgressHUD/Core (= 2.3.1) |
| | | - SVProgressHUD/Core (2.3.1) |
| | | - SwifterSwift (6.2.0): |
| | | - SwifterSwift/AppKit (= 6.2.0) |
| | | - SwifterSwift/Combine (= 6.2.0) |
| | | - SwifterSwift/CoreAnimation (= 6.2.0) |
| | | - SwifterSwift/CoreGraphics (= 6.2.0) |
| | | - SwifterSwift/CoreLocation (= 6.2.0) |
| | | - SwifterSwift/CryptoKit (= 6.2.0) |
| | | - SwifterSwift/Dispatch (= 6.2.0) |
| | | - SwifterSwift/Foundation (= 6.2.0) |
| | | - SwifterSwift/HealthKit (= 6.2.0) |
| | | - SwifterSwift/MapKit (= 6.2.0) |
| | | - SwifterSwift/SceneKit (= 6.2.0) |
| | | - SwifterSwift/SpriteKit (= 6.2.0) |
| | | - SwifterSwift/StoreKit (= 6.2.0) |
| | | - SwifterSwift/SwiftStdlib (= 6.2.0) |
| | | - SwifterSwift/UIKit (= 6.2.0) |
| | | - SwifterSwift/WebKit (= 6.2.0) |
| | | - SwifterSwift/AppKit (6.2.0) |
| | | - SwifterSwift/Combine (6.2.0) |
| | | - SwifterSwift/CoreAnimation (6.2.0) |
| | | - SwifterSwift/CoreGraphics (6.2.0) |
| | | - SwifterSwift/CoreLocation (6.2.0) |
| | | - SwifterSwift/CryptoKit (6.2.0) |
| | | - SwifterSwift/Dispatch (6.2.0) |
| | | - SwifterSwift/Foundation (6.2.0) |
| | | - SwifterSwift/HealthKit (6.2.0) |
| | | - SwifterSwift/MapKit (6.2.0) |
| | | - SwifterSwift/SceneKit (6.2.0) |
| | | - SwifterSwift/SpriteKit (6.2.0) |
| | | - SwifterSwift/StoreKit (6.2.0) |
| | | - SwifterSwift/SwiftStdlib (6.2.0) |
| | | - SwifterSwift/UIKit (6.2.0) |
| | | - SwifterSwift/WebKit (6.2.0) |
| | | - SwifterSwift (7.0.0): |
| | | - SwifterSwift/AppKit (= 7.0.0) |
| | | - SwifterSwift/CoreAnimation (= 7.0.0) |
| | | - SwifterSwift/CoreGraphics (= 7.0.0) |
| | | - SwifterSwift/CoreLocation (= 7.0.0) |
| | | - SwifterSwift/CryptoKit (= 7.0.0) |
| | | - SwifterSwift/Dispatch (= 7.0.0) |
| | | - SwifterSwift/Foundation (= 7.0.0) |
| | | - SwifterSwift/HealthKit (= 7.0.0) |
| | | - SwifterSwift/MapKit (= 7.0.0) |
| | | - SwifterSwift/SceneKit (= 7.0.0) |
| | | - SwifterSwift/SpriteKit (= 7.0.0) |
| | | - SwifterSwift/StoreKit (= 7.0.0) |
| | | - SwifterSwift/SwiftStdlib (= 7.0.0) |
| | | - SwifterSwift/UIKit (= 7.0.0) |
| | | - SwifterSwift/WebKit (= 7.0.0) |
| | | - SwifterSwift/AppKit (7.0.0) |
| | | - SwifterSwift/CoreAnimation (7.0.0) |
| | | - SwifterSwift/CoreGraphics (7.0.0) |
| | | - SwifterSwift/CoreLocation (7.0.0) |
| | | - SwifterSwift/CryptoKit (7.0.0) |
| | | - SwifterSwift/Dispatch (7.0.0) |
| | | - SwifterSwift/Foundation (7.0.0) |
| | | - SwifterSwift/HealthKit (7.0.0) |
| | | - SwifterSwift/MapKit (7.0.0) |
| | | - SwifterSwift/SceneKit (7.0.0) |
| | | - SwifterSwift/SpriteKit (7.0.0) |
| | | - SwifterSwift/StoreKit (7.0.0) |
| | | - SwifterSwift/SwiftStdlib (7.0.0) |
| | | - SwifterSwift/UIKit (7.0.0) |
| | | - SwifterSwift/WebKit (7.0.0) |
| | | - SwiftyStoreKit (0.16.1) |
| | | - TZImagePickerController (3.8.7): |
| | | - TZImagePickerController/Basic (= 3.8.7) |
| | | - TZImagePickerController/Location (= 3.8.7) |
| | | - TZImagePickerController/Basic (3.8.7) |
| | | - TZImagePickerController/Location (3.8.7) |
| | | - TZImagePickerController (3.8.8): |
| | | - TZImagePickerController/Basic (= 3.8.8) |
| | | - TZImagePickerController/Location (= 3.8.8) |
| | | - TZImagePickerController/Basic (3.8.8) |
| | | - TZImagePickerController/Location (3.8.8) |
| | | - UserDefaultsStore (1.5.0) |
| | | - VTMagic (1.2.4): |
| | | - VTMagic/Core (= 1.2.4) |
| | |
| | | - EmptyDataSet-Swift |
| | | - FFPage |
| | | - HandyJSON |
| | | - IQKeyboardCore |
| | | - IQKeyboardManager |
| | | - IQKeyboardManagerSwift |
| | | - IQKeyboardNotification |
| | | - IQKeyboardReturnManager |
| | | - IQKeyboardToolbar |
| | | - IQKeyboardToolbarManager |
| | | - IQTextInputViewNotification |
| | | - IQTextView |
| | | - Lantern |
| | | - MJRefresh |
| | | - ObjcExceptionBridging |
| | |
| | | :path: "/Users/yvkd/MyProject/JQTools" |
| | | |
| | | SPEC CHECKSUMS: |
| | | Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c |
| | | Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 |
| | | AliyunOSSiOS: b46648fd78909a567e3743fe94183748a407b175 |
| | | APNGKit: eb7e111277527cfd47636f797c9c8e7aab5d9601 |
| | | CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 |
| | |
| | | EmptyDataSet-Swift: eb382c0c87a2d9c678077385a595cec52da38171 |
| | | FFPage: 481cc0f2dde0f6be84a2359b6c86272e0024dc8d |
| | | HandyJSON: 9e4e236f5d2dbefad5155a77417bbea438201c03 |
| | | IQKeyboardCore: 28c8bf3bcd8ba5aa1570b318cbc4da94b861711e |
| | | IQKeyboardManager: c8665b3396bd0b79402b4c573eac345a31c7d485 |
| | | IQKeyboardManagerSwift: d7f3d3a562c237a0e7335e657cd598c452f57f1b |
| | | JQTools: af562f97302a433989c23bfb31e24458eb6469ad |
| | | IQKeyboardManagerSwift: 0c6fbbaa2e60739e48d7cf59f25661471a7a3a65 |
| | | IQKeyboardNotification: d7382c4466c5a5adef92c7452ebf861b36050088 |
| | | IQKeyboardReturnManager: 972be48528ce9e7508ab3ab15ac7efac803f17f5 |
| | | IQKeyboardToolbar: d4bdccfb78324aec2f3920659c77bb89acd33312 |
| | | IQKeyboardToolbarManager: 6c693c8478d6327a7ef2107528d29698b3514dbb |
| | | IQTextInputViewNotification: f5e954d8881fd9808b744e49e024cc0d4bcfe572 |
| | | IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7 |
| | | JQTools: 91910e06efed6aeabbccfae7d988d3016475b05a |
| | | Lantern: b192e7146c6d04e15e627f37281254a6a8593703 |
| | | MJRefresh: ff9e531227924c84ce459338414550a05d2aea78 |
| | | ObjcExceptionBridging: d3d37d62981bb7f252ecb31b62d7e23a96bbfb8a |
| | | ObjectMapper: e6e4d91ff7f2861df7aecc536c92d8363f4c9677 |
| | | QMUIKit: 2fc09ba9a31a44a4081916ed4c41467bac798821 |
| | | RxCocoa: f5609cb4637587a7faa99c5d5787e3ad582b75a4 |
| | | RxCocoa: ac16414696ae706516be3e1ab00fcce5bdc9be8a |
| | | RxDataSources: aa47cc1ed6c500fa0dfecac5c979b723542d79cf |
| | | RxRelay: 4151ba01152436b08271e08410135e099880eae5 |
| | | RxSwift: b9a93a26031785159e11abd40d1a55bcb8057e52 |
| | | SDWebImage: a79252b60f4678812d94316c91da69ec83089c9f |
| | | RxRelay: 6b0c930e5cef57d5fe2032571e5e65b78e3cbdb1 |
| | | RxSwift: 31649ace6aceeb422e16ff71c60804f9c3281ed9 |
| | | SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 |
| | | SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a |
| | | SPPageMenu: da182aafcec55719d5c326103cc7716c1e48f311 |
| | | SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22 |
| | | SwifterSwift: dd00873fb09cde19da88bdb2878f9fe70fe27b0f |
| | | SwifterSwift: e9caf990fc72e835432280755d1f4c43f2a483d5 |
| | | SwiftyStoreKit: 6b9c08810269f030586dac1fae8e75871a82e84a |
| | | TZImagePickerController: 5f35bb7266552e36ca834bafa955b869fe086124 |
| | | TZImagePickerController: d084a7b97c82d387e7669dd86dc9a9057500aacf |
| | | UserDefaultsStore: 905e30372ff432197d199ce1f6fe51be7bf69628 |
| | | VTMagic: b49e5f456dbcbfd9a3588ba92417233a105bc193 |
| | | WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f |
| | |
| | |  |
| | | |
| | | [](https://img.shields.io/badge/Swift-5.7_5.8_5.9-Orange?style=flat-square) |
| | | [](https://img.shields.io/badge/Swift-5.9_5.10_6.0-Orange?style=flat-square) |
| | | [](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_vision_OS_Linux_Windows_Android-Green?style=flat-square) |
| | | [](https://img.shields.io/cocoapods/v/Alamofire.svg) |
| | | [](https://github.com/Carthage/Carthage) |
| | |
| | | |
| | | | Platform | Minimum Swift Version | Installation | Status | |
| | | | ---------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------ | |
| | | | iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ | 5.7.1 / Xcode 14.1 | [CocoaPods](#cocoapods), [Carthage](#carthage), [Swift Package Manager](#swift-package-manager), [Manual](#manually) | Fully Tested | |
| | | | iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ | 5.9 / Xcode 15.0 | [CocoaPods](#cocoapods), [Carthage](#carthage), [Swift Package Manager](#swift-package-manager), [Manual](#manually) | Fully Tested | |
| | | | Linux | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | |
| | | | Windows | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | |
| | | | Android | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported | |
| | |
| | | |
| | | ```swift |
| | | dependencies: [ |
| | | .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.9.1")) |
| | | .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.10.0")) |
| | | ] |
| | | ``` |
| | | |
| | |
| | | #endif |
| | | |
| | | // Enforce minimum Swift version for all platforms and build systems. |
| | | #if swift(<5.7.1) |
| | | #error("Alamofire doesn't support Swift versions below 5.7.1.") |
| | | #if swift(<5.9.0) |
| | | #error("Alamofire doesn't support Swift versions below 5.9.") |
| | | #endif |
| | | |
| | | /// Reference to `Session.default` for quick bootstrapping and examples. |
| | |
| | | /// Namespace for informational Alamofire values. |
| | | public enum AFInfo { |
| | | /// Current Alamofire version. |
| | | public static let version = "5.9.1" |
| | | public static let version = "5.10.2" |
| | | } |
| | |
| | | import Foundation |
| | | |
| | | #if canImport(Security) |
| | | import Security |
| | | @preconcurrency import Security |
| | | #endif |
| | | |
| | | /// `AFError` is the error type returned by Alamofire. It encompasses a few different types of errors, each with |
| | | /// their own associated reasons. |
| | | public enum AFError: Error { |
| | | public enum AFError: Error, Sendable { |
| | | /// The underlying reason the `.multipartEncodingFailed` error occurred. |
| | | public enum MultipartEncodingFailureReason { |
| | | public enum MultipartEncodingFailureReason: Sendable { |
| | | /// The `fileURL` provided for reading an encodable body part isn't a file `URL`. |
| | | case bodyPartURLInvalid(url: URL) |
| | | /// The filename of the `fileURL` provided has either an empty `lastPathComponent` or `pathExtension`. |
| | |
| | | /// The file at the `fileURL` provided was not reachable. |
| | | case bodyPartFileNotReachable(at: URL) |
| | | /// Attempting to check the reachability of the `fileURL` provided threw an error. |
| | | case bodyPartFileNotReachableWithError(atURL: URL, error: Error) |
| | | case bodyPartFileNotReachableWithError(atURL: URL, error: any Error) |
| | | /// The file at the `fileURL` provided is actually a directory. |
| | | case bodyPartFileIsDirectory(at: URL) |
| | | /// The size of the file at the `fileURL` provided was not returned by the system. |
| | | case bodyPartFileSizeNotAvailable(at: URL) |
| | | /// The attempt to find the size of the file at the `fileURL` provided threw an error. |
| | | case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error) |
| | | case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: any Error) |
| | | /// An `InputStream` could not be created for the provided `fileURL`. |
| | | case bodyPartInputStreamCreationFailed(for: URL) |
| | | /// An `OutputStream` could not be created when attempting to write the encoded data to disk. |
| | |
| | | /// The `fileURL` provided for writing the encoded body data to disk is not a file `URL`. |
| | | case outputStreamURLInvalid(url: URL) |
| | | /// The attempt to write the encoded body data to disk failed with an underlying error. |
| | | case outputStreamWriteFailed(error: Error) |
| | | case outputStreamWriteFailed(error: any Error) |
| | | /// The attempt to read an encoded body part `InputStream` failed with underlying system error. |
| | | case inputStreamReadFailed(error: Error) |
| | | case inputStreamReadFailed(error: any Error) |
| | | } |
| | | |
| | | /// Represents unexpected input stream length that occur when encoding the `MultipartFormData`. Instances will be |
| | |
| | | } |
| | | |
| | | /// The underlying reason the `.parameterEncodingFailed` error occurred. |
| | | public enum ParameterEncodingFailureReason { |
| | | public enum ParameterEncodingFailureReason: Sendable { |
| | | /// The `URLRequest` did not have a `URL` to encode. |
| | | case missingURL |
| | | /// JSON serialization failed with an underlying system error during the encoding process. |
| | | case jsonEncodingFailed(error: Error) |
| | | case jsonEncodingFailed(error: any Error) |
| | | /// Custom parameter encoding failed due to the associated `Error`. |
| | | case customEncodingFailed(error: Error) |
| | | case customEncodingFailed(error: any Error) |
| | | } |
| | | |
| | | /// The underlying reason the `.parameterEncoderFailed` error occurred. |
| | | public enum ParameterEncoderFailureReason { |
| | | public enum ParameterEncoderFailureReason: Sendable { |
| | | /// Possible missing components. |
| | | public enum RequiredComponent { |
| | | public enum RequiredComponent: Sendable { |
| | | /// The `URL` was missing or unable to be extracted from the passed `URLRequest` or during encoding. |
| | | case url |
| | | /// The `HTTPMethod` could not be extracted from the passed `URLRequest`. |
| | |
| | | /// A `RequiredComponent` was missing during encoding. |
| | | case missingRequiredComponent(RequiredComponent) |
| | | /// The underlying encoder failed with the associated error. |
| | | case encoderFailed(error: Error) |
| | | case encoderFailed(error: any Error) |
| | | } |
| | | |
| | | /// The underlying reason the `.responseValidationFailed` error occurred. |
| | | public enum ResponseValidationFailureReason { |
| | | public enum ResponseValidationFailureReason: Sendable { |
| | | /// The data file containing the server response did not exist. |
| | | case dataFileNil |
| | | /// The data file containing the server response at the associated `URL` could not be read. |
| | |
| | | /// The response status code was not acceptable. |
| | | case unacceptableStatusCode(code: Int) |
| | | /// Custom response validation failed due to the associated `Error`. |
| | | case customValidationFailed(error: Error) |
| | | case customValidationFailed(error: any Error) |
| | | } |
| | | |
| | | /// The underlying reason the response serialization error occurred. |
| | | public enum ResponseSerializationFailureReason { |
| | | public enum ResponseSerializationFailureReason: Sendable { |
| | | /// The server response contained no data or the data was zero length. |
| | | case inputDataNilOrZeroLength |
| | | /// The file containing the server response did not exist. |
| | |
| | | /// String serialization failed using the provided `String.Encoding`. |
| | | case stringSerializationFailed(encoding: String.Encoding) |
| | | /// JSON serialization failed with an underlying system error. |
| | | case jsonSerializationFailed(error: Error) |
| | | case jsonSerializationFailed(error: any Error) |
| | | /// A `DataDecoder` failed to decode the response due to the associated `Error`. |
| | | case decodingFailed(error: Error) |
| | | case decodingFailed(error: any Error) |
| | | /// A custom response serializer failed due to the associated `Error`. |
| | | case customSerializationFailed(error: Error) |
| | | case customSerializationFailed(error: any Error) |
| | | /// Generic serialization failed for an empty response that wasn't type `Empty` but instead the associated type. |
| | | case invalidEmptyResponse(type: String) |
| | | } |
| | | |
| | | #if canImport(Security) |
| | | /// Underlying reason a server trust evaluation error occurred. |
| | | public enum ServerTrustFailureReason { |
| | | public enum ServerTrustFailureReason: Sendable { |
| | | /// The output of a server trust evaluation. |
| | | public struct Output { |
| | | public struct Output: Sendable { |
| | | /// The host for which the evaluation was performed. |
| | | public let host: String |
| | | /// The `SecTrust` value which was evaluated. |
| | |
| | | /// During evaluation, creation of the revocation policy failed. |
| | | case revocationPolicyCreationFailed |
| | | /// `SecTrust` evaluation failed with the associated `Error`, if one was produced. |
| | | case trustEvaluationFailed(error: Error?) |
| | | case trustEvaluationFailed(error: (any Error)?) |
| | | /// Default evaluation failed with the associated `Output`. |
| | | case defaultEvaluationFailed(output: Output) |
| | | /// Host validation failed with the associated `Output`. |
| | |
| | | /// Public key pinning failed. |
| | | case publicKeyPinningFailed(host: String, trust: SecTrust, pinnedKeys: [SecKey], serverKeys: [SecKey]) |
| | | /// Custom server trust evaluation failed due to the associated `Error`. |
| | | case customEvaluationFailed(error: Error) |
| | | case customEvaluationFailed(error: any Error) |
| | | } |
| | | #endif |
| | | |
| | | /// The underlying reason the `.urlRequestValidationFailed` error occurred. |
| | | public enum URLRequestValidationFailureReason { |
| | | public enum URLRequestValidationFailureReason: Sendable { |
| | | /// URLRequest with GET method had body data. |
| | | case bodyDataInGETRequest(Data) |
| | | } |
| | | |
| | | /// `UploadableConvertible` threw an error in `createUploadable()`. |
| | | case createUploadableFailed(error: Error) |
| | | case createUploadableFailed(error: any Error) |
| | | /// `URLRequestConvertible` threw an error in `asURLRequest()`. |
| | | case createURLRequestFailed(error: Error) |
| | | case createURLRequestFailed(error: any Error) |
| | | /// `SessionDelegate` threw an error while attempting to move downloaded file to destination URL. |
| | | case downloadedFileMoveFailed(error: Error, source: URL, destination: URL) |
| | | case downloadedFileMoveFailed(error: any Error, source: URL, destination: URL) |
| | | /// `Request` was explicitly cancelled. |
| | | case explicitlyCancelled |
| | | /// `URLConvertible` type failed to create a valid `URL`. |
| | | case invalidURL(url: URLConvertible) |
| | | case invalidURL(url: any URLConvertible) |
| | | /// Multipart form encoding failed. |
| | | case multipartEncodingFailed(reason: MultipartEncodingFailureReason) |
| | | /// `ParameterEncoding` threw an error during the encoding process. |
| | |
| | | /// `ParameterEncoder` threw an error while running the encoder. |
| | | case parameterEncoderFailed(reason: ParameterEncoderFailureReason) |
| | | /// `RequestAdapter` threw an error during adaptation. |
| | | case requestAdaptationFailed(error: Error) |
| | | case requestAdaptationFailed(error: any Error) |
| | | /// `RequestRetrier` threw an error during the request retry process. |
| | | case requestRetryFailed(retryError: Error, originalError: Error) |
| | | case requestRetryFailed(retryError: any Error, originalError: any Error) |
| | | /// Response validation failed. |
| | | case responseValidationFailed(reason: ResponseValidationFailureReason) |
| | | /// Response serialization failed. |
| | |
| | | /// `Session` which issued the `Request` was deinitialized, most likely because its reference went out of scope. |
| | | case sessionDeinitialized |
| | | /// `Session` was explicitly invalidated, possibly with the `Error` produced by the underlying `URLSession`. |
| | | case sessionInvalidated(error: Error?) |
| | | case sessionInvalidated(error: (any Error)?) |
| | | /// `URLSessionTask` completed with error. |
| | | case sessionTaskFailed(error: Error) |
| | | case sessionTaskFailed(error: any Error) |
| | | /// `URLRequest` failed validation. |
| | | case urlRequestValidationFailed(reason: URLRequestValidationFailureReason) |
| | | } |
| | |
| | | |
| | | extension AFError { |
| | | /// The `URLConvertible` associated with the error. |
| | | public var urlConvertible: URLConvertible? { |
| | | public var urlConvertible: (any URLConvertible)? { |
| | | guard case let .invalidURL(url) = self else { return nil } |
| | | return url |
| | | } |
| | |
| | | /// The underlying `Error` responsible for generating the failure associated with `.sessionInvalidated`, |
| | | /// `.parameterEncodingFailed`, `.parameterEncoderFailed`, `.multipartEncodingFailed`, `.requestAdaptationFailed`, |
| | | /// `.responseSerializationFailed`, `.requestRetryFailed` errors. |
| | | public var underlyingError: Error? { |
| | | public var underlyingError: (any Error)? { |
| | | switch self { |
| | | case let .multipartEncodingFailed(reason): |
| | | return reason.underlyingError |
| | |
| | | } |
| | | |
| | | extension AFError.ParameterEncodingFailureReason { |
| | | var underlyingError: Error? { |
| | | var underlyingError: (any Error)? { |
| | | switch self { |
| | | case let .jsonEncodingFailed(error), |
| | | let .customEncodingFailed(error): |
| | | return error |
| | | error |
| | | case .missingURL: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension AFError.ParameterEncoderFailureReason { |
| | | var underlyingError: Error? { |
| | | var underlyingError: (any Error)? { |
| | | switch self { |
| | | case let .encoderFailed(error): |
| | | return error |
| | | error |
| | | case .missingRequiredComponent: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | } |
| | |
| | | let .outputStreamURLInvalid(url), |
| | | let .bodyPartFileNotReachableWithError(url, _), |
| | | let .bodyPartFileSizeQueryFailedWithError(url, _): |
| | | return url |
| | | url |
| | | case .outputStreamWriteFailed, |
| | | .inputStreamReadFailed: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | |
| | | var underlyingError: Error? { |
| | | var underlyingError: (any Error)? { |
| | | switch self { |
| | | case let .bodyPartFileNotReachableWithError(_, error), |
| | | let .bodyPartFileSizeQueryFailedWithError(_, error), |
| | | let .outputStreamWriteFailed(error), |
| | | let .inputStreamReadFailed(error): |
| | | return error |
| | | error |
| | | case .bodyPartURLInvalid, |
| | | .bodyPartFilenameInvalid, |
| | | .bodyPartFileNotReachable, |
| | |
| | | .outputStreamCreationFailed, |
| | | .outputStreamFileAlreadyExists, |
| | | .outputStreamURLInvalid: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | } |
| | |
| | | switch self { |
| | | case let .missingContentType(types), |
| | | let .unacceptableContentType(types, _): |
| | | return types |
| | | types |
| | | case .dataFileNil, |
| | | .dataFileReadFailed, |
| | | .unacceptableStatusCode, |
| | | .customValidationFailed: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | |
| | | var responseContentType: String? { |
| | | switch self { |
| | | case let .unacceptableContentType(_, responseType): |
| | | return responseType |
| | | responseType |
| | | case .dataFileNil, |
| | | .dataFileReadFailed, |
| | | .missingContentType, |
| | | .unacceptableStatusCode, |
| | | .customValidationFailed: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | |
| | | var responseCode: Int? { |
| | | switch self { |
| | | case let .unacceptableStatusCode(code): |
| | | return code |
| | | code |
| | | case .dataFileNil, |
| | | .dataFileReadFailed, |
| | | .missingContentType, |
| | | .unacceptableContentType, |
| | | .customValidationFailed: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | |
| | | var underlyingError: Error? { |
| | | var underlyingError: (any Error)? { |
| | | switch self { |
| | | case let .customValidationFailed(error): |
| | | return error |
| | | error |
| | | case .dataFileNil, |
| | | .dataFileReadFailed, |
| | | .missingContentType, |
| | | .unacceptableContentType, |
| | | .unacceptableStatusCode: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | } |
| | |
| | | var failedStringEncoding: String.Encoding? { |
| | | switch self { |
| | | case let .stringSerializationFailed(encoding): |
| | | return encoding |
| | | encoding |
| | | case .inputDataNilOrZeroLength, |
| | | .inputFileNil, |
| | | .inputFileReadFailed(_), |
| | |
| | | .decodingFailed(_), |
| | | .customSerializationFailed(_), |
| | | .invalidEmptyResponse: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | |
| | | var underlyingError: Error? { |
| | | var underlyingError: (any Error)? { |
| | | switch self { |
| | | case let .jsonSerializationFailed(error), |
| | | let .decodingFailed(error), |
| | | let .customSerializationFailed(error): |
| | | return error |
| | | error |
| | | case .inputDataNilOrZeroLength, |
| | | .inputFileNil, |
| | | .inputFileReadFailed, |
| | | .stringSerializationFailed, |
| | | .invalidEmptyResponse: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | } |
| | |
| | | case let .defaultEvaluationFailed(output), |
| | | let .hostValidationFailed(output), |
| | | let .revocationCheckFailed(output, _): |
| | | return output |
| | | output |
| | | case .noRequiredEvaluator, |
| | | .noCertificatesFound, |
| | | .noPublicKeysFound, |
| | |
| | | .certificatePinningFailed, |
| | | .publicKeyPinningFailed, |
| | | .customEvaluationFailed: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | |
| | | var underlyingError: Error? { |
| | | var underlyingError: (any Error)? { |
| | | switch self { |
| | | case let .customEvaluationFailed(error): |
| | | return error |
| | | error |
| | | case let .trustEvaluationFailed(error): |
| | | return error |
| | | error |
| | | case .noRequiredEvaluator, |
| | | .noCertificatesFound, |
| | | .noPublicKeysFound, |
| | |
| | | .revocationCheckFailed, |
| | | .certificatePinningFailed, |
| | | .publicKeyPinningFailed: |
| | | return nil |
| | | nil |
| | | } |
| | | } |
| | | } |
| | |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case .missingURL: |
| | | return "URL request to encode was missing a URL" |
| | | "URL request to encode was missing a URL" |
| | | case let .jsonEncodingFailed(error): |
| | | return "JSON could not be encoded because of error:\n\(error.localizedDescription)" |
| | | "JSON could not be encoded because of error:\n\(error.localizedDescription)" |
| | | case let .customEncodingFailed(error): |
| | | return "Custom parameter encoder failed with error: \(error.localizedDescription)" |
| | | "Custom parameter encoder failed with error: \(error.localizedDescription)" |
| | | } |
| | | } |
| | | } |
| | |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case let .missingRequiredComponent(component): |
| | | return "Encoding failed due to a missing request component: \(component)" |
| | | "Encoding failed due to a missing request component: \(component)" |
| | | case let .encoderFailed(error): |
| | | return "The underlying encoder failed with the error: \(error)" |
| | | "The underlying encoder failed with the error: \(error)" |
| | | } |
| | | } |
| | | } |
| | |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case let .bodyPartURLInvalid(url): |
| | | return "The URL provided is not a file URL: \(url)" |
| | | "The URL provided is not a file URL: \(url)" |
| | | case let .bodyPartFilenameInvalid(url): |
| | | return "The URL provided does not have a valid filename: \(url)" |
| | | "The URL provided does not have a valid filename: \(url)" |
| | | case let .bodyPartFileNotReachable(url): |
| | | return "The URL provided is not reachable: \(url)" |
| | | "The URL provided is not reachable: \(url)" |
| | | case let .bodyPartFileNotReachableWithError(url, error): |
| | | return """ |
| | | """ |
| | | The system returned an error while checking the provided URL for reachability. |
| | | URL: \(url) |
| | | Error: \(error) |
| | | """ |
| | | case let .bodyPartFileIsDirectory(url): |
| | | return "The URL provided is a directory: \(url)" |
| | | "The URL provided is a directory: \(url)" |
| | | case let .bodyPartFileSizeNotAvailable(url): |
| | | return "Could not fetch the file size from the provided URL: \(url)" |
| | | "Could not fetch the file size from the provided URL: \(url)" |
| | | case let .bodyPartFileSizeQueryFailedWithError(url, error): |
| | | return """ |
| | | """ |
| | | The system returned an error while attempting to fetch the file size from the provided URL. |
| | | URL: \(url) |
| | | Error: \(error) |
| | | """ |
| | | case let .bodyPartInputStreamCreationFailed(url): |
| | | return "Failed to create an InputStream for the provided URL: \(url)" |
| | | "Failed to create an InputStream for the provided URL: \(url)" |
| | | case let .outputStreamCreationFailed(url): |
| | | return "Failed to create an OutputStream for URL: \(url)" |
| | | "Failed to create an OutputStream for URL: \(url)" |
| | | case let .outputStreamFileAlreadyExists(url): |
| | | return "A file already exists at the provided URL: \(url)" |
| | | "A file already exists at the provided URL: \(url)" |
| | | case let .outputStreamURLInvalid(url): |
| | | return "The provided OutputStream URL is invalid: \(url)" |
| | | "The provided OutputStream URL is invalid: \(url)" |
| | | case let .outputStreamWriteFailed(error): |
| | | return "OutputStream write failed with error: \(error)" |
| | | "OutputStream write failed with error: \(error)" |
| | | case let .inputStreamReadFailed(error): |
| | | return "InputStream read failed with error: \(error)" |
| | | "InputStream read failed with error: \(error)" |
| | | } |
| | | } |
| | | } |
| | |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case .inputDataNilOrZeroLength: |
| | | return "Response could not be serialized, input data was nil or zero length." |
| | | "Response could not be serialized, input data was nil or zero length." |
| | | case .inputFileNil: |
| | | return "Response could not be serialized, input file was nil." |
| | | "Response could not be serialized, input file was nil." |
| | | case let .inputFileReadFailed(url): |
| | | return "Response could not be serialized, input file could not be read: \(url)." |
| | | "Response could not be serialized, input file could not be read: \(url)." |
| | | case let .stringSerializationFailed(encoding): |
| | | return "String could not be serialized with encoding: \(encoding)." |
| | | "String could not be serialized with encoding: \(encoding)." |
| | | case let .jsonSerializationFailed(error): |
| | | return "JSON could not be serialized because of error:\n\(error.localizedDescription)" |
| | | "JSON could not be serialized because of error:\n\(error.localizedDescription)" |
| | | case let .invalidEmptyResponse(type): |
| | | return """ |
| | | """ |
| | | Empty response could not be serialized to type: \(type). \ |
| | | Use Empty as the expected type for such responses. |
| | | """ |
| | | case let .decodingFailed(error): |
| | | return "Response could not be decoded because of error:\n\(error.localizedDescription)" |
| | | "Response could not be decoded because of error:\n\(error.localizedDescription)" |
| | | case let .customSerializationFailed(error): |
| | | return "Custom response serializer failed with error:\n\(error.localizedDescription)" |
| | | "Custom response serializer failed with error:\n\(error.localizedDescription)" |
| | | } |
| | | } |
| | | } |
| | |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case .dataFileNil: |
| | | return "Response could not be validated, data file was nil." |
| | | "Response could not be validated, data file was nil." |
| | | case let .dataFileReadFailed(url): |
| | | return "Response could not be validated, data file could not be read: \(url)." |
| | | "Response could not be validated, data file could not be read: \(url)." |
| | | case let .missingContentType(types): |
| | | return """ |
| | | """ |
| | | Response Content-Type was missing and acceptable content types \ |
| | | (\(types.joined(separator: ","))) do not match "*/*". |
| | | """ |
| | | case let .unacceptableContentType(acceptableTypes, responseType): |
| | | return """ |
| | | """ |
| | | Response Content-Type "\(responseType)" does not match any acceptable types: \ |
| | | \(acceptableTypes.joined(separator: ",")). |
| | | """ |
| | | case let .unacceptableStatusCode(code): |
| | | return "Response status code was unacceptable: \(code)." |
| | | "Response status code was unacceptable: \(code)." |
| | | case let .customValidationFailed(error): |
| | | return "Custom response validation failed with error: \(error.localizedDescription)" |
| | | "Custom response validation failed with error: \(error.localizedDescription)" |
| | | } |
| | | } |
| | | } |
| | |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case let .noRequiredEvaluator(host): |
| | | return "A ServerTrustEvaluating value is required for host \(host) but none was found." |
| | | "A ServerTrustEvaluating value is required for host \(host) but none was found." |
| | | case .noCertificatesFound: |
| | | return "No certificates were found or provided for evaluation." |
| | | "No certificates were found or provided for evaluation." |
| | | case .noPublicKeysFound: |
| | | return "No public keys were found or provided for evaluation." |
| | | "No public keys were found or provided for evaluation." |
| | | case .policyApplicationFailed: |
| | | return "Attempting to set a SecPolicy failed." |
| | | "Attempting to set a SecPolicy failed." |
| | | case .settingAnchorCertificatesFailed: |
| | | return "Attempting to set the provided certificates as anchor certificates failed." |
| | | "Attempting to set the provided certificates as anchor certificates failed." |
| | | case .revocationPolicyCreationFailed: |
| | | return "Attempting to create a revocation policy failed." |
| | | "Attempting to create a revocation policy failed." |
| | | case let .trustEvaluationFailed(error): |
| | | return "SecTrust evaluation failed with error: \(error?.localizedDescription ?? "None")" |
| | | "SecTrust evaluation failed with error: \(error?.localizedDescription ?? "None")" |
| | | case let .defaultEvaluationFailed(output): |
| | | return "Default evaluation failed for host \(output.host)." |
| | | "Default evaluation failed for host \(output.host)." |
| | | case let .hostValidationFailed(output): |
| | | return "Host validation failed for host \(output.host)." |
| | | "Host validation failed for host \(output.host)." |
| | | case let .revocationCheckFailed(output, _): |
| | | return "Revocation check failed for host \(output.host)." |
| | | "Revocation check failed for host \(output.host)." |
| | | case let .certificatePinningFailed(host, _, _, _): |
| | | return "Certificate pinning failed for host \(host)." |
| | | "Certificate pinning failed for host \(host)." |
| | | case let .publicKeyPinningFailed(host, _, _, _): |
| | | return "Public key pinning failed for host \(host)." |
| | | "Public key pinning failed for host \(host)." |
| | | case let .customEvaluationFailed(error): |
| | | return "Custom trust evaluation failed with error: \(error.localizedDescription)" |
| | | "Custom trust evaluation failed with error: \(error.localizedDescription)" |
| | | } |
| | | } |
| | | } |
| | |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case let .bodyDataInGETRequest(data): |
| | | return """ |
| | | """ |
| | | Invalid URLRequest: Requests with GET method cannot have body data: |
| | | \(String(decoding: data, as: UTF8.self)) |
| | | """ |
| | |
| | | import Foundation |
| | | |
| | | /// `Request` subclass which handles in-memory `Data` download using `URLSessionDataTask`. |
| | | public class DataRequest: Request { |
| | | public class DataRequest: Request, @unchecked Sendable { |
| | | /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. |
| | | public let convertible: URLRequestConvertible |
| | | public let convertible: any URLRequestConvertible |
| | | /// `Data` read from the server so far. |
| | | public var data: Data? { dataMutableState.data } |
| | | |
| | | private struct DataMutableState { |
| | | var data: Data? |
| | | var httpResponseHandler: (queue: DispatchQueue, |
| | | handler: (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void)? |
| | | handler: @Sendable (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping @Sendable (ResponseDisposition) -> Void) -> Void)? |
| | | } |
| | | |
| | | private let dataMutableState = Protected(DataMutableState()) |
| | |
| | | /// - interceptor: `RequestInterceptor` used throughout the request lifecycle. |
| | | /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. |
| | | init(id: UUID = UUID(), |
| | | convertible: URLRequestConvertible, |
| | | convertible: any URLRequestConvertible, |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | delegate: RequestDelegate) { |
| | | eventMonitor: (any EventMonitor)?, |
| | | interceptor: (any RequestInterceptor)?, |
| | | delegate: any RequestDelegate) { |
| | | self.convertible = convertible |
| | | |
| | | super.init(id: id, |
| | |
| | | updateDownloadProgress() |
| | | } |
| | | |
| | | func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { |
| | | func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping @Sendable (URLSession.ResponseDisposition) -> Void) { |
| | | dataMutableState.read { dataMutableState in |
| | | guard let httpResponseHandler = dataMutableState.httpResponseHandler else { |
| | | underlyingQueue.async { completionHandler(.allow) } |
| | |
| | | /// - Parameter validation: `Validation` closure used to validate the response. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func validate(_ validation: @escaping Validation) -> Self { |
| | | let validator: () -> Void = { [unowned self] in |
| | | let validator: @Sendable () -> Void = { [unowned self] in |
| | | guard error == nil, let response else { return } |
| | | |
| | | let result = validation(request, response, data) |
| | |
| | | /// |
| | | /// - Returns: The instance. |
| | | @_disfavoredOverload |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func onHTTPResponse( |
| | | on queue: DispatchQueue = .main, |
| | | perform handler: @escaping (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void |
| | | perform handler: @escaping @Sendable (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping @Sendable (ResponseDisposition) -> Void) -> Void |
| | | ) -> Self { |
| | | dataMutableState.write { mutableState in |
| | | mutableState.httpResponseHandler = (queue, handler) |
| | |
| | | /// - handler: Closure called when the instance produces an `HTTPURLResponse`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func onHTTPResponse(on queue: DispatchQueue = .main, |
| | | perform handler: @escaping (HTTPURLResponse) -> Void) -> Self { |
| | | perform handler: @escaping @Sendable (HTTPURLResponse) -> Void) -> Self { |
| | | onHTTPResponse(on: queue) { response, completionHandler in |
| | | handler(response) |
| | | completionHandler(.allow) |
| | |
| | | /// - completionHandler: The code to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse<Data?>) -> Void) -> Self { |
| | | public func response(queue: DispatchQueue = .main, completionHandler: @escaping @Sendable (AFDataResponse<Data?>) -> Void) -> Self { |
| | | appendResponseSerializer { |
| | | // Start work that should be on the serialization queue. |
| | | let result = AFResult<Data?>(value: self.data, error: self.error) |
| | |
| | | |
| | | private func _response<Serializer: DataResponseSerializerProtocol>(queue: DispatchQueue = .main, |
| | | responseSerializer: Serializer, |
| | | completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void) |
| | | completionHandler: @escaping @Sendable (AFDataResponse<Serializer.SerializedObject>) -> Void) |
| | | -> Self { |
| | | appendResponseSerializer { |
| | | // Start work that should be on the serialization queue. |
| | |
| | | } |
| | | |
| | | delegate.retryResult(for: self, dueTo: serializerError) { retryResult in |
| | | var didComplete: (() -> Void)? |
| | | var didComplete: (@Sendable () -> Void)? |
| | | |
| | | defer { |
| | | if let didComplete { |
| | |
| | | /// - completionHandler: The code to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func response<Serializer: DataResponseSerializerProtocol>(queue: DispatchQueue = .main, |
| | | responseSerializer: Serializer, |
| | | completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void) |
| | | completionHandler: @escaping @Sendable (AFDataResponse<Serializer.SerializedObject>) -> Void) |
| | | -> Self { |
| | | _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) |
| | | } |
| | |
| | | /// - completionHandler: The code to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func response<Serializer: ResponseSerializer>(queue: DispatchQueue = .main, |
| | | responseSerializer: Serializer, |
| | | completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void) |
| | | completionHandler: @escaping @Sendable (AFDataResponse<Serializer.SerializedObject>) -> Void) |
| | | -> Self { |
| | | _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) |
| | | } |
| | |
| | | /// - completionHandler: A closure to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseData(queue: DispatchQueue = .main, |
| | | dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods, |
| | | completionHandler: @escaping (AFDataResponse<Data>) -> Void) -> Self { |
| | | completionHandler: @escaping @Sendable (AFDataResponse<Data>) -> Void) -> Self { |
| | | response(queue: queue, |
| | | responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | |
| | | /// - completionHandler: A closure to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseString(queue: DispatchQueue = .main, |
| | | dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods, |
| | | completionHandler: @escaping (AFDataResponse<String>) -> Void) -> Self { |
| | | completionHandler: @escaping @Sendable (AFDataResponse<String>) -> Void) -> Self { |
| | | response(queue: queue, |
| | | responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | encoding: encoding, |
| | |
| | | /// |
| | | /// - Returns: The request. |
| | | @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseJSON(queue: DispatchQueue = .main, |
| | | dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = JSONResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods, |
| | | options: JSONSerialization.ReadingOptions = .allowFragments, |
| | | completionHandler: @escaping (AFDataResponse<Any>) -> Void) -> Self { |
| | | completionHandler: @escaping @Sendable (AFDataResponse<Any>) -> Void) -> Self { |
| | | response(queue: queue, |
| | | responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | |
| | | /// - completionHandler: A closure to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseDecodable<T: Decodable>(of type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods, |
| | | completionHandler: @escaping (AFDataResponse<T>) -> Void) -> Self { |
| | | public func responseDecodable<Value>(of type: Value.Type = Value.self, |
| | | queue: DispatchQueue = .main, |
| | | dataPreprocessor: any DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<Value>.defaultEmptyRequestMethods, |
| | | completionHandler: @escaping @Sendable (AFDataResponse<Value>) -> Void) -> Self where Value: Decodable, Value: Sendable { |
| | | response(queue: queue, |
| | | responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | decoder: decoder, |
| | |
| | | import Foundation |
| | | |
| | | /// `Request` subclass which streams HTTP response `Data` through a `Handler` closure. |
| | | public final class DataStreamRequest: Request { |
| | | public final class DataStreamRequest: Request, @unchecked Sendable { |
| | | /// Closure type handling `DataStreamRequest.Stream` values. |
| | | public typealias Handler<Success, Failure: Error> = (Stream<Success, Failure>) throws -> Void |
| | | public typealias Handler<Success, Failure: Error> = @Sendable (Stream<Success, Failure>) throws -> Void |
| | | |
| | | /// Type encapsulating an `Event` as it flows through the stream, as well as a `CancellationToken` which can be used |
| | | /// to stop the stream at any time. |
| | | public struct Stream<Success, Failure: Error> { |
| | | public struct Stream<Success, Failure: Error>: Sendable where Success: Sendable, Failure: Sendable { |
| | | /// Latest `Event` from the stream. |
| | | public let event: Event<Success, Failure> |
| | | /// Token used to cancel the stream. |
| | |
| | | |
| | | /// Type representing an event flowing through the stream. Contains either the `Result` of processing streamed |
| | | /// `Data` or the completion of the stream. |
| | | public enum Event<Success, Failure: Error> { |
| | | public enum Event<Success, Failure: Error>: Sendable where Success: Sendable, Failure: Sendable { |
| | | /// Output produced every time the instance receives additional `Data`. The associated value contains the |
| | | /// `Result` of processing the incoming `Data`. |
| | | case stream(Result<Success, Failure>) |
| | |
| | | } |
| | | |
| | | /// Value containing the state of a `DataStreamRequest` when the stream was completed. |
| | | public struct Completion { |
| | | public struct Completion: Sendable { |
| | | /// Last `URLRequest` issued by the instance. |
| | | public let request: URLRequest? |
| | | /// Last `HTTPURLResponse` received by the instance. |
| | |
| | | } |
| | | |
| | | /// Type used to cancel an ongoing stream. |
| | | public struct CancellationToken { |
| | | public struct CancellationToken: Sendable { |
| | | weak var request: DataStreamRequest? |
| | | |
| | | init(_ request: DataStreamRequest) { |
| | |
| | | } |
| | | |
| | | /// `URLRequestConvertible` value used to create `URLRequest`s for this instance. |
| | | public let convertible: URLRequestConvertible |
| | | public let convertible: any URLRequestConvertible |
| | | /// Whether or not the instance will be cancelled if stream parsing encounters an error. |
| | | public let automaticallyCancelOnStreamError: Bool |
| | | |
| | |
| | | /// `OutputStream` bound to the `InputStream` produced by `asInputStream`, if it has been called. |
| | | var outputStream: OutputStream? |
| | | /// Stream closures called as `Data` is received. |
| | | var streams: [(_ data: Data) -> Void] = [] |
| | | var streams: [@Sendable (_ data: Data) -> Void] = [] |
| | | /// Number of currently executing streams. Used to ensure completions are only fired after all streams are |
| | | /// enqueued. |
| | | var numberOfExecutingStreams = 0 |
| | | /// Completion calls enqueued while streams are still executing. |
| | | var enqueuedCompletionEvents: [() -> Void] = [] |
| | | var enqueuedCompletionEvents: [@Sendable () -> Void] = [] |
| | | /// Handler for any `HTTPURLResponse`s received. |
| | | var httpResponseHandler: (queue: DispatchQueue, |
| | | handler: (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void)? |
| | | handler: @Sendable (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping @Sendable (ResponseDisposition) -> Void) -> Void)? |
| | | } |
| | | |
| | | let streamMutableState = Protected(StreamMutableState()) |
| | |
| | | /// - delegate: `RequestDelegate` that provides an interface to actions not performed by |
| | | /// the `Request`. |
| | | init(id: UUID = UUID(), |
| | | convertible: URLRequestConvertible, |
| | | convertible: any URLRequestConvertible, |
| | | automaticallyCancelOnStreamError: Bool, |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | delegate: RequestDelegate) { |
| | | eventMonitor: (any EventMonitor)?, |
| | | interceptor: (any RequestInterceptor)?, |
| | | delegate: any RequestDelegate) { |
| | | self.convertible = convertible |
| | | self.automaticallyCancelOnStreamError = automaticallyCancelOnStreamError |
| | | |
| | |
| | | } |
| | | #endif |
| | | state.numberOfExecutingStreams += state.streams.count |
| | | let localState = state |
| | | underlyingQueue.async { localState.streams.forEach { $0(data) } } |
| | | underlyingQueue.async { [streams = state.streams] in streams.forEach { $0(data) } } |
| | | } |
| | | } |
| | | |
| | | func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { |
| | | func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping @Sendable (URLSession.ResponseDisposition) -> Void) { |
| | | streamMutableState.read { dataMutableState in |
| | | guard let httpResponseHandler = dataMutableState.httpResponseHandler else { |
| | | underlyingQueue.async { completionHandler(.allow) } |
| | |
| | | /// - Returns: The `DataStreamRequest`. |
| | | @discardableResult |
| | | public func validate(_ validation: @escaping Validation) -> Self { |
| | | let validator: () -> Void = { [unowned self] in |
| | | let validator: @Sendable () -> Void = { [unowned self] in |
| | | guard error == nil, let response else { return } |
| | | |
| | | let result = validation(request, response) |
| | |
| | | /// |
| | | /// - Returns: The instance. |
| | | @_disfavoredOverload |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func onHTTPResponse( |
| | | on queue: DispatchQueue = .main, |
| | | perform handler: @escaping (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping (ResponseDisposition) -> Void) -> Void |
| | | perform handler: @escaping @Sendable (_ response: HTTPURLResponse, |
| | | _ completionHandler: @escaping @Sendable (ResponseDisposition) -> Void) -> Void |
| | | ) -> Self { |
| | | streamMutableState.write { mutableState in |
| | | mutableState.httpResponseHandler = (queue, handler) |
| | |
| | | /// - handler: Closure called when the instance produces an `HTTPURLResponse`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func onHTTPResponse(on queue: DispatchQueue = .main, |
| | | perform handler: @escaping (HTTPURLResponse) -> Void) -> Self { |
| | | perform handler: @escaping @Sendable (HTTPURLResponse) -> Void) -> Self { |
| | | onHTTPResponse(on: queue) { response, completionHandler in |
| | | handler(response) |
| | | completionHandler(.allow) |
| | |
| | | } |
| | | |
| | | func appendStreamCompletion<Success, Failure>(on queue: DispatchQueue, |
| | | stream: @escaping Handler<Success, Failure>) { |
| | | stream: @escaping Handler<Success, Failure>) where Success: Sendable, Failure: Sendable { |
| | | appendResponseSerializer { |
| | | self.underlyingQueue.async { |
| | | self.responseSerializerDidComplete { |
| | |
| | | } |
| | | |
| | | func enqueueCompletion<Success, Failure>(on queue: DispatchQueue, |
| | | stream: @escaping Handler<Success, Failure>) { |
| | | stream: @escaping Handler<Success, Failure>) where Success: Sendable, Failure: Sendable { |
| | | queue.async { |
| | | do { |
| | | let completion = Completion(request: self.request, |
| | |
| | | /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. |
| | | /// |
| | | /// - Returns: The `DataStreamRequest`. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseStream(on queue: DispatchQueue = .main, stream: @escaping Handler<Data, Never>) -> Self { |
| | | let parser = { [unowned self] (data: Data) in |
| | | let parser = { @Sendable [unowned self] (data: Data) in |
| | | queue.async { |
| | | self.capturingError { |
| | | try stream(.init(event: .stream(.success(data)), token: .init(self))) |
| | |
| | | /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. |
| | | /// |
| | | /// - Returns: The `DataStreamRequest`. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseStream<Serializer: DataStreamSerializer>(using serializer: Serializer, |
| | | on queue: DispatchQueue = .main, |
| | | stream: @escaping Handler<Serializer.SerializedObject, AFError>) -> Self { |
| | | let parser = { [unowned self] (data: Data) in |
| | | let parser = { @Sendable [unowned self] (data: Data) in |
| | | serializationQueue.async { |
| | | // Start work on serialization queue. |
| | | let result = Result { try serializer.serialize(data) } |
| | |
| | | /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. |
| | | /// |
| | | /// - Returns: The `DataStreamRequest`. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseStreamString(on queue: DispatchQueue = .main, |
| | | stream: @escaping Handler<String, Never>) -> Self { |
| | | let parser = { [unowned self] (data: Data) in |
| | | let parser = { @Sendable [unowned self] (data: Data) in |
| | | serializationQueue.async { |
| | | // Start work on serialization queue. |
| | | let string = String(decoding: data, as: UTF8.self) |
| | |
| | | /// - stream: `StreamHandler` closure called as `Data` is received. May be called multiple times. |
| | | /// |
| | | /// - Returns: The `DataStreamRequest`. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseStreamDecodable<T: Decodable>(of type: T.Type = T.self, |
| | | on queue: DispatchQueue = .main, |
| | | using decoder: DataDecoder = JSONDecoder(), |
| | | preprocessor: DataPreprocessor = PassthroughPreprocessor(), |
| | | stream: @escaping Handler<T, AFError>) -> Self { |
| | | using decoder: any DataDecoder = JSONDecoder(), |
| | | preprocessor: any DataPreprocessor = PassthroughPreprocessor(), |
| | | stream: @escaping Handler<T, AFError>) -> Self where T: Sendable { |
| | | responseStream(using: DecodableStreamSerializer<T>(decoder: decoder, dataPreprocessor: preprocessor), |
| | | on: queue, |
| | | stream: stream) |
| | | } |
| | | } |
| | |
| | | // MARK: - Serialization |
| | | |
| | | /// A type which can serialize incoming `Data`. |
| | | public protocol DataStreamSerializer { |
| | | public protocol DataStreamSerializer: Sendable { |
| | | /// Type produced from the serialized `Data`. |
| | | associatedtype SerializedObject |
| | | associatedtype SerializedObject: Sendable |
| | | |
| | | /// Serializes incoming `Data` into a `SerializedObject` value. |
| | | /// |
| | |
| | | } |
| | | |
| | | /// `DataStreamSerializer` which uses the provided `DataPreprocessor` and `DataDecoder` to serialize the incoming `Data`. |
| | | public struct DecodableStreamSerializer<T: Decodable>: DataStreamSerializer { |
| | | public struct DecodableStreamSerializer<T: Decodable>: DataStreamSerializer where T: Sendable { |
| | | /// `DataDecoder` used to decode incoming `Data`. |
| | | public let decoder: DataDecoder |
| | | public let decoder: any DataDecoder |
| | | /// `DataPreprocessor` incoming `Data` is passed through before being passed to the `DataDecoder`. |
| | | public let dataPreprocessor: DataPreprocessor |
| | | public let dataPreprocessor: any DataPreprocessor |
| | | |
| | | /// Creates an instance with the provided `DataDecoder` and `DataPreprocessor`. |
| | | /// - Parameters: |
| | | /// - decoder: ` DataDecoder` used to decode incoming `Data`. `JSONDecoder()` by default. |
| | | /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the |
| | | /// `decoder`. `PassthroughPreprocessor()` by default. |
| | | public init(decoder: DataDecoder = JSONDecoder(), dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) { |
| | | public init(decoder: any DataDecoder = JSONDecoder(), dataPreprocessor: any DataPreprocessor = PassthroughPreprocessor()) { |
| | | self.decoder = decoder |
| | | self.dataPreprocessor = dataPreprocessor |
| | | } |
| | |
| | | /// - dataPreprocessor: `DataPreprocessor` used to process incoming `Data` before it's passed through the |
| | | /// `decoder`. `PassthroughPreprocessor()` by default. |
| | | public static func decodable<T: Decodable>(of type: T.Type, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | dataPreprocessor: DataPreprocessor = PassthroughPreprocessor()) -> Self where Self == DecodableStreamSerializer<T> { |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | dataPreprocessor: any DataPreprocessor = PassthroughPreprocessor()) -> Self where Self == DecodableStreamSerializer<T> { |
| | | DecodableStreamSerializer<T>(decoder: decoder, dataPreprocessor: dataPreprocessor) |
| | | } |
| | | } |
| | |
| | | import Foundation |
| | | |
| | | /// `Request` subclass which downloads `Data` to a file on disk using `URLSessionDownloadTask`. |
| | | public final class DownloadRequest: Request { |
| | | public final class DownloadRequest: Request, @unchecked Sendable { |
| | | /// A set of options to be executed prior to moving a downloaded file from the temporary `URL` to the destination |
| | | /// `URL`. |
| | | public struct Options: OptionSet { |
| | | public struct Options: OptionSet, Sendable { |
| | | /// Specifies that intermediate directories for the destination URL should be created. |
| | | public static let createIntermediateDirectories = Options(rawValue: 1 << 0) |
| | | /// Specifies that any previous file at the destination `URL` should be removed. |
| | |
| | | /// |
| | | /// - Note: Downloads from a local `file://` `URL`s do not use the `Destination` closure, as those downloads do not |
| | | /// return an `HTTPURLResponse`. Instead the file is merely moved within the temporary directory. |
| | | public typealias Destination = (_ temporaryURL: URL, |
| | | _ response: HTTPURLResponse) -> (destinationURL: URL, options: Options) |
| | | public typealias Destination = @Sendable (_ temporaryURL: URL, |
| | | _ response: HTTPURLResponse) -> (destinationURL: URL, options: Options) |
| | | |
| | | /// Creates a download file destination closure which uses the default file manager to move the temporary file to a |
| | | /// file URL in the first available directory with the specified search path directory and search path domain mask. |
| | |
| | | |
| | | /// Default `URL` creation closure. Creates a `URL` in the temporary directory with `Alamofire_` prepended to the |
| | | /// provided file name. |
| | | static let defaultDestinationURL: (URL) -> URL = { url in |
| | | static let defaultDestinationURL: @Sendable (URL) -> URL = { url in |
| | | let filename = "Alamofire_\(url.lastPathComponent)" |
| | | let destination = url.deletingLastPathComponent().appendingPathComponent(filename) |
| | | |
| | |
| | | /// Type describing the source used to create the underlying `URLSessionDownloadTask`. |
| | | public enum Downloadable { |
| | | /// Download should be started from the `URLRequest` produced by the associated `URLRequestConvertible` value. |
| | | case request(URLRequestConvertible) |
| | | case request(any URLRequestConvertible) |
| | | /// Download should be started from the associated resume `Data` value. |
| | | case resumeData(Data) |
| | | } |
| | |
| | | downloadable: Downloadable, |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | delegate: RequestDelegate, |
| | | eventMonitor: (any EventMonitor)?, |
| | | interceptor: (any RequestInterceptor)?, |
| | | delegate: any RequestDelegate, |
| | | destination: @escaping Destination) { |
| | | self.downloadable = downloadable |
| | | self.destination = destination |
| | |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self { |
| | | cancel(optionallyProducingResumeData: shouldProduceResumeData ? { _ in } : nil) |
| | | cancel(optionallyProducingResumeData: shouldProduceResumeData ? { @Sendable _ in } : nil) |
| | | } |
| | | |
| | | /// Cancels the instance while producing resume data. Once cancelled, a `DownloadRequest` can no longer be resumed |
| | |
| | | /// want use an appropriate queue to perform your work. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void) -> Self { |
| | | public func cancel(byProducingResumeData completionHandler: @escaping @Sendable (_ data: Data?) -> Void) -> Self { |
| | | cancel(optionallyProducingResumeData: completionHandler) |
| | | } |
| | | |
| | |
| | | /// - Parameter completionHandler: Optional resume data handler. |
| | | /// |
| | | /// - Returns: The instance. |
| | | private func cancel(optionallyProducingResumeData completionHandler: ((_ resumeData: Data?) -> Void)?) -> Self { |
| | | private func cancel(optionallyProducingResumeData completionHandler: (@Sendable (_ resumeData: Data?) -> Void)?) -> Self { |
| | | mutableState.write { mutableState in |
| | | guard mutableState.state.canTransitionTo(.cancelled) else { return } |
| | | |
| | |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func validate(_ validation: @escaping Validation) -> Self { |
| | | let validator: () -> Void = { [unowned self] in |
| | | let validator: @Sendable () -> Void = { [unowned self] in |
| | | guard error == nil, let response else { return } |
| | | |
| | | let result = validation(request, response, fileURL) |
| | |
| | | /// - completionHandler: The code to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func response(queue: DispatchQueue = .main, |
| | | completionHandler: @escaping (AFDownloadResponse<URL?>) -> Void) |
| | | completionHandler: @escaping @Sendable (AFDownloadResponse<URL?>) -> Void) |
| | | -> Self { |
| | | appendResponseSerializer { |
| | | // Start work that should be on the serialization queue. |
| | |
| | | |
| | | private func _response<Serializer: DownloadResponseSerializerProtocol>(queue: DispatchQueue = .main, |
| | | responseSerializer: Serializer, |
| | | completionHandler: @escaping (AFDownloadResponse<Serializer.SerializedObject>) -> Void) |
| | | completionHandler: @escaping @Sendable (AFDownloadResponse<Serializer.SerializedObject>) -> Void) |
| | | -> Self { |
| | | appendResponseSerializer { |
| | | // Start work that should be on the serialization queue. |
| | |
| | | } |
| | | |
| | | delegate.retryResult(for: self, dueTo: serializerError) { retryResult in |
| | | var didComplete: (() -> Void)? |
| | | var didComplete: (@Sendable () -> Void)? |
| | | |
| | | defer { |
| | | if let didComplete { |
| | |
| | | |
| | | /// Adds a handler to be called once the request has finished. |
| | | /// |
| | | /// - Note: This handler will read the entire downloaded file into memory, use with caution. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: The queue on which the completion handler is dispatched. `.main` by default. |
| | | /// - responseSerializer: The response serializer responsible for serializing the request, response, and data |
| | |
| | | @discardableResult |
| | | public func response<Serializer: DownloadResponseSerializerProtocol>(queue: DispatchQueue = .main, |
| | | responseSerializer: Serializer, |
| | | completionHandler: @escaping (AFDownloadResponse<Serializer.SerializedObject>) -> Void) |
| | | completionHandler: @escaping @Sendable (AFDownloadResponse<Serializer.SerializedObject>) -> Void) |
| | | -> Self { |
| | | _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) |
| | | } |
| | | |
| | | /// Adds a handler to be called once the request has finished. |
| | | /// |
| | | /// - Note: This handler will read the entire downloaded file into memory, use with caution. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: The queue on which the completion handler is dispatched. `.main` by default. |
| | |
| | | @discardableResult |
| | | public func response<Serializer: ResponseSerializer>(queue: DispatchQueue = .main, |
| | | responseSerializer: Serializer, |
| | | completionHandler: @escaping (AFDownloadResponse<Serializer.SerializedObject>) -> Void) |
| | | completionHandler: @escaping @Sendable (AFDownloadResponse<Serializer.SerializedObject>) -> Void) |
| | | -> Self { |
| | | _response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) |
| | | } |
| | |
| | | /// - completionHandler: A closure to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseURL(queue: DispatchQueue = .main, |
| | | completionHandler: @escaping (AFDownloadResponse<URL>) -> Void) -> Self { |
| | | completionHandler: @escaping @Sendable (AFDownloadResponse<URL>) -> Void) -> Self { |
| | | response(queue: queue, responseSerializer: URLResponseSerializer(), completionHandler: completionHandler) |
| | | } |
| | | |
| | | /// Adds a handler using a `DataResponseSerializer` to be called once the request has finished. |
| | | /// |
| | | /// - Note: This handler will read the entire downloaded file into memory, use with caution. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: The queue on which the completion handler is called. `.main` by default. |
| | |
| | | /// - completionHandler: A closure to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseData(queue: DispatchQueue = .main, |
| | | dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods, |
| | | completionHandler: @escaping (AFDownloadResponse<Data>) -> Void) -> Self { |
| | | completionHandler: @escaping @Sendable (AFDownloadResponse<Data>) -> Void) -> Self { |
| | | response(queue: queue, |
| | | responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | |
| | | } |
| | | |
| | | /// Adds a handler using a `StringResponseSerializer` to be called once the request has finished. |
| | | /// |
| | | /// - Note: This handler will read the entire downloaded file into memory, use with caution. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: The queue on which the completion handler is dispatched. `.main` by default. |
| | |
| | | /// - completionHandler: A closure to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseString(queue: DispatchQueue = .main, |
| | | dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods, |
| | | completionHandler: @escaping (AFDownloadResponse<String>) -> Void) -> Self { |
| | | completionHandler: @escaping @Sendable (AFDownloadResponse<String>) -> Void) -> Self { |
| | | response(queue: queue, |
| | | responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | encoding: encoding, |
| | |
| | | } |
| | | |
| | | /// Adds a handler using a `JSONResponseSerializer` to be called once the request has finished. |
| | | /// |
| | | /// - Note: This handler will read the entire downloaded file into memory, use with caution. |
| | | /// |
| | | /// - Parameters: |
| | | /// - queue: The queue on which the completion handler is dispatched. `.main` by default. |
| | |
| | | /// |
| | | /// - Returns: The request. |
| | | @available(*, deprecated, message: "responseJSON deprecated and will be removed in Alamofire 6. Use responseDecodable instead.") |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseJSON(queue: DispatchQueue = .main, |
| | | dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = JSONResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods, |
| | | options: JSONSerialization.ReadingOptions = .allowFragments, |
| | | completionHandler: @escaping (AFDownloadResponse<Any>) -> Void) -> Self { |
| | | completionHandler: @escaping @Sendable (AFDownloadResponse<Any>) -> Void) -> Self { |
| | | response(queue: queue, |
| | | responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | emptyResponseCodes: emptyResponseCodes, |
| | |
| | | } |
| | | |
| | | /// Adds a handler using a `DecodableResponseSerializer` to be called once the request has finished. |
| | | /// |
| | | /// - Note: This handler will read the entire downloaded file into memory, use with caution. |
| | | /// |
| | | /// - Parameters: |
| | | /// - type: `Decodable` type to decode from response data. |
| | |
| | | /// - completionHandler: A closure to be executed once the request has finished. |
| | | /// |
| | | /// - Returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func responseDecodable<T: Decodable>(of type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | dataPreprocessor: any DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods, |
| | | completionHandler: @escaping (AFDownloadResponse<T>) -> Void) -> Self { |
| | | completionHandler: @escaping @Sendable (AFDownloadResponse<T>) -> Void) -> Self where T: Sendable { |
| | | response(queue: queue, |
| | | responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | | decoder: decoder, |
| | |
| | | /// |
| | | /// See the [Accept-Encoding HTTP header documentation](https://tools.ietf.org/html/rfc7230#section-4.2.3) . |
| | | public static let defaultAcceptEncoding: HTTPHeader = { |
| | | let encodings: [String] |
| | | if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { |
| | | encodings = ["br", "gzip", "deflate"] |
| | | let encodings: [String] = if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { |
| | | ["br", "gzip", "deflate"] |
| | | } else { |
| | | encodings = ["gzip", "deflate"] |
| | | ["gzip", "deflate"] |
| | | } |
| | | |
| | | return .acceptEncoding(encodings.qualityEncoded()) |
| | |
| | | |
| | | /// `EventMonitor` that provides Alamofire's notifications. |
| | | public final class AlamofireNotifications: EventMonitor { |
| | | /// Creates an instance. |
| | | public init() {} |
| | | |
| | | public func requestDidResume(_ request: Request) { |
| | | NotificationCenter.default.postNotification(named: Request.didResumeNotification, with: request) |
| | | } |
| | |
| | | import Foundation |
| | | |
| | | /// A type that can encode any `Encodable` type into a `URLRequest`. |
| | | public protocol ParameterEncoder { |
| | | public protocol ParameterEncoder: Sendable { |
| | | /// Encode the provided `Encodable` parameters into `request`. |
| | | /// |
| | | /// - Parameters: |
| | |
| | | /// - Returns: A `URLRequest` with the result of the encoding. |
| | | /// - Throws: An `Error` when encoding fails. For Alamofire provided encoders, this will be an instance of |
| | | /// `AFError.parameterEncoderFailed` with an associated `ParameterEncoderFailureReason`. |
| | | func encode<Parameters: Encodable>(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest |
| | | func encode<Parameters: Encodable & Sendable>(_ parameters: Parameters?, into request: URLRequest) throws -> URLRequest |
| | | } |
| | | |
| | | /// A `ParameterEncoder` that encodes types as JSON body data. |
| | | /// |
| | | /// If no `Content-Type` header is already set on the provided `URLRequest`s, it's set to `application/json`. |
| | | open class JSONParameterEncoder: ParameterEncoder { |
| | | open class JSONParameterEncoder: @unchecked Sendable, ParameterEncoder { |
| | | /// Returns an encoder with default parameters. |
| | | public static var `default`: JSONParameterEncoder { JSONParameterEncoder() } |
| | | |
| | |
| | | /// `application/x-www-form-urlencoded; charset=utf-8`. |
| | | /// |
| | | /// Encoding behavior can be customized by passing an instance of `URLEncodedFormEncoder` to the initializer. |
| | | open class URLEncodedFormParameterEncoder: ParameterEncoder { |
| | | open class URLEncodedFormParameterEncoder: @unchecked Sendable, ParameterEncoder { |
| | | /// Defines where the URL-encoded string should be set for each `URLRequest`. |
| | | public enum Destination { |
| | | /// Applies the encoded query string to any existing query string for `.get`, `.head`, and `.delete` request. |
| | |
| | | /// - Returns: Whether the URL-encoded string should be applied to a `URL`. |
| | | func encodesParametersInURL(for method: HTTPMethod) -> Bool { |
| | | switch self { |
| | | case .methodDependent: return [.get, .head, .delete].contains(method) |
| | | case .queryString: return true |
| | | case .httpBody: return false |
| | | case .methodDependent: [.get, .head, .delete].contains(method) |
| | | case .queryString: true |
| | | case .httpBody: false |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | if destination.encodesParametersInURL(for: method), |
| | | var components = URLComponents(url: url, resolvingAgainstBaseURL: false) { |
| | | let query: String = try Result<String, Error> { try encoder.encode(parameters) } |
| | | let query: String = try Result<String, any Error> { try encoder.encode(parameters) } |
| | | .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get() |
| | | let newQueryString = [components.percentEncodedQuery, query].compactMap { $0 }.joinedWithAmpersands() |
| | | components.percentEncodedQuery = newQueryString.isEmpty ? nil : newQueryString |
| | |
| | | request.headers.update(.contentType("application/x-www-form-urlencoded; charset=utf-8")) |
| | | } |
| | | |
| | | request.httpBody = try Result<Data, Error> { try encoder.encode(parameters) } |
| | | request.httpBody = try Result<Data, any Error> { try encoder.encode(parameters) } |
| | | .mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get() |
| | | } |
| | | |
| | |
| | | import Foundation |
| | | |
| | | /// A dictionary of parameters to apply to a `URLRequest`. |
| | | public typealias Parameters = [String: Any] |
| | | public typealias Parameters = [String: any Any & Sendable] |
| | | |
| | | /// A type used to define how a set of parameters are applied to a `URLRequest`. |
| | | public protocol ParameterEncoding { |
| | | public protocol ParameterEncoding: Sendable { |
| | | /// Creates a `URLRequest` by encoding parameters and applying them on the passed request. |
| | | /// |
| | | /// - Parameters: |
| | |
| | | /// |
| | | /// - Returns: The encoded `URLRequest`. |
| | | /// - Throws: Any `Error` produced during parameter encoding. |
| | | func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest |
| | | func encode(_ urlRequest: any URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest |
| | | } |
| | | |
| | | // MARK: - |
| | |
| | | |
| | | /// Defines whether the url-encoded query string is applied to the existing query string or HTTP body of the |
| | | /// resulting URL request. |
| | | public enum Destination { |
| | | public enum Destination: Sendable { |
| | | /// Applies encoded query string result to existing query string for `GET`, `HEAD` and `DELETE` requests and |
| | | /// sets as the HTTP body for requests with any other HTTP method. |
| | | case methodDependent |
| | |
| | | |
| | | func encodesParametersInURL(for method: HTTPMethod) -> Bool { |
| | | switch self { |
| | | case .methodDependent: return [.get, .head, .delete].contains(method) |
| | | case .queryString: return true |
| | | case .httpBody: return false |
| | | case .methodDependent: [.get, .head, .delete].contains(method) |
| | | case .queryString: true |
| | | case .httpBody: false |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Configures how `Array` parameters are encoded. |
| | | public enum ArrayEncoding { |
| | | public enum ArrayEncoding: Sendable { |
| | | /// An empty set of square brackets is appended to the key for every value. This is the default behavior. |
| | | case brackets |
| | | /// No brackets are appended. The key is encoded as is. |
| | |
| | | /// Brackets containing the item index are appended. This matches the jQuery and Node.js behavior. |
| | | case indexInBrackets |
| | | /// Provide a custom array key encoding with the given closure. |
| | | case custom((_ key: String, _ index: Int) -> String) |
| | | case custom(@Sendable (_ key: String, _ index: Int) -> String) |
| | | |
| | | func encode(key: String, atIndex index: Int) -> String { |
| | | switch self { |
| | | case .brackets: |
| | | return "\(key)[]" |
| | | "\(key)[]" |
| | | case .noBrackets: |
| | | return key |
| | | key |
| | | case .indexInBrackets: |
| | | return "\(key)[\(index)]" |
| | | "\(key)[\(index)]" |
| | | case let .custom(encoding): |
| | | return encoding(key, index) |
| | | encoding(key, index) |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// Configures how `Bool` parameters are encoded. |
| | | public enum BoolEncoding { |
| | | public enum BoolEncoding: Sendable { |
| | | /// Encode `true` as `1` and `false` as `0`. This is the default behavior. |
| | | case numeric |
| | | /// Encode `true` and `false` as string literals. |
| | |
| | | func encode(value: Bool) -> String { |
| | | switch self { |
| | | case .numeric: |
| | | return value ? "1" : "0" |
| | | value ? "1" : "0" |
| | | case .literal: |
| | | return value ? "true" : "false" |
| | | value ? "true" : "false" |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | // MARK: Encoding |
| | | |
| | | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { |
| | | public func encode(_ urlRequest: any URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { |
| | | var urlRequest = try urlRequest.asURLRequest() |
| | | |
| | | guard let parameters else { return urlRequest } |
| | |
| | | |
| | | // MARK: Encoding |
| | | |
| | | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { |
| | | public func encode(_ urlRequest: any URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { |
| | | var urlRequest = try urlRequest.asURLRequest() |
| | | |
| | | guard let parameters else { return urlRequest } |
| | |
| | | /// |
| | | /// - Returns: The encoded `URLRequest`. |
| | | /// - Throws: Any `Error` produced during encoding. |
| | | public func encode(_ urlRequest: URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest { |
| | | public func encode(_ urlRequest: any URLRequestConvertible, withJSONObject jsonObject: Any? = nil) throws -> URLRequest { |
| | | var urlRequest = try urlRequest.asURLRequest() |
| | | |
| | | guard let jsonObject else { return urlRequest } |
| | |
| | | |
| | | import Foundation |
| | | |
| | | private protocol Lock { |
| | | private protocol Lock: Sendable { |
| | | func lock() |
| | | func unlock() |
| | | } |
| | |
| | | } |
| | | |
| | | #if canImport(Darwin) |
| | | // Number of Apple engineers who insisted on inspecting this: 5 |
| | | /// An `os_unfair_lock` wrapper. |
| | | final class UnfairLock: Lock { |
| | | final class UnfairLock: Lock, @unchecked Sendable { |
| | | private let unfairLock: os_unfair_lock_t |
| | | |
| | | init() { |
| | |
| | | #else |
| | | #error("This platform needs a Lock-conforming type without Foundation.") |
| | | #endif |
| | | #if compiler(>=6) |
| | | private nonisolated(unsafe) var value: Value |
| | | #else |
| | | private var value: Value |
| | | #endif |
| | | |
| | | init(_ value: Value) { |
| | | self.value = value |
| | |
| | | } |
| | | } |
| | | |
| | | #if compiler(>=6) |
| | | extension Protected: Sendable {} |
| | | #else |
| | | extension Protected: @unchecked Sendable {} |
| | | #endif |
| | | |
| | | extension Protected where Value == Request.MutableState { |
| | | /// Attempts to transition to the passed `State`. |
| | | /// |
| | |
| | | |
| | | /// `Request` is the common superclass of all Alamofire request types and provides common state, delegate, and callback |
| | | /// handling. |
| | | public class Request { |
| | | public class Request: @unchecked Sendable { |
| | | /// State of the `Request`, with managed transitions between states set when calling `resume()`, `suspend()`, or |
| | | /// `cancel()` on the `Request`. |
| | | public enum State { |
| | |
| | | func canTransitionTo(_ state: State) -> Bool { |
| | | switch (self, state) { |
| | | case (.initialized, _): |
| | | return true |
| | | true |
| | | case (_, .initialized), (.cancelled, _), (.finished, _): |
| | | return false |
| | | false |
| | | case (.resumed, .cancelled), (.suspended, .cancelled), (.resumed, .suspended), (.suspended, .resumed): |
| | | return true |
| | | true |
| | | case (.suspended, .suspended), (.resumed, .resumed): |
| | | return false |
| | | false |
| | | case (_, .finished): |
| | | return true |
| | | true |
| | | } |
| | | } |
| | | } |
| | |
| | | /// The queue used for all serialization actions. By default it's a serial queue that targets `underlyingQueue`. |
| | | public let serializationQueue: DispatchQueue |
| | | /// `EventMonitor` used for event callbacks. |
| | | public let eventMonitor: EventMonitor? |
| | | public let eventMonitor: (any EventMonitor)? |
| | | /// The `Request`'s interceptor. |
| | | public let interceptor: RequestInterceptor? |
| | | public let interceptor: (any RequestInterceptor)? |
| | | /// The `Request`'s delegate. |
| | | public private(set) weak var delegate: RequestDelegate? |
| | | public private(set) weak var delegate: (any RequestDelegate)? |
| | | |
| | | // MARK: - Mutable State |
| | | |
| | |
| | | /// `ProgressHandler` and `DispatchQueue` provided for download progress callbacks. |
| | | var downloadProgressHandler: (handler: ProgressHandler, queue: DispatchQueue)? |
| | | /// `RedirectHandler` provided for to handle request redirection. |
| | | var redirectHandler: RedirectHandler? |
| | | var redirectHandler: (any RedirectHandler)? |
| | | /// `CachedResponseHandler` provided to handle response caching. |
| | | var cachedResponseHandler: CachedResponseHandler? |
| | | var cachedResponseHandler: (any CachedResponseHandler)? |
| | | /// Queue and closure called when the `Request` is able to create a cURL description of itself. |
| | | var cURLHandler: (queue: DispatchQueue, handler: (String) -> Void)? |
| | | var cURLHandler: (queue: DispatchQueue, handler: @Sendable (String) -> Void)? |
| | | /// Queue and closure called when the `Request` creates a `URLRequest`. |
| | | var urlRequestHandler: (queue: DispatchQueue, handler: (URLRequest) -> Void)? |
| | | var urlRequestHandler: (queue: DispatchQueue, handler: @Sendable (URLRequest) -> Void)? |
| | | /// Queue and closure called when the `Request` creates a `URLSessionTask`. |
| | | var urlSessionTaskHandler: (queue: DispatchQueue, handler: (URLSessionTask) -> Void)? |
| | | var urlSessionTaskHandler: (queue: DispatchQueue, handler: @Sendable (URLSessionTask) -> Void)? |
| | | /// Response serialization closures that handle response parsing. |
| | | var responseSerializers: [() -> Void] = [] |
| | | var responseSerializers: [@Sendable () -> Void] = [] |
| | | /// Response serialization completion closures executed once all response serializers are complete. |
| | | var responseSerializerCompletions: [() -> Void] = [] |
| | | var responseSerializerCompletions: [@Sendable () -> Void] = [] |
| | | /// Whether response serializer processing is finished. |
| | | var responseSerializerProcessingFinished = false |
| | | /// `URLCredential` used for authentication challenges. |
| | |
| | | // MARK: Progress |
| | | |
| | | /// Closure type executed when monitoring the upload or download progress of a request. |
| | | public typealias ProgressHandler = (Progress) -> Void |
| | | public typealias ProgressHandler = @Sendable (_ progress: Progress) -> Void |
| | | |
| | | /// `Progress` of the upload of the body of the executed `URLRequest`. Reset to `0` if the `Request` is retried. |
| | | public let uploadProgress = Progress(totalUnitCount: 0) |
| | |
| | | // MARK: Redirect Handling |
| | | |
| | | /// `RedirectHandler` set on the instance. |
| | | public internal(set) var redirectHandler: RedirectHandler? { |
| | | public internal(set) var redirectHandler: (any RedirectHandler)? { |
| | | get { mutableState.redirectHandler } |
| | | set { mutableState.redirectHandler = newValue } |
| | | } |
| | |
| | | // MARK: Cached Response Handling |
| | | |
| | | /// `CachedResponseHandler` set on the instance. |
| | | public internal(set) var cachedResponseHandler: CachedResponseHandler? { |
| | | public internal(set) var cachedResponseHandler: (any CachedResponseHandler)? { |
| | | get { mutableState.cachedResponseHandler } |
| | | set { mutableState.cachedResponseHandler = newValue } |
| | | } |
| | |
| | | // MARK: Validators |
| | | |
| | | /// `Validator` callback closures that store the validation calls enqueued. |
| | | let validators = Protected<[() -> Void]>([]) |
| | | let validators = Protected<[@Sendable () -> Void]>([]) |
| | | |
| | | // MARK: URLRequests |
| | | |
| | |
| | | init(id: UUID = UUID(), |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | delegate: RequestDelegate) { |
| | | eventMonitor: (any EventMonitor)?, |
| | | interceptor: (any RequestInterceptor)?, |
| | | delegate: any RequestDelegate) { |
| | | self.id = id |
| | | self.underlyingQueue = underlyingQueue |
| | | self.serializationQueue = serializationQueue |
| | |
| | | dispatchPrecondition(condition: .onQueue(underlyingQueue)) |
| | | |
| | | mutableState.read { state in |
| | | state.urlRequestHandler?.queue.async { state.urlRequestHandler?.handler(request) } |
| | | guard let urlRequestHandler = state.urlRequestHandler else { return } |
| | | |
| | | urlRequestHandler.queue.async { urlRequestHandler.handler(request) } |
| | | } |
| | | |
| | | eventMonitor?.request(self, didCreateURLRequest: request) |
| | |
| | | /// - Note: This method will also `resume` the instance if `delegate.startImmediately` returns `true`. |
| | | /// |
| | | /// - Parameter closure: The closure containing the response serialization call. |
| | | func appendResponseSerializer(_ closure: @escaping () -> Void) { |
| | | func appendResponseSerializer(_ closure: @escaping @Sendable () -> Void) { |
| | | mutableState.write { mutableState in |
| | | mutableState.responseSerializers.append(closure) |
| | | |
| | |
| | | /// Returns the next response serializer closure to execute if there's one left. |
| | | /// |
| | | /// - Returns: The next response serialization closure, if there is one. |
| | | func nextResponseSerializer() -> (() -> Void)? { |
| | | var responseSerializer: (() -> Void)? |
| | | func nextResponseSerializer() -> (@Sendable () -> Void)? { |
| | | var responseSerializer: (@Sendable () -> Void)? |
| | | |
| | | mutableState.write { mutableState in |
| | | let responseSerializerIndex = mutableState.responseSerializerCompletions.count |
| | |
| | | func processNextResponseSerializer() { |
| | | guard let responseSerializer = nextResponseSerializer() else { |
| | | // Execute all response serializer completions and clear them |
| | | var completions: [() -> Void] = [] |
| | | var completions: [@Sendable () -> Void] = [] |
| | | |
| | | mutableState.write { mutableState in |
| | | completions = mutableState.responseSerializerCompletions |
| | |
| | | /// |
| | | /// - Parameter completion: The completion handler provided with the response serializer, called when all serializers |
| | | /// are complete. |
| | | func responseSerializerDidComplete(completion: @escaping () -> Void) { |
| | | func responseSerializerDidComplete(completion: @escaping @Sendable () -> Void) { |
| | | mutableState.write { $0.responseSerializerCompletions.append(completion) } |
| | | processNextResponseSerializer() |
| | | } |
| | |
| | | /// - closure: The closure to be executed periodically as data is read from the server. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func downloadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { |
| | | mutableState.downloadProgressHandler = (handler: closure, queue: queue) |
| | |
| | | /// - closure: The closure to be executed periodically as data is sent to the server. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func uploadProgress(queue: DispatchQueue = .main, closure: @escaping ProgressHandler) -> Self { |
| | | mutableState.uploadProgressHandler = (handler: closure, queue: queue) |
| | |
| | | /// - Parameter handler: The `RedirectHandler`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func redirect(using handler: RedirectHandler) -> Self { |
| | | public func redirect(using handler: any RedirectHandler) -> Self { |
| | | mutableState.write { mutableState in |
| | | precondition(mutableState.redirectHandler == nil, "Redirect handler has already been set.") |
| | | mutableState.redirectHandler = handler |
| | |
| | | /// - Parameter handler: The `CachedResponseHandler`. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func cacheResponse(using handler: CachedResponseHandler) -> Self { |
| | | public func cacheResponse(using handler: any CachedResponseHandler) -> Self { |
| | | mutableState.write { mutableState in |
| | | precondition(mutableState.cachedResponseHandler == nil, "Cached response handler has already been set.") |
| | | mutableState.cachedResponseHandler = handler |
| | |
| | | /// - handler: Closure to be called when the cURL description is available. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping (String) -> Void) -> Self { |
| | | public func cURLDescription(on queue: DispatchQueue, calling handler: @escaping @Sendable (String) -> Void) -> Self { |
| | | mutableState.write { mutableState in |
| | | if mutableState.requests.last != nil { |
| | | queue.async { handler(self.cURLDescription()) } |
| | |
| | | /// `underlyingQueue` by default. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func cURLDescription(calling handler: @escaping (String) -> Void) -> Self { |
| | | public func cURLDescription(calling handler: @escaping @Sendable (String) -> Void) -> Self { |
| | | cURLDescription(on: underlyingQueue, calling: handler) |
| | | |
| | | return self |
| | |
| | | /// - handler: Closure to be called when a `URLRequest` is available. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func onURLRequestCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLRequest) -> Void) -> Self { |
| | | public func onURLRequestCreation(on queue: DispatchQueue = .main, perform handler: @escaping @Sendable (URLRequest) -> Void) -> Self { |
| | | mutableState.write { state in |
| | | if let request = state.requests.last { |
| | | queue.async { handler(request) } |
| | |
| | | /// - handler: Closure to be called when the `URLSessionTask` is available. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func onURLSessionTaskCreation(on queue: DispatchQueue = .main, perform handler: @escaping (URLSessionTask) -> Void) -> Self { |
| | | public func onURLSessionTaskCreation(on queue: DispatchQueue = .main, perform handler: @escaping @Sendable (URLSessionTask) -> Void) -> Self { |
| | | mutableState.write { state in |
| | | if let task = state.tasks.last { |
| | | queue.async { handler(task) } |
| | |
| | | |
| | | extension Request { |
| | | /// Type indicating how a `DataRequest` or `DataStreamRequest` should proceed after receiving an `HTTPURLResponse`. |
| | | public enum ResponseDisposition { |
| | | public enum ResponseDisposition: Sendable { |
| | | /// Allow the request to continue normally. |
| | | case allow |
| | | /// Cancel the request, similar to calling `cancel()`. |
| | |
| | | |
| | | var sessionDisposition: URLSession.ResponseDisposition { |
| | | switch self { |
| | | case .allow: return .allow |
| | | case .cancel: return .cancel |
| | | case .allow: .allow |
| | | case .cancel: .cancel |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | /// Protocol abstraction for `Request`'s communication back to the `SessionDelegate`. |
| | | public protocol RequestDelegate: AnyObject { |
| | | public protocol RequestDelegate: AnyObject, Sendable { |
| | | /// `URLSessionConfiguration` used to create the underlying `URLSessionTask`s. |
| | | var sessionConfiguration: URLSessionConfiguration { get } |
| | | |
| | |
| | | /// - request: `Request` which failed. |
| | | /// - error: `Error` which produced the failure. |
| | | /// - completion: Closure taking the `RetryResult` for evaluation. |
| | | func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) |
| | | func retryResult(for request: Request, dueTo error: AFError, completion: @escaping @Sendable (RetryResult) -> Void) |
| | | |
| | | /// Asynchronously retry the `Request`. |
| | | /// |
| | |
| | | public typealias AFDownloadResponse<Success> = DownloadResponse<Success, AFError> |
| | | |
| | | /// Type used to store all values associated with a serialized response of a `DataRequest` or `UploadRequest`. |
| | | public struct DataResponse<Success, Failure: Error> { |
| | | public struct DataResponse<Success, Failure: Error>: Sendable where Success: Sendable, Failure: Sendable { |
| | | /// The URL request sent to the server. |
| | | public let request: URLRequest? |
| | | |
| | |
| | | /// |
| | | /// - returns: A success or failure `DataResponse` depending on the result of the given closure. If this instance's |
| | | /// result is a failure, returns the same failure. |
| | | public func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> DataResponse<NewSuccess, Error> { |
| | | DataResponse<NewSuccess, Error>(request: request, |
| | | response: response, |
| | | data: data, |
| | | metrics: metrics, |
| | | serializationDuration: serializationDuration, |
| | | result: result.tryMap(transform)) |
| | | public func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> DataResponse<NewSuccess, any Error> { |
| | | DataResponse<NewSuccess, any Error>(request: request, |
| | | response: response, |
| | | data: data, |
| | | metrics: metrics, |
| | | serializationDuration: serializationDuration, |
| | | result: result.tryMap(transform)) |
| | | } |
| | | |
| | | /// Evaluates the specified closure when the `DataResponse` is a failure, passing the unwrapped error as a parameter. |
| | |
| | | /// - Parameter transform: A throwing closure that takes the error of the instance. |
| | | /// |
| | | /// - Returns: A `DataResponse` instance containing the result of the transform. |
| | | public func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> DataResponse<Success, Error> { |
| | | DataResponse<Success, Error>(request: request, |
| | | response: response, |
| | | data: data, |
| | | metrics: metrics, |
| | | serializationDuration: serializationDuration, |
| | | result: result.tryMapError(transform)) |
| | | public func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> DataResponse<Success, any Error> { |
| | | DataResponse<Success, any Error>(request: request, |
| | | response: response, |
| | | data: data, |
| | | metrics: metrics, |
| | | serializationDuration: serializationDuration, |
| | | result: result.tryMapError(transform)) |
| | | } |
| | | } |
| | | |
| | | // MARK: - |
| | | |
| | | /// Used to store all data associated with a serialized response of a download request. |
| | | public struct DownloadResponse<Success, Failure: Error> { |
| | | public struct DownloadResponse<Success, Failure: Error>: Sendable where Success: Sendable, Failure: Sendable { |
| | | /// The URL request sent to the server. |
| | | public let request: URLRequest? |
| | | |
| | |
| | | /// |
| | | /// - returns: A success or failure `DownloadResponse` depending on the result of the given closure. If this |
| | | /// instance's result is a failure, returns the same failure. |
| | | public func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> DownloadResponse<NewSuccess, Error> { |
| | | DownloadResponse<NewSuccess, Error>(request: request, |
| | | response: response, |
| | | fileURL: fileURL, |
| | | resumeData: resumeData, |
| | | metrics: metrics, |
| | | serializationDuration: serializationDuration, |
| | | result: result.tryMap(transform)) |
| | | public func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> DownloadResponse<NewSuccess, any Error> { |
| | | DownloadResponse<NewSuccess, any Error>(request: request, |
| | | response: response, |
| | | fileURL: fileURL, |
| | | resumeData: resumeData, |
| | | metrics: metrics, |
| | | serializationDuration: serializationDuration, |
| | | result: result.tryMap(transform)) |
| | | } |
| | | |
| | | /// Evaluates the specified closure when the `DownloadResponse` is a failure, passing the unwrapped error as a parameter. |
| | |
| | | /// - Parameter transform: A throwing closure that takes the error of the instance. |
| | | /// |
| | | /// - Returns: A `DownloadResponse` instance containing the result of the transform. |
| | | public func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> DownloadResponse<Success, Error> { |
| | | DownloadResponse<Success, Error>(request: request, |
| | | response: response, |
| | | fileURL: fileURL, |
| | | resumeData: resumeData, |
| | | metrics: metrics, |
| | | serializationDuration: serializationDuration, |
| | | result: result.tryMapError(transform)) |
| | | public func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> DownloadResponse<Success, any Error> { |
| | | DownloadResponse<Success, any Error>(request: request, |
| | | response: response, |
| | | fileURL: fileURL, |
| | | resumeData: resumeData, |
| | | metrics: metrics, |
| | | serializationDuration: serializationDuration, |
| | | result: result.tryMapError(transform)) |
| | | } |
| | | } |
| | | |
| | |
| | | /// `Session` creates and manages Alamofire's `Request` types during their lifetimes. It also provides common |
| | | /// functionality for all `Request`s, including queuing, interception, trust management, redirect handling, and response |
| | | /// cache handling. |
| | | open class Session { |
| | | open class Session: @unchecked Sendable { |
| | | /// Shared singleton instance used by all `AF.request` APIs. Cannot be modified. |
| | | public static let `default` = Session() |
| | | |
| | |
| | | public let serializationQueue: DispatchQueue |
| | | /// `RequestInterceptor` used for all `Request` created by the instance. `RequestInterceptor`s can also be set on a |
| | | /// per-`Request` basis, in which case the `Request`'s interceptor takes precedence over this value. |
| | | public let interceptor: RequestInterceptor? |
| | | public let interceptor: (any RequestInterceptor)? |
| | | /// `ServerTrustManager` instance used to evaluate all trust challenges and provide certificate and key pinning. |
| | | public let serverTrustManager: ServerTrustManager? |
| | | /// `RedirectHandler` instance used to provide customization for request redirection. |
| | | public let redirectHandler: RedirectHandler? |
| | | public let redirectHandler: (any RedirectHandler)? |
| | | /// `CachedResponseHandler` instance used to provide customization of cached response handling. |
| | | public let cachedResponseHandler: CachedResponseHandler? |
| | | /// `CompositeEventMonitor` used to compose Alamofire's `defaultEventMonitors` and any passed `EventMonitor`s. |
| | | public let cachedResponseHandler: (any CachedResponseHandler)? |
| | | /// `CompositeEventMonitor` used to compose any passed `EventMonitor`s. |
| | | public let eventMonitor: CompositeEventMonitor |
| | | /// `EventMonitor`s included in all instances. `[AlamofireNotifications()]` by default. |
| | | public let defaultEventMonitors: [EventMonitor] = [AlamofireNotifications()] |
| | | /// `EventMonitor`s included in all instances unless overwritten. `[AlamofireNotifications()]` by default. |
| | | @available(*, deprecated, message: "Use [AlamofireNotifications()] directly.") |
| | | public let defaultEventMonitors: [any EventMonitor] = [AlamofireNotifications()] |
| | | |
| | | /// Internal map between `Request`s and any `URLSessionTasks` that may be in flight for them. |
| | | var requestTaskMap = RequestTaskMap() |
| | |
| | | /// default. |
| | | /// - cachedResponseHandler: `CachedResponseHandler` to be used by all `Request`s created by this instance. |
| | | /// `nil` by default. |
| | | /// - eventMonitors: Additional `EventMonitor`s used by the instance. Alamofire always adds a |
| | | /// `AlamofireNotifications` `EventMonitor` to the array passed here. `[]` by default. |
| | | /// - eventMonitors: `EventMonitor`s used by the instance. `[AlamofireNotifications()]` by default. |
| | | public init(session: URLSession, |
| | | delegate: SessionDelegate, |
| | | rootQueue: DispatchQueue, |
| | | startRequestsImmediately: Bool = true, |
| | | requestQueue: DispatchQueue? = nil, |
| | | serializationQueue: DispatchQueue? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | serverTrustManager: ServerTrustManager? = nil, |
| | | redirectHandler: RedirectHandler? = nil, |
| | | cachedResponseHandler: CachedResponseHandler? = nil, |
| | | eventMonitors: [EventMonitor] = []) { |
| | | redirectHandler: (any RedirectHandler)? = nil, |
| | | cachedResponseHandler: (any CachedResponseHandler)? = nil, |
| | | eventMonitors: [any EventMonitor] = [AlamofireNotifications()]) { |
| | | precondition(session.configuration.identifier == nil, |
| | | "Alamofire does not support background URLSessionConfigurations.") |
| | | precondition(session.delegateQueue.underlyingQueue === rootQueue, |
| | |
| | | self.serverTrustManager = serverTrustManager |
| | | self.redirectHandler = redirectHandler |
| | | self.cachedResponseHandler = cachedResponseHandler |
| | | eventMonitor = CompositeEventMonitor(monitors: defaultEventMonitors + eventMonitors) |
| | | eventMonitor = CompositeEventMonitor(monitors: eventMonitors) |
| | | delegate.eventMonitor = eventMonitor |
| | | delegate.stateProvider = self |
| | | } |
| | |
| | | /// default. |
| | | /// - cachedResponseHandler: `CachedResponseHandler` to be used by all `Request`s created by this instance. |
| | | /// `nil` by default. |
| | | /// - eventMonitors: Additional `EventMonitor`s used by the instance. Alamofire always adds a |
| | | /// `AlamofireNotifications` `EventMonitor` to the array passed here. `[]` by default. |
| | | /// - eventMonitors: `EventMonitor`s used by the instance. `[AlamofireNotifications()]` by default. |
| | | public convenience init(configuration: URLSessionConfiguration = URLSessionConfiguration.af.default, |
| | | delegate: SessionDelegate = SessionDelegate(), |
| | | rootQueue: DispatchQueue = DispatchQueue(label: "org.alamofire.session.rootQueue"), |
| | | startRequestsImmediately: Bool = true, |
| | | requestQueue: DispatchQueue? = nil, |
| | | serializationQueue: DispatchQueue? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | serverTrustManager: ServerTrustManager? = nil, |
| | | redirectHandler: RedirectHandler? = nil, |
| | | cachedResponseHandler: CachedResponseHandler? = nil, |
| | | eventMonitors: [EventMonitor] = []) { |
| | | redirectHandler: (any RedirectHandler)? = nil, |
| | | cachedResponseHandler: (any CachedResponseHandler)? = nil, |
| | | eventMonitors: [any EventMonitor] = [AlamofireNotifications()]) { |
| | | precondition(configuration.identifier == nil, "Alamofire does not support background URLSessionConfigurations.") |
| | | |
| | | // Retarget the incoming rootQueue for safety, unless it's the main queue, which we know is safe. |
| | |
| | | /// |
| | | /// - Parameters: |
| | | /// - action: Closure to perform with all `Request`s. |
| | | public func withAllRequests(perform action: @escaping (Set<Request>) -> Void) { |
| | | public func withAllRequests(perform action: @escaping @Sendable (Set<Request>) -> Void) { |
| | | rootQueue.async { |
| | | action(self.activeRequests) |
| | | } |
| | |
| | | /// - Parameters: |
| | | /// - queue: `DispatchQueue` on which the completion handler is run. `.main` by default. |
| | | /// - completion: Closure to be called when all `Request`s have been cancelled. |
| | | public func cancelAllRequests(completingOnQueue queue: DispatchQueue = .main, completion: (() -> Void)? = nil) { |
| | | public func cancelAllRequests(completingOnQueue queue: DispatchQueue = .main, completion: (@Sendable () -> Void)? = nil) { |
| | | withAllRequests { requests in |
| | | requests.forEach { $0.cancel() } |
| | | queue.async { |
| | |
| | | // MARK: - DataRequest |
| | | |
| | | /// Closure which provides a `URLRequest` for mutation. |
| | | public typealias RequestModifier = (inout URLRequest) throws -> Void |
| | | public typealias RequestModifier = @Sendable (inout URLRequest) throws -> Void |
| | | |
| | | struct RequestConvertible: URLRequestConvertible { |
| | | let url: URLConvertible |
| | | let url: any URLConvertible |
| | | let method: HTTPMethod |
| | | let parameters: Parameters? |
| | | let encoding: ParameterEncoding |
| | | let encoding: any ParameterEncoding |
| | | let headers: HTTPHeaders? |
| | | let requestModifier: RequestModifier? |
| | | |
| | |
| | | /// parameters. `nil` by default. |
| | | /// |
| | | /// - Returns: The created `DataRequest`. |
| | | open func request(_ convertible: URLConvertible, |
| | | open func request(_ convertible: any URLConvertible, |
| | | method: HTTPMethod = .get, |
| | | parameters: Parameters? = nil, |
| | | encoding: ParameterEncoding = URLEncoding.default, |
| | | encoding: any ParameterEncoding = URLEncoding.default, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | requestModifier: RequestModifier? = nil) -> DataRequest { |
| | | let convertible = RequestConvertible(url: convertible, |
| | | method: method, |
| | |
| | | return request(convertible, interceptor: interceptor) |
| | | } |
| | | |
| | | struct RequestEncodableConvertible<Parameters: Encodable>: URLRequestConvertible { |
| | | let url: URLConvertible |
| | | struct RequestEncodableConvertible<Parameters: Encodable & Sendable>: URLRequestConvertible { |
| | | let url: any URLConvertible |
| | | let method: HTTPMethod |
| | | let parameters: Parameters? |
| | | let encoder: ParameterEncoder |
| | | let encoder: any ParameterEncoder |
| | | let headers: HTTPHeaders? |
| | | let requestModifier: RequestModifier? |
| | | |
| | |
| | | /// the provided parameters. `nil` by default. |
| | | /// |
| | | /// - Returns: The created `DataRequest`. |
| | | open func request<Parameters: Encodable>(_ convertible: URLConvertible, |
| | | method: HTTPMethod = .get, |
| | | parameters: Parameters? = nil, |
| | | encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | requestModifier: RequestModifier? = nil) -> DataRequest { |
| | | open func request<Parameters: Encodable & Sendable>(_ convertible: any URLConvertible, |
| | | method: HTTPMethod = .get, |
| | | parameters: Parameters? = nil, |
| | | encoder: any ParameterEncoder = URLEncodedFormParameterEncoder.default, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | requestModifier: RequestModifier? = nil) -> DataRequest { |
| | | let convertible = RequestEncodableConvertible(url: convertible, |
| | | method: method, |
| | | parameters: parameters, |
| | |
| | | /// - interceptor: `RequestInterceptor` value to be used by the returned `DataRequest`. `nil` by default. |
| | | /// |
| | | /// - Returns: The created `DataRequest`. |
| | | open func request(_ convertible: URLRequestConvertible, interceptor: RequestInterceptor? = nil) -> DataRequest { |
| | | open func request(_ convertible: any URLRequestConvertible, interceptor: (any RequestInterceptor)? = nil) -> DataRequest { |
| | | let request = DataRequest(convertible: convertible, |
| | | underlyingQueue: rootQueue, |
| | | serializationQueue: serializationQueue, |
| | |
| | | /// the provided parameters. `nil` by default. |
| | | /// |
| | | /// - Returns: The created `DataStream` request. |
| | | open func streamRequest<Parameters: Encodable>(_ convertible: URLConvertible, |
| | | method: HTTPMethod = .get, |
| | | parameters: Parameters? = nil, |
| | | encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, |
| | | headers: HTTPHeaders? = nil, |
| | | automaticallyCancelOnStreamError: Bool = false, |
| | | interceptor: RequestInterceptor? = nil, |
| | | requestModifier: RequestModifier? = nil) -> DataStreamRequest { |
| | | open func streamRequest<Parameters: Encodable & Sendable>(_ convertible: any URLConvertible, |
| | | method: HTTPMethod = .get, |
| | | parameters: Parameters? = nil, |
| | | encoder: any ParameterEncoder = URLEncodedFormParameterEncoder.default, |
| | | headers: HTTPHeaders? = nil, |
| | | automaticallyCancelOnStreamError: Bool = false, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | requestModifier: RequestModifier? = nil) -> DataStreamRequest { |
| | | let convertible = RequestEncodableConvertible(url: convertible, |
| | | method: method, |
| | | parameters: parameters, |
| | |
| | | /// the provided parameters. `nil` by default. |
| | | /// |
| | | /// - Returns: The created `DataStream` request. |
| | | open func streamRequest(_ convertible: URLConvertible, |
| | | open func streamRequest(_ convertible: any URLConvertible, |
| | | method: HTTPMethod = .get, |
| | | headers: HTTPHeaders? = nil, |
| | | automaticallyCancelOnStreamError: Bool = false, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | requestModifier: RequestModifier? = nil) -> DataStreamRequest { |
| | | let convertible = RequestEncodableConvertible(url: convertible, |
| | | method: method, |
| | |
| | | /// by default. |
| | | /// |
| | | /// - Returns: The created `DataStreamRequest`. |
| | | open func streamRequest(_ convertible: URLRequestConvertible, |
| | | open func streamRequest(_ convertible: any URLRequestConvertible, |
| | | automaticallyCancelOnStreamError: Bool = false, |
| | | interceptor: RequestInterceptor? = nil) -> DataStreamRequest { |
| | | interceptor: (any RequestInterceptor)? = nil) -> DataStreamRequest { |
| | | let request = DataStreamRequest(convertible: convertible, |
| | | automaticallyCancelOnStreamError: automaticallyCancelOnStreamError, |
| | | underlyingQueue: rootQueue, |
| | |
| | | #if canImport(Darwin) && !canImport(FoundationNetworking) // Only Apple platforms support URLSessionWebSocketTask. |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | @_spi(WebSocket) open func webSocketRequest( |
| | | to url: URLConvertible, |
| | | to url: any URLConvertible, |
| | | configuration: WebSocketRequest.Configuration = .default, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | requestModifier: RequestModifier? = nil |
| | | ) -> WebSocketRequest { |
| | | webSocketRequest( |
| | |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | @_spi(WebSocket) open func webSocketRequest<Parameters>( |
| | | to url: URLConvertible, |
| | | to url: any URLConvertible, |
| | | configuration: WebSocketRequest.Configuration = .default, |
| | | parameters: Parameters? = nil, |
| | | encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, |
| | | encoder: any ParameterEncoder = URLEncodedFormParameterEncoder.default, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | requestModifier: RequestModifier? = nil |
| | | ) -> WebSocketRequest where Parameters: Encodable { |
| | | ) -> WebSocketRequest where Parameters: Encodable & Sendable { |
| | | let convertible = RequestEncodableConvertible(url: url, |
| | | method: .get, |
| | | parameters: parameters, |
| | |
| | | } |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | @_spi(WebSocket) open func webSocketRequest(performing convertible: URLRequestConvertible, |
| | | @_spi(WebSocket) open func webSocketRequest(performing convertible: any URLRequestConvertible, |
| | | configuration: WebSocketRequest.Configuration = .default, |
| | | interceptor: RequestInterceptor? = nil) -> WebSocketRequest { |
| | | interceptor: (any RequestInterceptor)? = nil) -> WebSocketRequest { |
| | | let request = WebSocketRequest(convertible: convertible, |
| | | configuration: configuration, |
| | | underlyingQueue: rootQueue, |
| | |
| | | /// should be moved. `nil` by default. |
| | | /// |
| | | /// - Returns: The created `DownloadRequest`. |
| | | open func download(_ convertible: URLConvertible, |
| | | open func download(_ convertible: any URLConvertible, |
| | | method: HTTPMethod = .get, |
| | | parameters: Parameters? = nil, |
| | | encoding: ParameterEncoding = URLEncoding.default, |
| | | encoding: any ParameterEncoding = URLEncoding.default, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | requestModifier: RequestModifier? = nil, |
| | | to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { |
| | | let convertible = RequestConvertible(url: convertible, |
| | |
| | | /// should be moved. `nil` by default. |
| | | /// |
| | | /// - Returns: The created `DownloadRequest`. |
| | | open func download<Parameters: Encodable>(_ convertible: URLConvertible, |
| | | method: HTTPMethod = .get, |
| | | parameters: Parameters? = nil, |
| | | encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | requestModifier: RequestModifier? = nil, |
| | | to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { |
| | | open func download<Parameters: Encodable & Sendable>(_ convertible: any URLConvertible, |
| | | method: HTTPMethod = .get, |
| | | parameters: Parameters? = nil, |
| | | encoder: any ParameterEncoder = URLEncodedFormParameterEncoder.default, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | requestModifier: RequestModifier? = nil, |
| | | to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { |
| | | let convertible = RequestEncodableConvertible(url: convertible, |
| | | method: method, |
| | | parameters: parameters, |
| | |
| | | /// should be moved. `nil` by default. |
| | | /// |
| | | /// - Returns: The created `DownloadRequest`. |
| | | open func download(_ convertible: URLRequestConvertible, |
| | | interceptor: RequestInterceptor? = nil, |
| | | open func download(_ convertible: any URLRequestConvertible, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { |
| | | let request = DownloadRequest(downloadable: .request(convertible), |
| | | underlyingQueue: rootQueue, |
| | |
| | | /// |
| | | /// - Returns: The created `DownloadRequest`. |
| | | open func download(resumingWith data: Data, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { |
| | | let request = DownloadRequest(downloadable: .resumeData(data), |
| | | underlyingQueue: rootQueue, |
| | |
| | | // MARK: - UploadRequest |
| | | |
| | | struct ParameterlessRequestConvertible: URLRequestConvertible { |
| | | let url: URLConvertible |
| | | let url: any URLConvertible |
| | | let method: HTTPMethod |
| | | let headers: HTTPHeaders? |
| | | let requestModifier: RequestModifier? |
| | |
| | | } |
| | | |
| | | struct Upload: UploadConvertible { |
| | | let request: URLRequestConvertible |
| | | let uploadable: UploadableConvertible |
| | | let request: any URLRequestConvertible |
| | | let uploadable: any UploadableConvertible |
| | | |
| | | func createUploadable() throws -> UploadRequest.Uploadable { |
| | | try uploadable.createUploadable() |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(_ data: Data, |
| | | to convertible: URLConvertible, |
| | | to convertible: any URLConvertible, |
| | | method: HTTPMethod = .post, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default, |
| | | requestModifier: RequestModifier? = nil) -> UploadRequest { |
| | | let convertible = ParameterlessRequestConvertible(url: convertible, |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(_ data: Data, |
| | | with convertible: URLRequestConvertible, |
| | | interceptor: RequestInterceptor? = nil, |
| | | with convertible: any URLRequestConvertible, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default) -> UploadRequest { |
| | | upload(.data(data), with: convertible, interceptor: interceptor, fileManager: fileManager) |
| | | } |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(_ fileURL: URL, |
| | | to convertible: URLConvertible, |
| | | to convertible: any URLConvertible, |
| | | method: HTTPMethod = .post, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default, |
| | | requestModifier: RequestModifier? = nil) -> UploadRequest { |
| | | let convertible = ParameterlessRequestConvertible(url: convertible, |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(_ fileURL: URL, |
| | | with convertible: URLRequestConvertible, |
| | | interceptor: RequestInterceptor? = nil, |
| | | with convertible: any URLRequestConvertible, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default) -> UploadRequest { |
| | | upload(.file(fileURL, shouldRemove: false), with: convertible, interceptor: interceptor, fileManager: fileManager) |
| | | } |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(_ stream: InputStream, |
| | | to convertible: URLConvertible, |
| | | to convertible: any URLConvertible, |
| | | method: HTTPMethod = .post, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default, |
| | | requestModifier: RequestModifier? = nil) -> UploadRequest { |
| | | let convertible = ParameterlessRequestConvertible(url: convertible, |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(_ stream: InputStream, |
| | | with convertible: URLRequestConvertible, |
| | | interceptor: RequestInterceptor? = nil, |
| | | with convertible: any URLRequestConvertible, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default) -> UploadRequest { |
| | | upload(.stream(stream), with: convertible, interceptor: interceptor, fileManager: fileManager) |
| | | } |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(multipartFormData: @escaping (MultipartFormData) -> Void, |
| | | to url: URLConvertible, |
| | | to url: any URLConvertible, |
| | | usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, |
| | | method: HTTPMethod = .post, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default, |
| | | requestModifier: RequestModifier? = nil) -> UploadRequest { |
| | | let convertible = ParameterlessRequestConvertible(url: url, |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(multipartFormData: @escaping (MultipartFormData) -> Void, |
| | | with request: URLRequestConvertible, |
| | | with request: any URLRequestConvertible, |
| | | usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default) -> UploadRequest { |
| | | let formData = MultipartFormData(fileManager: fileManager) |
| | | multipartFormData(formData) |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(multipartFormData: MultipartFormData, |
| | | to url: URLConvertible, |
| | | to url: any URLConvertible, |
| | | usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, |
| | | method: HTTPMethod = .post, |
| | | headers: HTTPHeaders? = nil, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default, |
| | | requestModifier: RequestModifier? = nil) -> UploadRequest { |
| | | let convertible = ParameterlessRequestConvertible(url: url, |
| | |
| | | /// |
| | | /// - Returns: The created `UploadRequest`. |
| | | open func upload(multipartFormData: MultipartFormData, |
| | | with request: URLRequestConvertible, |
| | | with request: any URLRequestConvertible, |
| | | usingThreshold encodingMemoryThreshold: UInt64 = MultipartFormData.encodingMemoryThreshold, |
| | | interceptor: RequestInterceptor? = nil, |
| | | interceptor: (any RequestInterceptor)? = nil, |
| | | fileManager: FileManager = .default) -> UploadRequest { |
| | | let multipartUpload = MultipartUpload(encodingMemoryThreshold: encodingMemoryThreshold, |
| | | request: request, |
| | |
| | | // MARK: Uploadable |
| | | |
| | | func upload(_ uploadable: UploadRequest.Uploadable, |
| | | with convertible: URLRequestConvertible, |
| | | interceptor: RequestInterceptor?, |
| | | with convertible: any URLRequestConvertible, |
| | | interceptor: (any RequestInterceptor)?, |
| | | fileManager: FileManager) -> UploadRequest { |
| | | let uploadable = Upload(request: convertible, uploadable: uploadable) |
| | | |
| | | return upload(uploadable, interceptor: interceptor, fileManager: fileManager) |
| | | } |
| | | |
| | | func upload(_ upload: UploadConvertible, interceptor: RequestInterceptor?, fileManager: FileManager) -> UploadRequest { |
| | | func upload(_ upload: any UploadConvertible, interceptor: (any RequestInterceptor)?, fileManager: FileManager) -> UploadRequest { |
| | | let request = UploadRequest(convertible: upload, |
| | | underlyingQueue: rootQueue, |
| | | serializationQueue: serializationQueue, |
| | |
| | | } |
| | | |
| | | func performSetupOperations(for request: Request, |
| | | convertible: URLRequestConvertible, |
| | | shouldCreateTask: @escaping () -> Bool = { true }) { |
| | | convertible: any URLRequestConvertible, |
| | | shouldCreateTask: @escaping @Sendable () -> Bool = { true }) { |
| | | dispatchPrecondition(condition: .onQueue(requestQueue)) |
| | | |
| | | let initialRequest: URLRequest |
| | |
| | | |
| | | // MARK: - Adapters and Retriers |
| | | |
| | | func adapter(for request: Request) -> RequestAdapter? { |
| | | func adapter(for request: Request) -> (any RequestAdapter)? { |
| | | if let requestInterceptor = request.interceptor, let sessionInterceptor = interceptor { |
| | | return Interceptor(adapters: [requestInterceptor, sessionInterceptor]) |
| | | Interceptor(adapters: [requestInterceptor, sessionInterceptor]) |
| | | } else { |
| | | return request.interceptor ?? interceptor |
| | | request.interceptor ?? interceptor |
| | | } |
| | | } |
| | | |
| | | func retrier(for request: Request) -> RequestRetrier? { |
| | | func retrier(for request: Request) -> (any RequestRetrier)? { |
| | | if let requestInterceptor = request.interceptor, let sessionInterceptor = interceptor { |
| | | return Interceptor(retriers: [requestInterceptor, sessionInterceptor]) |
| | | Interceptor(retriers: [requestInterceptor, sessionInterceptor]) |
| | | } else { |
| | | return request.interceptor ?? interceptor |
| | | request.interceptor ?? interceptor |
| | | } |
| | | } |
| | | |
| | |
| | | activeRequests.remove(request) |
| | | } |
| | | |
| | | public func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) { |
| | | public func retryResult(for request: Request, dueTo error: AFError, completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | guard let retrier = retrier(for: request) else { |
| | | rootQueue.async { completion(.doNotRetry) } |
| | | return |
| | |
| | | |
| | | public func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) { |
| | | rootQueue.async { |
| | | let retry: () -> Void = { |
| | | let retry: @Sendable () -> Void = { |
| | | guard !request.isCancelled else { return } |
| | | |
| | | request.prepareForRetry() |
| | |
| | | session.configuration.urlCredentialStorage?.defaultCredential(for: protectionSpace) |
| | | } |
| | | |
| | | func cancelRequestsForSessionInvalidation(with error: Error?) { |
| | | func cancelRequestsForSessionInvalidation(with error: (any Error)?) { |
| | | dispatchPrecondition(condition: .onQueue(rootQueue)) |
| | | |
| | | requestTaskMap.requests.forEach { $0.finish(error: AFError.sessionInvalidated(error: error)) } |
| | |
| | | import Foundation |
| | | |
| | | /// Class which implements the various `URLSessionDelegate` methods to connect various Alamofire features. |
| | | open class SessionDelegate: NSObject { |
| | | open class SessionDelegate: NSObject, @unchecked Sendable { |
| | | private let fileManager: FileManager |
| | | |
| | | weak var stateProvider: SessionStateProvider? |
| | | var eventMonitor: EventMonitor? |
| | | weak var stateProvider: (any SessionStateProvider)? |
| | | var eventMonitor: (any EventMonitor)? |
| | | |
| | | /// Creates an instance from the given `FileManager`. |
| | | /// |
| | |
| | | } |
| | | |
| | | /// Type which provides various `Session` state values. |
| | | protocol SessionStateProvider: AnyObject { |
| | | protocol SessionStateProvider: AnyObject, Sendable { |
| | | var serverTrustManager: ServerTrustManager? { get } |
| | | var redirectHandler: RedirectHandler? { get } |
| | | var cachedResponseHandler: CachedResponseHandler? { get } |
| | | var redirectHandler: (any RedirectHandler)? { get } |
| | | var cachedResponseHandler: (any CachedResponseHandler)? { get } |
| | | |
| | | func request(for task: URLSessionTask) -> Request? |
| | | func didGatherMetricsForTask(_ task: URLSessionTask) |
| | | func didCompleteTask(_ task: URLSessionTask, completion: @escaping () -> Void) |
| | | func credential(for task: URLSessionTask, in protectionSpace: URLProtectionSpace) -> URLCredential? |
| | | func cancelRequestsForSessionInvalidation(with error: Error?) |
| | | func cancelRequestsForSessionInvalidation(with error: (any Error)?) |
| | | } |
| | | |
| | | // MARK: URLSessionDelegate |
| | | |
| | | extension SessionDelegate: URLSessionDelegate { |
| | | open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { |
| | | open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: (any Error)?) { |
| | | eventMonitor?.urlSession(session, didBecomeInvalidWithError: error) |
| | | |
| | | stateProvider?.cancelRequestsForSessionInvalidation(with: error) |
| | |
| | | stateProvider?.didGatherMetricsForTask(task) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { |
| | | open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { |
| | | // NSLog("URLSession: \(session), task: \(task), didCompleteWithError: \(error)") |
| | | eventMonitor?.urlSession(session, task: task, didCompleteWithError: error) |
| | | |
| | |
| | | open func urlSession(_ session: URLSession, |
| | | dataTask: URLSessionDataTask, |
| | | didReceive response: URLResponse, |
| | | completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { |
| | | completionHandler: @escaping @Sendable (URLSession.ResponseDisposition) -> Void) { |
| | | eventMonitor?.urlSession(session, dataTask: dataTask, didReceive: response) |
| | | |
| | | guard let response = response as? HTTPURLResponse else { completionHandler(.allow); return } |
| | |
| | | |
| | | /// Types adopting the `URLConvertible` protocol can be used to construct `URL`s, which can then be used to construct |
| | | /// `URLRequest`s. |
| | | public protocol URLConvertible { |
| | | public protocol URLConvertible: Sendable { |
| | | /// Returns a `URL` from the conforming instance or throws. |
| | | /// |
| | | /// - Returns: The `URL` created from the instance. |
| | |
| | | // MARK: - |
| | | |
| | | /// Types adopting the `URLRequestConvertible` protocol can be used to safely construct `URLRequest`s. |
| | | public protocol URLRequestConvertible { |
| | | public protocol URLRequestConvertible: Sendable { |
| | | /// Returns a `URLRequest` or throws if an `Error` was encountered. |
| | | /// |
| | | /// - Returns: A `URLRequest`. |
| | |
| | | /// - method: The `HTTPMethod`. |
| | | /// - headers: The `HTTPHeaders`, `nil` by default. |
| | | /// - Throws: Any error thrown while converting the `URLConvertible` to a `URL`. |
| | | public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws { |
| | | public init(url: any URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws { |
| | | let url = try url.asURL() |
| | | |
| | | self.init(url: url) |
| | |
| | | import Foundation |
| | | |
| | | /// `DataRequest` subclass which handles `Data` upload from memory, file, or stream using `URLSessionUploadTask`. |
| | | public final class UploadRequest: DataRequest { |
| | | public final class UploadRequest: DataRequest, @unchecked Sendable { |
| | | /// Type describing the origin of the upload, whether `Data`, file, or stream. |
| | | public enum Uploadable { |
| | | public enum Uploadable: @unchecked Sendable { // Must be @unchecked Sendable due to InputStream. |
| | | /// Upload from the provided `Data` value. |
| | | case data(Data) |
| | | /// Upload from the provided file `URL`, as well as a `Bool` determining whether the source file should be |
| | |
| | | // MARK: Initial State |
| | | |
| | | /// The `UploadableConvertible` value used to produce the `Uploadable` value for this instance. |
| | | public let upload: UploadableConvertible |
| | | public let upload: any UploadableConvertible |
| | | |
| | | /// `FileManager` used to perform cleanup tasks, including the removal of multipart form encoded payloads written |
| | | /// to disk. |
| | |
| | | /// encoded payloads written to disk. |
| | | /// - delegate: `RequestDelegate` that provides an interface to actions not performed by the `Request`. |
| | | init(id: UUID = UUID(), |
| | | convertible: UploadConvertible, |
| | | convertible: any UploadConvertible, |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | eventMonitor: (any EventMonitor)?, |
| | | interceptor: (any RequestInterceptor)?, |
| | | fileManager: FileManager, |
| | | delegate: RequestDelegate) { |
| | | delegate: any RequestDelegate) { |
| | | upload = convertible |
| | | self.fileManager = fileManager |
| | | |
| | |
| | | } |
| | | |
| | | /// A type that can produce an `UploadRequest.Uploadable` value. |
| | | public protocol UploadableConvertible { |
| | | public protocol UploadableConvertible: Sendable { |
| | | /// Produces an `UploadRequest.Uploadable` value from the instance. |
| | | /// |
| | | /// - Returns: The `UploadRequest.Uploadable`. |
| | |
| | | /// especially around adoption of the typed throws feature in Swift 6. Please report any missing features or |
| | | /// bugs to https://github.com/Alamofire/Alamofire/issues. |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | @_spi(WebSocket) public final class WebSocketRequest: Request { |
| | | @_spi(WebSocket) public final class WebSocketRequest: Request, @unchecked Sendable { |
| | | enum IncomingEvent { |
| | | case connected(protocol: String?) |
| | | case receivedMessage(URLSessionWebSocketTask.Message) |
| | |
| | | case completed(Completion) |
| | | } |
| | | |
| | | public struct Event<Success, Failure: Error> { |
| | | public enum Kind { |
| | | public struct Event<Success: Sendable, Failure: Error>: Sendable { |
| | | public enum Kind: Sendable { |
| | | case connected(protocol: String?) |
| | | case receivedMessage(Success) |
| | | case serializerFailed(Failure) |
| | |
| | | socket?.cancel() |
| | | } |
| | | |
| | | public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping (PingResponse) -> Void) { |
| | | public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping @Sendable (PingResponse) -> Void) { |
| | | socket?.sendPing(respondingOn: queue, onResponse: onResponse) |
| | | } |
| | | } |
| | | |
| | | public struct Completion { |
| | | public struct Completion: Sendable { |
| | | /// Last `URLRequest` issued by the instance. |
| | | public let request: URLRequest? |
| | | /// Last `HTTPURLResponse` received by the instance. |
| | |
| | | } |
| | | |
| | | /// Response to a sent ping. |
| | | public enum PingResponse { |
| | | public struct Pong { |
| | | public enum PingResponse: Sendable { |
| | | public struct Pong: Sendable { |
| | | let start: Date |
| | | let end: Date |
| | | let latency: TimeInterval |
| | |
| | | /// Received a pong with the associated state. |
| | | case pong(Pong) |
| | | /// Received an error. |
| | | case error(Error) |
| | | case error(any Error) |
| | | /// Did not send the ping, the request is cancelled or suspended. |
| | | case unsent |
| | | } |
| | |
| | | struct SocketMutableState { |
| | | var enqueuedSends: [(message: URLSessionWebSocketTask.Message, |
| | | queue: DispatchQueue, |
| | | completionHandler: (Result<Void, Error>) -> Void)] = [] |
| | | completionHandler: @Sendable (Result<Void, any Error>) -> Void)] = [] |
| | | var handlers: [(queue: DispatchQueue, handler: (_ event: IncomingEvent) -> Void)] = [] |
| | | var pingTimerItem: DispatchWorkItem? |
| | | } |
| | |
| | | task as? URLSessionWebSocketTask |
| | | } |
| | | |
| | | public let convertible: URLRequestConvertible |
| | | public let convertible: any URLRequestConvertible |
| | | public let configuration: Configuration |
| | | |
| | | init(id: UUID = UUID(), |
| | | convertible: URLRequestConvertible, |
| | | convertible: any URLRequestConvertible, |
| | | configuration: Configuration, |
| | | underlyingQueue: DispatchQueue, |
| | | serializationQueue: DispatchQueue, |
| | | eventMonitor: EventMonitor?, |
| | | interceptor: RequestInterceptor?, |
| | | delegate: RequestDelegate) { |
| | | eventMonitor: (any EventMonitor)?, |
| | | interceptor: (any RequestInterceptor)?, |
| | | delegate: any RequestDelegate) { |
| | | self.convertible = convertible |
| | | self.configuration = configuration |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping (PingResponse) -> Void) { |
| | | @preconcurrency |
| | | public func sendPing(respondingOn queue: DispatchQueue = .main, onResponse: @escaping @Sendable (PingResponse) -> Void) { |
| | | guard isResumed else { |
| | | queue.async { onResponse(.unsent) } |
| | | return |
| | |
| | | } |
| | | |
| | | let item = DispatchWorkItem { [weak self] in |
| | | guard let self, self.isResumed else { return } |
| | | guard let self, isResumed else { return } |
| | | |
| | | self.sendPing(respondingOn: self.underlyingQueue) { response in |
| | | sendPing(respondingOn: underlyingQueue) { response in |
| | | guard case .pong = response else { return } |
| | | |
| | | self.startAutomaticPing(every: pingInterval) |
| | |
| | | } |
| | | } |
| | | |
| | | #if swift(>=5.8) |
| | | @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) |
| | | func startAutomaticPing(every duration: Duration) { |
| | | let interval = TimeInterval(duration.components.seconds) + (Double(duration.components.attoseconds) / 1e18) |
| | | startAutomaticPing(every: interval) |
| | | } |
| | | #endif |
| | | |
| | | func cancelAutomaticPing() { |
| | | socketMutableState.write { mutableState in |
| | |
| | | } |
| | | } |
| | | |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func streamSerializer<Serializer>( |
| | | _ serializer: Serializer, |
| | | on queue: DispatchQueue = .main, |
| | | handler: @escaping (_ event: Event<Serializer.Output, Serializer.Failure>) -> Void |
| | | ) -> Self where Serializer: WebSocketMessageSerializer, Serializer.Failure == Error { |
| | | handler: @escaping @Sendable (_ event: Event<Serializer.Output, Serializer.Failure>) -> Void |
| | | ) -> Self where Serializer: WebSocketMessageSerializer, Serializer.Failure == any Error { |
| | | forIncomingEvent(on: queue) { incomingEvent in |
| | | let event: Event<Serializer.Output, Serializer.Failure> |
| | | switch incomingEvent { |
| | |
| | | } |
| | | } |
| | | |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func streamDecodableEvents<Value>( |
| | | _ type: Value.Type = Value.self, |
| | | on queue: DispatchQueue = .main, |
| | | using decoder: DataDecoder = JSONDecoder(), |
| | | handler: @escaping (_ event: Event<Value, Error>) -> Void |
| | | using decoder: any DataDecoder = JSONDecoder(), |
| | | handler: @escaping @Sendable (_ event: Event<Value, any Error>) -> Void |
| | | ) -> Self where Value: Decodable { |
| | | streamSerializer(DecodableWebSocketMessageDecoder<Value>(decoder: decoder), on: queue, handler: handler) |
| | | } |
| | | |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func streamDecodable<Value>( |
| | | _ type: Value.Type = Value.self, |
| | | on queue: DispatchQueue = .main, |
| | | using decoder: DataDecoder = JSONDecoder(), |
| | | handler: @escaping (_ value: Value) -> Void |
| | | ) -> Self where Value: Decodable { |
| | | using decoder: any DataDecoder = JSONDecoder(), |
| | | handler: @escaping @Sendable (_ value: Value) -> Void |
| | | ) -> Self where Value: Decodable & Sendable { |
| | | streamDecodableEvents(Value.self, on: queue) { event in |
| | | event.message.map(handler) |
| | | } |
| | | } |
| | | |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func streamMessageEvents( |
| | | on queue: DispatchQueue = .main, |
| | | handler: @escaping (_ event: Event<URLSessionWebSocketTask.Message, Never>) -> Void |
| | | handler: @escaping @Sendable (_ event: Event<URLSessionWebSocketTask.Message, Never>) -> Void |
| | | ) -> Self { |
| | | forIncomingEvent(on: queue) { incomingEvent in |
| | | let event: Event<URLSessionWebSocketTask.Message, Never> |
| | | switch incomingEvent { |
| | | let event: Event<URLSessionWebSocketTask.Message, Never> = switch incomingEvent { |
| | | case let .connected(`protocol`): |
| | | event = .init(socket: self, kind: .connected(protocol: `protocol`)) |
| | | .init(socket: self, kind: .connected(protocol: `protocol`)) |
| | | case let .receivedMessage(message): |
| | | event = .init(socket: self, kind: .receivedMessage(message)) |
| | | .init(socket: self, kind: .receivedMessage(message)) |
| | | case let .disconnected(closeCode, reason): |
| | | event = .init(socket: self, kind: .disconnected(closeCode: closeCode, reason: reason)) |
| | | .init(socket: self, kind: .disconnected(closeCode: closeCode, reason: reason)) |
| | | case let .completed(completion): |
| | | event = .init(socket: self, kind: .completed(completion)) |
| | | .init(socket: self, kind: .completed(completion)) |
| | | } |
| | | |
| | | queue.async { handler(event) } |
| | | } |
| | | } |
| | | |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func streamMessages( |
| | | on queue: DispatchQueue = .main, |
| | | handler: @escaping (_ message: URLSessionWebSocketTask.Message) -> Void |
| | | handler: @escaping @Sendable (_ message: URLSessionWebSocketTask.Message) -> Void |
| | | ) -> Self { |
| | | streamMessageEvents(on: queue) { event in |
| | | event.message.map(handler) |
| | | } |
| | | } |
| | | |
| | | func forIncomingEvent(on queue: DispatchQueue, handler: @escaping (IncomingEvent) -> Void) -> Self { |
| | | func forIncomingEvent(on queue: DispatchQueue, handler: @escaping @Sendable (IncomingEvent) -> Void) -> Self { |
| | | socketMutableState.write { state in |
| | | state.handlers.append((queue: queue, handler: { incomingEvent in |
| | | self.serializationQueue.async { |
| | |
| | | return self |
| | | } |
| | | |
| | | @preconcurrency |
| | | public func send(_ message: URLSessionWebSocketTask.Message, |
| | | queue: DispatchQueue = .main, |
| | | completionHandler: @escaping (Result<Void, Error>) -> Void) { |
| | | completionHandler: @escaping @Sendable (Result<Void, any Error>) -> Void) { |
| | | guard !(isCancelled || isFinished) else { return } |
| | | |
| | | guard let socket else { |
| | |
| | | } |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public protocol WebSocketMessageSerializer<Output, Failure> { |
| | | associatedtype Output |
| | | associatedtype Failure: Error = Error |
| | | public protocol WebSocketMessageSerializer<Output, Failure>: Sendable { |
| | | associatedtype Output: Sendable |
| | | associatedtype Failure: Error = any Error |
| | | |
| | | func decode(_ message: URLSessionWebSocketTask.Message) throws -> Output |
| | | } |
| | |
| | | } |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public struct DecodableWebSocketMessageDecoder<Value: Decodable>: WebSocketMessageSerializer { |
| | | public struct DecodableWebSocketMessageDecoder<Value: Decodable & Sendable>: WebSocketMessageSerializer { |
| | | public enum Error: Swift.Error { |
| | | case decoding(Swift.Error) |
| | | case decoding(any Swift.Error) |
| | | case unknownMessage(description: String) |
| | | } |
| | | |
| | | public let decoder: DataDecoder |
| | | public let decoder: any DataDecoder |
| | | |
| | | public init(decoder: DataDecoder) { |
| | | public init(decoder: any DataDecoder) { |
| | | self.decoder = decoder |
| | | } |
| | | |
| | |
| | | /// - Parameters: |
| | | /// - delay: `TimeInterval` to delay execution. |
| | | /// - closure: Closure to execute. |
| | | func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) { |
| | | func after(_ delay: TimeInterval, execute closure: @escaping @Sendable () -> Void) { |
| | | asyncAfter(deadline: .now() + delay, execute: closure) |
| | | } |
| | | } |
| | |
| | | /// |
| | | /// - returns: A `Result` containing the result of the given closure. If this instance is a failure, returns the |
| | | /// same failure. |
| | | func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> Result<NewSuccess, Error> { |
| | | func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> Result<NewSuccess, any Error> { |
| | | switch self { |
| | | case let .success(value): |
| | | do { |
| | |
| | | /// |
| | | /// - Returns: A `Result` instance containing the result of the transform. If this instance is a success, returns |
| | | /// the same success. |
| | | func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> Result<Success, Error> { |
| | | func tryMapError<NewFailure: Error>(_ transform: (Failure) throws -> NewFailure) -> Result<Success, any Error> { |
| | | switch self { |
| | | case let .failure(error): |
| | | do { |
| | |
| | | |
| | | /// Types adopting the `Authenticator` protocol can be used to authenticate `URLRequest`s with an |
| | | /// `AuthenticationCredential` as well as refresh the `AuthenticationCredential` when required. |
| | | public protocol Authenticator: AnyObject { |
| | | public protocol Authenticator: AnyObject, Sendable { |
| | | /// The type of credential associated with the `Authenticator` instance. |
| | | associatedtype Credential: AuthenticationCredential |
| | | associatedtype Credential: AuthenticationCredential & Sendable |
| | | |
| | | /// Applies the `Credential` to the `URLRequest`. |
| | | /// |
| | |
| | | /// - credential: The `Credential` to refresh. |
| | | /// - session: The `Session` requiring the refresh. |
| | | /// - completion: The closure to be executed once the refresh is complete. |
| | | func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void) |
| | | func refresh(_ credential: Credential, for session: Session, completion: @escaping @Sendable (Result<Credential, any Error>) -> Void) |
| | | |
| | | /// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`. |
| | | /// |
| | |
| | | /// - error: The `Error`. |
| | | /// |
| | | /// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise. |
| | | func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool |
| | | func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: any Error) -> Bool |
| | | |
| | | /// Determines whether the `URLRequest` is authenticated with the `Credential`. |
| | | /// |
| | |
| | | /// credential while the request was in flight. If it has already refreshed, then we don't need to trigger an |
| | | /// additional refresh. If it hasn't refreshed, then we need to refresh. |
| | | /// |
| | | /// Now that it is understood how the result of this method is used in the refresh lifecyle, let's walk through how |
| | | /// Now that it is understood how the result of this method is used in the refresh lifecycle, let's walk through how |
| | | /// to implement it. You should return `true` in this method if the `URLRequest` is authenticated in a way that |
| | | /// matches the values in the `Credential`. In the case of OAuth2, this would mean that the Bearer token in the |
| | | /// `Authorization` header of the `URLRequest` matches the access token in the `Credential`. If it matches, then we |
| | |
| | | |
| | | /// The `AuthenticationInterceptor` class manages the queuing and threading complexity of authenticating requests. |
| | | /// It relies on an `Authenticator` type to handle the actual `URLRequest` authentication and `Credential` refresh. |
| | | public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator { |
| | | public final class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor, Sendable where AuthenticatorType: Authenticator { |
| | | // MARK: Typealiases |
| | | |
| | | /// Type of credential used to authenticate requests. |
| | |
| | | private struct AdaptOperation { |
| | | let urlRequest: URLRequest |
| | | let session: Session |
| | | let completion: (Result<URLRequest, Error>) -> Void |
| | | let completion: @Sendable (Result<URLRequest, any Error>) -> Void |
| | | } |
| | | |
| | | private enum AdaptResult { |
| | |
| | | var refreshWindow: RefreshWindow? |
| | | |
| | | var adaptOperations: [AdaptOperation] = [] |
| | | var requestsToRetry: [(RetryResult) -> Void] = [] |
| | | var requestsToRetry: [@Sendable (RetryResult) -> Void] = [] |
| | | } |
| | | |
| | | // MARK: Properties |
| | |
| | | |
| | | // MARK: Adapt |
| | | |
| | | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | let adaptResult: AdaptResult = mutableState.write { mutableState in |
| | | // Queue the adapt operation if a refresh is already in place. |
| | | guard !mutableState.isRefreshing else { |
| | |
| | | |
| | | // MARK: Retry |
| | | |
| | | public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { |
| | | public func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | // Do not attempt retry if there was not an original request and response from the server. |
| | | guard let urlRequest = request.request, let response = request.response else { |
| | | completion(.doNotRetry) |
| | |
| | | } |
| | | } |
| | | |
| | | private func handleRefreshFailure(_ error: Error, insideLock mutableState: inout MutableState) { |
| | | private func handleRefreshFailure(_ error: any Error, insideLock mutableState: inout MutableState) { |
| | | let adaptOperations = mutableState.adaptOperations |
| | | let requestsToRetry = mutableState.requestsToRetry |
| | | |
| | |
| | | import Foundation |
| | | |
| | | /// A type that handles whether the data task should store the HTTP response in the cache. |
| | | public protocol CachedResponseHandler { |
| | | public protocol CachedResponseHandler: Sendable { |
| | | /// Determines whether the HTTP response should be stored in the cache. |
| | | /// |
| | | /// The `completion` closure should be passed one of three possible options: |
| | |
| | | /// response. |
| | | public struct ResponseCacher { |
| | | /// Defines the behavior of the `ResponseCacher` type. |
| | | public enum Behavior { |
| | | public enum Behavior: Sendable { |
| | | /// Stores the cached response in the cache. |
| | | case cache |
| | | /// Prevents the cached response from being stored in the cache. |
| | | case doNotCache |
| | | /// Modifies the cached response before storing it in the cache. |
| | | case modify((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?) |
| | | case modify(@Sendable (_ task: URLSessionDataTask, _ cachedResponse: CachedURLResponse) -> CachedURLResponse?) |
| | | } |
| | | |
| | | /// Returns a `ResponseCacher` with a `.cache` `Behavior`. |
| | |
| | | /// |
| | | /// - Parameter closure: Closure used to modify the `CachedURLResponse`. |
| | | /// - Returns: The `ResponseCacher`. |
| | | public static func modify(using closure: @escaping ((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)) -> ResponseCacher { |
| | | public static func modify(using closure: @escaping (@Sendable (URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)) -> ResponseCacher { |
| | | ResponseCacher(behavior: .modify(closure)) |
| | | } |
| | | } |
| | |
| | | |
| | | /// A Combine `Publisher` that publishes the `DataResponse<Value, AFError>` of the provided `DataRequest`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public struct DataResponsePublisher<Value>: Publisher { |
| | | public struct DataResponsePublisher<Value: Sendable>: Publisher { |
| | | public typealias Output = DataResponse<Value, AFError> |
| | | public typealias Failure = Never |
| | | |
| | | private typealias Handler = (@escaping (_ response: DataResponse<Value, AFError>) -> Void) -> DataRequest |
| | | private typealias Handler = (@escaping @Sendable (_ response: DataResponse<Value, AFError>) -> Void) -> DataRequest |
| | | |
| | | private let request: DataRequest |
| | | private let responseHandler: Handler |
| | |
| | | setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher() |
| | | } |
| | | |
| | | public func receive<S>(subscriber: S) where S: Subscriber, DataResponsePublisher.Failure == S.Failure, DataResponsePublisher.Output == S.Input { |
| | | public func receive<S>(subscriber: S) where S: Subscriber & Sendable, DataResponsePublisher.Failure == S.Failure, DataResponsePublisher.Output == S.Input { |
| | | subscriber.receive(subscription: Inner(request: request, |
| | | responseHandler: responseHandler, |
| | | downstream: subscriber)) |
| | | } |
| | | |
| | | private final class Inner<Downstream: Subscriber>: Subscription |
| | | private final class Inner<Downstream: Subscriber & Sendable>: Subscription |
| | | where Downstream.Input == Output { |
| | | typealias Failure = Downstream.Failure |
| | | |
| | |
| | | /// - Returns: The `DataResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishData(queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | preprocessor: any DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher<Data> { |
| | | publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor, |
| | |
| | | /// - Returns: The `DataResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishString(queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | preprocessor: any DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DataResponsePublisher<String> { |
| | |
| | | @available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).") |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | preprocessor: any DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyResponseMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DataResponsePublisher<T> { |
| | | publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, |
| | |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | preprocessor: any DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DataResponsePublisher<T> { |
| | | publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, |
| | |
| | | |
| | | // A Combine `Publisher` that publishes a sequence of `Stream<Value, AFError>` values received by the provided `DataStreamRequest`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public struct DataStreamPublisher<Value>: Publisher { |
| | | public struct DataStreamPublisher<Value: Sendable>: Publisher { |
| | | public typealias Output = DataStreamRequest.Stream<Value, AFError> |
| | | public typealias Failure = Never |
| | | |
| | |
| | | compactMap { stream in |
| | | switch stream.event { |
| | | case let .stream(result): |
| | | return result |
| | | result |
| | | // If the stream has completed with an error, send the error value downstream as a `.failure`. |
| | | case let .complete(completion): |
| | | return completion.error.map(Result.failure) |
| | | completion.error.map(Result.failure) |
| | | } |
| | | } |
| | | .eraseToAnyPublisher() |
| | |
| | | result().setFailureType(to: AFError.self).flatMap(\.publisher).eraseToAnyPublisher() |
| | | } |
| | | |
| | | public func receive<S>(subscriber: S) where S: Subscriber, DataStreamPublisher.Failure == S.Failure, DataStreamPublisher.Output == S.Input { |
| | | public func receive<S>(subscriber: S) where S: Subscriber & Sendable, DataStreamPublisher.Failure == S.Failure, DataStreamPublisher.Output == S.Input { |
| | | subscriber.receive(subscription: Inner(request: request, |
| | | streamHandler: streamHandler, |
| | | downstream: subscriber)) |
| | | } |
| | | |
| | | private final class Inner<Downstream: Subscriber>: Subscription |
| | | private final class Inner<Downstream: Subscriber & Sendable>: Subscription |
| | | where Downstream.Input == Output { |
| | | typealias Failure = Downstream.Failure |
| | | |
| | |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | preprocessor: DataPreprocessor = PassthroughPreprocessor()) -> DataStreamPublisher<T> { |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | preprocessor: any DataPreprocessor = PassthroughPreprocessor()) -> DataStreamPublisher<T> { |
| | | publishStream(using: DecodableStreamSerializer(decoder: decoder, |
| | | dataPreprocessor: preprocessor), |
| | | on: queue) |
| | |
| | | |
| | | /// A Combine `Publisher` that publishes the `DownloadResponse<Value, AFError>` of the provided `DownloadRequest`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public struct DownloadResponsePublisher<Value>: Publisher { |
| | | public struct DownloadResponsePublisher<Value: Sendable>: Publisher { |
| | | public typealias Output = DownloadResponse<Value, AFError> |
| | | public typealias Failure = Never |
| | | |
| | | private typealias Handler = (@escaping (_ response: DownloadResponse<Value, AFError>) -> Void) -> DownloadRequest |
| | | private typealias Handler = (@escaping @Sendable (_ response: DownloadResponse<Value, AFError>) -> Void) -> DownloadRequest |
| | | |
| | | private let request: DownloadRequest |
| | | private let responseHandler: Handler |
| | |
| | | setFailureType(to: AFError.self).flatMap(\.result.publisher).eraseToAnyPublisher() |
| | | } |
| | | |
| | | public func receive<S>(subscriber: S) where S: Subscriber, DownloadResponsePublisher.Failure == S.Failure, DownloadResponsePublisher.Output == S.Input { |
| | | public func receive<S>(subscriber: S) where S: Subscriber & Sendable, DownloadResponsePublisher.Failure == S.Failure, DownloadResponsePublisher.Output == S.Input { |
| | | subscriber.receive(subscription: Inner(request: request, |
| | | responseHandler: responseHandler, |
| | | downstream: subscriber)) |
| | | } |
| | | |
| | | private final class Inner<Downstream: Subscriber>: Subscription |
| | | private final class Inner<Downstream: Subscriber & Sendable>: Subscription |
| | | where Downstream.Input == Output { |
| | | typealias Failure = Downstream.Failure |
| | | |
| | |
| | | /// - Returns: The `DownloadResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishData(queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | preprocessor: any DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher<Data> { |
| | | publishResponse(using: DataResponseSerializer(dataPreprocessor: preprocessor, |
| | |
| | | /// - Returns: The `DownloadResponsePublisher`. |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishString(queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | preprocessor: any DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadResponsePublisher<String> { |
| | |
| | | @available(*, deprecated, message: "Renamed publishDecodable(type:queue:preprocessor:decoder:emptyResponseCodes:emptyRequestMethods).") |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | preprocessor: any DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyResponseMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DownloadResponsePublisher<T> { |
| | | publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, |
| | |
| | | @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) |
| | | public func publishDecodable<T: Decodable>(type: T.Type = T.self, |
| | | queue: DispatchQueue = .main, |
| | | preprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | preprocessor: any DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DownloadResponsePublisher<T> { |
| | | publishResponse(using: DecodableResponseSerializer(dataPreprocessor: preprocessor, |
| | |
| | | |
| | | /// Value used to `await` a `DataResponse` and associated values. |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public struct DataTask<Value> { |
| | | public struct DataTask<Value>: Sendable where Value: Sendable { |
| | | /// `DataResponse` produced by the `DataRequest` and its response handler. |
| | | public var response: DataResponse<Value, AFError> { |
| | | get async { |
| | | if shouldAutomaticallyCancel { |
| | | return await withTaskCancellationHandler { |
| | | await withTaskCancellationHandler { |
| | | await task.value |
| | | } onCancel: { |
| | | cancel() |
| | | } |
| | | } else { |
| | | return await task.value |
| | | await task.value |
| | | } |
| | | } |
| | | } |
| | |
| | | /// |
| | | /// - Returns: The `DataTask`. |
| | | public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataTask<Data> { |
| | | serializingResponse(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | |
| | | /// - Returns: The `DataTask`. |
| | | public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | dataPreprocessor: any DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<Value>.defaultEmptyRequestMethods) -> DataTask<Value> { |
| | | serializingResponse(using: DecodableResponseSerializer<Value>(dataPreprocessor: dataPreprocessor, |
| | |
| | | /// |
| | | /// - Returns: The `DataTask`. |
| | | public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DataTask<String> { |
| | |
| | | } |
| | | |
| | | private func dataTask<Value>(automaticallyCancelling shouldAutomaticallyCancel: Bool, |
| | | forResponse onResponse: @escaping (@escaping (DataResponse<Value, AFError>) -> Void) -> Void) |
| | | forResponse onResponse: @Sendable @escaping (@escaping @Sendable (DataResponse<Value, AFError>) -> Void) -> Void) |
| | | -> DataTask<Value> { |
| | | let task = Task { |
| | | await withTaskCancellationHandler { |
| | |
| | | |
| | | /// Value used to `await` a `DownloadResponse` and associated values. |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public struct DownloadTask<Value> { |
| | | public struct DownloadTask<Value>: Sendable where Value: Sendable { |
| | | /// `DownloadResponse` produced by the `DownloadRequest` and its response handler. |
| | | public var response: DownloadResponse<Value, AFError> { |
| | | get async { |
| | | if shouldAutomaticallyCancel { |
| | | return await withTaskCancellationHandler { |
| | | await withTaskCancellationHandler { |
| | | await task.value |
| | | } onCancel: { |
| | | cancel() |
| | | } |
| | | } else { |
| | | return await task.value |
| | | await task.value |
| | | } |
| | | } |
| | | } |
| | |
| | | /// |
| | | /// - Returns: The `DownloadTask`. |
| | | public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask<Data> { |
| | | serializingDownload(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | |
| | | /// - Returns: The `DownloadTask`. |
| | | public func serializingDecodable<Value: Decodable>(_ type: Value.Type = Value.self, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | dataPreprocessor: any DataPreprocessor = DecodableResponseSerializer<Value>.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<Value>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<Value>.defaultEmptyRequestMethods) -> DownloadTask<Value> { |
| | | serializingDownload(using: DecodableResponseSerializer<Value>(dataPreprocessor: dataPreprocessor, |
| | |
| | | /// |
| | | /// - Returns: The `DownloadTask`. |
| | | public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | dataPreprocessor: any DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask<String> { |
| | |
| | | } |
| | | |
| | | private func downloadTask<Value>(automaticallyCancelling shouldAutomaticallyCancel: Bool, |
| | | forResponse onResponse: @escaping (@escaping (DownloadResponse<Value, AFError>) -> Void) -> Void) |
| | | forResponse onResponse: @Sendable @escaping (@escaping @Sendable (DownloadResponse<Value, AFError>) -> Void) -> Void) |
| | | -> DownloadTask<Value> { |
| | | let task = Task { |
| | | await withTaskCancellationHandler { |
| | |
| | | // MARK: - DataStreamTask |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public struct DataStreamTask { |
| | | public struct DataStreamTask: Sendable { |
| | | // Type of created streams. |
| | | public typealias Stream<Success, Failure: Error> = StreamOf<DataStreamRequest.Stream<Success, Failure>> |
| | | |
| | |
| | | public func streamingDecodables<T>(_ type: T.Type = T.self, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | bufferingPolicy: Stream<T, AFError>.BufferingPolicy = .unbounded) |
| | | -> Stream<T, AFError> where T: Decodable { |
| | | -> Stream<T, AFError> where T: Decodable & Sendable { |
| | | streamingResponses(serializedUsing: DecodableStreamSerializer<T>(), |
| | | automaticallyCancelling: shouldAutomaticallyCancel, |
| | | bufferingPolicy: bufferingPolicy) |
| | |
| | | |
| | | private func createStream<Success, Failure: Error>(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | bufferingPolicy: Stream<Success, Failure>.BufferingPolicy = .unbounded, |
| | | forResponse onResponse: @escaping (@escaping (DataStreamRequest.Stream<Success, Failure>) -> Void) -> Void) |
| | | forResponse onResponse: @Sendable @escaping (@escaping @Sendable (DataStreamRequest.Stream<Success, Failure>) -> Void) -> Void) |
| | | -> Stream<Success, Failure> { |
| | | StreamOf(bufferingPolicy: bufferingPolicy) { |
| | | guard shouldAutomaticallyCancel, |
| | |
| | | // - MARK: WebSocketTask |
| | | |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | @_spi(WebSocket) public struct WebSocketTask { |
| | | @_spi(WebSocket) public struct WebSocketTask: Sendable { |
| | | private let request: WebSocketRequest |
| | | |
| | | fileprivate init(request: WebSocketRequest) { |
| | |
| | | } |
| | | } |
| | | |
| | | public func streamingDecodableEvents<Value: Decodable>( |
| | | public func streamingDecodableEvents<Value: Decodable & Sendable>( |
| | | _ type: Value.Type = Value.self, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | using decoder: DataDecoder = JSONDecoder(), |
| | | bufferingPolicy: EventStreamOf<Value, Error>.BufferingPolicy = .unbounded |
| | | ) -> EventStreamOf<Value, Error> { |
| | | using decoder: any DataDecoder = JSONDecoder(), |
| | | bufferingPolicy: EventStreamOf<Value, any Error>.BufferingPolicy = .unbounded |
| | | ) -> EventStreamOf<Value, any Error> { |
| | | createStream(automaticallyCancelling: shouldAutomaticallyCancel, |
| | | bufferingPolicy: bufferingPolicy, |
| | | transform: { $0 }) { onEvent in |
| | |
| | | } |
| | | } |
| | | |
| | | public func streamingDecodable<Value: Decodable>( |
| | | public func streamingDecodable<Value: Decodable & Sendable>( |
| | | _ type: Value.Type = Value.self, |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool = true, |
| | | using decoder: DataDecoder = JSONDecoder(), |
| | | using decoder: any DataDecoder = JSONDecoder(), |
| | | bufferingPolicy: StreamOf<Value>.BufferingPolicy = .unbounded |
| | | ) -> StreamOf<Value> { |
| | | createStream(automaticallyCancelling: shouldAutomaticallyCancel, |
| | |
| | | private func createStream<Success, Value, Failure: Error>( |
| | | automaticallyCancelling shouldAutomaticallyCancel: Bool, |
| | | bufferingPolicy: StreamOf<Value>.BufferingPolicy, |
| | | transform: @escaping (WebSocketRequest.Event<Success, Failure>) -> Value?, |
| | | forResponse onResponse: @escaping (@escaping (WebSocketRequest.Event<Success, Failure>) -> Void) -> Void |
| | | transform: @escaping @Sendable (WebSocketRequest.Event<Success, Failure>) -> Value?, |
| | | forResponse onResponse: @Sendable @escaping (@escaping @Sendable (WebSocketRequest.Event<Success, Failure>) -> Void) -> Void |
| | | ) -> StreamOf<Value> { |
| | | StreamOf(bufferingPolicy: bufferingPolicy) { |
| | | guard shouldAutomaticallyCancel, |
| | |
| | | |
| | | /// Protocol outlining the lifetime events inside Alamofire. It includes both events received from the various |
| | | /// `URLSession` delegate protocols as well as various events from the lifetime of `Request` and its subclasses. |
| | | public protocol EventMonitor { |
| | | public protocol EventMonitor: Sendable { |
| | | /// The `DispatchQueue` onto which Alamofire's root `CompositeEventMonitor` will dispatch events. `.main` by default. |
| | | var queue: DispatchQueue { get } |
| | | |
| | |
| | | // MARK: URLSessionDelegate Events |
| | | |
| | | /// Event called during `URLSessionDelegate`'s `urlSession(_:didBecomeInvalidWithError:)` method. |
| | | func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) |
| | | func urlSession(_ session: URLSession, didBecomeInvalidWithError error: (any Error)?) |
| | | |
| | | // MARK: URLSessionTaskDelegate Events |
| | | |
| | |
| | | func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) |
| | | |
| | | /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:task:didCompleteWithError:)` method. |
| | | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) |
| | | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) |
| | | |
| | | /// Event called during `URLSessionTaskDelegate`'s `urlSession(_:taskIsWaitingForConnectivity:)` method. |
| | | func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) |
| | |
| | | func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) |
| | | |
| | | /// Event called when a `DataRequest` calls a `ResponseSerializer` and creates a generic `DataResponse<Value, AFError>`. |
| | | func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) |
| | | func request<Value: Sendable>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) |
| | | |
| | | // MARK: DataStreamRequest Events |
| | | |
| | |
| | | /// - Parameters: |
| | | /// - request: `DataStreamRequest` for which the value was serialized. |
| | | /// - result: `Result` of the serialization attempt. |
| | | func request<Value>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) |
| | | func request<Value: Sendable>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) |
| | | |
| | | // MARK: UploadRequest Events |
| | | |
| | |
| | | func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) |
| | | |
| | | /// Event called when a `DownloadRequest` calls a `DownloadResponseSerializer` and creates a generic `DownloadResponse<Value, AFError>` |
| | | func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) |
| | | func request<Value: Sendable>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) |
| | | } |
| | | |
| | | extension EventMonitor { |
| | |
| | | |
| | | // MARK: Default Implementations |
| | | |
| | | public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {} |
| | | public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: (any Error)?) {} |
| | | public func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | didReceive challenge: URLAuthenticationChallenge) {} |
| | |
| | | public func urlSession(_ session: URLSession, |
| | | task: URLSessionTask, |
| | | didFinishCollecting metrics: URLSessionTaskMetrics) {} |
| | | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {} |
| | | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) {} |
| | | public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {} |
| | | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) {} |
| | | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {} |
| | |
| | | data: Data?, |
| | | withResult result: Request.ValidationResult) {} |
| | | public func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) {} |
| | | public func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {} |
| | | public func request<Value: Sendable>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {} |
| | | public func request(_ request: DataStreamRequest, |
| | | didValidateRequest urlRequest: URLRequest?, |
| | | response: HTTPURLResponse, |
| | | withResult result: Request.ValidationResult) {} |
| | | public func request<Value>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) {} |
| | | public func request<Value: Sendable>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) {} |
| | | public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) {} |
| | | public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: AFError) {} |
| | | public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) {} |
| | |
| | | fileURL: URL?, |
| | | withResult result: Request.ValidationResult) {} |
| | | public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse<URL?, AFError>) {} |
| | | public func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) {} |
| | | public func request<Value: Sendable>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) {} |
| | | } |
| | | |
| | | /// An `EventMonitor` which can contain multiple `EventMonitor`s and calls their methods on their queues. |
| | | public final class CompositeEventMonitor: EventMonitor { |
| | | public let queue = DispatchQueue(label: "org.alamofire.compositeEventMonitor") |
| | | |
| | | let monitors: [EventMonitor] |
| | | let monitors: Protected<[any EventMonitor]> |
| | | |
| | | init(monitors: [EventMonitor]) { |
| | | self.monitors = monitors |
| | | init(monitors: [any EventMonitor]) { |
| | | self.monitors = Protected(monitors) |
| | | } |
| | | |
| | | func performEvent(_ event: @escaping (EventMonitor) -> Void) { |
| | | func performEvent(_ event: @escaping @Sendable (any EventMonitor) -> Void) { |
| | | queue.async { |
| | | for monitor in self.monitors { |
| | | monitor.queue.async { event(monitor) } |
| | | self.monitors.read { monitors in |
| | | for monitor in monitors { |
| | | monitor.queue.async { event(monitor) } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { |
| | | public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: (any Error)?) { |
| | | performEvent { $0.urlSession(session, didBecomeInvalidWithError: error) } |
| | | } |
| | | |
| | |
| | | performEvent { $0.urlSession(session, task: task, didFinishCollecting: metrics) } |
| | | } |
| | | |
| | | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { |
| | | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { |
| | | performEvent { $0.urlSession(session, task: task, didCompleteWithError: error) } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | public func request<Value>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) { |
| | | public func request<Value: Sendable>(_ request: DataStreamRequest, didParseStream result: Result<Value, AFError>) { |
| | | performEvent { $0.request(request, didParseStream: result) } |
| | | } |
| | | |
| | |
| | | performEvent { $0.request(request, didParseResponse: response) } |
| | | } |
| | | |
| | | public func request<Value>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) { |
| | | public func request<Value: Sendable>(_ request: DownloadRequest, didParseResponse response: DownloadResponse<Value, AFError>) { |
| | | performEvent { $0.request(request, didParseResponse: response) } |
| | | } |
| | | } |
| | | |
| | | /// `EventMonitor` that allows optional closures to be set to receive events. |
| | | open class ClosureEventMonitor: EventMonitor { |
| | | open class ClosureEventMonitor: EventMonitor, @unchecked Sendable { |
| | | /// Closure called on the `urlSession(_:didBecomeInvalidWithError:)` event. |
| | | open var sessionDidBecomeInvalidWithError: ((URLSession, Error?) -> Void)? |
| | | open var sessionDidBecomeInvalidWithError: ((URLSession, (any Error)?) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:task:didReceive:completionHandler:)`. |
| | | open var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> Void)? |
| | |
| | | open var taskDidFinishCollectingMetrics: ((URLSession, URLSessionTask, URLSessionTaskMetrics) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:task:didCompleteWithError:)` event. |
| | | open var taskDidComplete: ((URLSession, URLSessionTask, Error?) -> Void)? |
| | | open var taskDidComplete: ((URLSession, URLSessionTask, (any Error)?) -> Void)? |
| | | |
| | | /// Closure called on the `urlSession(_:taskIsWaitingForConnectivity:)` event. |
| | | open var taskIsWaitingForConnectivity: ((URLSession, URLSessionTask) -> Void)? |
| | |
| | | self.queue = queue |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { |
| | | open func urlSession(_ session: URLSession, didBecomeInvalidWithError error: (any Error)?) { |
| | | sessionDidBecomeInvalidWithError?(session, error) |
| | | } |
| | | |
| | |
| | | taskDidFinishCollectingMetrics?(session, task, metrics) |
| | | } |
| | | |
| | | open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { |
| | | open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { |
| | | taskDidComplete?(session, task, error) |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data { |
| | | let boundaryText: String |
| | | |
| | | switch boundaryType { |
| | | let boundaryText = switch boundaryType { |
| | | case .initial: |
| | | boundaryText = "--\(boundary)\(EncodingCharacters.crlf)" |
| | | "--\(boundary)\(EncodingCharacters.crlf)" |
| | | case .encapsulated: |
| | | boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)" |
| | | "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)" |
| | | case .final: |
| | | boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)" |
| | | "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)" |
| | | } |
| | | |
| | | return Data(boundaryText.utf8) |
| | |
| | | |
| | | private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { |
| | | if bodyPart.hasFinalBoundary { |
| | | return try write(finalBoundaryData(), to: outputStream) |
| | | try write(finalBoundaryData(), to: outputStream) |
| | | } |
| | | } |
| | | |
| | |
| | | // MARK: - Private - Mime Type |
| | | |
| | | private func mimeType(forPathExtension pathExtension: String) -> String { |
| | | #if swift(>=5.9) |
| | | if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) { |
| | | return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream" |
| | | } else { |
| | |
| | | |
| | | return "application/octet-stream" |
| | | } |
| | | #else |
| | | if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) { |
| | | return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream" |
| | | } else { |
| | | if |
| | | let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), |
| | | let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { |
| | | return contentType as String |
| | | } |
| | | |
| | | return "application/octet-stream" |
| | | } |
| | | #endif |
| | | } |
| | | } |
| | | |
| | |
| | | import Foundation |
| | | |
| | | /// Internal type which encapsulates a `MultipartFormData` upload. |
| | | final class MultipartUpload { |
| | | lazy var result = Result { try build() } |
| | | final class MultipartUpload: @unchecked Sendable { // Must be @unchecked due to FileManager not being properly Sendable. |
| | | private let _result = Protected<Result<UploadRequest.Uploadable, any Error>?>(nil) |
| | | var result: Result<UploadRequest.Uploadable, any Error> { |
| | | if let value = _result.read({ $0 }) { |
| | | return value |
| | | } else { |
| | | let result = Result { try build() } |
| | | _result.write(result) |
| | | |
| | | return result |
| | | } |
| | | } |
| | | |
| | | private let multipartFormData: Protected<MultipartFormData> |
| | | |
| | | let encodingMemoryThreshold: UInt64 |
| | | let request: URLRequestConvertible |
| | | let request: any URLRequestConvertible |
| | | let fileManager: FileManager |
| | | |
| | | init(encodingMemoryThreshold: UInt64, |
| | | request: URLRequestConvertible, |
| | | request: any URLRequestConvertible, |
| | | multipartFormData: MultipartFormData) { |
| | | self.encodingMemoryThreshold = encodingMemoryThreshold |
| | | self.request = request |
| | |
| | | /// Reachability can be used to determine background information about why a network operation failed, or to retry |
| | | /// network requests when a connection is established. It should not be used to prevent a user from initiating a network |
| | | /// request, as it's possible that an initial request may be required to establish reachability. |
| | | open class NetworkReachabilityManager { |
| | | open class NetworkReachabilityManager: @unchecked Sendable { |
| | | /// Defines the various states of network reachability. |
| | | public enum NetworkReachabilityStatus { |
| | | public enum NetworkReachabilityStatus: Sendable { |
| | | /// It is unknown whether the network is reachable. |
| | | case unknown |
| | | /// The network is not reachable. |
| | |
| | | } |
| | | |
| | | /// Defines the various connection types detected by reachability flags. |
| | | public enum ConnectionType { |
| | | public enum ConnectionType: Sendable { |
| | | /// The connection type is either over Ethernet or WiFi. |
| | | case ethernetOrWiFi |
| | | /// The connection type is a cellular connection. |
| | |
| | | |
| | | /// A closure executed when the network reachability status changes. The closure takes a single argument: the |
| | | /// network reachability status. |
| | | public typealias Listener = (NetworkReachabilityStatus) -> Void |
| | | public typealias Listener = @Sendable (NetworkReachabilityStatus) -> Void |
| | | |
| | | /// Default `NetworkReachabilityManager` for the zero address and a `listenerQueue` of `.main`. |
| | | public static let `default` = NetworkReachabilityManager() |
| | |
| | | /// - listener: `Listener` closure called when reachability changes. |
| | | /// |
| | | /// - Returns: `true` if listening was started successfully, `false` otherwise. |
| | | @preconcurrency |
| | | @discardableResult |
| | | open func startListening(onQueue queue: DispatchQueue = .main, |
| | | onUpdatePerforming listener: @escaping Listener) -> Bool { |
| | |
| | | func notifyListener(_ flags: SCNetworkReachabilityFlags) { |
| | | let newStatus = NetworkReachabilityStatus(flags) |
| | | |
| | | mutableState.write { state in |
| | | mutableState.write { [newStatus] state in |
| | | guard state.previousStatus != newStatus else { return } |
| | | |
| | | state.previousStatus = newStatus |
| | |
| | | import Foundation |
| | | |
| | | /// A type that handles how an HTTP redirect response from a remote server should be redirected to the new request. |
| | | public protocol RedirectHandler { |
| | | public protocol RedirectHandler: Sendable { |
| | | /// Determines how the HTTP redirect response should be redirected to the new request. |
| | | /// |
| | | /// The `completion` closure should be passed one of three possible options: |
| | |
| | | /// `Redirector` is a convenience `RedirectHandler` making it easy to follow, not follow, or modify a redirect. |
| | | public struct Redirector { |
| | | /// Defines the behavior of the `Redirector` type. |
| | | public enum Behavior { |
| | | public enum Behavior: Sendable { |
| | | /// Follow the redirect as defined in the response. |
| | | case follow |
| | | /// Do not follow the redirect defined in the response. |
| | | case doNotFollow |
| | | /// Modify the redirect request defined in the response. |
| | | case modify((URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) |
| | | case modify(@Sendable (_ task: URLSessionTask, _ request: URLRequest, _ response: HTTPURLResponse) -> URLRequest?) |
| | | } |
| | | |
| | | /// Returns a `Redirector` with a `.follow` `Behavior`. |
| | |
| | | /// |
| | | /// - Parameter closure: Closure used to modify the redirect. |
| | | /// - Returns: The `Redirector`. |
| | | public static func modify(using closure: @escaping (URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) -> Redirector { |
| | | public static func modify(using closure: @escaping @Sendable (URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?) -> Redirector { |
| | | Redirector(behavior: .modify(closure)) |
| | | } |
| | | } |
| | |
| | | // THE SOFTWARE. |
| | | // |
| | | |
| | | #if canImport(zlib) |
| | | #if canImport(zlib) && !os(Android) |
| | | import Foundation |
| | | import zlib |
| | | |
| | |
| | | /// want to use a dedicated `requestQueue` in your `Session` instance. Finally, not all servers support request |
| | | /// compression, so test with all of your server configurations before deploying. |
| | | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) |
| | | public struct DeflateRequestCompressor: RequestInterceptor { |
| | | public struct DeflateRequestCompressor: Sendable, RequestInterceptor { |
| | | /// Type that determines the action taken when the `URLRequest` already has a `Content-Encoding` header. |
| | | public enum DuplicateHeaderBehavior { |
| | | public enum DuplicateHeaderBehavior: Sendable { |
| | | /// Throws a `DuplicateHeaderError`. The default. |
| | | case error |
| | | /// Replaces the existing header value with `deflate`. |
| | |
| | | /// Behavior to use when the outgoing `URLRequest` already has a `Content-Encoding` header. |
| | | public let duplicateHeaderBehavior: DuplicateHeaderBehavior |
| | | /// Closure which determines whether the outgoing body data should be compressed. |
| | | public let shouldCompressBodyData: (_ bodyData: Data) -> Bool |
| | | public let shouldCompressBodyData: @Sendable (_ bodyData: Data) -> Bool |
| | | |
| | | /// Creates an instance with the provided parameters. |
| | | /// |
| | |
| | | /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use. `.error` by default. |
| | | /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default. |
| | | public init(duplicateHeaderBehavior: DuplicateHeaderBehavior = .error, |
| | | shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true }) { |
| | | shouldCompressBodyData: @escaping @Sendable (_ bodyData: Data) -> Bool = { _ in true }) { |
| | | self.duplicateHeaderBehavior = duplicateHeaderBehavior |
| | | self.shouldCompressBodyData = shouldCompressBodyData |
| | | } |
| | | |
| | | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, any Error>) -> Void) { |
| | | // No need to compress unless we have body data. No support for compressing streams. |
| | | guard let bodyData = urlRequest.httpBody else { |
| | | completion(.success(urlRequest)) |
| | |
| | | /// - Returns: The `DeflateRequestCompressor`. |
| | | public static func deflateCompressor( |
| | | duplicateHeaderBehavior: DeflateRequestCompressor.DuplicateHeaderBehavior = .error, |
| | | shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true } |
| | | shouldCompressBodyData: @escaping @Sendable (_ bodyData: Data) -> Bool = { _ in true } |
| | | ) -> DeflateRequestCompressor { |
| | | DeflateRequestCompressor(duplicateHeaderBehavior: duplicateHeaderBehavior, |
| | | shouldCompressBodyData: shouldCompressBodyData) |
| | |
| | | import Foundation |
| | | |
| | | /// Stores all state associated with a `URLRequest` being adapted. |
| | | public struct RequestAdapterState { |
| | | public struct RequestAdapterState: Sendable { |
| | | /// The `UUID` of the `Request` associated with the `URLRequest` to adapt. |
| | | public let requestID: UUID |
| | | |
| | |
| | | // MARK: - |
| | | |
| | | /// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary. |
| | | public protocol RequestAdapter { |
| | | public protocol RequestAdapter: Sendable { |
| | | /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result. |
| | | /// |
| | | /// - Parameters: |
| | | /// - urlRequest: The `URLRequest` to adapt. |
| | | /// - session: The `Session` that will execute the `URLRequest`. |
| | | /// - completion: The completion handler that must be called when adaptation is complete. |
| | | func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) |
| | | func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (_ result: Result<URLRequest, any Error>) -> Void) |
| | | |
| | | /// Inspects and adapts the specified `URLRequest` in some manner and calls the completion handler with the Result. |
| | | /// |
| | |
| | | /// - urlRequest: The `URLRequest` to adapt. |
| | | /// - state: The `RequestAdapterState` associated with the `URLRequest`. |
| | | /// - completion: The completion handler that must be called when adaptation is complete. |
| | | func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) |
| | | func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping @Sendable (_ result: Result<URLRequest, any Error>) -> Void) |
| | | } |
| | | |
| | | extension RequestAdapter { |
| | | public func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | public func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping @Sendable (_ result: Result<URLRequest, any Error>) -> Void) { |
| | | adapt(urlRequest, for: state.session, completion: completion) |
| | | } |
| | | } |
| | |
| | | // MARK: - |
| | | |
| | | /// Outcome of determination whether retry is necessary. |
| | | public enum RetryResult { |
| | | public enum RetryResult: Sendable { |
| | | /// Retry should be attempted immediately. |
| | | case retry |
| | | /// Retry should be attempted after the associated `TimeInterval`. |
| | |
| | | /// Do not retry. |
| | | case doNotRetry |
| | | /// Do not retry due to the associated `Error`. |
| | | case doNotRetryWithError(Error) |
| | | case doNotRetryWithError(any Error) |
| | | } |
| | | |
| | | extension RetryResult { |
| | | var retryRequired: Bool { |
| | | switch self { |
| | | case .retry, .retryWithDelay: return true |
| | | default: return false |
| | | case .retry, .retryWithDelay: true |
| | | default: false |
| | | } |
| | | } |
| | | |
| | | var delay: TimeInterval? { |
| | | switch self { |
| | | case let .retryWithDelay(delay): return delay |
| | | default: return nil |
| | | case let .retryWithDelay(delay): delay |
| | | default: nil |
| | | } |
| | | } |
| | | |
| | | var error: Error? { |
| | | var error: (any Error)? { |
| | | guard case let .doNotRetryWithError(error) = self else { return nil } |
| | | return error |
| | | } |
| | |
| | | |
| | | /// A type that determines whether a request should be retried after being executed by the specified session manager |
| | | /// and encountering an error. |
| | | public protocol RequestRetrier { |
| | | public protocol RequestRetrier: Sendable { |
| | | /// Determines whether the `Request` should be retried by calling the `completion` closure. |
| | | /// |
| | | /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs |
| | |
| | | /// - session: `Session` that produced the `Request`. |
| | | /// - error: `Error` encountered while executing the `Request`. |
| | | /// - completion: Completion closure to be executed when a retry decision has been determined. |
| | | func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) |
| | | func retry(_ request: Request, for session: Session, dueTo error: any Error, completion: @escaping @Sendable (RetryResult) -> Void) |
| | | } |
| | | |
| | | // MARK: - |
| | |
| | | public protocol RequestInterceptor: RequestAdapter, RequestRetrier {} |
| | | |
| | | extension RequestInterceptor { |
| | | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | completion(.success(urlRequest)) |
| | | } |
| | | |
| | | @preconcurrency |
| | | public func retry(_ request: Request, |
| | | for session: Session, |
| | | dueTo error: Error, |
| | | completion: @escaping (RetryResult) -> Void) { |
| | | dueTo error: any Error, |
| | | completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | completion(.doNotRetry) |
| | | } |
| | | } |
| | | |
| | | /// `RequestAdapter` closure definition. |
| | | public typealias AdaptHandler = (URLRequest, Session, _ completion: @escaping (Result<URLRequest, Error>) -> Void) -> Void |
| | | public typealias AdaptHandler = @Sendable (_ request: URLRequest, |
| | | _ session: Session, |
| | | _ completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) -> Void |
| | | |
| | | /// `RequestRetrier` closure definition. |
| | | public typealias RetryHandler = (Request, Session, Error, _ completion: @escaping (RetryResult) -> Void) -> Void |
| | | public typealias RetryHandler = @Sendable (_ request: Request, |
| | | _ session: Session, |
| | | _ error: any Error, |
| | | _ completion: @escaping @Sendable (RetryResult) -> Void) -> Void |
| | | |
| | | // MARK: - |
| | | |
| | | /// Closure-based `RequestAdapter`. |
| | | open class Adapter: RequestInterceptor { |
| | | open class Adapter: @unchecked Sendable, RequestInterceptor { |
| | | private let adaptHandler: AdaptHandler |
| | | |
| | | /// Creates an instance using the provided closure. |
| | | /// |
| | | /// - Parameter adaptHandler: `AdaptHandler` closure to be executed when handling request adaptation. |
| | | /// |
| | | @preconcurrency |
| | | public init(_ adaptHandler: @escaping AdaptHandler) { |
| | | self.adaptHandler = adaptHandler |
| | | } |
| | | |
| | | open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | adaptHandler(urlRequest, session, completion) |
| | | } |
| | | |
| | | open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | adaptHandler(urlRequest, state.session, completion) |
| | | } |
| | | } |
| | |
| | | /// |
| | | /// - Parameter closure: `AdaptHandler` to use to adapt the request. |
| | | /// - Returns: The `Adapter`. |
| | | @preconcurrency |
| | | public static func adapter(using closure: @escaping AdaptHandler) -> Adapter { |
| | | Adapter(closure) |
| | | } |
| | |
| | | // MARK: - |
| | | |
| | | /// Closure-based `RequestRetrier`. |
| | | open class Retrier: RequestInterceptor { |
| | | open class Retrier: @unchecked Sendable, RequestInterceptor { |
| | | private let retryHandler: RetryHandler |
| | | |
| | | /// Creates an instance using the provided closure. |
| | | /// |
| | | /// - Parameter retryHandler: `RetryHandler` closure to be executed when handling request retry. |
| | | @preconcurrency |
| | | public init(_ retryHandler: @escaping RetryHandler) { |
| | | self.retryHandler = retryHandler |
| | | } |
| | | |
| | | @preconcurrency |
| | | open func retry(_ request: Request, |
| | | for session: Session, |
| | | dueTo error: Error, |
| | | completion: @escaping (RetryResult) -> Void) { |
| | | dueTo error: any Error, |
| | | completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | retryHandler(request, session, error, completion) |
| | | } |
| | | } |
| | |
| | | /// |
| | | /// - Parameter closure: `RetryHandler` to use to retry the request. |
| | | /// - Returns: The `Retrier`. |
| | | @preconcurrency |
| | | public static func retrier(using closure: @escaping RetryHandler) -> Retrier { |
| | | Retrier(closure) |
| | | } |
| | |
| | | // MARK: - |
| | | |
| | | /// `RequestInterceptor` which can use multiple `RequestAdapter` and `RequestRetrier` values. |
| | | open class Interceptor: RequestInterceptor { |
| | | open class Interceptor: @unchecked Sendable, RequestInterceptor { |
| | | /// All `RequestAdapter`s associated with the instance. These adapters will be run until one fails. |
| | | public let adapters: [RequestAdapter] |
| | | public let adapters: [any RequestAdapter] |
| | | /// All `RequestRetrier`s associated with the instance. These retriers will be run one at a time until one triggers retry. |
| | | public let retriers: [RequestRetrier] |
| | | public let retriers: [any RequestRetrier] |
| | | |
| | | /// Creates an instance from `AdaptHandler` and `RetryHandler` closures. |
| | | /// |
| | |
| | | /// - Parameters: |
| | | /// - adapter: `RequestAdapter` value to be used. |
| | | /// - retrier: `RequestRetrier` value to be used. |
| | | public init(adapter: RequestAdapter, retrier: RequestRetrier) { |
| | | public init(adapter: any RequestAdapter, retrier: any RequestRetrier) { |
| | | adapters = [adapter] |
| | | retriers = [retrier] |
| | | } |
| | |
| | | /// - adapters: `RequestAdapter` values to be used. |
| | | /// - retriers: `RequestRetrier` values to be used. |
| | | /// - interceptors: `RequestInterceptor`s to be used. |
| | | public init(adapters: [RequestAdapter] = [], retriers: [RequestRetrier] = [], interceptors: [RequestInterceptor] = []) { |
| | | public init(adapters: [any RequestAdapter] = [], |
| | | retriers: [any RequestRetrier] = [], |
| | | interceptors: [any RequestInterceptor] = []) { |
| | | self.adapters = adapters + interceptors |
| | | self.retriers = retriers + interceptors |
| | | } |
| | | |
| | | open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | open func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | adapt(urlRequest, for: session, using: adapters, completion: completion) |
| | | } |
| | | |
| | | private func adapt(_ urlRequest: URLRequest, |
| | | for session: Session, |
| | | using adapters: [RequestAdapter], |
| | | completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | using adapters: [any RequestAdapter], |
| | | completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | var pendingAdapters = adapters |
| | | |
| | | guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return } |
| | | |
| | | let adapter = pendingAdapters.removeFirst() |
| | | |
| | | adapter.adapt(urlRequest, for: session) { result in |
| | | adapter.adapt(urlRequest, for: session) { [pendingAdapters] result in |
| | | switch result { |
| | | case let .success(urlRequest): |
| | | self.adapt(urlRequest, for: session, using: pendingAdapters, completion: completion) |
| | |
| | | } |
| | | } |
| | | |
| | | open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | @preconcurrency |
| | | open func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | adapt(urlRequest, using: state, adapters: adapters, completion: completion) |
| | | } |
| | | |
| | | private func adapt(_ urlRequest: URLRequest, |
| | | using state: RequestAdapterState, |
| | | adapters: [RequestAdapter], |
| | | completion: @escaping (Result<URLRequest, Error>) -> Void) { |
| | | adapters: [any RequestAdapter], |
| | | completion: @escaping @Sendable (Result<URLRequest, any Error>) -> Void) { |
| | | var pendingAdapters = adapters |
| | | |
| | | guard !pendingAdapters.isEmpty else { completion(.success(urlRequest)); return } |
| | | |
| | | let adapter = pendingAdapters.removeFirst() |
| | | |
| | | adapter.adapt(urlRequest, using: state) { result in |
| | | adapter.adapt(urlRequest, using: state) { [pendingAdapters] result in |
| | | switch result { |
| | | case let .success(urlRequest): |
| | | self.adapt(urlRequest, using: state, adapters: pendingAdapters, completion: completion) |
| | |
| | | } |
| | | } |
| | | |
| | | @preconcurrency |
| | | open func retry(_ request: Request, |
| | | for session: Session, |
| | | dueTo error: Error, |
| | | completion: @escaping (RetryResult) -> Void) { |
| | | dueTo error: any Error, |
| | | completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | retry(request, for: session, dueTo: error, using: retriers, completion: completion) |
| | | } |
| | | |
| | | private func retry(_ request: Request, |
| | | for session: Session, |
| | | dueTo error: Error, |
| | | using retriers: [RequestRetrier], |
| | | completion: @escaping (RetryResult) -> Void) { |
| | | dueTo error: any Error, |
| | | using retriers: [any RequestRetrier], |
| | | completion: @escaping @Sendable (RetryResult) -> Void) { |
| | | var pendingRetriers = retriers |
| | | |
| | | guard !pendingRetriers.isEmpty else { completion(.doNotRetry); return } |
| | | |
| | | let retrier = pendingRetriers.removeFirst() |
| | | |
| | | retrier.retry(request, for: session, dueTo: error) { result in |
| | | retrier.retry(request, for: session, dueTo: error) { [pendingRetriers] result in |
| | | switch result { |
| | | case .retry, .retryWithDelay, .doNotRetryWithError: |
| | | completion(result) |
| | |
| | | /// - adapter: `AdapterHandler`to use to adapt the request. |
| | | /// - retrier: `RetryHandler` to use to retry the request. |
| | | /// - Returns: The `Interceptor`. |
| | | @preconcurrency |
| | | public static func interceptor(adapter: @escaping AdaptHandler, retrier: @escaping RetryHandler) -> Interceptor { |
| | | Interceptor(adaptHandler: adapter, retryHandler: retrier) |
| | | } |
| | |
| | | /// - adapter: `RequestAdapter` to use to adapt the request |
| | | /// - retrier: `RequestRetrier` to use to retry the request. |
| | | /// - Returns: The `Interceptor`. |
| | | public static func interceptor(adapter: RequestAdapter, retrier: RequestRetrier) -> Interceptor { |
| | | @preconcurrency |
| | | public static func interceptor(adapter: any RequestAdapter, retrier: any RequestRetrier) -> Interceptor { |
| | | Interceptor(adapter: adapter, retrier: retrier) |
| | | } |
| | | |
| | |
| | | /// a retry is triggered. |
| | | /// - interceptors: `RequestInterceptor`s to use to intercept the request. |
| | | /// - Returns: The `Interceptor`. |
| | | public static func interceptor(adapters: [RequestAdapter] = [], |
| | | retriers: [RequestRetrier] = [], |
| | | interceptors: [RequestInterceptor] = []) -> Interceptor { |
| | | @preconcurrency |
| | | public static func interceptor(adapters: [any RequestAdapter] = [], |
| | | retriers: [any RequestRetrier] = [], |
| | | interceptors: [any RequestInterceptor] = []) -> Interceptor { |
| | | Interceptor(adapters: adapters, retriers: retriers, interceptors: interceptors) |
| | | } |
| | | } |
| | |
| | | import Foundation |
| | | |
| | | /// The type to which all data response serializers must conform in order to serialize a response. |
| | | public protocol DataResponseSerializerProtocol<SerializedObject> { |
| | | public protocol DataResponseSerializerProtocol<SerializedObject>: Sendable { |
| | | /// The type of serialized object to be created. |
| | | associatedtype SerializedObject |
| | | associatedtype SerializedObject: Sendable |
| | | |
| | | /// Serialize the response `Data` into the provided type. |
| | | /// |
| | |
| | | /// |
| | | /// - Returns: The `SerializedObject`. |
| | | /// - Throws: Any `Error` produced during serialization. |
| | | func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject |
| | | func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: (any Error)?) throws -> SerializedObject |
| | | } |
| | | |
| | | /// The type to which all download response serializers must conform in order to serialize a response. |
| | | public protocol DownloadResponseSerializerProtocol<SerializedObject> { |
| | | public protocol DownloadResponseSerializerProtocol<SerializedObject>: Sendable { |
| | | /// The type of serialized object to be created. |
| | | associatedtype SerializedObject |
| | | associatedtype SerializedObject: Sendable |
| | | |
| | | /// Serialize the downloaded response `Data` from disk into the provided type. |
| | | /// |
| | |
| | | /// |
| | | /// - Returns: The `SerializedObject`. |
| | | /// - Throws: Any `Error` produced during serialization. |
| | | func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> SerializedObject |
| | | func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: (any Error)?) throws -> SerializedObject |
| | | } |
| | | |
| | | /// A serializer that can handle both data and download responses. |
| | | public protocol ResponseSerializer<SerializedObject>: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol { |
| | | /// `DataPreprocessor` used to prepare incoming `Data` for serialization. |
| | | var dataPreprocessor: DataPreprocessor { get } |
| | | var dataPreprocessor: any DataPreprocessor { get } |
| | | /// `HTTPMethod`s for which empty response bodies are considered appropriate. |
| | | var emptyRequestMethods: Set<HTTPMethod> { get } |
| | | /// HTTP response codes for which empty response bodies are considered appropriate. |
| | |
| | | } |
| | | |
| | | /// Type used to preprocess `Data` before it handled by a serializer. |
| | | public protocol DataPreprocessor { |
| | | public protocol DataPreprocessor: Sendable { |
| | | /// Process `Data` before it's handled by a serializer. |
| | | /// - Parameter data: The raw `Data` to process. |
| | | func preprocess(_ data: Data) throws -> Data |
| | |
| | | |
| | | extension ResponseSerializer { |
| | | /// Default `DataPreprocessor`. `PassthroughPreprocessor` by default. |
| | | public static var defaultDataPreprocessor: DataPreprocessor { PassthroughPreprocessor() } |
| | | public static var defaultDataPreprocessor: any DataPreprocessor { PassthroughPreprocessor() } |
| | | /// Default `HTTPMethod`s for which empty response bodies are always considered appropriate. `[.head]` by default. |
| | | public static var defaultEmptyRequestMethods: Set<HTTPMethod> { [.head] } |
| | | /// HTTP response codes for which empty response bodies are always considered appropriate. `[204, 205]` by default. |
| | | public static var defaultEmptyResponseCodes: Set<Int> { [204, 205] } |
| | | |
| | | public var dataPreprocessor: DataPreprocessor { Self.defaultDataPreprocessor } |
| | | public var dataPreprocessor: any DataPreprocessor { Self.defaultDataPreprocessor } |
| | | public var emptyRequestMethods: Set<HTTPMethod> { Self.defaultEmptyRequestMethods } |
| | | public var emptyResponseCodes: Set<Int> { Self.defaultEmptyResponseCodes } |
| | | |
| | |
| | | /// By default, any serializer declared to conform to both types will get file serialization for free, as it just feeds |
| | | /// the data read from disk into the data response serializer. |
| | | extension DownloadResponseSerializerProtocol where Self: DataResponseSerializerProtocol { |
| | | public func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> Self.SerializedObject { |
| | | public func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: (any Error)?) throws -> Self.SerializedObject { |
| | | guard error == nil else { throw error! } |
| | | |
| | | guard let fileURL else { |
| | |
| | | public func serializeDownload(request: URLRequest?, |
| | | response: HTTPURLResponse?, |
| | | fileURL: URL?, |
| | | error: Error?) throws -> URL { |
| | | error: (any Error)?) throws -> URL { |
| | | guard error == nil else { throw error! } |
| | | |
| | | guard let url = fileURL else { |
| | |
| | | /// request returning `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or the |
| | | /// response has an HTTP status code valid for empty responses, then an empty `Data` value is returned. |
| | | public final class DataResponseSerializer: ResponseSerializer { |
| | | public let dataPreprocessor: DataPreprocessor |
| | | public let dataPreprocessor: any DataPreprocessor |
| | | public let emptyResponseCodes: Set<Int> |
| | | public let emptyRequestMethods: Set<HTTPMethod> |
| | | |
| | |
| | | /// - dataPreprocessor: `DataPreprocessor` used to prepare the received `Data` for serialization. |
| | | /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. |
| | | public init(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | public init(dataPreprocessor: any DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) { |
| | | self.dataPreprocessor = dataPreprocessor |
| | |
| | | self.emptyRequestMethods = emptyRequestMethods |
| | | } |
| | | |
| | | public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Data { |
| | | public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: (any Error)?) throws -> Data { |
| | | guard error == nil else { throw error! } |
| | | |
| | | guard var data, !data.isEmpty else { |
| | |
| | | /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `DataResponseSerializer`. |
| | | public static func data(dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | public static func data(dataPreprocessor: any DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = DataResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DataResponseSerializer.defaultEmptyRequestMethods) -> DataResponseSerializer { |
| | | DataResponseSerializer(dataPreprocessor: dataPreprocessor, |
| | |
| | | /// data is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code |
| | | /// valid for empty responses, then an empty `String` is returned. |
| | | public final class StringResponseSerializer: ResponseSerializer { |
| | | public let dataPreprocessor: DataPreprocessor |
| | | public let dataPreprocessor: any DataPreprocessor |
| | | /// Optional string encoding used to validate the response. |
| | | public let encoding: String.Encoding? |
| | | public let emptyResponseCodes: Set<Int> |
| | |
| | | /// from the server response, falling back to the default HTTP character set, `ISO-8859-1`. |
| | | /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. |
| | | public init(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | public init(dataPreprocessor: any DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) { |
| | |
| | | self.emptyRequestMethods = emptyRequestMethods |
| | | } |
| | | |
| | | public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String { |
| | | public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: (any Error)?) throws -> String { |
| | | guard error == nil else { throw error! } |
| | | |
| | | guard var data, !data.isEmpty else { |
| | |
| | | /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. |
| | | /// |
| | | /// - Returns: The `StringResponseSerializer`. |
| | | public static func string(dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | public static func string(dataPreprocessor: any DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, |
| | | encoding: String.Encoding? = nil, |
| | | emptyResponseCodes: Set<Int> = StringResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = StringResponseSerializer.defaultEmptyRequestMethods) -> StringResponseSerializer { |
| | |
| | | /// `Decodable` and use a `DecodableResponseSerializer`. |
| | | @available(*, deprecated, message: "JSONResponseSerializer deprecated and will be removed in Alamofire 6. Use DecodableResponseSerializer instead.") |
| | | public final class JSONResponseSerializer: ResponseSerializer { |
| | | public let dataPreprocessor: DataPreprocessor |
| | | public let dataPreprocessor: any DataPreprocessor |
| | | public let emptyResponseCodes: Set<Int> |
| | | public let emptyRequestMethods: Set<HTTPMethod> |
| | | /// `JSONSerialization.ReadingOptions` used when serializing a response. |
| | |
| | | /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. |
| | | /// - options: The options to use. `.allowFragments` by default. |
| | | public init(dataPreprocessor: DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, |
| | | public init(dataPreprocessor: any DataPreprocessor = JSONResponseSerializer.defaultDataPreprocessor, |
| | | emptyResponseCodes: Set<Int> = JSONResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = JSONResponseSerializer.defaultEmptyRequestMethods, |
| | | options: JSONSerialization.ReadingOptions = .allowFragments) { |
| | |
| | | self.options = options |
| | | } |
| | | |
| | | public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Any { |
| | | public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: (any Error)?) throws -> Any { |
| | | guard error == nil else { throw error! } |
| | | |
| | | guard var data, !data.isEmpty else { |
| | |
| | | // MARK: - Empty |
| | | |
| | | /// Protocol representing an empty response. Use `T.emptyValue()` to get an instance. |
| | | public protocol EmptyResponse { |
| | | public protocol EmptyResponse: Sendable { |
| | | /// Empty value for the conforming type. |
| | | /// |
| | | /// - Returns: Value of `Self` to use for empty values. |
| | |
| | | // MARK: - DataDecoder Protocol |
| | | |
| | | /// Any type which can decode `Data` into a `Decodable` type. |
| | | public protocol DataDecoder { |
| | | public protocol DataDecoder: Sendable { |
| | | /// Decode `Data` into the provided type. |
| | | /// |
| | | /// - Parameters: |
| | |
| | | |
| | | // MARK: - Decodable |
| | | |
| | | /// A `ResponseSerializer` that decodes the response data as a generic value using any type that conforms to |
| | | /// `DataDecoder`. By default, this is an instance of `JSONDecoder`. Additionally, a request returning `nil` or no data |
| | | /// is considered an error. However, if the request has an `HTTPMethod` or the response has an HTTP status code valid |
| | | /// for empty responses then an empty value will be returned. If the decoded type conforms to `EmptyResponse`, the |
| | | /// type's `emptyValue()` will be returned. If the decoded type is `Empty`, the `.value` instance is returned. If the |
| | | /// decoded type *does not* conform to `EmptyResponse` and isn't `Empty`, an error will be produced. |
| | | public final class DecodableResponseSerializer<T: Decodable>: ResponseSerializer { |
| | | public let dataPreprocessor: DataPreprocessor |
| | | /// A `ResponseSerializer` that decodes the response data as a `Decodable` value using any decoder that conforms to |
| | | /// `DataDecoder`. By default, this is an instance of `JSONDecoder`. |
| | | /// |
| | | /// - Note: A request returning `nil` or no data is considered an error. However, if the request has an `HTTPMethod` or |
| | | /// the response has an HTTP status code valid for empty responses then an empty value will be returned. If the |
| | | /// decoded type conforms to `EmptyResponse`, the type's `emptyValue()` will be returned. If the decoded type is |
| | | /// `Empty`, the `.value` instance is returned. If the decoded type *does not* conform to `EmptyResponse` and |
| | | /// isn't `Empty`, an error will be produced. |
| | | /// |
| | | /// - Note: `JSONDecoder` and `PropertyListDecoder` are not `Sendable` on Apple platforms until macOS 13+ or iOS 16+, so |
| | | /// instances passed to a serializer should not be used outside of the serializer. Additionally, ensure a new |
| | | /// serializer is created for each request, do not use a single, shared serializer, so as to ensure separate |
| | | /// decoder instances. |
| | | public final class DecodableResponseSerializer<T: Decodable>: ResponseSerializer where T: Sendable { |
| | | public let dataPreprocessor: any DataPreprocessor |
| | | /// The `DataDecoder` instance used to decode responses. |
| | | public let decoder: DataDecoder |
| | | public let decoder: any DataDecoder |
| | | public let emptyResponseCodes: Set<Int> |
| | | public let emptyRequestMethods: Set<HTTPMethod> |
| | | |
| | |
| | | /// - decoder: The `DataDecoder`. `JSONDecoder()` by default. |
| | | /// - emptyResponseCodes: The HTTP response codes for which empty responses are allowed. `[204, 205]` by default. |
| | | /// - emptyRequestMethods: The HTTP request methods for which empty responses are allowed. `[.head]` by default. |
| | | public init(dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | public init(dataPreprocessor: any DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer.defaultEmptyRequestMethods) { |
| | | self.dataPreprocessor = dataPreprocessor |
| | |
| | | self.emptyRequestMethods = emptyRequestMethods |
| | | } |
| | | |
| | | public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T { |
| | | public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: (any Error)?) throws -> T { |
| | | guard error == nil else { throw error! } |
| | | |
| | | guard var data, !data.isEmpty else { |
| | |
| | | throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength) |
| | | } |
| | | |
| | | guard let emptyResponseType = T.self as? EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else { |
| | | guard let emptyResponseType = T.self as? any EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else { |
| | | throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)")) |
| | | } |
| | | |
| | |
| | | /// |
| | | /// - Returns: The `DecodableResponseSerializer`. |
| | | public static func decodable<T: Decodable>(of type: T.Type, |
| | | dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: DataDecoder = JSONDecoder(), |
| | | dataPreprocessor: any DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor, |
| | | decoder: any DataDecoder = JSONDecoder(), |
| | | emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes, |
| | | emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) -> DecodableResponseSerializer<T> where Self == DecodableResponseSerializer<T> { |
| | | DecodableResponseSerializer<T>(dataPreprocessor: dataPreprocessor, |
| | |
| | | |
| | | /// A retry policy that retries requests using an exponential backoff for allowed HTTP methods and HTTP status codes |
| | | /// as well as certain types of networking errors. |
| | | open class RetryPolicy: RequestInterceptor { |
| | | open class RetryPolicy: @unchecked Sendable, RequestInterceptor { |
| | | /// The default retry limit for retry policies. |
| | | public static let defaultRetryLimit: UInt = 2 |
| | | |
| | |
| | | |
| | | open func retry(_ request: Request, |
| | | for session: Session, |
| | | dueTo error: Error, |
| | | dueTo error: any Error, |
| | | completion: @escaping (RetryResult) -> Void) { |
| | | if request.retryCount < retryLimit, shouldRetry(request: request, dueTo: error) { |
| | | completion(.retryWithDelay(pow(Double(exponentialBackoffBase), Double(request.retryCount)) * exponentialBackoffScale)) |
| | |
| | | /// - error: `Error` encountered while executing the `Request`. |
| | | /// |
| | | /// - Returns: `Bool` determining whether or not to retry the `Request`. |
| | | open func shouldRetry(request: Request, dueTo error: Error) -> Bool { |
| | | open func shouldRetry(request: Request, dueTo error: any Error) -> Bool { |
| | | guard let httpMethod = request.request?.method, retryableHTTPMethods.contains(httpMethod) else { return false } |
| | | |
| | | if let statusCode = request.response?.statusCode, retryableHTTPStatusCodes.contains(statusCode) { |
| | |
| | | /// A retry policy that automatically retries idempotent requests for network connection lost errors. For more |
| | | /// information about retrying network connection lost errors, please refer to Apple's |
| | | /// [technical document](https://developer.apple.com/library/content/qa/qa1941/_index.html). |
| | | open class ConnectionLostRetryPolicy: RetryPolicy { |
| | | open class ConnectionLostRetryPolicy: RetryPolicy, @unchecked Sendable { |
| | | /// Creates a `ConnectionLostRetryPolicy` instance from the specified parameters. |
| | | /// |
| | | /// - Parameters: |
| | |
| | | // |
| | | |
| | | import Foundation |
| | | #if canImport(Security) |
| | | @preconcurrency import Security |
| | | #endif |
| | | |
| | | /// Responsible for managing the mapping of `ServerTrustEvaluating` values to given hosts. |
| | | open class ServerTrustManager { |
| | | open class ServerTrustManager: @unchecked Sendable { |
| | | /// Determines whether all hosts for this `ServerTrustManager` must be evaluated. `true` by default. |
| | | public let allHostsMustBeEvaluated: Bool |
| | | |
| | | /// The dictionary of policies mapped to a particular host. |
| | | public let evaluators: [String: ServerTrustEvaluating] |
| | | public let evaluators: [String: any ServerTrustEvaluating] |
| | | |
| | | /// Initializes the `ServerTrustManager` instance with the given evaluators. |
| | | /// |
| | |
| | | /// - allHostsMustBeEvaluated: The value determining whether all hosts for this instance must be evaluated. `true` |
| | | /// by default. |
| | | /// - evaluators: A dictionary of evaluators mapped to hosts. |
| | | public init(allHostsMustBeEvaluated: Bool = true, evaluators: [String: ServerTrustEvaluating]) { |
| | | public init(allHostsMustBeEvaluated: Bool = true, evaluators: [String: any ServerTrustEvaluating]) { |
| | | self.allHostsMustBeEvaluated = allHostsMustBeEvaluated |
| | | self.evaluators = evaluators |
| | | } |
| | |
| | | /// - Returns: The `ServerTrustEvaluating` value for the given host if found, `nil` otherwise. |
| | | /// - Throws: `AFError.serverTrustEvaluationFailed` if `allHostsMustBeEvaluated` is `true` and no matching |
| | | /// evaluators are found. |
| | | open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? { |
| | | open func serverTrustEvaluator(forHost host: String) throws -> (any ServerTrustEvaluating)? { |
| | | guard let evaluator = evaluators[host] else { |
| | | if allHostsMustBeEvaluated { |
| | | throw AFError.serverTrustEvaluationFailed(reason: .noRequiredEvaluator(host: host)) |
| | |
| | | } |
| | | |
| | | /// A protocol describing the API used to evaluate server trusts. |
| | | public protocol ServerTrustEvaluating { |
| | | public protocol ServerTrustEvaluating: Sendable { |
| | | #if !canImport(Security) |
| | | // Implement this once other platforms have API for evaluating server trusts. |
| | | #else |
| | |
| | | public final class RevocationTrustEvaluator: ServerTrustEvaluating { |
| | | /// Represents the options to be use when evaluating the status of a certificate. |
| | | /// Only Revocation Policy Constants are valid, and can be found in [Apple's documentation](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/policies/1563600-revocation_policy_constants). |
| | | public struct Options: OptionSet { |
| | | public struct Options: OptionSet, Sendable { |
| | | /// Perform revocation checking using the CRL (Certification Revocation List) method. |
| | | public static let crl = Options(rawValue: kSecRevocationCRLMethod) |
| | | /// Consult only locally cached replies; do not use network access. |
| | |
| | | try trust.af.performValidation(forHost: host) |
| | | } |
| | | |
| | | #if swift(>=5.9) |
| | | if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, visionOS 1, *) { |
| | | try trust.af.evaluate(afterApplying: SecPolicy.af.revocation(options: options)) |
| | | } else { |
| | |
| | | AFError.serverTrustEvaluationFailed(reason: .revocationCheckFailed(output: .init(host, trust, status, result), options: options)) |
| | | } |
| | | } |
| | | #else |
| | | if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { |
| | | try trust.af.evaluate(afterApplying: SecPolicy.af.revocation(options: options)) |
| | | } else { |
| | | try trust.af.validate(policy: SecPolicy.af.revocation(options: options)) { status, result in |
| | | AFError.serverTrustEvaluationFailed(reason: .revocationCheckFailed(output: .init(host, trust, status, result), options: options)) |
| | | } |
| | | } |
| | | #endif |
| | | } |
| | | } |
| | | |
| | |
| | | /// Uses the provided evaluators to validate the server trust. The trust is only considered valid if all of the |
| | | /// evaluators consider it valid. |
| | | public final class CompositeTrustEvaluator: ServerTrustEvaluating { |
| | | private let evaluators: [ServerTrustEvaluating] |
| | | private let evaluators: [any ServerTrustEvaluating] |
| | | |
| | | /// Creates a `CompositeTrustEvaluator` from the provided evaluators. |
| | | /// |
| | | /// - Parameter evaluators: The `ServerTrustEvaluating` values used to evaluate the server trust. |
| | | public init(evaluators: [ServerTrustEvaluating]) { |
| | | public init(evaluators: [any ServerTrustEvaluating]) { |
| | | self.evaluators = evaluators |
| | | } |
| | | |
| | |
| | | /// Creates a `CompositeTrustEvaluator` from the provided evaluators. |
| | | /// |
| | | /// - Parameter evaluators: The `ServerTrustEvaluating` values used to evaluate the server trust. |
| | | public static func composite(evaluators: [ServerTrustEvaluating]) -> CompositeTrustEvaluator { |
| | | public static func composite(evaluators: [any ServerTrustEvaluating]) -> CompositeTrustEvaluator { |
| | | CompositeTrustEvaluator(evaluators: evaluators) |
| | | } |
| | | } |
| | |
| | | @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate(afterApplying:)") |
| | | @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)") |
| | | @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate(afterApplying:)") |
| | | public func validate(policy: SecPolicy, errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws { |
| | | public func validate(policy: SecPolicy, errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> any Error) throws { |
| | | try apply(policy: policy).af.validate(errorProducer: errorProducer) |
| | | } |
| | | |
| | |
| | | @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate()") |
| | | @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate()") |
| | | @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate()") |
| | | public func validate(errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws { |
| | | public func validate(errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> any Error) throws { |
| | | var result = SecTrustResultType.invalid |
| | | let status = SecTrustEvaluate(type, &result) |
| | | |
| | |
| | | |
| | | /// The `SecCertificate`s contained in `self`. |
| | | public var certificates: [SecCertificate] { |
| | | #if swift(>=5.9) |
| | | if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1, *) { |
| | | return (SecTrustCopyCertificateChain(type) as? [SecCertificate]) ?? [] |
| | | (SecTrustCopyCertificateChain(type) as? [SecCertificate]) ?? [] |
| | | } else { |
| | | return (0..<SecTrustGetCertificateCount(type)).compactMap { index in |
| | | (0..<SecTrustGetCertificateCount(type)).compactMap { index in |
| | | SecTrustGetCertificateAtIndex(type, index) |
| | | } |
| | | } |
| | | #else |
| | | if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { |
| | | return (SecTrustCopyCertificateChain(type) as? [SecCertificate]) ?? [] |
| | | } else { |
| | | return (0..<SecTrustGetCertificateCount(type)).compactMap { index in |
| | | SecTrustGetCertificateAtIndex(type, index) |
| | | } |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | /// The `Data` values for all certificates contained in `self`. |
| | |
| | | /// - Parameter host: The hostname, used only in the error output if validation fails. |
| | | /// - Throws: An `AFError.serverTrustEvaluationFailed` instance with a `.defaultEvaluationFailed` reason. |
| | | public func performDefaultValidation(forHost host: String) throws { |
| | | #if swift(>=5.9) |
| | | if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, visionOS 1, *) { |
| | | try evaluate(afterApplying: SecPolicy.af.default) |
| | | } else { |
| | |
| | | AFError.serverTrustEvaluationFailed(reason: .defaultEvaluationFailed(output: .init(host, type, status, result))) |
| | | } |
| | | } |
| | | #else |
| | | if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { |
| | | try evaluate(afterApplying: SecPolicy.af.default) |
| | | } else { |
| | | try validate(policy: SecPolicy.af.default) { status, result in |
| | | AFError.serverTrustEvaluationFailed(reason: .defaultEvaluationFailed(output: .init(host, type, status, result))) |
| | | } |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | /// Validates `self` after applying `SecPolicy.af.hostname(host)`, which performs the default validation as well as |
| | |
| | | /// - Parameter host: The hostname to use in the validation. |
| | | /// - Throws: An `AFError.serverTrustEvaluationFailed` instance with a `.defaultEvaluationFailed` reason. |
| | | public func performValidation(forHost host: String) throws { |
| | | #if swift(>=5.9) |
| | | if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, visionOS 1, *) { |
| | | try evaluate(afterApplying: SecPolicy.af.hostname(host)) |
| | | } else { |
| | |
| | | AFError.serverTrustEvaluationFailed(reason: .hostValidationFailed(output: .init(host, type, status, result))) |
| | | } |
| | | } |
| | | #else |
| | | if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { |
| | | try evaluate(afterApplying: SecPolicy.af.hostname(host)) |
| | | } else { |
| | | try validate(policy: SecPolicy.af.hostname(host)) { status, result in |
| | | AFError.serverTrustEvaluationFailed(reason: .hostValidationFailed(output: .init(host, type, status, result))) |
| | | } |
| | | } |
| | | #endif |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | guard let createdTrust = trust, trustCreationStatus == errSecSuccess else { return nil } |
| | | |
| | | #if swift(>=5.9) |
| | | if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) { |
| | | return SecTrustCopyKey(createdTrust) |
| | | } else { |
| | | return SecTrustCopyPublicKey(createdTrust) |
| | | } |
| | | #else |
| | | if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) { |
| | | return SecTrustCopyKey(createdTrust) |
| | | } else { |
| | | return SecTrustCopyPublicKey(createdTrust) |
| | | } |
| | | #endif |
| | | } |
| | | } |
| | | |
| | |
| | | /// - Returns: The encoded key. |
| | | func encode(_ key: String, atIndex index: Int) -> String { |
| | | switch self { |
| | | case .brackets: return "\(key)[]" |
| | | case .noBrackets: return key |
| | | case .indexInBrackets: return "\(key)[\(index)]" |
| | | case let .custom(encoding): return encoding(key, index) |
| | | case .brackets: "\(key)[]" |
| | | case .noBrackets: key |
| | | case .indexInBrackets: "\(key)[\(index)]" |
| | | case let .custom(encoding): encoding(key, index) |
| | | } |
| | | } |
| | | } |
| | |
| | | /// - Returns: The encoded `String`. |
| | | func encode(_ value: Bool) -> String { |
| | | switch self { |
| | | case .numeric: return value ? "1" : "0" |
| | | case .literal: return value ? "true" : "false" |
| | | case .numeric: value ? "1" : "0" |
| | | case .literal: value ? "true" : "false" |
| | | } |
| | | } |
| | | } |
| | |
| | | /// `Encodable` implementation. |
| | | func encode(_ data: Data) throws -> String? { |
| | | switch self { |
| | | case .deferredToData: return nil |
| | | case .base64: return data.base64EncodedString() |
| | | case let .custom(encoding): return try encoding(data) |
| | | case .deferredToData: nil |
| | | case .base64: data.base64EncodedString() |
| | | case let .custom(encoding): try encoding(data) |
| | | } |
| | | } |
| | | } |
| | |
| | | /// Encoding to use for `Date` values. |
| | | public enum DateEncoding { |
| | | /// ISO8601 and RFC3339 formatter. |
| | | private static let iso8601Formatter: ISO8601DateFormatter = { |
| | | private static let iso8601Formatter = Protected<ISO8601DateFormatter>({ |
| | | let formatter = ISO8601DateFormatter() |
| | | formatter.formatOptions = .withInternetDateTime |
| | | return formatter |
| | | }() |
| | | }()) |
| | | |
| | | /// Defers encoding to the `Date` type. This is the default encoding. |
| | | case deferredToDate |
| | |
| | | func encode(_ date: Date) throws -> String? { |
| | | switch self { |
| | | case .deferredToDate: |
| | | return nil |
| | | nil |
| | | case .secondsSince1970: |
| | | return String(date.timeIntervalSince1970) |
| | | String(date.timeIntervalSince1970) |
| | | case .millisecondsSince1970: |
| | | return String(date.timeIntervalSince1970 * 1000.0) |
| | | String(date.timeIntervalSince1970 * 1000.0) |
| | | case .iso8601: |
| | | return DateEncoding.iso8601Formatter.string(from: date) |
| | | DateEncoding.iso8601Formatter.read { $0.string(from: date) } |
| | | case let .formatted(formatter): |
| | | return formatter.string(from: date) |
| | | formatter.string(from: date) |
| | | case let .custom(closure): |
| | | return try closure(date) |
| | | try closure(date) |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | func encode(_ key: String) -> String { |
| | | switch self { |
| | | case .useDefaultKeys: return key |
| | | case .convertToSnakeCase: return convertToSnakeCase(key) |
| | | case .convertToKebabCase: return convertToKebabCase(key) |
| | | case .capitalized: return String(key.prefix(1).uppercased() + key.dropFirst()) |
| | | case .uppercased: return key.uppercased() |
| | | case .lowercased: return key.lowercased() |
| | | case let .custom(encoding): return encoding(key) |
| | | case .useDefaultKeys: key |
| | | case .convertToSnakeCase: convertToSnakeCase(key) |
| | | case .convertToKebabCase: convertToKebabCase(key) |
| | | case .capitalized: String(key.prefix(1).uppercased() + key.dropFirst()) |
| | | case .uppercased: key.uppercased() |
| | | case .lowercased: key.lowercased() |
| | | case let .custom(encoding): encoding(key) |
| | | } |
| | | } |
| | | |
| | |
| | | /// |
| | | /// This encoding affects how the `parent`, `child`, `grandchild` path is encoded. Brackets are used by default. |
| | | /// e.g. `parent[child][grandchild]=value`. |
| | | public struct KeyPathEncoding { |
| | | public struct KeyPathEncoding: Sendable { |
| | | /// Encodes key paths by wrapping each component in brackets. e.g. `parent[child][grandchild]`. |
| | | public static let brackets = KeyPathEncoding { "[\($0)]" } |
| | | /// Encodes key paths by separating each component with dots. e.g. `parent.child.grandchild`. |
| | | public static let dots = KeyPathEncoding { ".\($0)" } |
| | | |
| | | private let encoding: (_ subkey: String) -> String |
| | | private let encoding: @Sendable (_ subkey: String) -> String |
| | | |
| | | /// Creates an instance with the encoding closure called for each sub-key in a key path. |
| | | /// |
| | | /// - Parameter encoding: Closure used to perform the encoding. |
| | | public init(encoding: @escaping (_ subkey: String) -> String) { |
| | | public init(encoding: @escaping @Sendable (_ subkey: String) -> String) { |
| | | self.encoding = encoding |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /// Encoding to use for `nil` values. |
| | | public struct NilEncoding { |
| | | public struct NilEncoding: Sendable { |
| | | /// Encodes `nil` by dropping the entire key / value pair. |
| | | public static let dropKey = NilEncoding { nil } |
| | | /// Encodes `nil` by dropping only the value. e.g. `value1=one&nilValue=&value2=two`. |
| | |
| | | /// Encodes `nil` as `null`. |
| | | public static let null = NilEncoding { "null" } |
| | | |
| | | private let encoding: () -> String? |
| | | private let encoding: @Sendable () -> String? |
| | | |
| | | /// Creates an instance with the encoding closure called for `nil` values. |
| | | /// |
| | | /// - Parameter encoding: Closure used to perform the encoding. |
| | | public init(encoding: @escaping () -> String?) { |
| | | public init(encoding: @escaping @Sendable () -> String?) { |
| | | self.encoding = encoding |
| | | } |
| | | |
| | |
| | | /// - Returns: The encoded `String`. |
| | | func encode(_ string: String) -> String { |
| | | switch self { |
| | | case .percentEscaped: return string.replacingOccurrences(of: " ", with: "%20") |
| | | case .plusReplaced: return string.replacingOccurrences(of: " ", with: "+") |
| | | case .percentEscaped: string.replacingOccurrences(of: " ", with: "%20") |
| | | case .plusReplaced: string.replacingOccurrences(of: " ", with: "+") |
| | | } |
| | | } |
| | | } |
| | |
| | | var localizedDescription: String { |
| | | switch self { |
| | | case let .invalidRootObject(object): |
| | | return "URLEncodedFormEncoder requires keyed root object. Received \(object) instead." |
| | | "URLEncodedFormEncoder requires keyed root object. Received \(object) instead." |
| | | } |
| | | } |
| | | } |
| | |
| | | self.allowedCharacters = allowedCharacters |
| | | } |
| | | |
| | | func encode(_ value: Encodable) throws -> URLEncodedFormComponent { |
| | | func encode(_ value: any Encodable) throws -> URLEncodedFormComponent { |
| | | let context = URLEncodedFormContext(.object([])) |
| | | let encoder = _URLEncodedFormEncoder(context: context, |
| | | boolEncoding: boolEncoding, |
| | |
| | | /// |
| | | /// - Returns: The encoded `String`. |
| | | /// - Throws: An `Error` or `EncodingError` instance if encoding fails. |
| | | public func encode(_ value: Encodable) throws -> String { |
| | | public func encode(_ value: any Encodable) throws -> String { |
| | | let component: URLEncodedFormComponent = try encode(value) |
| | | |
| | | guard case let .object(object) = component else { |
| | |
| | | /// - Returns: The encoded `Data`. |
| | | /// |
| | | /// - Throws: An `Error` or `EncodingError` instance if encoding fails. |
| | | public func encode(_ value: Encodable) throws -> Data { |
| | | public func encode(_ value: any Encodable) throws -> Data { |
| | | let string: String = try encode(value) |
| | | |
| | | return Data(string.utf8) |
| | |
| | | } |
| | | |
| | | final class _URLEncodedFormEncoder { |
| | | var codingPath: [CodingKey] |
| | | var codingPath: [any CodingKey] |
| | | // Returns an empty dictionary, as this encoder doesn't support userInfo. |
| | | var userInfo: [CodingUserInfoKey: Any] { [:] } |
| | | |
| | |
| | | private let nilEncoding: URLEncodedFormEncoder.NilEncoding |
| | | |
| | | init(context: URLEncodedFormContext, |
| | | codingPath: [CodingKey] = [], |
| | | codingPath: [any CodingKey] = [], |
| | | boolEncoding: URLEncodedFormEncoder.BoolEncoding, |
| | | dataEncoding: URLEncodedFormEncoder.DataEncoding, |
| | | dateEncoding: URLEncodedFormEncoder.DateEncoding, |
| | |
| | | return KeyedEncodingContainer(container) |
| | | } |
| | | |
| | | func unkeyedContainer() -> UnkeyedEncodingContainer { |
| | | func unkeyedContainer() -> any UnkeyedEncodingContainer { |
| | | _URLEncodedFormEncoder.UnkeyedContainer(context: context, |
| | | codingPath: codingPath, |
| | | boolEncoding: boolEncoding, |
| | |
| | | nilEncoding: nilEncoding) |
| | | } |
| | | |
| | | func singleValueContainer() -> SingleValueEncodingContainer { |
| | | func singleValueContainer() -> any SingleValueEncodingContainer { |
| | | _URLEncodedFormEncoder.SingleValueContainer(context: context, |
| | | codingPath: codingPath, |
| | | boolEncoding: boolEncoding, |
| | |
| | | /// Converts self to an `[URLEncodedFormData]` or returns `nil` if not convertible. |
| | | var array: [URLEncodedFormComponent]? { |
| | | switch self { |
| | | case let .array(array): return array |
| | | default: return nil |
| | | case let .array(array): array |
| | | default: nil |
| | | } |
| | | } |
| | | |
| | | /// Converts self to an `Object` or returns `nil` if not convertible. |
| | | var object: Object? { |
| | | switch self { |
| | | case let .object(object): return object |
| | | default: return nil |
| | | case let .object(object): object |
| | | default: nil |
| | | } |
| | | } |
| | | |
| | |
| | | /// - parameters: |
| | | /// - value: Value of `Self` to set at the supplied path. |
| | | /// - path: `CodingKey` path to update with the supplied value. |
| | | public mutating func set(to value: URLEncodedFormComponent, at path: [CodingKey]) { |
| | | public mutating func set(to value: URLEncodedFormComponent, at path: [any CodingKey]) { |
| | | set(&self, to: value, at: path) |
| | | } |
| | | |
| | | /// Recursive backing method to `set(to:at:)`. |
| | | private func set(_ context: inout URLEncodedFormComponent, to value: URLEncodedFormComponent, at path: [CodingKey]) { |
| | | private func set(_ context: inout URLEncodedFormComponent, to value: URLEncodedFormComponent, at path: [any CodingKey]) { |
| | | guard !path.isEmpty else { |
| | | context = value |
| | | return |
| | |
| | | |
| | | extension _URLEncodedFormEncoder { |
| | | final class KeyedContainer<Key> where Key: CodingKey { |
| | | var codingPath: [CodingKey] |
| | | var codingPath: [any CodingKey] |
| | | |
| | | private let context: URLEncodedFormContext |
| | | private let boolEncoding: URLEncodedFormEncoder.BoolEncoding |
| | |
| | | private let nilEncoding: URLEncodedFormEncoder.NilEncoding |
| | | |
| | | init(context: URLEncodedFormContext, |
| | | codingPath: [CodingKey], |
| | | codingPath: [any CodingKey], |
| | | boolEncoding: URLEncodedFormEncoder.BoolEncoding, |
| | | dataEncoding: URLEncodedFormEncoder.DataEncoding, |
| | | dateEncoding: URLEncodedFormEncoder.DateEncoding, |
| | |
| | | self.nilEncoding = nilEncoding |
| | | } |
| | | |
| | | private func nestedCodingPath(for key: CodingKey) -> [CodingKey] { |
| | | private func nestedCodingPath(for key: any CodingKey) -> [any CodingKey] { |
| | | codingPath + [key] |
| | | } |
| | | } |
| | |
| | | try container.encode(value) |
| | | } |
| | | |
| | | func nestedSingleValueEncoder(for key: Key) -> SingleValueEncodingContainer { |
| | | func nestedSingleValueEncoder(for key: Key) -> any SingleValueEncodingContainer { |
| | | let container = _URLEncodedFormEncoder.SingleValueContainer(context: context, |
| | | codingPath: nestedCodingPath(for: key), |
| | | boolEncoding: boolEncoding, |
| | |
| | | return container |
| | | } |
| | | |
| | | func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { |
| | | func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { |
| | | let container = _URLEncodedFormEncoder.UnkeyedContainer(context: context, |
| | | codingPath: nestedCodingPath(for: key), |
| | | boolEncoding: boolEncoding, |
| | |
| | | return KeyedEncodingContainer(container) |
| | | } |
| | | |
| | | func superEncoder() -> Encoder { |
| | | func superEncoder() -> any Encoder { |
| | | _URLEncodedFormEncoder(context: context, |
| | | codingPath: codingPath, |
| | | boolEncoding: boolEncoding, |
| | |
| | | nilEncoding: nilEncoding) |
| | | } |
| | | |
| | | func superEncoder(forKey key: Key) -> Encoder { |
| | | func superEncoder(forKey key: Key) -> any Encoder { |
| | | _URLEncodedFormEncoder(context: context, |
| | | codingPath: nestedCodingPath(for: key), |
| | | boolEncoding: boolEncoding, |
| | |
| | | |
| | | extension _URLEncodedFormEncoder { |
| | | final class SingleValueContainer { |
| | | var codingPath: [CodingKey] |
| | | var codingPath: [any CodingKey] |
| | | |
| | | private var canEncodeNewValue = true |
| | | |
| | |
| | | private let nilEncoding: URLEncodedFormEncoder.NilEncoding |
| | | |
| | | init(context: URLEncodedFormContext, |
| | | codingPath: [CodingKey], |
| | | codingPath: [any CodingKey], |
| | | boolEncoding: URLEncodedFormEncoder.BoolEncoding, |
| | | dataEncoding: URLEncodedFormEncoder.DataEncoding, |
| | | dateEncoding: URLEncodedFormEncoder.DateEncoding, |
| | |
| | | |
| | | extension _URLEncodedFormEncoder { |
| | | final class UnkeyedContainer { |
| | | var codingPath: [CodingKey] |
| | | var codingPath: [any CodingKey] |
| | | |
| | | var count = 0 |
| | | var nestedCodingPath: [CodingKey] { |
| | | var nestedCodingPath: [any CodingKey] { |
| | | codingPath + [AnyCodingKey(intValue: count)!] |
| | | } |
| | | |
| | |
| | | private let nilEncoding: URLEncodedFormEncoder.NilEncoding |
| | | |
| | | init(context: URLEncodedFormContext, |
| | | codingPath: [CodingKey], |
| | | codingPath: [any CodingKey], |
| | | boolEncoding: URLEncodedFormEncoder.BoolEncoding, |
| | | dataEncoding: URLEncodedFormEncoder.DataEncoding, |
| | | dateEncoding: URLEncodedFormEncoder.DateEncoding, |
| | |
| | | try container.encode(value) |
| | | } |
| | | |
| | | func nestedSingleValueContainer() -> SingleValueEncodingContainer { |
| | | func nestedSingleValueContainer() -> any SingleValueEncodingContainer { |
| | | defer { count += 1 } |
| | | |
| | | return _URLEncodedFormEncoder.SingleValueContainer(context: context, |
| | |
| | | return KeyedEncodingContainer(container) |
| | | } |
| | | |
| | | func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { |
| | | func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { |
| | | defer { count += 1 } |
| | | |
| | | return _URLEncodedFormEncoder.UnkeyedContainer(context: context, |
| | |
| | | nilEncoding: nilEncoding) |
| | | } |
| | | |
| | | func superEncoder() -> Encoder { |
| | | func superEncoder() -> any Encoder { |
| | | defer { count += 1 } |
| | | |
| | | return _URLEncodedFormEncoder(context: context, |
| | |
| | | |
| | | func serialize(_ component: URLEncodedFormComponent, forKey key: String) -> String { |
| | | switch component { |
| | | case let .string(string): return "\(escape(keyEncoding.encode(key)))=\(escape(string))" |
| | | case let .array(array): return serialize(array, forKey: key) |
| | | case let .object(object): return serialize(object, forKey: key) |
| | | case let .string(string): "\(escape(keyEncoding.encode(key)))=\(escape(string))" |
| | | case let .array(array): serialize(array, forKey: key) |
| | | case let .object(object): serialize(object, forKey: key) |
| | | } |
| | | } |
| | | |
| | |
| | | fileprivate typealias ErrorReason = AFError.ResponseValidationFailureReason |
| | | |
| | | /// Used to represent whether a validation succeeded or failed. |
| | | public typealias ValidationResult = Result<Void, Error> |
| | | public typealias ValidationResult = Result<Void, any(Error & Sendable)> |
| | | |
| | | fileprivate struct MIMEType { |
| | | let type: String |
| | |
| | | func matches(_ mime: MIMEType) -> Bool { |
| | | switch (type, subtype) { |
| | | case (mime.type, mime.subtype), (mime.type, "*"), ("*", mime.subtype), ("*", "*"): |
| | | return true |
| | | true |
| | | default: |
| | | return false |
| | | false |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | fileprivate func validate<S: Sequence>(contentType acceptableContentTypes: S, |
| | | response: HTTPURLResponse, |
| | | data: Data?) |
| | | isEmpty: Bool) |
| | | -> ValidationResult |
| | | where S.Iterator.Element == String { |
| | | guard let data, !data.isEmpty else { return .success(()) } |
| | | guard !isEmpty else { return .success(()) } |
| | | |
| | | return validate(contentType: acceptableContentTypes, response: response) |
| | | } |
| | |
| | | extension DataRequest { |
| | | /// A closure used to validate a request that takes a URL request, a URL response and data, and returns whether the |
| | | /// request was valid. |
| | | public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult |
| | | public typealias Validation = @Sendable (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult |
| | | |
| | | /// Validates that the response has a status code in the specified sequence. |
| | | /// |
| | |
| | | /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { |
| | | public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int, S: Sendable { |
| | | validate { [unowned self] _, response, _ in |
| | | self.validate(statusCode: acceptableStatusCodes, response: response) |
| | | } |
| | |
| | | /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. |
| | | /// |
| | | /// - returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { |
| | | public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @Sendable @autoclosure () -> S) -> Self where S.Iterator.Element == String, S: Sendable { |
| | | validate { [unowned self] _, response, data in |
| | | self.validate(contentType: acceptableContentTypes(), response: response, data: data) |
| | | self.validate(contentType: acceptableContentTypes(), response: response, isEmpty: (data == nil || data?.isEmpty == true)) |
| | | } |
| | | } |
| | | |
| | |
| | | /// - returns: The request. |
| | | @discardableResult |
| | | public func validate() -> Self { |
| | | let contentTypes: () -> [String] = { [unowned self] in |
| | | let contentTypes: @Sendable () -> [String] = { [unowned self] in |
| | | acceptableContentTypes |
| | | } |
| | | return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) |
| | |
| | | extension DataStreamRequest { |
| | | /// A closure used to validate a request that takes a `URLRequest` and `HTTPURLResponse` and returns whether the |
| | | /// request was valid. |
| | | public typealias Validation = (_ request: URLRequest?, _ response: HTTPURLResponse) -> ValidationResult |
| | | public typealias Validation = @Sendable (_ request: URLRequest?, _ response: HTTPURLResponse) -> ValidationResult |
| | | |
| | | /// Validates that the response has a status code in the specified sequence. |
| | | /// |
| | |
| | | /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { |
| | | public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int, S: Sendable { |
| | | validate { [unowned self] _, response in |
| | | self.validate(statusCode: acceptableStatusCodes, response: response) |
| | | } |
| | |
| | | /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. |
| | | /// |
| | | /// - returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { |
| | | public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @Sendable @autoclosure () -> S) -> Self where S.Iterator.Element == String, S: Sendable { |
| | | validate { [unowned self] _, response in |
| | | self.validate(contentType: acceptableContentTypes(), response: response) |
| | | } |
| | |
| | | /// - Returns: The instance. |
| | | @discardableResult |
| | | public func validate() -> Self { |
| | | let contentTypes: () -> [String] = { [unowned self] in |
| | | let contentTypes: @Sendable () -> [String] = { [unowned self] in |
| | | acceptableContentTypes |
| | | } |
| | | return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) |
| | |
| | | extension DownloadRequest { |
| | | /// A closure used to validate a request that takes a URL request, a URL response, a temporary URL and a |
| | | /// destination URL, and returns whether the request was valid. |
| | | public typealias Validation = (_ request: URLRequest?, |
| | | _ response: HTTPURLResponse, |
| | | _ fileURL: URL?) |
| | | public typealias Validation = @Sendable (_ request: URLRequest?, |
| | | _ response: HTTPURLResponse, |
| | | _ fileURL: URL?) |
| | | -> ValidationResult |
| | | |
| | | /// Validates that the response has a status code in the specified sequence. |
| | |
| | | /// - Parameter acceptableStatusCodes: `Sequence` of acceptable response status codes. |
| | | /// |
| | | /// - Returns: The instance. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int { |
| | | public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int, S: Sendable { |
| | | validate { [unowned self] _, response, _ in |
| | | self.validate(statusCode: acceptableStatusCodes, response: response) |
| | | } |
| | | } |
| | | |
| | | /// Validates that the response has a content type in the specified sequence. |
| | | /// Validates that the response has a `Content-Type` in the specified sequence. |
| | | /// |
| | | /// If validation fails, subsequent calls to response handlers will have an associated error. |
| | | /// |
| | | /// - parameter contentType: The acceptable content types, which may specify wildcard types and/or subtypes. |
| | | /// |
| | | /// - returns: The request. |
| | | @preconcurrency |
| | | @discardableResult |
| | | public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @autoclosure () -> S) -> Self where S.Iterator.Element == String { |
| | | public func validate<S: Sequence>(contentType acceptableContentTypes: @escaping @Sendable @autoclosure () -> S) -> Self where S.Iterator.Element == String, S: Sendable { |
| | | validate { [unowned self] _, response, fileURL in |
| | | guard let validFileURL = fileURL else { |
| | | guard let fileURL else { |
| | | return .failure(AFError.responseValidationFailed(reason: .dataFileNil)) |
| | | } |
| | | |
| | | do { |
| | | let data = try Data(contentsOf: validFileURL) |
| | | return self.validate(contentType: acceptableContentTypes(), response: response, data: data) |
| | | let isEmpty = try (fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0) == 0 |
| | | return self.validate(contentType: acceptableContentTypes(), response: response, isEmpty: isEmpty) |
| | | } catch { |
| | | return .failure(AFError.responseValidationFailed(reason: .dataFileReadFailed(at: validFileURL))) |
| | | return .failure(AFError.responseValidationFailed(reason: .dataFileReadFailed(at: fileURL))) |
| | | } |
| | | } |
| | | } |
| | |
| | | /// - returns: The request. |
| | | @discardableResult |
| | | public func validate() -> Self { |
| | | let contentTypes = { [unowned self] in |
| | | let contentTypes: @Sendable () -> [String] = { [unowned self] in |
| | | acceptableContentTypes |
| | | } |
| | | return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes()) |
| | |
| | | // https://github.com/hackiftekhar/IQKeyboardManager |
| | | // Copyright (c) 2013-24 Iftekhar Qurashi. |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | |
| | | import UIKit |
| | | import IQKeyboardCore |
| | | |
| | | @available(iOSApplicationExtension, unavailable) |
| | | @MainActor |
| | | internal extension IQKeyboardManager { |
| | | |
| | | /** Get all UITextField/UITextView siblings of textFieldView. */ |
| | | func responderViews() -> [UIView]? { |
| | | |
| | | guard let textFieldView: UIView = activeConfiguration.textFieldViewInfo?.textFieldView else { |
| | | return nil |
| | | } |
| | | |
| | | var superConsideredView: UIView? |
| | | |
| | | // If find any consider responderView in it's upper hierarchy then will get deepResponderView. |
| | | for allowedClass in toolbarPreviousNextAllowedClasses { |
| | | superConsideredView = textFieldView.iq.superviewOf(type: allowedClass) |
| | | if superConsideredView != nil { |
| | | break |
| | | } |
| | | } |
| | | |
| | | var swiftUIHostingView: UIView? |
| | | let swiftUIHostingViewName: String = "UIHostingView<" |
| | | var superView: UIView? = textFieldView.superview |
| | | while let unwrappedSuperView: UIView = superView { |
| | | |
| | | let classNameString: String = { |
| | | var name: String = "\(type(of: unwrappedSuperView.self))" |
| | | if name.hasPrefix("_") { |
| | | name.removeFirst() |
| | | } |
| | | return name |
| | | }() |
| | | |
| | | if classNameString.hasPrefix(swiftUIHostingViewName) { |
| | | swiftUIHostingView = unwrappedSuperView |
| | | break |
| | | } |
| | | |
| | | superView = unwrappedSuperView.superview |
| | | } |
| | | |
| | | // (Enhancement ID: #22) |
| | | // If there is a superConsideredView in view's hierarchy, |
| | | // then fetching all it's subview that responds. |
| | | // No sorting for superConsideredView, it's by subView position. |
| | | if let view: UIView = swiftUIHostingView { |
| | | return view.iq.deepResponderViews() |
| | | } else if let view: UIView = superConsideredView { |
| | | return view.iq.deepResponderViews() |
| | | } else { // Otherwise fetching all the siblings |
| | | |
| | | let textFields: [UIView] = textFieldView.iq.responderSiblings() |
| | | |
| | | // Sorting textFields according to behavior |
| | | switch toolbarConfiguration.manageBehavior { |
| | | // If autoToolbar behavior is bySubviews, then returning it. |
| | | case .bySubviews: return textFields |
| | | |
| | | // If autoToolbar behavior is by tag, then sorting it according to tag property. |
| | | case .byTag: return textFields.sortedByTag() |
| | | |
| | | // If autoToolbar behavior is by tag, then sorting it according to tag property. |
| | | case .byPosition: return textFields.sortedByPosition() |
| | | } |
| | | } |
| | | } |
| | | |
| | | func privateIsEnabled() -> Bool { |
| | | |
| | | var isEnabled: Bool = enable |
| | | |
| | | guard let textFieldViewInfo: IQTextFieldViewInfo = activeConfiguration.textFieldViewInfo else { |
| | | guard let textInputView: any IQTextInputView = activeConfiguration.textInputView else { |
| | | return isEnabled |
| | | } |
| | | |
| | | let enableMode: IQEnableMode = textFieldViewInfo.textFieldView.iq.enableMode |
| | | |
| | | if enableMode == .enabled { |
| | | isEnabled = true |
| | | } else if enableMode == .disabled { |
| | | isEnabled = false |
| | | } else if var textFieldViewController = textFieldViewInfo.textFieldView.iq.viewContainingController() { |
| | | switch textInputView.internalEnableMode { |
| | | case .default: |
| | | guard var controller = (textInputView as UIView).iq.viewContainingController() else { |
| | | return isEnabled |
| | | } |
| | | |
| | | // If it is searchBar textField embedded in Navigation Bar |
| | | if textFieldViewInfo.textFieldView.iq.textFieldSearchBar() != nil, |
| | | let navController: UINavigationController = textFieldViewController as? UINavigationController, |
| | | if (textInputView as UIView).iq.textFieldSearchBar() != nil, |
| | | let navController: UINavigationController = controller as? UINavigationController, |
| | | let topController: UIViewController = navController.topViewController { |
| | | textFieldViewController = topController |
| | | controller = topController |
| | | } |
| | | |
| | | // If viewController is kind of enable viewController class, then assuming it's enabled. |
| | | if !isEnabled, enabledDistanceHandlingClasses.contains(where: { textFieldViewController.isKind(of: $0) }) { |
| | | isEnabled = true |
| | | } |
| | | // If viewController is in enabledDistanceHandlingClasses, then assuming it's enabled. |
| | | let isWithEnabledClass: Bool = enabledDistanceHandlingClasses.contains(where: { controller.isKind(of: $0) }) |
| | | var isEnabled: Bool = isEnabled || isWithEnabledClass |
| | | |
| | | if isEnabled { |
| | | |
| | | // If viewController is kind of disabled viewController class, then assuming it's disabled. |
| | | if disabledDistanceHandlingClasses.contains(where: { textFieldViewController.isKind(of: $0) }) { |
| | | // If viewController is in disabledDistanceHandlingClasses, |
| | | // then assuming it's disabled. |
| | | if disabledDistanceHandlingClasses.contains(where: { controller.isKind(of: $0) }) { |
| | | isEnabled = false |
| | | } |
| | | |
| | | // Special Controllers |
| | | if isEnabled { |
| | | |
| | | let classNameString: String = "\(type(of: textFieldViewController.self))" |
| | | } else { |
| | | // Special Controllers |
| | | let classNameString: String = "\(type(of: controller.self))" |
| | | |
| | | // _UIAlertControllerTextFieldViewController |
| | | if classNameString.contains("UIAlertController"), |
| | |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | return isEnabled |
| | | } |
| | | |
| | | func privateIsEnableAutoToolbar() -> Bool { |
| | | |
| | | var isEnabled: Bool = enableAutoToolbar |
| | | |
| | | guard let textFieldViewInfo: IQTextFieldViewInfo = activeConfiguration.textFieldViewInfo, |
| | | var textFieldViewController = textFieldViewInfo.textFieldView.iq.viewContainingController() else { |
| | | return isEnabled |
| | | case .enabled: |
| | | return true |
| | | case .disabled: |
| | | return false |
| | | @unknown default: |
| | | return false |
| | | } |
| | | |
| | | // If it is searchBar textField embedded in Navigation Bar |
| | | if textFieldViewInfo.textFieldView.iq.textFieldSearchBar() != nil, |
| | | let navController: UINavigationController = textFieldViewController as? UINavigationController, |
| | | let topController: UIViewController = navController.topViewController { |
| | | textFieldViewController = topController |
| | | } |
| | | |
| | | if !isEnabled, enabledToolbarClasses.contains(where: { textFieldViewController.isKind(of: $0) }) { |
| | | isEnabled = true |
| | | } |
| | | |
| | | if isEnabled { |
| | | |
| | | // If found any toolbar disabled classes then return. |
| | | if disabledToolbarClasses.contains(where: { textFieldViewController.isKind(of: $0) }) { |
| | | isEnabled = false |
| | | } |
| | | |
| | | // Special Controllers |
| | | if isEnabled { |
| | | |
| | | let classNameString: String = "\(type(of: textFieldViewController.self))" |
| | | |
| | | // _UIAlertControllerTextFieldViewController |
| | | if classNameString.contains("UIAlertController"), classNameString.hasSuffix("TextFieldViewController") { |
| | | isEnabled = false |
| | | } |
| | | } |
| | | } |
| | | |
| | | return isEnabled |
| | | } |
| | | } |
| | | |
| | | func privateResignOnTouchOutside() -> Bool { |
| | | |
| | | var isEnabled: Bool = resignOnTouchOutside |
| | | |
| | | guard let textFieldViewInfo: IQTextFieldViewInfo = activeConfiguration.textFieldViewInfo else { |
| | | return isEnabled |
| | | } |
| | | |
| | | let enableMode: IQEnableMode = textFieldViewInfo.textFieldView.iq.resignOnTouchOutsideMode |
| | | |
| | | if enableMode == .enabled { |
| | | isEnabled = true |
| | | } else if enableMode == .disabled { |
| | | isEnabled = false |
| | | } else if var textFieldViewController = textFieldViewInfo.textFieldView.iq.viewContainingController() { |
| | | |
| | | // If it is searchBar textField embedded in Navigation Bar |
| | | if textFieldViewInfo.textFieldView.iq.textFieldSearchBar() != nil, |
| | | let navController: UINavigationController = textFieldViewController as? UINavigationController, |
| | | let topController: UIViewController = navController.topViewController { |
| | | textFieldViewController = topController |
| | | } |
| | | |
| | | // If viewController is kind of enable viewController class, then assuming resignOnTouchOutside is enabled. |
| | | if !isEnabled, |
| | | enabledTouchResignedClasses.contains(where: { textFieldViewController.isKind(of: $0) }) { |
| | | isEnabled = true |
| | | } |
| | | |
| | | if isEnabled { |
| | | |
| | | // If viewController is kind of disable viewController class, |
| | | // then assuming resignOnTouchOutside is disable. |
| | | if disabledTouchResignedClasses.contains(where: { textFieldViewController.isKind(of: $0) }) { |
| | | isEnabled = false |
| | | } |
| | | |
| | | // Special Controllers |
| | | if isEnabled { |
| | | |
| | | let classNameString: String = "\(type(of: textFieldViewController.self))" |
| | | |
| | | // _UIAlertControllerTextFieldViewController |
| | | if classNameString.contains("UIAlertController"), |
| | | classNameString.hasSuffix("TextFieldViewController") { |
| | | isEnabled = false |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return isEnabled |
| | | @available(iOSApplicationExtension, unavailable) |
| | | @MainActor |
| | | fileprivate extension IQTextInputView { |
| | | var internalEnableMode: IQEnableMode { |
| | | return iq.enableMode |
| | | } |
| | | } |
| | |
| | | // https://github.com/hackiftekhar/IQKeyboardManager |
| | | // Copyright (c) 2013-24 Iftekhar Qurashi. |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | |
| | | import UIKit |
| | | import IQKeyboardCore |
| | | |
| | | // swiftlint:disable file_length |
| | | @available(iOSApplicationExtension, unavailable) |
| | | public extension IQKeyboardManager { |
| | | @MainActor |
| | | @objc public extension IQKeyboardManager { |
| | | |
| | | private typealias IQLayoutGuide = (top: CGFloat, bottom: CGFloat) |
| | | |
| | | @MainActor |
| | | private struct AssociatedKeys { |
| | |
| | | } |
| | | |
| | | /** |
| | | moved distance to the top used to maintain distance between keyboard and textField. |
| | | moved distance to the top used to maintain distance between keyboard and textInputView. |
| | | Most of the time this will be a positive value. |
| | | */ |
| | | private(set) var movedDistance: CGFloat { |
| | |
| | | /** |
| | | Will be called then movedDistance will be changed |
| | | */ |
| | | @objc var movedDistanceChanged: ((CGFloat) -> Void)? { |
| | | var movedDistanceChanged: ((CGFloat) -> Void)? { |
| | | get { |
| | | return objc_getAssociatedObject(self, &AssociatedKeys.movedDistanceChanged) as? ((CGFloat) -> Void) |
| | | } |
| | |
| | | } |
| | | |
| | | /** Variable to save lastScrollView that was scrolled. */ |
| | | @nonobjc |
| | | internal var lastScrollViewConfiguration: IQScrollViewConfiguration? { |
| | | get { |
| | | return objc_getAssociatedObject(self, |
| | |
| | | } |
| | | |
| | | /** used to adjust contentInset of UITextView. */ |
| | | @nonobjc |
| | | internal var startingTextViewConfiguration: IQScrollViewConfiguration? { |
| | | get { |
| | | return objc_getAssociatedObject(self, |
| | |
| | | } |
| | | } |
| | | |
| | | internal func addActiveConfigurationObserver() { |
| | | activeConfiguration.registerChange(identifier: UUID().uuidString, changeHandler: { event, _, _ in |
| | | switch event { |
| | | case .show: |
| | | self.handleKeyboardTextFieldViewVisible() |
| | | case .change: |
| | | self.handleKeyboardTextFieldViewChanged() |
| | | case .hide: |
| | | self.handleKeyboardTextFieldViewHide() |
| | | } |
| | | }) |
| | | } |
| | | |
| | | @objc internal func applicationDidBecomeActive(_ notification: Notification) { |
| | | internal func applicationDidBecomeActive(_ notification: Notification) { |
| | | |
| | | guard privateIsEnabled(), |
| | | activeConfiguration.keyboardInfo.keyboardShowing, |
| | | activeConfiguration.keyboardInfo.isVisible, |
| | | activeConfiguration.isReady else { |
| | | return |
| | | } |
| | |
| | | } |
| | | |
| | | /* Adjusting RootViewController's frame according to interface orientation. */ |
| | | // swiftlint:disable cyclomatic_complexity |
| | | // swiftlint:disable function_body_length |
| | | internal func adjustPosition() { |
| | | |
| | | // We are unable to get textField object while keyboard showing on WKWebView's textField. (Bug ID: #11) |
| | | guard UIApplication.shared.applicationState == .active, |
| | | let textFieldView: UIView = activeConfiguration.textFieldViewInfo?.textFieldView, |
| | | let superview: UIView = textFieldView.superview, |
| | | let rootConfiguration = activeConfiguration.rootControllerConfiguration, |
| | | let window: UIWindow = rootConfiguration.rootController.view.window else { |
| | | let textInputView: any IQTextInputView = activeConfiguration.textInputView, |
| | | let superview: UIView = textInputView.superview, |
| | | let rootConfiguration = activeConfiguration.rootConfiguration, |
| | | let rootController: UIViewController = rootConfiguration.rootController, |
| | | let window: UIWindow = rootController.view.window else { |
| | | return |
| | | } |
| | | |
| | | showLog(">>>>> \(#function) started >>>>>", indentation: 1) |
| | | let startTime: CFTimeInterval = CACurrentMediaTime() |
| | | |
| | | let rootController: UIViewController = rootConfiguration.rootController |
| | | let textFieldViewRectInWindow: CGRect = superview.convert(textFieldView.frame, to: window) |
| | | let textFieldViewRectInRootSuperview: CGRect = superview.convert(textFieldView.frame, |
| | | defer { |
| | | showLog("<<<<< \(#function) ended <<<<<", indentation: -1) |
| | | } |
| | | |
| | | let textInputViewRectInWindow: CGRect = superview.convert(textInputView.frame, to: window) |
| | | let textInputViewRectInRootSuperview: CGRect = superview.convert(textInputView.frame, |
| | | to: rootController.view.superview) |
| | | |
| | | // Getting RootViewOrigin. |
| | | var rootViewOrigin: CGPoint = rootController.view.frame.origin |
| | | let rootViewOrigin: CGPoint = rootController.view.frame.origin |
| | | |
| | | let keyboardDistance: CGFloat |
| | | let keyboardDistance: CGFloat = getSpecialTextInputViewDistance(textInputView: textInputView) |
| | | |
| | | do { |
| | | // Maintain keyboardDistanceFromTextField |
| | | let specialKeyboardDistanceFromTextField: CGFloat |
| | | let kbSize: CGSize = Self.getKeyboardSize(keyboardDistance: keyboardDistance, |
| | | keyboardFrame: activeConfiguration.keyboardInfo.endFrame, |
| | | safeAreaInsets: rootConfiguration.beginSafeAreaInsets, |
| | | windowFrame: window.frame) |
| | | let originalKbSize: CGSize = activeConfiguration.keyboardInfo.endFrame.size |
| | | |
| | | if let searchBar: UIView = textFieldView.iq.textFieldSearchBar() { |
| | | specialKeyboardDistanceFromTextField = searchBar.iq.distanceFromKeyboard |
| | | } else { |
| | | specialKeyboardDistanceFromTextField = textFieldView.iq.distanceFromKeyboard |
| | | } |
| | | let isScrollableTextInputView: Bool |
| | | |
| | | if specialKeyboardDistanceFromTextField == UIView.defaultKeyboardDistance { |
| | | keyboardDistance = keyboardDistanceFromTextField |
| | | } else { |
| | | keyboardDistance = specialKeyboardDistanceFromTextField |
| | | } |
| | | } |
| | | |
| | | let kbSize: CGSize |
| | | let originalKbSize: CGSize = activeConfiguration.keyboardInfo.frame.size |
| | | |
| | | do { |
| | | var kbFrame: CGRect = activeConfiguration.keyboardInfo.frame |
| | | |
| | | kbFrame.origin.y -= keyboardDistance |
| | | kbFrame.size.height += keyboardDistance |
| | | |
| | | kbFrame.origin.y -= rootConfiguration.beginSafeAreaInsets.bottom |
| | | kbFrame.size.height += rootConfiguration.beginSafeAreaInsets.bottom |
| | | |
| | | // (Bug ID: #469) (Bug ID: #381) (Bug ID: #1506) |
| | | // Calculating actual keyboard covered size respect to window, |
| | | // keyboard frame may be different when hardware keyboard is attached |
| | | let intersectRect: CGRect = kbFrame.intersection(window.frame) |
| | | |
| | | if intersectRect.isNull { |
| | | kbSize = CGSize(width: kbFrame.size.width, height: 0) |
| | | } else { |
| | | kbSize = intersectRect.size |
| | | } |
| | | } |
| | | |
| | | let statusBarHeight: CGFloat |
| | | |
| | | let navigationBarAreaHeight: CGFloat |
| | | if let navigationController: UINavigationController = rootController.navigationController { |
| | | navigationBarAreaHeight = navigationController.navigationBar.frame.maxY |
| | | if let textInputView: UIScrollView = textInputView as? UITextView { |
| | | isScrollableTextInputView = textInputView.isScrollEnabled |
| | | } else { |
| | | statusBarHeight = window.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 |
| | | navigationBarAreaHeight = statusBarHeight |
| | | isScrollableTextInputView = false |
| | | } |
| | | |
| | | let isScrollableTextView: Bool |
| | | let layoutGuide: IQLayoutGuide = Self.getLayoutGuides(rootController: rootController, window: window, |
| | | isScrollableTextInputView: isScrollableTextInputView) |
| | | |
| | | if let textView: UIScrollView = textFieldView as? UIScrollView, |
| | | textFieldView.responds(to: #selector(getter: UITextView.isEditable)) { |
| | | isScrollableTextView = textView.isScrollEnabled |
| | | } else { |
| | | isScrollableTextView = false |
| | | } |
| | | |
| | | let directionalLayoutMargin: NSDirectionalEdgeInsets = rootController.view.directionalLayoutMargins |
| | | let topLayoutGuide: CGFloat = CGFloat.maximum(navigationBarAreaHeight, directionalLayoutMargin.top) |
| | | |
| | | // Validation of textView for case where there is a tab bar |
| | | // at the bottom or running on iPhone X and textView is at the bottom. |
| | | let bottomLayoutGuide: CGFloat = isScrollableTextView ? 0 : directionalLayoutMargin.bottom |
| | | |
| | | // Move positive = textField is hidden. |
| | | // Move negative = textField is showing. |
| | | // Move positive = textInputView is hidden. |
| | | // Move negative = textInputView is showing. |
| | | // Calculating move position. |
| | | var moveUp: CGFloat |
| | | |
| | | do { |
| | | let visibleHeight: CGFloat = window.frame.height-kbSize.height |
| | | |
| | | let topMovement: CGFloat = textFieldViewRectInRootSuperview.minY-topLayoutGuide |
| | | let bottomMovement: CGFloat = textFieldViewRectInWindow.maxY - visibleHeight + bottomLayoutGuide |
| | | moveUp = CGFloat.minimum(topMovement, bottomMovement) |
| | | moveUp = CGFloat(Int(moveUp)) |
| | | } |
| | | var moveUp: CGFloat = Self.getMoveUpDistance(keyboardSize: kbSize, |
| | | layoutGuide: layoutGuide, |
| | | textInputViewRectInRootSuperview: textInputViewRectInRootSuperview, |
| | | textInputViewRectInWindow: textInputViewRectInWindow, |
| | | windowFrame: window.frame) |
| | | |
| | | showLog("Need to move: \(moveUp), will be moving \(moveUp < 0 ? "down" : "up")") |
| | | |
| | | var superScrollView: UIScrollView? |
| | | var superView: UIScrollView? = textFieldView.iq.superviewOf(type: UIScrollView.self) |
| | | var superView: UIScrollView? = (textInputView as UIView).iq.superviewOf(type: UIScrollView.self) |
| | | |
| | | // Getting UIScrollView whose scrolling is enabled. // (Bug ID: #285) |
| | | while let view: UIScrollView = superView { |
| | |
| | | } |
| | | } |
| | | |
| | | // If there was a lastScrollView. // (Bug ID: #34) |
| | | setupActiveScrollViewConfiguration(superScrollView: superScrollView, textInputView: textInputView) |
| | | |
| | | // Special case for ScrollView. |
| | | // If we found lastScrollView then setting it's contentOffset to show textInputView. |
| | | if let lastScrollViewConfiguration: IQScrollViewConfiguration = lastScrollViewConfiguration { |
| | | adjustScrollViewContentOffsets(moveUp: &moveUp, textInputView: textInputView, |
| | | lastScrollViewConfiguration: lastScrollViewConfiguration, |
| | | rootSuperview: rootController.view.superview, layoutGuide: layoutGuide, |
| | | textInputViewRectInRootSuperview: textInputViewRectInRootSuperview, |
| | | isScrollableTextInputView: isScrollableTextInputView, window: window, |
| | | kbSize: kbSize, keyboardDistance: keyboardDistance, |
| | | rootBeginSafeAreaInsets: rootConfiguration.beginSafeAreaInsets) |
| | | } |
| | | |
| | | // Special case for UITextView |
| | | // (Readjusting textInputView.contentInset when textInputView hight is too big to fit on screen) |
| | | // _lastScrollView If not having inside any scrollView, now contentInset manages the full screen textInputView. |
| | | // If is a UITextView type |
| | | if isScrollableTextInputView, let textInputView = textInputView as? UITextView { |
| | | |
| | | adjustTextInputViewContentInset(window: window, originalKbSize: originalKbSize, |
| | | rootSuperview: rootController.view.superview, |
| | | layoutGuide: layoutGuide, |
| | | textInputView: textInputView) |
| | | } |
| | | |
| | | adjustRootController(moveUp: moveUp, rootViewOrigin: rootViewOrigin, originalKbSize: originalKbSize, |
| | | rootController: rootController, rootBeginOrigin: rootConfiguration.beginOrigin) |
| | | } |
| | | // swiftlint:enable function_body_length |
| | | |
| | | internal func restorePosition() { |
| | | |
| | | // Setting rootViewController frame to it's original position. // (Bug ID: #18) |
| | | guard let configuration: IQRootControllerConfiguration = activeConfiguration.rootConfiguration else { |
| | | return |
| | | } |
| | | showLog(">>>>> \(#function) started >>>>>", indentation: 1) |
| | | |
| | | defer { |
| | | showLog("<<<<< \(#function) ended <<<<<", indentation: -1) |
| | | } |
| | | |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | if configuration.hasChanged { |
| | | let classNameString: String = "\(type(of: configuration.rootController.self))" |
| | | self.showLog("Restoring \(classNameString) origin to: \(configuration.beginOrigin)") |
| | | } |
| | | configuration.restore() |
| | | |
| | | // Animating content if needed (Bug ID: #204) |
| | | if self.layoutIfNeededOnUpdate { |
| | | // Animating content (Bug ID: #160) |
| | | configuration.rootController?.view.setNeedsLayout() |
| | | configuration.rootController?.view.layoutIfNeeded() |
| | | } |
| | | }) |
| | | // Restoring the contentOffset of the lastScrollView |
| | | if let lastConfiguration: IQScrollViewConfiguration = lastScrollViewConfiguration { |
| | | // If we can't find current superScrollView, then setting lastScrollView to it's original form. |
| | | if superScrollView == nil { |
| | | let textInputView: (any IQTextInputView)? = activeConfiguration.textInputView |
| | | |
| | | if lastConfiguration.hasChanged { |
| | | if lastConfiguration.scrollView.contentInset != lastConfiguration.startingContentInset { |
| | | showLog("Restoring contentInset to: \(lastConfiguration.startingContentInset)") |
| | | } |
| | | restoreScrollViewConfigurationIfChanged(configuration: lastConfiguration, textInputView: textInputView) |
| | | |
| | | if lastConfiguration.scrollView.iq.restoreContentOffset, |
| | | !lastConfiguration.scrollView.contentOffset.equalTo(lastConfiguration.startingContentOffset) { |
| | | showLog("Restoring contentOffset to: \(lastConfiguration.startingContentOffset)") |
| | | } |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | // This is temporary solution. Have to implement the save and restore scrollView state |
| | | self.restoreScrollViewContentOffset(superScrollView: lastConfiguration.scrollView, |
| | | textInputView: textInputView) |
| | | }) |
| | | } |
| | | |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | lastConfiguration.restore(for: textFieldView) |
| | | }) |
| | | } |
| | | self.movedDistance = 0 |
| | | } |
| | | } |
| | | |
| | | self.lastScrollViewConfiguration = nil |
| | | } else if superScrollView != lastConfiguration.scrollView { |
| | | // If both scrollView's are different, |
| | | // then reset lastScrollView to it's original frame and setting current scrollView as last scrollView. |
| | | if lastConfiguration.hasChanged { |
| | | if lastConfiguration.scrollView.contentInset != lastConfiguration.startingContentInset { |
| | | showLog("Restoring contentInset to: \(lastConfiguration.startingContentInset)") |
| | | } |
| | | // swiftlint:disable function_parameter_count |
| | | @available(iOSApplicationExtension, unavailable) |
| | | @MainActor |
| | | private extension IQKeyboardManager { |
| | | |
| | | if lastConfiguration.scrollView.iq.restoreContentOffset, |
| | | !lastConfiguration.scrollView.contentOffset.equalTo(lastConfiguration.startingContentOffset) { |
| | | showLog("Restoring contentOffset to: \(lastConfiguration.startingContentOffset)") |
| | | } |
| | | func getSpecialTextInputViewDistance(textInputView: some IQTextInputView) -> CGFloat { |
| | | // Maintain keyboardDistance |
| | | let specialKeyboardDistance: CGFloat |
| | | |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | lastConfiguration.restore(for: textFieldView) |
| | | }) |
| | | } |
| | | if let searchBar: UISearchBar = textInputView.iq.textFieldSearchBar() { |
| | | specialKeyboardDistance = searchBar.iq.distanceFromKeyboard |
| | | } else { |
| | | specialKeyboardDistance = textInputView.iq.distanceFromKeyboard |
| | | } |
| | | |
| | | if let superScrollView = superScrollView { |
| | | let configuration = IQScrollViewConfiguration(scrollView: superScrollView, |
| | | canRestoreContentOffset: true) |
| | | self.lastScrollViewConfiguration = configuration |
| | | showLog(""" |
| | | if specialKeyboardDistance == UIView.defaultKeyboardDistance { |
| | | return keyboardDistance |
| | | } else { |
| | | return specialKeyboardDistance |
| | | } |
| | | } |
| | | |
| | | static func getKeyboardSize(keyboardDistance: CGFloat, keyboardFrame: CGRect, |
| | | safeAreaInsets: UIEdgeInsets, windowFrame: CGRect) -> CGSize { |
| | | let kbSize: CGSize |
| | | var kbFrame: CGRect = keyboardFrame |
| | | |
| | | kbFrame.origin.y -= keyboardDistance |
| | | kbFrame.size.height += keyboardDistance |
| | | |
| | | kbFrame.origin.y -= safeAreaInsets.bottom |
| | | kbFrame.size.height += safeAreaInsets.bottom |
| | | |
| | | // (Bug ID: #469) (Bug ID: #381) (Bug ID: #1506) |
| | | // Calculating actual keyboard covered size respect to window, |
| | | // keyboard frame may be different when hardware keyboard is attached |
| | | let intersectRect: CGRect = kbFrame.intersection(windowFrame) |
| | | |
| | | if intersectRect.isNull { |
| | | kbSize = CGSize(width: kbFrame.size.width, height: 0) |
| | | } else { |
| | | kbSize = intersectRect.size |
| | | } |
| | | return kbSize |
| | | } |
| | | |
| | | static private func getLayoutGuides(rootController: UIViewController, window: UIWindow, |
| | | isScrollableTextInputView: Bool) -> IQLayoutGuide { |
| | | let navigationBarAreaHeight: CGFloat |
| | | if let navigationController: UINavigationController = rootController.navigationController { |
| | | navigationBarAreaHeight = navigationController.navigationBar.frame.maxY |
| | | } else { |
| | | let statusBarHeight: CGFloat = window.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 |
| | | navigationBarAreaHeight = statusBarHeight |
| | | } |
| | | |
| | | let directionalLayoutMargin: NSDirectionalEdgeInsets = rootController.view.directionalLayoutMargins |
| | | let topLayoutGuide: CGFloat = CGFloat.maximum(navigationBarAreaHeight, directionalLayoutMargin.top) |
| | | |
| | | // Validation of textInputView for case where there is a tab bar |
| | | // at the bottom or running on iPhone X and textInputView is at the bottom. |
| | | let bottomLayoutGuide: CGFloat = isScrollableTextInputView ? 0 : directionalLayoutMargin.bottom |
| | | return (topLayoutGuide, bottomLayoutGuide) |
| | | } |
| | | |
| | | static private func getMoveUpDistance(keyboardSize: CGSize, |
| | | layoutGuide: IQLayoutGuide, |
| | | textInputViewRectInRootSuperview: CGRect, |
| | | textInputViewRectInWindow: CGRect, |
| | | windowFrame: CGRect) -> CGFloat { |
| | | |
| | | // Move positive = textInputView is hidden. |
| | | // Move negative = textInputView is showing. |
| | | // Calculating move position. |
| | | let visibleHeight: CGFloat = windowFrame.height-keyboardSize.height |
| | | |
| | | let topMovement: CGFloat = textInputViewRectInRootSuperview.minY-layoutGuide.top |
| | | let bottomMovement: CGFloat = textInputViewRectInWindow.maxY - visibleHeight + layoutGuide.bottom |
| | | var moveUp: CGFloat = CGFloat.minimum(topMovement, bottomMovement) |
| | | moveUp = CGFloat(Int(moveUp)) |
| | | return moveUp |
| | | } |
| | | |
| | | func setupActiveScrollViewConfiguration(superScrollView: UIScrollView?, textInputView: some IQTextInputView) { |
| | | // If there was a lastScrollView. // (Bug ID: #34) |
| | | guard let lastConfiguration: IQScrollViewConfiguration = lastScrollViewConfiguration else { |
| | | if let superScrollView: UIScrollView = superScrollView { |
| | | // If there was no lastScrollView and we found a current scrollView. then setting it as lastScrollView. |
| | | let configuration = IQScrollViewConfiguration(scrollView: superScrollView, |
| | | canRestoreContentOffset: true) |
| | | self.lastScrollViewConfiguration = configuration |
| | | showLog(""" |
| | | Saving ScrollView New contentInset: \(configuration.startingContentInset) |
| | | and contentOffset: \(configuration.startingContentOffset) |
| | | """) |
| | | } |
| | | return |
| | | } |
| | | |
| | | // If we can't find current superScrollView, then setting lastScrollView to it's original form. |
| | | if superScrollView == nil { |
| | | restoreScrollViewConfigurationIfChanged(configuration: lastConfiguration, |
| | | textInputView: textInputView) |
| | | self.lastScrollViewConfiguration = nil |
| | | } else if superScrollView != lastConfiguration.scrollView { |
| | | // If both scrollView's are different, |
| | | // then reset lastScrollView to it's original frame and setting current scrollView as last scrollView. |
| | | restoreScrollViewConfigurationIfChanged(configuration: lastConfiguration, |
| | | textInputView: textInputView) |
| | | |
| | | if let superScrollView = superScrollView { |
| | | let configuration = IQScrollViewConfiguration(scrollView: superScrollView, |
| | | canRestoreContentOffset: true) |
| | | self.lastScrollViewConfiguration = configuration |
| | | showLog(""" |
| | | Saving ScrollView New contentInset: \(configuration.startingContentInset) |
| | | and contentOffset: \(configuration.startingContentOffset) |
| | | """) |
| | | } else { |
| | | self.lastScrollViewConfiguration = nil |
| | | } |
| | | } else { |
| | | self.lastScrollViewConfiguration = nil |
| | | } |
| | | // Else the case where superScrollView == lastScrollView means we are on same scrollView |
| | | // after switching to different textField. So doing nothing, going ahead |
| | | } else if let superScrollView: UIScrollView = superScrollView { |
| | | // If there was no lastScrollView and we found a current scrollView. then setting it as lastScrollView. |
| | | } |
| | | // Else the case where superScrollView == lastScrollView means we are on same scrollView |
| | | // after switching to different textInputView. So doing nothing, going ahead |
| | | } |
| | | |
| | | let configuration = IQScrollViewConfiguration(scrollView: superScrollView, canRestoreContentOffset: true) |
| | | self.lastScrollViewConfiguration = configuration |
| | | showLog(""" |
| | | Saving ScrollView New contentInset: \(configuration.startingContentInset) |
| | | and contentOffset: \(configuration.startingContentOffset) |
| | | """) |
| | | func restoreScrollViewConfigurationIfChanged(configuration: IQScrollViewConfiguration, |
| | | textInputView: (some IQTextInputView)?) { |
| | | guard configuration.hasChanged else { return } |
| | | if configuration.scrollView.contentInset != configuration.startingContentInset { |
| | | showLog("Restoring contentInset to: \(configuration.startingContentInset)") |
| | | } |
| | | |
| | | // Special case for ScrollView. |
| | | // If we found lastScrollView then setting it's contentOffset to show textField. |
| | | if let lastScrollViewConfiguration: IQScrollViewConfiguration = lastScrollViewConfiguration { |
| | | // Saving |
| | | var lastView: UIView = textFieldView |
| | | var superScrollView: UIScrollView? = lastScrollViewConfiguration.scrollView |
| | | if configuration.scrollView.iq.restoreContentOffset, |
| | | !configuration.scrollView.contentOffset.equalTo(configuration.startingContentOffset) { |
| | | showLog("Restoring contentOffset to: \(configuration.startingContentOffset)") |
| | | } |
| | | |
| | | while let scrollView: UIScrollView = superScrollView { |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | configuration.restore(for: textInputView) |
| | | }) |
| | | } |
| | | |
| | | var isContinue: Bool = false |
| | | // swiftlint:disable function_body_length |
| | | private func adjustScrollViewContentOffsets(moveUp: inout CGFloat, textInputView: some IQTextInputView, |
| | | lastScrollViewConfiguration: IQScrollViewConfiguration, |
| | | rootSuperview: UIView?, |
| | | layoutGuide: IQLayoutGuide, |
| | | textInputViewRectInRootSuperview: CGRect, |
| | | isScrollableTextInputView: Bool, window: UIWindow, |
| | | kbSize: CGSize, keyboardDistance: CGFloat, |
| | | rootBeginSafeAreaInsets: UIEdgeInsets) { |
| | | // Saving |
| | | var lastView: UIView = textInputView |
| | | var superScrollView: UIScrollView? = lastScrollViewConfiguration.scrollView |
| | | |
| | | if moveUp > 0 { |
| | | isContinue = moveUp > (-scrollView.contentOffset.y - scrollView.contentInset.top) |
| | | while let scrollView: UIScrollView = superScrollView { |
| | | |
| | | } else if let tableView: UITableView = scrollView.iq.superviewOf(type: UITableView.self) { |
| | | // Special treatment for UITableView due to their cell reusing logic |
| | | var isContinue: Bool = false |
| | | |
| | | isContinue = scrollView.contentOffset.y > 0 |
| | | if moveUp > 0 { |
| | | isContinue = moveUp > (-scrollView.contentOffset.y - scrollView.contentInset.top) |
| | | |
| | | if isContinue, |
| | | let tableCell: UITableViewCell = textFieldView.iq.superviewOf(type: UITableViewCell.self), |
| | | let indexPath: IndexPath = tableView.indexPath(for: tableCell), |
| | | let previousIndexPath: IndexPath = tableView.previousIndexPath(of: indexPath) { |
| | | } else if let tableView: UITableView = scrollView.iq.superviewOf(type: UITableView.self) { |
| | | // Special treatment for UITableView due to their cell reusing logic |
| | | |
| | | let previousCellRect: CGRect = tableView.rectForRow(at: previousIndexPath) |
| | | if !previousCellRect.isEmpty { |
| | | let superview: UIView? = rootController.view.superview |
| | | let previousCellRectInRootSuperview: CGRect = tableView.convert(previousCellRect, |
| | | to: superview) |
| | | isContinue = scrollView.contentOffset.y > 0 |
| | | |
| | | moveUp = CGFloat.minimum(0, previousCellRectInRootSuperview.maxY - topLayoutGuide) |
| | | } |
| | | } |
| | | } else if let collectionView = scrollView.iq.superviewOf(type: UICollectionView.self) { |
| | | // Special treatment for UICollectionView due to their cell reusing logic |
| | | Self.handleTableViewCase(moveUp: &moveUp, isContinue: isContinue, textInputView: textInputView, |
| | | tableView: tableView, rootSuperview: rootSuperview, layoutGuide: layoutGuide) |
| | | } else if let collectionView = scrollView.iq.superviewOf(type: UICollectionView.self) { |
| | | // Special treatment for UICollectionView due to their cell reusing logic |
| | | |
| | | isContinue = scrollView.contentOffset.y > 0 |
| | | isContinue = scrollView.contentOffset.y > 0 |
| | | |
| | | if isContinue, |
| | | let collectionCell = textFieldView.iq.superviewOf(type: UICollectionViewCell.self), |
| | | let indexPath: IndexPath = collectionView.indexPath(for: collectionCell), |
| | | let previousIndexPath: IndexPath = collectionView.previousIndexPath(of: indexPath), |
| | | let attributes = collectionView.layoutAttributesForItem(at: previousIndexPath) { |
| | | Self.handleCollectionViewCase(moveUp: &moveUp, isContinue: isContinue, |
| | | textInputView: textInputView, collectionView: collectionView, |
| | | rootSuperview: rootSuperview, layoutGuide: layoutGuide) |
| | | } else { |
| | | isContinue = textInputViewRectInRootSuperview.minY < layoutGuide.top |
| | | |
| | | let previousCellRect: CGRect = attributes.frame |
| | | if !previousCellRect.isEmpty { |
| | | let superview: UIView? = rootController.view.superview |
| | | let previousCellRectInRootSuperview: CGRect = collectionView.convert(previousCellRect, |
| | | to: superview) |
| | | if isContinue { |
| | | moveUp = CGFloat.minimum(0, textInputViewRectInRootSuperview.minY - layoutGuide.top) |
| | | } |
| | | } |
| | | |
| | | moveUp = CGFloat.minimum(0, previousCellRectInRootSuperview.maxY - topLayoutGuide) |
| | | } |
| | | } |
| | | } else { |
| | | isContinue = textFieldViewRectInRootSuperview.minY < topLayoutGuide |
| | | // Looping in upper hierarchy until we don't found any scrollView |
| | | // in it's upper hierarchy till UIWindow object. |
| | | if isContinue { |
| | | |
| | | if isContinue { |
| | | moveUp = CGFloat.minimum(0, textFieldViewRectInRootSuperview.minY - topLayoutGuide) |
| | | var tempScrollView: UIScrollView? = scrollView.iq.superviewOf(type: UIScrollView.self) |
| | | var nextScrollView: UIScrollView? |
| | | while let view: UIScrollView = tempScrollView { |
| | | |
| | | if view.isScrollEnabled, !view.iq.ignoreScrollingAdjustment { |
| | | nextScrollView = view |
| | | break |
| | | } else { |
| | | tempScrollView = view.iq.superviewOf(type: UIScrollView.self) |
| | | } |
| | | } |
| | | |
| | | // Looping in upper hierarchy until we don't found any scrollView then |
| | | // in it's upper hierarchy till UIWindow object. |
| | | if isContinue { |
| | | // Getting lastViewRect. |
| | | if let lastViewRect: CGRect = lastView.superview?.convert(lastView.frame, to: scrollView) { |
| | | |
| | | var tempScrollView: UIScrollView? = scrollView.iq.superviewOf(type: UIScrollView.self) |
| | | var nextScrollView: UIScrollView? |
| | | while let view: UIScrollView = tempScrollView { |
| | | // Calculating the expected Y offset from move and scrollView's contentOffset. |
| | | let minimumMovement: CGFloat = CGFloat.minimum(scrollView.contentOffset.y, -moveUp) |
| | | var suggestedOffsetY: CGFloat = scrollView.contentOffset.y - minimumMovement |
| | | |
| | | if view.isScrollEnabled, !view.iq.ignoreScrollingAdjustment { |
| | | nextScrollView = view |
| | | break |
| | | } else { |
| | | tempScrollView = view.iq.superviewOf(type: UIScrollView.self) |
| | | } |
| | | // Rearranging the expected Y offset according to the view. |
| | | suggestedOffsetY = CGFloat.minimum(suggestedOffsetY, lastViewRect.minY) |
| | | |
| | | updateSuggestedOffsetYAndMoveUp(suggestedOffsetY: &suggestedOffsetY, moveUp: &moveUp, |
| | | isScrollableTextInputView: isScrollableTextInputView, |
| | | nextScrollView: nextScrollView, textInputView: textInputView, |
| | | window: window, layoutGuide: layoutGuide, |
| | | scrollViewContentOffset: scrollView.contentOffset) |
| | | |
| | | let newContentOffset: CGPoint = CGPoint(x: scrollView.contentOffset.x, y: suggestedOffsetY) |
| | | |
| | | if !scrollView.contentOffset.equalTo(newContentOffset) { |
| | | |
| | | updateScrollViewContentOffset(scrollView: scrollView, newContentOffset: newContentOffset, |
| | | moveUp: moveUp, textInputView: textInputView) |
| | | } |
| | | } |
| | | |
| | | // Getting lastViewRect. |
| | | if let lastViewRect: CGRect = lastView.superview?.convert(lastView.frame, to: scrollView) { |
| | | // Getting next lastView & superScrollView. |
| | | lastView = scrollView |
| | | superScrollView = nextScrollView |
| | | } else { |
| | | moveUp = 0 |
| | | break |
| | | } |
| | | } |
| | | |
| | | // Calculating the expected Y offset from move and scrollView's contentOffset. |
| | | let minimumMovement: CGFloat = CGFloat.minimum(scrollView.contentOffset.y, -moveUp) |
| | | var suggestedOffsetY: CGFloat = scrollView.contentOffset.y - minimumMovement |
| | | adjustScrollViewContentInset(lastScrollViewConfiguration: lastScrollViewConfiguration, window: window, |
| | | kbSize: kbSize, keyboardDistance: keyboardDistance, |
| | | rootBeginSafeAreaInsets: rootBeginSafeAreaInsets) |
| | | } |
| | | // swiftlint:enable function_body_length |
| | | |
| | | // Rearranging the expected Y offset according to the view. |
| | | suggestedOffsetY = CGFloat.minimum(suggestedOffsetY, lastViewRect.minY) |
| | | private static func handleTableViewCase(moveUp: inout CGFloat, isContinue: Bool, |
| | | textInputView: some IQTextInputView, tableView: UITableView, |
| | | rootSuperview: UIView?, layoutGuide: IQLayoutGuide) { |
| | | guard isContinue, |
| | | let tableCell: UITableViewCell = textInputView.iq.superviewOf(type: UITableViewCell.self), |
| | | let indexPath: IndexPath = tableView.indexPath(for: tableCell), |
| | | let previousIndexPath: IndexPath = tableView.previousIndexPath(of: indexPath) else { return } |
| | | |
| | | // [_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type |
| | | // nextScrollView == nil If processing scrollView is last scrollView in |
| | | // upper hierarchy (there is no other scrollView upper hierarchy.) |
| | | // [_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type |
| | | // suggestedOffsetY >= 0 suggestedOffsetY must be greater than in |
| | | // order to keep distance from navigationBar (Bug ID: #92) |
| | | if isScrollableTextView, |
| | | nextScrollView == nil, |
| | | suggestedOffsetY >= 0 { |
| | | let previousCellRect: CGRect = tableView.rectForRow(at: previousIndexPath) |
| | | guard !previousCellRect.isEmpty else { return } |
| | | |
| | | // Converting Rectangle according to window bounds. |
| | | if let superview: UIView = textFieldView.superview { |
| | | let previousCellRectInRootSuperview: CGRect = tableView.convert(previousCellRect, |
| | | to: rootSuperview) |
| | | |
| | | let currentTextFieldViewRect: CGRect = superview.convert(textFieldView.frame, |
| | | to: window) |
| | | moveUp = CGFloat.minimum(0, previousCellRectInRootSuperview.maxY - layoutGuide.top) |
| | | } |
| | | |
| | | // Calculating expected fix distance which needs to be managed from navigation bar |
| | | let expectedFixDistance: CGFloat = currentTextFieldViewRect.minY - topLayoutGuide |
| | | private static func handleCollectionViewCase(moveUp: inout CGFloat, isContinue: Bool, |
| | | textInputView: some IQTextInputView, collectionView: UICollectionView, |
| | | rootSuperview: UIView?, |
| | | layoutGuide: IQLayoutGuide) { |
| | | guard isContinue, |
| | | let collectionCell = textInputView.iq.superviewOf(type: UICollectionViewCell.self), |
| | | let indexPath: IndexPath = collectionView.indexPath(for: collectionCell), |
| | | let previousIndexPath: IndexPath = collectionView.previousIndexPath(of: indexPath), |
| | | let attributes = collectionView.layoutAttributesForItem(at: previousIndexPath) else { return } |
| | | |
| | | // Now if expectedOffsetY (scrollView.contentOffset.y + expectedFixDistance) |
| | | // is lower than current suggestedOffsetY, which means we're in a position where |
| | | // navigationBar up and hide, then reducing suggestedOffsetY with expectedOffsetY |
| | | // (scrollView.contentOffset.y + expectedFixDistance) |
| | | let expectedOffsetY: CGFloat = scrollView.contentOffset.y + expectedFixDistance |
| | | suggestedOffsetY = CGFloat.minimum(suggestedOffsetY, expectedOffsetY) |
| | | let previousCellRect: CGRect = attributes.frame |
| | | guard !previousCellRect.isEmpty else { return } |
| | | let previousCellRectInRootSuperview: CGRect = collectionView.convert(previousCellRect, |
| | | to: rootSuperview) |
| | | |
| | | // Setting move to 0 because now we don't want to move any view anymore |
| | | // (All will be managed by our contentInset logic. |
| | | moveUp = 0 |
| | | } else { |
| | | // Subtracting the Y offset from the move variable, |
| | | // because we are going to change scrollView's contentOffset.y to suggestedOffsetY. |
| | | moveUp -= (suggestedOffsetY-scrollView.contentOffset.y) |
| | | } |
| | | } else { |
| | | // Subtracting the Y offset from the move variable, |
| | | // because we are going to change scrollView's contentOffset.y to suggestedOffsetY. |
| | | moveUp -= (suggestedOffsetY-scrollView.contentOffset.y) |
| | | } |
| | | moveUp = CGFloat.minimum(0, previousCellRectInRootSuperview.maxY - layoutGuide.top) |
| | | } |
| | | |
| | | let newContentOffset: CGPoint = CGPoint(x: scrollView.contentOffset.x, y: suggestedOffsetY) |
| | | private func updateSuggestedOffsetYAndMoveUp(suggestedOffsetY: inout CGFloat, moveUp: inout CGFloat, |
| | | isScrollableTextInputView: Bool, nextScrollView: UIScrollView?, |
| | | textInputView: some IQTextInputView, window: UIWindow, |
| | | layoutGuide: IQLayoutGuide, |
| | | scrollViewContentOffset: CGPoint) { |
| | | // If is a UITextView type |
| | | // nextScrollView == nil |
| | | // If processing scrollView is last scrollView in upper hierarchy |
| | | // (there is no other scrollView in upper hierarchy.) |
| | | // |
| | | // suggestedOffsetY >= 0 |
| | | // suggestedOffsetY must be >= 0 in order to keep distance from navigationBar (Bug ID: #92) |
| | | guard isScrollableTextInputView, |
| | | nextScrollView == nil, |
| | | suggestedOffsetY >= 0, |
| | | let superview: UIView = textInputView.superview else { |
| | | // Subtracting the Y offset from the move variable, |
| | | // because we are going to change scrollView's contentOffset.y to suggestedOffsetY. |
| | | moveUp -= (suggestedOffsetY-scrollViewContentOffset.y) |
| | | return |
| | | } |
| | | |
| | | if !scrollView.contentOffset.equalTo(newContentOffset) { |
| | | let currentTextInputViewRect: CGRect = superview.convert(textInputView.frame, |
| | | to: window) |
| | | |
| | | showLog(""" |
| | | // Calculating expected fix distance which needs to be managed from navigation bar |
| | | let expectedFixDistance: CGFloat = currentTextInputViewRect.minY - layoutGuide.top |
| | | |
| | | // Now if expectedOffsetY (scrollView.contentOffset.y + expectedFixDistance) |
| | | // is lower than current suggestedOffsetY, which means we're in a position where |
| | | // navigationBar up and hide, then reducing suggestedOffsetY with expectedOffsetY |
| | | // (scrollView.contentOffset.y + expectedFixDistance) |
| | | let expectedOffsetY: CGFloat = scrollViewContentOffset.y + expectedFixDistance |
| | | suggestedOffsetY = CGFloat.minimum(suggestedOffsetY, expectedOffsetY) |
| | | |
| | | // Setting move to 0 because now we don't want to move any view anymore |
| | | // (All will be managed by our contentInset logic. |
| | | moveUp = 0 |
| | | } |
| | | |
| | | func updateScrollViewContentOffset(scrollView: UIScrollView, newContentOffset: CGPoint, |
| | | moveUp: CGFloat, textInputView: some IQTextInputView) { |
| | | showLog(""" |
| | | old contentOffset: \(scrollView.contentOffset) |
| | | new contentOffset: \(newContentOffset) |
| | | """) |
| | | self.showLog("Remaining Move: \(moveUp)") |
| | | showLog("Remaining Move: \(moveUp)") |
| | | |
| | | // Getting problem while using `setContentOffset:animated:`, So I used animation API. |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | // Getting problem while using `setContentOffset:animated:`, So I used animation API. |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | |
| | | // (Bug ID: #1365, #1508, #1541) |
| | | let stackView: UIStackView? = textFieldView.iq.superviewOf(type: UIStackView.self, |
| | | belowView: scrollView) |
| | | // (Bug ID: #1901, #1996) |
| | | let animatedContentOffset: Bool = stackView != nil || |
| | | scrollView is UICollectionView || |
| | | scrollView is UITableView |
| | | // (Bug ID: #1365, #1508, #1541) |
| | | let stackView: UIStackView? = textInputView.iq.superviewOf(type: UIStackView.self, |
| | | belowView: scrollView) |
| | | // (Bug ID: #1901, #1996) |
| | | let animatedContentOffset: Bool = stackView != nil || |
| | | scrollView is UICollectionView || |
| | | scrollView is UITableView |
| | | |
| | | if animatedContentOffset { |
| | | scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled) |
| | | } else { |
| | | scrollView.contentOffset = newContentOffset |
| | | } |
| | | }, completion: { |
| | | |
| | | if scrollView is UITableView || scrollView is UICollectionView { |
| | | // This will update the next/previous states |
| | | self.reloadInputViews() |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // Getting next lastView & superScrollView. |
| | | lastView = scrollView |
| | | superScrollView = nextScrollView |
| | | } else { |
| | | moveUp = 0 |
| | | break |
| | | } |
| | | if animatedContentOffset { |
| | | scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled) |
| | | } else { |
| | | scrollView.contentOffset = newContentOffset |
| | | } |
| | | }, completion: { |
| | | |
| | | // Updating contentInset |
| | | let lastScrollView = lastScrollViewConfiguration.scrollView |
| | | if let lastScrollViewRect: CGRect = lastScrollView.superview?.convert(lastScrollView.frame, to: window), |
| | | !lastScrollView.iq.ignoreContentInsetAdjustment { |
| | | |
| | | var bottomInset: CGFloat = (kbSize.height)-(window.frame.height-lastScrollViewRect.maxY) |
| | | let keyboardAndSafeArea: CGFloat = keyboardDistance + rootConfiguration.beginSafeAreaInsets.bottom |
| | | var bottomScrollIndicatorInset: CGFloat = bottomInset - keyboardAndSafeArea |
| | | |
| | | // Update the insets so that the scrollView doesn't shift incorrectly |
| | | // when the offset is near the bottom of the scroll view. |
| | | bottomInset = CGFloat.maximum(lastScrollViewConfiguration.startingContentInset.bottom, bottomInset) |
| | | let startingScrollInset: UIEdgeInsets = lastScrollViewConfiguration.startingScrollIndicatorInsets |
| | | bottomScrollIndicatorInset = CGFloat.maximum(startingScrollInset.bottom, |
| | | bottomScrollIndicatorInset) |
| | | |
| | | bottomInset -= lastScrollView.safeAreaInsets.bottom |
| | | bottomScrollIndicatorInset -= lastScrollView.safeAreaInsets.bottom |
| | | |
| | | var movedInsets: UIEdgeInsets = lastScrollView.contentInset |
| | | movedInsets.bottom = bottomInset |
| | | |
| | | if lastScrollView.contentInset != movedInsets { |
| | | showLog("old ContentInset: \(lastScrollView.contentInset) new ContentInset: \(movedInsets)") |
| | | |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | lastScrollView.contentInset = movedInsets |
| | | lastScrollView.layoutIfNeeded() // (Bug ID: #1996) |
| | | |
| | | var newScrollIndicatorInset: UIEdgeInsets = lastScrollView.verticalScrollIndicatorInsets |
| | | |
| | | newScrollIndicatorInset.bottom = bottomScrollIndicatorInset |
| | | lastScrollView.scrollIndicatorInsets = newScrollIndicatorInset |
| | | }) |
| | | } |
| | | if scrollView is UITableView || scrollView is UICollectionView { |
| | | // This will update the next/previous states |
| | | textInputView.reloadInputViews() |
| | | } |
| | | }) |
| | | } |
| | | |
| | | func adjustScrollViewContentInset(lastScrollViewConfiguration: IQScrollViewConfiguration, |
| | | window: UIWindow, kbSize: CGSize, keyboardDistance: CGFloat, |
| | | rootBeginSafeAreaInsets: UIEdgeInsets) { |
| | | |
| | | let lastScrollView = lastScrollViewConfiguration.scrollView |
| | | |
| | | guard let lastScrollViewRect: CGRect = lastScrollView.superview?.convert(lastScrollView.frame, to: window), |
| | | !lastScrollView.iq.ignoreContentInsetAdjustment else { return } |
| | | |
| | | // Updating contentInset |
| | | var bottomInset: CGFloat = (kbSize.height)-(window.frame.height-lastScrollViewRect.maxY) |
| | | let keyboardAndSafeArea: CGFloat = keyboardDistance + rootBeginSafeAreaInsets.bottom |
| | | var bottomScrollIndicatorInset: CGFloat = bottomInset - keyboardAndSafeArea |
| | | |
| | | // Update the insets so that the scrollView doesn't shift incorrectly |
| | | // when the offset is near the bottom of the scroll view. |
| | | bottomInset = CGFloat.maximum(lastScrollViewConfiguration.startingContentInset.bottom, bottomInset) |
| | | let startingScrollInset: UIEdgeInsets = lastScrollViewConfiguration.startingScrollIndicatorInsets |
| | | bottomScrollIndicatorInset = CGFloat.maximum(startingScrollInset.bottom, |
| | | bottomScrollIndicatorInset) |
| | | |
| | | bottomInset -= lastScrollView.safeAreaInsets.bottom |
| | | bottomScrollIndicatorInset -= lastScrollView.safeAreaInsets.bottom |
| | | |
| | | var movedInsets: UIEdgeInsets = lastScrollView.contentInset |
| | | movedInsets.bottom = bottomInset |
| | | |
| | | guard lastScrollView.contentInset != movedInsets else { return } |
| | | showLog(""" |
| | | old ContentInset: \(lastScrollView.contentInset) new ContentInset: \(movedInsets) |
| | | """) |
| | | |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | lastScrollView.contentInset = movedInsets |
| | | lastScrollView.layoutIfNeeded() // (Bug ID: #1996) |
| | | |
| | | var newScrollIndicatorInset: UIEdgeInsets = lastScrollView.verticalScrollIndicatorInsets |
| | | |
| | | newScrollIndicatorInset.bottom = bottomScrollIndicatorInset |
| | | lastScrollView.scrollIndicatorInsets = newScrollIndicatorInset |
| | | }) |
| | | } |
| | | |
| | | private func adjustTextInputViewContentInset(window: UIWindow, originalKbSize: CGSize, |
| | | rootSuperview: UIView?, |
| | | layoutGuide: IQLayoutGuide, |
| | | textInputView: UIScrollView) { |
| | | let keyboardYPosition: CGFloat = window.frame.height - originalKbSize.height |
| | | var rootSuperViewFrameInWindow: CGRect = window.frame |
| | | if let rootSuperview: UIView = rootSuperview { |
| | | rootSuperViewFrameInWindow = rootSuperview.convert(rootSuperview.bounds, to: window) |
| | | } |
| | | // Going ahead. No else if. |
| | | |
| | | // Special case for UITextView |
| | | // (Readjusting textView.contentInset when textView hight is too big to fit on screen) |
| | | // _lastScrollView If not having inside any scrollView, now contentInset manages the full screen textView. |
| | | // [_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type |
| | | if isScrollableTextView, let textView = textFieldView as? UIScrollView { |
| | | let keyboardOverlapping: CGFloat = rootSuperViewFrameInWindow.maxY - keyboardYPosition |
| | | |
| | | let keyboardYPosition: CGFloat = window.frame.height - originalKbSize.height |
| | | var rootSuperViewFrameInWindow: CGRect = window.frame |
| | | if let rootSuperview: UIView = rootController.view.superview { |
| | | rootSuperViewFrameInWindow = rootSuperview.convert(rootSuperview.bounds, to: window) |
| | | } |
| | | let availableHeight: CGFloat = rootSuperViewFrameInWindow.height-layoutGuide.top-keyboardOverlapping |
| | | let textInputViewHeight: CGFloat = CGFloat.minimum(textInputView.frame.height, availableHeight) |
| | | |
| | | let keyboardOverlapping: CGFloat = rootSuperViewFrameInWindow.maxY - keyboardYPosition |
| | | guard textInputView.frame.size.height-textInputView.contentInset.bottom>textInputViewHeight else { return } |
| | | // If frame is not change by library in past, then saving user textInputView properties (Bug ID: #92) |
| | | if startingTextViewConfiguration == nil { |
| | | startingTextViewConfiguration = IQScrollViewConfiguration(scrollView: textInputView, |
| | | canRestoreContentOffset: false) |
| | | } |
| | | |
| | | let availableHeight: CGFloat = rootSuperViewFrameInWindow.height-topLayoutGuide-keyboardOverlapping |
| | | let textViewHeight: CGFloat = CGFloat.minimum(textView.frame.height, availableHeight) |
| | | var newContentInset: UIEdgeInsets = textInputView.contentInset |
| | | newContentInset.bottom = textInputView.frame.size.height-textInputViewHeight |
| | | newContentInset.bottom -= textInputView.safeAreaInsets.bottom |
| | | |
| | | if textView.frame.size.height-textView.contentInset.bottom>textViewHeight { |
| | | // If frame is not change by library in past, then saving user textView properties (Bug ID: #92) |
| | | if startingTextViewConfiguration == nil { |
| | | startingTextViewConfiguration = IQScrollViewConfiguration(scrollView: textView, |
| | | canRestoreContentOffset: false) |
| | | } |
| | | |
| | | var newContentInset: UIEdgeInsets = textView.contentInset |
| | | newContentInset.bottom = textView.frame.size.height-textViewHeight |
| | | newContentInset.bottom -= textView.safeAreaInsets.bottom |
| | | |
| | | if textView.contentInset != newContentInset { |
| | | self.showLog(""" |
| | | \(textFieldView) Old UITextView.contentInset: \(textView.contentInset) |
| | | New UITextView.contentInset: \(newContentInset) |
| | | guard textInputView.contentInset != newContentInset else { return } |
| | | showLog(""" |
| | | \(textInputView) Old textInputView.contentInset: \(textInputView.contentInset) |
| | | New textInputView.contentInset: \(newContentInset) |
| | | """) |
| | | |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | |
| | | textView.contentInset = newContentInset |
| | | textView.layoutIfNeeded() // (Bug ID: #1996) |
| | | textView.scrollIndicatorInsets = newContentInset |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | textInputView.contentInset = newContentInset |
| | | textInputView.layoutIfNeeded() // (Bug ID: #1996) |
| | | textInputView.scrollIndicatorInsets = newContentInset |
| | | }) |
| | | } |
| | | |
| | | func adjustRootController(moveUp: CGFloat, rootViewOrigin: CGPoint, originalKbSize: CGSize, |
| | | rootController: UIViewController, rootBeginOrigin: CGPoint) { |
| | | // +Positive or zero. |
| | | var rootViewOrigin: CGPoint = rootViewOrigin |
| | | if moveUp >= 0 { |
| | | |
| | | rootViewOrigin.y = CGFloat.maximum(rootViewOrigin.y - moveUp, CGFloat.minimum(0, -originalKbSize.height)) |
| | |
| | | }) |
| | | } |
| | | |
| | | movedDistance = (rootConfiguration.beginOrigin.y-rootViewOrigin.y) |
| | | movedDistance = rootBeginOrigin.y-rootViewOrigin.y |
| | | } else { // -Negative |
| | | let disturbDistance: CGFloat = rootViewOrigin.y-rootConfiguration.beginOrigin.y |
| | | let disturbDistance: CGFloat = rootViewOrigin.y-rootBeginOrigin.y |
| | | |
| | | // disturbDistance Negative = frame disturbed. |
| | | // disturbDistance positive = frame not disturbed. |
| | |
| | | }) |
| | | } |
| | | |
| | | movedDistance = (rootConfiguration.beginOrigin.y-rootViewOrigin.y) |
| | | movedDistance = rootBeginOrigin.y-rootViewOrigin.y |
| | | } |
| | | } |
| | | |
| | | let elapsedTime: CFTimeInterval = CACurrentMediaTime() - startTime |
| | | showLog("<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1) |
| | | } |
| | | // swiftlint:enable cyclomatic_complexity |
| | | // swiftlint:enable function_body_length |
| | | |
| | | // swiftlint:disable cyclomatic_complexity |
| | | // swiftlint:disable function_body_length |
| | | internal func restorePosition() { |
| | | func restoreScrollViewContentOffset(superScrollView: UIScrollView, textInputView: (some IQTextInputView)?) { |
| | | var superScrollView: UIScrollView? = superScrollView |
| | | while let scrollView: UIScrollView = superScrollView { |
| | | |
| | | // Setting rootViewController frame to it's original position. // (Bug ID: #18) |
| | | guard let configuration: IQRootControllerConfiguration = activeConfiguration.rootControllerConfiguration else { |
| | | return |
| | | } |
| | | let startTime: CFTimeInterval = CACurrentMediaTime() |
| | | showLog(">>>>> \(#function) started >>>>>", indentation: 1) |
| | | let width: CGFloat = CGFloat.maximum(scrollView.contentSize.width, scrollView.frame.width) |
| | | let height: CGFloat = CGFloat.maximum(scrollView.contentSize.height, scrollView.frame.height) |
| | | let contentSize: CGSize = CGSize(width: width, height: height) |
| | | |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | if configuration.hasChanged { |
| | | let classNameString: String = "\(type(of: configuration.rootController.self))" |
| | | self.showLog("Restoring \(classNameString) origin to: \(configuration.beginOrigin)") |
| | | } |
| | | configuration.restore() |
| | | let minimumY: CGFloat = contentSize.height - scrollView.frame.height |
| | | |
| | | // Animating content if needed (Bug ID: #204) |
| | | if self.layoutIfNeededOnUpdate { |
| | | // Animating content (Bug ID: #160) |
| | | configuration.rootController.view.setNeedsLayout() |
| | | configuration.rootController.view.layoutIfNeeded() |
| | | } |
| | | }) |
| | | if minimumY < scrollView.contentOffset.y { |
| | | |
| | | // Restoring the contentOffset of the lastScrollView |
| | | if let lastConfiguration: IQScrollViewConfiguration = lastScrollViewConfiguration { |
| | | let textFieldView: UIView? = activeConfiguration.textFieldViewInfo?.textFieldView |
| | | let newContentOffset: CGPoint = CGPoint(x: scrollView.contentOffset.x, y: minimumY) |
| | | if !scrollView.contentOffset.equalTo(newContentOffset) { |
| | | |
| | | activeConfiguration.animate(alongsideTransition: { |
| | | |
| | | if lastConfiguration.hasChanged { |
| | | if lastConfiguration.scrollView.contentInset != lastConfiguration.startingContentInset { |
| | | self.showLog("Restoring contentInset to: \(lastConfiguration.startingContentInset)") |
| | | // (Bug ID: #1365, #1508, #1541) |
| | | let stackView: UIStackView? |
| | | if let textInputView: UIView = textInputView { |
| | | stackView = textInputView.iq.superviewOf(type: UIStackView.self, |
| | | belowView: scrollView) |
| | | } else { |
| | | stackView = nil |
| | | } |
| | | |
| | | if lastConfiguration.scrollView.iq.restoreContentOffset, |
| | | !lastConfiguration.scrollView.contentOffset.equalTo(lastConfiguration.startingContentOffset) { |
| | | self.showLog("Restoring contentOffset to: \(lastConfiguration.startingContentOffset)") |
| | | // (Bug ID: #1901, #1996) |
| | | let animatedContentOffset: Bool = stackView != nil || |
| | | scrollView is UICollectionView || |
| | | scrollView is UITableView |
| | | |
| | | if animatedContentOffset { |
| | | scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled) |
| | | } else { |
| | | scrollView.contentOffset = newContentOffset |
| | | } |
| | | |
| | | lastConfiguration.restore(for: textFieldView) |
| | | showLog("Restoring contentOffset to: \(newContentOffset)") |
| | | } |
| | | } |
| | | |
| | | // This is temporary solution. Have to implement the save and restore scrollView state |
| | | var superScrollView: UIScrollView? = lastConfiguration.scrollView |
| | | |
| | | while let scrollView: UIScrollView = superScrollView { |
| | | |
| | | let width: CGFloat = CGFloat.maximum(scrollView.contentSize.width, scrollView.frame.width) |
| | | let height: CGFloat = CGFloat.maximum(scrollView.contentSize.height, scrollView.frame.height) |
| | | let contentSize: CGSize = CGSize(width: width, height: height) |
| | | |
| | | let minimumY: CGFloat = contentSize.height - scrollView.frame.height |
| | | |
| | | if minimumY < scrollView.contentOffset.y { |
| | | |
| | | let newContentOffset: CGPoint = CGPoint(x: scrollView.contentOffset.x, y: minimumY) |
| | | if !scrollView.contentOffset.equalTo(newContentOffset) { |
| | | |
| | | // (Bug ID: #1365, #1508, #1541) |
| | | let stackView: UIStackView? = textFieldView?.iq.superviewOf(type: UIStackView.self, |
| | | belowView: scrollView) |
| | | |
| | | // (Bug ID: #1901, #1996) |
| | | let animatedContentOffset: Bool = stackView != nil || |
| | | scrollView is UICollectionView || |
| | | scrollView is UITableView |
| | | |
| | | if animatedContentOffset { |
| | | scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled) |
| | | } else { |
| | | scrollView.contentOffset = newContentOffset |
| | | } |
| | | |
| | | self.showLog("Restoring contentOffset to: \(newContentOffset)") |
| | | } |
| | | } |
| | | |
| | | superScrollView = scrollView.iq.superviewOf(type: UIScrollView.self) |
| | | } |
| | | }) |
| | | superScrollView = scrollView.iq.superviewOf(type: UIScrollView.self) |
| | | } |
| | | |
| | | self.movedDistance = 0 |
| | | let elapsedTime: CFTimeInterval = CACurrentMediaTime() - startTime |
| | | showLog("<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1) |
| | | } |
| | | // swiftlint:enable cyclomatic_complexity |
| | | // swiftlint:enable function_body_length |
| | | } |
| | | // swiftlint:enable function_parameter_count |
| | |
| | | // https://github.com/hackiftekhar/IQKeyboardManager |
| | | // Copyright (c) 2013-24 Iftekhar Qurashi. |
| | | // |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | // of this software and associated documentation files (the "Software"), to deal |
| | | // in the Software without restriction, including without limitation the rights |
| | | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | // copies of the Software, and to permit persons to whom the Software is |
| | | // furnished to do so, subject to the following conditions: |
| | | // |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // The above copyright notice and this permission notice shall be included in |
| | | // all copies or substantial portions of the Software. |
| | | // |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | // THE SOFTWARE. |
| | | |
| | | import UIKit |
| | | import CoreGraphics |
| | | import QuartzCore |
| | | |
| | | // MARK: IQToolbar tags |
| | | |
| | | // swiftlint:disable line_length |
| | | // A generic version of KeyboardManagement. (OLD DOCUMENTATION) LINK |
| | | // https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html |
| | | // https://developer.apple.com/documentation/uikit/keyboards_and_input/adjusting_your_layout_with_keyboard_layout_guide |
| | | // swiftlint:enable line_length |
| | | |
| | | /** |
| | | Code-less drop-in universal library allows to prevent issues of keyboard sliding up and cover UITextField/UITextView. |
| | | Code-less drop-in universal library allows to prevent issues of keyboard sliding up and cover TextInputView. |
| | | Neither need to write any code nor any setup required and much more. |
| | | */ |
| | | @available(iOSApplicationExtension, unavailable) |
| | | @MainActor |
| | | @objc public final class IQKeyboardManager: NSObject { |
| | | @objcMembers public final class IQKeyboardManager: NSObject { |
| | | |
| | | /** |
| | | Returns the default singleton instance. |
| | | */ |
| | | @MainActor |
| | | @objc public static let shared: IQKeyboardManager = .init() |
| | | public static let shared: IQKeyboardManager = .init() |
| | | |
| | | internal var activeConfiguration: IQActiveConfiguration = .init() |
| | | |
| | | // MARK: UIKeyboard handling |
| | | |
| | | /** |
| | | Enable/disable managing distance between keyboard and textField. |
| | | Enable/disable managing distance between keyboard and textInputView. |
| | | Default is YES(Enabled when class loads in `+(void)load` method). |
| | | */ |
| | | @objc public var enable: Bool = false { |
| | | |
| | | public var isEnabled: Bool = false { |
| | | didSet { |
| | | guard isEnabled != oldValue else { return } |
| | | // If not enable, enable it. |
| | | if enable, !oldValue { |
| | | if isEnabled { |
| | | // If keyboard is currently showing. |
| | | if activeConfiguration.keyboardInfo.keyboardShowing { |
| | | if activeConfiguration.keyboardInfo.isVisible { |
| | | adjustPosition() |
| | | } else { |
| | | restorePosition() |
| | | } |
| | | showLog("Enabled") |
| | | } else if !enable, oldValue { // If not disable, disable it. |
| | | } else { // If not disable, disable it. |
| | | restorePosition() |
| | | showLog("Disabled") |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | To set keyboard distance from textField. can't be less than zero. Default is 10.0. |
| | | To set keyboard distance from textInputView. can't be less than zero. Default is 10.0. |
| | | */ |
| | | @objc public var keyboardDistanceFromTextField: CGFloat = 10.0 |
| | | |
| | | // MARK: IQToolbar handling |
| | | |
| | | /** |
| | | Automatic add the IQToolbar functionality. Default is YES. |
| | | */ |
| | | @objc public var enableAutoToolbar: Bool = true { |
| | | didSet { |
| | | reloadInputViews() |
| | | showLog("enableAutoToolbar: \(enableAutoToolbar ? "Yes" : "NO")") |
| | | } |
| | | } |
| | | |
| | | internal var activeConfiguration: IQActiveConfiguration = .init() |
| | | |
| | | /** |
| | | Configurations related to the toolbar display over the keyboard. |
| | | */ |
| | | @objc public let toolbarConfiguration: IQToolbarConfiguration = .init() |
| | | |
| | | /** |
| | | Configuration related to keyboard appearance |
| | | */ |
| | | @objc public let keyboardConfiguration: IQKeyboardConfiguration = .init() |
| | | |
| | | // MARK: UITextField/UITextView Next/Previous/Resign handling |
| | | |
| | | /** |
| | | Resigns Keyboard on touching outside of UITextField/View. Default is NO. |
| | | */ |
| | | @objc public var resignOnTouchOutside: Bool = false { |
| | | |
| | | didSet { |
| | | resignFirstResponderGesture.isEnabled = privateResignOnTouchOutside() |
| | | |
| | | showLog("resignOnTouchOutside: \(resignOnTouchOutside ? "Yes" : "NO")") |
| | | } |
| | | } |
| | | |
| | | /** TapGesture to resign keyboard on view's touch. |
| | | It's a readonly property and exposed only for adding/removing dependencies |
| | | if your added gesture does have collision with this one |
| | | */ |
| | | @objc public lazy var resignFirstResponderGesture: UITapGestureRecognizer = { |
| | | |
| | | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapRecognized(_:))) |
| | | tapGesture.cancelsTouchesInView = false |
| | | tapGesture.delegate = self |
| | | |
| | | return tapGesture |
| | | }() |
| | | public var keyboardDistance: CGFloat = 10.0 |
| | | |
| | | /*******************************************/ |
| | | |
| | | /** |
| | | Resigns currently first responder field. |
| | | */ |
| | | @discardableResult |
| | | @objc public func resignFirstResponder() -> Bool { |
| | | |
| | | guard let textFieldRetain: UIView = activeConfiguration.textFieldViewInfo?.textFieldView else { |
| | | return false |
| | | } |
| | | |
| | | // Resigning first responder |
| | | guard textFieldRetain.resignFirstResponder() else { |
| | | showLog("Refuses to resign first responder: \(textFieldRetain)") |
| | | // If it refuses then becoming it as first responder again. (Bug ID: #96) |
| | | // If it refuses to resign then becoming it first responder again for getting notifications callback. |
| | | textFieldRetain.becomeFirstResponder() |
| | | return false |
| | | } |
| | | return true |
| | | } |
| | | |
| | | // MARK: UISound handling |
| | | |
| | | /** |
| | | If YES, then it plays inputClick sound on next/previous/done click. |
| | | */ |
| | | @objc public var playInputClicks: Bool = true |
| | | |
| | | // MARK: UIAnimation handling |
| | | |
| | | /** |
| | | If YES, then calls 'setNeedsLayout' and 'layoutIfNeeded' on any frame update of to viewController's view. |
| | | */ |
| | | @objc public var layoutIfNeededOnUpdate: Bool = false |
| | | public var layoutIfNeededOnUpdate: Bool = false |
| | | |
| | | // MARK: Class Level disabling methods |
| | | |
| | |
| | | Disable distance handling within the scope of disabled distance handling viewControllers classes. |
| | | Within this scope, 'enabled' property is ignored. Class should be kind of UIViewController. |
| | | */ |
| | | @objc public var disabledDistanceHandlingClasses: [UIViewController.Type] = [] |
| | | public var disabledDistanceHandlingClasses: [UIViewController.Type] = [ |
| | | UITableViewController.self, |
| | | UIInputViewController.self, |
| | | UIAlertController.self |
| | | ] |
| | | |
| | | /** |
| | | Enable distance handling within the scope of enabled distance handling viewControllers classes. |
| | |
| | | If same Class is added in disabledDistanceHandlingClasses list, |
| | | then enabledDistanceHandlingClasses will be ignored. |
| | | */ |
| | | @objc public var enabledDistanceHandlingClasses: [UIViewController.Type] = [] |
| | | public var enabledDistanceHandlingClasses: [UIViewController.Type] = [] |
| | | |
| | | /** |
| | | Disable automatic toolbar creation within the scope of disabled toolbar viewControllers classes. |
| | | Within this scope, 'enableAutoToolbar' property is ignored. Class should be kind of UIViewController. |
| | | */ |
| | | @objc public var disabledToolbarClasses: [UIViewController.Type] = [] |
| | | |
| | | /** |
| | | Enable automatic toolbar creation within the scope of enabled toolbar viewControllers classes. |
| | | Within this scope, 'enableAutoToolbar' property is ignored. Class should be kind of UIViewController. |
| | | If same Class is added in disabledToolbarClasses list, then enabledToolbarClasses will be ignore. |
| | | */ |
| | | @objc public var enabledToolbarClasses: [UIViewController.Type] = [] |
| | | |
| | | /** |
| | | Allowed subclasses of UIView to add all inner textField, |
| | | this will allow to navigate between textField contains in different superview. |
| | | Class should be kind of UIView. |
| | | */ |
| | | @objc public var toolbarPreviousNextAllowedClasses: [UIView.Type] = [] |
| | | |
| | | /** |
| | | Disabled classes to ignore resignOnTouchOutside' property, Class should be kind of UIViewController. |
| | | */ |
| | | @objc public var disabledTouchResignedClasses: [UIViewController.Type] = [] |
| | | |
| | | /** |
| | | Enabled classes to forcefully enable 'resignOnTouchOutside' property. |
| | | Class should be kind of UIViewController |
| | | . If same Class is added in disabledTouchResignedClasses list, then enabledTouchResignedClasses will be ignored. |
| | | */ |
| | | @objc public var enabledTouchResignedClasses: [UIViewController.Type] = [] |
| | | |
| | | /** |
| | | if resignOnTouchOutside is enabled then you can customize the behavior |
| | | to not recognize gesture touches on some specific view subclasses. |
| | | Class should be kind of UIView. Default is [UIControl, UINavigationBar] |
| | | */ |
| | | @objc public var touchResignedGestureIgnoreClasses: [UIView.Type] = [] |
| | | |
| | | // MARK: Third Party Library support |
| | | /// Add TextField/TextView Notifications customized Notifications. |
| | | /// For example while using YYTextView https://github.com/ibireme/YYText |
| | | |
| | | /**************************************************************************************/ |
| | | /**************************************************************************************/ |
| | | |
| | | // MARK: Initialization/De-initialization |
| | | |
| | | /* Singleton Object Initialization. */ |
| | | override init() { |
| | | private override init() { |
| | | |
| | | super.init() |
| | | |
| | | self.addActiveConfigurationObserver() |
| | | |
| | | // Creating gesture for resignOnTouchOutside. (Enhancement ID: #14) |
| | | resignFirstResponderGesture.isEnabled = resignOnTouchOutside |
| | | |
| | | disabledDistanceHandlingClasses.append(UITableViewController.self) |
| | | disabledDistanceHandlingClasses.append(UIInputViewController.self) |
| | | disabledDistanceHandlingClasses.append(UIAlertController.self) |
| | | |
| | | disabledToolbarClasses.append(UIAlertController.self) |
| | | disabledToolbarClasses.append(UIInputViewController.self) |
| | | |
| | | disabledTouchResignedClasses.append(UIAlertController.self) |
| | | disabledTouchResignedClasses.append(UIInputViewController.self) |
| | | |
| | | toolbarPreviousNextAllowedClasses.append(UITableView.self) |
| | | toolbarPreviousNextAllowedClasses.append(UICollectionView.self) |
| | | toolbarPreviousNextAllowedClasses.append(IQPreviousNextView.self) |
| | | |
| | | touchResignedGestureIgnoreClasses.append(UIControl.self) |
| | | touchResignedGestureIgnoreClasses.append(UINavigationBar.self) |
| | | |
| | | NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), |
| | | name: UIApplication.didBecomeActiveNotification, object: nil) |
| | | |
| | | // (Bug ID: #550) |
| | | // Loading IQToolbar, IQTitleBarButtonItem, IQBarButtonItem to fix first time keyboard appearance delay |
| | | // If you experience exception breakpoint issue at below line then try these solutions |
| | | // https://stackoverflow.com/questions/27375640/all-exception-break-point-is-stopping-for-no-reason-on-simulator |
| | | DispatchQueue.main.async { |
| | | let textField: UIView = UITextField() |
| | | textField.iq.addDone(target: nil, action: #selector(self.doneAction(_:))) |
| | | textField.iq.addPreviousNextDone(target: nil, previousAction: #selector(self.previousAction(_:)), |
| | | nextAction: #selector(self.nextAction(_:)), |
| | | doneAction: #selector(self.doneAction(_:))) |
| | | } |
| | | } |
| | | |
| | | deinit { |
| | | // Disable the keyboard manager. |
| | | enable = false |
| | | isEnabled = false |
| | | } |
| | | |
| | | // MARK: Public Methods |
| | | |
| | | /* Refreshes textField/textView position if any external changes is explicitly made by user. */ |
| | | @objc public func reloadLayoutIfNeeded() { |
| | | /* Refreshes textInputView position if any external changes is explicitly made by user. */ |
| | | public func reloadLayoutIfNeeded() { |
| | | |
| | | guard privateIsEnabled(), |
| | | activeConfiguration.keyboardInfo.keyboardShowing, |
| | | activeConfiguration.keyboardInfo.isVisible, |
| | | activeConfiguration.isReady else { |
| | | return |
| | | } |
| | | adjustPosition() |
| | | } |
| | | } |
| | | |
| | | @available(iOSApplicationExtension, unavailable) |
| | | extension IQKeyboardManager: UIGestureRecognizerDelegate { |
| | | |
| | | /** Resigning on tap gesture. (Enhancement ID: #14)*/ |
| | | @objc private func tapRecognized(_ gesture: UITapGestureRecognizer) { |
| | | |
| | | if gesture.state == .ended { |
| | | |
| | | // Resigning currently responder textField. |
| | | resignFirstResponder() |
| | | } |
| | | } |
| | | |
| | | /** Note: returning YES is guaranteed to allow simultaneous recognition. |
| | | returning NO is not guaranteed to prevent simultaneous recognition, |
| | | as the other gesture's delegate may return YES. |
| | | */ |
| | | @objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, |
| | | shouldRecognizeSimultaneouslyWith |
| | | otherGestureRecognizer: UIGestureRecognizer) -> Bool { |
| | | return false |
| | | } |
| | | |
| | | /** |
| | | To not detect touch events in a subclass of UIControl, |
| | | these may have added their own selector for specific work |
| | | */ |
| | | @objc public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, |
| | | shouldReceive touch: UITouch) -> Bool { |
| | | // (Bug ID: #145) |
| | | // Should not recognize gesture if the clicked view is either UIControl or UINavigationBar(<Back button etc...) |
| | | |
| | | for ignoreClass in touchResignedGestureIgnoreClasses where touch.view?.isKind(of: ignoreClass) ?? false { |
| | | return false |
| | | } |
| | | |
| | | return true |
| | | } |
| | | |
| | | } |
| | |
| | | <p align="center"> |
| | | <img src="https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Demo/Resources/icon.png" alt="Icon"/> |
| | | <img src="https://raw.githubusercontent.com/hackiftekhar/IQKeyboardManager/master/Screenshot/Social.png" alt="Icon"/> |
| | | </p> |
| | | <H1 align="center">IQKeyboardManager</H1> |
| | | <p align="center"> |
| | | <img src="https://img.shields.io/github/license/hackiftekhar/IQKeyboardManager.svg" |
| | | alt="GitHub license"/> |
| | | |
| | | |
| | | []([https://travis-ci.org/hackiftekhar/IQKeyboardManager](https://github.com/hackiftekhar/IQKeyboardManager/blob/master/LICENSE.md)) |
| | | [](https://travis-ci.org/hackiftekhar/IQKeyboardManager) |
| | |  |
| | | [](http://cocoadocs.org/docsets/IQKeyboardManagerSwift) |
| | | [](https://github.com/hackiftekhar/IQKeyboardManager/tags) |
| | | |
| | | ## Big updates are coming soon! |
| | | |
| | | #### First of all, Thank You for using IQKeyboardManager! |
| | | It's been 11 years since it's first release in 2013. The library has grown a lot and we have added many new features since then. |
| | | |
| | | #### Motivation |
| | | Recently while working on bug fixes, I realized that in 2013 there were only 2 files IQKeyboardManager.{h,m} in Objective-C version, while now in Swift version there are 50+ files which makes the debugging a lot difficult than before. Also some of the features are rarely used in apps. |
| | | |
| | | #### New Idea |
| | | I realized that some of the features are not tightly linked to each other and can be moved out of the library easily. For Example:- |
| | | - `IQTextView` class |
| | | - `IQKeyboardListener` class |
| | | - `IQTextFieldViewListener` class |
| | | - `IQReturnKeyHandler` class |
| | | - Toolbar related features like `IQToolbar` and `IQBarButtonItem` and their support classes. |
| | | - ... |
| | | |
| | | Moving above things out will make the library more lightweight and user can plug in/out features as per their needs. |
| | | |
| | | #### Action Plan |
| | | I have decided to move loosly linked features out, and publish them to their separate github repo, and use them as dependencies as per requirements. |
| | | |
| | | Below are the action plans |
| | | - [x] Publish [IQKeyboardCore](https://github.com/hackiftekhar/IQKeyboardCore) |
| | | - This contains necessary classes and functions to be used by IQKeyboardManager related libraries. Please note that you shouldn't directly install this as dependency |
| | | - [x] Publish [IQTextView](https://github.com/hackiftekhar/IQTextView) |
| | | - This is purely separated a separated library now. |
| | | - This usually used for showing placeholder in UITextView |
| | | - [x] Publish [IQKeyboardReturnManager](https://github.com/hackiftekhar/IQKeyboardReturnManager) |
| | | - This is a renamed of `IQReturnKeyHandler`. This is also separated from the library and can be used independently. |
| | | - This depends on `IQKeyboardCore` for `TextInputView` type confirmation. |
| | | - [x] Publish [IQTextInputViewNotification](https://github.com/hackiftekhar/IQTextInputViewNotification) |
| | | - This is a renamed of `IQTextFieldViewListener`. This can be used independently to subscribe/unsubscribe for UITextView/UITextField beginEditing/endEditing events. |
| | | - This depends on the `IQKeyboardCore` to add some additional customized features for UITextView/UITextField. |
| | | - [x] Publish [IQKeyboardToolbar](https://github.com/hackiftekhar/IQKeyboardToolbar) |
| | | - This contains toolbar related classes like IQKeyboardToolbar, IQBarButtonItem, IQTitleBarButtonItems, their configuration classes and other useful functions to add toolbar in keyboard. This can be used independently to add toolbar in keyboard. |
| | | - This depends on the `IQKeyboardCore` to add some additional customized features for UITextView/UITextField. |
| | | - [x] Publish [IQKeyboardToolbarManager](https://github.com/hackiftekhar/IQKeyboardToolbarManager) |
| | | - This is something similar to IQKeyboardManager. This has been moved out of the library as a huge update. |
| | | - This depends on the `IQTextInputViewNotification` to know which textField is currently in focus. |
| | | - This depends on the `IQKeyboardToolbar` to add/remove toolbars over keyboard. |
| | | - [x] Publish [IQKeyboardNotification](https://github.com/hackiftekhar/IQKeyboardNotification) |
| | | - This is a renamed of `IQKeyboardListener`. This can be used independently to subscribe/unsubscribe for keyboard events. |
| | | - [ ] Publish [IQKeyboardManager](https://github.com/hackiftekhar/IQKeyboardManager) 7.2.0 for all the current support without any compilation error but by deprecating most of the things which are moved out of the library. |
| | | - This now only contains functions for handling distance between UITextView/UITextField and their useful functions. |
| | | - This depends on the `IQKeyboardNotification` to get keyboard notification callbacks. |
| | | - This depends on the `IQTextInputViewNotification` to know which textField is currently in focus. |
| | | - Now there are also subspecs for now as of 7.2.0, but some of them will be removed in 8.0.0 because we already have separate library for this. |
| | | - `IQKeyboardManagerSwift/Appearance` |
| | | - `IQKeyboardManagerSwift/IQKeyboardReturnKeyHandler` |
| | | - `IQKeyboardManagerSwift/IQKeyboardToolbarManager` |
| | | - `IQKeyboardManagerSwift/IQKeyboardToolbarManager/IQKeyboardToolbar` |
| | | - `IQKeyboardManagerSwift/IQTextView` |
| | | - `IQKeyboardManagerSwift/Resign` |
| | | - [ ] Bug fixes which may have arrived due to the library segregation. |
| | | - We need your support on this one. |
| | | - [ ] Publish [IQKeyboardManager](https://github.com/hackiftekhar/IQKeyboardManager) 8.0.0 by marking deprecated classes as unavailable. |
| | | - In this release we will be removing all the deprecated classes and marking some of them as unavailable for easier migration. |
| | | |
| | | ## Introduction |
| | | While developing iOS apps, we often run into issues where the iPhone keyboard slides up and covers the `UITextField/UITextView`. `IQKeyboardManager` allows you to prevent this issue of keyboard sliding up and covering `UITextField/UITextView` without needing you to write any code or make any additional setup. To use `IQKeyboardManager` you simply need to add source files to your project. |
| | | |
| | | |
| | | #### Key Features |
| | | ## Key Features |
| | | |
| | | 1) `One Lines of Code` |
| | | |
| | |
| | | - If **IQKeyboardManager** conflicts with other **third-party library**, then it's **developer responsibility** to **enable/disable IQKeyboardManager** when **presenting/dismissing** third-party library UI. Third-party libraries are not responsible to handle IQKeyboardManager. |
| | | |
| | | ## Requirements |
| | | []() |
| | | |
| | | | | Language | Minimum iOS Target | Minimum Xcode Version | |
| | | |------------------------|----------|--------------------|-----------------------| |
| | | | IQKeyboardManager | Obj-C | iOS 11.0 | Xcode 13 | |
| | | | IQKeyboardManager | Obj-C | iOS 13.0 | Xcode 13 | |
| | | | IQKeyboardManagerSwift | Swift | iOS 13.0 | Xcode 13 | |
| | | | Demo Project | | | Xcode 15 | |
| | | |
| | |
| | | ========================== |
| | | |
| | | #### Installation with CocoaPods |
| | | |
| | | [](http://cocoadocs.org/docsets/IQKeyboardManager) |
| | | |
| | | ***IQKeyboardManager (Objective-C):*** IQKeyboardManager is available through [CocoaPods](http://cocoapods.org). To install |
| | | it, simply add the following line to your Podfile: ([#9](https://github.com/hackiftekhar/IQKeyboardManager/issues/9)) |
| | |
| | | |
| | | |
| | | #### Installation with Source Code |
| | | |
| | | []() |
| | | |
| | | |
| | | |
| | | ***IQKeyboardManager (Objective-C):*** Just ***drag and drop*** `IQKeyboardManager` directory from demo project to your project. That's it. |
| | | |
| | |
| | | |
| | | ], |
| | | "RxSwift": [ |
| | | |
| | | "~>6.9.0" |
| | | ], |
| | | "RxCocoa": [ |
| | | |
| | | "~>6.9.0" |
| | | ], |
| | | "RxDataSources": [ |
| | | |
| | | "~>5.0.0" |
| | | ], |
| | | "UserDefaultsStore": [ |
| | | "~>1.5.0" |
| | |
| | | PODS: |
| | | - Alamofire (5.9.1) |
| | | - Alamofire (5.10.2) |
| | | - AliyunOSSiOS (2.10.22) |
| | | - APNGKit (2.3.0): |
| | | - Delegate (~> 1.3) |
| | |
| | | - EmptyDataSet-Swift (5.0.0) |
| | | - FFPage (3.0.0) |
| | | - HandyJSON (5.0.2) |
| | | - IQKeyboardCore (1.0.5) |
| | | - IQKeyboardManager (6.5.19) |
| | | - IQKeyboardManagerSwift (7.1.1) |
| | | - IQKeyboardManagerSwift (8.0.0): |
| | | - IQKeyboardManagerSwift/Appearance (= 8.0.0) |
| | | - IQKeyboardManagerSwift/Core (= 8.0.0) |
| | | - IQKeyboardManagerSwift/IQKeyboardReturnManager (= 8.0.0) |
| | | - IQKeyboardManagerSwift/IQKeyboardToolbarManager (= 8.0.0) |
| | | - IQKeyboardManagerSwift/IQTextView (= 8.0.0) |
| | | - IQKeyboardManagerSwift/Resign (= 8.0.0) |
| | | - IQKeyboardManagerSwift/Appearance (8.0.0): |
| | | - IQKeyboardManagerSwift/Core |
| | | - IQKeyboardManagerSwift/Core (8.0.0): |
| | | - IQKeyboardNotification |
| | | - IQTextInputViewNotification |
| | | - IQKeyboardManagerSwift/IQKeyboardReturnManager (8.0.0): |
| | | - IQKeyboardReturnManager |
| | | - IQKeyboardManagerSwift/IQKeyboardToolbarManager (8.0.0): |
| | | - IQKeyboardManagerSwift/Core |
| | | - IQKeyboardToolbarManager |
| | | - IQKeyboardManagerSwift/IQTextView (8.0.0): |
| | | - IQTextView |
| | | - IQKeyboardManagerSwift/Resign (8.0.0): |
| | | - IQKeyboardManagerSwift/Core |
| | | - IQKeyboardNotification (1.0.3) |
| | | - IQKeyboardReturnManager (1.0.4): |
| | | - IQKeyboardCore (= 1.0.5) |
| | | - IQKeyboardToolbar (1.1.1): |
| | | - IQKeyboardCore |
| | | - IQKeyboardToolbar/Core (= 1.1.1) |
| | | - IQKeyboardToolbar/Core (1.1.1): |
| | | - IQKeyboardCore |
| | | - IQKeyboardToolbar/Placeholderable |
| | | - IQKeyboardToolbar/Placeholderable (1.1.1): |
| | | - IQKeyboardCore |
| | | - IQKeyboardToolbarManager (1.1.3): |
| | | - IQKeyboardToolbar |
| | | - IQTextInputViewNotification |
| | | - IQTextInputViewNotification (1.0.8): |
| | | - IQKeyboardCore |
| | | - IQTextView (1.0.5): |
| | | - IQKeyboardToolbar/Placeholderable |
| | | - JQTools (0.1.5): |
| | | - EmptyDataSet-Swift |
| | | - HandyJSON |
| | |
| | | - MJRefresh |
| | | - ObjectMapper |
| | | - QMUIKit (~> 4.7.0) |
| | | - RxCocoa |
| | | - RxDataSources |
| | | - RxSwift |
| | | - RxCocoa (~> 6.9.0) |
| | | - RxDataSources (~> 5.0.0) |
| | | - RxSwift (~> 6.9.0) |
| | | - SDWebImage |
| | | - SnapKit |
| | | - SVProgressHUD |
| | |
| | | - QMUIKit/QMUILog |
| | | - QMUIKit/QMUIResources (4.7.0) |
| | | - QMUIKit/QMUIWeakObjectContainer (4.7.0) |
| | | - RxCocoa (6.7.1): |
| | | - RxRelay (= 6.7.1) |
| | | - RxSwift (= 6.7.1) |
| | | - RxCocoa (6.9.0): |
| | | - RxRelay (= 6.9.0) |
| | | - RxSwift (= 6.9.0) |
| | | - RxDataSources (5.0.0): |
| | | - Differentiator (~> 5.0) |
| | | - RxCocoa (~> 6.0) |
| | | - RxSwift (~> 6.0) |
| | | - RxRelay (6.7.1): |
| | | - RxSwift (= 6.7.1) |
| | | - RxSwift (6.7.1) |
| | | - SDWebImage (5.19.6): |
| | | - SDWebImage/Core (= 5.19.6) |
| | | - SDWebImage/Core (5.19.6) |
| | | - RxRelay (6.9.0): |
| | | - RxSwift (= 6.9.0) |
| | | - RxSwift (6.9.0) |
| | | - SDWebImage (5.20.0): |
| | | - SDWebImage/Core (= 5.20.0) |
| | | - SDWebImage/Core (5.20.0) |
| | | - SnapKit (5.7.1) |
| | | - SPPageMenu (3.5.0) |
| | | - SVProgressHUD (2.3.1): |
| | | - SVProgressHUD/Core (= 2.3.1) |
| | | - SVProgressHUD/Core (2.3.1) |
| | | - SwifterSwift (6.2.0): |
| | | - SwifterSwift/AppKit (= 6.2.0) |
| | | - SwifterSwift/Combine (= 6.2.0) |
| | | - SwifterSwift/CoreAnimation (= 6.2.0) |
| | | - SwifterSwift/CoreGraphics (= 6.2.0) |
| | | - SwifterSwift/CoreLocation (= 6.2.0) |
| | | - SwifterSwift/CryptoKit (= 6.2.0) |
| | | - SwifterSwift/Dispatch (= 6.2.0) |
| | | - SwifterSwift/Foundation (= 6.2.0) |
| | | - SwifterSwift/HealthKit (= 6.2.0) |
| | | - SwifterSwift/MapKit (= 6.2.0) |
| | | - SwifterSwift/SceneKit (= 6.2.0) |
| | | - SwifterSwift/SpriteKit (= 6.2.0) |
| | | - SwifterSwift/StoreKit (= 6.2.0) |
| | | - SwifterSwift/SwiftStdlib (= 6.2.0) |
| | | - SwifterSwift/UIKit (= 6.2.0) |
| | | - SwifterSwift/WebKit (= 6.2.0) |
| | | - SwifterSwift/AppKit (6.2.0) |
| | | - SwifterSwift/Combine (6.2.0) |
| | | - SwifterSwift/CoreAnimation (6.2.0) |
| | | - SwifterSwift/CoreGraphics (6.2.0) |
| | | - SwifterSwift/CoreLocation (6.2.0) |
| | | - SwifterSwift/CryptoKit (6.2.0) |
| | | - SwifterSwift/Dispatch (6.2.0) |
| | | - SwifterSwift/Foundation (6.2.0) |
| | | - SwifterSwift/HealthKit (6.2.0) |
| | | - SwifterSwift/MapKit (6.2.0) |
| | | - SwifterSwift/SceneKit (6.2.0) |
| | | - SwifterSwift/SpriteKit (6.2.0) |
| | | - SwifterSwift/StoreKit (6.2.0) |
| | | - SwifterSwift/SwiftStdlib (6.2.0) |
| | | - SwifterSwift/UIKit (6.2.0) |
| | | - SwifterSwift/WebKit (6.2.0) |
| | | - SwifterSwift (7.0.0): |
| | | - SwifterSwift/AppKit (= 7.0.0) |
| | | - SwifterSwift/CoreAnimation (= 7.0.0) |
| | | - SwifterSwift/CoreGraphics (= 7.0.0) |
| | | - SwifterSwift/CoreLocation (= 7.0.0) |
| | | - SwifterSwift/CryptoKit (= 7.0.0) |
| | | - SwifterSwift/Dispatch (= 7.0.0) |
| | | - SwifterSwift/Foundation (= 7.0.0) |
| | | - SwifterSwift/HealthKit (= 7.0.0) |
| | | - SwifterSwift/MapKit (= 7.0.0) |
| | | - SwifterSwift/SceneKit (= 7.0.0) |
| | | - SwifterSwift/SpriteKit (= 7.0.0) |
| | | - SwifterSwift/StoreKit (= 7.0.0) |
| | | - SwifterSwift/SwiftStdlib (= 7.0.0) |
| | | - SwifterSwift/UIKit (= 7.0.0) |
| | | - SwifterSwift/WebKit (= 7.0.0) |
| | | - SwifterSwift/AppKit (7.0.0) |
| | | - SwifterSwift/CoreAnimation (7.0.0) |
| | | - SwifterSwift/CoreGraphics (7.0.0) |
| | | - SwifterSwift/CoreLocation (7.0.0) |
| | | - SwifterSwift/CryptoKit (7.0.0) |
| | | - SwifterSwift/Dispatch (7.0.0) |
| | | - SwifterSwift/Foundation (7.0.0) |
| | | - SwifterSwift/HealthKit (7.0.0) |
| | | - SwifterSwift/MapKit (7.0.0) |
| | | - SwifterSwift/SceneKit (7.0.0) |
| | | - SwifterSwift/SpriteKit (7.0.0) |
| | | - SwifterSwift/StoreKit (7.0.0) |
| | | - SwifterSwift/SwiftStdlib (7.0.0) |
| | | - SwifterSwift/UIKit (7.0.0) |
| | | - SwifterSwift/WebKit (7.0.0) |
| | | - SwiftyStoreKit (0.16.1) |
| | | - TZImagePickerController (3.8.7): |
| | | - TZImagePickerController/Basic (= 3.8.7) |
| | | - TZImagePickerController/Location (= 3.8.7) |
| | | - TZImagePickerController/Basic (3.8.7) |
| | | - TZImagePickerController/Location (3.8.7) |
| | | - TZImagePickerController (3.8.8): |
| | | - TZImagePickerController/Basic (= 3.8.8) |
| | | - TZImagePickerController/Location (= 3.8.8) |
| | | - TZImagePickerController/Basic (3.8.8) |
| | | - TZImagePickerController/Location (3.8.8) |
| | | - UserDefaultsStore (1.5.0) |
| | | - VTMagic (1.2.4): |
| | | - VTMagic/Core (= 1.2.4) |
| | |
| | | - EmptyDataSet-Swift |
| | | - FFPage |
| | | - HandyJSON |
| | | - IQKeyboardCore |
| | | - IQKeyboardManager |
| | | - IQKeyboardManagerSwift |
| | | - IQKeyboardNotification |
| | | - IQKeyboardReturnManager |
| | | - IQKeyboardToolbar |
| | | - IQKeyboardToolbarManager |
| | | - IQTextInputViewNotification |
| | | - IQTextView |
| | | - Lantern |
| | | - MJRefresh |
| | | - ObjcExceptionBridging |
| | |
| | | :path: "/Users/yvkd/MyProject/JQTools" |
| | | |
| | | SPEC CHECKSUMS: |
| | | Alamofire: f36a35757af4587d8e4f4bfa223ad10be2422b8c |
| | | Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 |
| | | AliyunOSSiOS: b46648fd78909a567e3743fe94183748a407b175 |
| | | APNGKit: eb7e111277527cfd47636f797c9c8e7aab5d9601 |
| | | CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 |
| | |
| | | EmptyDataSet-Swift: eb382c0c87a2d9c678077385a595cec52da38171 |
| | | FFPage: 481cc0f2dde0f6be84a2359b6c86272e0024dc8d |
| | | HandyJSON: 9e4e236f5d2dbefad5155a77417bbea438201c03 |
| | | IQKeyboardCore: 28c8bf3bcd8ba5aa1570b318cbc4da94b861711e |
| | | IQKeyboardManager: c8665b3396bd0b79402b4c573eac345a31c7d485 |
| | | IQKeyboardManagerSwift: d7f3d3a562c237a0e7335e657cd598c452f57f1b |
| | | JQTools: af562f97302a433989c23bfb31e24458eb6469ad |
| | | IQKeyboardManagerSwift: 0c6fbbaa2e60739e48d7cf59f25661471a7a3a65 |
| | | IQKeyboardNotification: d7382c4466c5a5adef92c7452ebf861b36050088 |
| | | IQKeyboardReturnManager: 972be48528ce9e7508ab3ab15ac7efac803f17f5 |
| | | IQKeyboardToolbar: d4bdccfb78324aec2f3920659c77bb89acd33312 |
| | | IQKeyboardToolbarManager: 6c693c8478d6327a7ef2107528d29698b3514dbb |
| | | IQTextInputViewNotification: f5e954d8881fd9808b744e49e024cc0d4bcfe572 |
| | | IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7 |
| | | JQTools: 91910e06efed6aeabbccfae7d988d3016475b05a |
| | | Lantern: b192e7146c6d04e15e627f37281254a6a8593703 |
| | | MJRefresh: ff9e531227924c84ce459338414550a05d2aea78 |
| | | ObjcExceptionBridging: d3d37d62981bb7f252ecb31b62d7e23a96bbfb8a |
| | | ObjectMapper: e6e4d91ff7f2861df7aecc536c92d8363f4c9677 |
| | | QMUIKit: 2fc09ba9a31a44a4081916ed4c41467bac798821 |
| | | RxCocoa: f5609cb4637587a7faa99c5d5787e3ad582b75a4 |
| | | RxCocoa: ac16414696ae706516be3e1ab00fcce5bdc9be8a |
| | | RxDataSources: aa47cc1ed6c500fa0dfecac5c979b723542d79cf |
| | | RxRelay: 4151ba01152436b08271e08410135e099880eae5 |
| | | RxSwift: b9a93a26031785159e11abd40d1a55bcb8057e52 |
| | | SDWebImage: a79252b60f4678812d94316c91da69ec83089c9f |
| | | RxRelay: 6b0c930e5cef57d5fe2032571e5e65b78e3cbdb1 |
| | | RxSwift: 31649ace6aceeb422e16ff71c60804f9c3281ed9 |
| | | SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 |
| | | SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a |
| | | SPPageMenu: da182aafcec55719d5c326103cc7716c1e48f311 |
| | | SVProgressHUD: 4837c74bdfe2e51e8821c397825996a8d7de6e22 |
| | | SwifterSwift: dd00873fb09cde19da88bdb2878f9fe70fe27b0f |
| | | SwifterSwift: e9caf990fc72e835432280755d1f4c43f2a483d5 |
| | | SwiftyStoreKit: 6b9c08810269f030586dac1fae8e75871a82e84a |
| | | TZImagePickerController: 5f35bb7266552e36ca834bafa955b869fe086124 |
| | | TZImagePickerController: d084a7b97c82d387e7669dd86dc9a9057500aacf |
| | | UserDefaultsStore: 905e30372ff432197d199ce1f6fe51be7bf69628 |
| | | VTMagic: b49e5f456dbcbfd9a3588ba92417233a105bc193 |
| | | WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f |
| | |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardCore-IQKeyboardCore.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardCore.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardManager-IQKeyboardManager.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardManagerSwift.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardNotification-IQKeyboardNotification.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardNotification.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardReturnManager-IQKeyboardReturnManager.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardReturnManager.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardToolbar-IQKeyboardToolbar.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardToolbar.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardToolbarManager-IQKeyboardToolbarManager.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQKeyboardToolbarManager.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQTextInputViewNotification-IQTextInputViewNotification.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQTextInputViewNotification.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQTextView-IQTextView.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>IQTextView.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>RxCocoa-RxCocoa_Privacy.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>RxCocoa.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>RxRelay-RxRelay_Privacy.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>RxRelay.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>RxSwift-RxSwift_Privacy.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | | <false/> |
| | | </dict> |
| | | <key>RxSwift.xcscheme</key> |
| | | <dict> |
| | | <key>isShown</key> |
| | |
| | | **The MIT License** |
| | | **Copyright © 2015 Krunoslav Zaher, Shai Mishali** |
| | | **Copyright © 2015 Shai Mishali, Krunoslav Zaher** |
| | | **All rights reserved.** |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| | |
| | | <br /> |
| | | <a href="https://cocoapods.org/pods/RxSwift" alt="RxSwift on CocoaPods" title="RxSwift on CocoaPods"><img src="https://img.shields.io/cocoapods/v/RxSwift.svg" /></a> |
| | | <a href="https://github.com/Carthage/Carthage" alt="RxSwift on Carthage" title="RxSwift on Carthage"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat" /></a> |
| | | <a href="https://github.com/apple/swift-package-manager" alt="RxSwift on Swift Package Manager" title="RxSwift on Swift Package Manager"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg" /></a> |
| | | <a href="https://github.com/swiftlang/swift-package-manager" alt="RxSwift on Swift Package Manager" title="RxSwift on Swift Package Manager"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg" /></a> |
| | | </p> |
| | | |
| | | Rx is a [generic abstraction of computation](https://youtu.be/looJcaeboBY) expressed through `Observable<Element>` interface, which lets you broadcast and subscribe to values and other events from an `Observable` stream. |
| | | Rx is a [generic abstraction of computation](https://youtu.be/looJcaeboBY) expressed through `Observable<Element>` interface, which lets you broadcast and subscribe to values and other events from an `Observable` stream. |
| | | |
| | | RxSwift is the Swift-specific implementation of the [Reactive Extensions](http://reactivex.io) standard. |
| | | |
| | |
| | | ┌──────────────┐ ┌──────────────┐ |
| | | │ RxCocoa ├────▶ RxRelay │ |
| | | └───────┬──────┘ └──────┬───────┘ |
| | | │ │ |
| | | │ │ |
| | | ┌───────▼──────────────────▼───────┐ |
| | | │ RxSwift │ |
| | | └───────▲──────────────────▲───────┘ |
| | | │ │ |
| | | │ │ |
| | | ┌───────┴──────┐ ┌──────┴───────┐ |
| | | │ RxTest │ │ RxBlocking │ |
| | | └──────────────┘ └──────────────┘ |
| | |
| | | |
| | | * **RxSwift**: The core of RxSwift, providing the Rx standard as (mostly) defined by [ReactiveX](https://reactivex.io). It has no other dependencies. |
| | | * **RxCocoa**: Provides Cocoa-specific capabilities for general iOS/macOS/watchOS & tvOS app development, such as Shared Sequences, Traits, and much more. It depends on both `RxSwift` and `RxRelay`. |
| | | * **RxRelay**: Provides `PublishRelay`, `BehaviorRelay` and `ReplayRelay`, three [simple wrappers around Subjects](https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Subjects.md#relays). It depends on `RxSwift`. |
| | | * **RxRelay**: Provides `PublishRelay`, `BehaviorRelay` and `ReplayRelay`, three [simple wrappers around Subjects](https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Subjects.md#relays). It depends on `RxSwift`. |
| | | * **RxTest** and **RxBlocking**: Provides testing capabilities for Rx-based systems. It depends on `RxSwift`. |
| | | |
| | | ## Usage |
| | |
| | | use_frameworks! |
| | | |
| | | target 'YOUR_TARGET_NAME' do |
| | | pod 'RxSwift', '6.7.0' |
| | | pod 'RxCocoa', '6.7.0' |
| | | pod 'RxSwift', '6.9.0' |
| | | pod 'RxCocoa', '6.9.0' |
| | | end |
| | | |
| | | # RxTest and RxBlocking make the most sense in the context of unit/integration tests |
| | | target 'YOUR_TESTING_TARGET' do |
| | | pod 'RxBlocking', '6.7.0' |
| | | pod 'RxTest', '6.7.0' |
| | | pod 'RxBlocking', '6.9.0' |
| | | pod 'RxTest', '6.9.0' |
| | | end |
| | | ``` |
| | | |
| | |
| | | Add this to `Cartfile` |
| | | |
| | | ``` |
| | | github "ReactiveX/RxSwift" "6.7.0" |
| | | github "ReactiveX/RxSwift" "6.9.0" |
| | | ``` |
| | | |
| | | ```bash |
| | |
| | | |
| | | #### Carthage as a Static Library |
| | | |
| | | Carthage defaults to building RxSwift as a Dynamic Library. |
| | | Carthage defaults to building RxSwift as a Dynamic Library. |
| | | |
| | | If you wish to build RxSwift as a Static Library using Carthage you may use the script below to manually modify the framework type before building with Carthage: |
| | | |
| | |
| | | carthage build RxSwift --platform iOS |
| | | ``` |
| | | |
| | | ### [Swift Package Manager](https://github.com/apple/swift-package-manager) |
| | | ### [Swift Package Manager](https://github.com/swiftlang/swift-package-manager) |
| | | |
| | | > **Note**: There is a critical cross-dependency bug affecting many projects including RxSwift in Swift Package Manager. We've [filed a bug (SR-12303)](https://bugs.swift.org/browse/SR-12303) in early 2020 but have no answer yet. Your mileage may vary. A partial workaround can be found [here](https://github.com/ReactiveX/RxSwift/issues/2127#issuecomment-717830502). |
| | | |
| | |
| | | **The MIT License** |
| | | **Copyright © 2015 Krunoslav Zaher, Shai Mishali** |
| | | **Copyright © 2015 Shai Mishali, Krunoslav Zaher** |
| | | **All rights reserved.** |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| | |
| | | <br /> |
| | | <a href="https://cocoapods.org/pods/RxSwift" alt="RxSwift on CocoaPods" title="RxSwift on CocoaPods"><img src="https://img.shields.io/cocoapods/v/RxSwift.svg" /></a> |
| | | <a href="https://github.com/Carthage/Carthage" alt="RxSwift on Carthage" title="RxSwift on Carthage"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat" /></a> |
| | | <a href="https://github.com/apple/swift-package-manager" alt="RxSwift on Swift Package Manager" title="RxSwift on Swift Package Manager"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg" /></a> |
| | | <a href="https://github.com/swiftlang/swift-package-manager" alt="RxSwift on Swift Package Manager" title="RxSwift on Swift Package Manager"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg" /></a> |
| | | </p> |
| | | |
| | | Rx is a [generic abstraction of computation](https://youtu.be/looJcaeboBY) expressed through `Observable<Element>` interface, which lets you broadcast and subscribe to values and other events from an `Observable` stream. |
| | | Rx is a [generic abstraction of computation](https://youtu.be/looJcaeboBY) expressed through `Observable<Element>` interface, which lets you broadcast and subscribe to values and other events from an `Observable` stream. |
| | | |
| | | RxSwift is the Swift-specific implementation of the [Reactive Extensions](http://reactivex.io) standard. |
| | | |
| | |
| | | ┌──────────────┐ ┌──────────────┐ |
| | | │ RxCocoa ├────▶ RxRelay │ |
| | | └───────┬──────┘ └──────┬───────┘ |
| | | │ │ |
| | | │ │ |
| | | ┌───────▼──────────────────▼───────┐ |
| | | │ RxSwift │ |
| | | └───────▲──────────────────▲───────┘ |
| | | │ │ |
| | | │ │ |
| | | ┌───────┴──────┐ ┌──────┴───────┐ |
| | | │ RxTest │ │ RxBlocking │ |
| | | └──────────────┘ └──────────────┘ |
| | |
| | | |
| | | * **RxSwift**: The core of RxSwift, providing the Rx standard as (mostly) defined by [ReactiveX](https://reactivex.io). It has no other dependencies. |
| | | * **RxCocoa**: Provides Cocoa-specific capabilities for general iOS/macOS/watchOS & tvOS app development, such as Shared Sequences, Traits, and much more. It depends on both `RxSwift` and `RxRelay`. |
| | | * **RxRelay**: Provides `PublishRelay`, `BehaviorRelay` and `ReplayRelay`, three [simple wrappers around Subjects](https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Subjects.md#relays). It depends on `RxSwift`. |
| | | * **RxRelay**: Provides `PublishRelay`, `BehaviorRelay` and `ReplayRelay`, three [simple wrappers around Subjects](https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Subjects.md#relays). It depends on `RxSwift`. |
| | | * **RxTest** and **RxBlocking**: Provides testing capabilities for Rx-based systems. It depends on `RxSwift`. |
| | | |
| | | ## Usage |
| | |
| | | use_frameworks! |
| | | |
| | | target 'YOUR_TARGET_NAME' do |
| | | pod 'RxSwift', '6.7.0' |
| | | pod 'RxCocoa', '6.7.0' |
| | | pod 'RxSwift', '6.9.0' |
| | | pod 'RxCocoa', '6.9.0' |
| | | end |
| | | |
| | | # RxTest and RxBlocking make the most sense in the context of unit/integration tests |
| | | target 'YOUR_TESTING_TARGET' do |
| | | pod 'RxBlocking', '6.7.0' |
| | | pod 'RxTest', '6.7.0' |
| | | pod 'RxBlocking', '6.9.0' |
| | | pod 'RxTest', '6.9.0' |
| | | end |
| | | ``` |
| | | |
| | |
| | | Add this to `Cartfile` |
| | | |
| | | ``` |
| | | github "ReactiveX/RxSwift" "6.7.0" |
| | | github "ReactiveX/RxSwift" "6.9.0" |
| | | ``` |
| | | |
| | | ```bash |
| | |
| | | |
| | | #### Carthage as a Static Library |
| | | |
| | | Carthage defaults to building RxSwift as a Dynamic Library. |
| | | Carthage defaults to building RxSwift as a Dynamic Library. |
| | | |
| | | If you wish to build RxSwift as a Static Library using Carthage you may use the script below to manually modify the framework type before building with Carthage: |
| | | |
| | |
| | | carthage build RxSwift --platform iOS |
| | | ``` |
| | | |
| | | ### [Swift Package Manager](https://github.com/apple/swift-package-manager) |
| | | ### [Swift Package Manager](https://github.com/swiftlang/swift-package-manager) |
| | | |
| | | > **Note**: There is a critical cross-dependency bug affecting many projects including RxSwift in Swift Package Manager. We've [filed a bug (SR-12303)](https://bugs.swift.org/browse/SR-12303) in early 2020 but have no answer yet. Your mileage may vary. A partial workaround can be found [here](https://github.com/ReactiveX/RxSwift/issues/2127#issuecomment-717830502). |
| | | |
| | |
| | | **The MIT License** |
| | | **Copyright © 2015 Krunoslav Zaher, Shai Mishali** |
| | | **Copyright © 2015 Shai Mishali, Krunoslav Zaher** |
| | | **All rights reserved.** |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| | |
| | | // Copyright © 2018 Krunoslav Zaher. All rights reserved. |
| | | // |
| | | |
| | | import CoreFoundation |
| | | // This CoreFoundation import can be dropped when this issue is resolved: |
| | | // https://github.com/swiftlang/swift-corelibs-foundation/pull/5122 |
| | | import Foundation |
| | | |
| | | final class AtomicInt: NSLock { |
| | | final class AtomicInt: NSLock, @unchecked Sendable { |
| | | fileprivate var value: Int32 |
| | | public init(_ value: Int32 = 0) { |
| | | self.value = value |
| | |
| | | <br /> |
| | | <a href="https://cocoapods.org/pods/RxSwift" alt="RxSwift on CocoaPods" title="RxSwift on CocoaPods"><img src="https://img.shields.io/cocoapods/v/RxSwift.svg" /></a> |
| | | <a href="https://github.com/Carthage/Carthage" alt="RxSwift on Carthage" title="RxSwift on Carthage"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat" /></a> |
| | | <a href="https://github.com/apple/swift-package-manager" alt="RxSwift on Swift Package Manager" title="RxSwift on Swift Package Manager"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg" /></a> |
| | | <a href="https://github.com/swiftlang/swift-package-manager" alt="RxSwift on Swift Package Manager" title="RxSwift on Swift Package Manager"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg" /></a> |
| | | </p> |
| | | |
| | | Rx is a [generic abstraction of computation](https://youtu.be/looJcaeboBY) expressed through `Observable<Element>` interface, which lets you broadcast and subscribe to values and other events from an `Observable` stream. |
| | | Rx is a [generic abstraction of computation](https://youtu.be/looJcaeboBY) expressed through `Observable<Element>` interface, which lets you broadcast and subscribe to values and other events from an `Observable` stream. |
| | | |
| | | RxSwift is the Swift-specific implementation of the [Reactive Extensions](http://reactivex.io) standard. |
| | | |
| | |
| | | ┌──────────────┐ ┌──────────────┐ |
| | | │ RxCocoa ├────▶ RxRelay │ |
| | | └───────┬──────┘ └──────┬───────┘ |
| | | │ │ |
| | | │ │ |
| | | ┌───────▼──────────────────▼───────┐ |
| | | │ RxSwift │ |
| | | └───────▲──────────────────▲───────┘ |
| | | │ │ |
| | | │ │ |
| | | ┌───────┴──────┐ ┌──────┴───────┐ |
| | | │ RxTest │ │ RxBlocking │ |
| | | └──────────────┘ └──────────────┘ |
| | |
| | | |
| | | * **RxSwift**: The core of RxSwift, providing the Rx standard as (mostly) defined by [ReactiveX](https://reactivex.io). It has no other dependencies. |
| | | * **RxCocoa**: Provides Cocoa-specific capabilities for general iOS/macOS/watchOS & tvOS app development, such as Shared Sequences, Traits, and much more. It depends on both `RxSwift` and `RxRelay`. |
| | | * **RxRelay**: Provides `PublishRelay`, `BehaviorRelay` and `ReplayRelay`, three [simple wrappers around Subjects](https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Subjects.md#relays). It depends on `RxSwift`. |
| | | * **RxRelay**: Provides `PublishRelay`, `BehaviorRelay` and `ReplayRelay`, three [simple wrappers around Subjects](https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Subjects.md#relays). It depends on `RxSwift`. |
| | | * **RxTest** and **RxBlocking**: Provides testing capabilities for Rx-based systems. It depends on `RxSwift`. |
| | | |
| | | ## Usage |
| | |
| | | use_frameworks! |
| | | |
| | | target 'YOUR_TARGET_NAME' do |
| | | pod 'RxSwift', '6.7.0' |
| | | pod 'RxCocoa', '6.7.0' |
| | | pod 'RxSwift', '6.9.0' |
| | | pod 'RxCocoa', '6.9.0' |
| | | end |
| | | |
| | | # RxTest and RxBlocking make the most sense in the context of unit/integration tests |
| | | target 'YOUR_TESTING_TARGET' do |
| | | pod 'RxBlocking', '6.7.0' |
| | | pod 'RxTest', '6.7.0' |
| | | pod 'RxBlocking', '6.9.0' |
| | | pod 'RxTest', '6.9.0' |
| | | end |
| | | ``` |
| | | |
| | |
| | | Add this to `Cartfile` |
| | | |
| | | ``` |
| | | github "ReactiveX/RxSwift" "6.7.0" |
| | | github "ReactiveX/RxSwift" "6.9.0" |
| | | ``` |
| | | |
| | | ```bash |
| | |
| | | |
| | | #### Carthage as a Static Library |
| | | |
| | | Carthage defaults to building RxSwift as a Dynamic Library. |
| | | Carthage defaults to building RxSwift as a Dynamic Library. |
| | | |
| | | If you wish to build RxSwift as a Static Library using Carthage you may use the script below to manually modify the framework type before building with Carthage: |
| | | |
| | |
| | | carthage build RxSwift --platform iOS |
| | | ``` |
| | | |
| | | ### [Swift Package Manager](https://github.com/apple/swift-package-manager) |
| | | ### [Swift Package Manager](https://github.com/swiftlang/swift-package-manager) |
| | | |
| | | > **Note**: There is a critical cross-dependency bug affecting many projects including RxSwift in Swift Package Manager. We've [filed a bug (SR-12303)](https://bugs.swift.org/browse/SR-12303) in early 2020 but have no answer yet. Your mileage may vary. A partial workaround can be found [here](https://github.com/ReactiveX/RxSwift/issues/2127#issuecomment-717830502). |
| | | |
| | |
| | | private var isExecuting: Bool = false |
| | | private var hasFaulted: Bool = false |
| | | |
| | | // lock { |
| | | /** |
| | | Locks the current instance, preventing other threads from modifying it until `unlock()` is called. |
| | | |
| | | This method is used to create a critical section where only one thread is allowed to access the protected resources at a time. |
| | | |
| | | Example usage: |
| | | ```swift |
| | | let lock = AsyncLock<SomeAction>() |
| | | lock.lock() |
| | | // Critical section |
| | | lock.unlock() |
| | | ``` |
| | | */ |
| | | func lock() { |
| | | self._lock.lock() |
| | | } |
| | | |
| | | /** |
| | | Unlocks the current instance, allowing other threads to access the protected resources. |
| | | |
| | | This method is called after a `lock()` to release the critical section, ensuring that other waiting threads can proceed. |
| | | |
| | | Example usage: |
| | | ```swift |
| | | let lock = AsyncLock<SomeAction>() |
| | | lock.lock() |
| | | // Critical section |
| | | lock.unlock() |
| | | ``` |
| | | */ |
| | | func unlock() { |
| | | self._lock.unlock() |
| | | } |
| | | // } |
| | | |
| | | // MARK: - Queue Methods |
| | | |
| | | /** |
| | | Enqueues an action into the internal queue for deferred execution. |
| | | |
| | | If no actions are currently being executed, the method returns the action for immediate execution. Otherwise, the action is enqueued for deferred execution when the lock is available. |
| | | |
| | | - Parameter action: The action to enqueue. |
| | | - Returns: The action if it can be executed immediately, or `nil` if it has been enqueued. |
| | | |
| | | Example usage: |
| | | ```swift |
| | | let lock = AsyncLock<SomeAction>() |
| | | if let action = lock.enqueue(someAction) { |
| | | action.invoke() // Execute the action immediately if it's not deferred. |
| | | } |
| | | ``` |
| | | */ |
| | | private func enqueue(_ action: I) -> I? { |
| | | self.lock(); defer { self.unlock() } |
| | | if self.hasFaulted { |
| | |
| | | return action |
| | | } |
| | | |
| | | /** |
| | | Dequeues the next action for execution, if available. |
| | | |
| | | If the queue is empty, this method resets the `isExecuting` flag to indicate that no actions are currently being executed. |
| | | |
| | | - Returns: The next action from the queue, or `nil` if the queue is empty. |
| | | |
| | | Example usage: |
| | | ```swift |
| | | let nextAction = lock.dequeue() |
| | | nextAction?.invoke() // Execute the next action if one is available. |
| | | ``` |
| | | */ |
| | | private func dequeue() -> I? { |
| | | self.lock(); defer { self.unlock() } |
| | | if !self.queue.isEmpty { |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | Invokes the provided action, ensuring that actions are executed sequentially. |
| | | |
| | | The first action is executed immediately if no other actions are currently running. If other actions are already in the queue, the new action is enqueued and executed sequentially after the current actions are completed. |
| | | |
| | | - Parameter action: The action to be invoked. |
| | | |
| | | Example usage: |
| | | ```swift |
| | | let lock = AsyncLock<SomeAction>() |
| | | lock.invoke(someAction) // Invoke or enqueue the action. |
| | | ``` |
| | | */ |
| | | func invoke(_ action: I) { |
| | | let firstEnqueuedAction = self.enqueue(action) |
| | | |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | // MARK: - Dispose Methods |
| | | |
| | | /** |
| | | Disposes of the `AsyncLock` by clearing the internal queue and preventing further actions from being executed. |
| | | |
| | | This method ensures that all pending actions are discarded, and the lock enters a faulted state where no new actions can be enqueued or executed. |
| | | |
| | | Example usage: |
| | | ```swift |
| | | let lock = AsyncLock<SomeAction>() |
| | | lock.dispose() // Clear the queue and prevent further actions. |
| | | ``` |
| | | */ |
| | | func dispose() { |
| | | self.synchronizedDispose() |
| | | } |
| | | |
| | | /** |
| | | Synchronously disposes of the internal queue and marks the lock as faulted. |
| | | |
| | | This method is typically used internally to handle disposal of the lock in a thread-safe manner. |
| | | |
| | | Example usage: |
| | | ```swift |
| | | lock.synchronized_dispose() |
| | | ``` |
| | | */ |
| | | func synchronized_dispose() { |
| | | self.queue = Queue(capacity: 0) |
| | | self.hasFaulted = true |
| | |
| | | // Copyright © 2015 Krunoslav Zaher. All rights reserved. |
| | | // |
| | | |
| | | /// Represents an observable sequence of elements that have a common key. |
| | | /// Represents an observable sequence of elements that share a common key. |
| | | /// `GroupedObservable` is typically created by the `groupBy` operator. |
| | | /// Each `GroupedObservable` instance represents a collection of elements |
| | | /// that are grouped by a specific key. |
| | | /// |
| | | /// Example usage: |
| | | /// ``` |
| | | /// let observable = Observable.of("Apple", "Banana", "Apricot", "Blueberry", "Avocado") |
| | | /// |
| | | /// let grouped = observable.groupBy { fruit in |
| | | /// fruit.first! // Grouping by the first letter of each fruit |
| | | /// } |
| | | /// |
| | | /// _ = grouped.subscribe { group in |
| | | /// print("Group: \(group.key)") |
| | | /// _ = group.subscribe { event in |
| | | /// print(event) |
| | | /// } |
| | | /// } |
| | | /// ``` |
| | | /// This will print: |
| | | /// ``` |
| | | /// Group: A |
| | | /// next(Apple) |
| | | /// next(Apricot) |
| | | /// next(Avocado) |
| | | /// Group: B |
| | | /// next(Banana) |
| | | /// next(Blueberry) |
| | | /// ``` |
| | | public struct GroupedObservable<Key, Element> : ObservableType { |
| | | /// Gets the common key. |
| | | /// The key associated with this grouped observable sequence. |
| | | /// All elements emitted by this observable share this common key. |
| | | public let key: Key |
| | | |
| | | private let source: Observable<Element> |
| | | |
| | | /// Initializes grouped observable sequence with key and source observable sequence. |
| | | /// Initializes a grouped observable sequence with a key and a source observable sequence. |
| | | /// |
| | | /// - parameter key: Grouped observable sequence key |
| | | /// - parameter source: Observable sequence that represents sequence of elements for the key |
| | | /// - returns: Grouped observable sequence of elements for the specific key |
| | | /// - Parameters: |
| | | /// - key: The key associated with this grouped observable sequence. |
| | | /// - source: The observable sequence of elements for the specified key. |
| | | /// |
| | | /// Example usage: |
| | | /// ``` |
| | | /// let sourceObservable = Observable.of("Apple", "Apricot", "Avocado") |
| | | /// let groupedObservable = GroupedObservable(key: "A", source: sourceObservable) |
| | | /// |
| | | /// _ = groupedObservable.subscribe { event in |
| | | /// print(event) |
| | | /// } |
| | | /// ``` |
| | | /// This will print: |
| | | /// ``` |
| | | /// next(Apple) |
| | | /// next(Apricot) |
| | | /// next(Avocado) |
| | | /// ``` |
| | | public init(key: Key, source: Observable<Element>) { |
| | | self.key = key |
| | | self.source = source |
| | | } |
| | | |
| | | /// Subscribes `observer` to receive events for this sequence. |
| | | /// Subscribes an observer to receive events emitted by the source observable sequence. |
| | | /// |
| | | /// - Parameter observer: The observer that will receive the events of the source observable. |
| | | /// - Returns: A `Disposable` representing the subscription, which can be used to cancel the subscription. |
| | | /// |
| | | /// Example usage: |
| | | /// ``` |
| | | /// let fruitsObservable = Observable.of("Apple", "Banana", "Apricot", "Blueberry", "Avocado") |
| | | /// let grouped = fruitsObservable.groupBy { $0.first! } // Group by first letter |
| | | /// |
| | | /// _ = grouped.subscribe { group in |
| | | /// if group.key == "A" { |
| | | /// _ = group.subscribe { event in |
| | | /// print(event) |
| | | /// } |
| | | /// } |
| | | /// } |
| | | /// ``` |
| | | /// This will print: |
| | | /// ``` |
| | | /// next(Apple) |
| | | /// next(Apricot) |
| | | /// next(Avocado) |
| | | /// ``` |
| | | public func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable where Observer.Element == Element { |
| | | self.source.subscribe(observer) |
| | | } |
| | | |
| | | /// Converts `self` to `Observable` sequence. |
| | | /// Converts this `GroupedObservable` into a regular `Observable` sequence. |
| | | /// This allows you to work with the sequence without directly interacting with the key. |
| | | /// |
| | | /// - Returns: The underlying `Observable` sequence of elements for the specified key. |
| | | /// |
| | | /// Example usage: |
| | | /// ``` |
| | | /// let fruitsObservable = Observable.of("Apple", "Banana", "Apricot", "Blueberry", "Avocado") |
| | | /// let grouped = fruitsObservable.groupBy { $0.first! } // Group by first letter |
| | | /// |
| | | /// _ = grouped.subscribe { group in |
| | | /// if group.key == "A" { |
| | | /// let regularObservable = group.asObservable() |
| | | /// _ = regularObservable.subscribe { event in |
| | | /// print(event) |
| | | /// } |
| | | /// } |
| | | /// } |
| | | /// ``` |
| | | /// This will print: |
| | | /// ``` |
| | | /// next(Apple) |
| | | /// next(Apricot) |
| | | /// next(Avocado) |
| | | /// ``` |
| | | public func asObservable() -> Observable<Element> { |
| | | self.source |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | observer.onCompleted() |
| | | } catch is CancellationError { |
| | | observer.onCompleted() |
| | | } catch { |
| | | observer.onError(error) |
| | | } |
| | |
| | | self.parent.dispose() |
| | | case .completed: |
| | | self.parent.group.remove(for: self.disposeKey) |
| | | if let next = self.parent.queue.dequeue() { |
| | | self.parent.subscribe(next, group: self.parent.group) |
| | | } |
| | | else { |
| | | self.parent.activeCount -= 1 |
| | | |
| | | if self.parent.stopped && self.parent.activeCount == 0 { |
| | | self.parent.forwardOn(.completed) |
| | | self.parent.dispose() |
| | | } |
| | | } |
| | | self.parent.dequeueNextAndSubscribe() |
| | | } |
| | | } |
| | | } |
| | |
| | | return self.group |
| | | } |
| | | |
| | | func subscribe(_ innerSource: SourceSequence, group: CompositeDisposable) { |
| | | @discardableResult |
| | | func subscribe(_ innerSource: SourceSequence, group: CompositeDisposable) -> Disposable { |
| | | let subscription = SingleAssignmentDisposable() |
| | | |
| | | let key = group.insert(subscription) |
| | |
| | | let disposable = innerSource.asObservable().subscribe(observer) |
| | | subscription.setDisposable(disposable) |
| | | } |
| | | return subscription |
| | | } |
| | | |
| | | func dequeueNextAndSubscribe() { |
| | | if let next = queue.dequeue() { |
| | | // subscribing immediately can produce values immediately which can re-enter and cause stack overflows |
| | | let disposable = CurrentThreadScheduler.instance.schedule(()) { _ in |
| | | // lock again |
| | | self.lock.performLocked { |
| | | self.subscribe(next, group: self.group) |
| | | } |
| | | } |
| | | _ = group.insert(disposable) |
| | | } |
| | | else { |
| | | activeCount -= 1 |
| | | |
| | | if stopped && activeCount == 0 { |
| | | forwardOn(.completed) |
| | | dispose() |
| | | } |
| | | } |
| | | } |
| | | |
| | | func performMap(_ element: SourceElement) throws -> SourceSequence { |
| | |
| | | |
| | | private var nextId = 0 |
| | | |
| | | private let thread: Thread |
| | | |
| | | /// - returns: Current time. |
| | | public var now: RxTime { |
| | | self.converter.convertFromVirtualTime(self.clock) |
| | |
| | | self.currentClock = initialClock |
| | | self.running = false |
| | | self.converter = converter |
| | | self.thread = Thread.current |
| | | self.schedulerQueue = PriorityQueue(hasHigherPriority: { |
| | | switch converter.compareVirtualTime($0.time, $1.time) { |
| | | case .lessThan: |
| | |
| | | - returns: The disposable object used to cancel the scheduled action (best effort). |
| | | */ |
| | | public func scheduleAbsoluteVirtual<StateType>(_ state: StateType, time: VirtualTime, action: @escaping (StateType) -> Disposable) -> Disposable { |
| | | MainScheduler.ensureExecutingOnScheduler() |
| | | |
| | | ensusreRunningOnCorrectThread() |
| | | let compositeDisposable = CompositeDisposable() |
| | | |
| | | let item = VirtualSchedulerItem(action: { |
| | |
| | | |
| | | /// Starts the virtual time scheduler. |
| | | public func start() { |
| | | MainScheduler.ensureExecutingOnScheduler() |
| | | |
| | | if self.running { |
| | | return |
| | | } |
| | | |
| | | ensusreRunningOnCorrectThread() |
| | | self.running = true |
| | | repeat { |
| | | guard let next = self.findNext() else { |
| | |
| | | /// |
| | | /// - parameter virtualTime: Absolute time to advance the scheduler's clock to. |
| | | public func advanceTo(_ virtualTime: VirtualTime) { |
| | | MainScheduler.ensureExecutingOnScheduler() |
| | | |
| | | if self.running { |
| | | fatalError("Scheduler is already running") |
| | | } |
| | | |
| | | ensusreRunningOnCorrectThread() |
| | | self.running = true |
| | | repeat { |
| | | guard let next = self.findNext() else { |
| | |
| | | |
| | | /// Advances the scheduler's clock by the specified relative time. |
| | | public func sleep(_ virtualInterval: VirtualTimeInterval) { |
| | | MainScheduler.ensureExecutingOnScheduler() |
| | | |
| | | ensusreRunningOnCorrectThread() |
| | | let sleepTo = self.converter.offsetVirtualTime(self.clock, offset: virtualInterval) |
| | | if self.converter.compareVirtualTime(sleepTo, self.clock).lessThen { |
| | | fatalError("Can't sleep to past.") |
| | |
| | | |
| | | /// Stops the virtual time scheduler. |
| | | public func stop() { |
| | | MainScheduler.ensureExecutingOnScheduler() |
| | | |
| | | ensusreRunningOnCorrectThread() |
| | | self.running = false |
| | | } |
| | | |
| | |
| | | _ = Resources.decrementTotal() |
| | | } |
| | | #endif |
| | | |
| | | private func ensusreRunningOnCorrectThread() { |
| | | guard Thread.current == thread else { |
| | | rxFatalError("Executing on the wrong thread. Please ensure all work on the same thread.") |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: description |
| | |
| | | } |
| | | |
| | | override func run<Observer: ObserverType>(_ observer: Observer, cancel: Cancelable) -> (sink: Disposable, subscription: Disposable) where Observer.Element == Element { |
| | | let sink = ConcatCompletableSink(parent: self, observer: observer, cancel: cancel) |
| | | let subscription = sink.run() |
| | | let sink = ConcatCompletableSink(second: second, observer: observer, cancel: cancel) |
| | | let subscription = sink.run(completable: completable) |
| | | return (sink: sink, subscription: subscription) |
| | | } |
| | | } |
| | |
| | | typealias Element = Never |
| | | typealias Parent = ConcatCompletable<Observer.Element> |
| | | |
| | | private let parent: Parent |
| | | private let second: Observable<Observer.Element> |
| | | private let subscription = SerialDisposable() |
| | | |
| | | init(parent: Parent, observer: Observer, cancel: Cancelable) { |
| | | self.parent = parent |
| | | init(second: Observable<Observer.Element>, observer: Observer, cancel: Cancelable) { |
| | | self.second = second |
| | | super.init(observer: observer, cancel: cancel) |
| | | } |
| | | |
| | |
| | | break |
| | | case .completed: |
| | | let otherSink = ConcatCompletableSinkOther(parent: self) |
| | | self.subscription.disposable = self.parent.second.subscribe(otherSink) |
| | | self.subscription.disposable = self.second.subscribe(otherSink) |
| | | } |
| | | } |
| | | |
| | | func run() -> Disposable { |
| | | func run(completable: Observable<Never>) -> Disposable { |
| | | let subscription = SingleAssignmentDisposable() |
| | | self.subscription.disposable = subscription |
| | | subscription.setDisposable(self.parent.completable.subscribe(self)) |
| | | subscription.setDisposable(completable.subscribe(self)) |
| | | return self.subscription |
| | | } |
| | | } |
| | |
| | | let callStack = [String]() |
| | | #endif |
| | | |
| | | let disposable: Disposable |
| | | if let onDisposed = onDisposed { |
| | | disposable = Disposables.create(with: onDisposed) |
| | | } else { |
| | | disposable = Disposables.create() |
| | | } |
| | | let disposable: Disposable = onDisposed.map(Disposables.create(with:)) ?? Disposables.create() |
| | | |
| | | let observer: CompletableObserver = { event in |
| | | switch event { |
| | |
| | | #else |
| | | let callStack = [String]() |
| | | #endif |
| | | let disposable: Disposable |
| | | if let onDisposed = onDisposed { |
| | | disposable = Disposables.create(with: onDisposed) |
| | | } else { |
| | | disposable = Disposables.create() |
| | | } |
| | | let disposable: Disposable = onDisposed.map(Disposables.create(with:)) ?? Disposables.create() |
| | | |
| | | let observer: MaybeObserver = { event in |
| | | switch event { |
| | |
| | | #if swift(>=5.6) && canImport(_Concurrency) && !os(Linux) |
| | | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) |
| | | public extension PrimitiveSequenceType where Trait == SingleTrait { |
| | | /** |
| | | Creates a `Single` from the result of an asynchronous operation |
| | | |
| | | - seealso: [create operator on reactivex.io](http://reactivex.io/documentation/operators/create.html) |
| | | |
| | | - parameter work: An `async` closure expected to return an element of type `Element` |
| | | |
| | | - returns: A `Single` of the `async` closure's element type |
| | | */ |
| | | @_disfavoredOverload |
| | | static func create( |
| | | detached: Bool = false, |
| | | priority: TaskPriority? = nil, |
| | | work: @Sendable @escaping () async throws -> Element |
| | | ) -> PrimitiveSequence<Trait, Element> { |
| | | .create { single in |
| | | let operation: () async throws -> Void = { |
| | | await single( |
| | | Result { try await work() } |
| | | ) |
| | | } |
| | | |
| | | let task = if detached { |
| | | Task.detached(priority: priority, operation: operation) |
| | | } else { |
| | | Task(priority: priority, operation: operation) |
| | | } |
| | | |
| | | return Disposables.create { task.cancel() } |
| | | } |
| | | } |
| | | |
| | | /// Allows awaiting the success or failure of this `Single` |
| | | /// asynchronously via Swift's concurrency features (`async/await`) |
| | | /// |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) |
| | | extension Result where Failure == Swift.Error { |
| | | @_disfavoredOverload |
| | | init(catching body: () async throws -> Success) async { |
| | | do { |
| | | self = try await .success(body()) |
| | | } catch { |
| | | self = .failure(error) |
| | | } |
| | | } |
| | | } |
| | | #endif |
| | |
| | | let callStack = [String]() |
| | | #endif |
| | | |
| | | let disposable: Disposable |
| | | if let onDisposed = onDisposed { |
| | | disposable = Disposables.create(with: onDisposed) |
| | | } else { |
| | | disposable = Disposables.create() |
| | | } |
| | | let disposable: Disposable = onDisposed.map(Disposables.create(with:)) ?? Disposables.create() |
| | | |
| | | let observer: SingleObserver = { event in |
| | | switch event { |
| | |
| | | |
| | | This library provides an async image downloader with cache support. For convenience, we added categories for UI elements like `UIImageView`, `UIButton`, `MKAnnotationView`. |
| | | |
| | | Note: `SD` is the prefix for **Simple Design** (which is the team name in Daily Motion company from the author Olivier Poitrey) |
| | | > 💡NOTE: `SD` is the prefix for **Simple Design** (which is the team name in Daily Motion company from the author Olivier Poitrey) |
| | | |
| | | ## Features |
| | | |
| | |
| | | - WebP format from iOS 14/macOS 11.0 via [SDWebImageAWebPCoder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#awebp-coder). For lower firmware, use coder plugin [SDWebImageWebPCoder](https://github.com/SDWebImage/SDWebImageWebPCoder) |
| | | - JPEG-XL format from iOS 17/macOS 14.0 built-in. For lower firmware, use coder plugin [SDWebImageJPEGXLCoder](https://github.com/SDWebImage/SDWebImageJPEGXLCoder) |
| | | - Support extendable coder plugins for new image formats like BPG, AVIF. And vector format like PDF, SVG. See all the list in [Image coder plugin List](https://github.com/SDWebImage/SDWebImage/wiki/Coder-Plugin-List) |
| | | |
| | | > 💡NOTE: For new user |
| | | |
| | | SDWebImage use [Coder Plugin System](https://github.com/SDWebImage/SDWebImage/wiki/Coder-Plugin-List) to support both Apple's built-in and external image format. For static image we always use Apple's built-in as fallback, but not for animated image. Currently (updated to 5.19.x version) we only register traditional animated format like GIF/APNG by default, without the modern format like AWebP/HEICS/AVIF, even on the latest firmware. |
| | | |
| | | If you want these animated image format support, simply register by yourself with one-line code, see more in [WebP Coder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#awebp-coder) and [HEIC Coder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#heic-coder) |
| | | |
| | | In future we will change this behavior by always registering all Apple's built-in animated image format, to make it easy for new user to integrate. |
| | | |
| | | ## Additional modules and Ecosystem |
| | | |
| | |
| | | Then run `carthage update` |
| | | If this is your first time using Carthage in the project, you'll need to go through some additional steps as explained [over at Carthage](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). |
| | | |
| | | > NOTE: At this time, Carthage does not provide a way to build only specific repository subcomponents (or equivalent of CocoaPods's subspecs). All components and their dependencies will be built with the above command. However, you don't need to copy frameworks you aren't using into your project. For instance, if you aren't using `SDWebImageMapKit`, feel free to delete that framework from the Carthage Build directory after `carthage update` completes. |
| | | > 💡NOTE: At this time, Carthage does not provide a way to build only specific repository subcomponents (or equivalent of CocoaPods's subspecs). All components and their dependencies will be built with the above command. However, you don't need to copy frameworks you aren't using into your project. For instance, if you aren't using `SDWebImageMapKit`, feel free to delete that framework from the Carthage Build directory after `carthage update` completes. |
| | | |
| | | > NOTE: [Apple requires SDWebImage contains signatures](https://developer.apple.com/support/third-party-SDK-requirements/). So, by default the `carthage build` binary framework does not do codesign, this will cause validation error. You can sign yourself with the Apple Developer Program identity, or using the binary framework: |
| | | > 💡NOTE: [Apple requires SDWebImage contains signatures](https://developer.apple.com/support/third-party-SDK-requirements/). So, by default the `carthage build` binary framework does not do codesign, this will cause validation error. You can sign yourself with the Apple Developer Program identity, or using the binary framework: |
| | | |
| | | ``` |
| | | binary "https://github.com/SDWebImage/SDWebImage/raw/master/SDWebImage.json" |
| | |
| | | #else |
| | | self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation]; |
| | | #endif |
| | | // Defines the associated object that holds the format for static images |
| | | super.sd_imageFormat = format; |
| | | return self; |
| | | } |
| | | } |
| | |
| | | } else { |
| | | return [super sd_imageFormat]; |
| | | } |
| | | } |
| | | |
| | | - (void)setSd_imageFormat:(SDImageFormat)sd_imageFormat { |
| | | return; |
| | | } |
| | | |
| | | - (BOOL)sd_isVector { |
| | |
| | | |
| | | #import "SDAnimatedImage.h" |
| | | #import "SDAnimatedImagePlayer.h" |
| | | #import "SDImageTransformer.h" |
| | | |
| | | /** |
| | | A drop-in replacement for UIImageView/NSImageView, you can use this for animated image rendering. |
| | |
| | | @property (nonatomic, strong, readonly, nullable) SDAnimatedImagePlayer *player; |
| | | |
| | | /** |
| | | The transformer for each decoded animated image frame. |
| | | We supports post-transform on animated image frame from version 5.20. |
| | | When you configure the transformer on `SDAnimatedImageView` and animation is playing, the `transformedImageWithImage:forKey:` will be called just after the frame is decoded. (note: The `key` arg is always empty for backward-compatible and may be removed in the future) |
| | | |
| | | Example to tint the alpha animated image frame with a black color: |
| | | * @code |
| | | imageView.animationTransformer = [SDImageTintTransformer transformerWithColor:UIColor.blackColor]; |
| | | * @endcode |
| | | @note The `transformerKey` property is used to ensure the buffer cache available. So make sure it's correct value match the actual logic on transformer. Which means, for the `same frame index + same transformer key`, the transformed image should always be the same. |
| | | */ |
| | | @property (nonatomic, strong, nullable) id<SDImageTransformer> animationTransformer; |
| | | |
| | | /** |
| | | Current display frame image. This value is KVO Compliance. |
| | | */ |
| | | @property (nonatomic, strong, readonly, nullable) UIImage *currentFrame; |
| | |
| | | #import "SDInternalMacros.h" |
| | | #import "objc/runtime.h" |
| | | |
| | | // A wrapper to implements the transformer on animated image, like tint color |
| | | @interface SDAnimatedImageFrameProvider : NSObject <SDAnimatedImageProvider> |
| | | @property (nonatomic, strong) id<SDAnimatedImageProvider> provider; |
| | | @property (nonatomic, strong) id<SDImageTransformer> transformer; |
| | | @end |
| | | |
| | | @implementation SDAnimatedImageFrameProvider |
| | | |
| | | - (instancetype)initWithProvider:(id<SDAnimatedImageProvider>)provider transformer:(id<SDImageTransformer>)transformer { |
| | | self = [super init]; |
| | | if (self) { |
| | | _provider = provider; |
| | | _transformer = transformer; |
| | | } |
| | | return self; |
| | | } |
| | | |
| | | - (NSUInteger)hash { |
| | | NSUInteger prime = 31; |
| | | NSUInteger result = 1; |
| | | NSUInteger providerHash = self.provider.hash; |
| | | NSUInteger transformerHash = self.transformer.transformerKey.hash; |
| | | result = prime * result + providerHash; |
| | | result = prime * result + transformerHash; |
| | | return result; |
| | | } |
| | | |
| | | - (BOOL)isEqual:(id)object { |
| | | if (nil == object) { |
| | | return NO; |
| | | } |
| | | if (self == object) { |
| | | return YES; |
| | | } |
| | | if (![object isKindOfClass:[self class]]) { |
| | | return NO; |
| | | } |
| | | return self.provider == [object provider] |
| | | && [self.transformer.transformerKey isEqualToString:[object transformer].transformerKey]; |
| | | } |
| | | |
| | | - (NSData *)animatedImageData { |
| | | return self.provider.animatedImageData; |
| | | } |
| | | |
| | | - (NSUInteger)animatedImageFrameCount { |
| | | return self.provider.animatedImageFrameCount; |
| | | } |
| | | |
| | | - (NSUInteger)animatedImageLoopCount { |
| | | return self.provider.animatedImageLoopCount; |
| | | } |
| | | |
| | | - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index { |
| | | return [self.provider animatedImageDurationAtIndex:index]; |
| | | } |
| | | |
| | | - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { |
| | | UIImage *frame = [self.provider animatedImageFrameAtIndex:index]; |
| | | return [self.transformer transformedImageWithImage:frame forKey:@""]; |
| | | } |
| | | |
| | | @end |
| | | |
| | | @interface UIImageView () <CALayerDelegate> |
| | | @end |
| | | |
| | |
| | | provider = (id<SDAnimatedImage>)image; |
| | | } |
| | | // Create animated player |
| | | self.player = [SDAnimatedImagePlayer playerWithProvider:provider]; |
| | | if (self.animationTransformer) { |
| | | // Check if post-transform animation available |
| | | provider = [[SDAnimatedImageFrameProvider alloc] initWithProvider:provider transformer:self.animationTransformer]; |
| | | self.player = [SDAnimatedImagePlayer playerWithProvider:provider]; |
| | | } else { |
| | | // Normal animation without post-transform |
| | | self.player = [SDAnimatedImagePlayer playerWithProvider:provider]; |
| | | } |
| | | } else { |
| | | // Update Frame Count |
| | | self.player.totalFrameCount = [(id<SDAnimatedImage>)image animatedImageFrameCount]; |
| | |
| | | - (NSData *)dataForKey:(NSString *)key { |
| | | NSParameterAssert(key); |
| | | NSString *filePath = [self cachePathForKey:key]; |
| | | // if filePath is nil or (null),framework will crash with this: |
| | | // Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[_NSPlaceholderData initWithContentsOfFile:options:maxLength:error:]: nil file argument' |
| | | if (filePath == nil || [@"(null)" isEqualToString: filePath]) { |
| | | return nil; |
| | | } |
| | | NSData *data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil]; |
| | | if (data) { |
| | | [[NSURL fileURLWithPath:filePath] setResourceValue:[NSDate date] forKey:NSURLContentAccessDateKey error:nil]; |
| | | return data; |
| | | } |
| | | |
| | | // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name |
| | | // checking the key with and without the extension |
| | | data = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil]; |
| | | filePath = filePath.stringByDeletingPathExtension; |
| | | data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil]; |
| | | if (data) { |
| | | [[NSURL fileURLWithPath:filePath] setResourceValue:[NSDate date] forKey:NSURLContentAccessDateKey error:nil]; |
| | | return data; |
| | | } |
| | | |
| | |
| | | NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; |
| | | |
| | | // Compute content date key to be used for tests |
| | | NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey; |
| | | NSURLResourceKey cacheContentDateKey; |
| | | switch (self.config.diskCacheExpireType) { |
| | | case SDImageCacheConfigExpireTypeAccessDate: |
| | | cacheContentDateKey = NSURLContentAccessDateKey; |
| | | break; |
| | | case SDImageCacheConfigExpireTypeModificationDate: |
| | | cacheContentDateKey = NSURLContentModificationDateKey; |
| | | break; |
| | |
| | | case SDImageCacheConfigExpireTypeChangeDate: |
| | | cacheContentDateKey = NSURLAttributeModificationDateKey; |
| | | break; |
| | | case SDImageCacheConfigExpireTypeAccessDate: |
| | | default: |
| | | cacheContentDateKey = NSURLContentAccessDateKey; |
| | | break; |
| | | } |
| | | |
| | |
| | | |
| | | #pragma mark - Hash |
| | | |
| | | static inline NSString *SDSanitizeFileNameString(NSString * _Nullable fileName) { |
| | | if ([fileName length] == 0) { |
| | | return fileName; |
| | | } |
| | | // note: `:` is the only invalid char on Apple file system |
| | | // but `/` or `\` is valid |
| | | // \0 is also special case (which cause Foundation API treat the C string as EOF) |
| | | NSCharacterSet* illegalFileNameCharacters = [NSCharacterSet characterSetWithCharactersInString:@"\0:"]; |
| | | return [[fileName componentsSeparatedByCharactersInSet:illegalFileNameCharacters] componentsJoinedByString:@""]; |
| | | } |
| | | |
| | | #define SD_MAX_FILE_EXTENSION_LENGTH (NAME_MAX - CC_MD5_DIGEST_LENGTH * 2 - 1) |
| | | |
| | | #pragma clang diagnostic push |
| | |
| | | } |
| | | unsigned char r[CC_MD5_DIGEST_LENGTH]; |
| | | CC_MD5(str, (CC_LONG)strlen(str), r); |
| | | NSString *ext; |
| | | // 1. Use URL path extname if valid |
| | | NSURL *keyURL = [NSURL URLWithString:key]; |
| | | NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension; |
| | | if (keyURL) { |
| | | ext = keyURL.pathExtension; |
| | | } |
| | | // 2. Use file extname if valid |
| | | if (!ext) { |
| | | ext = key.pathExtension; |
| | | } |
| | | // 3. Check if extname valid on file system |
| | | ext = SDSanitizeFileNameString(ext); |
| | | // File system has file name length limit, we need to check if ext is too long, we don't add it to the filename |
| | | if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) { |
| | | ext = nil; |
| | |
| | | |
| | | /* |
| | | * The attribute which the clear cache will be checked against when clearing the disk cache |
| | | * Default is Modified Date |
| | | * Default is Access Date |
| | | */ |
| | | @property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType; |
| | | |
| | |
| | | _diskCacheWritingOptions = NSDataWritingAtomic; |
| | | _maxDiskAge = kDefaultCacheMaxDiskAge; |
| | | _maxDiskSize = 0; |
| | | _diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate; |
| | | _diskCacheExpireType = SDImageCacheConfigExpireTypeAccessDate; |
| | | _fileManager = nil; |
| | | if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, watchOS 3.0, *)) { |
| | | _ioQueueAttributes = DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL; // DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM |
| | |
| | | |
| | | // Specify File Size for lossy format encoding, like JPEG |
| | | static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize"; |
| | | // Avoid ImageIO translate JFIF orientation to EXIF orientation which cause bug because returned CGImage already apply the orientation transform |
| | | static NSString * kSDCGImageSourceSkipMetadata = @"kCGImageSourceSkipMetadata"; |
| | | |
| | | // This strip the un-wanted CGImageProperty, like the internal CGImageSourceRef in iOS 15+ |
| | | // However, CGImageCreateCopy still keep those CGImageProperty, not suit for our use case |
| | |
| | | } |
| | | } |
| | | // Parse the image properties |
| | | NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)@{kSDCGImageSourceSkipMetadata : @(YES)}); |
| | | NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, NULL); |
| | | CGFloat pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue]; |
| | | CGFloat pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue]; |
| | | CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue]; |
| | | if (!exifOrientation) { |
| | | exifOrientation = kCGImagePropertyOrientationUp; |
| | | CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp; |
| | | NSNumber *exifOrientationValue = properties[(__bridge NSString *)kCGImagePropertyOrientation]; |
| | | if (exifOrientationValue != NULL) { |
| | | exifOrientation = [exifOrientationValue unsignedIntValue]; |
| | | } |
| | | |
| | | NSMutableDictionary *decodingOptions; |
| | |
| | | /** |
| | | A transformer protocol to transform the image load from cache or from download. |
| | | You can provide transformer to cache and manager (Through the `transformer` property or context option `SDWebImageContextImageTransformer`). |
| | | From v5.20, the transformer class also can be used on animated image frame post-transform logic, see `SDAnimatedImageView`. |
| | | |
| | | @note The transform process is called from a global queue in order to not to block the main queue. |
| | | */ |
| | |
| | | @required |
| | | /** |
| | | For each transformer, it must contains its cache key to used to store the image cache or query from the cache. This key will be appened after the original cache key generated by URL or from user. |
| | | Which means, the cache should match what your transformer logic do. The same `input image` + `transformer key`, should always generate the same `output image`. |
| | | |
| | | @return The cache key to appended after the original cache key. Should not be nil. |
| | | */ |
| | |
| | | The tint color. |
| | | */ |
| | | @property (nonatomic, strong, readonly, nonnull) UIColor *tintColor; |
| | | /// The blend mode, defaults to `sourceIn` if you use the initializer without blend mode |
| | | @property (nonatomic, assign, readonly) CGBlendMode blendMode; |
| | | |
| | | - (nonnull instancetype)init NS_UNAVAILABLE; |
| | | + (nonnull instancetype)new NS_UNAVAILABLE; |
| | | |
| | | + (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor; |
| | | + (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode; |
| | | |
| | | @end |
| | | |
| | |
| | | @interface SDImageTintTransformer () |
| | | |
| | | @property (nonatomic, strong, nonnull) UIColor *tintColor; |
| | | @property (nonatomic, assign) CGBlendMode blendMode; |
| | | |
| | | @end |
| | | |
| | | @implementation SDImageTintTransformer |
| | | |
| | | + (instancetype)transformerWithColor:(UIColor *)tintColor { |
| | | return [self transformerWithColor:tintColor blendMode:kCGBlendModeSourceIn]; |
| | | } |
| | | |
| | | + (instancetype)transformerWithColor:(UIColor *)tintColor blendMode:(CGBlendMode)blendMode { |
| | | SDImageTintTransformer *transformer = [SDImageTintTransformer new]; |
| | | transformer.tintColor = tintColor; |
| | | transformer.blendMode = blendMode; |
| | | |
| | | return transformer; |
| | | } |
| | | |
| | | - (NSString *)transformerKey { |
| | | return [NSString stringWithFormat:@"SDImageTintTransformer(%@)", self.tintColor.sd_hexString]; |
| | | return [NSString stringWithFormat:@"SDImageTintTransformer(%@,%d)", self.tintColor.sd_hexString, self.blendMode]; |
| | | } |
| | | |
| | | - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { |
| | |
| | | #pragma mark - Image Blending |
| | | |
| | | /** |
| | | Return a tinted image with the given color. This actually use alpha blending of current image and the tint color. |
| | | Return a tinted image with the given color. This actually use `sourceIn` blend mode. |
| | | @note Before 5.20, this API actually use `sourceAtop` and cause naming confusing. After 5.20, we match UIKit's behavior using `sourceIn`. |
| | | |
| | | @param tintColor The tint color. |
| | | @return The new image with the tint color. |
| | |
| | | - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor; |
| | | |
| | | /** |
| | | Return a tinted image with the given color and blend mode. |
| | | @note The blend mode treat `self` as background image (destination), treat `tintColor` as input image (source). So mostly you need `source` variant blend mode (use `sourceIn` not `destinationIn`), which is different from UIKit's `+[UIImage imageWithTintColor:]`. |
| | | |
| | | @param tintColor The tint color. |
| | | @param blendMode The blend mode. |
| | | @return The new image with the tint color. |
| | | */ |
| | | - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode; |
| | | |
| | | /** |
| | | Return the pixel color at specify position. The point is from the top-left to the bottom-right and 0-based. The returned the color is always be RGBA format. The image must be CG-based. |
| | | @note The point's x/y will be converted into integer. |
| | | @note The point's x/y should not be smaller than 0, or greater than or equal to width/height. |
| | |
| | | |
| | | #pragma mark - Image Blending |
| | | |
| | | static NSString * _Nullable SDGetCIFilterNameFromBlendMode(CGBlendMode blendMode) { |
| | | // CGBlendMode: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-CJBIJEFG |
| | | // CIFilter: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW71 |
| | | NSString *filterName; |
| | | switch (blendMode) { |
| | | case kCGBlendModeMultiply: |
| | | filterName = @"CIMultiplyBlendMode"; |
| | | break; |
| | | case kCGBlendModeScreen: |
| | | filterName = @"CIScreenBlendMode"; |
| | | break; |
| | | case kCGBlendModeOverlay: |
| | | filterName = @"CIOverlayBlendMode"; |
| | | break; |
| | | case kCGBlendModeDarken: |
| | | filterName = @"CIDarkenBlendMode"; |
| | | break; |
| | | case kCGBlendModeLighten: |
| | | filterName = @"CILightenBlendMode"; |
| | | break; |
| | | case kCGBlendModeColorDodge: |
| | | filterName = @"CIColorDodgeBlendMode"; |
| | | break; |
| | | case kCGBlendModeColorBurn: |
| | | filterName = @"CIColorBurnBlendMode"; |
| | | break; |
| | | case kCGBlendModeSoftLight: |
| | | filterName = @"CISoftLightBlendMode"; |
| | | break; |
| | | case kCGBlendModeHardLight: |
| | | filterName = @"CIHardLightBlendMode"; |
| | | break; |
| | | case kCGBlendModeDifference: |
| | | filterName = @"CIDifferenceBlendMode"; |
| | | break; |
| | | case kCGBlendModeExclusion: |
| | | filterName = @"CIExclusionBlendMode"; |
| | | break; |
| | | case kCGBlendModeHue: |
| | | filterName = @"CIHueBlendMode"; |
| | | break; |
| | | case kCGBlendModeSaturation: |
| | | filterName = @"CISaturationBlendMode"; |
| | | break; |
| | | case kCGBlendModeColor: |
| | | // Color blend mode uses the luminance values of the background with the hue and saturation values of the source image. |
| | | filterName = @"CIColorBlendMode"; |
| | | break; |
| | | case kCGBlendModeLuminosity: |
| | | filterName = @"CILuminosityBlendMode"; |
| | | break; |
| | | |
| | | // macOS 10.5+ |
| | | case kCGBlendModeSourceAtop: |
| | | case kCGBlendModeDestinationAtop: |
| | | filterName = @"CISourceAtopCompositing"; |
| | | break; |
| | | case kCGBlendModeSourceIn: |
| | | case kCGBlendModeDestinationIn: |
| | | filterName = @"CISourceInCompositing"; |
| | | break; |
| | | case kCGBlendModeSourceOut: |
| | | case kCGBlendModeDestinationOut: |
| | | filterName = @"CISourceOutCompositing"; |
| | | break; |
| | | case kCGBlendModeNormal: // SourceOver |
| | | case kCGBlendModeDestinationOver: |
| | | filterName = @"CISourceOverCompositing"; |
| | | break; |
| | | |
| | | // need special handling |
| | | case kCGBlendModeClear: |
| | | // use clear color instead |
| | | break; |
| | | case kCGBlendModeCopy: |
| | | // use input color instead |
| | | break; |
| | | case kCGBlendModeXOR: |
| | | // unsupported |
| | | break; |
| | | case kCGBlendModePlusDarker: |
| | | // chain filters |
| | | break; |
| | | case kCGBlendModePlusLighter: |
| | | // chain filters |
| | | break; |
| | | } |
| | | return filterName; |
| | | } |
| | | |
| | | - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor { |
| | | return [self sd_tintedImageWithColor:tintColor blendMode:kCGBlendModeSourceIn]; |
| | | } |
| | | |
| | | - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode { |
| | | BOOL hasTint = CGColorGetAlpha(tintColor.CGColor) > __FLT_EPSILON__; |
| | | if (!hasTint) { |
| | | return self; |
| | | } |
| | | |
| | | // blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing |
| | | #if SD_UIKIT || SD_MAC |
| | | // CIImage shortcut |
| | | if (self.CIImage) { |
| | | CIImage *ciImage = self.CIImage; |
| | | CIImage *ciImage = self.CIImage; |
| | | if (ciImage) { |
| | | CIImage *colorImage = [CIImage imageWithColor:[[CIColor alloc] initWithColor:tintColor]]; |
| | | colorImage = [colorImage imageByCroppingToRect:ciImage.extent]; |
| | | CIFilter *filter = [CIFilter filterWithName:@"CISourceAtopCompositing"]; |
| | | [filter setValue:colorImage forKey:kCIInputImageKey]; |
| | | [filter setValue:ciImage forKey:kCIInputBackgroundImageKey]; |
| | | ciImage = filter.outputImage; |
| | | NSString *filterName = SDGetCIFilterNameFromBlendMode(blendMode); |
| | | // Some blend mode is not nativelly supported |
| | | if (filterName) { |
| | | CIFilter *filter = [CIFilter filterWithName:filterName]; |
| | | [filter setValue:colorImage forKey:kCIInputImageKey]; |
| | | [filter setValue:ciImage forKey:kCIInputBackgroundImageKey]; |
| | | ciImage = filter.outputImage; |
| | | } else { |
| | | if (blendMode == kCGBlendModeClear) { |
| | | // R = 0 |
| | | CIColor *clearColor; |
| | | if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)) { |
| | | clearColor = CIColor.clearColor; |
| | | } else { |
| | | clearColor = [[CIColor alloc] initWithColor:UIColor.clearColor]; |
| | | } |
| | | colorImage = [CIImage imageWithColor:clearColor]; |
| | | colorImage = [colorImage imageByCroppingToRect:ciImage.extent]; |
| | | ciImage = colorImage; |
| | | } else if (blendMode == kCGBlendModeCopy) { |
| | | // R = S |
| | | ciImage = colorImage; |
| | | } else if (blendMode == kCGBlendModePlusLighter) { |
| | | // R = MIN(1, S + D) |
| | | // S + D |
| | | CIFilter *filter = [CIFilter filterWithName:@"CIAdditionCompositing"]; |
| | | [filter setValue:colorImage forKey:kCIInputImageKey]; |
| | | [filter setValue:ciImage forKey:kCIInputBackgroundImageKey]; |
| | | ciImage = filter.outputImage; |
| | | // MIN |
| | | ciImage = [ciImage imageByApplyingFilter:@"CIColorClamp" withInputParameters:nil]; |
| | | } else if (blendMode == kCGBlendModePlusDarker) { |
| | | // R = MAX(0, (1 - D) + (1 - S)) |
| | | // (1 - D) |
| | | CIFilter *filter1 = [CIFilter filterWithName:@"CIColorControls"]; |
| | | [filter1 setValue:ciImage forKey:kCIInputImageKey]; |
| | | [filter1 setValue:@(-0.5) forKey:kCIInputBrightnessKey]; |
| | | ciImage = filter1.outputImage; |
| | | // (1 - S) |
| | | CIFilter *filter2 = [CIFilter filterWithName:@"CIColorControls"]; |
| | | [filter2 setValue:colorImage forKey:kCIInputImageKey]; |
| | | [filter2 setValue:@(-0.5) forKey:kCIInputBrightnessKey]; |
| | | colorImage = filter2.outputImage; |
| | | // + |
| | | CIFilter *filter = [CIFilter filterWithName:@"CIAdditionCompositing"]; |
| | | [filter setValue:colorImage forKey:kCIInputImageKey]; |
| | | [filter setValue:ciImage forKey:kCIInputBackgroundImageKey]; |
| | | ciImage = filter.outputImage; |
| | | // MAX |
| | | ciImage = [ciImage imageByApplyingFilter:@"CIColorClamp" withInputParameters:nil]; |
| | | } else { |
| | | SD_LOG("UIImage+Transform error: Unsupported blend mode: %d", blendMode); |
| | | ciImage = nil; |
| | | } |
| | | } |
| | | |
| | | if (ciImage) { |
| | | #if SD_UIKIT |
| | | UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; |
| | | #else |
| | | UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; |
| | | #endif |
| | | return image; |
| | | } |
| | | } |
| | | #endif |
| | | |
| | | CGSize size = self.size; |
| | | CGRect rect = { CGPointZero, size }; |
| | | CGFloat scale = self.scale; |
| | | |
| | | // blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing |
| | | CGBlendMode blendMode = kCGBlendModeSourceAtop; |
| | | |
| | | SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; |
| | | format.scale = scale; |
| | |
| | | static NSMapTable *providerFramePoolMap; |
| | | static dispatch_once_t onceToken; |
| | | dispatch_once(&onceToken, ^{ |
| | | providerFramePoolMap = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; |
| | | // Key use `hash` && `isEqual:` |
| | | providerFramePoolMap = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; |
| | | }); |
| | | return providerFramePoolMap; |
| | | } |
| | |
| | | <li><a href="https://github.com/SwifterSwift/SwifterSwift/tree/master/Sources/SwifterSwift/Foundation/NSPredicateExtensions.swift"><code>NSPredicate extensions</code></a></li> |
| | | <li><a href="https://github.com/SwifterSwift/SwifterSwift/tree/master/Sources/SwifterSwift/Foundation/URLExtensions.swift"><code>URL extensions</code></a></li> |
| | | <li><a href="https://github.com/SwifterSwift/SwifterSwift/tree/master/Sources/SwifterSwift/Foundation/URLRequestExtensions.swift"><code>URLRequest extensions</code></a></li> |
| | | <li><a href="https://github.com/SwifterSwift/SwifterSwift/tree/master/Sources/SwifterSwift/Foundation/URLSessionExtensions.swift"><code>URLSession extensions</code></a></li> |
| | | <li><a href="https://github.com/SwifterSwift/SwifterSwift/tree/master/Sources/SwifterSwift/Foundation/UserDefaultsExtensions.swift"><code>UserDefaults extensions</code></a></li> |
| | | </ul> |
| | | </details> |
| | |
| | | </br> |
| | | <ul> |
| | | <li><a href="https://github.com/SwifterSwift/SwifterSwift/blob/master/Tests/XCTest/XCTestExtensions.swift"><code>XCTest extensions</code></a></li> |
| | | </ul> |
| | | </details> |
| | | |
| | | <details> |
| | | <summary>Combine Extensions</summary> |
| | | </br> |
| | | <ul> |
| | | <li><a href="https://github.com/SwifterSwift/SwifterSwift/blob/master/Sources/SwifterSwift/Combine/FutureExtensions.swift"><code>Future extensions</code></a></li> |
| | | </ul> |
| | | </details> |
| | | |
| | |
| | | guard imageHeight > 0 else { return self } |
| | | |
| | | // Get ratio (landscape or portrait) |
| | | let ratio: CGFloat |
| | | if imageWidth > imageHeight { |
| | | let ratio: CGFloat = if imageWidth > imageHeight { |
| | | // Landscape |
| | | ratio = maxSize.width / imageWidth |
| | | maxSize.width / imageWidth |
| | | } else { |
| | | // Portrait |
| | | ratio = maxSize.height / imageHeight |
| | | maxSize.height / imageHeight |
| | | } |
| | | |
| | | // Calculate new size based on the ratio |
| | |
| | | /// - endPoint: end point corresponds to the last gradient stop |
| | | /// - type: The kind of gradient that will be drawn. Currently, the only allowed values are `axial' (the default |
| | | /// value), `radial', and `conic'. |
| | | @available(macOS 10.14, *) |
| | | convenience init(colors: [SFColor], |
| | | locations: [CGFloat]? = nil, |
| | | startPoint: CGPoint = CGPoint(x: 0.5, y: 0), |
| | |
| | | |
| | | // swiftlint:disable identifier_name |
| | | |
| | | #if canImport(QuartzCore) |
| | | #if canImport(QuartzCore) && !os(watchOS) |
| | | |
| | | import QuartzCore |
| | | |
| | | // MARK: - Equatable |
| | | |
| | | extension CATransform3D: Equatable { |
| | | extension CATransform3D: Swift.Equatable { |
| | | // swiftlint:disable missing_swifterswift_prefix |
| | | |
| | | /// Returns a Boolean value indicating whether two values are equal. |
| | |
| | | #if canImport(CoreGraphics) |
| | | import CoreGraphics |
| | | |
| | | #if canImport(QuartzCore) |
| | | #if canImport(QuartzCore) && !os(watchOS) |
| | | |
| | | import QuartzCore |
| | | |
| | |
| | | // MARK: - Properties |
| | | |
| | | public extension DispatchQueue { |
| | | #if !os(Linux) |
| | | /// SwifterSwift: A Boolean value indicating whether the current dispatch queue is the main queue. |
| | | static var isMainQueue: Bool { |
| | | enum Static { |
| | | static var key: DispatchSpecificKey<Void> = { |
| | | static let key: DispatchSpecificKey<Void> = { |
| | | let key = DispatchSpecificKey<Void>() |
| | | DispatchQueue.main.setSpecific(key: key, value: ()) |
| | | return key |
| | |
| | | } |
| | | return DispatchQueue.getSpecific(key: Static.key) != nil |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | // MARK: - Methods |
| | |
| | | func asyncAfter(delay: Double, |
| | | qos: DispatchQoS = .unspecified, |
| | | flags: DispatchWorkItemFlags = [], |
| | | execute work: @escaping () -> Void) { |
| | | execute work: @Sendable @escaping () -> Void) { |
| | | asyncAfter(deadline: .now() + delay, qos: qos, flags: flags, execute: work) |
| | | } |
| | | |
| | | #if !os(Linux) |
| | | @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) |
| | | func debounce(delay: Double, action: @escaping () -> Void) -> () -> Void { |
| | | // http://stackoverflow.com/questions/27116684/how-can-i-debounce-a-method-call |
| | | var lastFireTime = DispatchTime.now() |
| | | let deadline = { lastFireTime + delay } |
| | | return { |
| | | self.asyncAfter(deadline: deadline()) { |
| | | self.asyncAfterUnsafe(deadline: deadline()) { |
| | | let now = DispatchTime.now() |
| | | if now >= deadline() { |
| | | lastFireTime = now |
| | |
| | | } |
| | | } |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | #endif |
| | |
| | | /// - range: The range in which to create a random date. `range` must not be empty. |
| | | /// - generator: The random number generator to use when creating the new random date. |
| | | /// - Returns: A random date within the bounds of `range`. |
| | | static func random<T>(in range: Range<Date>, using generator: inout T) -> Date where T: RandomNumberGenerator { |
| | | static func random(in range: Range<Date>, using generator: inout some RandomNumberGenerator) -> Date { |
| | | return Date(timeIntervalSinceReferenceDate: |
| | | TimeInterval.random( |
| | | in: range.lowerBound.timeIntervalSinceReferenceDate..<range.upperBound.timeIntervalSinceReferenceDate, |
| | |
| | | /// - range: The range in which to create a random date. |
| | | /// - generator: The random number generator to use when creating the new random date. |
| | | /// - Returns: A random date within the bounds of `range`. |
| | | static func random<T>(in range: ClosedRange<Date>, using generator: inout T) -> Date |
| | | where T: RandomNumberGenerator { |
| | | static func random(in range: ClosedRange<Date>, using generator: inout some RandomNumberGenerator) -> Date { |
| | | return Date(timeIntervalSinceReferenceDate: |
| | | TimeInterval.random( |
| | | in: range.lowerBound.timeIntervalSinceReferenceDate...range.upperBound.timeIntervalSinceReferenceDate, |
| | |
| | | /// - Throws: An error if a temporary directory cannot be found or created. |
| | | /// - Returns: A URL to a new directory for saving temporary files. |
| | | func createTemporaryDirectory() throws -> URL { |
| | | #if !os(Linux) |
| | | return try url(for: .itemReplacementDirectory, |
| | | in: .userDomainMask, |
| | | appropriateFor: temporaryDirectory, |
| | | create: true) |
| | | #else |
| | | let envs = ProcessInfo.processInfo.environment |
| | | let env = envs["TMPDIR"] ?? envs["TEMP"] ?? envs["TMP"] ?? "/tmp" |
| | | let dir = "/\(env)/file-temp.XXXXXX" |
| | | var template = [UInt8](dir.utf8).map { Int8($0) } + [Int8(0)] |
| | | guard mkdtemp(&template) != nil else { throw CocoaError.error(.featureUnsupported) } |
| | | return URL(fileURLWithPath: String(cString: template)) |
| | | #endif |
| | | try url(for: .itemReplacementDirectory, |
| | | in: .userDomainMask, |
| | | appropriateFor: temporaryDirectory, |
| | | create: true) |
| | | } |
| | | } |
| | | |
| | |
| | | /// Adapted from https://stackoverflow.com/a/30403199/1627511 |
| | | /// - Returns: A flag emoji string for the given region code (optional). |
| | | static func flagEmoji(forRegionCode isoRegionCode: String) -> String? { |
| | | #if !os(Linux) |
| | | guard isoRegionCodes.contains(isoRegionCode) else { return nil } |
| | | #endif |
| | | if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) { |
| | | guard Locale.Region.isoRegions.contains(where: { $0.identifier == isoRegionCode }) else { return nil } |
| | | } else { |
| | | guard isoRegionCodes.contains(isoRegionCode) else { return nil } |
| | | } |
| | | |
| | | return isoRegionCode.unicodeScalars.reduce(into: String()) { |
| | | guard let flagScalar = UnicodeScalar(UInt32(127_397) + $1.value) else { return } |
| | |
| | | /// - attributes: Dictionary of attributes. |
| | | /// - target: a subsequence string for the attributes to be applied to. |
| | | /// - Returns: An NSAttributedString with attributes applied on the target string. |
| | | func applying<T: StringProtocol>(attributes: [Key: Any], |
| | | toOccurrencesOf target: T) -> NSAttributedString { |
| | | func applying(attributes: [Key: Any], |
| | | toOccurrencesOf target: some StringProtocol) -> NSAttributedString { |
| | | let pattern = "\\Q\(target)\\E" |
| | | |
| | | return applying(attributes: attributes, toRangesMatching: pattern) |
| | |
| | | #if canImport(Foundation) |
| | | import Foundation |
| | | |
| | | #if canImport(Combine) |
| | | import Combine |
| | | |
| | | public extension NotificationCenter { |
| | | /// SwifterSwift: Adds a one-time entry to the notification center's dispatch table that includes a notification |
| | | /// queue and a block to add to the queue, and an optional notification name and sender. |
| | |
| | | /// |
| | | /// The block takes one argument: |
| | | /// - notification: The notification. |
| | | func observeOnce(forName name: NSNotification.Name?, |
| | | object obj: Any? = nil, |
| | | queue: OperationQueue? = nil, |
| | | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) |
| | | func observeOnce(forName name: NSNotification.Name, |
| | | object obj: AnyObject? = nil, |
| | | using block: @escaping (_ notification: Notification) -> Void) { |
| | | var handler: (any NSObjectProtocol)! |
| | | let removeObserver = { [unowned self] in |
| | | self.removeObserver(handler!) |
| | | } |
| | | handler = addObserver(forName: name, object: obj, queue: queue) { |
| | | removeObserver() |
| | | var handler: AnyCancellable! |
| | | handler = publisher(for: name, object: obj).sink { |
| | | handler.cancel() |
| | | block($0) |
| | | } |
| | | } |
| | | } |
| | | |
| | | #endif |
| | | |
| | | #endif |
| | |
| | | /// interpreted relative to `url`. |
| | | /// - url: The base URL for the `URL` object. |
| | | init?(string: String?, relativeTo url: URL? = nil) { |
| | | guard let string = string else { return nil } |
| | | guard let string else { return nil } |
| | | self.init(string: string, relativeTo: url) |
| | | } |
| | | |
| | |
| | | /// let url = URL(string: "https://domain.com")! |
| | | /// print(url.droppedScheme()) // prints "domain.com" |
| | | func droppedScheme() -> URL? { |
| | | if let scheme = scheme { |
| | | if let scheme { |
| | | let droppedScheme = String(absoluteString.dropFirst(scheme.count + 3)) |
| | | return URL(string: droppedScheme) |
| | | } |
| | |
| | | |
| | | /// SwifterSwift: cURL command representation of this URL request. |
| | | var curlString: String { |
| | | guard let url = url else { return "" } |
| | | guard let url else { return "" } |
| | | |
| | | var baseCommand = "curl \(url.absoluteString)" |
| | | if httpMethod == "HEAD" { |
| | |
| | | /// - object: Codable object to store. |
| | | /// - key: Identifier of the object. |
| | | /// - encoder: Custom JSONEncoder instance. Defaults to `JSONEncoder()`. |
| | | func set<T: Codable>(object: T, forKey key: String, usingEncoder encoder: JSONEncoder = JSONEncoder()) { |
| | | func set(object: some Codable, forKey key: String, usingEncoder encoder: JSONEncoder = JSONEncoder()) { |
| | | let data = try? encoder.encode(object) |
| | | set(data, forKey: key) |
| | | } |
| | |
| | | } |
| | | |
| | | // swiftlint:disable missing_swifterswift_prefix |
| | | extension NSEdgeInsets: Equatable { |
| | | extension NSEdgeInsets: Swift.Equatable { |
| | | /// Returns a Boolean value indicating whether two values are equal. |
| | | /// |
| | | /// Equality is the inverse of inequality. For any values `a` and `b`, |
| | |
| | | /// mySKNode.descendants() -> [childNodeOne, childNodeTwo] |
| | | /// |
| | | func descendants() -> [SKNode] { |
| | | var children = self.children |
| | | var children = children |
| | | children.append(contentsOf: children.reduce(into: [SKNode]()) { $0.append(contentsOf: $1.descendants()) }) |
| | | return children |
| | | } |
| | |
| | | /// |
| | | /// - Parameter path: Key path to compare, the value must be Equatable. |
| | | /// - Returns: an array of unique elements. |
| | | func withoutDuplicates<E: Equatable>(keyPath path: KeyPath<Element, E>) -> [Element] { |
| | | func withoutDuplicates(keyPath path: KeyPath<Element, some Equatable>) -> [Element] { |
| | | return reduce(into: [Element]()) { result, element in |
| | | if !result.contains(where: { $0[keyPath: path] == element[keyPath: path] }) { |
| | | result.append(element) |
| | |
| | | |
| | | // MARK: - Methods |
| | | |
| | | public extension Collection { |
| | | public extension Collection where Self: Sendable { |
| | | #if canImport(Dispatch) |
| | | /// SwifterSwift: Performs `each` closure for each element of collection in parallel. |
| | | /// |
| | |
| | | /// } |
| | | /// |
| | | /// - Parameter each: closure to run for each element. |
| | | func forEachInParallel(_ each: (Self.Element) -> Void) { |
| | | func forEachInParallel(_ each: @Sendable (Self.Element) -> Void) { |
| | | DispatchQueue.concurrentPerform(iterations: count) { |
| | | each(self[index(startIndex, offsetBy: $0)]) |
| | | } |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | public extension Collection { |
| | | /// SwifterSwift: Safe protects the array from out of bounds by use of optional. |
| | | /// |
| | | /// let arr = [1, 2, 3, 4, 5] |
| | |
| | | return slices |
| | | } |
| | | |
| | | #if !os(Linux) |
| | | /// SwifterSwift: Get all indices where condition is met. |
| | | /// |
| | | /// [1, 7, 1, 2, 4, 1, 8].indices(where: { $0 == 1 }) -> [0, 2, 5] |
| | |
| | | /// - Parameter condition: condition to evaluate each element against. |
| | | /// - Returns: all indices where the specified condition evaluates to true (optional). |
| | | func indices(where condition: (Element) throws -> Bool) rethrows -> [Index]? { |
| | | let indices = try self.indices.filter { try condition(self[$0]) } |
| | | let indices = try indices.filter { try condition(self[$0]) } |
| | | return indices.isEmpty ? nil : indices |
| | | } |
| | | #endif |
| | | |
| | | /// SwifterSwift: Calls the given closure with an array of size of the parameter slice. |
| | | /// |
| | |
| | | /// dict.keys.contains("key2") -> false |
| | | /// |
| | | /// - Parameter keys: keys to be removed. |
| | | mutating func removeAll<S: Sequence>(keys: S) where S.Element == Key { |
| | | mutating func removeAll(keys: some Sequence<Key>) { |
| | | keys.forEach { removeValue(forKey: $0) } |
| | | } |
| | | |
| | |
| | | /// - lhs: dictionary. |
| | | /// - keys: array with the keys to be removed. |
| | | /// - Returns: a new dictionary with keys removed. |
| | | static func - <S: Sequence>(lhs: [Key: Value], keys: S) -> [Key: Value] where S.Element == Key { |
| | | static func - (lhs: [Key: Value], keys: some Sequence<Key>) -> [Key: Value] { |
| | | var result = lhs |
| | | result.removeAll(keys: keys) |
| | | return result |
| | |
| | | /// - Parameters: |
| | | /// - lhs: dictionary. |
| | | /// - keys: array with the keys to be removed. |
| | | static func -= <S: Sequence>(lhs: inout [Key: Value], keys: S) where S.Element == Key { |
| | | static func -= (lhs: inout [Key: Value], keys: some Sequence<Key>) { |
| | | lhs.removeAll(keys: keys) |
| | | } |
| | | } |
| | |
| | | /// SwifterSwift: Sort the collection based on a keypath. |
| | | /// |
| | | /// - Parameter keyPath: Key path to sort by. The key path type must be Comparable. |
| | | mutating func sort<T: Comparable>(by keyPath: KeyPath<Element, T>) { |
| | | mutating func sort(by keyPath: KeyPath<Element, some Comparable>) { |
| | | sort { $0[keyPath: keyPath] < $1[keyPath: keyPath] } |
| | | } |
| | | |
| | |
| | | /// - Parameters: |
| | | /// - keyPath1: Key path to sort by. Must be Comparable. |
| | | /// - keyPath2: Key path to sort by in case the values of `keyPath1` match. Must be Comparable. |
| | | mutating func sort<T: Comparable, U: Comparable>(by keyPath1: KeyPath<Element, T>, |
| | | and keyPath2: KeyPath<Element, U>) { |
| | | mutating func sort(by keyPath1: KeyPath<Element, some Comparable>, |
| | | and keyPath2: KeyPath<Element, some Comparable>) { |
| | | sort { |
| | | if $0[keyPath: keyPath1] != $1[keyPath: keyPath1] { |
| | | return $0[keyPath: keyPath1] < $1[keyPath: keyPath1] |
| | |
| | | /// - keyPath1: Key path to sort by. Must be Comparable. |
| | | /// - keyPath2: Key path to sort by in case the values of `keyPath1` match. Must be Comparable. |
| | | /// - keyPath3: Key path to sort by in case the values of `keyPath1` and `keyPath2` match. Must be Comparable. |
| | | mutating func sort<T: Comparable, U: Comparable, V: Comparable>(by keyPath1: KeyPath<Element, T>, |
| | | and keyPath2: KeyPath<Element, U>, |
| | | and keyPath3: KeyPath<Element, V>) { |
| | | mutating func sort(by keyPath1: KeyPath<Element, some Comparable>, |
| | | and keyPath2: KeyPath<Element, some Comparable>, |
| | | and keyPath3: KeyPath<Element, some Comparable>) { |
| | | sort { |
| | | if $0[keyPath: keyPath1] != $1[keyPath: keyPath1] { |
| | | return $0[keyPath: keyPath1] < $1[keyPath: keyPath1] |
| | |
| | | /// - lhs: Any? |
| | | /// - rhs: Any? |
| | | static func ??= (lhs: inout Optional, rhs: Optional) { |
| | | guard let rhs = rhs else { return } |
| | | guard let rhs else { return } |
| | | lhs = rhs |
| | | } |
| | | |
| | |
| | | /// SwifterSwift: Remove all duplicate elements using KeyPath to compare. |
| | | /// |
| | | /// - Parameter path: Key path to compare, the value must be Equatable. |
| | | mutating func removeDuplicates<E: Equatable>(keyPath path: KeyPath<Element, E>) { |
| | | mutating func removeDuplicates(keyPath path: KeyPath<Element, some Equatable>) { |
| | | var items = [Element]() |
| | | removeAll { element -> Bool in |
| | | guard items.contains(where: { $0[keyPath: path] == element[keyPath: path] }) else { |
| | |
| | | - Parameter newElement: The optional element to append to the array |
| | | */ |
| | | mutating func appendIfNonNil(_ newElement: Element?) { |
| | | guard let newElement = newElement else { return } |
| | | guard let newElement else { return } |
| | | append(newElement) |
| | | } |
| | | |
| | |
| | | - Parameter newElements: The optional sequence to append to the array |
| | | */ |
| | | mutating func appendIfNonNil<S>(contentsOf newElements: S?) where Element == S.Element, S: Sequence { |
| | | guard let newElements = newElements else { return } |
| | | guard let newElements else { return } |
| | | append(contentsOf: newElements) |
| | | } |
| | | } |
| | |
| | | /// - Parameter compare: Comparison function that will determine the ordering. |
| | | /// - Returns: The sorted array. |
| | | func sorted<T>(by map: (Element) throws -> T, with compare: (T, T) -> Bool) rethrows -> [Element] { |
| | | return try sorted { compare(try map($0), try map($1)) } |
| | | return try sorted { try compare(map($0), map($1)) } |
| | | } |
| | | |
| | | /// SwifterSwift: Return a sorted array based on a key path. |
| | | /// |
| | | /// - Parameter keyPath: Key path to sort by. The key path type must be Comparable. |
| | | /// - Returns: The sorted array. |
| | | func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] { |
| | | func sorted(by keyPath: KeyPath<Element, some Comparable>) -> [Element] { |
| | | return sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] } |
| | | } |
| | | |
| | |
| | | /// |
| | | /// - Parameter map: Function that defines the property to sort by. The output type must be Comparable. |
| | | /// - Returns: The sorted array. |
| | | func sorted<T: Comparable>(by map: (Element) throws -> T) rethrows -> [Element] { |
| | | func sorted(by map: (Element) throws -> some Comparable) rethrows -> [Element] { |
| | | return try sorted { try map($0) < map($1) } |
| | | } |
| | | |
| | |
| | | /// - Parameters: |
| | | /// - keyPath1: Key path to sort by. Must be Comparable. |
| | | /// - keyPath2: Key path to sort by in case the values of `keyPath1` match. Must be Comparable. |
| | | func sorted<T: Comparable, U: Comparable>(by keyPath1: KeyPath<Element, T>, |
| | | and keyPath2: KeyPath<Element, U>) -> [Element] { |
| | | func sorted(by keyPath1: KeyPath<Element, some Comparable>, |
| | | and keyPath2: KeyPath<Element, some Comparable>) -> [Element] { |
| | | return sorted { |
| | | if $0[keyPath: keyPath1] != $1[keyPath: keyPath1] { |
| | | return $0[keyPath: keyPath1] < $1[keyPath: keyPath1] |
| | |
| | | } |
| | | } |
| | | |
| | | /// SwifterSwift: Returns a sorted sequence based on two map functions. The second one will be used in case the values |
| | | /// of the first one match. |
| | | /// SwifterSwift: Returns a sorted sequence based on two map functions. The second one will be used in case the |
| | | /// values of the first one match. |
| | | /// |
| | | /// - Parameters: |
| | | /// - map1: Map function to sort by. Output type must be Comparable. |
| | | /// - map2: Map function to sort by in case the values of `map1` match. Output type must be Comparable. |
| | | func sorted<T: Comparable, U: Comparable>(by map1: (Element) throws -> T, |
| | | and map2: (Element) throws -> U) rethrows -> [Element] { |
| | | func sorted(by map1: (Element) throws -> some Comparable, |
| | | and map2: (Element) throws -> some Comparable) rethrows -> [Element] { |
| | | return try sorted { |
| | | let value10 = try map1($0) |
| | | let value11 = try map1($1) |
| | |
| | | /// - keyPath1: Key path to sort by. Must be Comparable. |
| | | /// - keyPath2: Key path to sort by in case the values of `keyPath1` match. Must be Comparable. |
| | | /// - keyPath3: Key path to sort by in case the values of `keyPath1` and `keyPath2` match. Must be Comparable. |
| | | func sorted<T: Comparable, U: Comparable, V: Comparable>(by keyPath1: KeyPath<Element, T>, |
| | | and keyPath2: KeyPath<Element, U>, |
| | | and keyPath3: KeyPath<Element, V>) -> [Element] { |
| | | func sorted(by keyPath1: KeyPath<Element, some Comparable>, |
| | | and keyPath2: KeyPath<Element, some Comparable>, |
| | | and keyPath3: KeyPath<Element, some Comparable>) |
| | | -> [Element] { |
| | | return sorted { |
| | | if $0[keyPath: keyPath1] != $1[keyPath: keyPath1] { |
| | | return $0[keyPath: keyPath1] < $1[keyPath: keyPath1] |
| | |
| | | } |
| | | } |
| | | |
| | | /// SwifterSwift: Returns a sorted sequence based on three map functions. Whenever the values of one map function match, the |
| | | /// next one will be used. |
| | | /// SwifterSwift: Returns a sorted sequence based on three map functions. Whenever the values of one map function |
| | | /// match, the next one will be used. |
| | | /// |
| | | /// - Parameters: |
| | | /// - map1: Map function to sort by. Output type must be Comparable. |
| | | /// - map2: Map function to sort by in case the values of `map1` match. Output type must be Comparable. |
| | | /// - map3: Map function to sort by in case the values of `map1` and `map2` match. Output type must be Comparable. |
| | | func sorted<T: Comparable, U: Comparable, V: Comparable>(by map1: (Element) throws -> T, |
| | | and map2: (Element) throws -> U, |
| | | and map3: (Element) throws -> V) rethrows -> [Element] { |
| | | /// - map3: Map function to sort by in case the values of `map1` and `map2` match. |
| | | /// Output type must be Comparable. |
| | | func sorted(by map1: (Element) throws -> some Comparable, |
| | | and map2: (Element) throws -> some Comparable, |
| | | and map3: (Element) throws -> some Comparable) rethrows |
| | | -> [Element] { |
| | | return try sorted { |
| | | let value10 = try map1($0) |
| | | let value11 = try map1($1) |
| | | if value10 != value11 { |
| | | return value10 < value11 |
| | | } |
| | | |
| | | |
| | | let value20 = try map2($0) |
| | | let value21 = try map2($1) |
| | | if value20 != value21 { |
| | |
| | | /// - Parameters: |
| | | /// - map: Function for `Element` to compare. |
| | | /// - value: The value to compare with `Element` property. |
| | | /// - Returns: The first element of the collection that has property by given map function equals to given `value` or |
| | | /// `nil` if there is no such element. |
| | | /// - Returns: The first element of the collection that has property by given map function equals to given `value` |
| | | /// or `nil` if there is no such element. |
| | | func first<T: Equatable>(where map: (Element) throws -> T, equals value: T) rethrows -> Element? { |
| | | return try first { try map($0) == value } |
| | | } |
| | |
| | | /// "".firstCharacterAsString -> nil |
| | | /// |
| | | var firstCharacterAsString: String? { |
| | | guard let first = first else { return nil } |
| | | guard let first else { return nil } |
| | | return String(first) |
| | | } |
| | | |
| | |
| | | /// "".lastCharacterAsString -> nil |
| | | /// |
| | | var lastCharacterAsString: String? { |
| | | guard let last = last else { return nil } |
| | | guard let last else { return nil } |
| | | return String(last) |
| | | } |
| | | |
| | |
| | | |
| | | #if os(iOS) || os(tvOS) |
| | | /// SwifterSwift: Check if the given string spelled correctly. |
| | | @MainActor |
| | | var isSpelledCorrectly: Bool { |
| | | let checker = UITextChecker() |
| | | let range = NSRange(startIndex..<endIndex, in: self) |
| | |
| | | /// |
| | | /// - Returns: The string in slug format. |
| | | func toSlug() -> String { |
| | | let lowercased = self.lowercased() |
| | | let lowercased = lowercased() |
| | | let latinized = lowercased.folding(options: .diacriticInsensitive, locale: Locale.current) |
| | | let withDashes = latinized.replacingOccurrences(of: " ", with: "-") |
| | | |
| | |
| | | /// "".firstCharacterUppercased() -> "" |
| | | /// |
| | | mutating func firstCharacterUppercased() { |
| | | guard let first = first else { return } |
| | | guard let first else { return } |
| | | self = String(first).uppercased() + dropFirst() |
| | | } |
| | | |
| | |
| | | /// - Parameter base64: base64 string. |
| | | init?(base64: String) { |
| | | guard let decodedData = Data(base64Encoded: base64) else { return nil } |
| | | guard let str = String(data: decodedData, encoding: .utf8) else { return nil } |
| | | self.init(str) |
| | | self.init(data: decodedData, encoding: .utf8) |
| | | } |
| | | #endif |
| | | } |
| | |
| | | /// - Parameter aString: The string with which to compare the receiver. |
| | | /// - Parameter options: Options for the comparison. |
| | | /// - Returns: The longest common suffix of the receiver and the given String. |
| | | func commonSuffix<T: StringProtocol>(with aString: T, options: String.CompareOptions = []) -> String { |
| | | func commonSuffix(with aString: some StringProtocol, options: String.CompareOptions = []) -> String { |
| | | return String(zip(reversed(), aString.reversed()) |
| | | .lazy |
| | | .prefix(while: { (lhs: Character, rhs: Character) in |
| | |
| | | /// - searchRange: The range in the receiver in which to search. |
| | | /// - Returns: A new string in which all occurrences of regex pattern in searchRange of the receiver are replaced by |
| | | /// template. |
| | | func replacingOccurrences<Target, Replacement>( |
| | | ofPattern pattern: Target, |
| | | withTemplate template: Replacement, |
| | | func replacingOccurrences( |
| | | ofPattern pattern: some StringProtocol, |
| | | withTemplate template: some StringProtocol, |
| | | options: String.CompareOptions = [.regularExpression], |
| | | range searchRange: Range<Self.Index>? = nil) -> String where Target: StringProtocol, |
| | | Replacement: StringProtocol { |
| | | range searchRange: Range<Self.Index>? = nil) -> String { |
| | | assert( |
| | | options.isStrictSubset(of: [.regularExpression, .anchored, .caseInsensitive]), |
| | | "Invalid options for regular expression replacement") |
| | |
| | | /// - Parameters: |
| | | /// - nib: Nib file used to create the collectionView cell. |
| | | /// - name: UICollectionViewCell type. |
| | | func register<T: UICollectionViewCell>(nib: UINib?, forCellWithClass name: T.Type) { |
| | | func register(nib: UINib?, forCellWithClass name: (some UICollectionViewCell).Type) { |
| | | register(nib, forCellWithReuseIdentifier: String(describing: name)) |
| | | } |
| | | |
| | |
| | | /// - nib: Nib file used to create the reusable view. |
| | | /// - kind: the kind of supplementary view to retrieve. This value is defined by the layout object. |
| | | /// - name: UICollectionReusableView type. |
| | | func register<T: UICollectionReusableView>(nib: UINib?, forSupplementaryViewOfKind kind: String, |
| | | withClass name: T.Type) { |
| | | func register(nib: UINib?, forSupplementaryViewOfKind kind: String, |
| | | withClass name: (some UICollectionReusableView).Type) { |
| | | register(nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: String(describing: name)) |
| | | } |
| | | |
| | |
| | | /// - Parameters: |
| | | /// - name: UICollectionViewCell type. |
| | | /// - bundleClass: Class in which the Bundle instance will be based on. |
| | | func register<T: UICollectionViewCell>(nibWithCellClass name: T.Type, at bundleClass: AnyClass? = nil) { |
| | | func register(nibWithCellClass name: (some UICollectionViewCell).Type, at bundleClass: AnyClass? = nil) { |
| | | let identifier = String(describing: name) |
| | | var bundle: Bundle? |
| | | |
| | |
| | | /// - Returns: UIImage with all corners rounded. |
| | | func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? { |
| | | let maxRadius = min(size.width, size.height) / 2 |
| | | let cornerRadius: CGFloat |
| | | if let radius = radius, radius > 0, radius <= maxRadius { |
| | | cornerRadius = radius |
| | | let cornerRadius: CGFloat = if let radius, radius > 0, radius <= maxRadius { |
| | | radius |
| | | } else { |
| | | cornerRadius = maxRadius |
| | | maxRadius |
| | | } |
| | | |
| | | let actions = { |
| | |
| | | /// - Parameters: |
| | | /// - color: Color of image. |
| | | /// - Returns: UIImage with color. |
| | | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) |
| | | @available(iOS 13.0, macCatalyst 13.1, tvOS 13.0, watchOS 6.0, *) |
| | | func withAlwaysOriginalTintColor(_ color: UIColor) -> UIImage { |
| | | return withTintColor(color, renderingMode: .alwaysOriginal) |
| | | } |
| | |
| | | /// - contentMode: imageView content mode (default is .scaleAspectFit). |
| | | /// - placeHolder: optional placeholder image |
| | | /// - completionHandler: optional completion handler to run when download finishes (default is nil). |
| | | @available(iOS 13.0, macCatalyst 13.1, tvOS 13.0, *) |
| | | func download( |
| | | from url: URL, |
| | | contentMode: UIView.ContentMode = .scaleAspectFit, |
| | | placeholder: UIImage? = nil, |
| | | completionHandler: ((UIImage?) -> Void)? = nil) { |
| | | completionHandler: (@MainActor (UIImage?) -> Void)? = nil) { |
| | | image = placeholder |
| | | self.contentMode = contentMode |
| | | URLSession.shared.dataTask(with: url) { data, response, _ in |
| | | guard |
| | | let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200, |
| | | let mimeType = response?.mimeType, mimeType.hasPrefix("image"), |
| | | let data = data, |
| | | let data, |
| | | let image = UIImage(data: data) else { |
| | | completionHandler?(nil) |
| | | Task { |
| | | await completionHandler?(nil) |
| | | } |
| | | return |
| | | } |
| | | DispatchQueue.main.async { [unowned self] in |
| | |
| | | #if os(iOS) || os(tvOS) |
| | | import UIKit |
| | | |
| | | extension UILayoutPriority: ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral { |
| | | extension UILayoutPriority: Swift.ExpressibleByFloatLiteral, Swift.ExpressibleByIntegerLiteral { |
| | | // MARK: - Initializers |
| | | |
| | | /// SwifterSwift: Initialize `UILayoutPriority` with a float literal. |
| | |
| | | public extension UITableView { |
| | | /// SwifterSwift: Index path of last row in tableView. |
| | | var indexPathForLastRow: IndexPath? { |
| | | guard let lastSection = lastSection else { return nil } |
| | | guard let lastSection else { return nil } |
| | | return indexPathForLastRow(inSection: lastSection) |
| | | } |
| | | |
| | |
| | | /// - Parameters: |
| | | /// - nib: Nib file used to create the header or footer view. |
| | | /// - name: UITableViewHeaderFooterView type. |
| | | func register<T: UITableViewHeaderFooterView>(nib: UINib?, withHeaderFooterViewClass name: T.Type) { |
| | | func register(nib: UINib?, withHeaderFooterViewClass name: (some UITableViewHeaderFooterView).Type) { |
| | | register(nib, forHeaderFooterViewReuseIdentifier: String(describing: name)) |
| | | } |
| | | |
| | |
| | | /// - Parameters: |
| | | /// - nib: Nib file used to create the tableView cell. |
| | | /// - name: UITableViewCell type. |
| | | func register<T: UITableViewCell>(nib: UINib?, withCellClass name: T.Type) { |
| | | func register(nib: UINib?, withCellClass name: (some UITableViewCell).Type) { |
| | | register(nib, forCellReuseIdentifier: String(describing: name)) |
| | | } |
| | | |
| | |
| | | /// - Parameters: |
| | | /// - name: UITableViewCell type. |
| | | /// - bundleClass: Class in which the Bundle instance will be based on. |
| | | func register<T: UITableViewCell>(nibWithCellClass name: T.Type, at bundleClass: AnyClass? = nil) { |
| | | func register(nibWithCellClass name: (some UITableViewCell).Type, at bundleClass: AnyClass? = nil) { |
| | | let identifier = String(describing: name) |
| | | var bundle: Bundle? |
| | | |
| | |
| | | }) |
| | | alertController.addAction(action) |
| | | // Check which button to highlight |
| | | if let highlightedButtonIndex = highlightedButtonIndex, index == highlightedButtonIndex { |
| | | if let highlightedButtonIndex, index == highlightedButtonIndex { |
| | | alertController.preferredAction = action |
| | | } |
| | | } |
| | |
| | | completion: (() -> Void)? = nil) { |
| | | popoverContent.modalPresentationStyle = .popover |
| | | |
| | | if let size = size { |
| | | if let size { |
| | | popoverContent.preferredContentSize = size |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /// SwifterSwift: Add gradient directions |
| | | struct GradientDirection { |
| | | struct GradientDirection: Sendable { |
| | | public static let topToBottom = GradientDirection(startPoint: CGPoint(x: 0.5, y: 0.0), |
| | | endPoint: CGPoint(x: 0.5, y: 1.0)) |
| | | public static let bottomToTop = GradientDirection(startPoint: CGPoint(x: 0.5, y: 1.0), |
| | |
| | | |
| | | /// SwifterSwift: Remove all gesture recognizers from view. |
| | | func removeGestureRecognizers() { |
| | | gestureRecognizers?.forEach(removeGestureRecognizer) |
| | | try? gestureRecognizers?.forEach(removeGestureRecognizer) |
| | | } |
| | | |
| | | /// SwifterSwift: Attaches gesture recognizers to the view. Attaching gesture recognizers to a view defines the |
| | |
| | | animationType: ShakeAnimationType = .easeOut, |
| | | completion: (() -> Void)? = nil) { |
| | | CATransaction.begin() |
| | | let animation: CAKeyframeAnimation |
| | | switch direction { |
| | | let animation = switch direction { |
| | | case .horizontal: |
| | | animation = CAKeyframeAnimation(keyPath: "transform.translation.x") |
| | | CAKeyframeAnimation(keyPath: "transform.translation.x") |
| | | case .vertical: |
| | | animation = CAKeyframeAnimation(keyPath: "transform.translation.y") |
| | | CAKeyframeAnimation(keyPath: "transform.translation.y") |
| | | } |
| | | switch animationType { |
| | | case .linear: |
| | |
| | | func fillToSuperview() { |
| | | // https://videos.letsbuildthatapp.com/ |
| | | translatesAutoresizingMaskIntoConstraints = false |
| | | if let superview = superview { |
| | | if let superview { |
| | | let left = leftAnchor.constraint(equalTo: superview.leftAnchor) |
| | | let right = rightAnchor.constraint(equalTo: superview.rightAnchor) |
| | | let top = topAnchor.constraint(equalTo: superview.topAnchor) |
| | |
| | | |
| | | var anchors = [NSLayoutConstraint]() |
| | | |
| | | if let top = top { |
| | | if let top { |
| | | anchors.append(topAnchor.constraint(equalTo: top, constant: topConstant)) |
| | | } |
| | | |
| | | if let left = left { |
| | | if let left { |
| | | anchors.append(leftAnchor.constraint(equalTo: left, constant: leftConstant)) |
| | | } |
| | | |
| | | if let bottom = bottom { |
| | | if let bottom { |
| | | anchors.append(bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant)) |
| | | } |
| | | |
| | | if let right = right { |
| | | if let right { |
| | | anchors.append(rightAnchor.constraint(equalTo: right, constant: -rightConstant)) |
| | | } |
| | | |
| | |
| | | func loadURLString(_ urlString: String, timeout: TimeInterval? = nil) -> WKNavigation? { |
| | | guard let url = URL(string: urlString) else { return nil } |
| | | var request = URLRequest(url: url) |
| | | if let timeout = timeout { |
| | | if let timeout { |
| | | request.timeoutInterval = timeout |
| | | } |
| | | return load(request) |
| | |
| | | |
| | | ## 重要提示1:提issue前,请先对照Demo、常见问题自查!Demo正常说明你可以升级下新版试试。 |
| | | |
| | | ## 重要提示2:3.8.7版本修复了iOS18下无照片的问题 |
| | | ## 重要提示2:3.8.8版本修复了iOS18下无照片和openURL失效的问题 |
| | | 关于iOS14模拟器的问题 |
| | | PHAuthorizationStatusLimited授权模式下,iOS14模拟器有bug,未授权照片无法显示,真机正常,暂可忽略:https://github.com/banchichen/TZImagePickerController/issues/1347 |
| | | |
| | |
| | | |
| | | ## 六. Release Notes 最近更新 |
| | | |
| | | **3.8.7 支持iOS18** [#1686](https://github.com/banchichen/TZImagePickerController/issues/1686) |
| | | **3.8.8 支持iOS18,修复openURL的失效问题** [#1686](https://github.com/banchichen/TZImagePickerController/issues/1686) |
| | | **3.8.5 新增隐私清单文件** [#1675](https://github.com/banchichen/TZImagePickerController/pull/1675) |
| | | **3.8.4 支持使用不带定位代码的版本** [#1606](https://github.com/banchichen/TZImagePickerController/pull/1606) |
| | | 3.8.1 iOS14下可添加访问更多照片,详见PR内的评论 [#1526](https://github.com/banchichen/TZImagePickerController/pull/1526) |
| | |
| | | // |
| | | // Created by 谭真 on 15/12/24. |
| | | // Copyright © 2015年 谭真. All rights reserved. |
| | | // version 3.8.7 - 2024.08.14 |
| | | // version 3.8.8 - 2024.10.27 |
| | | // 更多信息,请前往项目的github地址:https://github.com/banchichen/TZImagePickerController |
| | | |
| | | /* |
| | |
| | | // |
| | | // Created by 谭真 on 15/12/24. |
| | | // Copyright © 2015年 谭真. All rights reserved. |
| | | // version 3.8.7 - 2024.08.14 |
| | | // version 3.8.8 - 2024.10.27 |
| | | // 更多信息,请前往项目的github地址:https://github.com/banchichen/TZImagePickerController |
| | | |
| | | #import "TZImagePickerController.h" |
| | |
| | | } |
| | | |
| | | - (void)settingBtnClick { |
| | | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; |
| | | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; |
| | | } |
| | | |
| | | - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { |
| | |
| | | UIAlertAction *cancelAct = [UIAlertAction actionWithTitle:[NSBundle tz_localizedStringForKey:@"Cancel"] style:UIAlertActionStyleCancel handler:nil]; |
| | | [alertController addAction:cancelAct]; |
| | | UIAlertAction *settingAct = [UIAlertAction actionWithTitle:[NSBundle tz_localizedStringForKey:@"Setting"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { |
| | | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; |
| | | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; |
| | | }]; |
| | | [alertController addAction:settingAct]; |
| | | [self.navigationController presentViewController:alertController animated:YES completion:nil]; |
| | |
| | | } |
| | | |
| | | - (void)openSettingsApplication { |
| | | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; |
| | | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; |
| | | } |
| | | |
| | | - (void)addMorePhoto { |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>FMWK</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>5.9.1</string> |
| | | <string>5.10.2</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>BNDL</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>5.9.1</string> |
| | | <string>5.10.2</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>FMWK</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>7.1.1</string> |
| | | <string>8.0.0</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO |
| | | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift |
| | | ENABLE_USER_SCRIPT_SANDBOXING = NO |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" |
| | | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 |
| | | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift $(SDKROOT)/usr/lib/swift |
| | | OTHER_LDFLAGS = $(inherited) -l"swiftCoreGraphics" -framework "CoreGraphics" -framework "Foundation" -framework "QuartzCore" -framework "UIKit" |
| | | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift |
| | | OTHER_LDFLAGS = $(inherited) -framework "Combine" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "UIKit" |
| | | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS |
| | | PODS_BUILD_DIR = ${BUILD_DIR} |
| | | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) |
| | |
| | | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO |
| | | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift |
| | | ENABLE_USER_SCRIPT_SANDBOXING = NO |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" |
| | | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 |
| | | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift $(SDKROOT)/usr/lib/swift |
| | | OTHER_LDFLAGS = $(inherited) -l"swiftCoreGraphics" -framework "CoreGraphics" -framework "Foundation" -framework "QuartzCore" -framework "UIKit" |
| | | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift |
| | | OTHER_LDFLAGS = $(inherited) -framework "Combine" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "UIKit" |
| | | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS |
| | | PODS_BUILD_DIR = ${BUILD_DIR} |
| | | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>BNDL</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>7.1.1</string> |
| | | <string>8.0.0</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO |
| | | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JQTools |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger" |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger" |
| | | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 |
| | | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift |
| | | OTHER_LDFLAGS = $(inherited) -framework "AVFoundation" -framework "CoreGraphics" -framework "CoreImage" -framework "CoreMedia" -framework "CoreServices" -framework "EmptyDataSet_Swift" -framework "Foundation" -framework "HandyJSON" -framework "IQKeyboardManager" -framework "IQKeyboardManagerSwift" -framework "ImageIO" -framework "MJRefresh" -framework "ObjectMapper" -framework "Photos" -framework "PhotosUI" -framework "QMUIKit" -framework "QuartzCore" -framework "RxCocoa" -framework "RxDataSources" -framework "RxSwift" -framework "SDWebImage" -framework "SVProgressHUD" -framework "SnapKit" -framework "TZImagePickerController" -framework "UIKit" -framework "UserDefaultsStore" -framework "VTMagic" -framework "XCGLogger" |
| | |
| | | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO |
| | | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JQTools |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger" |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger" |
| | | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 |
| | | LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift |
| | | OTHER_LDFLAGS = $(inherited) -framework "AVFoundation" -framework "CoreGraphics" -framework "CoreImage" -framework "CoreMedia" -framework "CoreServices" -framework "EmptyDataSet_Swift" -framework "Foundation" -framework "HandyJSON" -framework "IQKeyboardManager" -framework "IQKeyboardManagerSwift" -framework "ImageIO" -framework "MJRefresh" -framework "ObjectMapper" -framework "Photos" -framework "PhotosUI" -framework "QMUIKit" -framework "QuartzCore" -framework "RxCocoa" -framework "RxDataSources" -framework "RxSwift" -framework "SDWebImage" -framework "SVProgressHUD" -framework "SnapKit" -framework "TZImagePickerController" -framework "UIKit" -framework "UserDefaultsStore" -framework "VTMagic" -framework "XCGLogger" |
| | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| | | |
| | | |
| | | ## IQKeyboardCore |
| | | |
| | | MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | |
| | | |
| | | ## IQKeyboardManager |
| | | |
| | | MIT License |
| | |
| | | MIT License |
| | | |
| | | Copyright (c) 2013-2023 Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | |
| | | |
| | | ## IQKeyboardNotification |
| | | |
| | | MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | |
| | | |
| | | ## IQKeyboardReturnManager |
| | | |
| | | MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | |
| | | |
| | | ## IQKeyboardToolbar |
| | | |
| | | MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | |
| | | |
| | | ## IQKeyboardToolbarManager |
| | | |
| | | MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | |
| | | |
| | | ## IQTextInputViewNotification |
| | | |
| | | Copyright (c) 2024 hackiftekhar <ideviftekhar@gmail.com> |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in |
| | | all copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | THE SOFTWARE. |
| | | |
| | | |
| | | ## IQTextView |
| | | |
| | | MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | |
| | | ## RxCocoa |
| | | |
| | | **The MIT License** |
| | | **Copyright © 2015 Krunoslav Zaher, Shai Mishali** |
| | | **Copyright © 2015 Shai Mishali, Krunoslav Zaher** |
| | | **All rights reserved.** |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| | |
| | | ## RxRelay |
| | | |
| | | **The MIT License** |
| | | **Copyright © 2015 Krunoslav Zaher, Shai Mishali** |
| | | **Copyright © 2015 Shai Mishali, Krunoslav Zaher** |
| | | **All rights reserved.** |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| | |
| | | ## RxSwift |
| | | |
| | | **The MIT License** |
| | | **Copyright © 2015 Krunoslav Zaher, Shai Mishali** |
| | | **Copyright © 2015 Shai Mishali, Krunoslav Zaher** |
| | | **All rights reserved.** |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| | |
| | | <key>FooterText</key> |
| | | <string>MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | </string> |
| | | <key>License</key> |
| | | <string>MIT</string> |
| | | <key>Title</key> |
| | | <string>IQKeyboardCore</string> |
| | | <key>Type</key> |
| | | <string>PSGroupSpecifier</string> |
| | | </dict> |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>MIT License |
| | | |
| | | Copyright (c) 2013-2023 Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | |
| | | <string>MIT</string> |
| | | <key>Title</key> |
| | | <string>IQKeyboardManagerSwift</string> |
| | | <key>Type</key> |
| | | <string>PSGroupSpecifier</string> |
| | | </dict> |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | </string> |
| | | <key>License</key> |
| | | <string>MIT</string> |
| | | <key>Title</key> |
| | | <string>IQKeyboardNotification</string> |
| | | <key>Type</key> |
| | | <string>PSGroupSpecifier</string> |
| | | </dict> |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | </string> |
| | | <key>License</key> |
| | | <string>MIT</string> |
| | | <key>Title</key> |
| | | <string>IQKeyboardReturnManager</string> |
| | | <key>Type</key> |
| | | <string>PSGroupSpecifier</string> |
| | | </dict> |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | </string> |
| | | <key>License</key> |
| | | <string>MIT</string> |
| | | <key>Title</key> |
| | | <string>IQKeyboardToolbar</string> |
| | | <key>Type</key> |
| | | <string>PSGroupSpecifier</string> |
| | | </dict> |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | </string> |
| | | <key>License</key> |
| | | <string>MIT</string> |
| | | <key>Title</key> |
| | | <string>IQKeyboardToolbarManager</string> |
| | | <key>Type</key> |
| | | <string>PSGroupSpecifier</string> |
| | | </dict> |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>Copyright (c) 2024 hackiftekhar <ideviftekhar@gmail.com> |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in |
| | | all copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| | | THE SOFTWARE. |
| | | </string> |
| | | <key>License</key> |
| | | <string>MIT</string> |
| | | <key>Title</key> |
| | | <string>IQTextInputViewNotification</string> |
| | | <key>Type</key> |
| | | <string>PSGroupSpecifier</string> |
| | | </dict> |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>MIT License |
| | | |
| | | Copyright (c) 2024 Mohd Iftekhar Qurashi |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy |
| | | of this software and associated documentation files (the "Software"), to deal |
| | | in the Software without restriction, including without limitation the rights |
| | | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| | | copies of the Software, and to permit persons to whom the Software is |
| | | furnished to do so, subject to the following conditions: |
| | | |
| | | The above copyright notice and this permission notice shall be included in all |
| | | copies or substantial portions of the Software. |
| | | |
| | | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| | | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| | | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| | | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| | | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| | | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| | | SOFTWARE. |
| | | </string> |
| | | <key>License</key> |
| | | <string>MIT</string> |
| | | <key>Title</key> |
| | | <string>IQTextView</string> |
| | | <key>Type</key> |
| | | <string>PSGroupSpecifier</string> |
| | | </dict> |
| | |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>**The MIT License** |
| | | **Copyright © 2015 Krunoslav Zaher, Shai Mishali** |
| | | **Copyright © 2015 Shai Mishali, Krunoslav Zaher** |
| | | **All rights reserved.** |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| | |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>**The MIT License** |
| | | **Copyright © 2015 Krunoslav Zaher, Shai Mishali** |
| | | **Copyright © 2015 Shai Mishali, Krunoslav Zaher** |
| | | **All rights reserved.** |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| | |
| | | <dict> |
| | | <key>FooterText</key> |
| | | <string>**The MIT License** |
| | | **Copyright © 2015 Krunoslav Zaher, Shai Mishali** |
| | | **Copyright © 2015 Shai Mishali, Krunoslav Zaher** |
| | | **All rights reserved.** |
| | | |
| | | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| | |
| | | ${BUILT_PRODUCTS_DIR}/EmptyDataSet-Swift/EmptyDataSet_Swift.framework |
| | | ${BUILT_PRODUCTS_DIR}/FFPage/FFPage.framework |
| | | ${BUILT_PRODUCTS_DIR}/HandyJSON/HandyJSON.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardManager/IQKeyboardManager.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework |
| | | ${BUILT_PRODUCTS_DIR}/JQTools/JQTools.framework |
| | | ${BUILT_PRODUCTS_DIR}/Lantern/Lantern.framework |
| | | ${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework |
| | |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EmptyDataSet_Swift.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FFPage.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HandyJSON.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardCore.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManager.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardNotification.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardReturnManager.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardToolbar.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardToolbarManager.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQTextInputViewNotification.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQTextView.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JQTools.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Lantern.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MJRefresh.framework |
| | |
| | | ${BUILT_PRODUCTS_DIR}/EmptyDataSet-Swift/EmptyDataSet_Swift.framework |
| | | ${BUILT_PRODUCTS_DIR}/FFPage/FFPage.framework |
| | | ${BUILT_PRODUCTS_DIR}/HandyJSON/HandyJSON.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardManager/IQKeyboardManager.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework |
| | | ${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework |
| | | ${BUILT_PRODUCTS_DIR}/JQTools/JQTools.framework |
| | | ${BUILT_PRODUCTS_DIR}/Lantern/Lantern.framework |
| | | ${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework |
| | |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EmptyDataSet_Swift.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FFPage.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HandyJSON.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardCore.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManager.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManagerSwift.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardNotification.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardReturnManager.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardToolbar.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardToolbarManager.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQTextInputViewNotification.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQTextView.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JQTools.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Lantern.framework |
| | | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MJRefresh.framework |
| | |
| | | install_framework "${BUILT_PRODUCTS_DIR}/EmptyDataSet-Swift/EmptyDataSet_Swift.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/FFPage/FFPage.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/HandyJSON/HandyJSON.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManager/IQKeyboardManager.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/JQTools/JQTools.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/Lantern/Lantern.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework" |
| | |
| | | install_framework "${BUILT_PRODUCTS_DIR}/EmptyDataSet-Swift/EmptyDataSet_Swift.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/FFPage/FFPage.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/HandyJSON/HandyJSON.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManager/IQKeyboardManager.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/JQTools/JQTools.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/Lantern/Lantern.framework" |
| | | install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework" |
| | |
| | | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES |
| | | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/APNGKit" "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/AliyunOSSiOS" "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift" "${PODS_CONFIGURATION_BUILD_DIR}/Delegate" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/FFPage" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/JQTools" "${PODS_CONFIGURATION_BUILD_DIR}/Lantern" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SPPageMenu" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwifterSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyStoreKit" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger" "${PODS_ROOT}/WechatOpenSDK-XCFramework" |
| | | ENABLE_USER_SCRIPT_SANDBOXING = NO |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/APNGKit" "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/AliyunOSSiOS" "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift" "${PODS_CONFIGURATION_BUILD_DIR}/Delegate" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/FFPage" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_CONFIGURATION_BUILD_DIR}/JQTools" "${PODS_CONFIGURATION_BUILD_DIR}/Lantern" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SPPageMenu" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwifterSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyStoreKit" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger" "${PODS_ROOT}/WechatOpenSDK-XCFramework" |
| | | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 |
| | | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/APNGKit/APNGKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/AliyunOSSiOS/AliyunOSSiOS.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift/CryptoSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Delegate/Delegate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator/Differentiator.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift/EmptyDataSet_Swift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/FFPage/FFPage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON/HandyJSON.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager/IQKeyboardManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JQTools/JQTools.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Lantern/Lantern.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging/ObjcExceptionBridging.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit/QMUIKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa/RxCocoa.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources/RxDataSources.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay/RxRelay.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift/RxSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SPPageMenu/SPPageMenu.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwifterSwift/SwifterSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyStoreKit/SwiftyStoreKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController/TZImagePickerController.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore/UserDefaultsStore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic/VTMagic.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger/XCGLogger.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework/Headers" |
| | | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/APNGKit/APNGKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/AliyunOSSiOS/AliyunOSSiOS.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift/CryptoSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Delegate/Delegate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator/Differentiator.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift/EmptyDataSet_Swift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/FFPage/FFPage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON/HandyJSON.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager/IQKeyboardManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JQTools/JQTools.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Lantern/Lantern.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging/ObjcExceptionBridging.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit/QMUIKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa/RxCocoa.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources/RxDataSources.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay/RxRelay.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift/RxSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SPPageMenu/SPPageMenu.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwifterSwift/SwifterSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyStoreKit/SwiftyStoreKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController/TZImagePickerController.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore/UserDefaultsStore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic/VTMagic.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger/XCGLogger.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework/Headers" |
| | | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' |
| | | LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift $(SDKROOT)/usr/lib/swift |
| | | OTHER_LDFLAGS = $(inherited) -l"WechatOpenSDK" -l"c++" -l"resolv" -l"sqlite3.0" -l"swiftCoreGraphics" -l"z" -framework "APNGKit" -framework "AVFoundation" -framework "Alamofire" -framework "AliyunOSSiOS" -framework "CFNetwork" -framework "CoreGraphics" -framework "CoreImage" -framework "CoreMedia" -framework "CoreServices" -framework "CoreTelephony" -framework "CryptoSwift" -framework "Delegate" -framework "Differentiator" -framework "EmptyDataSet_Swift" -framework "FFPage" -framework "Foundation" -framework "HandyJSON" -framework "IQKeyboardManager" -framework "IQKeyboardManagerSwift" -framework "ImageIO" -framework "JQTools" -framework "Lantern" -framework "MJRefresh" -framework "ObjcExceptionBridging" -framework "ObjectMapper" -framework "Photos" -framework "PhotosUI" -framework "QMUIKit" -framework "QuartzCore" -framework "RxCocoa" -framework "RxDataSources" -framework "RxRelay" -framework "RxSwift" -framework "SDWebImage" -framework "SPPageMenu" -framework "SVProgressHUD" -framework "Security" -framework "SnapKit" -framework "SwifterSwift" -framework "SwiftyStoreKit" -framework "SystemConfiguration" -framework "TZImagePickerController" -framework "UIKit" -framework "UserDefaultsStore" -framework "VTMagic" -framework "WebKit" -framework "XCGLogger" |
| | | OTHER_LDFLAGS = $(inherited) -l"WechatOpenSDK" -l"c++" -l"resolv" -l"sqlite3.0" -l"swiftCoreGraphics" -l"z" -framework "APNGKit" -framework "AVFoundation" -framework "Alamofire" -framework "AliyunOSSiOS" -framework "CFNetwork" -framework "Combine" -framework "CoreGraphics" -framework "CoreImage" -framework "CoreMedia" -framework "CoreServices" -framework "CoreTelephony" -framework "CryptoSwift" -framework "Delegate" -framework "Differentiator" -framework "EmptyDataSet_Swift" -framework "FFPage" -framework "Foundation" -framework "HandyJSON" -framework "IQKeyboardCore" -framework "IQKeyboardManager" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "ImageIO" -framework "JQTools" -framework "Lantern" -framework "MJRefresh" -framework "ObjcExceptionBridging" -framework "ObjectMapper" -framework "Photos" -framework "PhotosUI" -framework "QMUIKit" -framework "QuartzCore" -framework "RxCocoa" -framework "RxDataSources" -framework "RxRelay" -framework "RxSwift" -framework "SDWebImage" -framework "SPPageMenu" -framework "SVProgressHUD" -framework "Security" -framework "SnapKit" -framework "SwifterSwift" -framework "SwiftyStoreKit" -framework "SystemConfiguration" -framework "TZImagePickerController" -framework "UIKit" -framework "UserDefaultsStore" -framework "VTMagic" -framework "WebKit" -framework "XCGLogger" |
| | | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS |
| | | PODS_BUILD_DIR = ${BUILD_DIR} |
| | | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) |
| | |
| | | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES |
| | | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/APNGKit" "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/AliyunOSSiOS" "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift" "${PODS_CONFIGURATION_BUILD_DIR}/Delegate" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/FFPage" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/JQTools" "${PODS_CONFIGURATION_BUILD_DIR}/Lantern" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SPPageMenu" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwifterSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyStoreKit" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger" "${PODS_ROOT}/WechatOpenSDK-XCFramework" |
| | | ENABLE_USER_SCRIPT_SANDBOXING = NO |
| | | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/APNGKit" "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/AliyunOSSiOS" "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift" "${PODS_CONFIGURATION_BUILD_DIR}/Delegate" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift" "${PODS_CONFIGURATION_BUILD_DIR}/FFPage" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_CONFIGURATION_BUILD_DIR}/JQTools" "${PODS_CONFIGURATION_BUILD_DIR}/Lantern" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/SPPageMenu" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit" "${PODS_CONFIGURATION_BUILD_DIR}/SwifterSwift" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyStoreKit" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger" "${PODS_ROOT}/WechatOpenSDK-XCFramework" |
| | | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 |
| | | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/APNGKit/APNGKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/AliyunOSSiOS/AliyunOSSiOS.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift/CryptoSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Delegate/Delegate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator/Differentiator.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift/EmptyDataSet_Swift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/FFPage/FFPage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON/HandyJSON.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager/IQKeyboardManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JQTools/JQTools.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Lantern/Lantern.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging/ObjcExceptionBridging.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit/QMUIKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa/RxCocoa.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources/RxDataSources.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay/RxRelay.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift/RxSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SPPageMenu/SPPageMenu.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwifterSwift/SwifterSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyStoreKit/SwiftyStoreKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController/TZImagePickerController.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore/UserDefaultsStore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic/VTMagic.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger/XCGLogger.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework/Headers" |
| | | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/APNGKit/APNGKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/AliyunOSSiOS/AliyunOSSiOS.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift/CryptoSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Delegate/Delegate.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Differentiator/Differentiator.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/EmptyDataSet-Swift/EmptyDataSet_Swift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/FFPage/FFPage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/HandyJSON/HandyJSON.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManager/IQKeyboardManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/JQTools/JQTools.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Lantern/Lantern.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjcExceptionBridging/ObjcExceptionBridging.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ObjectMapper/ObjectMapper.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/QMUIKit/QMUIKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxCocoa/RxCocoa.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxDataSources/RxDataSources.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxRelay/RxRelay.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/RxSwift/RxSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SPPageMenu/SPPageMenu.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SnapKit/SnapKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwifterSwift/SwifterSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftyStoreKit/SwiftyStoreKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/TZImagePickerController/TZImagePickerController.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/UserDefaultsStore/UserDefaultsStore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/VTMagic/VTMagic.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/XCGLogger/XCGLogger.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework/Headers" |
| | | LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' |
| | | LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift $(SDKROOT)/usr/lib/swift |
| | | OTHER_LDFLAGS = $(inherited) -l"WechatOpenSDK" -l"c++" -l"resolv" -l"sqlite3.0" -l"swiftCoreGraphics" -l"z" -framework "APNGKit" -framework "AVFoundation" -framework "Alamofire" -framework "AliyunOSSiOS" -framework "CFNetwork" -framework "CoreGraphics" -framework "CoreImage" -framework "CoreMedia" -framework "CoreServices" -framework "CoreTelephony" -framework "CryptoSwift" -framework "Delegate" -framework "Differentiator" -framework "EmptyDataSet_Swift" -framework "FFPage" -framework "Foundation" -framework "HandyJSON" -framework "IQKeyboardManager" -framework "IQKeyboardManagerSwift" -framework "ImageIO" -framework "JQTools" -framework "Lantern" -framework "MJRefresh" -framework "ObjcExceptionBridging" -framework "ObjectMapper" -framework "Photos" -framework "PhotosUI" -framework "QMUIKit" -framework "QuartzCore" -framework "RxCocoa" -framework "RxDataSources" -framework "RxRelay" -framework "RxSwift" -framework "SDWebImage" -framework "SPPageMenu" -framework "SVProgressHUD" -framework "Security" -framework "SnapKit" -framework "SwifterSwift" -framework "SwiftyStoreKit" -framework "SystemConfiguration" -framework "TZImagePickerController" -framework "UIKit" -framework "UserDefaultsStore" -framework "VTMagic" -framework "WebKit" -framework "XCGLogger" |
| | | OTHER_LDFLAGS = $(inherited) -l"WechatOpenSDK" -l"c++" -l"resolv" -l"sqlite3.0" -l"swiftCoreGraphics" -l"z" -framework "APNGKit" -framework "AVFoundation" -framework "Alamofire" -framework "AliyunOSSiOS" -framework "CFNetwork" -framework "Combine" -framework "CoreGraphics" -framework "CoreImage" -framework "CoreMedia" -framework "CoreServices" -framework "CoreTelephony" -framework "CryptoSwift" -framework "Delegate" -framework "Differentiator" -framework "EmptyDataSet_Swift" -framework "FFPage" -framework "Foundation" -framework "HandyJSON" -framework "IQKeyboardCore" -framework "IQKeyboardManager" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "ImageIO" -framework "JQTools" -framework "Lantern" -framework "MJRefresh" -framework "ObjcExceptionBridging" -framework "ObjectMapper" -framework "Photos" -framework "PhotosUI" -framework "QMUIKit" -framework "QuartzCore" -framework "RxCocoa" -framework "RxDataSources" -framework "RxRelay" -framework "RxSwift" -framework "SDWebImage" -framework "SPPageMenu" -framework "SVProgressHUD" -framework "Security" -framework "SnapKit" -framework "SwifterSwift" -framework "SwiftyStoreKit" -framework "SystemConfiguration" -framework "TZImagePickerController" -framework "UIKit" -framework "UserDefaultsStore" -framework "VTMagic" -framework "WebKit" -framework "XCGLogger" |
| | | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS |
| | | PODS_BUILD_DIR = ${BUILD_DIR} |
| | | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>FMWK</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>6.7.1</string> |
| | | <string>6.9.0</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>FMWK</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>6.7.1</string> |
| | | <string>6.9.0</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>FMWK</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>6.7.1</string> |
| | | <string>6.9.0</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>BNDL</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>5.19.6</string> |
| | | <string>5.20.0</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>FMWK</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>5.19.6</string> |
| | | <string>5.20.0</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>BNDL</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>6.2.0</string> |
| | | <string>7.0.0</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>FMWK</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>6.2.0</string> |
| | | <string>7.0.0</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | <key>CFBundlePackageType</key> |
| | | <string>FMWK</string> |
| | | <key>CFBundleShortVersionString</key> |
| | | <string>3.8.7</string> |
| | | <string>3.8.8</string> |
| | | <key>CFBundleSignature</key> |
| | | <string>????</string> |
| | | <key>CFBundleVersion</key> |
| | |
| | | CODE_SIGN_IDENTITY = "Apple Development"; |
| | | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; |
| | | CODE_SIGN_STYLE = Manual; |
| | | CURRENT_PROJECT_VERSION = 12; |
| | | CURRENT_PROJECT_VERSION = 15; |
| | | DEVELOPMENT_TEAM = ""; |
| | | "DEVELOPMENT_TEAM[sdk=iphoneos*]" = M9T3KVL537; |
| | | ENABLE_USER_SCRIPT_SANDBOXING = NO; |
| | |
| | | "$(inherited)", |
| | | "@executable_path/Frameworks", |
| | | ); |
| | | MARKETING_VERSION = 1.0.3; |
| | | MARKETING_VERSION = 1.0.4; |
| | | OTHER_LDFLAGS = ( |
| | | "$(inherited)", |
| | | "-ObjC", |
| | |
| | | CODE_SIGN_IDENTITY = "Apple Development"; |
| | | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; |
| | | CODE_SIGN_STYLE = Manual; |
| | | CURRENT_PROJECT_VERSION = 12; |
| | | CURRENT_PROJECT_VERSION = 15; |
| | | DEVELOPMENT_TEAM = ""; |
| | | "DEVELOPMENT_TEAM[sdk=iphoneos*]" = M9T3KVL537; |
| | | ENABLE_USER_SCRIPT_SANDBOXING = NO; |
| | |
| | | "$(inherited)", |
| | | "@executable_path/Frameworks", |
| | | ); |
| | | MARKETING_VERSION = 1.0.3; |
| | | MARKETING_VERSION = 1.0.4; |
| | | OTHER_LDFLAGS = ( |
| | | "$(inherited)", |
| | | "-ObjC", |
| | |
| | | self.cell0!.setContent(title: "导师简介", content: m.tutorIntroduction) |
| | | self.cell1!.setItems(m.list) |
| | | self.tableView?.reloadData() |
| | | self.headerView.updateVideoUrl(m.videoUrl,autoPlay: false,placeHoderImageUrl: m.coverUrl.jq_urlEncoded()) |
| | | self.headerView.updateVideoUrl(m.videoUrl,autoPlay: false,placeHoderImageUrl: m.coverUrl.jq_urlEncoded(),delegate: self) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | |
| | |
| | | |
| | | |
| | | } |
| | | |
| | | extension CourseVCTeacherSpecialVC:CLPlayerDelegate{ |
| | | func player(_ player: CLPlayer, playProgressChanged value: CGFloat) { |
| | | AudioPlayer.getSharedInstance().pauseBGM() |
| | | AudioPlayer.getSharedInstance().pauseScene() |
| | | } |
| | | } |
| | |
| | | <rect key="frame" x="0.0" y="173" width="165" height="45"/> |
| | | <subviews> |
| | | <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_play" translatesAutoresizingMaskIntoConstraints="NO" id="KBd-aZ-LFh"> |
| | | <rect key="frame" x="119" y="8" width="29" height="29"/> |
| | | <rect key="frame" x="123" y="8" width="29" height="29"/> |
| | | <constraints> |
| | | <constraint firstAttribute="width" constant="29" id="Wk0-Zg-gK2"/> |
| | | <constraint firstAttribute="height" constant="29" id="ogD-HU-izV"/> |
| | | </constraints> |
| | | </imageView> |
| | | <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="--" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fsN-0y-q8f"> |
| | | <rect key="frame" x="20" y="10" width="94" height="10.666666666666664"/> |
| | | <constraints> |
| | | <constraint firstAttribute="height" constant="10.57" id="mJn-oc-6Gw"/> |
| | | </constraints> |
| | | <fontDescription key="fontDescription" type="system" pointSize="10"/> |
| | | <rect key="frame" x="15" y="5" width="103" height="19.333333333333332"/> |
| | | <fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/> |
| | | <color key="textColor" red="0.94901960784313721" green="0.92549019607843142" blue="0.97254901960784312" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" text="--" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AYK-rI-cj9"> |
| | | <rect key="frame" x="20" y="26" width="6.3333333333333321" height="7.3333333333333357"/> |
| | | <fontDescription key="fontDescription" type="system" pointSize="6"/> |
| | | <rect key="frame" x="15" y="27.333333333333343" width="9" height="11"/> |
| | | <fontDescription key="fontDescription" type="system" pointSize="9"/> |
| | | <color key="textColor" red="0.94901960780000005" green="0.92549019610000005" blue="0.97254901959999995" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="icon_use_small" translatesAutoresizingMaskIntoConstraints="NO" id="3TE-Zp-CLh"> |
| | | <rect key="frame" x="34" y="26.666666666666657" width="6" height="6"/> |
| | | <rect key="frame" x="31.666666666666671" y="30" width="6" height="6"/> |
| | | <constraints> |
| | | <constraint firstAttribute="height" constant="6" id="BJm-m4-3Q1"/> |
| | | <constraint firstAttribute="width" constant="6" id="zT4-1f-9rs"/> |
| | | </constraints> |
| | | </imageView> |
| | | <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZK6-Lk-6Cs"> |
| | | <rect key="frame" x="42" y="26" width="67" height="7.3333333333333357"/> |
| | | <rect key="frame" x="39.666666666666664" y="29.333333333333339" width="73.333333333333343" height="7.3333333333333321"/> |
| | | <fontDescription key="fontDescription" type="system" pointSize="6"/> |
| | | <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/> |
| | | <nil key="highlightedColor"/> |
| | |
| | | <constraints> |
| | | <constraint firstAttribute="height" constant="45" id="10l-1C-Vxq"/> |
| | | <constraint firstItem="KBd-aZ-LFh" firstAttribute="leading" secondItem="fsN-0y-q8f" secondAttribute="trailing" constant="5" id="2Ea-AQ-Tu0"/> |
| | | <constraint firstItem="AYK-rI-cj9" firstAttribute="top" secondItem="fsN-0y-q8f" secondAttribute="bottom" constant="5.5" id="DEn-H7-L71"/> |
| | | <constraint firstItem="AYK-rI-cj9" firstAttribute="top" secondItem="fsN-0y-q8f" secondAttribute="bottom" constant="3" id="DEn-H7-L71"/> |
| | | <constraint firstItem="3TE-Zp-CLh" firstAttribute="leading" secondItem="AYK-rI-cj9" secondAttribute="trailing" constant="7.5" id="Dgm-MY-Int"/> |
| | | <constraint firstItem="ZK6-Lk-6Cs" firstAttribute="leading" secondItem="3TE-Zp-CLh" secondAttribute="trailing" constant="2" id="Ic6-HW-ORl"/> |
| | | <constraint firstItem="AYK-rI-cj9" firstAttribute="leading" secondItem="fsN-0y-q8f" secondAttribute="leading" id="M3h-qb-5fd"/> |
| | | <constraint firstItem="fsN-0y-q8f" firstAttribute="top" secondItem="Emq-gn-QOX" secondAttribute="top" constant="10" id="PfC-RB-Ljg"/> |
| | | <constraint firstItem="fsN-0y-q8f" firstAttribute="top" secondItem="Emq-gn-QOX" secondAttribute="top" constant="5" id="PfC-RB-Ljg"/> |
| | | <constraint firstItem="KBd-aZ-LFh" firstAttribute="centerY" secondItem="Emq-gn-QOX" secondAttribute="centerY" id="Vtg-Dw-LYO"/> |
| | | <constraint firstItem="KBd-aZ-LFh" firstAttribute="leading" secondItem="ZK6-Lk-6Cs" secondAttribute="trailing" constant="10" id="YoN-ge-YLi"/> |
| | | <constraint firstItem="3TE-Zp-CLh" firstAttribute="centerY" secondItem="AYK-rI-cj9" secondAttribute="centerY" id="Zcl-sX-hPV"/> |
| | | <constraint firstItem="fsN-0y-q8f" firstAttribute="leading" secondItem="Emq-gn-QOX" secondAttribute="leading" constant="20" id="aKT-wu-4QP"/> |
| | | <constraint firstAttribute="trailing" secondItem="KBd-aZ-LFh" secondAttribute="trailing" constant="17" id="sog-4H-upG"/> |
| | | <constraint firstItem="fsN-0y-q8f" firstAttribute="leading" secondItem="Emq-gn-QOX" secondAttribute="leading" constant="15" id="aKT-wu-4QP"/> |
| | | <constraint firstAttribute="trailing" secondItem="KBd-aZ-LFh" secondAttribute="trailing" constant="13" id="sog-4H-upG"/> |
| | | <constraint firstItem="ZK6-Lk-6Cs" firstAttribute="centerY" secondItem="AYK-rI-cj9" secondAttribute="centerY" id="w1d-x5-sLe"/> |
| | | </constraints> |
| | | </view> |
| | |
| | | @IBOutlet weak var view_shadow: UIView! |
| | | @IBOutlet weak var label_title: UILabel! |
| | | @IBOutlet weak var label_subTitle: UILabel! |
| | | |
| | | private var gradientLayer:CAGradientLayer? |
| | | |
| | | var meditationModel:MeditationModel? |
| | | private var showType:DisplayType! |
| | | |
| | |
| | | |
| | | override func layoutSubviews() { |
| | | super.layoutSubviews() |
| | | view_shadow.jq_gradientColor(colorArr: [UIColor.black.withAlphaComponent(0.3).cgColor,UIColor.clear.cgColor], cornerRadius: 0, startPoint: CGPoint(x: 1, y: 1), endPoint: CGPoint(x: 1, y: 0), bounds: CGRect(x: 0, y: 0, width: JQ_ScreenW, height: 100), locations: nil) |
| | | |
| | | if gradientLayer == nil{ |
| | | gradientLayer = view_shadow.jq_gradientColor(colorArr: [UIColor.black.withAlphaComponent(0.3).cgColor,UIColor.clear.cgColor], cornerRadius: 0, startPoint: CGPoint(x: 1, y: 1), endPoint: CGPoint(x: 1, y: 0), bounds: CGRect(x: 0, y: 0, width: JQ_ScreenW, height: 100), locations: nil) |
| | | } |
| | | } |
| | | |
| | | |
| | |
| | | if !(UserViewModel.getLoginInfo()?.accessToken.isEmpty ?? true){ |
| | | Services.isFirst().subscribe(onNext: {[weak self]data in |
| | | if data.data == true{ |
| | | self?.navigationController?.tabBarController?.selectedIndex = 2 |
| | | var viewModel = UserDefaultSettingViewModel.getSetting() |
| | | viewModel?.userFirstOpenTreeTask = true |
| | | UserDefaultSettingViewModel.saveSetting(viewModel!) |
| | | self?.navigationController?.tabBarController?.selectedIndex = 2 |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | |
| | |
| | | func setModels(_ items:[MeditationModel],showType: DisplayType){ |
| | | self.showType = showType |
| | | self.meditationModels = items |
| | | collectionView.reloadData() |
| | | |
| | | if items.count >= 3 { |
| | | collectionView.scrollToItem(at: IndexPath(row: 1, section: 0), at: .centeredHorizontally, animated: true) |
| | | // if items.count >= 3 { |
| | | |
| | | // } |
| | | |
| | | collectionView.reloadData() |
| | | |
| | | DispatchQueue.main.asyncAfter(delay: 0.1) { |
| | | let centerIndex = ceil(Double(Int(1000 / items.count))) |
| | | self.collectionView.scrollToItem(at: IndexPath(row: Int(centerIndex), section: 0), at: .centeredHorizontally, animated: false) |
| | | } |
| | | } |
| | | } |
| | | |
| | | extension Home_Style_3_TCell:UICollectionViewDelegate & UICollectionViewDataSource{ |
| | | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { |
| | | |
| | | print("--->\(collectionView.visibleCells)") |
| | | |
| | | var showAtCell:UICollectionViewCell? |
| | | if collectionView.visibleCells.count == 2{ |
| | | showAtCell = collectionView.visibleCells.last |
| | | } |
| | | |
| | | if collectionView.visibleCells.count == 3{ |
| | | showAtCell = collectionView.visibleCells[1] |
| | | } |
| | | |
| | | if let cell = showAtCell,let indexPath = collectionView.indexPath(for: cell){ |
| | | let realIndex = indexPath.row % 1000 |
| | | collectionView.scrollToItem(at: IndexPath(row: realIndex, section: 0), at: .centeredHorizontally, animated: false) |
| | | |
| | | } |
| | | |
| | | // collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
| | | return meditationModels.count |
| | | return meditationModels.count * 1000 |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| | | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "_HomeRelaxBannerCCell", for: indexPath) as! HomeRelaxBannerCCell |
| | | cell.setMeditationModel(meditationModels[indexPath.row],showType: showType) |
| | | let realIndex = indexPath.item % meditationModels.count |
| | | cell.setMeditationModel(meditationModels[realIndex],showType: showType) |
| | | return cell |
| | | } |
| | | |
| | | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
| | | let item = meditationModels[indexPath.row] |
| | | |
| | | let index = indexPath.item % meditationModels.count |
| | | |
| | | let item = meditationModels[index] |
| | | |
| | | if (item.chargeType == .payment || item.chargeType == .vipFree || item.id == 0) && (UserViewModel.getLoginInfo()?.token.isEmpty ?? true){ |
| | | sceneDelegate?.checkisLoginState() |
| | |
| | | private var totalInterval:TimeInterval = 0 |
| | | |
| | | private var model:MeditationModel? |
| | | private var gradientLayer:CAGradientLayer? |
| | | |
| | | init(model:MeditationModel) { |
| | | super.init(nibName: nil, bundle: nil) |
| | |
| | | override func viewDidDisappear(_ animated: Bool) { |
| | | super.viewDidDisappear(animated) |
| | | |
| | | if let m = model,timeLook > 0{ |
| | | audioPlayer.lisenMuseTime = 0 |
| | | Services.watchMuse(id: m.id, timeLook: timeLook).subscribe(onNext: {_ in |
| | | NotificationCenter.default.post(name: TreeTaskUpdate_Noti, object: nil) |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | // if let m = model,timeLook > 0{ |
| | | // audioPlayer.lisenMuseTime = 0 |
| | | // Services.watchMuse(id: m.id, timeLook: timeLook).subscribe(onNext: {_ in |
| | | // NotificationCenter.default.post(name: TreeTaskUpdate_Noti, object: nil) |
| | | // }).disposed(by: disposeBag) |
| | | // } |
| | | } |
| | | |
| | | override func setUI() { |
| | |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | | |
| | | view_function.jq_gradientColor(colorArr: [UIColor.black.withAlphaComponent(0.35).cgColor,UIColor.clear.cgColor], cornerRadius: 0, startPoint: CGPoint(x: 0, y: 1), endPoint: CGPoint(x: 0, y: 0), bounds: nil, locations: nil) |
| | | if gradientLayer == nil{ |
| | | gradientLayer = view_function.jq_gradientColor(colorArr: [UIColor.black.withAlphaComponent(0.35).cgColor,UIColor.clear.cgColor], cornerRadius: 0, startPoint: CGPoint(x: 0, y: 1), endPoint: CGPoint(x: 0, y: 0), bounds: nil, locations: nil) |
| | | } |
| | | } |
| | | |
| | | |
| | |
| | | case .code: |
| | | tf_content.isSecureTextEntry = false |
| | | image_security.image = UIImage(named: "icon_code") |
| | | #if DEBUG |
| | | tf_content.text = "220125" |
| | | #endif |
| | | case .pwd: |
| | | tf_content.isSecureTextEntry = btn_eye.isSelected |
| | | image_security.image = UIImage(named: "icon_pwd") |
| | |
| | | @IBOutlet weak var btn_account: QMUIButton! |
| | | @IBOutlet weak var btn_buy: QMUIButton! |
| | | @IBOutlet weak var btn_customer: QMUIButton! |
| | | @IBOutlet weak var btn_customer_1: QMUIButton! |
| | | @IBOutlet weak var btn_setting: QMUIButton! |
| | | @IBOutlet weak var btn_setting_1: QMUIButton! |
| | | @IBOutlet weak var btn_share: QMUIButton! |
| | | @IBOutlet weak var view_rank: GradientView! |
| | | @IBOutlet weak var image_vipBg: UIImageView! |
| | |
| | | |
| | | |
| | | var needLaunch:Bool = true |
| | | var turnState:Bool? |
| | | |
| | | override func viewDidLoad() { |
| | | super.viewDidLoad() |
| | | |
| | | view_otherGradient.isHidden = true |
| | | view_loveRanking.isHidden = true |
| | | btn_jump.isHidden = true |
| | | btn_buy.isHidden = true |
| | | btn_history.isHidden = true |
| | | btn_account.isHidden = true |
| | | btn_setting_1.isHidden = false |
| | | btn_customer_1.isHidden = false |
| | | |
| | | let tap = UITapGestureRecognizer(target: self, action: #selector(rankAction)) |
| | | view_rank.isUserInteractionEnabled = true |
| | | view_rank.addGestureRecognizer(tap) |
| | | |
| | | getData() |
| | | queryNotice() |
| | | if !isSimulator{ |
| | | Services.getTurn(progress: false).subscribe(onNext: {[weak self]data in |
| | | guard let weakSelf = self else { return } |
| | | if let m = data.data,m == true{ |
| | | weakSelf.view_otherGradient.isHidden = false |
| | | weakSelf.view_loveRanking.isHidden = false |
| | | weakSelf.btn_jump.isHidden = false |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | |
| | | loginNav.modalPresentationStyle = .fullScreen |
| | | JQ_currentViewController().present(loginNav, animated: true) |
| | | } |
| | | //通讯录通讯录通讯录 |
| | | |
| | | if !isSimulator{ |
| | | Services.getTurn(progress: false).subscribe(onNext: {[weak self]data in |
| | | guard let weakSelf = self else { return } |
| | | |
| | | guard weakSelf.turnState != data.data else {return} |
| | | |
| | | if let m = data.data,m == true{ |
| | | weakSelf.view_otherGradient.isHidden = false |
| | | weakSelf.view_loveRanking.isHidden = false |
| | | weakSelf.btn_jump.isHidden = false |
| | | weakSelf.btn_buy.isHidden = false |
| | | weakSelf.btn_history.isHidden = false |
| | | weakSelf.btn_account.isHidden = false |
| | | weakSelf.btn_record.isHidden = false |
| | | weakSelf.btn_setting_1.isHidden = true |
| | | weakSelf.btn_customer_1.isHidden = true |
| | | }else{ |
| | | weakSelf.view_otherGradient.isHidden = true |
| | | weakSelf.view_loveRanking.isHidden = true |
| | | weakSelf.btn_jump.isHidden = true |
| | | weakSelf.btn_buy.isHidden = true |
| | | weakSelf.btn_history.isHidden = true |
| | | weakSelf.btn_account.isHidden = true |
| | | weakSelf.btn_record.isHidden = true |
| | | weakSelf.btn_setting_1.isHidden = false |
| | | weakSelf.btn_customer_1.isHidden = false |
| | | } |
| | | },onError: {[weak self] _ in |
| | | guard let weakSelf = self else { return } |
| | | weakSelf.view_otherGradient.isHidden = true |
| | | weakSelf.view_loveRanking.isHidden = true |
| | | weakSelf.btn_jump.isHidden = true |
| | | weakSelf.btn_buy.isHidden = true |
| | | weakSelf.btn_history.isHidden = true |
| | | weakSelf.btn_account.isHidden = true |
| | | weakSelf.btn_record.isHidden = true |
| | | weakSelf.btn_setting_1.isHidden = false |
| | | weakSelf.btn_customer_1.isHidden = false |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | needLaunch = false |
| | | |
| | | getData() |
| | | queryNotice() |
| | | } |
| | | |
| | | override func setUI() { |
| | |
| | | btn_customer.imagePosition = .top |
| | | btn_setting.imagePosition = .top |
| | | btn_share.imagePosition = .top |
| | | |
| | | btn_customer_1.imagePosition = .top |
| | | btn_setting_1.imagePosition = .top |
| | | } |
| | | |
| | | private func queryNotice(){ |
| | |
| | | |
| | | private func getData(){ |
| | | |
| | | Services.getUserDetail().subscribe(onNext: {[weak self]data in |
| | | Services.getUserDetail(showProgress: false).subscribe(onNext: {[weak self]data in |
| | | if let model = data.data{ |
| | | UserViewModel.saveAvatarInfo(model) |
| | | self?.setUserUI(model: model) |
| | |
| | | <outlet property="btn_buy" destination="EZo-PT-9vW" id="CIZ-Xp-uuw"/> |
| | | <outlet property="btn_collect" destination="ISF-ez-mrg" id="Evq-fr-yUk"/> |
| | | <outlet property="btn_customer" destination="9fM-W9-vHq" id="lDM-Ym-HRC"/> |
| | | <outlet property="btn_customer_1" destination="lDL-vF-m2n" id="CIz-0L-VHE"/> |
| | | <outlet property="btn_history" destination="css-Hl-o5F" id="hNT-9T-ggx"/> |
| | | <outlet property="btn_jump" destination="nbE-cS-9bV" id="yQF-I5-IYN"/> |
| | | <outlet property="btn_notice" destination="cNy-lc-dn6" id="iMB-5G-Hna"/> |
| | | <outlet property="btn_record" destination="ohz-jh-MYr" id="I71-ch-fvw"/> |
| | | <outlet property="btn_setting" destination="gCl-Xh-n2C" id="QuC-zj-2x4"/> |
| | | <outlet property="btn_setting_1" destination="Jks-lt-JlW" id="Bki-8U-4qG"/> |
| | | <outlet property="btn_share" destination="503-ii-Lw9" id="v1J-Bx-FQ7"/> |
| | | <outlet property="image_medal" destination="dut-7H-aMv" id="7p1-yV-vbS"/> |
| | | <outlet property="image_userAvatar" destination="dFl-Si-mEi" id="j1A-z8-kad"/> |
| | |
| | | <action selector="myColletAction:" destination="-1" eventType="touchUpInside" id="vYn-1g-tgD"/> |
| | | </connections> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9fM-W9-vHq" customClass="QMUIButton"> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Jks-lt-JlW" customClass="QMUIButton"> |
| | | <rect key="frame" x="184" y="0.0" width="92" height="23"/> |
| | | <fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" title="联系客服" image="icon_customer"> |
| | | <color key="titleColor" red="0.13725490200000001" green="0.13725490200000001" blue="0.13725490200000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | </state> |
| | | <userDefinedRuntimeAttributes> |
| | | <userDefinedRuntimeAttribute type="number" keyPath="spacingBetweenImageAndTitle"> |
| | | <real key="value" value="23.5"/> |
| | | </userDefinedRuntimeAttribute> |
| | | </userDefinedRuntimeAttributes> |
| | | <connections> |
| | | <action selector="customerAction:" destination="-1" eventType="touchUpInside" id="Qh6-1S-wWO"/> |
| | | </connections> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gCl-Xh-n2C" customClass="QMUIButton"> |
| | | <rect key="frame" x="276" y="0.0" width="92" height="23"/> |
| | | <fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" title="设置" image="icon_setting"> |
| | |
| | | </userDefinedRuntimeAttribute> |
| | | </userDefinedRuntimeAttributes> |
| | | <connections> |
| | | <action selector="settingAction:" destination="-1" eventType="touchUpInside" id="I2H-D6-zdo"/> |
| | | <action selector="settingAction:" destination="-1" eventType="touchUpInside" id="Zfu-mL-T8s"/> |
| | | </connections> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lDL-vF-m2n" customClass="QMUIButton"> |
| | | <rect key="frame" x="276" y="0.0" width="92" height="23"/> |
| | | <fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" title="联系客服" image="icon_customer"> |
| | | <color key="titleColor" red="0.13725490200000001" green="0.13725490200000001" blue="0.13725490200000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | </state> |
| | | <userDefinedRuntimeAttributes> |
| | | <userDefinedRuntimeAttribute type="number" keyPath="spacingBetweenImageAndTitle"> |
| | | <real key="value" value="23.5"/> |
| | | </userDefinedRuntimeAttribute> |
| | | </userDefinedRuntimeAttributes> |
| | | <connections> |
| | | <action selector="customerAction:" destination="-1" eventType="touchUpInside" id="ufR-AM-GoM"/> |
| | | </connections> |
| | | </button> |
| | | </subviews> |
| | |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="Omi-TP-JuV"> |
| | | <rect key="frame" x="0.0" y="97" width="368" height="50"/> |
| | | <rect key="frame" x="0.0" y="124.00000000000011" width="368" height="23"/> |
| | | <subviews> |
| | | <button hidden="YES" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EZo-PT-9vW" customClass="QMUIButton"> |
| | | <rect key="frame" x="0.0" y="0.0" width="0.0" height="50"/> |
| | | <rect key="frame" x="0.0" y="0.0" width="0.0" height="23"/> |
| | | <fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" title="我的已购" image="icon_buy"> |
| | |
| | | <action selector="paymentCourseAction:" destination="-1" eventType="touchUpInside" id="Tnq-iq-nI0"/> |
| | | </connections> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="503-ii-Lw9" customClass="QMUIButton"> |
| | | <rect key="frame" x="0.0" y="0.0" width="92" height="50"/> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9fM-W9-vHq" customClass="QMUIButton"> |
| | | <rect key="frame" x="0.0" y="0.0" width="122.66666666666667" height="23"/> |
| | | <fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" title="分享赚钱" image="icon_share"> |
| | | <state key="normal" title="联系客服" image="icon_customer"> |
| | | <color key="titleColor" red="0.13725490200000001" green="0.13725490200000001" blue="0.13725490200000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | </state> |
| | | <userDefinedRuntimeAttributes> |
| | | <userDefinedRuntimeAttribute type="number" keyPath="spacingBetweenImageAndTitle"> |
| | | <real key="value" value="23.5"/> |
| | | </userDefinedRuntimeAttribute> |
| | | </userDefinedRuntimeAttributes> |
| | | <connections> |
| | | <action selector="customerAction:" destination="-1" eventType="touchUpInside" id="Qh6-1S-wWO"/> |
| | | </connections> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gCl-Xh-n2C" customClass="QMUIButton"> |
| | | <rect key="frame" x="122.66666666666666" y="0.0" width="122.66666666666666" height="23"/> |
| | | <fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" title="设置" image="icon_setting"> |
| | | <color key="titleColor" red="0.13725490200000001" green="0.13725490200000001" blue="0.13725490200000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | </state> |
| | | <userDefinedRuntimeAttributes> |
| | | <userDefinedRuntimeAttribute type="number" keyPath="spacingBetweenImageAndTitle"> |
| | | <real key="value" value="23.5"/> |
| | | </userDefinedRuntimeAttribute> |
| | | </userDefinedRuntimeAttributes> |
| | | <connections> |
| | | <action selector="settingAction:" destination="-1" eventType="touchUpInside" id="I2H-D6-zdo"/> |
| | | </connections> |
| | | </button> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="503-ii-Lw9" customClass="QMUIButton"> |
| | | <rect key="frame" x="245.33333333333334" y="0.0" width="122.66666666666666" height="23"/> |
| | | <fontDescription key="fontDescription" type="system" weight="medium" pointSize="13"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" title="分享" image="icon_share"> |
| | | <color key="titleColor" red="0.13725490200000001" green="0.13725490200000001" blue="0.13725490200000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | </state> |
| | | <userDefinedRuntimeAttributes> |
| | |
| | | <action selector="shareAction:" destination="-1" eventType="touchUpInside" id="GjN-ls-QGE"/> |
| | | </connections> |
| | | </button> |
| | | <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CWK-p3-BUm"> |
| | | <rect key="frame" x="92" y="0.0" width="92" height="50"/> |
| | | <fontDescription key="fontDescription" type="system" pointSize="17"/> |
| | | <nil key="textColor"/> |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="95E-qm-lja"> |
| | | <rect key="frame" x="184" y="0.0" width="92" height="50"/> |
| | | <fontDescription key="fontDescription" type="system" pointSize="17"/> |
| | | <nil key="textColor"/> |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="N7N-gO-EXJ"> |
| | | <rect key="frame" x="276" y="0.0" width="92" height="50"/> |
| | | <fontDescription key="fontDescription" type="system" pointSize="17"/> |
| | | <nil key="textColor"/> |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | </subviews> |
| | | </stackView> |
| | | </subviews> |
| | |
| | | viewModel.beginRefresh() |
| | | |
| | | Services.getCustomerCode().subscribe(onNext: {data in |
| | | if let m = data.data{ |
| | | if let m = data.data?.jq_urlEncoded(){ |
| | | self.customerImageView?.sd_setImage(with: URL(string: m)) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | |
| | | <rect key="frame" x="0.0" y="59" width="393" height="759"/> |
| | | <subviews> |
| | | <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fRO-Jr-RG3"> |
| | | <rect key="frame" x="0.0" y="0.0" width="393" height="576.66666666666663"/> |
| | | <rect key="frame" x="0.0" y="0.0" width="393" height="596.66666666666663"/> |
| | | <subviews> |
| | | <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="邀好友赚分佣抽成" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oFE-SE-88r"> |
| | | <rect key="frame" x="45.333333333333343" y="74" width="302.66666666666663" height="48"/> |
| | | <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="邀好友" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oFE-SE-88r"> |
| | | <rect key="frame" x="139.66666666666666" y="82" width="113.66666666666666" height="60"/> |
| | | <constraints> |
| | | <constraint firstAttribute="height" constant="60" id="HMb-H4-7Ck"/> |
| | | </constraints> |
| | | <fontDescription key="fontDescription" type="system" pointSize="40"/> |
| | | <color key="textColor" red="0.54117647058823526" green="0.68235294117647061" blue="0.396078431372549" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="INVITE FRIENDS TO EARN COMMISSION" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lCK-Mw-vTJ"> |
| | | <rect key="frame" x="116.66666666666667" y="131" width="159.66666666666663" height="9.6666666666666572"/> |
| | | <rect key="frame" x="116.66666666666667" y="151" width="159.66666666666663" height="9.6666666666666572"/> |
| | | <fontDescription key="fontDescription" type="system" pointSize="8"/> |
| | | <color key="textColor" red="0.54117647059999996" green="0.68235294120000001" blue="0.39607843139999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bff-wn-eyk"> |
| | | <rect key="frame" x="113.33333333333333" y="177.33333333333337" width="166.33333333333337" height="168"/> |
| | | <rect key="frame" x="113.33333333333333" y="197.33333333333331" width="166.33333333333337" height="168"/> |
| | | <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <constraints> |
| | | <constraint firstAttribute="width" constant="166.5" id="BYS-EF-Iz8"/> |
| | |
| | | </constraints> |
| | | </imageView> |
| | | <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="XbP-tV-8pd"> |
| | | <rect key="frame" x="53.666666666666657" y="375.66666666666669" width="286" height="42.666666666666686"/> |
| | | <rect key="frame" x="53.666666666666657" y="395.66666666666669" width="286" height="42.666666666666686"/> |
| | | <color key="backgroundColor" red="0.54117647059999996" green="0.68235294120000001" blue="0.39607843139999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> |
| | | <constraints> |
| | | <constraint firstAttribute="height" constant="42.5" id="jad-bO-pIf"/> |
| | | </constraints> |
| | | <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="15"/> |
| | | <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> |
| | | <state key="normal" title="分享给好友赚佣金"/> |
| | | <state key="normal" title="分享给好友"/> |
| | | <connections> |
| | | <action selector="shareAction:" destination="-1" eventType="touchUpInside" id="Atz-SI-etO"/> |
| | | </connections> |
| | | </button> |
| | | <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nfc-1h-yql"> |
| | | <rect key="frame" x="20" y="448.33333333333331" width="353" height="98.333333333333314"/> |
| | | <rect key="frame" x="20" y="468.33333333333331" width="353" height="98.333333333333314"/> |
| | | <subviews> |
| | | <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="分佣规则说明" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OEa-I3-lut"> |
| | | <rect key="frame" x="132" y="20.000000000000057" width="89.333333333333314" height="18"/> |
| | | <rect key="frame" x="132" y="20" width="89.333333333333314" height="18"/> |
| | | <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="15"/> |
| | | <nil key="textColor"/> |
| | | <nil key="highlightedColor"/> |
| | | </label> |
| | | <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PXe-Ef-0Zn"> |
| | | <rect key="frame" x="20" y="58.000000000000064" width="313" height="20.333333333333336"/> |
| | | <rect key="frame" x="20" y="58.000000000000007" width="313" height="20.333333333333336"/> |
| | | <fontDescription key="fontDescription" type="system" pointSize="17"/> |
| | | <nil key="textColor"/> |
| | | <nil key="highlightedColor"/> |
| | |
| | | <constraints> |
| | | <constraint firstItem="nfc-1h-yql" firstAttribute="top" secondItem="XbP-tV-8pd" secondAttribute="bottom" constant="30" id="04U-uD-YFh"/> |
| | | <constraint firstItem="XbP-tV-8pd" firstAttribute="top" secondItem="bff-wn-eyk" secondAttribute="bottom" constant="30.5" id="0ND-qB-iY4"/> |
| | | <constraint firstItem="oFE-SE-88r" firstAttribute="top" secondItem="fRO-Jr-RG3" secondAttribute="top" constant="74" id="3f7-Q1-n5C"/> |
| | | <constraint firstItem="lCK-Mw-vTJ" firstAttribute="centerX" secondItem="oFE-SE-88r" secondAttribute="centerX" id="87n-J7-eVR"/> |
| | | <constraint firstItem="oFE-SE-88r" firstAttribute="centerX" secondItem="fRO-Jr-RG3" secondAttribute="centerX" id="Alc-26-vLT"/> |
| | | <constraint firstItem="lCK-Mw-vTJ" firstAttribute="top" secondItem="oFE-SE-88r" secondAttribute="bottom" constant="9" id="Fxq-TA-MMT"/> |
| | | <constraint firstItem="oFE-SE-88r" firstAttribute="top" secondItem="fRO-Jr-RG3" secondAttribute="topMargin" constant="74" id="Hyc-9X-wKK"/> |
| | | <constraint firstItem="XbP-tV-8pd" firstAttribute="leading" secondItem="fRO-Jr-RG3" secondAttribute="leading" constant="53.5" id="P23-ld-LdM"/> |
| | | <constraint firstItem="bff-wn-eyk" firstAttribute="top" secondItem="lCK-Mw-vTJ" secondAttribute="bottom" constant="36.5" id="Uas-XO-jrk"/> |
| | | <constraint firstItem="bff-wn-eyk" firstAttribute="centerX" secondItem="fRO-Jr-RG3" secondAttribute="centerX" id="V0p-rN-phX"/> |
| | |
| | | |
| | | lazy var pageVC:FFPageViewController = { |
| | | let pageViewController = FFPageViewController() |
| | | pageViewController.view.backgroundColor = .clear |
| | | pageViewController.view.backgroundColor = .white |
| | | pageViewController.delegate = self |
| | | pageViewController.scrollview.backgroundColor = .clear |
| | | pageViewController.scrollview.backgroundColor = .white |
| | | return pageViewController |
| | | }() |
| | | |
| | |
| | | errorString.append("\n【错误码:\(code)】") |
| | | } |
| | | if !ignoreAlert{ |
| | | alert(msg: errorString) |
| | | alert(msg: "网络连接超时") |
| | | } |
| | | ob.onError(response.error!) |
| | | return |
| | |
| | | ob.onError(NetRequestError.InvaildSession) |
| | | default: |
| | | if !ignoreAlert{ |
| | | alertError(msg: "\(next.msg)") |
| | | alertError(msg: "网络连接超时") |
| | | } |
| | | ob.onError(NetRequestError.Other(next.code,next.msg)) |
| | | } |
| | |
| | | //let All_Url = "http://192.168.110.64:9000" |
| | | #endif |
| | | |
| | | let ShareUrl = "http://113.45.158.158/share/#/pages" |
| | | let ShareUrl = "https://xq.xqzhihui.com/share/#/pages" |
| | | |
| | | |
| | | class Services: NSObject { |
| | |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/meditation/client/meditation/home/getTodayMeditation") |
| | | .append(key: "apipost_id", value: "25c3e3d0b0e15d") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false,ignoreAlert: true) |
| | | } |
| | | |
| | | /// 获取私人定制 |
| | |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/meditation/client/meditation/home/getPersonalityPlan") |
| | | .append(key: "apipost_id", value: "25c3e3d0b0e15c") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false,ignoreAlert: true) |
| | | } |
| | | |
| | | /// 获取全部的冥想音频及分类列表 |
| | |
| | | let params = ParamsAppender.build(url: All_Url) |
| | | params.interface(url: "/meditation/client/meditation/home/getMeditationAndCateList") |
| | | .append(key: "apipost_id", value: "25c3e3d0b0e157") |
| | | return NetworkRequest.request(params: params, method: .get, progress: false) |
| | | return NetworkRequest.request(params: params, method: .get, progress: false,ignoreAlert: true) |
| | | } |
| | | |
| | | |
| | |
| | | } |
| | | |
| | | |
| | | func updateVideoUrl(_ url:String,autoPlay:Bool = false,placeHoderImageUrl:String? = nil){ |
| | | |
| | | func updateVideoUrl(_ url:String,autoPlay:Bool = false,placeHoderImageUrl:String? = nil,delegate:CLPlayerDelegate? = nil){ |
| | | self.player.delegate = delegate |
| | | self.placeHoderImageUrl = placeHoderImageUrl |
| | | if placeHoderImageUrl != nil{ |
| | | addPlaceHoderView(placeHoderImage: placeHoderImageUrl!) |
| | |
| | | |
| | | private var viewModel = PavilionViewModel() |
| | | private var locationTool = JQ_LocationTool.instance() |
| | | private var isFirst:Bool = true //首次进入 |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | super.viewDidAppear(animated) |
| | |
| | | viewModel.configure(collectionView) |
| | | viewModel.beginRefresh() |
| | | |
| | | locationTool.startLocation { location in |
| | | locationTool.startLocation(isSingle: true) {location in |
| | | self.viewModel.location.accept(location.coordinate) |
| | | self.viewModel.beginRefresh() |
| | | } errorClouse: { error in |
| | |
| | | @objc func showDetailAction(){ |
| | | if let id = meditationModel?.id{ |
| | | |
| | | Services.getMeditationDetail(id: id).subscribe(onNext: {[weak self]data in |
| | | guard let weakSelf = self else { return } |
| | | Services.getMeditationDetail(id: id).subscribe(onNext: { data in |
| | | if let m = data.data{ |
| | | let isVip = m.isVip == .yes |
| | | if m.chargeType == .free || (isVip && m.chargeType == .vipFree) || (m.chargeType == .payment && m.isBuy == .yes){ |
| | |
| | | |
| | | if btn.isSelected{ |
| | | self.audioPlayer.bgmPlayer?.pause() |
| | | // self.audioPlayer.masterPlayer?.pause() |
| | | self.stopRunloopAni() |
| | | PayMusicVC.updateStatus(.pause) |
| | | }else{ |
| | |
| | | } |
| | | |
| | | self.audioPlayer.bgmPlayer?.play() |
| | | // self.audioPlayer.masterPlayer?.play() |
| | | // self.audioPlayer.masterPlayer?.volume = Float(UserDefaultSettingViewModel.getSetting()?.masterVolume ?? 0.5) |
| | | self.startRunloopAni() |
| | | PayMusicVC.updateStatus(.playing) |
| | | } |
| | |
| | | CommonAlertView.show(title: "提示", content: "是否关闭当前播放音频?") {[weak self] state in |
| | | guard let weakSelf = self else { return } |
| | | if state{ |
| | | self?.audioPlayer.cleanMuse() |
| | | self?.view.removeFromSuperview() |
| | | self?.removeFromParent() |
| | | |
| | | if let id = self?.audioPlayer.meditationModel?.id{ |
| | | Services.watchMuse(id: id, timeLook: self?.audioPlayer.lisenMuseTime ?? 0).subscribe(onNext: { _ in |
| | |
| | | }).disposed(by: weakSelf.disposeBag) |
| | | } |
| | | |
| | | if UserDefaultSettingViewModel.getSetting()?.sceneMusicModel != nil{ |
| | | AudioPlayer.getSharedInstance().playSceneAt(UserDefaultSettingViewModel.getSetting()!.sceneMusicModel!.audioFile) |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(delay: 0.4) { |
| | | NotificationCenter.default.post(name: ReloadData_Noti, object: nil, userInfo: nil) |
| | | } |
| | | |
| | | DispatchQueue.main.async { |
| | | self?.audioPlayer.bgmPlayer = nil |
| | | MPNowPlayingInfoCenter.default().nowPlayingInfo = nil |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(delay: 3.0) { |
| | | if UserDefaultSettingViewModel.getSetting()?.sceneMusicModel != nil{ |
| | | AudioPlayer.getSharedInstance().playSceneAt(UserDefaultSettingViewModel.getSetting()!.sceneMusicModel!.audioFile) |
| | | } |
| | | } |
| | | |
| | | self?.audioPlayer.cleanMuse() |
| | | self?.view.removeFromSuperview() |
| | | self?.removeFromParent() |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | class AudioPlayer { |
| | | private(set) var bgmPlayer:AVPlayer? // 背景音 |
| | | private(set) var scenePlayer:AVPlayer? //场景音 |
| | | // private(set) var masterPlayer:AVPlayer? //大师音 |
| | | var bgmPlayer:AVPlayer? // 背景音 |
| | | var scenePlayer:AVPlayer? //场景音 |
| | | private(set) var playIndex:Int = 0 //播放的角标 |
| | | private var cacheDirectory:URL! |
| | | private let session = URLSession.shared |
| | |
| | | |
| | | //清除之前的 |
| | | self.urls.removeAll() |
| | | // self.masterPlayer?.pause() |
| | | self.bgmPlayer?.pause() |
| | | // self.masterPlayer = nil |
| | | self.bgmPlayer = nil |
| | | |
| | | self.delegate = delegate |
| | |
| | | alertError(msg: "数据获取失败");return |
| | | } |
| | | |
| | | let masterUrl = URL(string: model.tutorAudioUrl.jq_urlEncoded()) |
| | | |
| | | autoreleasepool{[unowned self] () in |
| | | for url in urls { |
| | | self.checkCacheAudio(from: url) {[unowned self] _, url in |
| | |
| | | } |
| | | } |
| | | self.bgmPlayer = AVPlayer(url: self.urls[firstPlayIndex]) |
| | | |
| | | // if masterUrl != nil{ |
| | | // self.masterPlayer = AVPlayer(url: masterUrl!) |
| | | // self.masterPlayer?.volume = Float(UserDefaultSettingViewModel.getSetting()?.masterVolume ?? 0.5) |
| | | // } |
| | | } |
| | | |
| | | self.bgmPlayer?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 1), queue: DispatchQueue.main) { [weak self](time) in |
| | |
| | | weakSelf.delegate?.playState(.playing) |
| | | guard weakSelf.bgmPlayer != nil else {return} |
| | | weakSelf.lisenMuseTime += 1 |
| | | |
| | | if weakSelf.lisenMuseTime == 60{ |
| | | if let id = self?.meditationModel?.id{ |
| | | Services.watchMuse(id: id, timeLook: 60).subscribe(onNext: { _ in |
| | | weakSelf.lisenMuseTime = 0 |
| | | NotificationCenter.default.post(name: TreeTaskUpdate_Noti, object: nil) |
| | | NotificationCenter.default.post(name: UpdateUserProfile_Noti, object: nil) |
| | | }).disposed(by:weakSelf.disposeBag) |
| | | } |
| | | } |
| | | |
| | | //当前正在播放的时间 |
| | | let loadTime = CMTimeGetSeconds(time) |
| | | //视频总时间 |
| | |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | |
| | | |
| | | |
| | | self.bgmPlayer?.currentItem?.rx.observe(AVPlayerItem.Status.self,"status").subscribe(onNext: { _ in |
| | | // print("---MasterStatus1: \(self.masterPlayer?.status.rawValue ?? 0)") |
| | | print("---bgmStatus1: \(self.bgmPlayer?.status.rawValue ?? 0)") |
| | | if self.bgmPlayer?.status == .readyToPlay{ |
| | | self.bgmPlayer?.play() |
| | | // self.masterPlayer?.play() |
| | | } |
| | | |
| | | }).disposed(by: disposeBag) |
| | | |
| | | // self.masterPlayer?.currentItem?.rx.observe(AVPlayerItem.Status.self,"status").subscribe(onNext: { _ in |
| | | // print("---MasterStatus: \(self.masterPlayer?.status.rawValue ?? 0)") |
| | | // print("---bgmStatus: \(self.bgmPlayer?.status.rawValue ?? 0)") |
| | | // if self.bgmPlayer?.status == .readyToPlay && self.masterPlayer?.status == .readyToPlay{ |
| | | // self.bgmPlayer?.play() |
| | | // self.masterPlayer?.play() |
| | | // } |
| | | // }).disposed(by: disposeBag) |
| | | |
| | | setLockScreen() |
| | | |
| | | do { |
| | | try AVAudioSession.sharedInstance().setActive(true) |
| | | print("Playback OK") |
| | | try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.allowBluetooth, .allowAirPlay]) |
| | | try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.allowBluetooth, .allowAirPlay]) |
| | | print("Session is Active") |
| | | } catch { |
| | | print(error) |
| | |
| | | }else{ |
| | | scenePlayer?.replaceCurrentItem(with: AVPlayerItem(url: URL)) |
| | | } |
| | | |
| | | do { |
| | | try AVAudioSession.sharedInstance().setActive(false) |
| | | try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default, options: []) |
| | | } catch { |
| | | print(error) |
| | | } |
| | | |
| | | DispatchQueue.main.asyncAfter(delay: 0.5) { |
| | | self.scenePlayer?.play() |
| | | self.scenePlayer?.volume = Float(UserDefaultSettingViewModel.getSetting()?.volume ?? 0.5) |
| | | // self.scenePlayer?.allowsExternalPlayback = false |
| | | // self.scenePlayer?.usesExternalPlaybackWhileExternalScreenIsActive = false |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | func pauseScene(){ |
| | | scenePlayer?.pause() |
| | | if scenePlayer?.timeControlStatus == .playing{ |
| | | scenePlayer?.pause() |
| | | } |
| | | } |
| | | |
| | | func pauseBGM(){ |
| | | if bgmPlayer?.timeControlStatus == .playing{ |
| | | bgmPlayer?.pause() |
| | | } |
| | | } |
| | | |
| | | func playScene(){ |
| | | if UserDefaultSettingViewModel.getSetting()?.sceneMusicModel != nil{ |
| | |
| | | // 修改进度 |
| | | center.changePlaybackPositionCommand.addTarget {[unowned self] event in |
| | | |
| | | print("1:--->\(event)") |
| | | guard let event = event as? MPChangePlaybackPositionCommandEvent else { |
| | | return .commandFailed |
| | | } |
| | |
| | | |
| | | // 播放 |
| | | center.playCommand.addTarget {[weak self] event in |
| | | print("2:--->\(event)") |
| | | self?.bgmPlayer?.play() |
| | | // self?.masterPlayer?.play() |
| | | PayMusicVC.updateStatus(.playing) |
| | |
| | | |
| | | // 暂停 |
| | | center.pauseCommand.addTarget {[weak self] event in |
| | | print("3:--->\(event)") |
| | | self?.bgmPlayer?.pause() |
| | | // self?.masterPlayer?.pause() |
| | | PayMusicVC.updateStatus(.pause) |
| | | return .success |
| | | } |
| | | |
| | | // 下一首 |
| | | center.nextTrackCommand.addTarget { event in |
| | | return .success |
| | | } |
| | | |
| | | center.nextTrackCommand.isEnabled = false |
| | | center.previousTrackCommand.isEnabled = false |
| | | |
| | | // 上一首 |
| | | center.previousTrackCommand.addTarget { event in |
| | | return .success |
| | | } |
| | | } |
| | | |
| | | func setTimer(times:Int){ |
| | |
| | | weakSelf.times.accept(nil) |
| | | weakSelf.stopTimer() |
| | | weakSelf.bgmPlayer?.pause() |
| | | // weakSelf.masterPlayer?.pause() |
| | | weakSelf.scenePlayer?.pause() |
| | | MPNowPlayingInfoCenter.default().nowPlayingInfo = nil |
| | | AudioPlayer.destroy() |
| | |
| | | let downloadTask = session.downloadTask(with: url) { tempLocalUrl, response, error in |
| | | if let tempLocalUrl = tempLocalUrl, error == nil { |
| | | do { |
| | | let temp = videoCacheUrl.appendingPathExtension(url.pathExtension) |
| | | let temp = videoCacheUrl |
| | | try FileManager.default.moveItem(at: tempLocalUrl, to: temp) |
| | | } catch { |
| | | print("视频缓存失败:catch") |
| | |
| | | aPNGTreeImageView?.stopAnimating() |
| | | } |
| | | |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | player.play() |
| | | aPNGSunImageView?.startAnimating() |
| | | aPNGTreeImageView?.startAnimating() |
| | | override func viewDidAppear(_ animated: Bool) { |
| | | player.play() |
| | | aPNGSunImageView?.startAnimating() |
| | | aPNGTreeImageView?.startAnimating() |
| | | |
| | | Services.energyExchangeGift(page: 1).subscribe(onNext: {[weak self]data in |
| | | self?.btn_exchange.isHidden = (data.data?.count ?? 0) == 0 |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | |
| | | getTreeData() |
| | | if !(UserViewModel.getLoginInfo()?.accessToken.isEmpty ?? true){ |
| | | Services.isFirst().subscribe(onNext: {[weak self]data in |
| | | |
| | | var viewModel = UserDefaultSettingViewModel.getSetting() |
| | | |
| | | if viewModel?.userFirstOpenTreeTask == true || data.data == true{ |
| | | DispatchQueue.main.asyncAfter(delay: 0.5) { |
| | | let h = (JQ_ScreenW - 90) * 0.8766 |
| | | self?.ruleView = TreeTeskFirstRuleView.show(title: "生命之树", content:"亲爱的家人,生命之树的种子已植入这片沃土,请以农夫的心态用心浇灌,为你加油哦。",textAlignment: .left,height: h,textTopOffset: 22) |
| | | self?.voicePlayer.replaceCurrentItem(with: AVPlayerItem(url: URL(string: TreeLevel.level_1.sound)!)) |
| | | self?.voicePlayer.play() |
| | | self?.ruleView?.setEnableBtn(state: false) |
| | | } |
| | | viewModel?.userFirstOpenTreeTask = true |
| | | UserDefaultSettingViewModel.saveSetting(viewModel!) |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | } |
| | | } |
| | | |
| | | override func viewDidLayoutSubviews() { |
| | | super.viewDidLayoutSubviews() |
| | |
| | | super.viewDidLoad() |
| | | title = "树苗打卡站" |
| | | btn_seedingAgain.isHidden = true |
| | | |
| | | if settingModel!.userFirstOpenTreeTask{ |
| | | DispatchQueue.main.asyncAfter(delay: 0.5) { |
| | | let h = (JQ_ScreenW - 90) * 0.8766 |
| | | self.ruleView = TreeTeskFirstRuleView.show(title: "生命之树", content:"亲爱的家人,生命之树的种子已植入这片沃土,请以农夫的心态用心浇灌,为你加油哦。",textAlignment: .left,height: h,textTopOffset: 22) |
| | | self.voicePlayer.replaceCurrentItem(with: AVPlayerItem(url: URL(string: TreeLevel.level_1.sound)!)) |
| | | self.voicePlayer.play() |
| | | self.ruleView?.setEnableBtn(state: false) |
| | | } |
| | | } |
| | | |
| | | getTreeData() |
| | | } |
| | | |
| | | override func setUI() { |
| | |
| | | weakSelf.treeInfoModel?.growthValue = m.growthValue |
| | | weakSelf.treeInfoModel?.energyValue = 0 |
| | | weakSelf.treeInfoModel?.nextLevel = m.nextLevel |
| | | weakSelf.icon_energy.text = String(format: "当前能量值:%ld", 0) |
| | | |
| | | NotificationCenter.default.post(name: UpdateUserProfile_Noti, object: nil) |
| | | weakSelf.getTreeData() |
| | | } |
| | | }).disposed(by: disposeBag) |
| | | |
| | |
| | | getData() |
| | | |
| | | Services.getCustomerCode().subscribe(onNext: {[weak self] data in |
| | | self?.image_qrCode.sd_setImage(with: URL(string: data.data ?? "")) |
| | | self?.image_qrCode.sd_setImage(with: URL(string: data.data?.jq_urlEncoded() ?? "")) |
| | | }).disposed(by: disposeBag) |
| | | |
| | | let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressCopyAction(_:))) |