diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..3e5d6997 Binary files /dev/null and b/.DS_Store differ diff --git a/KFImageView.swift b/KFImageView.swift new file mode 100644 index 00000000..57eb6830 --- /dev/null +++ b/KFImageView.swift @@ -0,0 +1,21 @@ +// +// KFImageView.swift +// SDCycleScrollView +// +// Created by york on 2025/6/26. +// + +import UIKit + +@objc +class KFImageView: UIImageView { + + /* + // Only override draw() if you perform custom drawing. + // An empty implementation adversely affects performance during animation. + override func draw(_ rect: CGRect) { + // Drawing code + } + */ + +} diff --git a/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist b/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..05e23317 --- /dev/null +++ b/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + Pods-SDCycleScrollView.xcscheme_^#shared#^_ + + orderHint + 2 + + SDWebImage.xcscheme_^#shared#^_ + + orderHint + 1 + + + + diff --git a/README.md b/README.md index 6f6c6131..89e8b883 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ 北上广深杭大量招人,各种研发岗位都在找,有意向者可以直接点击后面链接投递简历或者联系我 gsdios@126.com https://job.toutiao.com/s/eL91QMm, ### 支持pod导入 -pod 'SDCycleScrollView','>= 1.82' +pod 'SDCycleScrollView', :git=> "https://github.com/QiuYeHong90/SDCycleScrollView", :tag => "1.8.3" +### 支持pod导 的方案swift Kingfisher 模式依赖,默认依赖 +pod 'SDCycleScrollView', :subspecs => ["SwiftNOSD"], :git=> "https://github.com/QiuYeHong90/SDCycleScrollView", :tag => "1.8.3" ### 更改记录: diff --git a/SDCycleScrollView.podspec b/SDCycleScrollView.podspec index f535dc38..eb098539 100644 --- a/SDCycleScrollView.podspec +++ b/SDCycleScrollView.podspec @@ -10,19 +10,26 @@ s.license = "MIT" s.author = { "GSD_iOS" => "gsdios@126.com" } -s.platform = :ios -s.platform = :ios, "8.0" +s.platform = :ios +s.ios.deployment_target = "11.0" s.source = { :git => "https://github.com/gsdios/SDCycleScrollView.git", :tag => s.version} - -s.source_files = "SDCycleScrollView/Lib/SDCycleScrollView/**/*.{h,m}" +s.requires_arc = true +s.default_subspec = "Core" +s.swift_versions = ['5.0'] +s.subspec "Core" do |core| + core.source_files = "SDCycleScrollView/Lib/SDCycleScrollView/**/*.{h,m}" + core.dependency 'SDWebImage', '>= 5.0.0' +end +s.subspec "SwiftNOSD" do |core| + core.source_files = ["SDCycleScrollView/Lib/SDCycleScrollView/**/*.{h,m}","SDCycleScrollView/Lib/SDCycleScrollView/**/*.swift"] + core.dependency 'Kingfisher' +end -s.requires_arc = true -s.dependency 'SDWebImage', '>= 5.0.0' end diff --git a/SDCycleScrollView.xcodeproj/project.pbxproj b/SDCycleScrollView.xcodeproj/project.pbxproj index cebd06f0..19cabdb7 100644 --- a/SDCycleScrollView.xcodeproj/project.pbxproj +++ b/SDCycleScrollView.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 5F55240E74603C429F5220D2 /* Pods-SDCycleScrollView.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDCycleScrollView.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SDCycleScrollView/Pods-SDCycleScrollView.debug.xcconfig"; sourceTree = ""; }; 6BAA27FB1EFEA798008693AB /* CustomCollectionViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomCollectionViewCell.h; sourceTree = ""; }; 6BAA27FC1EFEA798008693AB /* CustomCollectionViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomCollectionViewCell.m; sourceTree = ""; }; + 980C9E1F2E0D460400B74484 /* SDCycleScrollView.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = SDCycleScrollView.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 9900D16E1ABEB2490077A6CB /* SDCycleScrollView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SDCycleScrollView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9900D1721ABEB2490077A6CB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9900D1731ABEB2490077A6CB /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -181,6 +182,7 @@ 9900D1711ABEB2490077A6CB /* Supporting Files */, 6BAA27FB1EFEA798008693AB /* CustomCollectionViewCell.h */, 6BAA27FC1EFEA798008693AB /* CustomCollectionViewCell.m */, + 980C9E1F2E0D460400B74484 /* SDCycleScrollView.podspec */, ); path = SDCycleScrollView; sourceTree = ""; @@ -316,6 +318,7 @@ TargetAttributes = { 9900D16D1ABEB2490077A6CB = { CreatedOnToolsVersion = 6.1; + DevelopmentTeam = YQ2BBNYL5U; ProvisioningStyle = Automatic; }; 9900D1861ABEB2490077A6CB = { @@ -329,6 +332,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -528,9 +532,9 @@ ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = YQ2BBNYL5U; INFOPLIST_FILE = SDCycleScrollView/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -546,9 +550,9 @@ ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = YQ2BBNYL5U; INFOPLIST_FILE = SDCycleScrollView/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 12; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -569,6 +573,7 @@ "$(inherited)", ); INFOPLIST_FILE = SDCycleScrollViewTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SDCycleScrollView.app/SDCycleScrollView"; @@ -584,6 +589,7 @@ "$(inherited)", ); INFOPLIST_FILE = SDCycleScrollViewTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SDCycleScrollView.app/SDCycleScrollView"; diff --git a/SDCycleScrollView.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist b/SDCycleScrollView.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..2523af60 --- /dev/null +++ b/SDCycleScrollView.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + SDCycleScrollView.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/SDCycleScrollView.xcworkspace/xcuserdata/york.xcuserdatad/UserInterfaceState.xcuserstate b/SDCycleScrollView.xcworkspace/xcuserdata/york.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..61ab2dcb Binary files /dev/null and b/SDCycleScrollView.xcworkspace/xcuserdata/york.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/SDCycleScrollView/.DS_Store b/SDCycleScrollView/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/SDCycleScrollView/.DS_Store differ diff --git a/SDCycleScrollView/Lib/.DS_Store b/SDCycleScrollView/Lib/.DS_Store new file mode 100644 index 00000000..8b4e78f6 Binary files /dev/null and b/SDCycleScrollView/Lib/.DS_Store differ diff --git a/SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.h b/SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.h index 817b2f6a..6be6872d 100644 --- a/SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.h +++ b/SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.h @@ -207,5 +207,5 @@ typedef enum { /** 清除图片缓存(兼容旧版本方法) */ - (void)clearCache; - +-(void)loadWithImageView: (UIImageView *)imageView url: (NSURL *)url; @end diff --git a/SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.m b/SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.m index 2ce019b9..3e659a4a 100644 --- a/SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.m +++ b/SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.m @@ -33,8 +33,14 @@ #import "SDCollectionViewCell.h" #import "UIView+SDExtension.h" #import "TAPageControl.h" + +#if __has_include() #import "SDWebImageManager.h" #import "UIImageView+WebCache.h" +#else +#import +#endif + #define kCycleScrollViewInitialPageControlDotSize CGSizeMake(10, 10) @@ -65,13 +71,23 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } -- (void)awakeFromNib +- (instancetype)initWithCoder:(NSCoder *)coder { - [super awakeFromNib]; - [self initialization]; - [self setupMainView]; + self = [super initWithCoder:coder]; + if (self) { + [self initialization]; + [self setupMainView]; + } + return self; } +//- (void)awakeFromNib +//{ +// [super awakeFromNib]; +// [self initialization]; +// [self setupMainView]; +//} + - (void)initialization { _pageControlAliment = SDCycleScrollViewPageContolAlimentCenter; @@ -477,7 +493,11 @@ - (void)clearCache + (void)clearImagesCache { + +#if __has_include() [[[SDWebImageManager sharedManager] imageCache] clearWithCacheType:SDImageCacheTypeDisk completion:nil]; +#else +#endif } #pragma mark - life circles @@ -593,7 +613,7 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell if (!self.onlyDisplayText && [imagePath isKindOfClass:[NSString class]]) { if ([imagePath hasPrefix:@"http"]) { - [cell.imageView sd_setImageWithURL:[NSURL URLWithString:imagePath] placeholderImage:self.placeholderImage]; + [self loadWithImageView:cell.imageView url:[NSURL URLWithString:imagePath]]; } else { UIImage *image = [UIImage imageNamed:imagePath]; if (!image) { @@ -624,6 +644,14 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell return cell; } +-(void)loadWithImageView: (UIImageView *)imageView url: (NSURL *)url { +#if __has_include() + [imageView sd_setImageWithURL:url placeholderImage:self.placeholderImage]; +#else + [imageView setYQLoadImage:url placeholderImage:self.placeholderImage]; +#endif +} + - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { if ([self.delegate respondsToSelector:@selector(cycleScrollView:didSelectItemAtIndex:)]) { diff --git a/SDCycleScrollView/Lib/SDCycleScrollView/SDSwiftCycleScrollView.swift b/SDCycleScrollView/Lib/SDCycleScrollView/SDSwiftCycleScrollView.swift new file mode 100644 index 00000000..5ff5ed9b --- /dev/null +++ b/SDCycleScrollView/Lib/SDCycleScrollView/SDSwiftCycleScrollView.swift @@ -0,0 +1,16 @@ +// +// SDSwiftCycleScrollView.swift +// SDCycleScrollView +// +// Created by york on 2025/6/26. +// + +import UIKit + +public class SDSwiftCycleScrollView: SDCycleScrollView { + + public override func load(with imageView: UIImageView!, url: URL!) { + imageView.setYQLoadImage(url, placeholderImage: self.placeholderImage) + } + +} diff --git a/SDCycleScrollView/Lib/SDCycleScrollView/UIImageView+KF.swift b/SDCycleScrollView/Lib/SDCycleScrollView/UIImageView+KF.swift new file mode 100644 index 00000000..e1c4d0ca --- /dev/null +++ b/SDCycleScrollView/Lib/SDCycleScrollView/UIImageView+KF.swift @@ -0,0 +1,15 @@ +// +// UIImageView+KF.swift +// SDCycleScrollView +// +// Created by york on 2025/6/26. +// + +import UIKit +import Kingfisher + +public extension UIImageView { + @objc func setYQLoadImage(_ url: URL?, placeholderImage: UIImage?) { + self.kf.setImage(with: url,placeholder: placeholderImage) + } +} diff --git a/SwiftDemo/.DS_Store b/SwiftDemo/.DS_Store new file mode 100644 index 00000000..c278be95 Binary files /dev/null and b/SwiftDemo/.DS_Store differ diff --git a/SwiftDemo/SDCycleSrollViewDemo/.DS_Store b/SwiftDemo/SDCycleSrollViewDemo/.DS_Store new file mode 100644 index 00000000..7f2dcd40 Binary files /dev/null and b/SwiftDemo/SDCycleSrollViewDemo/.DS_Store differ diff --git a/SwiftDemo/SDCycleSrollViewDemo/Podfile b/SwiftDemo/SDCycleSrollViewDemo/Podfile new file mode 100644 index 00000000..8a0e6682 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Podfile @@ -0,0 +1,45 @@ +# Uncomment the next line to define a global platform for your project + platform :ios, '11.0' + +target 'SDCycleSrollViewDemo' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + pod "SDCycleScrollView", :subspecs => ["SwiftNOSD"], :path => "../../" + # Pods for SDCycleSrollViewDemo +end + + + +post_install do |installer| + + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + config.build_settings['ENABLE_BITCODE'] = 'NO' + end + + + end + + installer.pods_project.build_configurations.each do |config| + config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" + config.build_settings['CODE_SIGN_IDENTITY'] = '' + end + + + bitcode_strip_path = `xcrun --find bitcode_strip`.chop! + def strip_bitcode_from_framework(bitcode_strip_path, framework_relative_path) + framework_path = File.join(Dir.pwd, framework_relative_path) + command = "#{bitcode_strip_path} #{framework_path} -r -o #{framework_path}" + puts "Stripping bitcode: #{command}" + system(command) + end + + framework_paths = [ + + ] + + framework_paths.each do |framework_relative_path| + strip_bitcode_from_framework(bitcode_strip_path, framework_relative_path) + end +end diff --git a/SwiftDemo/SDCycleSrollViewDemo/Podfile.lock b/SwiftDemo/SDCycleSrollViewDemo/Podfile.lock new file mode 100644 index 00000000..f89365ea --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - Kingfisher (8.3.3) + - SDCycleScrollView/SwiftNOSD (1.82): + - Kingfisher + +DEPENDENCIES: + - SDCycleScrollView/SwiftNOSD (from `../../`) + +SPEC REPOS: + trunk: + - Kingfisher + +EXTERNAL SOURCES: + SDCycleScrollView: + :path: "../../" + +SPEC CHECKSUMS: + Kingfisher: ff82cb91d9266ddb56cbb2f72d32c26f00d3e5be + SDCycleScrollView: 357d9df5ad77352f6e36b9c94ab8e34f0eba1513 + +PODFILE CHECKSUM: 43fcfd2a94ca076de4190913c33da62fffa2a25f + +COCOAPODS: 1.16.2 diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/LICENSE b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/LICENSE new file mode 100644 index 00000000..80888ba5 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +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. + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/README-LLM.md b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/README-LLM.md new file mode 100644 index 00000000..b3c21885 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/README-LLM.md @@ -0,0 +1,55 @@ + + +# Kingfisher + +Kingfisher is a powerful, pure-Swift library for downloading and caching images from the web, providing elegant async APIs for iOS, macOS, tvOS, watchOS, and visionOS applications. The library handles the complete image lifecycle with multi-layer caching, built-in processing, and extensive UI component integrations. + +## Quick Start + +**Core API Entry Points:** +- `Sources/General/KingfisherManager.swift` - Central coordinator +- `Sources/General/KF.swift` - Builder pattern API (`KF.url()...`) +- `Sources/Extensions/ImageView+Kingfisher.swift` - UIKit/AppKit extensions +- `Sources/SwiftUI/KFImage.swift` - SwiftUI components + +**Essential Build Commands:** +```bash +# Install dependencies and run all tests +bundle install && bundle exec fastlane tests + +# Build for specific platform +swift build + +# Full release workflow +bundle exec fastlane release version:X.X.X +``` + +## Documentation + +**For LLMs and Developers:** + +- **[Project Overview](docs/project-overview.md)** - What Kingfisher does, core purpose, technology stack, and platform support +- **[Architecture](docs/architecture.md)** - System organization, component map, key files, and data flow with specific file references +- **[Build System](docs/build-system.md)** - Swift Package Manager and Fastlane workflows, platform setup, and troubleshooting +- **[Testing](docs/testing.md)** - Test categories, running tests, and test infrastructure with file locations +- **[Development](docs/development.md)** - Code style, implementation patterns, workflows, and common solutions +- **[Deployment](docs/deployment.md)** - Package types, platform deployment, release management, and CI/CD +- **[File Catalog](docs/files.md)** - Comprehensive file organization with specific file purposes and relationships + +**Configuration Files:** +- `Package.swift` - Swift Package Manager manifest +- `Kingfisher.podspec` - CocoaPods specification +- `fastlane/Fastfile` - Build automation +- `Sources/Documentation.docc/` - DocC documentation + +**Key Patterns:** +- Namespace wrapper (`.kf` property) in `Sources/General/Kingfisher.swift` +- Builder pattern API in `Sources/General/KF.swift` +- Options system in `Sources/General/KingfisherOptionsInfo.swift` +- Protocol-oriented design throughout `Sources/Image/ImageProcessor.swift` + +## Requirements + +- **Swift 5.9+** (Swift 6 strict concurrency ready) +- **iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ / visionOS 1.0+** +- **SwiftUI support**: iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ / visionOS 1.0+ \ No newline at end of file diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift new file mode 100644 index 00000000..1fd88e64 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift @@ -0,0 +1,122 @@ +// +// CacheSerializer.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/02. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +import CoreGraphics +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A `CacheSerializer` is used to convert some data to an image object after retrieving it from disk storage, +/// and vice versa, to convert an image to a data object for storing it to the disk storage. +public protocol CacheSerializer: Sendable { + + /// Retrieves the serialized data from a provided image and optional original data for caching to disk. + /// + /// - Parameters: + /// - image: The image to be serialized. + /// - original: The original data that was just downloaded. + /// If the image is retrieved from the cache instead of being downloaded, it will be `nil`. + /// - Returns: The data object for storing to disk, or `nil` when no valid data can be serialized. + func data(with image: KFCrossPlatformImage, original: Data?) -> Data? + + /// Retrieves an image from the provided serialized data. + /// + /// - Parameters: + /// - data: The data from which an image should be deserialized. + /// - options: The parsed options for deserialization. + /// - Returns: A deserialized image, or `nil` when no valid image can be deserialized. + func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? + + /// Indicates whether this serializer prefers to cache the original data in its implementation. + /// + /// If `true`, during storing phase, the original data is preferred to be stored to the disk if exists. When + /// retrieving image from the disk cache, after creating the image from the loaded data, Kingfisher will continue + /// to apply the processor to get the final image. + /// + /// By default, it is `false`, and the actual processed image is assumed to be serialized to and later deserialized + /// from the disk. That means the processed version of the image is stored and loaded. + var originalDataUsed: Bool { get } +} + +public extension CacheSerializer { + var originalDataUsed: Bool { false } +} + +/// Represents a basic and default `CacheSerializer` used in the Kingfisher disk cache system. +/// +/// It can serialize and deserialize images in PNG, JPEG, and GIF formats. For images other than these formats, a +/// normalized ``KingfisherWrapper/pngRepresentation()`` will be used. +/// +/// When converting an `image` to the date, it will only be converted to the corresponding data type when `original` +/// contains valid PNG, JPEG, and GIF format data. If the `original` is provided but not valid, or if `original` is +/// `nil`, the input `image` will be encoded as PNG data. +public struct DefaultCacheSerializer: CacheSerializer { + + /// The default general cache serializer utilized throughout Kingfisher's caching mechanism. + public static let `default` = DefaultCacheSerializer() + + /// The compression quality used when converting an image to lossy format data (such as JPEG). + /// + /// Default is 1.0. + public var compressionQuality: CGFloat = 1.0 + + /// Determines whether the original data should be prioritized during image serialization. + /// + /// If set to `true`, the original input data will be initially inspected and used, unless the data is `nil`. + /// In the event of a `nil` data, the serialization process will revert to generating data from the image. + /// + /// > This value is used as ``CacheSerializer/originalDataUsed-d2v9``. + public var preferCacheOriginalData: Bool = false + + public var originalDataUsed: Bool { preferCacheOriginalData } + + /// Creates a cache serializer that serializes and deserializes images in PNG, JPEG, and GIF formats. + /// + /// > Prefer to use the ``DefaultCacheSerializer/default`` value unless you need to specify your own properties. + public init() { } + + public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { + if preferCacheOriginalData { + return original ?? + image.kf.data( + format: original?.kf.imageFormat ?? .unknown, + compressionQuality: compressionQuality + ) + } else { + return image.kf.data( + format: original?.kf.imageFormat ?? .unknown, + compressionQuality: compressionQuality + ) + } + } + + public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/DiskStorage.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/DiskStorage.swift new file mode 100644 index 00000000..8e6e277c --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/DiskStorage.swift @@ -0,0 +1,681 @@ +// +// DiskStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + + +/// Represents the concepts related to storage that stores a specific type of value in disk. +/// +/// This serves as a namespace for memory storage types. A ``DiskStorage/Backend`` with a particular +/// ``DiskStorage/Config`` is used to define the storage. +/// +/// Refer to these composite types for further details. +public enum DiskStorage { + + /// Represents a storage backend for the ``DiskStorage``. + /// + /// The value is serialized to binary data and stored as a file in the file system under a specified location. + /// + /// You can configure a ``DiskStorage/Backend`` in its ``DiskStorage/Backend/init(config:)`` by passing a + /// ``DiskStorage/Config`` value or by modifying the ``DiskStorage/Backend/config`` property after it has been + /// created. The ``DiskStorage/Backend`` will use the file's attributes to keep track of a file for its expiration + /// or size limitation. + public class Backend: @unchecked Sendable { + + private let propertyQueue = DispatchQueue(label: "com.onevcat.kingfisher.DiskStorage.Backend.propertyQueue") + + private var _config: Config + /// The configuration used for this disk storage. + /// + /// It is a value you can set and use to configure the storage as needed. + public var config: Config { + get { propertyQueue.sync { _config } } + set { propertyQueue.sync { _config = newValue } } + } + + /// The final storage URL on disk of the disk storage ``DiskStorage/Backend``, considering the + /// ``DiskStorage/Config/name`` and the ``DiskStorage/Config/cachePathBlock``. + public let directoryURL: URL + + let metaChangingQueue: DispatchQueue + + // A shortcut (which contains false-positive) to improve matching performance. + var maybeCached : Set? + let maybeCachedCheckingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.maybeCachedCheckingQueue") + + // `false` if the storage initialized with an error. + // This prevents unexpected forcibly crash when creating storage in the default cache. + private var storageReady: Bool = true + + /// Creates a disk storage with the given ``DiskStorage/Config``. + /// + /// - Parameter config: The configuration used for this disk storage. + /// - Throws: An error if the folder for storage cannot be obtained or created. + public convenience init(config: Config) throws { + self.init(noThrowConfig: config, creatingDirectory: false) + try prepareDirectory() + } + + // If `creatingDirectory` is `false`, the directory preparation will be skipped. + // We need to call `prepareDirectory` manually after this returns. + init(noThrowConfig config: Config, creatingDirectory: Bool) { + var config = config + + let creation = Creation(config) + self.directoryURL = creation.directoryURL + + // Break any possible retain cycle set by outside. + config.cachePathBlock = nil + _config = config + + metaChangingQueue = DispatchQueue(label: creation.cacheName) + setupCacheChecking() + + if creatingDirectory { + try? prepareDirectory() + } + } + + private func setupCacheChecking() { + DispatchQueue.global(qos: .default).async { + do { + let allFiles = try self.config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path) + let maybeCached = Set(allFiles) + self.maybeCachedCheckingQueue.async { + self.maybeCached = maybeCached + } + } catch { + self.maybeCachedCheckingQueue.async { + // Just disable the functionality if we fail to initialize it properly. This will just revert to + // the behavior which is to check file existence on disk directly. + self.maybeCached = nil + } + } + } + } + + // Creates the storage folder. + private func prepareDirectory() throws { + let fileManager = config.fileManager + let path = directoryURL.path + + guard !fileManager.fileExists(atPath: path) else { return } + + do { + try fileManager.createDirectory( + atPath: path, + withIntermediateDirectories: true, + attributes: nil) + } catch { + self.storageReady = false + throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error)) + } + } + + /// Stores a value in the storage under the specified key and expiration policy. + /// + /// - Parameters: + /// - value: The value to be stored. + /// - key: The key to which the `value` will be stored. If there is already a value under the key, the old + /// value will be overwritten by the new `value`. + /// - expiration: The expiration policy used by this storage action. + /// - writeOptions: Data writing options used for the new files. + /// - forcedExtension: The file extension, if exists. + /// - Throws: An error during converting the value to a data format or during writing it to disk. + public func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil, + writeOptions: Data.WritingOptions = [], + forcedExtension: String? = nil + ) throws + { + guard storageReady else { + throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) + } + + let expiration = expiration ?? config.expiration + // The expiration indicates that already expired, no need to store. + guard !expiration.isExpired else { return } + + let data: Data + do { + data = try value.toData() + } catch { + throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error)) + } + + let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension) + do { + try data.write(to: fileURL, options: writeOptions) + } catch { + if error.isFolderMissing { + // The whole cache folder is deleted. Try to recreate it and write file again. + do { + try prepareDirectory() + try data.write(to: fileURL, options: writeOptions) + } catch { + throw KingfisherError.cacheError( + reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error) + ) + } + } else { + throw KingfisherError.cacheError( + reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error) + ) + } + } + + let now = Date() + let attributes: [FileAttributeKey : Any] = [ + // The last access date. + .creationDate: now.fileAttributeDate, + // The estimated expiration date. + .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate + ] + do { + try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path) + } catch { + try? config.fileManager.removeItem(at: fileURL) + throw KingfisherError.cacheError( + reason: .cannotSetCacheFileAttribute( + filePath: fileURL.path, + attributes: attributes, + error: error + ) + ) + } + + maybeCachedCheckingQueue.async { + self.maybeCached?.insert(fileURL.lastPathComponent) + } + } + + /// Retrieves a value from the storage. + /// - Parameters: + /// - key: The cache key of the value. + /// - forcedExtension: The file extension, if exists. + /// - extendingExpiration: The expiration policy used by this retrieval action. + /// - Throws: An error during converting the data to a value or during the operation of disk files. + /// - Returns: The value under `key` if it is valid and found in the storage; otherwise, `nil`. + public func value( + forKey key: String, + forcedExtension: String? = nil, + extendingExpiration: ExpirationExtending = .cacheTime + ) throws -> T? { + try value( + forKey: key, + referenceDate: Date(), + actuallyLoad: true, + extendingExpiration: extendingExpiration, + forcedExtension: forcedExtension + ) + } + + func value( + forKey key: String, + referenceDate: Date, + actuallyLoad: Bool, + extendingExpiration: ExpirationExtending, + forcedExtension: String? + ) throws -> T? + { + guard storageReady else { + throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) + } + + let fileManager = config.fileManager + let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension) + let filePath = fileURL.path + + let fileMaybeCached = maybeCachedCheckingQueue.sync { + return maybeCached?.contains(fileURL.lastPathComponent) ?? true + } + guard fileMaybeCached else { + return nil + } + guard fileManager.fileExists(atPath: filePath) else { + return nil + } + + let meta: FileMeta + do { + let resourceKeys: Set = [.contentModificationDateKey, .creationDateKey] + meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys) + } catch { + throw KingfisherError.cacheError( + reason: .invalidURLResource(error: error, key: key, url: fileURL)) + } + + if meta.expired(referenceDate: referenceDate) { + return nil + } + if !actuallyLoad { return T.empty } + + do { + let data = try Data(contentsOf: fileURL) + let obj = try T.fromData(data) + metaChangingQueue.async { + meta.extendExpiration(with: self.config.fileManager, extendingExpiration: extendingExpiration) + } + return obj + } catch { + throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error)) + } + } + + /// Determines whether there is valid cached data under a given key. + /// + /// - Parameters: + /// - key: The cache key of the value. + /// - forcedExtension: The file extension, if exists. + /// - Returns: `true` if there is valid data under the key and file extension; otherwise, `false`. + /// + /// > This method does not actually load the data from disk, so it is faster than directly loading the cached + /// value by checking the nullability of the + /// ``DiskStorage/Backend/value(forKey:forcedExtension:extendingExpiration:)`` method. + public func isCached(forKey key: String, forcedExtension: String? = nil) -> Bool { + return isCached(forKey: key, referenceDate: Date(), forcedExtension: forcedExtension) + } + + /// Determines whether there is valid cached data under a given key and a reference date. + /// + /// - Parameters: + /// - key: The cache key of the value. + /// - referenceDate: A reference date to check whether the cache is still valid. + /// - forcedExtension: The file extension, if exists. + /// + /// - Returns: `true` if there is valid data under the key; otherwise, `false`. + /// + /// If you pass `Date()` as the `referenceDate`, this method is identical to + /// ``DiskStorage/Backend/isCached(forKey:forcedExtension:)``. Use the `referenceDate` to determine whether the + /// cache is still valid for a future date. + public func isCached(forKey key: String, referenceDate: Date, forcedExtension: String? = nil) -> Bool { + do { + let result = try value( + forKey: key, + referenceDate: referenceDate, + actuallyLoad: false, + extendingExpiration: .none, + forcedExtension: forcedExtension + ) + return result != nil + } catch { + return false + } + } + + /// Removes a value from a specified key. + /// - Parameters: + /// - key: The cache key of the value. + /// - forcedExtension: The file extension, if exists. + /// - Throws: An error during the removal of the value. + public func remove(forKey key: String, forcedExtension: String? = nil) throws { + let fileURL = cacheFileURL(forKey: key, forcedExtension: forcedExtension) + try removeFile(at: fileURL) + } + + func removeFile(at url: URL) throws { + try config.fileManager.removeItem(at: url) + } + + /// Removes all values in this storage. + /// - Throws: An error during the removal of the values. + public func removeAll() throws { + try removeAll(skipCreatingDirectory: false) + } + + func removeAll(skipCreatingDirectory: Bool) throws { + try config.fileManager.removeItem(at: directoryURL) + if !skipCreatingDirectory { + try prepareDirectory() + } + } + + /// The URL of the cached file with a given computed `key`. + /// - Parameters: + /// - key: The final computed key used when caching the image. Please note that usually this is not + /// the ``Source/cacheKey`` of an image ``Source``. It is the computed key with the processor identifier + /// considered. + /// - forcedExtension: The file extension, if exists. + /// - Returns: The expected file URL on the disk based on the `key` and the `forcedExtension`. + /// + /// This method does not guarantee that an image is already cached at the returned URL. It just provides the URL + /// where the image should be if it exists in the disk storage, with the given key and file extension. + /// + public func cacheFileURL(forKey key: String, forcedExtension: String? = nil) -> URL { + let fileName = cacheFileName(forKey: key, forcedExtension: forcedExtension) + return directoryURL.appendingPathComponent(fileName, isDirectory: false) + } + + func cacheFileName(forKey key: String, forcedExtension: String? = nil) -> String { + let baseName = config.usesHashedFileName ? key.kf.sha256 : key + + if let ext = fileExtension(key: key, forcedExtension: forcedExtension) { + return "\(baseName).\(ext)" + } + + return baseName + } + + func fileExtension(key: String, forcedExtension: String?) -> String? { + if let ext = forcedExtension ?? config.pathExtension { + return ext + } + + if config.usesHashedFileName && config.autoExtAfterHashedFileName { + return key.kf.ext + } + + return nil + } + + func allFileURLs(for propertyKeys: [URLResourceKey]) throws -> [URL] { + let fileManager = config.fileManager + + guard let directoryEnumerator = fileManager.enumerator( + at: directoryURL, includingPropertiesForKeys: propertyKeys, options: .skipsHiddenFiles) else + { + throw KingfisherError.cacheError(reason: .fileEnumeratorCreationFailed(url: directoryURL)) + } + + guard let urls = directoryEnumerator.allObjects as? [URL] else { + throw KingfisherError.cacheError(reason: .invalidFileEnumeratorContent(url: directoryURL)) + } + return urls + } + + /// Removes all expired values from this storage. + /// - Throws: A file manager error during the removal of the file. + /// - Returns: The URLs for the removed files. + public func removeExpiredValues() throws -> [URL] { + return try removeExpiredValues(referenceDate: Date()) + } + + func removeExpiredValues(referenceDate: Date) throws -> [URL] { + let propertyKeys: [URLResourceKey] = [ + .isDirectoryKey, + .contentModificationDateKey + ] + + let urls = try allFileURLs(for: propertyKeys) + let keys = Set(propertyKeys) + let expiredFiles = urls.filter { fileURL in + do { + let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) + if meta.isDirectory { + return false + } + return meta.expired(referenceDate: referenceDate) + } catch { + return true + } + } + try expiredFiles.forEach { url in + try removeFile(at: url) + } + return expiredFiles + } + + /// Removes all size-exceeded values from this storage. + /// - Throws: A file manager error during the removal of the file. + /// - Returns: The URLs for the removed files. + /// + /// This method checks ``DiskStorage/Config/sizeLimit`` and removes cached files in an LRU + /// (Least Recently Used) way. + public func removeSizeExceededValues() throws -> [URL] { + + if config.sizeLimit == 0 { return [] } // Back compatible. 0 means no limit. + + var size = try totalSize() + if size < config.sizeLimit { return [] } + + let propertyKeys: [URLResourceKey] = [ + .isDirectoryKey, + .creationDateKey, + .fileSizeKey + ] + let keys = Set(propertyKeys) + + let urls = try allFileURLs(for: propertyKeys) + var pendings: [FileMeta] = urls.compactMap { fileURL in + guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else { + return nil + } + return meta + } + // Sort by last access date. Most recent file first. + pendings.sort(by: FileMeta.lastAccessDate) + + var removed: [URL] = [] + let target = config.sizeLimit / 2 + while size > target, let meta = pendings.popLast() { + size -= UInt(meta.fileSize) + try removeFile(at: meta.url) + removed.append(meta.url) + } + return removed + } + + /// Gets the total file size of the cache folder in bytes. + public func totalSize() throws -> UInt { + let propertyKeys: [URLResourceKey] = [.fileSizeKey] + let urls = try allFileURLs(for: propertyKeys) + let keys = Set(propertyKeys) + let totalSize: UInt = urls.reduce(0) { size, fileURL in + do { + let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) + return size + UInt(meta.fileSize) + } catch { + return size + } + } + return totalSize + } + } +} + +extension DiskStorage { + + /// Represents the configuration used in a ``DiskStorage/Backend``. + public struct Config: @unchecked Sendable { + + /// The file size limit on disk of the storage in bytes. + /// + /// `0` means no limit. + public var sizeLimit: UInt + + /// The `StorageExpiration` used in this disk storage. + /// + /// The default is `.days(7)`, which means that the disk cache will expire in one week if not accessed anymore. + public var expiration: StorageExpiration = .days(7) + + /// The preferred extension of the cache item. It will be appended to the file name as its extension. + /// + /// The default is `nil`, which means that the cache file does not contain a file extension. + public var pathExtension: String? = nil + + /// Whether the cache file name will be hashed before storing. + /// + /// The default is `true`, which means that file name is hashed to protect user information (for example, the + /// original download URL which is used as the cache key). + public var usesHashedFileName = true + + + /// Whether the image extension will be extracted from the original file name and appended to the hashed file + /// name, which will be used as the cache key on disk. + /// + /// The default is `false`. + public var autoExtAfterHashedFileName = false + + /// A closure that takes in the initial directory path and generates the final disk cache path. + /// + /// You can use it to fully customize your cache path. + public var cachePathBlock: (@Sendable (_ directory: URL, _ cacheName: String) -> URL)! = { + (directory, cacheName) in + return directory.appendingPathComponent(cacheName, isDirectory: true) + } + + /// The desired name of the disk cache. + /// + /// This name will be used as a part of the cache folder name by default. + public let name: String + + let fileManager: FileManager + let directory: URL? + + /// Creates a config value based on the given parameters. + /// + /// - Parameters: + /// - name: The name of the cache. It is used as part of the storage folder and to identify the disk storage. + /// Two storages with the same `name` would share the same folder on the disk, and this should be prevented. + /// - sizeLimit: The size limit in bytes for all existing files in the disk storage. + /// - fileManager: The `FileManager` used to manipulate files on the disk. The default is `FileManager.default`. + /// - directory: The URL where the disk storage should reside. The storage will use this as the root folder, + /// and append a path that is constructed by the input `name`. The default is `nil`, indicating that + /// the cache directory under the user domain mask will be used. + public init( + name: String, + sizeLimit: UInt, + fileManager: FileManager = .default, + directory: URL? = nil) + { + self.name = name + self.fileManager = fileManager + self.directory = directory + self.sizeLimit = sizeLimit + } + } +} + +extension DiskStorage { + struct FileMeta { + + let url: URL + + let lastAccessDate: Date? + let estimatedExpirationDate: Date? + let isDirectory: Bool + let fileSize: Int + + static func lastAccessDate(lhs: FileMeta, rhs: FileMeta) -> Bool { + return lhs.lastAccessDate ?? .distantPast > rhs.lastAccessDate ?? .distantPast + } + + init(fileURL: URL, resourceKeys: Set) throws { + let meta = try fileURL.resourceValues(forKeys: resourceKeys) + self.init( + fileURL: fileURL, + lastAccessDate: meta.creationDate, + estimatedExpirationDate: meta.contentModificationDate, + isDirectory: meta.isDirectory ?? false, + fileSize: meta.fileSize ?? 0) + } + + init( + fileURL: URL, + lastAccessDate: Date?, + estimatedExpirationDate: Date?, + isDirectory: Bool, + fileSize: Int) + { + self.url = fileURL + self.lastAccessDate = lastAccessDate + self.estimatedExpirationDate = estimatedExpirationDate + self.isDirectory = isDirectory + self.fileSize = fileSize + } + + func expired(referenceDate: Date) -> Bool { + return estimatedExpirationDate?.isPast(referenceDate: referenceDate) ?? true + } + + func extendExpiration(with fileManager: FileManager, extendingExpiration: ExpirationExtending) { + guard let lastAccessDate = lastAccessDate, + let lastEstimatedExpiration = estimatedExpirationDate else + { + return + } + + let attributes: [FileAttributeKey : Any] + + switch extendingExpiration { + case .none: + // not extending expiration time here + return + case .cacheTime: + let originalExpiration: StorageExpiration = + .seconds(lastEstimatedExpiration.timeIntervalSince(lastAccessDate)) + attributes = [ + .creationDate: Date().fileAttributeDate, + .modificationDate: originalExpiration.estimatedExpirationSinceNow.fileAttributeDate + ] + case .expirationTime(let expirationTime): + attributes = [ + .creationDate: Date().fileAttributeDate, + .modificationDate: expirationTime.estimatedExpirationSinceNow.fileAttributeDate + ] + } + + try? fileManager.setAttributes(attributes, ofItemAtPath: url.path) + } + } +} + +extension DiskStorage { + struct Creation { + let directoryURL: URL + let cacheName: String + + init(_ config: Config) { + let url: URL + if let directory = config.directory { + url = directory + } else { + url = config.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] + } + + cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)" + directoryURL = config.cachePathBlock(url, cacheName) + } + } +} + +fileprivate extension Error { + var isFolderMissing: Bool { + let nsError = self as NSError + guard nsError.domain == NSCocoaErrorDomain, nsError.code == 4 else { + return false + } + guard let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError else { + return false + } + guard underlyingError.domain == NSPOSIXErrorDomain, underlyingError.code == 2 else { + return false + } + return true + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift new file mode 100644 index 00000000..3ff198fb --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift @@ -0,0 +1,138 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Junyu Kuang on 5/28/17. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +import CoreGraphics +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// The ``FormatIndicatedCacheSerializer`` enables you to specify an image format for serialized caches. +/// +/// It can serialize and deserialize PNG, JPEG, and GIF images. For images other than these formats, a normalized +/// ``KingfisherWrapper/pngRepresentation()`` will be used. +/// +/// **Example:** +/// +/// ```swift +/// let profileImageSize = CGSize(width: 44, height: 44) +/// +/// // A round corner image. +/// let imageProcessor = RoundCornerImageProcessor( +/// cornerRadius: profileImageSize.width / 2, targetSize: profileImageSize) +/// +/// let optionsInfo: KingfisherOptionsInfo = [ +/// .cacheSerializer(FormatIndicatedCacheSerializer.png), +/// .processor(imageProcessor) +/// ] +/// +/// // A URL pointing to a JPEG image. +/// let url = URL(string: "https://example.com/image.jpg")! +/// +/// // The image will always be cached as PNG format to preserve the alpha channel for the round rectangle. +/// // When you load it from the cache later, it will still be round cornered. +/// // Otherwise, the corner part would be filled by a white color (since JPEG does not contain an alpha channel). +/// imageView.kf.setImage(with: url, options: optionsInfo) +/// ``` +public struct FormatIndicatedCacheSerializer: CacheSerializer { + + /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the PNG format. + /// + /// If the image cannot be represented in the PNG format, it will fallback to its actual format determined by the + /// `original` data in ``CacheSerializer/data(with:original:)``. + public static let png = FormatIndicatedCacheSerializer(imageFormat: .PNG, jpegCompressionQuality: nil) + + /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format. If the image cannot be + /// represented by JPEG format, it will fallback to its real format which is determined by `original` data. + /// The compression quality is 1.0 when using this serializer. If you need to set a customized compression quality, + /// use `jpeg(compressionQuality:)`. + /// + + /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the JPEG format. + /// + /// If the image cannot be represented in the JPEG format, it will fallback to its actual format determined by the + /// `original` data in ``CacheSerializer/data(with:original:)``. + /// + /// > The compression quality is 1.0 when using this serializer. To set a customized compression quality, + /// use ``FormatIndicatedCacheSerializer/jpeg(compressionQuality:)``. + public static let jpeg = FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: 1.0) + + /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the JPEG format. + /// + /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. + /// + /// If the image cannot be represented in the JPEG format, it will fallback to its actual format determined by the + /// `original` data in ``CacheSerializer/data(with:original:)``. + public static func jpeg(compressionQuality: CGFloat) -> FormatIndicatedCacheSerializer { + return FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: compressionQuality) + } + + /// A ``FormatIndicatedCacheSerializer`` instance that converts images to and from the GIF format. + /// + /// If the image cannot be represented in the GIF format, it will fallback to its actual format determined by the + /// `original` data in ``CacheSerializer/data(with:original:)``. + public static let gif = FormatIndicatedCacheSerializer(imageFormat: .GIF, jpegCompressionQuality: nil) + + // The specified image format. + private let imageFormat: ImageFormat + + // The compression quality used for lossy image formats (like JPEG). + private let jpegCompressionQuality: CGFloat? + + public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { + + func imageData(withFormat imageFormat: ImageFormat) -> Data? { + return autoreleasepool { () -> Data? in + switch imageFormat { + case .PNG: return image.kf.pngRepresentation() + case .JPEG: return image.kf.jpegRepresentation(compressionQuality: jpegCompressionQuality ?? 1.0) + case .GIF: return image.kf.gifRepresentation() + case .unknown: return nil + } + } + } + + // generate data with indicated image format + if let data = imageData(withFormat: imageFormat) { + return data + } + + let originalFormat = original?.kf.imageFormat ?? .unknown + + // generate data with original image's format + if originalFormat != imageFormat, let data = imageData(withFormat: originalFormat) { + return data + } + + return original ?? image.kf.normalized.kf.pngRepresentation() + } + + public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/ImageCache.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/ImageCache.swift new file mode 100644 index 00000000..182ad4ee --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/ImageCache.swift @@ -0,0 +1,1308 @@ +// +// ImageCache.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension Notification.Name { + + /// This notification is sent when the disk cache is cleared, either due to expired cached files or the total size + /// exceeding the maximum allowed size. + /// + /// The `object` of this notification is the ``ImageCache`` object that sends the notification. You can retrieve a + /// list of removed hashes (files) by accessing the array under the ``KingfisherDiskCacheCleanedHashKey`` key in + /// the `userInfo` of the received notification object. By checking the array, you can determine the hash codes + /// of the removed files. + /// + /// > Invoking the `clearDiskCache` method manually will not trigger this notification. + public static let KingfisherDidCleanDiskCache = + Notification.Name("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache") +} + +/// Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCache` notification. +public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash" + +/// The type of cache for a cached image. +public enum CacheType: Sendable { + /// The image is not yet cached when retrieving it. + /// + /// This indicates that the image was recently downloaded or generated rather than being retrieved from either + /// memory or disk cache. + case none + + /// The image is cached in memory and retrieved from there. + case memory + + /// The image is cached in disk and retrieved from there. + case disk + + /// Indicates whether the cache type represents the image is already cached or not. + public var cached: Bool { + switch self { + case .memory, .disk: return true + case .none: return false + } + } +} + +/// Represents the result of the caching operation. +public struct CacheStoreResult: Sendable { + + /// The caching result for memory cache. + /// + /// Caching an image to memory will never fail. + public let memoryCacheResult: Result<(), Never> + + /// The caching result for disk cache. + /// + /// If an error occurs during the caching operation, you can retrieve it from the `.failure` case of this value. + /// Usually, the error contains a ``KingfisherError/CacheErrorReason``. + public let diskCacheResult: Result<(), KingfisherError> +} + +extension KFCrossPlatformImage: CacheCostCalculable { + /// The cost of an image. + /// + /// It is an estimated size represented as a bitmap, measured in bytes of all pixels. A larger cost indicates that + /// when cached in memory, it occupies more memory space. This cost contributes to the + /// ``MemoryStorage/Config/countLimit``. + public var cacheCost: Int { return kf.cost } +} + +extension Data: DataTransformable { + public func toData() throws -> Data { + self + } + + public static func fromData(_ data: Data) throws -> Data { + data + } + + public static let empty = Data() +} + + +/// Represents the result of the operation to retrieve an image from the cache. +public enum ImageCacheResult: Sendable { + + /// The image can be retrieved from the disk cache. + case disk(KFCrossPlatformImage) + + /// The image can be retrieved from the memory cache. + case memory(KFCrossPlatformImage) + + /// The image does not exist in the cache. + case none + + /// Extracts the image from cache result. + /// + /// It returns the associated `Image` value for ``ImageCacheResult/disk(_:)`` and ``ImageCacheResult/memory(_:)`` + /// case. For ``ImageCacheResult/none`` case, returns `nil`. + public var image: KFCrossPlatformImage? { + switch self { + case .disk(let image): return image + case .memory(let image): return image + case .none: return nil + } + } + + /// Returns the corresponding ``CacheType`` value based on the result type of `self`. + public var cacheType: CacheType { + switch self { + case .disk: return .disk + case .memory: return .memory + case .none: return .none + } + } +} + +/// Represents a hybrid caching system composed of a ``MemoryStorage`` and a ``DiskStorage``. +/// +/// ``ImageCache`` serves as a high-level abstraction for storing an image and its data in memory and on disk, as well +/// as retrieving them. You can define configurations for the memory cache backend and disk cache backend, and the the +/// unified methods to store images to the cache or retrieve images from either the memory cache or the disk cache. +/// +/// > While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create +/// your own cache object and configure its storages according to your needs. This class also provides an interface for +/// configuring the memory and disk storage. +open class ImageCache: @unchecked Sendable { + + // MARK: Singleton + /// The default ``ImageCache`` object. + /// + /// Kingfisher uses this value for its related methods if no other cache is specified. + /// + /// > Warning: The `name` of this default cache is reserved as "default", and you should not use this name for any + /// of your custom caches. Otherwise, different caches might become mixed up and corrupted. + public static let `default` = ImageCache(name: "default") + + // MARK: Public Properties + /// The ``MemoryStorage/Backend`` object for the memory cache used in this cache. + /// + /// This storage stores loaded images in memory with a reasonable expire duration and a maximum memory usage. + /// + /// > To modify the configuration of a storage, just set the storage ``MemoryStorage/Config`` and its properties. + public let memoryStorage: MemoryStorage.Backend + + /// The ``DiskStorage/Backend`` object for the disk cache used in this cache. + /// + /// This storage stores loaded images on disk with a reasonable expire duration and a maximum disk usage. + /// + /// > To modify the configuration of a storage, just set the storage ``DiskStorage/Config`` and its properties. + public let diskStorage: DiskStorage.Backend + + private let ioQueue: DispatchQueue + + /// A closure that specifies the disk cache path based on a given path and the cache name. + public typealias DiskCachePathClosure = @Sendable (URL, String) -> URL + + // MARK: Initializers + + /// Creates an ``ImageCache`` with a customized ``MemoryStorage`` and ``DiskStorage``. + /// + /// - Parameters: + /// - memoryStorage: The ``MemoryStorage/Backend`` object to be used in the image memory cache. + /// - diskStorage: The ``DiskStorage/Backend`` object to be used in the image disk cache. + public init( + memoryStorage: MemoryStorage.Backend, + diskStorage: DiskStorage.Backend) + { + self.memoryStorage = memoryStorage + self.diskStorage = diskStorage + let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(UUID().uuidString)" + ioQueue = DispatchQueue(label: ioQueueName) + + Task { @MainActor in + let notifications: [(Notification.Name, Selector)] + #if !os(macOS) && !os(watchOS) + notifications = [ + (UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)), + (UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)), + (UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache)) + ] + #elseif os(macOS) + notifications = [ + (NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)), + ] + #else + notifications = [] + #endif + notifications.forEach { + NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil) + } + } + } + + /// Creates an ``ImageCache`` with a given `name`. + /// + /// Both the ``MemoryStorage`` and the ``DiskStorage`` will be created with a default configuration based on the `name`. + /// + /// - Parameter name: The name of the cache object. It is used to set up disk cache directories and IO queues. + /// You should not use the same `name` for different caches; otherwise, the disk storages would conflict with each + /// other. The `name` should not be an empty string. + /// + /// > Warning: The `name` "default" is reserved to be used as the name of ``ImageCache/default`` in Kingfisher, + /// and you should not use this name for any of your custom caches. Otherwise, different caches might become mixed + /// up and corrupted. + public convenience init(name: String) { + self.init(noThrowName: name, cacheDirectoryURL: nil, diskCachePathClosure: nil) + } + + /// Creates an ``ImageCache`` with a given `name`, the cache directory `path`, and a closure to modify the cache + /// directory. + /// + /// - Parameters: + /// - name: The name of the cache object. It is used to set up disk cache directories and IO queues. + /// You should not use the same `name` for different caches; otherwise, the disk storages would conflict with each + /// other. The `name` should not be an empty string. + /// - cacheDirectoryURL: The location of the cache directory URL on disk. It will be passed internally to the + /// initializer of the ``DiskStorage`` as the disk cache directory. If `nil`, the cache directory under the user + /// domain mask will be used. + /// - diskCachePathClosure: A closure that takes in an optional initial path string and generates the final disk + /// cache path. You can use it to fully customize your cache path. + /// - Throws: An error that occurs during the creation of the image cache, such as being unable to create a + /// directory at the given path. + /// + /// > Warning: The `name` "default" is reserved to be used as the name of ``ImageCache/default`` in Kingfisher, + /// and you should not use this name for any of your custom caches. Otherwise, different caches might become mixed + /// up and corrupted. + public convenience init( + name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? = nil + ) throws + { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") + } + + let memoryStorage = ImageCache.createMemoryStorage() + + let config = ImageCache.createConfig( + name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure + ) + let diskStorage = try DiskStorage.Backend(config: config) + self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) + } + + convenience init( + noThrowName name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? + ) + { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") + } + + let memoryStorage = ImageCache.createMemoryStorage() + + let config = ImageCache.createConfig( + name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure + ) + let diskStorage = DiskStorage.Backend(noThrowConfig: config, creatingDirectory: true) + self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) + } + + private static func createMemoryStorage() -> MemoryStorage.Backend { + let totalMemory = ProcessInfo.processInfo.physicalMemory + let costLimit = totalMemory / 4 + let memoryStorage = MemoryStorage.Backend(config: + .init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit))) + return memoryStorage + } + + private static func createConfig( + name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? = nil + ) -> DiskStorage.Config + { + var diskConfig = DiskStorage.Config( + name: name, + sizeLimit: 0, + directory: cacheDirectoryURL + ) + if let closure = diskCachePathClosure { + diskConfig.cachePathBlock = closure + } + return diskConfig + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Storing Images + + /// Stores an image to the cache. + /// + /// - Parameters: + /// - image: The image that to be stored. + /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for + /// further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for + /// caching in disk. It checks the image format based on the `original` data to determine the appropriate image + /// format to use. For other types of `serializer`, it depends on their implementation details on how to use this + /// original data. + /// - key: The key used for caching the image. + /// - options: The options which contains configurations for caching the image. + /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. + /// Otherwise, it is cached in both memory storage and disk storage. The default is `true`. + /// - completionHandler: A closure which is invoked when the cache operation finishes. + open func store( + _ image: KFCrossPlatformImage, + original: Data? = nil, + forKey key: String, + options: KingfisherParsedOptionsInfo, + toDisk: Bool = true, + completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil + ) + { + let identifier = options.processor.identifier + let callbackQueue = options.callbackQueue + + let computedKey = key.computedKey(with: identifier) + // Memory storage should not throw. + memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration) + + guard toDisk else { + if let completionHandler = completionHandler { + let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) + callbackQueue.execute { completionHandler(result) } + } + return + } + + ioQueue.async { + let serializer = options.cacheSerializer + if let data = serializer.data(with: image, original: original) { + self.syncStoreToDisk( + data, + forKey: key, + forcedExtension: options.forcedExtension, + processorIdentifier: identifier, + callbackQueue: callbackQueue, + expiration: options.diskCacheExpiration, + writeOptions: options.diskStoreWriteOptions, + completionHandler: completionHandler) + } else { + guard let completionHandler = completionHandler else { return } + + let diskError = KingfisherError.cacheError( + reason: .cannotSerializeImage(image: image, original: original, serializer: serializer)) + let result = CacheStoreResult( + memoryCacheResult: .success(()), + diskCacheResult: .failure(diskError)) + callbackQueue.execute { completionHandler(result) } + } + } + } + + /// Stores an image in the cache. + /// + /// - Parameters: + /// - image: The image to be stored. + /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for + /// further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for + /// caching in disk. It checks the image format based on the `original` data to determine the appropriate image + /// format to use. For other types of `serializer`, it depends on their implementation details on how to use this + /// original data. + /// - key: The key used for caching the image. + /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the + /// image, pass the identifier of the processor to this parameter. + /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the + /// disk storage configuration instead. + /// - serializer: The ``CacheSerializer`` used to convert the `image` and `original` to the data that will be + /// stored to disk. By default, the ``DefaultCacheSerializer/default`` will be used. + /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. + /// Otherwise, it is cached in both memory storage and disk storage. The default is `true`. + /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is + /// ``CallbackQueue/untouch``. Under this default ``CallbackQueue/untouch`` queue, if `toDisk` is `false`, it + /// means the `completionHandler` will be invoked from the caller queue of this method; if `toDisk` is `true`, + /// the `completionHandler` will be called from an internal file IO queue. To change this behavior, specify + /// another ``CallbackQueue`` value. + /// - completionHandler: A closure that is invoked when the cache operation finishes. + open func store( + _ image: KFCrossPlatformImage, + original: Data? = nil, + forKey key: String, + processorIdentifier identifier: String = "", + forcedExtension: String? = nil, + cacheSerializer serializer: any CacheSerializer = DefaultCacheSerializer.default, + toDisk: Bool = true, + callbackQueue: CallbackQueue = .untouch, + completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil + ) + { + struct TempProcessor: ImageProcessor { + let identifier: String + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return nil + } + } + + let options = KingfisherParsedOptionsInfo([ + .processor(TempProcessor(identifier: identifier)), + .cacheSerializer(serializer), + .callbackQueue(callbackQueue), + .forcedCacheFileExtension(forcedExtension) + ]) + store( + image, + original: original, + forKey: key, + options: options, + toDisk: toDisk, + completionHandler: completionHandler + ) + } + + /// Store some data to the disk. + /// + /// - Parameters: + /// - data: The data to be stored. + /// - key: The key used for caching the data. + /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the + /// image, pass the identifier of the processor to this parameter. + /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the + /// disk storage configuration instead. + /// - expiration: The expiration policy used by this storage action. + /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is + /// ``CallbackQueue/untouch``. Under this default ``CallbackQueue/untouch`` queue, if `toDisk` is `false`, it + /// means the `completionHandler` will be invoked from the caller queue of this method; if `toDisk` is `true`, + /// the `completionHandler` will be called from an internal file IO queue. To change this behavior, specify + /// another ``CallbackQueue`` value. + /// - completionHandler: A closure that is invoked when the cache operation finishes. + open func storeToDisk( + _ data: Data, + forKey key: String, + processorIdentifier identifier: String = "", + forcedExtension: String? = nil, + expiration: StorageExpiration? = nil, + callbackQueue: CallbackQueue = .untouch, + completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil) + { + ioQueue.async { + self.syncStoreToDisk( + data, + forKey: key, + forcedExtension: forcedExtension, + processorIdentifier: identifier, + callbackQueue: callbackQueue, + expiration: expiration, + completionHandler: completionHandler + ) + } + } + + private func syncStoreToDisk( + _ data: Data, + forKey key: String, + forcedExtension: String?, + processorIdentifier identifier: String = "", + callbackQueue: CallbackQueue = .untouch, + expiration: StorageExpiration? = nil, + writeOptions: Data.WritingOptions = [], + completionHandler: (@Sendable (CacheStoreResult) -> Void)? = nil) + { + let computedKey = key.computedKey(with: identifier) + let result: CacheStoreResult + do { + try self.diskStorage.store( + value: data, + forKey: computedKey, + expiration: expiration, + writeOptions: writeOptions, + forcedExtension: forcedExtension + ) + result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) + } catch { + let diskError: KingfisherError + if let error = error as? KingfisherError { + diskError = error + } else { + diskError = .cacheError(reason: .cannotConvertToData(object: data, error: error)) + } + + result = CacheStoreResult( + memoryCacheResult: .success(()), + diskCacheResult: .failure(diskError) + ) + } + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler(result) } + } + } + + // MARK: Removing Images + + /// Removes the image for the given key from the cache. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the + /// image, pass the identifier of the processor to this parameter. + /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the + /// disk storage configuration instead. + /// - fromMemory: Whether this image should be removed from memory storage or not. If `false`, the image won't be + /// removed from the memory storage. The default is `true`. + /// - fromDisk: Whether this image should be removed from the disk storage or not. If `false`, the image won't be + /// removed from the disk storage. The default is `true`. + /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. The default is + /// ``CallbackQueue/untouch``. + /// - completionHandler: A closure that is invoked when the cache removal operation finishes. + open func removeImage( + forKey key: String, + processorIdentifier identifier: String = "", + forcedExtension: String? = nil, + fromMemory: Bool = true, + fromDisk: Bool = true, + callbackQueue: CallbackQueue = .untouch, + completionHandler: (@Sendable () -> Void)? = nil + ) + { + removeImage( + forKey: key, + processorIdentifier: identifier, + forcedExtension: forcedExtension, + fromMemory: fromMemory, + fromDisk: fromDisk, + callbackQueue: callbackQueue, + completionHandler: { _ in completionHandler?() } // This is a version which ignores error. + ) + } + + func removeImage( + forKey key: String, + processorIdentifier identifier: String = "", + forcedExtension: String?, + fromMemory: Bool = true, + fromDisk: Bool = true, + callbackQueue: CallbackQueue = .untouch, + completionHandler: (@Sendable ((any Error)?) -> Void)? = nil) + { + let computedKey = key.computedKey(with: identifier) + + if fromMemory { + memoryStorage.remove(forKey: computedKey) + } + + @Sendable func callHandler(_ error: (any Error)?) { + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler(error) } + } + } + + if fromDisk { + ioQueue.async{ + do { + try self.diskStorage.remove(forKey: computedKey, forcedExtension: forcedExtension) + callHandler(nil) + } catch { + callHandler(error) + } + } + } else { + callHandler(nil) + } + } + + // MARK: Getting Images + + /// Retrieves an image for a given key from the cache, either from memory storage or disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The ``KingfisherParsedOptionsInfo`` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. + /// The default is ``CallbackQueue/mainCurrentOrAsync``. + /// - completionHandler: A closure that is invoked when the image retrieval operation finishes. If the image + /// retrieval operation finishes without any problems, an ``ImageCacheResult`` value will be sent to this closure + /// as a result. Otherwise, a ``KingfisherError`` result with detailed failure reason will be sent. + open func retrieveImage( + forKey key: String, + options: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .mainCurrentOrAsync, + completionHandler: (@Sendable (Result) -> Void)?) + { + // No completion handler. No need to start working and early return. + guard let completionHandler = completionHandler else { return } + + // Try to check the image from memory cache first. + if let image = retrieveImageInMemoryCache(forKey: key, options: options) { + callbackQueue.execute { completionHandler(.success(.memory(image))) } + } else if options.fromMemoryCacheOrRefresh { + callbackQueue.execute { completionHandler(.success(.none)) } + } else { + + // Begin to disk search. + self.retrieveImageInDiskCache(forKey: key, options: options, callbackQueue: callbackQueue) { + result in + switch result { + case .success(let image): + + guard let image = image else { + // No image found in disk storage. + callbackQueue.execute { completionHandler(.success(.none)) } + return + } + + // Cache the disk image to memory. + // We are passing `false` to `toDisk`, the memory cache does not change + // callback queue, we can call `completionHandler` without another dispatch. + var cacheOptions = options + cacheOptions.callbackQueue = .untouch + self.store( + image, + forKey: key, + options: cacheOptions, + toDisk: false) + { + _ in + callbackQueue.execute { completionHandler(.success(.disk(image))) } + } + case .failure(let error): + callbackQueue.execute { completionHandler(.failure(error)) } + } + } + } + } + + /// Retrieves an image for a given key from the cache, either from memory storage or disk storage. + /// + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The ``KingfisherOptionsInfo`` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. + /// The default is ``CallbackQueue/mainCurrentOrAsync``. + /// - completionHandler: A closure that is invoked when the image retrieval operation finishes. If the image + /// retrieval operation finishes without any problems, an ``ImageCacheResult`` value will be sent to this closure + /// as a result. Otherwise, a ``KingfisherError`` result with detailed failure reason will be sent. + /// + /// > This method is marked as `open` for compatibility purposes only. Do not override this method. Instead, + /// override the version ``ImageCache/retrieveImageInDiskCache(forKey:options:callbackQueue:completionHandler:)`` + /// accepts a ``KingfisherParsedOptionsInfo`` value. + open func retrieveImage( + forKey key: String, + options: KingfisherOptionsInfo? = nil, + callbackQueue: CallbackQueue = .mainCurrentOrAsync, + completionHandler: (@Sendable (Result) -> Void)? + ) + { + retrieveImage( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + /// Retrieves an image associated with a given key from the memory storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The ``KingfisherParsedOptionsInfo`` options setting used to fetch the image. + /// - Returns: The image stored in the memory cache if it exists and is valid. If the image does not exist or has + /// already expired, `nil` is returned. + open func retrieveImageInMemoryCache( + forKey key: String, + options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? + { + let computedKey = key.computedKey(with: options.processor.identifier) + return memoryStorage.value( + forKey: computedKey, + extendingExpiration: options.memoryCacheAccessExtendingExpiration + ) + } + + /// Retrieves an image associated with a given key from the memory storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The ``KingfisherOptionsInfo`` options setting used to fetch the image. + /// - Returns: The image stored in the memory cache if it exists and is valid. If the image does not exist or has + /// already expired, `nil` is returned. + /// + /// > This method is marked as `open` for compatibility purposes only. Do not override this method. Instead, + /// override the version ``ImageCache/retrieveImageInMemoryCache(forKey:options:)-2xj0`` that accepts a + /// ``KingfisherParsedOptionsInfo`` value. + open func retrieveImageInMemoryCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil) -> KFCrossPlatformImage? + { + return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options)) + } + + func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .untouch, + completionHandler: @escaping @Sendable (Result) -> Void) + { + let computedKey = key.computedKey(with: options.processor.identifier) + let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue) + loadingQueue.execute { + do { + var image: KFCrossPlatformImage? = nil + if let data = try self.diskStorage.value( + forKey: computedKey, + forcedExtension: options.forcedExtension, + extendingExpiration: options.diskCacheAccessExtendingExpiration + ) { + image = options.cacheSerializer.image(with: data, options: options) + } + if options.backgroundDecode { + image = image?.kf.decoded(scale: options.scaleFactor) + } + callbackQueue.execute { [image] in completionHandler(.success(image)) } + } catch let error as KingfisherError { + callbackQueue.execute { completionHandler(.failure(error)) } + } catch { + assertionFailure("The internal thrown error should be a `KingfisherError`.") + } + } + } + + /// Retrieves an image associated with a given key from the disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The ``KingfisherOptionsInfo`` options setting used to fetch the image. + /// - callbackQueue: The callback queue on which the `completionHandler` is invoked. + /// The default is ``CallbackQueue/untouch``. + /// - completionHandler: A closure that is invoked when the operation is finished. + open func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil, + callbackQueue: CallbackQueue = .untouch, + completionHandler: @escaping @Sendable (Result) -> Void) + { + retrieveImageInDiskCache( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + // MARK: Cleaning + /// Clears the memory and disk storage of this cache. + /// + /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. + /// + /// - Parameter handler: A closure that is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + public func clearCache(completion handler: (@Sendable () -> Void)? = nil) { + clearMemoryCache() + clearDiskCache(completion: handler) + } + + /// Clears the memory storage of this cache. + @objc public func clearMemoryCache() { + memoryStorage.removeAll() + } + + /// Clears the disk storage of this cache. + /// + /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. + /// + /// - Parameter handler: A closure that is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + open func clearDiskCache(completion handler: (@Sendable () -> Void)? = nil) { + ioQueue.async { + do { + try self.diskStorage.removeAll() + } catch _ { } + if let handler = handler { + DispatchQueue.main.async { handler() } + } + } + } + + /// Clears the expired images from the memory and disk storage. + /// + /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. + open func cleanExpiredCache(completion handler: (@Sendable () -> Void)? = nil) { + cleanExpiredMemoryCache() + cleanExpiredDiskCache(completion: handler) + } + + /// Clears the expired images from the memory storage. + open func cleanExpiredMemoryCache() { + memoryStorage.removeExpired() + } + + /// Clears the expired images from disk storage. + /// + /// This is an async operation. + @objc func cleanExpiredDiskCache() { + cleanExpiredDiskCache(completion: nil) + } + + /// Clears the expired images from disk storage. + /// + /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. + /// + /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + open func cleanExpiredDiskCache(completion handler: (@Sendable () -> Void)? = nil) { + ioQueue.async { + do { + var removed: [URL] = [] + let removedExpired = try self.diskStorage.removeExpiredValues() + removed.append(contentsOf: removedExpired) + + let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues() + removed.append(contentsOf: removedSizeExceeded) + + if !removed.isEmpty { + DispatchQueue.main.async { [removed] in + let cleanedHashes = removed.map { $0.lastPathComponent } + NotificationCenter.default.post( + name: .KingfisherDidCleanDiskCache, + object: self, + userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes]) + } + } + + if let handler = handler { + DispatchQueue.main.async { handler() } + } + } catch {} + } + } + +#if !os(macOS) && !os(watchOS) + /// Clears the expired images from disk storage when the app is in the background. + /// + /// This is an asynchronous operation. When the cache clearing operation finishes, the `handler` will be invoked. + /// + /// In most cases, you should not call this method explicitly. It will be called automatically when a + /// `UIApplicationDidEnterBackgroundNotification` is received. + @MainActor + @objc public func backgroundCleanExpiredDiskCache() { + // if 'sharedApplication()' is unavailable, then return + guard let sharedApplication = KingfisherWrapper.shared else { return } + + let taskActor = ActorBox(nil) + + let createdTask = sharedApplication.beginBackgroundTask(withName: "Kingfisher:backgroundCleanExpiredDiskCache") { + Task { + guard let bgTask = await taskActor.value, bgTask != .invalid else { return } + sharedApplication.endBackgroundTask(bgTask) + await taskActor.setValue(.invalid) + } + } + + cleanExpiredDiskCache { + Task { + guard let bgTask = await taskActor.value, bgTask != .invalid else { return } + #if compiler(>=6) + sharedApplication.endBackgroundTask(bgTask) + #else + await sharedApplication.endBackgroundTask(bgTask) + #endif + await taskActor.setValue(.invalid) + } + } + + Task { + await taskActor.setValue(createdTask) + } + } +#endif + + // MARK: Image Cache State + + /// Returns the cache type for a given `key` and `identifier` combination. + /// + /// This method is used to check whether an image is cached in the current cache. It also provides information on + /// which kind of cache the image can be found in the return value. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: The processor identifier used for this image. The default value is the + /// ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor. + /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the + /// disk storage configuration instead. + /// + /// - Returns: A ``CacheType`` instance that indicates the cache status. ``CacheType/none`` indicates that the + /// image is not in the cache or that it has already expired. + open func imageCachedType( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, + forcedExtension: String? = nil + ) -> CacheType + { + let computedKey = key.computedKey(with: identifier) + if memoryStorage.isCached(forKey: computedKey) { return .memory } + if diskStorage.isCached(forKey: computedKey, forcedExtension: forcedExtension) { return .disk } + return .none + } + + /// Returns whether the file exists in the cache for a given `key` and `identifier` combination. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: The processor identifier used for this image. The default value is the + /// ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor. + /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the + /// disk storage configuration instead. + /// + /// - Returns: A `Bool` value indicating whether a cache matches the given `key` and `identifier` combination. + /// + /// > The return value does not contain information about the kind of storage the cache matches from. + /// > To obtain information about the cache type according to ``CacheType``, use + /// ``ImageCache/imageCachedType(forKey:processorIdentifier:forcedExtension:)`` instead. + public func isCached( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, + forcedExtension: String? = nil + ) -> Bool + { + return imageCachedType(forKey: key, processorIdentifier: identifier, forcedExtension: forcedExtension).cached + } + + /// Retrieves the hash used as the cache file name for the key. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: The processor identifier used for this image. The default value is the + /// ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor. + /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the + /// disk storage configuration instead. + /// + /// - Returns: The hash used as the cache file name. + /// + /// > By default, for a given combination of `key` and `identifier`, the ``ImageCache`` instance uses the value + /// returned by this method as the cache file name. You can use this value to check and match the cache file if + /// needed. + open func hash( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, + forcedExtension: String? = nil + ) -> String + { + let computedKey = key.computedKey(with: identifier) + return diskStorage.cacheFileName(forKey: computedKey, forcedExtension: forcedExtension) + } + + /// Calculates the size taken by the disk storage. + /// + /// It represents the total file size of all cached files in the ``ImageCache/diskStorage`` on disk in bytes. + /// + /// - Parameter handler: Called when the size calculation is complete. This closure is invoked from the main queue. + open func calculateDiskStorageSize( + completion handler: @escaping (@Sendable (Result) -> Void) + ) { + ioQueue.async { + do { + let size = try self.diskStorage.totalSize() + DispatchQueue.main.async { handler(.success(size)) } + } catch let error as KingfisherError { + DispatchQueue.main.async { handler(.failure(error)) } + } catch { + assertionFailure("The internal thrown error should be a `KingfisherError`.") + } + } + } + + /// Retrieves the cache path for the key. + /// + /// It is useful for projects with a web view or for anyone who needs access to the local file path. + /// For instance, replacing the `` tag in your HTML. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: The processor identifier used for this image. The default value is the + /// ``DefaultImageProcessor/identifier`` of the ``DefaultImageProcessor/default`` image processor. + /// - forcedExtension: The expected extension of the file. If `nil`, the file extension will be determined by the + /// disk storage configuration instead. + /// + /// - Returns: The disk path of the cached image under the given `key` and `identifier`. + /// + /// > This method does not guarantee that there is an image already cached in the returned path. It simply provides + /// > the path where the image should be if it exists in the disk storage. + /// > + /// > You could use the ``ImageCache/isCached(forKey:processorIdentifier:forcedExtension:)`` method to check whether the image is + /// cached under that key on disk if necessary. + open func cachePath( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, + forcedExtension: String? = nil + ) -> String + { + let computedKey = key.computedKey(with: identifier) + return diskStorage.cacheFileURL(forKey: computedKey, forcedExtension: forcedExtension).path + } + + /// Returns the file URL if a disk cache file is existing for the target key, identifier and forcedExtension + /// combination. Otherwise, if the requested cache value is not on the disk as a file, `nil`. + /// + /// - Parameters: + /// - key: The key used for caching the item. + /// - identifier: The processor identifier used for this image. It involves into calculating the final cache key. + /// - forcedExtension: The expected extension of the file. + /// - Returns: The file URL if a disk cache file is existing for the combination. Otherwise, `nil`. + open func cacheFileURLIfOnDisk( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier, + forcedExtension: String? = nil + ) -> URL? + { + let computedKey = key.computedKey(with: identifier) + return diskStorage.isCached( + forKey: computedKey, + forcedExtension: forcedExtension + ) ? diskStorage.cacheFileURL(forKey: computedKey, forcedExtension: forcedExtension) : nil + } + + // MARK: - Concurrency + + /// Stores an image to the cache. + /// + /// - Parameters: + /// - image: The image that to be stored. + /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for + /// further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for + /// caching in disk. It checks the image format based on the `original` data to determine the appropriate image + /// format to use. For other types of `serializer`, it depends on their implementation details on how to use this + /// original data. + /// - key: The key used for caching the image. + /// - options: The options which contains configurations for caching the image. + /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. + /// Otherwise, it is cached in both memory storage and disk storage. The default is `true`. + open func store( + _ image: KFCrossPlatformImage, + original: Data? = nil, + forKey key: String, + options: KingfisherParsedOptionsInfo, + toDisk: Bool = true + ) async throws { + try await withCheckedThrowingContinuation { continuation in + store(image, original: original, forKey: key, options: options, toDisk: toDisk) { + continuation.resume(with: $0.diskCacheResult) + } + } + } + + /// Stores an image in the cache. + /// + /// - Parameters: + /// - image: The image to be stored. + /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for + /// further use. By default, Kingfisher uses a ``DefaultCacheSerializer`` to serialize the image to data for + /// caching in disk. It checks the image format based on the `original` data to determine the appropriate image + /// format to use. For other types of `serializer`, it depends on their implementation details on how to use this + /// original data. + /// - key: The key used for caching the image. + /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the + /// image, pass the identifier of the processor to this parameter. + /// - forcedExtension: The file extension, if exists. + /// - serializer: The ``CacheSerializer`` used to convert the `image` and `original` to the data that will be + /// stored to disk. By default, the ``DefaultCacheSerializer/default`` will be used. + /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. + /// Otherwise, it is cached in both memory storage and disk storage. The default is `true`. + open func store( + _ image: KFCrossPlatformImage, + original: Data? = nil, + forKey key: String, + processorIdentifier identifier: String = "", + forcedExtension: String? = nil, + cacheSerializer serializer: any CacheSerializer = DefaultCacheSerializer.default, + toDisk: Bool = true + ) async throws { + try await withCheckedThrowingContinuation { continuation in + store( + image, + original: original, + forKey: key, + processorIdentifier: identifier, + forcedExtension: forcedExtension, + cacheSerializer: serializer, + toDisk: toDisk) { + // Only `diskCacheResult` can fail + continuation.resume(with: $0.diskCacheResult) + } + } + } + + open func storeToDisk( + _ data: Data, + forKey key: String, + processorIdentifier identifier: String = "", + forcedExtension: String? = nil, + expiration: StorageExpiration? = nil + ) async throws + { + try await withCheckedThrowingContinuation { continuation in + storeToDisk( + data, + forKey: key, + processorIdentifier: identifier, + forcedExtension: forcedExtension, + expiration: expiration) { + // Only `diskCacheResult` can fail + continuation.resume(with: $0.diskCacheResult) + } + } + } + + /// Removes the image for the given key from the cache. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: The identifier of the processor being used for caching. If you are using a processor for the + /// image, pass the identifier of the processor to this parameter. + /// - forcedExtension: The file extension, if exists. + /// - fromMemory: Whether this image should be removed from memory storage or not. If `false`, the image won't be + /// removed from the memory storage. The default is `true`. + /// - fromDisk: Whether this image should be removed from the disk storage or not. If `false`, the image won't be + /// removed from the disk storage. The default is `true`. + open func removeImage( + forKey key: String, + processorIdentifier identifier: String = "", + forcedExtension: String? = nil, + fromMemory: Bool = true, + fromDisk: Bool = true + ) async throws { + return try await withCheckedThrowingContinuation { continuation in + removeImage( + forKey: key, + processorIdentifier: identifier, + forcedExtension: forcedExtension, + fromMemory: fromMemory, + fromDisk: fromDisk, + completionHandler: { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + ) + } + } + + /// Retrieves an image for a given key from the cache, either from memory storage or disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The ``KingfisherParsedOptionsInfo`` options setting used for retrieving the image. + /// - Returns: + /// If the image retrieving operation finishes without problem, an ``ImageCacheResult`` value. + /// + /// - Throws: An error of type ``KingfisherError``, if any error happens inside Kingfisher framework. + open func retrieveImage( + forKey key: String, + options: KingfisherParsedOptionsInfo + ) async throws -> ImageCacheResult { + try await withCheckedThrowingContinuation { continuation in + retrieveImage(forKey: key, options: options) { continuation.resume(with: $0) } + } + } + + /// Retrieves an image for a given key from the cache, either from memory storage or disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The ``KingfisherOptionsInfo`` options setting used for retrieving the image. + /// + /// - Returns: If the image retrieving operation finishes without problem, an ``ImageCacheResult`` value. + /// + /// - Throws: An error of type ``KingfisherError``, if any error happens inside Kingfisher framework. + /// + /// > This method is marked as `open` for compatibility purposes only. Do not override this method. Instead, + /// override the version ``ImageCache/retrieveImage(forKey:options:callbackQueue:completionHandler:)-1jjo3`` that + /// accepts a ``KingfisherParsedOptionsInfo`` value. + open func retrieveImage( + forKey key: String, + options: KingfisherOptionsInfo? = nil + ) async throws -> ImageCacheResult { + try await withCheckedThrowingContinuation { continuation in + retrieveImage(forKey: key, options: options) { continuation.resume(with: $0) } + } + } + + /// Retrieves an image associated with a given key from the disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The ``KingfisherOptionsInfo`` options setting used to fetch the image. + /// + /// - Returns: The image stored in the disk cache if it exists and is valid. If the image does not exist or has + /// already expired, `nil` is returned. + /// + /// - Returns: If the image retrieving operation finishes without problem, an ``ImageCacheResult`` value. + /// + /// - Throws: An error of type ``KingfisherError``, if any error happens inside Kingfisher framework. + /// ``KingfisherParsedOptionsInfo`` value. + open func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil + ) async throws -> KFCrossPlatformImage? { + try await withCheckedThrowingContinuation { continuation in + retrieveImageInDiskCache(forKey: key, options: options) { + continuation.resume(with: $0) + } + } + } + + /// Clears the memory and disk storage of this cache. + /// + /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns. + open func clearCache() async { + await withCheckedContinuation { continuation in + clearCache { continuation.resume() } + } + } + + /// Clears the disk storage of this cache. + /// + /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns. + open func clearDiskCache() async { + await withCheckedContinuation { continuation in + clearDiskCache { continuation.resume() } + } + } + + /// Clears the expired images from the memory and disk storage. + /// + /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns. + open func cleanExpiredCache() async { + await withCheckedContinuation { continuation in + cleanExpiredCache { continuation.resume() } + } + } + + /// Clears the expired images from disk storage. + /// + /// This is an asynchronous operation. When the cache clearing operation finishes, the whole method returns. + open func cleanExpiredDiskCache() async { + await withCheckedContinuation { continuation in + cleanExpiredDiskCache { continuation.resume() } + } + } + + /// Calculates the size taken by the disk storage. + /// + /// It represents the total file size of all cached files in the ``ImageCache/diskStorage`` on disk in bytes. + open var diskStorageSize: UInt { + get async throws { + try await withCheckedThrowingContinuation { continuation in + calculateDiskStorageSize { continuation.resume(with: $0) } + } + } + } + +} + +// Concurrency + + +#if !os(macOS) && !os(watchOS) +// MARK: - For App Extensions +extension UIApplication: KingfisherCompatible { } +extension KingfisherWrapper where Base: UIApplication { + public static var shared: UIApplication? { + let selector = NSSelectorFromString("sharedApplication") + guard Base.responds(to: selector) else { return nil } + return Base.perform(selector).takeUnretainedValue() as? UIApplication + } +} +#endif + +extension String { + func computedKey(with identifier: String) -> String { + if identifier.isEmpty { + return self + } else { + return appending("@\(identifier)") + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift new file mode 100644 index 00000000..435e8ba3 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift @@ -0,0 +1,311 @@ +// +// MemoryStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +/// Represents the concepts related to storage that stores a specific type of value in memory. +/// +/// This serves as a namespace for memory storage types. A ``MemoryStorage/Backend`` with a particular +/// ``MemoryStorage/Config`` is used to define the storage. +/// +/// Refer to these composite types for further details. +public enum MemoryStorage { + + /// Represents a storage that stores a specific type of value in memory. + /// + /// It provides fast access but has a limited storage size. The stored value type needs to conform to the + /// ``CacheCostCalculable`` protocol, and its ``CacheCostCalculable/cacheCost`` will be used to determine the cost + /// of the cache item's size in the memory. + /// + /// You can configure a ``MemoryStorage/Backend`` in its ``MemoryStorage/Backend/init(config:)`` method by passing + /// a ``MemoryStorage/Config`` value or by modifying the ``MemoryStorage/Backend/config`` property after it's + /// created. + /// + /// The ``MemoryStorage`` backend has an upper limit on the total cost size in memory and item count. All items in + /// the storage have an expiration date. When retrieved, if the target item is already expired, it will be + /// recognized as if it does not exist in the storage. + /// + /// The `MemoryStorage` also includes a scheduled self-cleaning task to evict expired items from memory. + /// + /// > This class is thready safe. + public class Backend: @unchecked Sendable { + + let storage = NSCache>() + + // Keys track the objects once inside the storage. + // + // For object removing triggered by user, the corresponding key would be also removed. However, for the object + // removing triggered by cache rule/policy of system, the key will be remained there until next `removeExpired` + // happens. + // + // Breaking the strict tracking could save additional locking behaviors and improve the cache performance. + // See https://github.com/onevcat/Kingfisher/issues/1233 + var keys = Set() + + private var cleanTimer: Timer? = nil + private let lock = NSLock() + + /// The configuration used in this storage. + /// + /// It is a value you can set and use to configure the storage as needed. + public var config: Config { + didSet { + storage.totalCostLimit = config.totalCostLimit + storage.countLimit = config.countLimit + cleanTimer?.invalidate() + cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.removeExpired() + } + } + } + + /// Creates a ``MemoryStorage/Backend`` with a given ``MemoryStorage/Config`` value. + /// + /// - Parameter config: The configuration used to create the storage. It determines the maximum size limitation, + /// default expiration settings, and more. + public init(config: Config) { + self.config = config + storage.totalCostLimit = config.totalCostLimit + storage.countLimit = config.countLimit + + cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.removeExpired() + } + } + + /// Removes the expired values from the storage. + public func removeExpired() { + lock.lock() + defer { lock.unlock() } + for key in keys { + let nsKey = key as NSString + guard let object = storage.object(forKey: nsKey) else { + // This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule. + // We didn't remove the key yet until now, since we do not want to introduce additional lock. + // See https://github.com/onevcat/Kingfisher/issues/1233 + keys.remove(key) + continue + } + if object.isExpired { + storage.removeObject(forKey: nsKey) + keys.remove(key) + } + } + } + + /// Stores a value in the storage under the specified key and expiration policy. + /// + /// - Parameters: + /// - value: The value to be stored. + /// - key: The key to which the `value` will be stored. + /// - expiration: The expiration policy used by this storage action. + public func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) + { + storeNoThrow(value: value, forKey: key, expiration: expiration) + } + + // The no throw version for storing value in cache. Kingfisher knows the detail so it + // could use this version to make syntax simpler internally. + func storeNoThrow( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) + { + lock.lock() + defer { lock.unlock() } + let expiration = expiration ?? config.expiration + // The expiration indicates that already expired, no need to store. + guard !expiration.isExpired else { return } + + let object: StorageObject + if config.keepWhenEnteringBackground { + object = BackgroundKeepingStorageObject(value, expiration: expiration) + } else { + object = StorageObject(value, expiration: expiration) + } + storage.setObject(object, forKey: key as NSString, cost: value.cacheCost) + keys.insert(key) + } + + /// Gets a value from the storage. + /// + /// - Parameters: + /// - key: The cache key of the value. + /// - extendingExpiration: The expiration policy used by this retrieval action. + /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`. + public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? { + guard let object = storage.object(forKey: key as NSString) else { + return nil + } + if object.isExpired { + return nil + } + object.extendExpiration(extendingExpiration) + return object.value + } + + /// Determines whether there is valid cached data under a given key. + /// + /// - Parameter key: The cache key of the value. + /// - Returns: `true` if there is valid data under the key, otherwise `false`. + public func isCached(forKey key: String) -> Bool { + guard let _ = value(forKey: key, extendingExpiration: .none) else { + return false + } + return true + } + + /// Removes a value from a specified key. + /// + /// - Parameter key: The cache key of the value. + public func remove(forKey key: String) { + lock.lock() + defer { lock.unlock() } + storage.removeObject(forKey: key as NSString) + keys.remove(key) + } + + /// Removes all values in this storage. + public func removeAll() { + lock.lock() + defer { lock.unlock() } + storage.removeAllObjects() + keys.removeAll() + } + } +} + +extension MemoryStorage { + /// Represents the configuration used in a ``MemoryStorage/Backend``. + public struct Config { + + /// The total cost limit of the storage. + /// + /// This counts up the value of ``CacheCostCalculable/cacheCost``. If adding this object to the cache causes + /// the cache’s total cost to rise above totalCostLimit, the cache may automatically evict objects until its + /// total cost falls below this value. + public var totalCostLimit: Int + + /// The item count limit of the memory storage. + /// + /// The default value is `Int.max`, which means no hard limitation of the item count. + public var countLimit: Int = .max + + /// The ``StorageExpiration`` used in this memory storage. + /// + /// The default is `.seconds(300)`, which means that the memory cache will expire in 5 minutes if not accessed. + public var expiration: StorageExpiration = .seconds(300) + + /// The time interval between the storage performing cleaning work for sweeping expired items. + public var cleanInterval: TimeInterval + + /// Determine whether newly added items to memory cache should be purged when the app goes to the background. + /// + /// By default, cached items in memory will be purged as soon as the app goes to the background to ensure a + /// minimal memory footprint. Enabling this prevents this behavior and keeps the items alive in the cache even + /// when your app is not in the foreground. + /// + /// The default value is `false`. After setting it to `true`, only newly added cache objects are affected. + /// Existing objects that were already in the cache while this value was `false` will still be purged when the + /// app enters the background. + public var keepWhenEnteringBackground: Bool = false + + /// Creates a configuration from a given ``MemoryStorage/Config/totalCostLimit`` value and a + /// ``MemoryStorage/Config/cleanInterval``. + /// + /// - Parameters: + /// - totalCostLimit: The total cost limit of the storage in bytes. + /// - cleanInterval: The time interval between the storage performing cleaning work for sweeping expired items. + /// The default is 120, which means auto eviction happens once every two minutes. + /// + /// > Other properties of the ``MemoryStorage/Config`` will use their default values when created. + public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) { + self.totalCostLimit = totalCostLimit + self.cleanInterval = cleanInterval + } + } +} + +extension MemoryStorage { + + class BackgroundKeepingStorageObject: StorageObject, NSDiscardableContent { + var accessing = true + func beginContentAccess() -> Bool { + if value != nil { + accessing = true + } else { + accessing = false + } + return accessing + } + + func endContentAccess() { + accessing = false + } + + func discardContentIfPossible() { + value = nil + } + + func isContentDiscarded() -> Bool { + return value == nil + } + } + + class StorageObject { + var value: T? + let expiration: StorageExpiration + + private(set) var estimatedExpiration: Date + + init(_ value: T, expiration: StorageExpiration) { + self.value = value + self.expiration = expiration + + self.estimatedExpiration = expiration.estimatedExpirationSinceNow + } + + func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) { + switch extendingExpiration { + case .none: + return + case .cacheTime: + self.estimatedExpiration = expiration.estimatedExpirationSinceNow + case .expirationTime(let expirationTime): + self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow + } + } + + var isExpired: Bool { + return estimatedExpiration.isPast + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/Storage.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/Storage.swift new file mode 100644 index 00000000..ffbb96f6 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Cache/Storage.swift @@ -0,0 +1,125 @@ +// +// Storage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +/// Constants for certain time intervals. +struct TimeConstants { + // Seconds in a day, a.k.a 86,400s, roughly. + static let secondsInOneDay = 86_400 +} + +/// Represents the expiration strategy utilized in storage. +public enum StorageExpiration: Sendable { + + /// The item never expires. + case never + + /// The item expires after a duration of the provided number of seconds from now. + case seconds(TimeInterval) + + /// The item expires after a duration of the provided number of days from now. + case days(Int) + + /// The item expires after a specified date. + case date(Date) + + /// Indicates that the item has already expired. + /// + /// Use this to bypass the cache. + case expired + + + func estimatedExpirationSince(_ date: Date) -> Date { + switch self { + case .never: + return .distantFuture + case .seconds(let seconds): + return date.addingTimeInterval(seconds) + case .days(let days): + let duration: TimeInterval = TimeInterval(TimeConstants.secondsInOneDay * days) + return date.addingTimeInterval(duration) + case .date(let ref): + return ref + case .expired: + return .distantPast + } + } + + var estimatedExpirationSinceNow: Date { + estimatedExpirationSince(Date()) + } + + var isExpired: Bool { + timeInterval <= 0 + } + + var timeInterval: TimeInterval { + switch self { + case .never: return .infinity + case .seconds(let seconds): return seconds + case .days(let days): return TimeInterval(TimeConstants.secondsInOneDay * days) + case .date(let ref): return ref.timeIntervalSinceNow + case .expired: return -(.infinity) + } + } +} + +/// Represents the expiration extension strategy used in storage after access. +public enum ExpirationExtending: Sendable { + /// The item expires after the original time, without extension after access. + case none + /// The item expiration extends to the original cache time after each access. + case cacheTime + /// The item expiration extends by the provided time after each access. + case expirationTime(_ expiration: StorageExpiration) +} + +/// Represents types for which the memory cost can be calculated. +public protocol CacheCostCalculable { + var cacheCost: Int { get } +} + +/// Represents types that can be converted to and from data. +public protocol DataTransformable { + + /// Converts the current value to a `Data` representation. + /// - Returns: The data object which can represent the value of the conforming type. + /// - Throws: If any error happens during the conversion. + func toData() throws -> Data + + /// Convert some data to the value. + /// - Parameter data: The data object which should represent the conforming value. + /// - Returns: The converted value of the conforming type. + /// - Throws: If any error happens during the conversion. + static func fromData(_ data: Data) throws -> Self + + /// An empty object of `Self`. + /// + /// > In the cache, when the data is not actually loaded, this value will be returned as a placeholder. + /// > This variable should be returned quickly without any heavy operation inside. + static var empty: Self { get } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-SampleCell-1.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-SampleCell-1.swift new file mode 100644 index 00000000..416f888b --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-SampleCell-1.swift @@ -0,0 +1,5 @@ +import UIKit + +class SampleCell: UITableViewCell { + +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-SampleCell-2.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-SampleCell-2.swift new file mode 100644 index 00000000..59c79f40 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-SampleCell-2.swift @@ -0,0 +1,9 @@ +import UIKit + +class SampleCell: UITableViewCell { + var sampleImageView: UIImageView = { + let imageView = UIImageView(frame: .zero) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-SampleCell-3.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-SampleCell-3.swift new file mode 100644 index 00000000..519ab2cc --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-SampleCell-3.swift @@ -0,0 +1,38 @@ +import UIKit + +class SampleCell: UITableViewCell { + var sampleImageView: UIImageView = { + let imageView = UIImageView(frame: .zero) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + var sampleLabel: UILabel = { + let label = UILabel(frame: .zero) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + contentView.addSubview(sampleImageView) + NSLayoutConstraint.activate([ + sampleImageView.widthAnchor.constraint(equalToConstant: 64), + sampleImageView.heightAnchor.constraint(equalToConstant: 64), + sampleImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12), + sampleImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + ]) + + contentView.addSubview(sampleLabel) + NSLayoutConstraint.activate([ + sampleLabel.leadingAnchor.constraint(equalTo: sampleImageView.trailingAnchor, constant: 12), + sampleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + ]) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-1.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-1.swift new file mode 100644 index 00000000..ae980e1e --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-1.swift @@ -0,0 +1,9 @@ +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-10.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-10.swift new file mode 100644 index 00000000..8ef5cc3d --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-10.swift @@ -0,0 +1,14 @@ +override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + print(KingfisherManager.shared) + + tableView.dataSource = self + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-11.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-11.swift new file mode 100644 index 00000000..db723a49 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-11.swift @@ -0,0 +1,25 @@ +override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + print(KingfisherManager.shared) + + tableView.dataSource = self + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) + + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + switch result { + case .success(let size): + print("Size: \(Double(size) / 1024 / 1024) MB") + case .failure(let error): + print("Some error: \(error)") + } + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-12.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-12.swift new file mode 100644 index 00000000..ce563866 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-12.swift @@ -0,0 +1,31 @@ +override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + print(KingfisherManager.shared) + + tableView.dataSource = self + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) + + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + switch result { + case .success(let size): + let sizeInMB = Double(size) / 1024 / 1024 + let alert = UIAlertController(title: nil, message: String(format: "Kingfisher Disk Cache: %.2fMB", sizeInMB), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Purge", style: .destructive) { _ in + + }) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + self.present(alert, animated: true) + case .failure(let error): + print("Some error: \(error)") + } + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-13.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-13.swift new file mode 100644 index 00000000..81ff08ef --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-13.swift @@ -0,0 +1,33 @@ +override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + print(KingfisherManager.shared) + + tableView.dataSource = self + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) + + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + switch result { + case .success(let size): + let sizeInMB = Double(size) / 1024 / 1024 + let alert = UIAlertController(title: nil, message: String(format: "Kingfisher Disk Cache: %.2fMB", sizeInMB), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Purge", style: .destructive) { _ in + KingfisherManager.shared.cache.clearCache { + self.tableView.reloadData() + } + }) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + self.present(alert, animated: true) + case .failure(let error): + print("Some error: \(error)") + } + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-2.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-2.swift new file mode 100644 index 00000000..19b42ef0 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-2.swift @@ -0,0 +1,11 @@ +import UIKit +import Kingfisher + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + print(KingfisherManager.shared) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-3.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-3.swift new file mode 100644 index 00000000..19b42ef0 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-3.swift @@ -0,0 +1,11 @@ +import UIKit +import Kingfisher + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + print(KingfisherManager.shared) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-4.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-4.swift new file mode 100644 index 00000000..e88b7f8a --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-4.swift @@ -0,0 +1,19 @@ +import UIKit +import Kingfisher + +class ViewController: UIViewController { + + lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero) + tableView.register(SampleCell.self, forCellReuseIdentifier: "SampleCell") + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.rowHeight = 80 + return tableView + }() + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + print(KingfisherManager.shared) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-5.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-5.swift new file mode 100644 index 00000000..89778227 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-5.swift @@ -0,0 +1,41 @@ +import UIKit +import Kingfisher + +class ViewController: UIViewController { + + lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero) + tableView.register(SampleCell.self, forCellReuseIdentifier: "SampleCell") + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.rowHeight = 80 + return tableView + }() + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + print(KingfisherManager.shared) + + tableView.dataSource = self + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) + } +} + +extension ViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 1 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell + cell.sampleLabel.text = "Index \(indexPath.row)" + cell.sampleImageView.backgroundColor = .lightGray + return cell + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-6-0.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-6-0.swift new file mode 100644 index 00000000..fa15c745 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-6-0.swift @@ -0,0 +1,12 @@ +extension ViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 1 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell + cell.sampleLabel.text = "Index \(indexPath.row)" + cell.sampleImageView.backgroundColor = .lightGray + return cell + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-6.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-6.swift new file mode 100644 index 00000000..d1d880f6 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-6.swift @@ -0,0 +1,17 @@ +extension ViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 1 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell + cell.sampleLabel.text = "Index \(indexPath.row)" + + let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" + let url = URL(string: "\(urlPrefix)-1.jpg") + cell.sampleImageView.kf.setImage(with: url) + + cell.sampleImageView.backgroundColor = .lightGray + return cell + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-7.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-7.swift new file mode 100644 index 00000000..314426ad --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-7.swift @@ -0,0 +1,17 @@ +extension ViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 10 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell + cell.sampleLabel.text = "Index \(indexPath.row)" + + let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" + let url = URL(string: "\(urlPrefix)-\(indexPath.row + 1).jpg") + cell.sampleImageView.kf.setImage(with: url) + + cell.sampleImageView.backgroundColor = .lightGray + return cell + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-8.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-8.swift new file mode 100644 index 00000000..efa0bfea --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-8.swift @@ -0,0 +1,24 @@ +extension ViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 10 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "SampleCell", for: indexPath) as! SampleCell + cell.sampleLabel.text = "Index \(indexPath.row)" + + let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" + let url = URL(string: "\(urlPrefix)-\(indexPath.row + 1).jpg") + + cell.sampleImageView.kf.indicatorType = .activity + + let roundCorner = RoundCornerImageProcessor(radius: .widthFraction(0.5), roundingCorners: [.topLeft, .bottomRight]) + let pngSerializer = FormatIndicatedCacheSerializer.png + cell.sampleImageView.kf.setImage( + with: url, + options: [.processor(roundCorner), .cacheSerializer(pngSerializer)] + ) + cell.sampleImageView.backgroundColor = .clear + return cell + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-9.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-9.swift new file mode 100644 index 00000000..6fd5a8d3 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/01-ViewController-9.swift @@ -0,0 +1,9 @@ +// cell.sampleImageView.kf.setImage(with: url, options: [.processor(roundCorner)]) +cell.sampleImageView.kf.setImage(with: url, options: [.processor(roundCorner)]) { result in + switch result { + case .success(let imageResult): + print("Image loaded from cache: \(imageResult.cacheType)") + case .failure(let error): + print("Error: \(error)") + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-1.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-1.swift new file mode 100644 index 00000000..acf0f453 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-1.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-10.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-10.swift new file mode 100644 index 00000000..3ac64e9c --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-10.swift @@ -0,0 +1,34 @@ +@State var showAlert = false +@State var cacheSizeResult: Result? = nil + +var body: some View { + List { + Button("Check Cache") { + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + cacheSizeResult = result + showAlert = true + } + } + .alert( + "Disk Cache", + isPresented: $showAlert, + presenting: cacheSizeResult, + actions: { result in + // TODO: Actions + }, message: { result in + switch result { + case .success(let size): + Text("Size: \(Double(size) / 1024 / 1024) MB") + case .failure(let error): + Text(error.localizedDescription) + } + }) + + ForEach(0 ..< 10) { i in + HStack { + KFImage(url(at: i)) + // ... + } + } + }.listStyle(.plain) +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-11.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-11.swift new file mode 100644 index 00000000..f0a55825 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-11.swift @@ -0,0 +1,42 @@ +@State var showAlert = false +@State var cacheSizeResult: Result? = nil + +var body: some View { + List { + Button("Check Cache") { + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + cacheSizeResult = result + showAlert = true + } + } + .alert( + "Disk Cache", + isPresented: $showAlert, + presenting: cacheSizeResult, + actions: { result in + switch result { + case .success: + Button("Clear") { + KingfisherManager.shared.cache.clearCache() + } + Button("Cancel", role: .cancel) {} + case .failure: + Button("OK") { } + } + }, message: { result in + switch result { + case .success(let size): + Text("Size: \(Double(size) / 1024 / 1024) MB") + case .failure(let error): + Text(error.localizedDescription) + } + }) + + ForEach(0 ..< 10) { i in + HStack { + KFImage(url(at: i)) + // ... + } + } + }.listStyle(.plain) +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-2.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-2.swift new file mode 100644 index 00000000..f2cd01a4 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-2.swift @@ -0,0 +1,17 @@ +import SwiftUI +import Kingfisher + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + .onAppear { + print(KingfisherManager.shared) + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-3.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-3.swift new file mode 100644 index 00000000..d53ce446 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-3.swift @@ -0,0 +1,16 @@ +import SwiftUI +import Kingfisher + +struct ContentView: View { + var body: some View { + List { + ForEach(0 ..< 10) { i in + HStack { + Rectangle().fill(Color.gray) + .frame(width: 64, height: 64) + Text("Index \(i)") + } + } + }.listStyle(.plain) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-4.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-4.swift new file mode 100644 index 00000000..434dcac9 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-4.swift @@ -0,0 +1,22 @@ +import SwiftUI +import Kingfisher + +struct ContentView: View { + func url(at index: Int) -> URL? { + let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" + return URL(string: "\(urlPrefix)-\(index + 1).jpg") + } + + var body: some View { + List { + ForEach(0 ..< 10) { i in + HStack { + KFImage(url(at: i)) + .resizable() + .frame(width: 64, height: 64) + Text("Index \(i)") + } + } + }.listStyle(.plain) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-5.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-5.swift new file mode 100644 index 00000000..6b5c92b7 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-5.swift @@ -0,0 +1,27 @@ +import SwiftUI +import Kingfisher + +struct ContentView: View { + func url(at index: Int) -> URL? { + let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" + return URL(string: "\(urlPrefix)-\(index + 1).jpg") + } + + var body: some View { + List { + ForEach(0 ..< 10) { i in + HStack { + KFImage(url(at: i)) + .resizable() + .roundCorner( + radius: .widthFraction(0.5), + roundingCorners: [.topLeft, .bottomRight] + ) + .serialize(as: .PNG) + .frame(width: 64, height: 64) + Text("Index \(i)") + } + } + }.listStyle(.plain) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-6.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-6.swift new file mode 100644 index 00000000..ab6f95c9 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-6.swift @@ -0,0 +1,33 @@ +import SwiftUI +import Kingfisher + +struct ContentView: View { + func url(at index: Int) -> URL? { + let urlPrefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher" + return URL(string: "\(urlPrefix)-\(index + 1).jpg") + } + + var body: some View { + List { + ForEach(0 ..< 10) { i in + HStack { + KFImage(url(at: i)) + .resizable() + .roundCorner( + radius: .widthFraction(0.5), + roundingCorners: [.topLeft, .bottomRight] + ) + .serialize(as: .PNG) + .onSuccess { result in + print("Image loaded from cache: \(result.cacheType)") + } + .onFailure { error in + print("Error: \(error)") + } + .frame(width: 64, height: 64) + Text("Index \(i)") + } + } + }.listStyle(.plain) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-7.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-7.swift new file mode 100644 index 00000000..4b28d334 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-7.swift @@ -0,0 +1,10 @@ +var body: some View { + List { + ForEach(0 ..< 10) { i in + HStack { + KFImage(url(at: i)) + // ... + } + } + }.listStyle(.plain) +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-8.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-8.swift new file mode 100644 index 00000000..56e4647a --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-8.swift @@ -0,0 +1,20 @@ +var body: some View { + List { + Button("Check Cache") { + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + switch result { + case .success(let size): + print("Size: \(Double(size) / 1024 / 1024) MB") + case .failure(let error): + print("Some error: \(error)") + } + } + } + ForEach(0 ..< 10) { i in + HStack { + KFImage(url(at: i)) + // ... + } + } + }.listStyle(.plain) +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-9.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-9.swift new file mode 100644 index 00000000..0f0f86b7 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Documentation.docc/Resources/code-files/02-ContentView-9.swift @@ -0,0 +1,19 @@ +@State var showAlert = false +@State var cacheSizeResult: Result? = nil + +var body: some View { + List { + Button("Check Cache") { + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + cacheSizeResult = result + showAlert = true + } + } + ForEach(0 ..< 10) { i in + HStack { + KFImage(url(at: i)) + // ... + } + } + }.listStyle(.plain) +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift new file mode 100644 index 00000000..78dde039 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift @@ -0,0 +1,180 @@ + +// +// CPListItem+Kingfisher.swift +// Kingfisher +// +// Created by Wayne Hartman on 2021-08-29. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay + +@available(iOS 14.0, *) +@MainActor +extension KingfisherWrapper where Base: CPListItem { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? [])) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the image. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: (any Resource)?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + return setImage( + with: source, + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(image) + #else + if let image = image { + self.base.setImage(image) + } + #endif + }, + getImage: { + self.base.image + } + ), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { mutatingSelf.taskIdentifier = $0 }, + getTaskIdentifier: { mutatingSelf.taskIdentifier }, + setTask: { mutatingSelf.imageTask = $0 } + ), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +@MainActor private var taskIdentifierKey: Void? +@MainActor private var imageTaskKey: Void? + +// MARK: Properties +@MainActor +extension KingfisherWrapper where Base: CPListItem { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/HasImageComponent+Kingfisher.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/HasImageComponent+Kingfisher.swift new file mode 100644 index 00000000..633291c7 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -0,0 +1,499 @@ +// +// KingfisherHasImageComponent+Kingfisher.swift +// Kingfisher +// +// Created by JH on 2023/12/5. +// +// Copyright (c) 2023 Wei Wang +// +// 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. + +public protocol KingfisherImageSettable: KingfisherCompatible { + @MainActor func setImage( + _ image: KFCrossPlatformImage?, + options: KingfisherParsedOptionsInfo + ) + @MainActor func getImage() -> KFCrossPlatformImage? +} + +public protocol KingfisherHasImageComponent: KingfisherImageSettable { + @MainActor var image: KFCrossPlatformImage? { set get } +} + +extension KingfisherHasImageComponent { + @MainActor + public func setImage(_ image: KFCrossPlatformImage?, options: KingfisherParsedOptionsInfo) { + self.image = image + } + + @MainActor + public func getImage() -> KFCrossPlatformImage? { + image + } +} + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +@available(macOS 13.0, *) +extension NSComboButton: KingfisherHasImageComponent {} +@available(macOS 13.0, *) +extension NSColorWell: KingfisherHasImageComponent {} +extension NSTableViewRowAction: KingfisherHasImageComponent {} +extension NSMenuItem: KingfisherHasImageComponent {} +extension NSPathControlItem: KingfisherHasImageComponent {} +extension NSToolbarItem: KingfisherHasImageComponent {} +extension NSTabViewItem: KingfisherHasImageComponent {} +extension NSStatusItem: KingfisherHasImageComponent {} +extension NSCell: KingfisherHasImageComponent {} +#endif + +#if canImport(UIKit) && !os(watchOS) +import UIKit +@available(iOS 13.0, tvOS 13.0, *) +extension UIAction: KingfisherHasImageComponent {} +@available(iOS 13.0, tvOS 13.0, *) +extension UICommand: KingfisherHasImageComponent {} +extension UIBarItem: KingfisherHasImageComponent {} +#endif + +#if canImport(WatchKit) +import WatchKit +extension WKInterfaceImage: KingfisherHasImageComponent { + @MainActor public var image: KFCrossPlatformImage? { + get { nil } + set { setImage(newValue) } + } +} +#endif + +#if canImport(TVUIKit) +import TVUIKit +extension TVMonogramView: KingfisherHasImageComponent {} +#endif + +struct ImagePropertyAccessor: Sendable { + let setImage: @Sendable @MainActor (ImageType?, KingfisherParsedOptionsInfo) -> Void + let getImage: @Sendable @MainActor () -> ImageType? +} + +struct TaskPropertyAccessor: Sendable { + let setTaskIdentifier: @Sendable @MainActor (Source.Identifier.Value?) -> Void + let getTaskIdentifier: @Sendable @MainActor () -> Source.Identifier.Value? + let setTask: @Sendable @MainActor (DownloadTask?) -> Void +} + +@MainActor +extension KingfisherWrapper where Base: KingfisherImageSettable { + + // MARK: Setting Image + + /// Sets an image to the image view with a ``Source``. + /// + /// - Parameters: + /// - source: The ``Source`` object that defines data information from the network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension + /// methods. So the code above is equivalent to: + /// + /// ```swift + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a ``Source``. + /// + /// - Parameters: + /// - source: The ``Source`` object that defines data information from the network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension + /// methods. So the code above is equivalent to: + /// + /// ```swift + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: source, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested ``Resource``. + /// + /// - Parameters: + /// - resource: The ``Resource`` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a URL resource. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with resource: (any Resource)?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested ``Resource``. + /// + /// - Parameters: + /// - resource: The ``Resource`` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a URL resource. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with resource: (any Resource)?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: resource, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a ``ImageDataProvider``. + /// + /// - Parameters: + /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with provider: (any ImageDataProvider)?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: provider.map { .provider($0) }, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a ``ImageDataProvider``. + /// + /// - Parameters: + /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with provider: (any ImageDataProvider)?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: provider, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? { + return setImage( + with: source, + imageAccessor: ImagePropertyAccessor( + setImage: { base.setImage($0, options: $1) }, + getImage: { base.getImage() } + ), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { + var mutatingSelf = self + mutatingSelf.taskIdentifier = $0 + }, + getTaskIdentifier: { self.taskIdentifier }, + setTask: { task in + var mutatingSelf = self + mutatingSelf.imageTask = task + } + ), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } +} + +@MainActor +extension KingfisherWrapper { + func setImage( + with source: Source?, + imageAccessor: ImagePropertyAccessor, + taskAccessor: TaskPropertyAccessor, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + guard let source = source else { + imageAccessor.setImage(placeholder, parsedOptions) + taskAccessor.setTaskIdentifier(nil) + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + + // Always set placeholder while there is no image/placeholder yet. +#if os(watchOS) + let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading +#else + let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading || imageAccessor.getImage() == nil +#endif + if usePlaceholderDuringLoading { + imageAccessor.setImage(placeholder, options) + } + + let issuedIdentifier = Source.Identifier.next() + taskAccessor.setTaskIdentifier(issuedIdentifier) + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { task in + Task { @MainActor in taskAccessor.setTask(task) } + }, + progressiveImageSetter: { imageAccessor.setImage($0, options) }, + referenceTaskIdentifierChecker: { issuedIdentifier == taskAccessor.getTaskIdentifier() }, + completionHandler: { result in + CallbackQueueMain.currentOrAsync { + guard issuedIdentifier == taskAccessor.getTaskIdentifier() else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + taskAccessor.setTask(nil) + taskAccessor.setTaskIdentifier(nil) + + switch result { + case .success(let value): + imageAccessor.setImage(value.image, options) + case .failure: + if let image = options.onFailureImage { + imageAccessor.setImage(image, options) + } + } + completionHandler?(result) + } + } + ) + taskAccessor.setTask(task) + return task + } +} + +// MARK: - Associated Object +@MainActor private var taskIdentifierKey: Void? +@MainActor private var imageTaskKey: Void? + +@MainActor +extension KingfisherWrapper where Base: KingfisherImageSettable { + + // MARK: Properties + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + /// Cancels the image download task of the image view if it is running. + /// + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift new file mode 100644 index 00000000..9a54bcb6 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift @@ -0,0 +1,548 @@ +// +// ImageView+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if !os(watchOS) + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +@MainActor +extension KingfisherWrapper where Base: KFCrossPlatformImageView { + + // MARK: Setting Image + + /// Sets an image to the image view with a ``Source``. + /// + /// - Parameters: + /// - source: The ``Source`` object that defines data information from the network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension + /// methods. So the code above is equivalent to: + /// + /// ```swift + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with source: Source?, + placeholder: (any Placeholder)? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage(with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler) + } + + /// Sets an image to the image view with a ``Source``. + /// + /// - Parameters: + /// - source: The ``Source`` object that defines data information from the network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension + /// methods. So the code above is equivalent to: + /// + /// ```swift + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with source: Source?, + placeholder: (any Placeholder)? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: source, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested ``Resource``. + /// + /// - Parameters: + /// - resource: The ``Resource`` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a URL resource. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with resource: (any Resource)?, + placeholder: (any Placeholder)? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Sets an image to the image view with a requested ``Resource``. + /// + /// - Parameters: + /// - resource: The ``Resource`` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a URL resource. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with resource: (any Resource)?, + placeholder: (any Placeholder)? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: resource, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a ``ImageDataProvider``. + /// + /// - Parameters: + /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with provider: (any ImageDataProvider)?, + placeholder: (any Placeholder)? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: provider.map { .provider($0) }, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Sets an image to the image view with a ``ImageDataProvider``. + /// + /// - Parameters: + /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with provider: (any ImageDataProvider)?, + placeholder: (any Placeholder)? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: provider, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + func setImage( + with source: Source?, + placeholder: (any Placeholder)? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + mutatingSelf.placeholder = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + + let isEmptyImage = base.image == nil && self.placeholder == nil + if !options.keepCurrentImageWhileLoading || isEmptyImage { + // Always set placeholder while there is no image/placeholder yet. + mutatingSelf.placeholder = placeholder + } + + let maybeIndicator = indicator + maybeIndicator?.startAnimatingView() + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if base.shouldPreloadAllAnimation() { + options.preloadAllAnimationData = true + } + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { task in + Task { @MainActor in mutatingSelf.imageTask = task } + }, + progressiveImageSetter: { self.base.image = $0 }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, + completionHandler: { result in + CallbackQueueMain.currentOrAsync { + maybeIndicator?.stopAnimatingView() + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + guard self.needsTransition(options: options, cacheType: value.cacheType) else { + mutatingSelf.placeholder = nil + self.base.image = value.image + completionHandler?(result) + return + } + + self.makeTransition(image: value.image, transition: options.transition) { + completionHandler?(result) + } + + case .failure: + if let image = options.onFailureImage { + mutatingSelf.placeholder = nil + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the image view if it is running. + /// + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } + + private func needsTransition(options: KingfisherParsedOptionsInfo, cacheType: CacheType) -> Bool { + switch options.transition { + case .none: + return false + #if os(macOS) + case .fade: // Fade is only a placeholder for SwiftUI on macOS. + return false + #else + default: + if options.forceTransition { return true } + if cacheType == .none { return true } + return false + #endif + } + } + + private func makeTransition(image: KFCrossPlatformImage, transition: ImageTransition, done: @escaping () -> Void) { + #if !os(macOS) + // Force hiding the indicator without transition first. + UIView.transition( + with: self.base, + duration: 0.0, + options: [], + animations: { self.indicator?.stopAnimatingView() }, + completion: { _ in + var mutatingSelf = self + mutatingSelf.placeholder = nil + UIView.transition( + with: self.base, + duration: transition.duration, + options: [transition.animationOptions, .allowUserInteraction], + animations: { transition.animations?(self.base, image) }, + completion: { finished in + transition.completion?(finished) + done() + } + ) + } + ) + #else + done() + #endif + } +} + +// MARK: - Associated Object +@MainActor private var taskIdentifierKey: Void? +@MainActor private var indicatorKey: Void? +@MainActor private var indicatorTypeKey: Void? +@MainActor private var placeholderKey: Void? +@MainActor private var imageTaskKey: Void? + +@MainActor +extension KingfisherWrapper where Base: KFCrossPlatformImageView { + + // MARK: Properties + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + /// Specifies which indicator type is going to be used. + /// + /// The default is ``IndicatorType/none``, which means no indicator will be shown while downloading. + public var indicatorType: IndicatorType { + get { + return getAssociatedObject(base, &indicatorTypeKey) ?? .none + } + + set { + switch newValue { + case .none: indicator = nil + case .activity: indicator = ActivityIndicator() + case .image(let data): indicator = ImageIndicator(imageData: data) + case .custom(let anIndicator): indicator = anIndicator + } + + setRetainedAssociatedObject(base, &indicatorTypeKey, newValue) + } + } + + /// Holds any type that conforms to the protocol ``Indicator``. + /// + /// The protocol `Indicator` has a `view` property that will be shown when loading an image. + /// It will be `nil` if ``KingfisherWrapper/indicatorType`` is ``IndicatorType/none``. + public private(set) var indicator: (any Indicator)? { + get { + let box: Box? = getAssociatedObject(base, &indicatorKey) + return box?.value + } + + set { + // Remove previous + if let previousIndicator = indicator { + previousIndicator.view.removeFromSuperview() + } + + // Add new + if let newIndicator = newValue { + // Set default indicator layout + let view = newIndicator.view + + base.addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + view.centerXAnchor.constraint( + equalTo: base.centerXAnchor, constant: newIndicator.centerOffset.x).isActive = true + view.centerYAnchor.constraint( + equalTo: base.centerYAnchor, constant: newIndicator.centerOffset.y).isActive = true + + switch newIndicator.sizeStrategy(in: base) { + case .intrinsicSize: + break + case .full: + view.heightAnchor.constraint(equalTo: base.heightAnchor, constant: 0).isActive = true + view.widthAnchor.constraint(equalTo: base.widthAnchor, constant: 0).isActive = true + case .size(let size): + view.heightAnchor.constraint(equalToConstant: size.height).isActive = true + view.widthAnchor.constraint(equalToConstant: size.width).isActive = true + } + + newIndicator.view.isHidden = true + } + + // Save in associated object + // Wrap newValue with Box to workaround an issue that Swift does not recognize + // and casting protocol for associate object correctly. https://github.com/onevcat/Kingfisher/issues/872 + setRetainedAssociatedObject(base, &indicatorKey, newValue.map(Box.init)) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + /// Represents the ``Placeholder`` used for this image view. + /// + /// A ``Placeholder`` will be shown in the view while it is downloading an image. + public private(set) var placeholder: (any Placeholder)? { + get { return getAssociatedObject(base, &placeholderKey) } + set { + if let previousPlaceholder = placeholder { + previousPlaceholder.remove(from: base) + } + + if let newPlaceholder = newValue { + newPlaceholder.add(to: base) + } else { + base.image = nil + } + setRetainedAssociatedObject(base, &placeholderKey, newValue) + } + } +} + +extension KFCrossPlatformImageView { + @objc func shouldPreloadAllAnimation() -> Bool { return true } +} + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift new file mode 100644 index 00000000..cb8bd7c5 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift @@ -0,0 +1,275 @@ +// +// NSButton+Kingfisher.swift +// Kingfisher +// +// Created by Jie Zhang on 14/04/2016. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) + +import AppKit + +@MainActor +extension KingfisherWrapper where Base: NSButton { + + // MARK: Setting Image + + /// Sets an image to the button with a ``Source``. + /// + /// - Parameters: + /// - source: The ``Source`` object that defines data information from the network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the button with a ``Resource``. + /// + /// - Parameters: + /// - resource: The ``Resource`` object that defines data information from the network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with resource: (any Resource)?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + var mutatingSelf = self + return setImage( + with: source, + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in + base.image = image + }, getImage: { + base.image + }), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { mutatingSelf.taskIdentifier = $0 }, + getTaskIdentifier: { mutatingSelf.taskIdentifier }, + setTask: { mutatingSelf.imageTask = $0 }), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelImageDownloadTask() { + imageTask?.cancel() + } + + // MARK: Setting Alternate Image + + @discardableResult + public func setAlternateImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setAlternateImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an alternate image to the button with a ``Resource``. + /// + /// - Parameters: + /// - resource: The ``Resource`` object that defines data information from the network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setAlternateImage( + with resource: (any Resource)?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setAlternateImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setAlternateImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + var mutatingSelf = self + return setImage( + with: source, + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in + base.alternateImage = image + }, getImage: { + base.alternateImage + }), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { mutatingSelf.alternateTaskIdentifier = $0 }, + getTaskIdentifier: { mutatingSelf.alternateTaskIdentifier }, + setTask: { mutatingSelf.alternateImageTask = $0 } + ), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + // MARK: Cancelling Alternate Image Downloading Task + + /// Cancels the image download task of the image view if it is running. + /// + /// Nothing will happen if the downloading has already finished. + public func cancelAlternateImageDownloadTask() { + alternateImageTask?.cancel() + } +} + + +// MARK: - Associated Object +@MainActor private var taskIdentifierKey: Void? +@MainActor private var imageTaskKey: Void? + +@MainActor private var alternateTaskIdentifierKey: Void? +@MainActor private var alternateImageTaskKey: Void? + +@MainActor +extension KingfisherWrapper where Base: NSButton { + + // MARK: Properties + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + public private(set) var alternateTaskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &alternateTaskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box) + } + } + + private var alternateImageTask: DownloadTask? { + get { return getAssociatedObject(base, &alternateImageTaskKey) } + set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)} + } +} +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift new file mode 100644 index 00000000..1a68968b --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift @@ -0,0 +1,275 @@ +// +// NSTextAttachment+Kingfisher.swift +// Kingfisher +// +// Created by Benjamin Briggs on 22/07/2019. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if !os(watchOS) + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +@MainActor +extension KingfisherWrapper where Base: NSTextAttachment { + + // MARK: Setting Image + + /// Sets an image to the text attachment with a source. + /// + /// - Parameters: + /// - source: The ``Source`` object that defines data information from the network or a data provider. + /// - attributedView: The owner of the attributed string to which this `NSTextAttachment` is added. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the requested source. Since this method will + /// perform UI changes, it is your responsibility of calling it from the main thread. + /// + /// The retrieved image will be set to the `NSTextAttachment.image` property. Because it is not an image view-based + /// rendering, options related to the view, such as ``KingfisherOptionsInfoItem/transition(_:)``, are not supported. + /// + /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task is done. It gives the view a + /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an + /// attributed string with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. + /// + /// Here is a typical use case: + /// + /// ```swift + /// let label: UILabel = // ... + /// + /// let textAttachment = NSTextAttachment() + /// textAttachment.kf.setImage( + /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, + /// attributedView: label, + /// options: [ + /// .processor( + /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) + /// |> RoundCornerImageProcessor(cornerRadius: 15)) + /// ] + /// ) + /// + /// let attributedText = NSMutableAttributedString(string: "Hello World") + /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) + /// label.attributedText = attributedText + /// ``` + @discardableResult + public func setImage( + with source: Source?, + attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + attributedView: attributedView, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the text attachment with a source. + /// + /// - Parameters: + /// - resource: The ``Resource`` object that defines data information from the network or a data provider. + /// - attributedView: The owner of the attributed string to which this `NSTextAttachment` is added. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the requested source. Since this method will + /// perform UI changes, it is your responsibility of calling it from the main thread. + /// + /// The retrieved image will be set to the `NSTextAttachment.image` property. Because it is not an image view-based + /// rendering, options related to the view, such as ``KingfisherOptionsInfoItem/transition(_:)``, are not supported. + /// + /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task is done. It gives the view a + /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an + /// attributed string with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. + /// + /// Here is a typical use case: + /// + /// ```swift + /// let label: UILabel = // ... + /// + /// let textAttachment = NSTextAttachment() + /// textAttachment.kf.setImage( + /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, + /// attributedView: label, + /// options: [ + /// .processor( + /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) + /// |> RoundCornerImageProcessor(cornerRadius: 15)) + /// ] + /// ) + /// + /// let attributedText = NSMutableAttributedString(string: "Hello World") + /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) + /// label.attributedText = attributedText + /// ``` + @discardableResult + public func setImage( + with resource: (any Resource)?, + attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: resource.map { .network($0) }, + attributedView: attributedView, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + func setImage( + with source: Source?, + attributedView: @escaping @Sendable () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + progressiveImageSetter: { self.base.image = $0 }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, + completionHandler: { result in + CallbackQueueMain.currentOrAsync { + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + self.base.image = value.image + let view = attributedView() + #if canImport(UIKit) + view.setNeedsDisplay() + #else + view.setNeedsDisplay(view.bounds) + #endif + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + } + completionHandler?(result) + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bound to the text attachment if it is running. + /// + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +@MainActor private var taskIdentifierKey: Void? +@MainActor private var imageTaskKey: Void? + +// MARK: Properties +@MainActor +extension KingfisherWrapper where Base: NSTextAttachment { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/PHLivePhotoView+Kingfisher.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/PHLivePhotoView+Kingfisher.swift new file mode 100644 index 00000000..c88cd5d4 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/PHLivePhotoView+Kingfisher.swift @@ -0,0 +1,308 @@ +// +// PHLivePhotoView+Kingfisher.swift +// Kingfisher +// +// Created by onevcat on 2024/10/04. +// +// Copyright (c) 2024 Wei Wang +// +// 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. + +#if os(watchOS) +// Only a placeholder. +public struct RetrieveLivePhotoResult: @unchecked Sendable { +} +#else +@preconcurrency import PhotosUI + +/// A result type that contains the information of a retrieved live photo. +/// +/// This struct is used to encapsulate the result of a live photo retrieval operation, including the loading information, +/// the retrieved `PHLivePhoto` object, and any additional information provided by the result handler. +/// +/// - Note: The `info` dictionary is considered sendable based on the documentation for "Result Handler Info Dictionary Keys". +/// See: [Result Handler Info Dictionary Keys](https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys) +public struct RetrieveLivePhotoResult: @unchecked Sendable { + /// The loading information of the live photo. + public let loadingInfo: LivePhotoLoadingInfoResult + + /// The retrieved live photo object which is given by the + /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method from + /// the result handler. + public let livePhoto: PHLivePhoto? + + + // According to "Result Handler Info Dictionary Keys", we can trust the `info` in handler is sendable. + // https://developer.apple.com/documentation/photokit/phlivephoto/result_handler_info_dictionary_keys + /// The additional information provided by the result handler when retrieving the live photo. + public let info: [AnyHashable : Any]? +} + +@MainActor private var taskIdentifierKey: Void? +@MainActor private var targetSizeKey: Void? +@MainActor private var contentModeKey: Void? + +@MainActor +extension KingfisherWrapper where Base: PHLivePhotoView { + /// Gets the task identifier associated with the image view for the live photo task. + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + /// The target size of the live photo view. It is used in the + /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as + /// the `targetSize` argument when loading the live photo. + /// + /// If not set, `.zero` will be used. + public var targetSize: CGSize { + get { getAssociatedObject(base, &targetSizeKey) ?? .zero } + set { setRetainedAssociatedObject(base, &targetSizeKey, newValue) } + } + + /// The content mode of the live photo view. It is used in the + /// `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` method as + /// the `contentMode` argument when loading the live photo. + /// + /// If not set, `.default` will be used. + public var contentMode: PHImageContentMode { + get { getAssociatedObject(base, &contentModeKey) ?? .default } + set { setRetainedAssociatedObject(base, &contentModeKey, newValue) } + } + + /// Sets a live photo to the view with an array of `URL`. + /// + /// - Parameters: + /// - urls: The `URL`s defining the live photo resource. It should contains two URLs, one for the still image and + /// one for the video. + /// - options: An options set to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image setting process finishes. + /// - Returns: A task represents the image downloading. + /// The return value will be `nil` if the image is set with a empty source. + /// + /// - Note: Not all options in ``KingfisherOptionsInfo`` are supported in this method, for example, the live photo + /// does not support any custom processors. Different from the extension method for a normal image view on the + /// platform, the `placeholder` and `progressBlock` are not supported yet, and will be implemented in the future. + /// + /// - Note: To get refined control of the resources, use the ``setImage(with:options:completionHandler:)-1n4p2`` + /// method with a ``LivePhotoSource`` object. + /// + /// Example: + /// + /// ```swift + /// let urls = [ + /// URL(string: "https://example.com/image.heic")!, // imageURL + /// URL(string: "https://example.com/video.mov")! // videoURL + /// ] + /// let livePhotoView = PHLivePhotoView() + /// livePhotoView.kf.setImage(with: urls) { result in + /// switch result { + /// case .success(let retrieveResult): + /// print("Live photo loaded: \(retrieveResult.livePhoto).") + /// print("Cache type: \(retrieveResult.loadingInfo.cacheType).") + /// case .failure(let error): + /// print("Error: \(error)") + /// } + /// ``` + @discardableResult + public func setImage( + with urls: [URL], + // placeholder: KFCrossPlatformImage? = nil, // Not supported yet + options: KingfisherOptionsInfo? = nil, + // progressBlock: DownloadProgressBlock? = nil, // Not supported yet + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> Task<(), Never>? { + setImage( + with: LivePhotoSource(urls: urls), + options: options, + completionHandler: completionHandler + ) + } + + /// Sets a live photo to the view with a ``LivePhotoSource``. + /// + /// - Parameters: + /// - source: The ``LivePhotoSource`` object defining the live photo resource. + /// - options: An options set to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image setting process finishes. + /// - Returns: A task represents the image downloading. + /// The return value will be `nil` if the image is set with a empty source. + /// + /// - Note: Not all options in ``KingfisherOptionsInfo`` are supported in this method, for example, the live photo + /// does not support any custom processors. Different from the extension method for a normal image view on the + /// platform, the `placeholder` and `progressBlock` are not supported yet, and will be implemented in the future. + /// + /// Sample: + /// ```swift + /// let source = LivePhotoSource(urls: [ + /// URL(string: "https://example.com/image.heic")!, // imageURL + /// URL(string: "https://example.com/video.mov")! // videoURL + /// ]) + /// let livePhotoView = PHLivePhotoView() + /// livePhotoView.kf.setImage(with: source) { result in + /// switch result { + /// case .success(let retrieveResult): + /// print("Live photo loaded: \(retrieveResult.livePhoto).") + /// print("Cache type: \(retrieveResult.loadingInfo.cacheType).") + /// case .failure(let error): + /// print("Error: \(error)") + /// } + /// ``` + @discardableResult + public func setImage( + with source: LivePhotoSource?, + // placeholder: KFCrossPlatformImage? = nil, // Not supported yet + options: KingfisherOptionsInfo? = nil, + // progressBlock: DownloadProgressBlock? = nil, // Not supported yet + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> Task<(), Never>? { + var mutatingSelf = self + + // Empty source fails the loading early and clear the current task identifier. + guard let source = source else { + base.livePhoto = nil + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + let taskIdentifierChecking = { issuedIdentifier == self.taskIdentifier } + + // Copy these associated values to prevent issues from reentrance. + let targetSize = targetSize + let contentMode = contentMode + + let task = Task { @MainActor in + do { + let loadingInfo = try await KingfisherManager.shared.retrieveLivePhoto( + with: source, + options: options, + progressBlock: nil, // progressBlock, // Not supported yet + referenceTaskIdentifierChecker: taskIdentifierChecking + ) + if let notCurrentTaskError = self.checkNotCurrentTask( + issuedIdentifier: issuedIdentifier, + result: .init(loadingInfo: loadingInfo, livePhoto: nil, info: nil), + error: nil, + source: source + ) { + completionHandler?(.failure(notCurrentTaskError)) + return + } + + PHLivePhoto.request( + withResourceFileURLs: loadingInfo.fileURLs, + placeholderImage: nil, + targetSize: targetSize, + contentMode: contentMode, + resultHandler: { livePhoto, info in + let result = RetrieveLivePhotoResult( + loadingInfo: loadingInfo, + livePhoto: livePhoto, + info: info + ) + + if let notCurrentTaskError = self.checkNotCurrentTask( + issuedIdentifier: issuedIdentifier, + result: result, + error: nil, + source: source + ) { + completionHandler?(.failure(notCurrentTaskError)) + return + } + + base.livePhoto = livePhoto + + if let error = info[PHLivePhotoInfoErrorKey] as? NSError { + let failingReason: KingfisherError.ImageSettingErrorReason = + .livePhotoResultError(result: result, error: error, source: source) + completionHandler?(.failure(.imageSettingError(reason: failingReason))) + return + } + + // Since we are not returning the request ID, seems no way for user to cancel it if the + // `request` method is called. However, we are sure the request method will always load the + // image from disk, it should not be a problem. In case we still report the error in the + // completion + if (info[PHLivePhotoInfoCancelledKey] as? NSNumber)?.boolValue ?? false { + completionHandler?(.failure( + .requestError(reason: .livePhotoTaskCancelled(source: source))) + ) + return + } + + // If the PHLivePhotoInfoIsDegradedKey value in your result handler’s info dictionary is true, + // Photos will call your result handler again. + if (info[PHLivePhotoInfoIsDegradedKey] as? NSNumber)?.boolValue == true { + // This ensures `completionHandler` be only called once. + return + } + + completionHandler?(.success(result)) + } + ) + } catch { + if let notCurrentTaskError = self.checkNotCurrentTask( + issuedIdentifier: issuedIdentifier, + result: nil, + error: error, + source: source + ) { + completionHandler?(.failure(notCurrentTaskError)) + return + } + + if let kfError = error as? KingfisherError { + completionHandler?(.failure(kfError)) + } else if error is CancellationError { + completionHandler?(.failure(.requestError(reason: .livePhotoTaskCancelled(source: source)))) + } else { + completionHandler?(.failure(.imageSettingError( + reason: .livePhotoResultError(result: nil, error: error, source: source))) + ) + } + } + } + + return task + } + + private func checkNotCurrentTask( + issuedIdentifier: Source.Identifier.Value, + result: RetrieveLivePhotoResult?, + error: (any Error)?, + source: LivePhotoSource + ) -> KingfisherError? { + if issuedIdentifier == self.taskIdentifier { + return nil + } + return .imageSettingError(reason: .notCurrentLivePhotoSourceTask(result: result, error: error, source: source)) + } +} +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift new file mode 100644 index 00000000..3b36e4ba --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift @@ -0,0 +1,319 @@ +// +// UIButton+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/13. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if !os(watchOS) + +#if canImport(UIKit) +import UIKit + +@MainActor +extension KingfisherWrapper where Base: UIButton { + + // MARK: Setting Image + /// Sets an image to the button for a specified state with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + for: state, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the button for a specified state with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: (any Resource)?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + @discardableResult + public func setImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + return setImage( + with: source, + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in base.setImage(image, for: state) }, + getImage: { base.image(for: state) } + ), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { setTaskIdentifier($0, for: state) }, + getTaskIdentifier: { taskIdentifier(for: state) }, + setTask: { mutatingSelf.imageTask = $0 } + ), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelImageDownloadTask() { + imageTask?.cancel() + } + + // MARK: Setting Background Image + + /// Sets a background image to the button for a specified state with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setBackgroundImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setBackgroundImage( + with: source, + for: state, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets a background image to the button for a specified state with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setBackgroundImage( + with resource: (any Resource)?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@Sendable (Result) -> Void)? = nil) -> DownloadTask? + { + return setBackgroundImage( + with: resource?.convertToSource(), + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setBackgroundImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + return setImage( + with: source, + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in + base.setBackgroundImage(image, for: state) + }, + getImage: { + base.backgroundImage(for: state) + } + ), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { setBackgroundTaskIdentifier($0, for: state) }, + getTaskIdentifier: { backgroundTaskIdentifier(for: state) }, + setTask: { mutatingSelf.backgroundImageTask = $0 } + ), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + // MARK: Cancelling Background Downloading Task + + /// Cancels the background image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelBackgroundImageDownloadTask() { + backgroundImageTask?.cancel() + } +} + +// MARK: - Associated Object +@MainActor private var taskIdentifierKey: Void? +@MainActor private var imageTaskKey: Void? + +// MARK: Properties +@MainActor +extension KingfisherWrapper where Base: UIButton { + + private typealias TaskIdentifier = Box<[UInt: Source.Identifier.Value]> + + public func taskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { + return taskIdentifierInfo.value[state.rawValue] + } + + private func setTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { + taskIdentifierInfo.value[state.rawValue] = identifier + } + + private var taskIdentifierInfo: TaskIdentifier { + return getAssociatedObject(base, &taskIdentifierKey) ?? { + setRetainedAssociatedObject(base, &taskIdentifierKey, $0) + return $0 + } (TaskIdentifier([:])) + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + + +@MainActor private var backgroundTaskIdentifierKey: Void? +@MainActor private var backgroundImageTaskKey: Void? + +// MARK: Background Properties +@MainActor +extension KingfisherWrapper where Base: UIButton { + + public func backgroundTaskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { + return backgroundTaskIdentifierInfo.value[state.rawValue] + } + + private func setBackgroundTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { + backgroundTaskIdentifierInfo.value[state.rawValue] = identifier + } + + private var backgroundTaskIdentifierInfo: TaskIdentifier { + return getAssociatedObject(base, &backgroundTaskIdentifierKey) ?? { + setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, $0) + return $0 + } (TaskIdentifier([:])) + } + + private var backgroundImageTask: DownloadTask? { + get { return getAssociatedObject(base, &backgroundImageTaskKey) } + mutating set { setRetainedAssociatedObject(base, &backgroundImageTaskKey, newValue) } + } +} +#endif + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift new file mode 100644 index 00000000..6aedae40 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift @@ -0,0 +1,157 @@ +// +// AVAssetImageDataProvider.swift +// Kingfisher +// +// Created by onevcat on 2020/08/09. +// +// Copyright (c) 2020 Wei Wang +// +// 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. + +#if !os(watchOS) + +import Foundation +import AVKit + +#if canImport(MobileCoreServices) +import MobileCoreServices +#else +import CoreServices +#endif + +#if compiler(>=6) +extension AVAssetImageGenerator: @unchecked @retroactive Sendable { } +#else +extension AVAssetImageGenerator: @unchecked Sendable { } +#endif + +/// A data provider to provide thumbnail data from a given AVKit asset. +public struct AVAssetImageDataProvider: ImageDataProvider { + + /// The possible error might be caused by the ``AVAssetImageDataProvider``. + public enum AVAssetImageDataProviderError: Error { + /// The data provider process is cancelled. + case userCancelled + /// The retrieved image is invalid. + /// - Parameter image: The image object that is not recognized as valid. + case invalidImage(_ image: CGImage?) + } + + /// The asset image generator bound to `self`. + public let assetImageGenerator: AVAssetImageGenerator + + /// The time at which the image should be generated in the asset. + public let time: CMTime + + private var internalKey: String { + guard let url = (assetImageGenerator.asset as? AVURLAsset)?.url else { + return UUID().uuidString + } + return url.cacheKey + } + + /// The cache key used by `self`. + public var cacheKey: String { + return "\(internalKey)_\(time.seconds)" + } + + /// Creates an asset image data provider. + /// - Parameters: + /// - assetImageGenerator: The asset image generator that controls data providing behaviors. + /// - time: The time at which the image should be generated in the asset. + public init(assetImageGenerator: AVAssetImageGenerator, time: CMTime) { + self.assetImageGenerator = assetImageGenerator + self.time = time + } + + /// Creates an asset image data provider. + /// - Parameters: + /// - assetURL: The URL of asset for providing image data. + /// - time: At which time in the asset the image should be generated. + /// + /// This method uses the `assetURL` parameter to create an `AVAssetImageGenerator` object, then calls + /// the ``init(assetImageGenerator:time:)`` initializer. + public init(assetURL: URL, time: CMTime) { + let asset = AVAsset(url: assetURL) + let generator = AVAssetImageGenerator(asset: asset) + generator.appliesPreferredTrackTransform = true + self.init(assetImageGenerator: generator, time: time) + } + + /// Creates an asset image data provider. + /// + /// - Parameters: + /// - assetURL: The URL of asset for providing image data. + /// - seconds: At which time in seconds in the asset the image should be generated. + /// + /// This method uses the `assetURL` parameter to create an `AVAssetImageGenerator` object, uses the `seconds` + /// parameter to create a `CMTime`, then calls the ``init(assetImageGenerator:time:)`` initializer. + /// + public init(assetURL: URL, seconds: TimeInterval) { + let time = CMTime(seconds: seconds, preferredTimescale: 600) + self.init(assetURL: assetURL, time: time) + } + + public func data(handler: @Sendable @escaping (Result) -> Void) { + assetImageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { + (requestedTime, image, imageTime, result, error) in + if let error = error { + handler(.failure(error)) + return + } + + if result == .cancelled { + handler(.failure(AVAssetImageDataProviderError.userCancelled)) + return + } + + guard let cgImage = image, let data = cgImage.jpegData else { + handler(.failure(AVAssetImageDataProviderError.invalidImage(image))) + return + } + + handler(.success(data)) + } + } +} + +extension CGImage { + var jpegData: Data? { + guard let mutableData = CFDataCreateMutable(nil, 0) else { + return nil + } +#if os(visionOS) + guard let destination = CGImageDestinationCreateWithData( + mutableData, UTType.jpeg.identifier as CFString , 1, nil + ) else { + return nil + } +#else + guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else { + return nil + } +#endif + + CGImageDestinationAddImage(destination, self, nil) + guard CGImageDestinationFinalize(destination) else { return nil } + return mutableData as Data + } +} + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift new file mode 100644 index 00000000..54855570 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift @@ -0,0 +1,274 @@ +// +// ImageDataProvider.swift +// Kingfisher +// +// Created by onevcat on 2018/11/13. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +import ImageIO + +/// Represents a data provider to provide image data to Kingfisher when setting with +/// ``Source/provider(_:)`` source. Compared to ``Source/network(_:)`` member, it gives a chance +/// to load some image data in your own way, as long as you can provide the data +/// representation for the image. +public protocol ImageDataProvider: Sendable { + + /// The key used in cache. + var cacheKey: String { get } + + /// Provides the data which represents image. Kingfisher uses the data you pass in the + /// handler to process images and caches it for later use. + /// + /// - Parameter handler: The handler you should call when you prepared your data. + /// If the data is loaded successfully, call the handler with + /// a `.success` with the data associated. Otherwise, call it + /// with a `.failure` and pass the error. + /// + /// - Note: If the `handler` is called with a `.failure` with error, + /// a ``KingfisherError/ImageSettingErrorReason/dataProviderError(provider:error:)`` will be finally thrown out to + /// you as the ``KingfisherError`` from the framework. + func data(handler: @escaping @Sendable (Result) -> Void) + + /// The content URL represents this provider, if exists. + var contentURL: URL? { get } +} + +extension ImageDataProvider { + func data() async throws -> Data { + try await withCheckedThrowingContinuation { continuation in + data(handler: { continuation.resume(with: $0) }) + } + } +} + +public extension ImageDataProvider { + var contentURL: URL? { return nil } + func convertToSource() -> Source { + .provider(self) + } +} + +/// Represents an image data provider for loading from a local file URL on disk. +/// Uses this type for adding a disk image to Kingfisher. Compared to loading it +/// directly, you can get benefit of using Kingfisher's extension methods, as well +/// as applying ``ImageProcessor``s and storing the image to ``ImageCache`` of Kingfisher. +public struct LocalFileImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + + /// The file URL from which the image be loaded. + public let fileURL: URL + private let loadingQueue: ExecutionQueue + + // MARK: Initializers + + /// Creates an image data provider by supplying the target local file URL. + /// + /// - Parameters: + /// - fileURL: The file URL from which the image be loaded. + /// - cacheKey: The key is used for caching the image data. By default, + /// the `absoluteString` of ``LocalFileImageDataProvider/fileURL`` is used. + /// - loadingQueue: The queue where the file loading should happen. By default, the dispatch queue of + /// `.global(qos: .userInitiated)` will be used. + public init( + fileURL: URL, + cacheKey: String? = nil, + loadingQueue: ExecutionQueue = .dispatch(DispatchQueue.global(qos: .userInitiated)) + ) { + self.fileURL = fileURL + self.cacheKey = cacheKey ?? fileURL.localFileCacheKey + self.loadingQueue = loadingQueue + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: @escaping @Sendable (Result) -> Void) { + loadingQueue.execute { + handler(Result(catching: { try Data(contentsOf: fileURL) })) + } + } + + public var data: Data { + get async throws { + try await withCheckedThrowingContinuation { continuation in + loadingQueue.execute { + do { + let data = try Data(contentsOf: fileURL) + continuation.resume(returning: data) + } catch { + continuation.resume(throwing: error) + } + } + } + } + } + + /// The URL of the local file on the disk. + public var contentURL: URL? { + return fileURL + } +} + +/// Represents an image data provider for loading image from a given Base64 encoded string. +public struct Base64ImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + /// The encoded Base64 string for the image. + public let base64String: String + + // MARK: Initializers + + /// Creates an image data provider by supplying the Base64 encoded string. + /// + /// - Parameters: + /// - base64String: The Base64 encoded string for an image. + /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. + public init(base64String: String, cacheKey: String) { + self.base64String = base64String + self.cacheKey = cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: (Result) -> Void) { + let data = Data(base64Encoded: base64String)! + handler(.success(data)) + } +} + +/// Represents an image data provider for a raw data object. +public struct RawImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + + /// The raw data object to provide to Kingfisher image loader. + public let data: Data + + // MARK: Initializers + + /// Creates an image data provider by the given raw `data` value and a `cacheKey` be used in Kingfisher cache. + /// + /// - Parameters: + /// - data: The raw data represents an image. + /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. + public init(data: Data, cacheKey: String) { + self.data = data + self.cacheKey = cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: @escaping (Result) -> Void) { + handler(.success(data)) + } +} + +/// A data provider that creates a thumbnail from a URL using Core Graphics. +public struct ThumbnailImageDataProvider: ImageDataProvider { + + public enum ThumbnailImageDataProviderError: Error { + case invalidImageSource + case invalidThumbnail + case writeDataError + case finalizeDataError + } + + /// The URL from which to load the image + public let url: URL + + /// The maximum size of the thumbnail in pixels + public var maxPixelSize: CGFloat + + /// Whether to always create a thumbnail even if the image is smaller than maxPixelSize + public var alwaysCreateThumbnail: Bool + + /// The cache key for this provider + public var cacheKey: String + + /// Creates a new thumbnail data provider + /// - Parameters: + /// - url: The URL from which to load the image + /// - maxPixelSize: The maximum size of the thumbnail in pixels + /// - alwaysCreateThumbnail: Whether to always create a thumbnail even if the image is smaller than maxPixelSize + public init( + url: URL, + maxPixelSize: CGFloat, + alwaysCreateThumbnail: Bool = true, + cacheKey: String? = nil + ) { + self.url = url + self.maxPixelSize = maxPixelSize + self.alwaysCreateThumbnail = alwaysCreateThumbnail + self.cacheKey = cacheKey ?? "\(url.absoluteString)_thumb_\(maxPixelSize)_\(alwaysCreateThumbnail)" + } + + public func data(handler: @escaping @Sendable (Result) -> Void) { + DispatchQueue.global(qos: .userInitiated).async { + do { + guard let url = URL(string: url.absoluteString) else { + throw KingfisherError.imageSettingError(reason: .emptySource) + + } + + guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { + throw ThumbnailImageDataProviderError.invalidImageSource + } + + let options = [ + kCGImageSourceThumbnailMaxPixelSize: maxPixelSize, + kCGImageSourceCreateThumbnailFromImageAlways: alwaysCreateThumbnail, + kCGImageSourceCreateThumbnailWithTransform: true + ] + + guard let thumbnailRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { + throw ThumbnailImageDataProviderError.invalidThumbnail + } + + let data = NSMutableData() + guard let destination = CGImageDestinationCreateWithData( + data, CGImageSourceGetType(imageSource)!, 1, nil + ) else { + throw ThumbnailImageDataProviderError.writeDataError + } + + CGImageDestinationAddImage(destination, thumbnailRef, nil) + if CGImageDestinationFinalize(destination) { + handler(.success(data as Data)) + } else { + throw ThumbnailImageDataProviderError.finalizeDataError + } + } catch { + handler(.failure(error)) + } + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/LivePhotoSource.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/LivePhotoSource.swift new file mode 100644 index 00000000..eb3bda6d --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/LivePhotoSource.swift @@ -0,0 +1,205 @@ +// +// LivePhotoSource.swift +// Kingfisher +// +// Created by onevcat on 2024/10/01. +// +// Copyright (c) 2024 Wei Wang +// +// 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. + +import Foundation + +/// A type represents a loadable resource for a Live Photo, which consists of a still image and a video. +/// +/// Kingfisher expects a ``LivePhotoSource`` value to load a Live Photo with its high-level APIs. +/// A ``LivePhotoSource`` is typically a collection of two ``LivePhotoResource`` values, one for the still image and +/// one for the video. +public struct LivePhotoSource: Sendable { + + /// The resources of a Live Photo. + public let resources: [LivePhotoResource] + + /// Creates a Live Photo source with given resources. + /// - Parameter resources: The downloadable resource for a Live Photo. It should contain two resources, one for the + /// still image and one for the video. + public init(resources: [any Resource]) { + let livePhotoResources = resources.map { LivePhotoResource(resource: $0) } + self.init(livePhotoResources) + } + + /// Creates a Live Photo source with given URLs. + /// - Parameter urls: The URLs of the downloadable resources for a Live Photo. It should contain two URLs, one for + /// the still image and one for the video. + public init(urls: [URL]) { + let resources = urls.map { KF.ImageResource(downloadURL: $0) } + self.init(resources: resources) + } + + /// Creates a Live Photo source with given resources. + /// - Parameter resources: The resources for a Live Photo. It should contain two resources, one for the still image + /// and one for the video. + public init(_ resources: [LivePhotoResource]) { + self.resources = resources + } +} + + +/// A resource type representing a component of a Live Photo, which consists of a still image and a video. +/// +/// ``LivePhotoResource`` encapsulates the necessary information to download and cache a single component of a Live +/// Photo: it is either a still image (typically in HEIF format with "heic" filename extension) or a video (typically in +/// QuickTime format with "mov" filename extension). Multiple ``LivePhotoResource`` values (typically two, one for the +/// image and one for the video) can form a ``LivePhotoSource``, which is expected by Kingfisher in its live photo +/// loading high level APIs. +/// +/// The Live Photo data can be retrieved by `PHAssetResourceManager.requestData` method and uploaded to your server. +/// You should not modify the metadata or other information of the data, otherwise, it is possible that the +/// `PHLivePhoto` class cannot read and recognize it anymore. For more information, please refer to Apple's +/// documentation of Photos framework. +public struct LivePhotoResource: Sendable { + + /// The file type of a ``LivePhotoResource``. + public enum FileType: Sendable, Equatable { + /// File type HEIC. Usually it represents the still image in a Live Photo. + case heic + /// File type MOV. Usually it represents the video in a Live Photo. + case mov + /// Other file types with the file extension. + case other(String) + + var fileExtension: String { + switch self { + case .heic: return "heic" + case .mov: return "mov" + case .other(let ext): return ext + } + } + } + + /// The data source of a Live Photo resource. + /// + /// This is a general ``Source`` type, which can be either a network resource (as ``Source/network(_:)``) or a + /// provided resource as ``Source/provider(_:)``. + public let dataSource: Source + + /// The file type of the resource. + public let referenceFileType: FileType + + var cacheKey: String { dataSource.cacheKey } + var downloadURL: URL? { dataSource.url } + + /// Creates a Live Photo resource with given download URL, cache key and file type. + /// - Parameters: + /// - downloadURL: The URL to download the resource. + /// - cacheKey: The cache key for the resource. If `nil`, Kingfisher will use the `absoluteString` of the URL as + /// the cache key. + /// - fileType: The file type of the resource. If `nil`, Kingfisher will try to guess the file type from the URL. + /// + /// The file type is important for Kingfisher to determine how to handle the downloaded data and store them + /// in the cache. Photos framework requires the still image to be in HEIC extension and the video to be in MOV + /// extension. Otherwise, the `PHLivePhoto` class might not be able to recognize the data. If you are not sure about + /// the file type, you can leave it as `nil` and Kingfisher will try to guess it from the URL and the downloaded + /// data. + public init(downloadURL: URL, cacheKey: String? = nil, fileType: FileType? = nil) { + let resource = KF.ImageResource(downloadURL: downloadURL, cacheKey: cacheKey) + dataSource = .network(resource) + referenceFileType = fileType ?? resource.guessedFileType + } + + /// Creates a Live Photo resource with given resource and file type. + /// - Parameters: + /// - resource: The resource to download the data. + /// - fileType: The file type of the resource. If `nil`, Kingfisher will try to guess the file type from the URL. + /// + /// The file type is important for Kingfisher to determine how to handle the downloaded data and store them + /// in the cache. Photos framework requires the still image to be in HEIC extension and the video to be in MOV + /// extension. Otherwise, the `PHLivePhoto` class might not be able to recognize the data. If you are not sure about + /// the file type, you can leave it as `nil` and Kingfisher will try to guess it from the URL and the downloaded + /// data. + public init(resource: any Resource, fileType: FileType? = nil) { + self.dataSource = .network(resource) + referenceFileType = fileType ?? resource.guessedFileType + } + + /// Creates a Live Photo resource with given data source and file type. + /// - Parameters: + /// - source: The data source of the resource. It can be either a network resource or a provided resource. + /// - fileType: The file type of the resource. If `nil`, Kingfisher will try to guess the file type from the URL. + /// + /// The file type is important for Kingfisher to determine how to handle the downloaded data and store them + /// in the cache. Photos framework requires the still image to be in HEIC extension and the video to be in MOV + /// extension. Otherwise, the `PHLivePhoto` class might not be able to recognize the data. If you are not sure about + /// the file type, you can leave it as `nil` and Kingfisher will try to guess it from the URL and the downloaded + /// data. + public init(source: Source, fileType: FileType? = nil) { + self.dataSource = source + referenceFileType = fileType ?? source.url?.guessedFileType ?? .other("") + } +} + +extension LivePhotoResource.FileType { + func determinedFileExtension(_ data: Data) -> String? { + switch self { + case .mov: return "mov" + case .heic: return "heic" + case .other(let ext): + if !ext.isEmpty { + return ext + } + return Self.guessedFileExtension(from: data) + } + } + + static let fytpChunk: [UInt8] = [0x66, 0x74, 0x79, 0x70] // fytp (file type box) + static let heicChunk: [UInt8] = [0x68, 0x65, 0x69, 0x63] // heic (HEIF) + static let qtChunk: [UInt8] = [0x71, 0x74, 0x20, 0x20] // qt (QuickTime), .mov + + static func guessedFileExtension(from data: Data) -> String? { + + guard data.count >= 12 else { return nil } + + var buffer = [UInt8](repeating: 0, count: 12) + data.copyBytes(to: &buffer, count: 12) + + guard Array(buffer[4..<8]) == fytpChunk else { + return nil + } + + let fileTypeChunk = Array(buffer[8..<12]) + if fileTypeChunk == heicChunk { + return "heic" + } + if fileTypeChunk == qtChunk { + return "mov" + } + return nil + } +} + +extension Resource { + var guessedFileType: LivePhotoResource.FileType { + let pathExtension = downloadURL.pathExtension.lowercased() + switch pathExtension { + case "mov": return .mov + case "heic": return .heic + default: return .other(pathExtension) + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/PHPickerResultImageDataProvider.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/PHPickerResultImageDataProvider.swift new file mode 100644 index 00000000..364ed8af --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/PHPickerResultImageDataProvider.swift @@ -0,0 +1,94 @@ +// +// PHPickerResultImageDataProvider.swift +// Kingfisher +// +// Created by nuomi1 on 2024-04-17. +// +// Copyright (c) 2024 Wei Wang +// +// 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. + +import Foundation + +#if os(iOS) || os(macOS) || os(visionOS) + +import PhotosUI + +#if compiler(>=6) +@available(iOS 14.0, macOS 13.0, *) +extension PHPickerResult: @unchecked @retroactive Sendable { } +#else +@available(iOS 14.0, macOS 13.0, *) +extension PHPickerResult: @unchecked Sendable { } +#endif + +/// A data provider to provide image data from a given `PHPickerResult`. +@available(iOS 14.0, macOS 13.0, *) +public struct PHPickerResultImageDataProvider: ImageDataProvider { + + /// The possible error might be caused by the `PHPickerResultImageDataProvider`. + /// - invalidImage: The retrieved image is invalid. + public enum PHPickerResultImageDataProviderError: Error { + /// An error happens during picking up image through the item provider of `PHPickerResult`. + case pickerProviderError(any Error) + /// The retrieved image is invalid. + case invalidImage + } + + /// The picker result bound to `self`. + public let pickerResult: PHPickerResult + + /// The content type of the image. + public let contentType: UTType + + private var internalKey: String { + pickerResult.assetIdentifier ?? UUID().uuidString + } + + public var cacheKey: String { + "\(internalKey)_\(contentType.identifier)" + } + + /// Creates an image data provider from a given `PHPickerResult`. + /// - Parameters: + /// - pickerResult: The picker result to provide image data. + /// - contentType: The content type of the image. Default is `UTType.image`. + public init(pickerResult: PHPickerResult, contentType: UTType = UTType.image) { + self.pickerResult = pickerResult + self.contentType = contentType + } + + public func data(handler: @escaping @Sendable (Result) -> Void) { + pickerResult.itemProvider.loadDataRepresentation(forTypeIdentifier: contentType.identifier) { data, error in + if let error { + handler(.failure(PHPickerResultImageDataProviderError.pickerProviderError(error))) + return + } + + guard let data else { + handler(.failure(PHPickerResultImageDataProviderError.invalidImage)) + return + } + + handler(.success(data)) + } + } +} + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift new file mode 100644 index 00000000..42a1d1fa --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift @@ -0,0 +1,128 @@ +// +// Resource.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +/// Represents an image resource at a certain url and a given cache key. +/// Kingfisher will use a ``Resource`` to download a resource from network and cache it with the cache key when +/// using ``Source/network(_:)`` as its image setting source. +public protocol Resource: Sendable { + + /// The key used in cache. + var cacheKey: String { get } + + /// The target image URL. + var downloadURL: URL { get } +} + +extension Resource { + + /// Converts `self` to a valid ``Source`` based on the ``Resource/downloadURL`` scheme. A ``Source/provider(_:)`` + /// with ``LocalFileImageDataProvider`` associated will be returned if the URL points to a local file. Otherwise, + /// ``Source/network(_:)`` is returned. + /// + /// - Parameter overrideCacheKey: The key should be used to override the ``Resource/cacheKey`` when performing the + /// conversion. `nil` if not overridden and ``Resource/cacheKey`` of `self` is used. + /// - Returns: The converted source. + /// + public func convertToSource(overrideCacheKey: String? = nil) -> Source { + let key = overrideCacheKey ?? cacheKey + return downloadURL.isFileURL ? + .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) : + .network(KF.ImageResource(downloadURL: downloadURL, cacheKey: key)) + } +} + +@available(*, deprecated, message: "This type conflicts with `GeneratedAssetSymbols.ImageResource` in Swift 5.9. Renamed to avoid issues in the future.", renamed: "KF.ImageResource") +public typealias ImageResource = KF.ImageResource + + +extension KF { + /// ``ImageResource`` is a simple combination of ``downloadURL`` and ``cacheKey``. + /// When passed to image view set methods, Kingfisher will try to download the target + /// image from the ``downloadURL``, and then store it with the ``cacheKey`` as the key in cache. + public struct ImageResource: Resource { + + // MARK: - Initializers + + /// Creates an image resource. + /// + /// - Parameters: + /// - downloadURL: The target image URL from where the image can be downloaded. + /// - cacheKey: + /// The cache key. If `nil`, Kingfisher will use the `absoluteString` of ``ImageResource/downloadURL`` as + /// the key. Default is `nil`. + /// + public init(downloadURL: URL, cacheKey: String? = nil) { + self.downloadURL = downloadURL + self.cacheKey = cacheKey ?? downloadURL.cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public let cacheKey: String + + /// The target image URL. + public let downloadURL: URL + } +} + +/// URL conforms to ``Resource`` in Kingfisher. +/// The `absoluteString` of this URL is used as ``cacheKey``. And the URL itself will be used as `downloadURL`. +/// If you need customize the url and/or cache key, use `ImageResource` instead. +extension URL: Resource { + public var cacheKey: String { return isFileURL ? localFileCacheKey : absoluteString } + public var downloadURL: URL { return self } +} + +extension URL { + static let localFileCacheKeyPrefix = "kingfisher.local.cacheKey" + + // The special version of cache key for a local file on disk. Every time the app is reinstalled on the disk, + // the system assigns a new container folder to hold the .app (and the extensions, .appex) folder. So the URL for + // the same image in bundle might be different. + // + // This getter only uses the fixed part in the URL (until the bundle name folder) to provide a stable cache key + // for the image under the same path inside the bundle. + // + // See #1825 (https://github.com/onevcat/Kingfisher/issues/1825) + var localFileCacheKey: String { + var validComponents: [String] = [] + for part in pathComponents.reversed() { + validComponents.append(part) + if part.hasSuffix(".app") || part.hasSuffix(".appex") { + break + } + } + let fixedPath = "\(Self.localFileCacheKeyPrefix)/\(validComponents.reversed().joined(separator: "/"))" + if let q = query { + return "\(fixedPath)?\(q)" + } else { + return fixedPath + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/Source.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/Source.swift new file mode 100644 index 00000000..83c7620f --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/ImageSource/Source.swift @@ -0,0 +1,121 @@ +// +// Source.swift +// Kingfisher +// +// Created by onevcat on 2018/11/17. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + + +/// Represents an image source setting for Kingfisher methods. +/// +/// A ``Source`` value indicates the way in which the target image can be retrieved and cached. +/// +/// - `network`: The target image should be retrieved from the network remotely. The associated ``Resource`` +/// value defines detailed information like image URL and cache key. +/// - `provider`: The target image should be provided in a data format. Normally, it can be an image +/// from local storage or in any other encoding format (like Base64). +/// +public enum Source: Sendable { + + /// Represents the source task identifier when setting an image to a view with extension methods. + public enum Identifier { + + /// The underlying value type of source identifier. + public typealias Value = UInt + + @MainActor static private(set) var current: Value = 0 + + // Not thread safe. Expected to be always called on the main thread. + @MainActor static func next() -> Value { + current += 1 + return current + } + } + + // MARK: Member Cases + + /// The target image should be fetched from the network remotely. The associated `Resource` + /// value defines detailed information such as the image URL and cache key. + case network(any Resource) + + /// The target image should be provided in a data format, typically as an image + /// from local storage or in any other encoding format, such as Base64. + case provider(any ImageDataProvider) + + // MARK: Getting Properties + + /// The cache key defined for this source value. + public var cacheKey: String { + switch self { + case .network(let resource): return resource.cacheKey + case .provider(let provider): return provider.cacheKey + } + } + + /// The URL defined for this source value. + /// + /// For a ``Source/network(_:)`` source, it is the ``Resource/downloadURL`` of associated ``Resource`` instance. + /// For a ``Source/provider(_:)`` value, it is always `nil`. + public var url: URL? { + switch self { + case .network(let resource): return resource.downloadURL + case .provider(let provider): return provider.contentURL + } + } +} + +extension Source: Hashable { + public static func == (lhs: Source, rhs: Source) -> Bool { + switch (lhs, rhs) { + case (.network(let r1), .network(let r2)): + return r1.cacheKey == r2.cacheKey && r1.downloadURL == r2.downloadURL + case (.provider(let p1), .provider(let p2)): + return p1.cacheKey == p2.cacheKey && p1.contentURL == p2.contentURL + case (.provider(_), .network(_)): + return false + case (.network(_), .provider(_)): + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .network(let r): + hasher.combine(r.cacheKey) + hasher.combine(r.downloadURL) + case .provider(let p): + hasher.combine(p.cacheKey) + hasher.combine(p.contentURL) + } + } +} + +extension Source { + var asResource: (any Resource)? { + guard case .network(let resource) = self else { + return nil + } + return resource + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KF.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KF.swift new file mode 100644 index 00000000..c3f7ccc9 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KF.swift @@ -0,0 +1,450 @@ +// +// KF.swift +// Kingfisher +// +// Created by onevcat on 2020/09/21. +// +// Copyright (c) 2020 Wei Wang +// +// 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. + +#if canImport(UIKit) +import UIKit +#endif + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay +#endif + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +#if canImport(WatchKit) +import WatchKit +#endif + +#if canImport(TVUIKit) +import TVUIKit +#endif + +/// A helper type to create image setting tasks in a builder pattern. +/// +/// Use methods in this type to create a ``KF/Builder`` instance and configure image tasks there. +public enum KF { + + /// Creates a builder for a given ``Source``. + /// - Parameter source: The ``Source`` object defines data information from network or a data provider. + /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its + /// `Builder/set(to:)` to start the image loading. + public static func source(_ source: Source?) -> KF.Builder { + Builder(source: source) + } + + /// Creates a builder for a given ``Resource``. + /// - Parameter resource: The ``Resource`` object defines data information like key or URL. + /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its + /// `Builder/set(to:)` to start the image loading. + public static func resource(_ resource: (any Resource)?) -> KF.Builder { + source(resource?.convertToSource()) + } + + /// Creates a builder for a given `URL` and an optional cache key. + /// - Parameters: + /// - url: The URL where the image should be downloaded. + /// - cacheKey: The key used to store the downloaded image in cache. + /// If `nil`, the `absoluteString` of `url` is used as the cache key. + /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its + /// `Builder/set(to:)` to start the image loading. + public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder { + source(url?.convertToSource(overrideCacheKey: cacheKey)) + } + + /// Creates a builder for a given ``ImageDataProvider``. + /// - Parameter provider: The ``ImageDataProvider`` object contains information about the data. + /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its + /// `Builder/set(to:)` to start the image loading. + public static func dataProvider(_ provider: (any ImageDataProvider)?) -> KF.Builder { + source(provider?.convertToSource()) + } + + /// Creates a builder for some given raw data and a cache key. + /// - Parameters: + /// - data: The data object from which the image should be created. + /// - cacheKey: The key used to store the downloaded image in cache. + /// - Returns: A ``Builder`` for future configuration. After configuring the builder, call its + /// `Builder/set(to:)` to start the image loading. + public static func data(_ data: Data?, cacheKey: String) -> KF.Builder { + if let data = data { + return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) + } else { + return dataProvider(nil) + } + } +} + + +extension KF { + + /// A builder class to configure an image retrieving task and set it to a holder view or component. + public class Builder: @unchecked Sendable { + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.KF.Builder.propertyQueue") + + private let source: Source? + + #if os(watchOS) + private var _placeholder: KFCrossPlatformImage? + private var placeholder: KFCrossPlatformImage? { + get { propertyQueue.sync { _placeholder } } + set { propertyQueue.sync { _placeholder = newValue } } + } + #else + private var _placeholder: (any Placeholder)? + private var placeholder: (any Placeholder)? { + get { propertyQueue.sync { _placeholder } } + set { propertyQueue.sync { _placeholder = newValue } } + } + #endif + + private var _options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions) + public var options: KingfisherParsedOptionsInfo { + get { propertyQueue.sync { _options } } + set { propertyQueue.sync { _options = newValue } } + } + + public let onFailureDelegate = Delegate() + public let onSuccessDelegate = Delegate() + public let onProgressDelegate = Delegate<(Int64, Int64), Void>() + + init(source: Source?) { + self.source = source + } + + private var resultHandler: (@Sendable (Result) -> Void)? { + { + switch $0 { + case .success(let result): + self.onSuccessDelegate(result) + case .failure(let error): + self.onFailureDelegate(error) + } + } + } + + private var progressBlock: DownloadProgressBlock? { + onProgressDelegate.isSet ? { self.onProgressDelegate(($0, $1)) } : nil + } + } +} + +@MainActor +extension KF.Builder { + #if !os(watchOS) + + /// Builds the image task request and sets it to an image view. + /// - Parameter imageView: The image view which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to imageView: KFCrossPlatformImageView) -> DownloadTask? { + imageView.kf.setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to an `NSTextAttachment` object. + /// - Parameters: + /// - attachment: The text attachment object which loads the task and should be set with the image. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set( + to attachment: NSTextAttachment, + attributedView: @autoclosure @escaping @Sendable () -> KFCrossPlatformView) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return attachment.kf.setImage( + with: source, + attributedView: attributedView, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + #if canImport(UIKit) + + /// Builds the image task request and sets it to a button. + /// - Parameters: + /// - button: The button which loads the task and should be set with the image. + /// - state: The button state to which the image should be set. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to button: UIButton, for state: UIControl.State) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setImage( + with: source, + for: state, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to the background image for a button. + /// - Parameters: + /// - button: The button which loads the task and should be set with the image. + /// - state: The button state to which the image should be set. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func setBackground(to button: UIButton, for state: UIControl.State) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setBackgroundImage( + with: source, + for: state, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(UIKit) + + #if canImport(CarPlay) && !targetEnvironment(macCatalyst) + + /// Builds the image task request and sets it to the image for a list item. + /// - Parameters: + /// - listItem: The list item which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @available(iOS 14.0, *) + @discardableResult + public func set(to listItem: CPListItem) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return listItem.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + + } + + #endif + + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + /// Builds the image task request and sets it to a button. + /// - Parameter button: The button which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to button: NSButton) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to the alternative image for a button. + /// - Parameter button: The button which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func setAlternative(to button: NSButton) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setAlternateImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(AppKit) + #endif // end of !os(watchOS) + + #if canImport(WatchKit) + /// Builds the image task request and sets it to a `WKInterfaceImage` object. + /// - Parameter interfaceImage: The watch interface image which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to interfaceImage: WKInterfaceImage) -> DownloadTask? { + return interfaceImage.kf.setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(WatchKit) + + #if canImport(TVUIKit) + /// Builds the image task request and sets it to a TV monogram view. + /// - Parameter monogramView: The monogram view which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @available(tvOS 12.0, *) + @discardableResult + public func set(to monogramView: TVMonogramView) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return monogramView.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(TVUIKit) +} + +#if !os(watchOS) +extension KF.Builder { + #if os(iOS) || os(tvOS) || os(visionOS) + + /// Sets a placeholder which is used while retrieving the image. + /// - Parameter placeholder: A placeholder to show while retrieving the image from its source. + /// - Returns: A ``KF/Builder`` with changes applied. + public func placeholder(_ placeholder: (any Placeholder)?) -> Self { + self.placeholder = placeholder + return self + } + #endif + + /// Sets a placeholder image which is used while retrieving the image. + /// - Parameters: + /// - image: An image to show while retrieving the image from its source. + /// - Returns: A ``KF/Builder`` with changes applied. + public func placeholder(_ image: KFCrossPlatformImage?) -> Self { + self.placeholder = image + return self + } +} +#endif + +extension KF.Builder { + + #if os(iOS) || os(tvOS) || os(visionOS) + /// Sets the transition for the image task. + /// - Parameter transition: The desired transition effect when setting the image to image view. + /// - Returns: A ``KF/Builder`` with changes applied. + /// + /// Kingfisher will use the `transition` parameter to animate the image in if it is downloaded from web. + /// The transition will not happen when the image is retrieved from either memory or disk cache by default. + /// If you need to do the transition even when the image being retrieved from cache, also call + /// ``KFOptionSetter/forceRefresh(_:)`` on the returned ``KF/Builder``. + public func transition(_ transition: ImageTransition) -> Self { + options.transition = transition + return self + } + + /// Sets a fade transition for the image task. + /// - Parameter duration: The duration of the fade transition. + /// - Returns: A ``KF/Builder`` with changes applied. + /// + /// Kingfisher will use the `transition` parameter to animate the image in if it is downloaded from web. + /// The transition will not happen when the image is retrieved from either memory or disk cache by default. + /// If you need to do the transition even when the image being retrieved from cache, also call + /// ``KFOptionSetter/forceRefresh(_:)`` on the returned ``KF/Builder``. + public func fade(duration: TimeInterval) -> Self { + options.transition = .fade(duration) + return self + } + #endif + + /// Sets whether keeping the existing image of image view while setting another image to it. + /// - Parameter enabled: Whether the existing image should be kept. + /// - Returns: A ``KF/Builder`` with changes applied. + /// + /// By setting this option, the placeholder image parameter of image view extension method + /// will be ignored and the current image will be kept while loading or downloading the new image. + /// + public func keepCurrentImageWhileLoading(_ enabled: Bool = true) -> Self { + options.keepCurrentImageWhileLoading = enabled + return self + } + + /// Sets whether only the first frame from an animated image file should be loaded as a single image. + /// - Parameter enabled: Whether the only the first frame should be loaded. + /// - Returns: A ``KF/Builder`` with changes applied. + /// + /// Loading an animated images may take too much memory. It will be useful when you want to display a + /// static preview of the first frame from an animated image. + /// + /// This option will be ignored if the target image is not animated image data. + /// + public func onlyLoadFirstFrame(_ enabled: Bool = true) -> Self { + options.onlyLoadFirstFrame = enabled + return self + } +} + +// MARK: - Deprecated +extension KF.Builder { + /// Starts the loading process of `self` immediately. + /// + /// By default, a ``KFImage`` will not load its source until the `onAppear` is called. This is a lazily loading + /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a + /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once + /// could help avoiding the flickering, with some performance trade-off. + /// + /// - Returns: The `Self` value with changes applied. + @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") + public func loadImmediately(_ start: Bool = true) -> Self { + return self + } +} + +// MARK: - Redirect Handler +extension KF { + + /// Represents the detail information when a task redirect happens. It is wrapping necessary information for a + /// ``ImageDownloadRedirectHandler``. See that protocol for more information. + public struct RedirectPayload { + + /// The related session data task when the redirect happens. It is + /// the current ``SessionDataTask`` which triggers this redirect. + public let task: SessionDataTask + + /// The response received during redirection. + public let response: HTTPURLResponse + + /// The request for redirection which can be modified. + public let newRequest: URLRequest + + /// A closure for being called with modified request. + public let completionHandler: (URLRequest?) -> Void + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift new file mode 100644 index 00000000..2d4dfd25 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift @@ -0,0 +1,812 @@ +// +// KFOptionsSetter.swift +// Kingfisher +// +// Created by onevcat on 2020/12/22. +// +// Copyright (c) 2020 Wei Wang +// +// 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. + +import Foundation +import CoreGraphics +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// A protocol that Kingfisher can use to perform chained setting in builder pattern. +@MainActor +public protocol KFOptionSetter { + var options: KingfisherParsedOptionsInfo { get nonmutating set } + + var onFailureDelegate: Delegate { get } + var onSuccessDelegate: Delegate { get } + var onProgressDelegate: Delegate<(Int64, Int64), Void> { get } +} + +extension KF.Builder: KFOptionSetter { } + +final actor KFDelegateObserver { + static let `default` = KFDelegateObserver() +} + +// MARK: - Life cycles +extension KFOptionSetter { + /// Sets the progress block to current builder. + /// + /// - Parameter block: + /// Called when the image downloading progress gets updated. If the response does not contain an + /// [`expectedContentLength`](https://developer.apple.com/documentation/foundation/urlresponse/1413507-expectedcontentlength) + /// in the received `URLResponse`, this block will not be called. If `block` is `nil`, the callback will be reset. + /// + /// - Returns: A `Self` value with changes applied. + /// + public func onProgress(_ block: DownloadProgressBlock?) -> Self { + onProgressDelegate.delegate(on: KFDelegateObserver.default) { (_, result) in + block?(result.0, result.1) + } + return self + } + + /// Sets the done block to current builder. + /// - Parameter block: Called when the image task successfully completes and the image set is done. If `block` + /// is `nil`, the callback will be reset. + /// - Returns: A `Self` with changes applied. + /// + public func onSuccess(_ block: ((RetrieveImageResult) -> Void)?) -> Self { + onSuccessDelegate.delegate(on: KFDelegateObserver.default) { (_, result) in + block?(result) + } + return self + } + + /// Sets the catch block to current builder. + /// - Parameter block: Called when an error happens during the image task. If `block` + /// is `nil`, the callback will be reset. + /// - Returns: A `Self` with changes applied. + /// + public func onFailure(_ block: ((KingfisherError) -> Void)?) -> Self { + onFailureDelegate.delegate(on: KFDelegateObserver.default) { (_, error) in + block?(error) + } + return self + } +} + +// MARK: - Basic options settings. +extension KFOptionSetter { + + /// Sets the target image cache for this task. + /// + /// - Parameter cache: The target cache to be used for the task. + /// - Returns: A `Self` value with changes applied. + /// + /// Kingfisher will utilize the associated ``ImageCache`` object when performing related operations, + /// such as attempting to retrieve cached images and storing downloaded images within it. + /// + public func targetCache(_ cache: ImageCache) -> Self { + options.targetCache = cache + return self + } + + /// Sets the target image cache to store the original downloaded image for this task. + /// + /// - Parameter cache: The target cache is about to be used for storing the original downloaded image from the task. + /// - Returns: A `Self` value with changes applied. + /// + /// The ``ImageCache`` for storing and retrieving original images. If ``KingfisherOptionsInfoItem/originalCache(_:)`` + /// is contained in the options, it will be preferred for storing and retrieving original images. + /// If there is no ``KingfisherOptionsInfoItem/originalCache(_:)`` in the options, + /// ``KingfisherOptionsInfoItem/targetCache(_:)`` will be used to store original images. + /// + /// When using ``KingfisherManager`` to download and store an image, if + /// ``KingfisherOptionsInfoItem/cacheOriginalImage`` is applied in the option, the original image will be stored to + /// the `cache` you pass as parameter in this method. At the same time, if a requested final image (with processor + /// applied) cannot be found in the cache defined by ``KingfisherOptionsInfoItem/targetCache(_:)``, Kingfisher + /// will try to search the original image to check whether it is already there. If found, it will be used and + /// applied with the given processor. It is an optimization for not downloading the same image for multiple times. + /// + public func originalCache(_ cache: ImageCache) -> Self { + options.originalCache = cache + return self + } + + /// Sets the downloader to be used for the image download task. + /// + /// - Parameter downloader: The `ImageDownloader` instance to use for downloading. + /// - Returns: A `Self` value with the changes applied. + /// + /// Kingfisher will utilize the specified ``ImageDownloader`` instance to download requested images. + /// + public func downloader(_ downloader: ImageDownloader) -> Self { + options.downloader = downloader + return self + } + + /// Sets the download priority for the image task. + /// + /// - Parameter priority: The download priority of the image download task. + /// - Returns: A `Self` value with changes applied. + /// + /// The `priority` value will be configured as the priority of the image download task. Valid values range between + /// 0.0 and 1.0. You can select a value from `URLSessionTask.defaultPriority`, `URLSessionTask.lowPriority`, + /// or `URLSessionTask.highPriority`. If this option is not set, the default value + /// (`URLSessionTask.defaultPriority`) will be used. + /// + public func downloadPriority(_ priority: Float) -> Self { + options.downloadPriority = priority + return self + } + + /// Sets whether Kingfisher should ignore the cache and attempt to initiate a download task for the image source. + /// + /// - Parameter enabled: Enable force refresh or not. + /// - Returns: A `Self` value with the changes applied. + /// + public func forceRefresh(_ enabled: Bool = true) -> Self { + options.forceRefresh = enabled + return self + } + + /// Sets whether Kingfisher should attempt to retrieve the image from the memory cache first. If the image is not + /// found in the memory cache, it bypasses the disk cache and initiates a download task for the image source. + /// + /// - Parameter enabled: Enable memory-only cache searching or not. + /// - Returns: A `Self` value with the changes applied. + /// + /// This option is useful when you want to display a changeable image with the same URL during the same app session + /// while avoiding multiple downloads of the same image. + /// + public func fromMemoryCacheOrRefresh(_ enabled: Bool = true) -> Self { + options.fromMemoryCacheOrRefresh = enabled + return self + } + + /// Sets whether the image should be cached only in memory and not on disk. + /// + /// - Parameter enabled: Enable memory-only caching for the image or not. + /// - Returns: A `Self` value with the changes applied. + /// + public func cacheMemoryOnly(_ enabled: Bool = true) -> Self { + options.cacheMemoryOnly = enabled + return self + } + + /// Sets whether Kingfisher should wait for caching operations to be completed before invoking the `onSuccess` + /// or `onFailure` block. + /// + /// - Parameter enabled: Enable waiting for caching operations or not. + /// - Returns: A `Self` value with the changes applied. + /// + public func waitForCache(_ enabled: Bool = true) -> Self { + options.waitForCache = enabled + return self + } + + /// Sets whether Kingfisher should exclusively attempt to retrieve the image from the cache and not from the network. + /// + /// - Parameter enabled: Enable cache-only image retrieval or not. + /// - Returns: A `Self` value with the changes applied. + /// + /// If the image is not found in the cache, the image retrieval will fail with a + /// ``KingfisherError/CacheErrorReason/imageNotExisting(key:)`` error. + /// + public func onlyFromCache(_ enabled: Bool = true) -> Self { + options.onlyFromCache = enabled + return self + } + + /// Sets whether the image should be decoded on a background thread before usage. + /// + /// - Parameter enabled: Enable background image decoding or not. + /// - Returns: A `Self` value with the changes applied. + /// + /// When set to `true`, the downloaded image data will be decoded and undergo off-screen rendering to extract pixel + /// information in the background. This can enhance display speed but may consume additional time and memory for + /// image preparation before usage. + /// + public func backgroundDecode(_ enabled: Bool = true) -> Self { + options.backgroundDecode = enabled + return self + } + + /// Sets the callback queue used as the target queue for dispatching callbacks when retrieving images from the + /// cache. If not set, Kingfisher will use the main queue for callbacks. + /// + /// - Parameter queue: The target queue on which cache retrieval callbacks will be invoked. + /// - Returns: A `Self` value with the changes applied. + /// + /// - Note: This option does not impact callbacks for UI-related extension methods or ``KFImage`` result handlers. + /// Callbacks for those methods will always be executed on the main queue. + /// + public func callbackQueue(_ queue: CallbackQueue) -> Self { + options.callbackQueue = queue + return self + } + + /// Sets the scale factor value used when converting retrieved data to an image. + /// + /// - Parameter factor: The scale factor value to use. + /// - Returns: A `Self` value with the changes applied. + /// + /// Specify the image scale factor, which may differ from your screen's scale. This is particularly important when + /// working with 2x or 3x retina images. Failure to set the correct scale factor may result in Kingfisher + /// converting the data to an image object with a `scale` of 1.0. + /// + public func scaleFactor(_ factor: CGFloat) -> Self { + options.scaleFactor = factor + return self + } + + /// Sets whether the original image should be cached, even when the original image has been processed by other ``ImageProcessor``s. + /// + /// - Parameter enabled: Whether to cache the original image. + /// - Returns: A `Self` value with the changes applied. + /// + /// When this option is set, and an ``ImageProcessor`` is used, Kingfisher will attempt to cache both the final + /// processed image and the original image. This ensures that the original image can be reused when another + /// processor is applied to the same resource, without the need for redownloading. You can use + /// ``KingfisherOptionsInfoItem/originalCache(_:)`` to specify a cache for the original images. + /// + /// - Note: The original image will be cached only in disk storage. + /// + public func cacheOriginalImage(_ enabled: Bool = true) -> Self { + options.cacheOriginalImage = enabled + return self + } + + /// Sets writing options for an original image on its initial write to disk storage. + /// + /// - Parameter writingOptions: Options that control the data writing operation to disk storage. + /// - Returns: A `Self` value with the changes applied. + /// + /// If these options are set, they will be applied to the storage operation for new files. This can be useful if + /// you want to implement features such as file encryption on the initial write, for example, + /// using `[.completeFileProtection]`. + /// + public func diskStoreWriteOptions(_ writingOptions: Data.WritingOptions) -> Self { + options.diskStoreWriteOptions = writingOptions + return self + } + + /// Sets whether disk storage loading should occur in the same calling queue. + /// + /// - Parameter enabled: Whether disk storage loading should happen in the same calling queue. + /// - Returns: A `Self` value with the changes applied. + /// + /// By default, disk storage file loading operates in its own queue with asynchronous dispatch behavior. While this + /// provides better non-blocking disk loading performance, it can result in flickering when reloading an image + /// from disk if the image view already has an image set. + /// + /// Enabling this option prevents flickering by performing all loading in the same queue (typically the UI queue if + /// you are using Kingfisher's extension methods to set an image). However, this may come at the cost of loading + /// performance. + /// + public func loadDiskFileSynchronously(_ enabled: Bool = true) -> Self { + options.loadDiskFileSynchronously = enabled + return self + } + + /// Sets the queue on which image processing should occur. + /// + /// - Parameter queue: The queue on which image processing should take place. + /// - Returns: A `Self` value with the changes applied. + /// + /// By default, Kingfisher employs a pre-defined serial queue for image processing. Use this option to modify this + /// behavior. For example, specify `.mainCurrentOrAsync` to process the image on the main queue, which can prevent + /// potential flickering but may lead to UI blocking if the processor requires substantial time to execute. + /// + public func processingQueue(_ queue: CallbackQueue?) -> Self { + options.processingQueue = queue + return self + } + + /// Sets the alternative sources to be used when loading the original input `Source` fails. + /// + /// - Parameter sources: The alternative sources to be used. + /// - Returns: A `Self` value with the changes applied. + /// + /// The values in the `sources` array will be employed to initiate a new image loading task if the previous task + /// fails due to an error. The image source loading process will terminate as soon as one of the alternative + /// sources is successfully loaded. If all `sources` are used but loading still fails, + /// a ``KingfisherError/ImageSettingErrorReason/alternativeSourcesExhausted(_:)`` error will be thrown in the + /// `catch` block. + /// + /// This feature is valuable when implementing a fallback solution for setting images. + /// + /// - Note: User cancellation or calling on ``DownloadTask/cancel()`` on ``DownloadTask`` will not trigger the + /// loading of alternative sources. + /// + public func alternativeSources(_ sources: [Source]?) -> Self { + options.alternativeSources = sources + return self + } + + /// Sets a retry strategy to be used when issues arise during image retrieval. + /// + /// - Parameter strategy: The provided strategy that defines how retry attempts should occur. + /// - Returns: A `Self` value with the changes applied. + /// + public func retry(_ strategy: (any RetryStrategy)?) -> Self { + options.retryStrategy = strategy + return self + } + + /// Sets a retry strategy with a maximum retry count and retry interval. + /// + /// - Parameters: + /// - maxCount: The maximum number of retry attempts before the retry stops. + /// - interval: The time interval between each retry attempt. + /// - Returns: A `Self` value with the changes applied. + /// + /// This defines a straightforward retry strategy that retries a failing request for a specified number of times + /// with a designated time interval between each attempt. For example, `.retry(maxCount: 3, interval: .second(3))` + /// indicates a maximum of three retry attempts, with a 3-second pause between each retry if the previous attempt + /// fails. + /// + public func retry(maxCount: Int, interval: DelayRetryStrategy.Interval = .seconds(3)) -> Self { + let strategy = DelayRetryStrategy(maxRetryCount: maxCount, retryInterval: interval) + options.retryStrategy = strategy + return self + } + + /// Sets the `Source` to be loaded when the user enables Low Data Mode and the original source fails with an + /// `NSURLErrorNetworkUnavailableReason.constrained` error. + /// + /// - Parameter source: The `Source` to be loaded under low data mode. + /// - Returns: A `Self` value with the changes applied. + /// + /// When this option is set, the `allowsConstrainedNetworkAccess` property of the request for the original source + /// will be set to `false`, and the specified ``Source`` will be used to retrieve the image in low data mode. + /// Typically, you can provide a low-resolution version of your image or a local image provider to display a + /// placeholder. + /// + /// If this option is not set or the `source` is `nil`, the device's Low Data Mode setting will be disregarded, + /// and the original source will be loaded following the system's default behavior in a regular manner. + /// + public func lowDataModeSource(_ source: Source?) -> Self { + options.lowDataModeSource = source + return self + } + + /// Sets whether the image setting for an image view should include a transition even when the image is retrieved + /// from the cache. + /// + /// - Parameter enabled: Enable the use of a transition or not. + /// - Returns: A `Self` value with the changes applied. + /// + public func forceTransition(_ enabled: Bool = true) -> Self { + options.forceTransition = enabled + return self + } + + /// Sets the image to be used in the event of a failure during image retrieval. + /// + /// - Parameter image: The image to be used when an error occurs. + /// - Returns: A `Self` value with the changes applied. + /// + /// If this option is set and an image retrieval error occurs, Kingfisher will use the provided image (or an empty + /// image) in place of the requested one. This is useful when you do not want to display a placeholder during the + /// loading process but prefer to use a default image when requests fail. + /// + public func onFailureImage(_ image: KFCrossPlatformImage?) -> Self { + options.onFailureImage = .some(image) + return self + } +} + +// MARK: - Request Modifier +extension KFOptionSetter { + + /// Sets an ``ImageDownloadRequestModifier`` to alter the image download request before it is sent. + /// + /// - Parameter modifier: The modifier to be used for changing the request before it is sent. + /// - Returns: A `Self` value with the changes applied. + /// + /// This is your last opportunity to modify the image download request. You can use this for customization + /// purposes, such as adding an authentication token to the header, implementing basic HTTP authentication, + /// or URL mapping. + public func requestModifier(_ modifier: any AsyncImageDownloadRequestModifier) -> Self { + options.requestModifier = modifier + return self + } + + /// Sets a block to modify the image download request before it is sent. + /// + /// - Parameter modifyBlock: The modifying block that will be called to change the request before it is sent. + /// - Returns: A `Self` value with the changes applied. + /// + /// This is your last opportunity to modify the image download request. You can use this for customization purposes, + /// such as adding an authentication token to the header, implementing basic HTTP authentication, or URL mapping. + /// + public func requestModifier(_ modifyBlock: @escaping @Sendable (inout URLRequest) -> Void) -> Self { + options.requestModifier = AnyModifier { r -> URLRequest? in + var request = r + modifyBlock(&request) + return request + } + return self + } +} + +// MARK: - Redirect Handler +extension KFOptionSetter { + + /// Sets an `ImageDownloadRedirectHandler` to modify the image download request during redirection. + /// + /// - Parameter handler: The handler to be used for redirection. + /// - Returns: A `Self` value with the changes applied. + /// + /// This provides an opportunity to modify the image download request during redirection. You can use this for + /// customization purposes, such as adding an authentication token to the header, implementing basic HTTP + /// authentication, or URL mapping. By default, the original redirection request will be sent without any + /// modification. + /// + public func redirectHandler(_ handler: any ImageDownloadRedirectHandler) -> Self { + options.redirectHandler = handler + return self + } + + /// Sets a block to modify the image download request during redirection. + /// + /// - Parameter block: The block to be used for redirection. + /// - Returns: A `Self` value with the changes applied. + /// + /// This provides an opportunity to modify the image download request during redirection. You can use this for + /// customization purposes, such as adding an authentication token to the header, implementing basic HTTP + /// authentication, or URL mapping. By default, the original redirection request will be sent without any + /// modification. + /// + public func redirectHandler(_ block: @escaping @Sendable (KF.RedirectPayload) -> Void) -> Self { + let redirectHandler = AnyRedirectHandler { (task, response, request, handler) in + let payload = KF.RedirectPayload( + task: task, response: response, newRequest: request, completionHandler: handler + ) + block(payload) + } + options.redirectHandler = redirectHandler + return self + } +} + +// MARK: - Processor +extension KFOptionSetter { + + /// Sets an image processor for the image task, replacing the current image processor settings. + /// + /// - Parameter processor: The processor to use for processing the image after it is downloaded. + /// - Returns: A `Self` value with the changes applied. + /// + /// - Note: To append a processor to the current ones instead of replacing them all, use ``appendProcessor(_:)``. + /// + public func setProcessor(_ processor: any ImageProcessor) -> Self { + options.processor = processor + return self + } + + /// Enables progressive image loading with a specified `ImageProgressive` setting to process the + /// progressive JPEG data and display it in a progressive way. + /// - Parameter progressive: The progressive settings which is used while loading. + /// - Returns: A ``KF/Builder`` with changes applied. + public func progressiveJPEG(_ progressive: ImageProgressive? = .init()) -> Self { + options.progressiveJPEG = progressive + return self + } + + /// Sets an array of image processors for the image task, replacing the current image processor settings. + /// + /// - Parameter processors: An array of processors. The processors in this array will be concatenated one by one to + /// form a processor pipeline. + /// - Returns: A `Self` value with the changes applied. + /// + /// - Note: To append processors to the current ones instead of replacing them all, concatenate them using the + /// `|>` operator, and then use ``KFOptionSetter/appendProcessor(_:)``. + /// + public func setProcessors(_ processors: [any ImageProcessor]) -> Self { + switch processors.count { + case 0: + options.processor = DefaultImageProcessor.default + case 1...: + options.processor = processors.dropFirst().reduce(processors[0]) { $0 |> $1 } + default: + assertionFailure("Never happen") + } + return self + } + + /// Appends a processor to the current set of processors. + /// + /// - Parameter processor: The processor to append to the current processor settings. + /// - Returns: A `Self` value with the changes applied. + /// + public func appendProcessor(_ processor: any ImageProcessor) -> Self { + options.processor = options.processor |> processor + return self + } + + /// Appends a ``RoundCornerImageProcessor`` to the current set of processors. + /// + /// - Parameters: + /// - radius: The radius to apply during processing. Specify a certain point value with `.point`, or a fraction + /// of the target image with `.widthFraction` or `.heightFraction`. For example, with a square image where width + /// and height are equal, `.widthFraction(0.5)` means using half of the length of the size to make the final + /// image round. + /// - targetSize: The target size for the output image. If `nil`, the image will retain its original size after + /// processing. + /// - corners: The target corners to round. + /// - backgroundColor: The background color of the output image. If `nil`, a transparent background will be used. + /// - Returns: A `Self` value with the changes applied. + /// + public func roundCorner( + radius: Radius, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> Self + { + let processor = RoundCornerImageProcessor( + radius: radius, + targetSize: targetSize, + roundingCorners: corners, + backgroundColor: backgroundColor + ) + return appendProcessor(processor) + } + + /// Appends a ``BlurImageProcessor`` to the current set of processors. + /// + /// - Parameter radius: The blur radius for simulating Gaussian blur. + /// - Returns: A `Self` value with the changes applied. + /// + public func blur(radius: CGFloat) -> Self { + appendProcessor( + BlurImageProcessor(blurRadius: radius) + ) + } + + /// Appends an ``OverlayImageProcessor`` to the current set of processors. + /// + /// - Parameters: + /// - color: The overlay color to be used when overlaying the input image. + /// - fraction: The fraction to be used when overlaying the color onto the image. + /// - Returns: A `Self` value with the changes applied. + /// + public func overlay(color: KFCrossPlatformColor, fraction: CGFloat = 0.5) -> Self { + appendProcessor( + OverlayImageProcessor(overlay: color, fraction: fraction) + ) + } + + /// Appends a ``TintImageProcessor`` to the current set of processors. + /// + /// - Parameter color: The tint color to be used for tinting the input image. + /// - Returns: A `Self` value with the changes applied. + /// + public func tint(color: KFCrossPlatformColor) -> Self { + appendProcessor( + TintImageProcessor(tint: color) + ) + } + + /// Appends a ``BlackWhiteProcessor`` to the current set of processors. + /// + /// - Returns: A `Self` value with the changes applied. + /// + public func blackWhite() -> Self { + appendProcessor( + BlackWhiteProcessor() + ) + } + + /// Appends a ``CroppingImageProcessor`` to the current set of processors. + /// + /// - Parameters: + /// - size: The target size for the output image. + /// - anchor: The anchor point from which the output size should be calculated. The anchor point is represented + /// by two values between 0.0 and 1.0, indicating a relative point in the current image. See + /// ``CroppingImageProcessor/init(size:anchor:)`` for more details. + /// - Returns: A `Self` value with the changes applied. + /// + public func cropping(size: CGSize, anchor: CGPoint = .init(x: 0.5, y: 0.5)) -> Self { + appendProcessor( + CroppingImageProcessor(size: size, anchor: anchor) + ) + } + + /// Appends a ``DownsamplingImageProcessor`` to the current set of processors. + /// + /// Compared to the ``ResizingImageProcessor``, the ``DownsamplingImageProcessor`` doesn't render the original + /// images and then resize them. Instead, it directly downsamples the input data to a thumbnail image, making it + /// more efficient than the ``ResizingImageProcessor``. It is recommended to use the ``DownsamplingImageProcessor`` + /// whenever possible instead of the ``ResizingImageProcessor``. + /// + /// - Parameter size: The target size for the output image. It should be smaller than the size of the input image. If it is larger, the resulting image will be the same size as the input data without downsampling. + /// - Returns: A `Self` value with the changes applied. + /// + /// - Note: Only CG-based images are supported, and animated images (e.g., GIF) are not supported. + /// + public func downsampling(size: CGSize) -> Self { + let processor = DownsamplingImageProcessor(size: size) + if options.processor == DefaultImageProcessor.default { + return setProcessor(processor) + } else { + return appendProcessor(processor) + } + } + + /// Appends a ``ResizingImageProcessor`` to the current set of processors. + /// + /// If you need to resize a data-represented image to a smaller size, it is recommended to use the + /// ``DownsamplingImageProcessor`` instead, which is more efficient and uses less memory. + /// + /// - Parameters: + /// - referenceSize: The reference size for the resizing operation in points. + /// - mode: The target content mode for the output image. The default is `.none`. + /// - Returns: A `Self` value with the changes applied. + /// + public func resizing(referenceSize: CGSize, mode: ContentMode = .none) -> Self { + appendProcessor( + ResizingImageProcessor(referenceSize: referenceSize, mode: mode) + ) + } +} + +// MARK: - Cache Serializer +extension KFOptionSetter { + + /// Uses a specified ``CacheSerializer`` to convert data to an image object for retrieval from the disk cache or + /// vice versa for storage to the disk cache. + /// + /// - Parameter cacheSerializer: The ``CacheSerializer`` to be used. + /// - Returns: A `Self` value with the changes applied. + /// + public func serialize(by cacheSerializer: any CacheSerializer) -> Self { + options.cacheSerializer = cacheSerializer + return self + } + + /// Uses a specified format to serialize the image data to disk. It converts the image object to the given data + /// format. + /// + /// - Parameters: + /// - format: The desired data encoding format when storing the image on disk. + /// - jpegCompressionQuality: If the format is ``ImageFormat/JPEG``, it specifies the compression quality when + /// converting the image to JPEG data. Otherwise, it is ignored. + /// - Returns: A `Self` value with the changes applied. + /// + public func serialize(as format: ImageFormat, jpegCompressionQuality: CGFloat? = nil) -> Self { + let cacheSerializer: FormatIndicatedCacheSerializer + switch format { + case .JPEG: + cacheSerializer = .jpeg(compressionQuality: jpegCompressionQuality ?? 1.0) + case .PNG: + cacheSerializer = .png + case .GIF: + cacheSerializer = .gif + case .unknown: + cacheSerializer = .png + } + options.cacheSerializer = cacheSerializer + return self + } +} + +// MARK: - Image Modifier +extension KFOptionSetter { + + /// Sets an ``ImageModifier`` for the image task. Use this to modify the fetched image object's properties if needed. + /// + /// If the image was fetched directly from the downloader, the modifier will run directly after the + /// ``ImageProcessor``. If the image is being fetched from a cache, the modifier will run after the + /// ``CacheSerializer``. + /// + /// - Parameter modifier: The ``ImageModifier`` to be used for modifying the image object. + /// - Returns: A `Self` value with the changes applied. + /// + public func imageModifier(_ modifier: (any ImageModifier)?) -> Self { + options.imageModifier = modifier + return self + } + + /// Sets a block to modify the image object. Use this to modify the fetched image object's properties if needed. + /// + /// If the image was fetched directly from the downloader, the modifier block will run directly after the + /// ``ImageProcessor``. If the image is being fetched from a cache, the modifier will run after the + /// ``CacheSerializer``. + /// + /// - Parameter block: The block used to modify the image object. + /// - Returns: A `Self` value with the changes applied. + /// + public func imageModifier(_ block: @escaping @Sendable (inout KFCrossPlatformImage) throws -> Void) -> Self { + let modifier = AnyImageModifier { image -> KFCrossPlatformImage in + var image = image + try block(&image) + return image + } + options.imageModifier = modifier + return self + } +} + + +// MARK: - Cache Expiration +extension KFOptionSetter { + + /// Sets the expiration setting for the memory cache of this image task. + /// + /// By default, the underlying ``MemoryStorage/Backend`` uses the expiration in its configuration for all items. + /// If set, the ``MemoryStorage/Backend`` will use this value to overwrite the configuration setting for this + /// caching item. + /// + /// - Parameter expiration: The expiration setting used in cache storage. + /// - Returns: A `Self` value with the changes applied. + /// + public func memoryCacheExpiration(_ expiration: StorageExpiration?) -> Self { + options.memoryCacheExpiration = expiration + return self + } + + /// Sets the expiration extending setting for the memory cache. The item expiration time will be incremented by this + /// value after access. + /// + /// By default, the underlying ``MemoryStorage/Backend`` uses the initial cache expiration as the extending value: + /// ``ExpirationExtending/cacheTime``. + /// + /// To disable the extending option entirely, set `.none` to it. + /// + /// - Parameter extending: The expiration extending setting used in cache storage. + /// - Returns: A `Self` value with the changes applied. + /// + public func memoryCacheAccessExtending(_ extending: ExpirationExtending) -> Self { + options.memoryCacheAccessExtendingExpiration = extending + return self + } + + /// Sets the expiration setting for the disk cache of this image task. + /// + /// By default, the underlying ``DiskStorage/Backend`` uses the expiration in its configuration for all items. + /// If set, the ``DiskStorage/Backend`` will use this value to overwrite the configuration setting for this caching + /// item. + /// + /// - Parameter expiration: The expiration setting used in cache storage. + /// - Returns: A `Self` value with the changes applied. + /// + public func diskCacheExpiration(_ expiration: StorageExpiration?) -> Self { + options.diskCacheExpiration = expiration + return self + } + + /// Sets the expiration extending setting for the disk cache. The item expiration time will be incremented by this + /// value after access. + /// + /// By default, the underlying ``DiskStorage/Backend`` uses the initial cache expiration as the extending + /// value: ``ExpirationExtending/cacheTime``. + /// + /// To disable the extending option entirely, set `.none` to it. + /// + /// - Parameter extending: The expiration extending setting used in cache storage. + /// - Returns: A `Self` value with the changes applied. + /// + public func diskCacheAccessExtending(_ extending: ExpirationExtending) -> Self { + options.diskCacheAccessExtendingExpiration = extending + return self + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/Kingfisher.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/Kingfisher.swift new file mode 100644 index 00000000..b890d24b --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/Kingfisher.swift @@ -0,0 +1,131 @@ +// +// Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 16/9/14. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +import ImageIO + +#if os(macOS) +import AppKit +public typealias KFCrossPlatformImage = NSImage +public typealias KFCrossPlatformView = NSView +public typealias KFCrossPlatformColor = NSColor +public typealias KFCrossPlatformImageView = NSImageView +public typealias KFCrossPlatformButton = NSButton + +// `NSImage` is not yet Sendable. We have to assume it sendable to resolve warnings in Kingfisher. +#if compiler(>=6) +extension KFCrossPlatformImage: @retroactive @unchecked Sendable { } +#else +extension KFCrossPlatformImage: @unchecked Sendable { } +#endif // compiler(>=6) +#else // os(macOS) +import UIKit +public typealias KFCrossPlatformImage = UIImage +public typealias KFCrossPlatformColor = UIColor +#if !os(watchOS) +public typealias KFCrossPlatformImageView = UIImageView +public typealias KFCrossPlatformView = UIView +public typealias KFCrossPlatformButton = UIButton +#if canImport(TVUIKit) +import TVUIKit +#endif // canImport(TVUIKit) +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay +#endif // canImport(CarPlay) && !targetEnvironment(macCatalyst) +#else // !os(watchOS) +import WatchKit +#endif // !os(watchOS) +#endif // os(macOS) + +/// Wrapper for Kingfisher compatible types. This type provides an extension point for +/// convenience methods in Kingfisher. +public struct KingfisherWrapper: @unchecked Sendable { + public let base: Base + public init(_ base: Base) { + self.base = base + } +} + +/// Represents an object type that is compatible with Kingfisher. You can use ``kf`` property to get a +/// value in the namespace of Kingfisher. +/// +/// In Kingfisher, most of related classes that contains an image (such as `UIImage`, `UIButton`, `NSImageView` and +/// more) conform to this protocol, and provides the helper methods for setting an image easily. You can access the `kf` +/// property and call its `setImage` method with a certain URL: +/// +/// ```swift +/// let imageView: UIImageView +/// let url = URL(string: "https://example.com/image.jpg") +/// imageView.kf.setImage(with: url) +/// ``` +/// +/// For more about basic usage of Kingfisher, check the documentation. +public protocol KingfisherCompatible: AnyObject { } + +/// Represents a value type that is compatible with Kingfisher. You can use ``kf`` property to get a +/// value in the namespace of Kingfisher. +public protocol KingfisherCompatibleValue {} + +extension KingfisherCompatible { + /// Gets a namespace holder for Kingfisher compatible types. + public var kf: KingfisherWrapper { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension KingfisherCompatibleValue { + /// Gets a namespace holder for Kingfisher compatible types. + public var kf: KingfisherWrapper { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension KFCrossPlatformImage : KingfisherCompatible { } +#if !os(watchOS) +extension KFCrossPlatformImageView : KingfisherCompatible { } +extension KFCrossPlatformButton : KingfisherCompatible { } +extension NSTextAttachment : KingfisherCompatible { } +#else +extension WKInterfaceImage : KingfisherCompatible { } +#endif + +#if canImport(PhotosUI) && !os(watchOS) +import PhotosUI +extension PHLivePhotoView : KingfisherCompatible { } +#endif + + +#if os(tvOS) && canImport(TVUIKit) +@available(tvOS 12.0, *) +extension TVMonogramView : KingfisherCompatible { } +#endif + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +@available(iOS 14.0, *) +extension CPListItem : KingfisherCompatible { } +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherError.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherError.swift new file mode 100644 index 00000000..9b5d6ebe --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherError.swift @@ -0,0 +1,669 @@ +// +// KingfisherError.swift +// Kingfisher +// +// Created by onevcat on 2018/09/26. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension Never {} + +/// Represents all the errors that can occur in the Kingfisher framework. +/// +/// Kingfisher-related methods always throw a ``KingfisherError`` or invoke the callback with ``KingfisherError`` +/// as its error type. To handle errors from Kingfisher, you switch over the error to get a reason catalog, +/// then switch over the reason to understand the error details. +/// +public enum KingfisherError: Error { + + // MARK: Error Reason Types + + /// Represents the error reasons during the networking request phase. + public enum RequestErrorReason: Sendable { + + /// The request is empty. + /// + /// Error Code: 1001 + case emptyRequest + + /// The URL of the request is invalid. + /// + /// - Parameter request: The request is intended to be sent, but its URL is invalid. + /// + /// Error Code: 1002 + case invalidURL(request: URLRequest) + + /// The downloading task is canceled by the user. + /// + /// - Parameters: + /// - task: The session data task which is canceled. + /// - token: The cancel token which is used for canceling the task. + /// + /// Error Code: 1003 + case taskCancelled(task: SessionDataTask, token: SessionDataTask.CancelToken) + + /// The live photo downloading task is canceled by the user. + /// + /// - Parameters: + /// - source: The live phot source. + /// + /// Error Code: 1004 + case livePhotoTaskCancelled(source: LivePhotoSource) + + case asyncTaskContextCancelled + } + + /// Represents the error reason during networking response phase. + public enum ResponseErrorReason: Sendable { + + /// The response is not a valid URL response. + /// + /// - Parameters: + /// - response: The received invalid URL response. + /// The response is expected to be an HTTP response, but it is not. + /// + /// Error Code: 2001 + case invalidURLResponse(response: URLResponse) + + /// The response contains an invalid HTTP status code. + /// + /// - Parameters: + /// - response: The received response. + /// + /// Error Code: 2002 + /// + /// - Note: By default, status code 200..<400 is recognized as valid. You can override + /// this behavior by conforming to the `ImageDownloaderDelegate`. + case invalidHTTPStatusCode(response: HTTPURLResponse) + + /// An error happens in the system URL session. + /// + /// - Parameters: + /// - error: The underlying URLSession error object. + /// + /// Error Code: 2003 + case URLSessionError(error: any Error) + + /// Data modifying fails on returning a valid data. + /// + /// - Parameters: + /// - task: The failed task. + /// + /// Error Code: 2004 + case dataModifyingFailed(task: SessionDataTask) + + /// The task is done but no URL response found. + /// + /// - Parameters: + /// - task: The failed task. + /// + /// Error Code: 2005 + case noURLResponse(task: SessionDataTask) + + /// The task is cancelled by ``ImageDownloaderDelegate`` due to the `.cancel` response disposition is + /// specified by the delegate method. + /// + /// - Parameters: + /// - task: The cancelled task. + /// + /// Error Code: 2006 + case cancelledByDelegate(response: URLResponse) + } + + /// Represents the error reason during Kingfisher caching. + public enum CacheErrorReason: @unchecked Sendable { + + /// Cannot create a file enumerator for a certain disk URL. + /// + /// - Parameters: + /// - url: The target disk URL from which the file enumerator should be created. + /// + /// Error Code: 3001 + case fileEnumeratorCreationFailed(url: URL) + + /// Cannot get correct file contents from a file enumerator. + /// + /// - Parameters: + /// - url: The target disk URL from which the content of a file enumerator should be obtained. + /// + /// Error Code: 3002 + case invalidFileEnumeratorContent(url: URL) + + /// The file at the target URL exists, but its URL resource is unavailable. + /// + /// - Parameters: + /// - error: The underlying error thrown by the file manager. + /// - key: The key used to retrieve the resource from cache. + /// - url: The disk URL where the target cached file exists. + /// + /// Error Code: 3003 + case invalidURLResource(error: any Error, key: String, url: URL) + + /// The file at the target URL exists, but the data cannot be loaded from it. + /// + /// - Parameters: + /// - url: The disk URL where the target cached file exists. + /// - error: The underlying error that describes why this error occurs. + /// + /// Error Code: 3004 + case cannotLoadDataFromDisk(url: URL, error: any Error) + + /// Cannot create a folder at a given path. + /// + /// - Parameters: + /// - path: The disk path where the directory creation operation fails. + /// - error: The underlying error that describes why this error occurs. + /// + /// Error Code: 3005 + case cannotCreateDirectory(path: String, error: any Error) + + /// The requested image does not exist in the cache. + /// + /// - Parameters: + /// - key: The key of the requested image in the cache. + /// + /// Error Code: 3006 + case imageNotExisting(key: String) + + /// Unable to convert an object to data for storage. + /// + /// - Parameters: + /// - object: The object that needs to be converted to data. + /// + /// Error Code: 3007 + case cannotConvertToData(object: Any, error: any Error) + + /// Unable to serialize an image to data for storage. + /// + /// - Parameters: + /// - image: The input image that needs to be serialized to cache. + /// - original: The original image data, if it exists. + /// - serializer: The ``CacheSerializer`` used for the image serialization. + /// + /// Error Code: 3008 + case cannotSerializeImage(image: KFCrossPlatformImage?, original: Data?, serializer: any CacheSerializer) + + /// Unable to create the cache file at a specified `fileURL` under a given `key`. + /// + /// - Parameters: + /// - fileURL: The URL where the cache file should be created. + /// - key: The cache key used for the cache. When caching a file through ``KingfisherManager`` and Kingfisher's + /// extension method, it is the resolved cache key based on your input ``Source`` and the image + /// processors. + /// - data: The data to be cached. + /// - error: The underlying error originally thrown by Foundation when attempting to write the `data` to the disk file at + /// `fileURL`. + /// + /// Error Code: 3009 + case cannotCreateCacheFile(fileURL: URL, key: String, data: Data, error: any Error) + + /// Unable to set file attributes for a cached file. + /// + /// - Parameters: + /// - filePath: The path of the target cache file. + /// - attributes: The file attributes to be set for the target file. + /// - error: The underlying error originally thrown by the Foundation framework when attempting to set the specified + /// `attributes` for the disk file at `filePath`. + /// + /// Error Code: 3010 + case cannotSetCacheFileAttribute(filePath: String, attributes: [FileAttributeKey : Any], error: any Error) + + + /// The disk storage for caching is not ready. + /// + /// - Parameters: + /// - cacheURL: The intended URL that should be the storage folder. + /// + /// This issue typically arises due to an extreme lack of space on the disk storage. Kingfisher fails to create + /// the cache folder under these circumstances, rendering the disk storage unusable. In such cases, it is + /// recommended to prompt the user to free up storage space and restart the app to restore functionality. + /// + /// Error Code: 3011 + case diskStorageIsNotReady(cacheURL: URL) + + /// The resource is expected on the disk, but now missing for some reason. + /// + /// This happens when the expected resource is not on the disk for some reason during loading a live photo. + /// + /// Error Code: 3012 + case missingLivePhotoResourceOnDisk(_ resource: LivePhotoResource) + } + + /// Represents the error reason during image processing phase. + public enum ProcessorErrorReason: Sendable { + /// Image processing has failed, and there is no valid output image generated by the processor. + /// + /// - Parameters: + /// - processor: The `ImageProcessor` responsible for processing the image or its data in `item`. + /// - item: The image or its data content. + /// + /// Error Code: 4001 + case processingFailed(processor: any ImageProcessor, item: ImageProcessItem) + } + + /// Represents the error reason during image setting in a view related class. + public enum ImageSettingErrorReason: Sendable { + + /// The input resource is empty or `nil`. + /// + /// Error Code: 5001 + case emptySource + + /// The resource task is completed, but it is not the one that was expected. This typically occurs when you set + /// another resource on the view without canceling the current ongoing task. The previous task will fail with the + /// `.notCurrentSourceTask` error when a result is obtained, regardless of whether it was successful or not for + /// that task. + /// + /// - Parameters: + /// - result: The `RetrieveImageResult` if the source task is completed without any issues. `nil` if an error occurred. + /// - error: The `Error` if there was a problem during the image setting task. `nil` if the task completed successfully. + /// - source: The original source value of the task. + /// + /// Error Code: 5002 + case notCurrentSourceTask(result: RetrieveImageResult?, error: (any Error)?, source: Source) + + /// An error occurs while retrieving data from an `ImageDataProvider`. + /// + /// - Parameters: + /// - provider: The ``ImageDataProvider`` that encountered the error. + /// - error: The underlying error that describes why this error occurred. + /// + /// Error Code: 5003 + case dataProviderError(provider: any ImageDataProvider, error: any Error) + + /// No more alternative ``Source`` can be used in current loading process. It means that the + /// ``KingfisherOptionsInfoItem/alternativeSources(_:)`` are set and Kingfisher tried to recovery from the original error, but still + /// fails for all the given alternative sources. The associated value holds all the errors encountered during + /// the load process, including the original source loading error and all the alternative sources errors. + /// Code 5004. + + /// No more alternative `Source` can be used in the current loading process. + /// + /// - Parameters: + /// - error : A ``PropagationError`` contains more information about the source and error. + /// + /// This means that the ``KingfisherOptionsInfoItem/alternativeSources(_:)`` option is set, and Kingfisher attempted to recover from the original error, + /// but still failed for all the provided alternative sources. The associated value holds all the errors encountered during + /// the loading process, including the original source loading error and all the alternative sources errors. + /// + /// Error Code: 5004 + case alternativeSourcesExhausted([PropagationError]) + + /// The resource task is completed, but it is not the one that was expected. This typically occurs when you set + /// another resource on the view without canceling the current ongoing task. The previous task will fail with the + /// `.notCurrentLivePhotoSourceTask` error when a result is obtained, regardless of whether it was successful or + /// not for that task. + /// + /// This error is the live photo version of the `.notCurrentSourceTask` error (error 5002). + /// + /// - Parameters: + /// - result: The `RetrieveImageResult` if the source task is completed without any issues. `nil` if an error occurred. + /// - error: The `Error` if there was a problem during the image setting task. `nil` if the task completed successfully. + /// - source: The original source value of the task. + /// + /// Error Code: 5005 + case notCurrentLivePhotoSourceTask( + result: RetrieveLivePhotoResult?, error: (any Error)?, source: LivePhotoSource + ) + + /// The error happens during processing the live photo. + /// + /// When creating the final `PHLivePhoto` object from the downloaded image files, the internal Photos framework + /// method `PHLivePhoto.request(withResourceFileURLs:placeholderImage:targetSize:contentMode:resultHandler:)` + /// invokes its `resultHandler`. If the `info` dictionary in `resultHandler` contains `PHLivePhotoInfoErrorKey`, + /// Kingfisher raises this error reason to pass the information to outside. + /// + /// If the processing fails due to any error that is not a `KingfisherError` case, Kingfisher also reports it + /// with this reason. + /// + /// - Parameters: + /// - result: The `RetrieveLivePhotoResult` if the source task is completed and a result is already existing. + /// - error: The `NSError` if `PHLivePhotoInfoErrorKey` is contained in the `resultHandler` info dictionary. + /// - source: The original source value of the task. + /// + /// - Note: It is possible that both `result` and `error` are non-nil value. Check the + /// ``RetrieveLivePhotoResult/info`` property for the raw values that are from the Photos framework. + /// + /// Error Code: 5006 + case livePhotoResultError(result: RetrieveLivePhotoResult?, error: (any Error)?, source: LivePhotoSource) + } + + // MARK: Member Cases + + /// Represents the error reasons that can occur during the networking request phase. + case requestError(reason: RequestErrorReason) + /// Represents the error reason that can occur during networking response phase. + case responseError(reason: ResponseErrorReason) + /// Represents the error reason that can occur during Kingfisher caching phase. + case cacheError(reason: CacheErrorReason) + /// Represents the error reason that can occur during image processing phase. + case processorError(reason: ProcessorErrorReason) + /// Represents the error reason that can occur during image setting in a view related class. + case imageSettingError(reason: ImageSettingErrorReason) + + // MARK: Helper Properties & Methods + + /// A helper property to determine if this error is of type `RequestErrorReason.taskCancelled`. + public var isTaskCancelled: Bool { + if case .requestError(reason: .taskCancelled) = self { + return true + } + return false + } + + /// Helper method to check whether this error is a ``ResponseErrorReason/invalidHTTPStatusCode(response:)`` + /// and the associated value is a given status code. + /// + /// - Parameter code: The given status code. + /// - Returns: If `self` is a `ResponseErrorReason.invalidHTTPStatusCode` error + /// and its status code equals to `code`, `true` is returned. Otherwise, `false`. + /// + + + /// A helper method for checking HTTP status code. + /// + /// Use this helper method to determine whether this error corresponds to a + /// ``ResponseErrorReason/invalidHTTPStatusCode(response:)`` with a specific status code. + /// + /// - Parameter code: The desired HTTP status code for comparison. + /// - Returns: `true` if the error is of type ``ResponseErrorReason/invalidHTTPStatusCode(response:)`` and its + /// status code matches the provided `code`, otherwise `false`. + public func isInvalidResponseStatusCode(_ code: Int) -> Bool { + if case .responseError(reason: .invalidHTTPStatusCode(let response)) = self { + return response.statusCode == code + } + return false + } + + + /// A helper method for checking the error is type of ``ResponseErrorReason/invalidHTTPStatusCode(response:)``. + public var isInvalidResponseStatusCode: Bool { + if case .responseError(reason: .invalidHTTPStatusCode) = self { + return true + } + return false + } + + /// A helper property that indicates whether this error is of type + /// ``ImageSettingErrorReason/notCurrentSourceTask(result:error:source:)`` or not. + /// + /// This property is used to check if a new image setting task starts while the old one is still running. + /// In such a scenario, the identifier of the new task will overwrite the identifier of the old task. + /// + /// When the old task finishes, a ``ImageSettingErrorReason/notCurrentSourceTask(result:error:source:)`` error will + /// be raised to notify you that the setting process has completed with a certain result, but the image view or + /// button has not been updated. + /// + /// - Returns: `true` if the error is of type ``ImageSettingErrorReason/notCurrentSourceTask(result:error:source:)``, + /// `false` otherwise. + public var isNotCurrentTask: Bool { + if case .imageSettingError(reason: .notCurrentSourceTask(_, _, _)) = self { + return true + } + return false + } + + var isLowDataModeConstrained: Bool { + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *), + case .responseError(reason: .URLSessionError(let sessionError)) = self, + let urlError = sessionError as? URLError, + urlError.networkUnavailableReason == .constrained + { + return true + } + return false + } + +} + +// MARK: - LocalizedError Conforming +extension KingfisherError: LocalizedError { + + /// Provides a localized message describing the error that occurred. + /// + /// Use this property to obtain a human-readable description of the error for display to the user. + public var errorDescription: String? { + switch self { + case .requestError(let reason): return reason.errorDescription + case .responseError(let reason): return reason.errorDescription + case .cacheError(let reason): return reason.errorDescription + case .processorError(let reason): return reason.errorDescription + case .imageSettingError(let reason): return reason.errorDescription + } + } +} + + +// MARK: - CustomNSError Conforming +extension KingfisherError: CustomNSError { + + /// The error domain for ``KingfisherError``. All errors generated by Kingfisher are categorized under this domain. + /// + /// When handling errors from the Kingfisher library, you can use this domain to identify and distinguish them + /// from other types of errors in your application. + /// + /// - Note: The error domain is a string identifier associated with each error. + public static let domain = "com.onevcat.Kingfisher.Error" + + /// Represents the error code within the specified error domain. + /// + /// Use this property to retrieve the specific error code associated with a ``KingfisherError``. The error code + /// provides additional context and information about the error, allowing you to handle and respond to different + /// error scenarios. + /// + /// - Note: Error codes are numerical values associated with each error within a domain. Check the error code in the + /// API reference of each error reason for the detail. + /// + /// - Returns: The error code as an integer. + public var errorCode: Int { + switch self { + case .requestError(let reason): return reason.errorCode + case .responseError(let reason): return reason.errorCode + case .cacheError(let reason): return reason.errorCode + case .processorError(let reason): return reason.errorCode + case .imageSettingError(let reason): return reason.errorCode + } + } +} + +extension KingfisherError.RequestErrorReason { + var errorDescription: String? { + switch self { + case .emptyRequest: + return "The request is empty or `nil`." + case .invalidURL(let request): + return "The request contains an invalid or empty URL. Request: \(request)." + case .taskCancelled(let task, let token): + return "The session task was cancelled. Task: \(task), cancel token: \(token)." + case .livePhotoTaskCancelled(let source): + return "The live photo download task was cancelled. Source: \(source)" + case .asyncTaskContextCancelled: + return "The async task context was cancelled. This usually happens when the task is cancelled before it starts." + } + } + + var errorCode: Int { + switch self { + case .emptyRequest: return 1001 + case .invalidURL: return 1002 + case .taskCancelled: return 1003 + case .livePhotoTaskCancelled: return 1004 + case .asyncTaskContextCancelled: return 1005 + } + } +} + +extension KingfisherError.ResponseErrorReason { + var errorDescription: String? { + switch self { + case .invalidURLResponse(let response): + return "The URL response is invalid: \(response)" + case .invalidHTTPStatusCode(let response): + return "The HTTP status code in response is invalid. Code: \(response.statusCode), response: \(response)." + case .URLSessionError(let error): + return "A URL session error happened. The underlying error: \(error)" + case .dataModifyingFailed(let task): + return "The data modifying delegate returned `nil` for the downloaded data. Task: \(task)." + case .noURLResponse(let task): + return "No URL response received. Task: \(task)." + case .cancelledByDelegate(let response): + return "The downloading task is cancelled by the downloader delegate. Response: \(response)." + + } + } + + var errorCode: Int { + switch self { + case .invalidURLResponse: return 2001 + case .invalidHTTPStatusCode: return 2002 + case .URLSessionError: return 2003 + case .dataModifyingFailed: return 2004 + case .noURLResponse: return 2005 + case .cancelledByDelegate: return 2006 + } + } +} + +extension KingfisherError.CacheErrorReason { + var errorDescription: String? { + switch self { + case .fileEnumeratorCreationFailed(let url): + return "Cannot create file enumerator for URL: \(url)." + case .invalidFileEnumeratorContent(let url): + return "Cannot get contents from the file enumerator at URL: \(url)." + case .invalidURLResource(let error, let key, let url): + return "Cannot get URL resource values or data for the given URL: \(url). " + + "Cache key: \(key). Underlying error: \(error)" + case .cannotLoadDataFromDisk(let url, let error): + return "Cannot load data from disk at URL: \(url). Underlying error: \(error)" + case .cannotCreateDirectory(let path, let error): + return "Cannot create directory at given path: Path: \(path). Underlying error: \(error)" + case .imageNotExisting(let key): + return "The image is not in cache, but you requires it should only be " + + "from cache by enabling the `.onlyFromCache` option. Key: \(key)." + case .cannotConvertToData(let object, let error): + return "Cannot convert the input object to a `Data` object when storing it to disk cache. " + + "Object: \(object). Underlying error: \(error)" + case .cannotSerializeImage(let image, let originalData, let serializer): + return "Cannot serialize an image due to the cache serializer returning `nil`. " + + "Image: \(String(describing:image)), original data: \(String(describing: originalData)), " + + "serializer: \(serializer)." + case .cannotCreateCacheFile(let fileURL, let key, let data, let error): + return "Cannot create cache file at url: \(fileURL), key: \(key), data length: \(data.count). " + + "Underlying foundation error: \(error)." + case .cannotSetCacheFileAttribute(let filePath, let attributes, let error): + return "Cannot set file attribute for the cache file at path: \(filePath), attributes: \(attributes)." + + "Underlying foundation error: \(error)." + case .diskStorageIsNotReady(let cacheURL): + return "The disk storage is not ready to use yet at URL: '\(cacheURL)'. " + + "This is usually caused by extremely lack of disk space. Ask users to free up some space and restart the app." + case .missingLivePhotoResourceOnDisk(let resource): + return "The live photo resource '\(resource)' is missing in the cache. Usually a re-download" + + " can fix this issue." + } + } + + var errorCode: Int { + switch self { + case .fileEnumeratorCreationFailed: return 3001 + case .invalidFileEnumeratorContent: return 3002 + case .invalidURLResource: return 3003 + case .cannotLoadDataFromDisk: return 3004 + case .cannotCreateDirectory: return 3005 + case .imageNotExisting: return 3006 + case .cannotConvertToData: return 3007 + case .cannotSerializeImage: return 3008 + case .cannotCreateCacheFile: return 3009 + case .cannotSetCacheFileAttribute: return 3010 + case .diskStorageIsNotReady: return 3011 + case .missingLivePhotoResourceOnDisk: return 3012 + } + } +} + +extension KingfisherError.ProcessorErrorReason { + var errorDescription: String? { + switch self { + case .processingFailed(let processor, let item): + return "Processing image failed. Processor: \(processor). Processing item: \(item)." + } + } + + var errorCode: Int { + switch self { + case .processingFailed: return 4001 + } + } +} + +extension KingfisherError.ImageSettingErrorReason { + var errorDescription: String? { + switch self { + case .emptySource: + return "The input resource is empty." + case .notCurrentSourceTask(let result, let error, let resource): + if let result = result { + return "Retrieving resource succeeded, but this source is " + + "not the one currently expected. Result: \(result). Resource: \(resource)." + } else if let error = error { + return "Retrieving resource failed, and this resource is " + + "not the one currently expected. Error: \(error). Resource: \(resource)." + } else { + return nil + } + case .dataProviderError(let provider, let error): + return "Image data provider fails to provide data. Provider: \(provider), error: \(error)" + case .alternativeSourcesExhausted(let errors): + return "Image setting from alternative sources failed: \(errors)" + case .notCurrentLivePhotoSourceTask(let result, let error, let source): + if let result = result { + return "Retrieving live photo resource succeeded, but this source is " + + "not the one currently expected. Result: \(result). Resource: \(source)." + } else if let error = error { + return "Retrieving live photo resource failed, and this resource is " + + "not the one currently expected. Error: \(error). Resource: \(source)." + } else { + return nil + } + case .livePhotoResultError(let result, let error, let source): + return "An error occurred while processing live photo. Source: \(source). " + + "Result: \(String(describing: result)). Error: \(String(describing: error))" + } + } + + var errorCode: Int { + switch self { + case .emptySource: return 5001 + case .notCurrentSourceTask: return 5002 + case .dataProviderError: return 5003 + case .alternativeSourcesExhausted: return 5004 + case .notCurrentLivePhotoSourceTask: return 5005 + case .livePhotoResultError: return 5006 + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherManager+LivePhoto.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherManager+LivePhoto.swift new file mode 100644 index 00000000..81347c11 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherManager+LivePhoto.swift @@ -0,0 +1,272 @@ +// +// KingfisherManager+LivePhoto.swift +// Kingfisher +// +// Created by onevcat on 2024/10/01. +// +// Copyright (c) 2024 Wei Wang +// +// 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. + +#if !os(watchOS) +@preconcurrency import Photos + +/// A structure that contains information about the result of loading a live photo. +public struct LivePhotoLoadingInfoResult: Sendable { + + /// Retrieves the live photo disk URLs from this result. + public let fileURLs: [URL] + + /// Retrieves the cache source of the image, indicating from which cache layer it was retrieved. + /// + /// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned. + /// Otherwise, ``CacheType/disk`` will be returned for the live photo. ``CacheType/memory`` is not available for + /// live photos since it may take too much memory. All cached live photos are loaded from disk only. + public let cacheType: CacheType + + /// The ``LivePhotoSource`` to which this result is related. This indicates where the `livePhoto` referenced by + /// `self` is located. + public let source: LivePhotoSource + + /// The original ``LivePhotoSource`` from which the retrieval task begins. It may differ from the ``source`` property. + /// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the + /// ``originalSource`` will retain the initial ``source`` that initiated the image loading process. + public let originalSource: LivePhotoSource + + /// Retrieves the data associated with this result. + /// + /// When this result is obtained from a network download (when `cacheType == .none`), calling this method returns + /// the downloaded data. If the result is from the cache, it serializes the image using the specified cache + /// serializer from the loading options and returns the result. + /// + /// - Note: Retrieving this data can be a time-consuming operation, so it is advisable to store it if you need to + /// use it multiple times and avoid frequent calls to this method. + public let data: @Sendable () -> [Data] +} + +extension KingfisherManager { + + /// Retrieves a live photo from the specified source. + /// + /// This method asynchronously loads a live photo from the given source, applying the specified options and + /// reporting progress if a progress block is provided. + /// + /// - Parameters: + /// - source: The ``LivePhotoSource`` from which to retrieve the live photo. + /// - options: A dictionary of options to apply to the retrieval process. If `nil`, the default options will be + /// used. + /// - progressBlock: An optional closure to be called periodically during the download process. + /// - referenceTaskIdentifierChecker: An optional closure that returns a Boolean value indicating whether the task + /// should proceed. + /// + /// - Returns: A ``LivePhotoLoadingInfoResult`` containing information about the retrieved live photo. + /// + /// - Throws: An error if the retrieval process fails. + /// + /// - Note: This method uses `LivePhotoImageProcessor` by default. Custom processors are not supported for live photos. + /// + /// - Warning: Not all options are working for this method. And currently the `progressBlock` is not working. + /// It will be implemented in the future. + public func retrieveLivePhoto( + with source: LivePhotoSource, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + referenceTaskIdentifierChecker: (() -> Bool)? = nil + ) async throws -> LivePhotoLoadingInfoResult { + let fullOptions = currentDefaultOptions + (options ?? .empty) + var checkedOptions = KingfisherParsedOptionsInfo(fullOptions) + + if checkedOptions.processor == DefaultImageProcessor.default { + // The default processor is a default behavior so we replace it silently. + checkedOptions.processor = LivePhotoImageProcessor.default + } else if checkedOptions.processor != LivePhotoImageProcessor.default { + // Warn the framework user that the processor is not supported. + assertionFailure("[Kingfisher] Using of custom processors during loading of live photo resource is not supported.") + checkedOptions.processor = LivePhotoImageProcessor.default + } + + if let checker = referenceTaskIdentifierChecker { + checkedOptions.onDataReceived?.forEach { + $0.onShouldApply = checker + } + } + + // TODO. We ignore the retry of live photo and the progress now to suppress the complexity. + + let missingResources = missingResources(source, options: checkedOptions) + let resourcesResult = try await downloadAndCache(resources: missingResources, options: checkedOptions) + + let targetCache = checkedOptions.targetCache ?? cache + var fileURLs = [URL]() + for resource in source.resources { + let url = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: checkedOptions) + guard let url else { + // This should not happen normally if the previous `downloadAndCache` done without issue, but in case. + throw KingfisherError.cacheError(reason: .missingLivePhotoResourceOnDisk(resource)) + } + fileURLs.append(url) + } + + return LivePhotoLoadingInfoResult( + fileURLs: fileURLs, + cacheType: missingResources.isEmpty ? .disk : .none, + source: source, + originalSource: source, + data: { + resourcesResult.map { $0.originalData } + }) + } + + // Returns the missing resources for the given source and options. If the resource is not in the cache, it will be + // returned as a missing resource. + func missingResources(_ source: LivePhotoSource, options: KingfisherParsedOptionsInfo) -> [LivePhotoResource] { + let missingResources: [LivePhotoResource] + if options.forceRefresh { + missingResources = source.resources + } else { + let targetCache = options.targetCache ?? cache + missingResources = source.resources.reduce([], { r, resource in + // Check if the resource is in the cache. It includes a guess of the file extension. + let cachedFileURL = targetCache.possibleCacheFileURLIfOnDisk(resource: resource, options: options) + if cachedFileURL == nil { + return r + [resource] + } else { + return r + } + }) + } + return missingResources + } + + // Download the resources and store them to the cache. + // If the resource does not specify a file extension (from either the URL extension or the explicit + // `referenceFileType`), we infer it from the file signature. + func downloadAndCache( + resources: [LivePhotoResource], + options: KingfisherParsedOptionsInfo + ) async throws -> [LivePhotoResourceDownloadingResult] { + if resources.isEmpty { + return [] + } + let downloader = options.downloader ?? downloader + let cache = options.targetCache ?? cache + + // Download all resources concurrently. + return try await withThrowingTaskGroup(of: LivePhotoResourceDownloadingResult.self) { + group in + + for resource in resources { + group.addTask { + + let downloadedResource: LivePhotoResourceDownloadingResult + + switch resource.dataSource { + case .network(let urlResource): + downloadedResource = try await downloader.downloadLivePhotoResource( + with: urlResource.downloadURL, + options: options + ) + case .provider(let provider): + downloadedResource = try await LivePhotoResourceDownloadingResult( + originalData: provider.data(), + url: provider.contentURL + ) + } + + // We need to specify the extension so the file is saved correctly. Live photo loading requires + // the file extension to be correct. Otherwise, a 3302 error will be thrown. + // https://developer.apple.com/documentation/photokit/phphotoserror/code/invalidresource + let fileExtension = resource.referenceFileType + .determinedFileExtension(downloadedResource.originalData) + try await cache.storeToDisk( + downloadedResource.originalData, + forKey: resource.cacheKey, + processorIdentifier: options.processor.identifier, + forcedExtension: fileExtension, + expiration: options.diskCacheExpiration + ) + return downloadedResource + } + } + + var result: [LivePhotoResourceDownloadingResult] = [] + for try await resource in group { + result.append(resource) + } + return result + } + } +} + +extension ImageCache { + + func possibleCacheFileURLIfOnDisk( + resource: LivePhotoResource, + options: KingfisherParsedOptionsInfo + ) -> URL? { + possibleCacheFileURLIfOnDisk( + forKey: resource.cacheKey, + processorIdentifier: options.processor.identifier, + referenceFileType: resource.referenceFileType + ) + } + + // Returns the possible cache file URL for the given key and processor identifier. If the file is on disk, it will + // return the URL. Otherwise, it will return `nil`. + // + // This method also tries to guess the file extension if it is not specified in the `referenceFileType`. + // `PHLivePhoto`'s `request` method requires the file extension to be correct on the disk, and we also stored the + // downloaded data with the correct extension (if it is not specified in the `referenceFileType`, we infer it from + // the file signature. See `FileType.determinedFileExtension` for more). + func possibleCacheFileURLIfOnDisk( + forKey key: String, + processorIdentifier identifier: String, + referenceFileType: LivePhotoResource.FileType + ) -> URL? { + switch referenceFileType { + case .heic, .mov: + // The extension is specified and is what necessary to load a live photo, use it. + return cacheFileURLIfOnDisk( + forKey: key, processorIdentifier: identifier, forcedExtension: referenceFileType.fileExtension + ) + case .other(let ext): + if ext.isEmpty { + // The extension is not specified. Guess from the default set of values. + let possibleFileTypes: [LivePhotoResource.FileType] = [.heic, .mov] + for fileType in possibleFileTypes { + let url = cacheFileURLIfOnDisk( + forKey: key, processorIdentifier: identifier, forcedExtension: fileType.fileExtension + ) + if url != nil { + // Found, early return. + return url + } + } + return nil + } else { + // The extension is specified but maybe not valid for live photo. Trust the user and use it to find the + // file. + return cacheFileURLIfOnDisk( + forKey: key, processorIdentifier: identifier, forcedExtension: ext + ) + } + } + } +} +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherManager.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherManager.swift new file mode 100644 index 00000000..c1dbd360 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherManager.swift @@ -0,0 +1,1009 @@ +// +// KingfisherManager.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + + +import Foundation +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Represents the type for a downloading progress block. +/// +/// This block type is used to monitor the progress of data being downloaded. It takes two parameters: +/// +/// 1. `receivedSize`: The size of the data received in the current response. +/// 2. `expectedSize`: The total expected data length from the response's "Content-Length" header. If the expected +/// length is not available, this block will not be called. +/// +/// You can use this progress block to track the download progress and update user interfaces or perform additional +/// actions based on the progress. +/// +/// - Parameters: +/// - receivedSize: The size of the data received. +/// - expectedSize: The expected total data length from the "Content-Length" header. +public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void) + +/// Represents the result of a Kingfisher image retrieval task. +/// +/// This type encapsulates the outcome of an image retrieval operation performed by Kingfisher. +/// It holds a successful result with the retrieved image. +public struct RetrieveImageResult: Sendable { + /// Retrieves the image object from this result. + public let image: KFCrossPlatformImage + + /// Retrieves the cache source of the image, indicating from which cache layer it was retrieved. + /// + /// If the image was freshly downloaded from the network and not retrieved from any cache, `.none` will be returned. + /// Otherwise, either ``CacheType/memory`` or ``CacheType/disk`` will be returned, allowing you to determine whether + /// the image was retrieved from memory or disk cache. + public let cacheType: CacheType + + /// The ``Source`` to which this result is related. This indicates where the `image` referenced by `self` is located. + public let source: Source + + /// The original ``Source`` from which the retrieval task begins. It may differ from the ``source`` property. + /// When an alternative source loading occurs, the ``source`` will represent the replacement loading target, while the + /// ``originalSource`` will retain the initial ``source`` that initiated the image loading process. + public let originalSource: Source + + /// Retrieves the data associated with this result. + /// + /// When this result is obtained from a network download (when `cacheType == .none`), calling this method returns + /// the downloaded data. If the result is from the cache, it serializes the image using the specified cache + /// serializer from the loading options and returns the result. + /// + /// - Note: Retrieving this data can be a time-consuming operation, so it is advisable to store it if you need to + /// use it multiple times and avoid frequent calls to this method. + public let data: @Sendable () -> Data? +} + +/// A structure that stores related information about a ``KingfisherError``. It provides contextual information +/// to facilitate the identification of the error. +public struct PropagationError: Sendable { + + /// The ``Source`` to which current `error` is bound. + public let source: Source + + /// The actual error happens in framework. + public let error: KingfisherError +} + +/// The block type used for handling updates during the downloading task. +/// +/// The `newTask` parameter represents the updated task for the image loading process. It is `nil` if the image loading +/// doesn't involve a downloading process. When an image download is initiated, this value will contain the actual +/// ``DownloadTask`` instance, allowing you to retain it or cancel it later if necessary. +public typealias DownloadTaskUpdatedBlock = (@Sendable (_ newTask: DownloadTask?) -> Void) + +/// The main manager class of Kingfisher. It connects the Kingfisher downloader and cache to offer a set of convenient +/// methods for working with Kingfisher tasks. +/// +/// You can utilize this class to retrieve an image via a specified URL from the web or cache. +public class KingfisherManager: @unchecked Sendable { + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.KingfisherManagerPropertyQueue") + + /// Represents a shared manager used across Kingfisher. + /// Use this instance for getting or storing images with Kingfisher. + public static let shared = KingfisherManager() + + // Mark: Public Properties + + private var _cache: ImageCache + + /// The ``ImageCache`` utilized by this manager, which defaults to ``ImageCache/default``. + /// + /// If a cache is specified in ``KingfisherManager/defaultOptions`` or ``KingfisherOptionsInfoItem/targetCache(_:)``, + /// those specified values will take precedence when Kingfisher attempts to retrieve or store images in the cache. + public var cache: ImageCache { + get { propertyQueue.sync { _cache } } + set { propertyQueue.sync { _cache = newValue } } + } + + private var _downloader: ImageDownloader + + /// The ``ImageDownloader`` utilized by this manager, which defaults to ``ImageDownloader/default``. + /// + /// If a downloader is specified in ``KingfisherManager/defaultOptions`` or ``KingfisherOptionsInfoItem/downloader(_:)``, + /// those specified values will take precedence when Kingfisher attempts to download the image data from a remote + /// server. + public var downloader: ImageDownloader { + get { propertyQueue.sync { _downloader } } + set { propertyQueue.sync { _downloader = newValue } } + } + + /// The default options used by the ``KingfisherManager`` instance. + /// + /// These options are utilized in Kingfisher manager-related methods, as well as all view extension methods. + /// You can also pass additional options for each image task by providing an `options` parameter to Kingfisher's APIs. + /// + /// Per-image options will override the default ones if there is a conflict. + public var defaultOptions = KingfisherOptionsInfo.empty + + // Use `defaultOptions` to overwrite the `downloader` and `cache`. + var currentDefaultOptions: KingfisherOptionsInfo { + return [.downloader(downloader), .targetCache(cache)] + defaultOptions + } + + private let processingQueue: CallbackQueue + + private convenience init() { + self.init(downloader: .default, cache: .default) + } + + /// Creates an image setting manager with the specified downloader and cache. + /// + /// - Parameters: + /// - downloader: The image downloader used for image downloads. + /// - cache: The image cache that stores images in memory and on disk. + /// + public init(downloader: ImageDownloader, cache: ImageCache) { + _downloader = downloader + _cache = cache + + let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)" + processingQueue = .dispatch(DispatchQueue(label: processQueueName)) + } + + // MARK: - Getting Images + + /// Retrieves an image from a specified resource. + /// + /// - Parameters: + /// - resource: The ``Resource`` object defining data information, such as a key or URL. + /// - options: Options to use when creating the image. + /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response + /// contains an `expectedContentLength` and always runs on the main queue. + /// - downloadTaskUpdated: Called when a new image download task is created for the current image retrieval. This + /// typically occurs when an alternative source is used to replace the original (failed) task. You can update your + /// reference to the ``DownloadTask`` if you want to manually invoke ``DownloadTask/cancel()`` on the new task. + /// - completionHandler: Called when the image retrieval and setting are completed. This completion handler is + /// invoked from the `options.callbackQueue`. If not specified, the main queue is used. + /// + /// - Returns: A task representing the image download. If a download task is initiated for a ``Source/network(_:)`` resource, + /// the started ``DownloadTask`` is returned; otherwise, `nil` is returned. + /// + /// - Note: This method first checks whether the requested `resource` is already in the cache. If it is cached, + /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads + /// the `resource`, stores it in the cache, and then calls the `completionHandler`. + /// + @discardableResult + public func retrieveImage( + with resource: any Resource, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? + { + return retrieveImage( + with: resource.convertToSource(), + options: options, + progressBlock: progressBlock, + downloadTaskUpdated: downloadTaskUpdated, + completionHandler: completionHandler + ) + } + + /// Retrieves an image from a specified source. + /// + /// - Parameters: + /// - source: The ``Source`` object defining data information, such as a key or URL. + /// - options: Options to use when creating the image. + /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response + /// contains an `expectedContentLength` and always runs on the main queue. + /// - downloadTaskUpdated: Called when a new image download task is created for the current image retrieval. This + /// typically occurs when an alternative source is used to replace the original (failed) task. You can update your + /// reference to the ``DownloadTask`` if you want to manually invoke ``DownloadTask/cancel()`` on the new task. + /// - completionHandler: Called when the image retrieval and setting are completed. This completion handler is + /// invoked from the `options.callbackQueue`. If not specified, the main queue is used. + /// + /// - Returns: A task representing the image download. If a download task is initiated for a ``Source/network(_:)`` resource, + /// the started ``DownloadTask`` is returned; otherwise, `nil` is returned. + /// + /// - Note: This method first checks whether the requested `source` is already in the cache. If it is cached, + /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads + /// the `source`, stores it in the cache, and then calls the `completionHandler`. + /// + @discardableResult + public func retrieveImage( + with source: Source, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? + { + let options = currentDefaultOptions + (options ?? .empty) + let info = KingfisherParsedOptionsInfo(options) + return retrieveImage( + with: source, + options: info, + progressBlock: progressBlock, + downloadTaskUpdated: downloadTaskUpdated, + completionHandler: completionHandler) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock?, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil, + completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? + { + var info = options + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return retrieveImage( + with: source, + options: info, + downloadTaskUpdated: downloadTaskUpdated, + progressiveImageSetter: progressiveImageSetter, + completionHandler: completionHandler) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil, + referenceTaskIdentifierChecker: (() -> Bool)? = nil, + completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? + { + var options = options + let retryStrategy = options.retryStrategy + + let progressiveJPEG = options.progressiveJPEG + if let provider = ImageProgressiveProvider(options: options, refresh: { image in + guard let setter = progressiveImageSetter else { + return + } + guard let strategy = progressiveJPEG?.onImageUpdated(image) else { + setter(image) + return + } + switch strategy { + case .default: setter(image) + case .keepCurrent: break + case .replace(let newImage): setter(newImage) + } + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + if let checker = referenceTaskIdentifierChecker { + options.onDataReceived?.forEach { + $0.onShouldApply = checker + } + } + + let retrievingContext = RetrievingContext(options: options, originalSource: source) + + @Sendable func startNewRetrieveTask( + with source: Source, + retryContext: RetryContext?, + downloadTaskUpdated: DownloadTaskUpdatedBlock? + ) { + let newTask = self.retrieveImage(with: source, context: retrievingContext) { result in + handler(currentSource: source, retryContext: retryContext, result: result) + } + downloadTaskUpdated?(newTask) + } + + @Sendable func failCurrentSource(_ source: Source, retryContext: RetryContext?, with error: KingfisherError) { + // Skip alternative sources if the user cancelled it. + guard !error.isTaskCancelled else { + completionHandler?(.failure(error)) + return + } + // When low data mode constrained error, retry with the low data mode source instead of use alternative on fly. + guard !error.isLowDataModeConstrained else { + if let source = retrievingContext.options.lowDataModeSource { + retrievingContext.options.lowDataModeSource = nil + startNewRetrieveTask(with: source, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated) + } else { + // This should not happen. + completionHandler?(.failure(error)) + } + return + } + if let nextSource = retrievingContext.popAlternativeSource() { + retrievingContext.appendError(error, to: source) + startNewRetrieveTask(with: nextSource, retryContext: retryContext, downloadTaskUpdated: downloadTaskUpdated) + } else { + // No other alternative source. Finish with error. + if retrievingContext.propagationErrors.isEmpty { + completionHandler?(.failure(error)) + } else { + retrievingContext.appendError(error, to: source) + let finalError = KingfisherError.imageSettingError( + reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors) + ) + completionHandler?(.failure(finalError)) + } + } + } + + @Sendable func handler( + currentSource: Source, + retryContext: RetryContext?, + result: (Result) + ) -> Void { + switch result { + case .success: + completionHandler?(result) + case .failure(let error): + if let retryStrategy = retryStrategy { + let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error) + retryStrategy.retry(context: context) { decision in + switch decision { + case .retry(let userInfo): + context.userInfo = userInfo + startNewRetrieveTask(with: source, retryContext: context, downloadTaskUpdated: downloadTaskUpdated) + case .stop: + failCurrentSource(currentSource, retryContext: context, with: error) + } + } + } else { + failCurrentSource(currentSource, retryContext: retryContext, with: error) + } + } + } + + return retrieveImage( + with: source, + context: retrievingContext) + { + result in + handler(currentSource: source, retryContext: nil, result: result) + } + + } + + private func retrieveImage( + with source: Source, + context: RetrievingContext, + completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask? + { + let options = context.options + if options.forceRefresh { + return loadAndCacheImage( + source: source, + context: context, + completionHandler: completionHandler)?.value + + } else { + let loadedFromCache = retrieveImageFromCache( + source: source, + context: context, + completionHandler: completionHandler) + + if loadedFromCache { + return nil + } + + if options.onlyFromCache { + let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey)) + completionHandler?(.failure(error)) + return nil + } + + return loadAndCacheImage( + source: source, + context: context, + completionHandler: completionHandler)?.value + } + } + + func provideImage( + provider: any ImageDataProvider, + options: KingfisherParsedOptionsInfo, + completionHandler: (@Sendable (Result) -> Void)?) + { + guard let completionHandler = completionHandler else { return } + provider.data { result in + switch result { + case .success(let data): + (options.processingQueue ?? self.processingQueue).execute { + let processor = options.processor + let processingItem = ImageProcessItem.data(data) + guard let image = processor.process(item: processingItem, options: options) else { + options.callbackQueue.execute { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: processingItem)) + completionHandler(.failure(error)) + } + return + } + + options.callbackQueue.execute { + let result = ImageLoadingResult(image: image, url: nil, originalData: data) + completionHandler(.success(result)) + } + } + case .failure(let error): + options.callbackQueue.execute { + let error = KingfisherError.imageSettingError( + reason: .dataProviderError(provider: provider, error: error)) + completionHandler(.failure(error)) + } + + } + } + } + + private func cacheImage( + source: Source, + options: KingfisherParsedOptionsInfo, + context: RetrievingContext, + result: Result, + completionHandler: (@Sendable (Result) -> Void)? + ) + { + switch result { + case .success(let value): + let needToCacheOriginalImage = options.cacheOriginalImage && + options.processor != DefaultImageProcessor.default + let coordinator = CacheCallbackCoordinator( + shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage) + let result = RetrieveImageResult( + image: options.imageModifier?.modify(value.image) ?? value.image, + cacheType: .none, + source: source, + originalSource: context.originalSource, + data: { value.originalData } + ) + // Add image to cache. + let targetCache = options.targetCache ?? self.cache + targetCache.store( + value.image, + original: value.originalData, + forKey: source.cacheKey, + options: options, + toDisk: !options.cacheMemoryOnly) + { + _ in + coordinator.apply(.cachingImage) { + completionHandler?(.success(result)) + } + } + + // Add original image to cache if necessary. + + if needToCacheOriginalImage { + let originalCache = options.originalCache ?? targetCache + originalCache.storeToDisk( + value.originalData, + forKey: source.cacheKey, + processorIdentifier: DefaultImageProcessor.default.identifier, + expiration: options.diskCacheExpiration) + { + _ in + coordinator.apply(.cachingOriginalImage) { + completionHandler?(.success(result)) + } + } + } + + coordinator.apply(.cacheInitiated) { + completionHandler?(.success(result)) + } + + case .failure(let error): + completionHandler?(.failure(error)) + } + } + + @discardableResult + func loadAndCacheImage( + source: Source, + context: RetrievingContext, + completionHandler: (@Sendable (Result) -> Void)?) -> DownloadTask.WrappedTask? + { + let options = context.options + @Sendable func _cacheImage(_ result: Result) { + cacheImage( + source: source, + options: options, + context: context, + result: result, + completionHandler: completionHandler + ) + } + + switch source { + case .network(let resource): + let downloader = options.downloader ?? self.downloader + let task = downloader.downloadImage( + with: resource.downloadURL, options: options, completionHandler: _cacheImage + ) + + + // The code below is neat, but it fails the Swift 5.2 compiler with a runtime crash when + // `BUILD_LIBRARY_FOR_DISTRIBUTION` is turned on. I believe it is a bug in the compiler. + // Let's fallback to a traditional style before it can be fixed in Swift. + // + // https://github.com/onevcat/Kingfisher/issues/1436 + // + // return task.map(DownloadTask.WrappedTask.download) + + if task.isInitialized { + return .download(task) + } else { + return nil + } + + case .provider(let provider): + provideImage(provider: provider, options: options, completionHandler: _cacheImage) + return .dataProviding + } + } + + /// Retrieves an image from either memory or disk cache. + /// + /// - Parameters: + /// - source: The target source from which to retrieve the image. + /// - key: The key to use for caching the image. + /// - url: The image request URL. This is not used when retrieving an image from the cache; it is solely used for + /// compatibility with ``RetrieveImageResult`` callbacks. + /// - options: Options on how to retrieve the image from the image cache. + /// - completionHandler: Called when the image retrieval is complete, either with a successful + /// ``RetrieveImageResult`` or an error. + /// + /// - Returns: `true` if the requested image or the original image before processing exists in the cache. Otherwise, this method returns `false`. + /// + /// - Note: Image retrieval can occur in either the memory cache or the disk cache. The + /// ``KingfisherOptionsInfoItem/processor(_:)`` option in `options` is considered when searching the cache. If no + /// processed image is found, Kingfisher attempts to determine whether an original version of the image exists. If + /// an original exists, Kingfisher retrieves it from the cache and processes it. Subsequently, the processed image + /// is stored back in the cache for future use. + /// + func retrieveImageFromCache( + source: Source, + context: RetrievingContext, + completionHandler: (@Sendable (Result) -> Void)?) -> Bool + { + let options = context.options + // 1. Check whether the image was already in target cache. If so, just get it. + let targetCache = options.targetCache ?? cache + let key = source.cacheKey + let targetImageCached = targetCache.imageCachedType( + forKey: key, processorIdentifier: options.processor.identifier) + + let validCache = targetImageCached.cached && + (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory) + if validCache { + targetCache.retrieveImage(forKey: key, options: options) { result in + guard let completionHandler = completionHandler else { return } + + // TODO: Optimize it when we can use async across all the project. + @Sendable func checkResultImageAndCallback(_ inputImage: KFCrossPlatformImage) { + var image = inputImage + if image.kf.imageFrameCount != nil && image.kf.imageFrameCount != 1, options.imageCreatingOptions != image.kf.imageCreatingOptions, let data = image.kf.animatedImageData { + // Recreate animated image representation when loaded in different options. + // https://github.com/onevcat/Kingfisher/issues/1923 + image = options.processor.process(item: .data(data), options: options) ?? .init() + } + if let modifier = options.imageModifier { + image = modifier.modify(image) + } + let value = result.map { + RetrieveImageResult( + image: image, + cacheType: $0.cacheType, + source: source, + originalSource: context.originalSource, + data: { [image] in options.cacheSerializer.data(with: image, original: nil) } + ) + } + completionHandler(value) + } + + result.match { cacheResult in + options.callbackQueue.execute { + guard let image = cacheResult.image else { + completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))) + return + } + + if options.cacheSerializer.originalDataUsed { + let processor = options.processor + (options.processingQueue ?? self.processingQueue).execute { + let item = ImageProcessItem.image(image) + guard let processedImage = processor.process(item: item, options: options) else { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: item)) + options.callbackQueue.execute { completionHandler(.failure(error)) } + return + } + options.callbackQueue.execute { + checkResultImageAndCallback(processedImage) + } + } + } else { + checkResultImageAndCallback(image) + } + } + } onFailure: { error in + options.callbackQueue.execute { + completionHandler(.failure(error)) + } + } + } + return true + } + + // 2. Check whether the original image exists. If so, get it, process it, save to storage and return. + let originalCache = options.originalCache ?? targetCache + // No need to store the same file in the same cache again. + if originalCache === targetCache && options.processor == DefaultImageProcessor.default { + return false + } + + // Check whether the unprocessed image existing or not. + let originalImageCacheType = originalCache.imageCachedType( + forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier) + let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh + + let canUseOriginalImageCache = + (canAcceptDiskCache && originalImageCacheType.cached) || + (!canAcceptDiskCache && originalImageCacheType == .memory) + + if canUseOriginalImageCache { + // Now we are ready to get found the original image from cache. We need the unprocessed image, so remove + // any processor from options first. + var optionsWithoutProcessor = options + optionsWithoutProcessor.processor = DefaultImageProcessor.default + originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in + + result.match( + onSuccess: { cacheResult in + guard let image = cacheResult.image else { + assertionFailure("The image (under key: \(key) should be existing in the original cache.") + return + } + + let processor = options.processor + (options.processingQueue ?? self.processingQueue).execute { + let item = ImageProcessItem.image(image) + guard let processedImage = processor.process(item: item, options: options) else { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: item)) + options.callbackQueue.execute { completionHandler?(.failure(error)) } + return + } + + var cacheOptions = options + cacheOptions.callbackQueue = .untouch + + let coordinator = CacheCallbackCoordinator( + shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false) + + let image = options.imageModifier?.modify(processedImage) ?? processedImage + let result = RetrieveImageResult( + image: image, + cacheType: .none, + source: source, + originalSource: context.originalSource, + data: { options.cacheSerializer.data(with: processedImage, original: nil) } + ) + + targetCache.store( + processedImage, + forKey: key, + options: cacheOptions, + toDisk: !options.cacheMemoryOnly) + { + _ in + coordinator.apply(.cachingImage) { + options.callbackQueue.execute { completionHandler?(.success(result)) } + } + } + + coordinator.apply(.cacheInitiated) { + options.callbackQueue.execute { completionHandler?(.success(result)) } + } + } + }, + onFailure: { error in + // This should not happen actually, since we already confirmed `originalImageCached` is `true`. + // Just in case... + if let completionHandler = completionHandler { + options.callbackQueue.execute { completionHandler(.failure(error)) } + } + } + ) + } + return true + } + + return false + } +} + +// Concurrency +extension KingfisherManager { + + /// Retrieves an image from a specified resource. + /// + /// - Parameters: + /// - resource: The ``Resource`` object defining data information, such as a key or URL. + /// - options: Options to use when creating the image. + /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response + /// contains an `expectedContentLength` and always runs on the main queue. + /// + /// - Returns: The ``RetrieveImageResult`` containing the retrieved image object and cache type. + /// - Throws: A ``KingfisherError`` if any issue occurred during the image retrieving progress. + /// + /// - Note: This method first checks whether the requested `resource` is already in the cache. If it is cached, + /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads + /// the `resource`, stores it in the cache, and then calls the `completionHandler`. + /// + public func retrieveImage( + with resource: any Resource, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil + ) async throws -> RetrieveImageResult + { + try await retrieveImage( + with: resource.convertToSource(), + options: options, + progressBlock: progressBlock + ) + } + + /// Retrieves an image from a specified source. + /// + /// - Parameters: + /// - source: The ``Source`` object defining data information, such as a key or URL. + /// - options: Options to use when creating the image. + /// - progressBlock: Called when the image download progress is updated. This block is invoked only if the response + /// contains an `expectedContentLength` and always runs on the main queue. + /// + /// - Returns: The ``RetrieveImageResult`` containing the retrieved image object and cache type. + /// - Throws: A ``KingfisherError`` if any issue occurred during the image retrieving progress. + /// + /// - Note: This method first checks whether the requested `source` is already in the cache. If it is cached, + /// it returns `nil` and invokes the `completionHandler` after retrieving the cached image. Otherwise, it downloads + /// the `source`, stores it in the cache, and then calls the `completionHandler`. + /// + public func retrieveImage( + with source: Source, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil + ) async throws -> RetrieveImageResult + { + let options = currentDefaultOptions + (options ?? .empty) + let info = KingfisherParsedOptionsInfo(options) + return try await retrieveImage( + with: source, + options: info, + progressBlock: progressBlock + ) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil + ) async throws -> RetrieveImageResult + { + var info = options + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return try await retrieveImage( + with: source, + options: info, + progressiveImageSetter: nil + ) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil, + referenceTaskIdentifierChecker: (() -> Bool)? = nil + ) async throws -> RetrieveImageResult + { + // Early cancellation check + if Task.isCancelled { + throw CancellationError() + } + + let task = CancellationDownloadTask() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + // Use an actor to ensure continuation is only resumed once in a Swift 6 compatible way + actor ContinuationState { + var isResumed = false + + func tryResume() -> Bool { + if !isResumed { + isResumed = true + return true + } + return false + } + } + + let state = ContinuationState() + + @Sendable func safeResume(with result: Result) { + Task { + if await state.tryResume() { + continuation.resume(with: result) + } + } + } + + let downloadTask = retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { newTask in + Task { + await task.setTask(newTask) + } + }, + progressiveImageSetter: progressiveImageSetter, + referenceTaskIdentifierChecker: referenceTaskIdentifierChecker, + completionHandler: { result in + safeResume(with: result) + } + ) + + // Check for cancellation that may have occurred during setup + if Task.isCancelled { + downloadTask?.cancel() + let error: KingfisherError + if let sessionTask = downloadTask?.sessionTask, let cancelToken = downloadTask?.cancelToken { + error = .requestError(reason: .taskCancelled(task: sessionTask, token: cancelToken)) + } else { + error = .requestError(reason: .asyncTaskContextCancelled) + } + safeResume(with: .failure(error)) + } else { + Task { + await task.setTask(downloadTask) + } + } + } + } onCancel: { + Task { + await task.task?.cancel() + } + } + } +} + +class RetrievingContext: @unchecked Sendable { + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.RetrievingContextPropertyQueue") + + private var _options: KingfisherParsedOptionsInfo + var options: KingfisherParsedOptionsInfo { + get { propertyQueue.sync { _options } } + set { propertyQueue.sync { _options = newValue } } + } + + let originalSource: SourceType + var propagationErrors: [PropagationError] = [] + + init(options: KingfisherParsedOptionsInfo, originalSource: SourceType) { + self.originalSource = originalSource + _options = options + } + + func popAlternativeSource() -> Source? { + var localOptions = options + guard var alternativeSources = localOptions.alternativeSources, !alternativeSources.isEmpty else { + return nil + } + let nextSource = alternativeSources.removeFirst() + + localOptions.alternativeSources = alternativeSources + options = localOptions + + return nextSource + } + + @discardableResult + func appendError(_ error: KingfisherError, to source: Source) -> [PropagationError] { + let item = PropagationError(source: source, error: error) + propagationErrors.append(item) + return propagationErrors + } +} + +class CacheCallbackCoordinator: @unchecked Sendable { + + enum State { + case idle + case imageCached + case originalImageCached + case done + } + + enum Action { + case cacheInitiated + case cachingImage + case cachingOriginalImage + } + + private let shouldWaitForCache: Bool + private let shouldCacheOriginal: Bool + private let stateQueue: DispatchQueue + private var threadSafeState: State = .idle + + private(set) var state: State { + set { stateQueue.sync { threadSafeState = newValue } } + get { stateQueue.sync { threadSafeState } } + } + + init(shouldWaitForCache: Bool, shouldCacheOriginal: Bool) { + self.shouldWaitForCache = shouldWaitForCache + self.shouldCacheOriginal = shouldCacheOriginal + let stateQueueName = "com.onevcat.Kingfisher.CacheCallbackCoordinator.stateQueue.\(UUID().uuidString)" + self.stateQueue = DispatchQueue(label: stateQueueName) + } + + func apply(_ action: Action, trigger: () -> Void) { + switch (state, action) { + case (.done, _): + break + + // From .idle + case (.idle, .cacheInitiated): + if !shouldWaitForCache { + state = .done + trigger() + } + case (.idle, .cachingImage): + if shouldCacheOriginal { + state = .imageCached + } else { + state = .done + trigger() + } + case (.idle, .cachingOriginalImage): + state = .originalImageCached + + // From .imageCached + case (.imageCached, .cachingOriginalImage): + state = .done + trigger() + + // From .originalImageCached + case (.originalImageCached, .cachingImage): + state = .done + trigger() + + default: + assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)") + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift new file mode 100644 index 00000000..57162a00 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift @@ -0,0 +1,501 @@ +// +// KingfisherOptionsInfo.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/23. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + + +/// `KingfisherOptionsInfo` is a typealias for `[KingfisherOptionsInfoItem]`. +/// You can utilize the enum of option items with values to control certain behaviors of Kingfisher. +public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem] + +extension Array where Element == KingfisherOptionsInfoItem { + static let empty: KingfisherOptionsInfo = [] +} + +/// Represents the available option items that can be used in ``KingfisherOptionsInfo``. +public enum KingfisherOptionsInfoItem: Sendable { + + /// Kingfisher will utilize the associated ``ImageCache`` object when performing related operations, such as + /// attempting to retrieve cached images and storing downloaded images in it. + case targetCache(ImageCache) + + /// The ``ImageCache`` used for storing and retrieving original images. + /// + /// If ``originalCache(_:)`` is specified in the options, it will be given preference for storing and retrieving + /// original images. If there is no ``originalCache(_:)`` option, ``targetCache(_:)`` will be used to + /// store original images as well. + /// + /// When using ``KingfisherManager`` to download and store an image, if ``cacheOriginalImage`` is applied in the + /// options, the original image will be stored in the associated ``ImageCache`` of this option. + /// + /// Simultaneously, if a requested final image (with a processor applied) cannot be found in the ``targetCache(_:)``, + /// Kingfisher will attempt to search for the original image to see if it already exists. If found, it will be + /// utilized and processed with the given processor. This optimization prevents downloading the same image multiple + /// times. + case originalCache(ImageCache) + + /// Kingfisher will utilize the associated ``ImageDownloader`` object to download the requested images. + case downloader(ImageDownloader) + + /// This enum defines the transition effect to be applied when setting an image to an image view. + /// + /// Kingfisher uses the ``ImageTransition`` specified by this enum to animate the image in if it's downloaded from + /// the web. + /// + /// By default, the transition does not occur when the image is retrieved from either memory or disk cache. To + /// force the transition even when the image is retrieved from the cache, also set + /// ``KingfisherOptionsInfoItem/forceTransition``. + case transition(ImageTransition) + + /// The associated `Float` value to be set as the priority of the image download task. + /// + /// This value should fall within the range of 0.0 to 1.0. If this option is not set, the default value + /// (`URLSessionTask.defaultPriority`) will be used. + case downloadPriority(Float) + + /// When set, Kingfisher will disregard the cache and attempt to initiate a download task for the image source. + case forceRefresh + + /// Sets whether Kingfisher should try to load from memory cache first, and then perform a refresh from network. + /// + /// When set, Kingfisher will attempt to retrieve the image from memory cache first. If the image is not found in + /// the memory cache, it will skip the disk cache and download the image again from the network. This is useful + /// when you want to display a changeable image with the same URL within the same app session, while avoiding + /// multiple downloads. + case fromMemoryCacheOrRefresh + + /// When set, applying a transition to set the image in an image view will occur even when the image is retrieved + /// from the cache. Refer to the ``transition(_:)`` option for more details. + case forceTransition + + /// When set, Kingfisher will cache the value only in memory and not on disk. + case cacheMemoryOnly + + /// When set, Kingfisher will wait for the caching operation to be completed before invoking the completion block. + case waitForCache + + /// When set, Kingfisher will attempt to retrieve the image solely from the cache and not from the network. + /// + /// If the image is not found in the cache, the image retrieval will fail with a + /// ``KingfisherError/CacheErrorReason/imageNotExisting(key:)`` error. + case onlyFromCache + + /// Decode the image on a background thread before usage. + /// + /// This process involves decoding the downloaded image data and performing off-screen rendering to extract pixel + /// information in the background. While this can accelerate display performance, it may require additional time + /// to prepare the image for use. + case backgroundDecode + + /// The associated value will be used as the target queue of dispatch callbacks when retrieving images from + /// cache. If not set, Kingfisher will use `.mainCurrentOrAsync` for callbacks. + /// + /// - Note: This option does not affect the callbacks for UI related extension methods. You will always get the + /// callbacks called from main queue. + + /// The associated value will serve as the target queue for dispatch callbacks when retrieving images from the cache. + /// + /// If not set, Kingfisher will use ``CallbackQueue/mainCurrentOrAsync`` for callbacks. + /// + /// - Note: This option does not impact the callbacks for UI-related extension methods. Those callbacks will always + /// occur on the main queue. + case callbackQueue(CallbackQueue) + + /// The associated value will be used as the scale factor when converting retrieved image data to an image object. + /// + /// Specify the image scale rather than your screen scale. You should set the correct scale when dealing with 2x or + /// 3x retina images. Otherwise, Kingfisher will convert the data to an image object with a scale of 1.0. + case scaleFactor(CGFloat) + + /// Determines whether all the animated image data should be preloaded. + /// + /// The default value is `false`, which means only the following frames will be loaded on demand. If set to `true`, + /// all the animated image data will be loaded and decoded into memory. + /// + /// This option is primarily used for internal backward compatibility. It should not be set directly. Instead, you + /// should choose the appropriate image view class to control the GIF data loading. Kingfisher offers two classes + /// for displaying GIF images: ``AnimatedImageView``, which does not preload all data, consumes less memory, but uses + /// more CPU during display; and a regular image view (`UIImageView` or `NSImageView`), which loads all data at + /// once, consumes more memory, but decodes image frames only once. + case preloadAllAnimationData + + /// The contained ``ImageDownloadRequestModifier`` will be used to alter the request before it is sent. + /// + /// This is the final opportunity to modify the image download request. You can customize the request for various + /// purposes, such as adding an authentication token to the header, performing basic HTTP authentication, or URL + /// mapping. + /// + /// By default, the original request is sent without any modifications. + case requestModifier(any AsyncImageDownloadRequestModifier) + + /// The contained ``ImageDownloadRedirectHandler`` will be used to alter the request during redirection. + /// + /// This provides an opportunity to customize the image download request during redirection. You can modify the + /// request for various purposes, such as adding an authentication token to the header, performing basic HTTP + /// authentication, or URL mapping. + /// + /// By default, the original redirection request is sent without any modifications. + case redirectHandler(any ImageDownloadRedirectHandler) + + /// The processor used in the image retrieval task. + /// + /// After downloading is complete, a processor will convert the downloaded data into an image and/or apply various + /// filters or transformations to it. + /// + /// If a cache is linked to the downloader (which occurs when you use ``KingfisherManager`` or any of the view + /// extension methods), the converted image will also be stored in the cache. If not set, the + /// ``DefaultImageProcessor/default`` will be used. + case processor(any ImageProcessor) + + /// Offers a ``CacheSerializer`` to convert data into an image object for retrieval from disk cache, or vice versa + /// for storage in the disk cache. + /// + /// If not set, the ``DefaultCacheSerializer/default`` will be used. + case cacheSerializer(any CacheSerializer) + + /// An ``ImageModifier`` for making adjustments to an image right before it is used. + /// + /// If the image was directly fetched from the downloader, the modifier will be applied immediately after the + /// ``ImageProcessor``. If the image is retrieved from a cache, the modifier will be applied after the + /// ``CacheSerializer``. + /// + /// Use the ``ImageModifier`` when you need to set properties that do not persist when caching the image with a + /// specific image type. Examples include setting the `renderingMode` or `alignmentInsets` of a `UIImage`. + case imageModifier(any ImageModifier) + + /// Keep the existing image of image view while setting another image to it. + /// By setting this option, the placeholder image parameter of image view extension method + /// will be ignored and the current image will be kept while loading or downloading the new image. + case keepCurrentImageWhileLoading + + /// When set, Kingfisher will load only the first frame from an animated image file as a single image. + /// + /// Loading animated images can consume a significant amount of memory. This option is useful when you want to + /// display a static preview of the first frame from an animated image. It will be ignored if the target image is + /// not animated image data. + case onlyLoadFirstFrame + + /// When set and an non-default ``ImageProcessor`` is used, Kingfisher will attempt to cache both the final result + /// and the original image. + /// + /// Kingfisher will have the opportunity to use the original image when another processor is applied to the same + /// resource, instead of downloading it anew. You can use ``KingfisherOptionsInfoItem/originalCache(_:)`` to + /// specify a cache for the original images if necessary. + /// + /// The original image will only be cached to disk storage. + case cacheOriginalImage + + /// When set and an image retrieval error occurs, Kingfisher will replace the requested image with the provided + /// image (or an empty image). + /// + /// This is useful when you prefer not to display a placeholder during loading but want to use a default image when + /// requests fail. + case onFailureImage(KFCrossPlatformImage?) + + /// When set and used in methods of ``ImagePrefetcher``, the prefetching operation will aggressively load the images + /// into memory storage. + /// + /// By default, this option is not included in the options. This means that if the requested image is already in + /// the disk cache, Kingfisher will not attempt to load it into memory. + case alsoPrefetchToMemory + + /// When set, disk storage loading will occur in the same calling queue. + /// + /// By default, disk storage file loading operates on its own queue with asynchronous dispatch behavior. While this + /// provides improved non-blocking disk loading performance, it can lead to flickering when you reload an image from + /// disk if the image view already has an image set. + /// + /// Setting this option will eliminate that flickering by keeping all loading in the same queue (typically the UI + /// queue if you are using Kingfisher's extension methods to set an image). However, this comes with a tradeoff in + /// loading performance. + case loadDiskFileSynchronously + + /// Options for controlling the data writing process to disk storage. + /// + /// When set, these options will be passed to the store operation for new files. + case diskStoreWriteOptions(Data.WritingOptions) + + /// When set, use the associated ``StorageExpiration`` value for the memory cache to determine the expiration date. + /// + /// By default, the underlying ``MemoryStorage/Backend`` uses the expiration defined in its ``MemoryStorage/Config`` + /// for all items. If this option is set, the ``MemoryStorage/Backend`` will utilize the associated value to + /// override the configuration setting for this caching item. + case memoryCacheExpiration(StorageExpiration) + + /// When set, use the associated ``ExpirationExtending`` value for the memory cache to determine the extending policy + /// when setting the next expiration date. + /// + /// The item's expiration date will be extended after access to keep the "most recently accessed" items alive for a + /// longer duration in the cache. + /// + /// By default, the underlying ``MemoryStorage/Backend`` uses the initial cache expiration as the extending value, + /// which is ``ExpirationExtending/cacheTime``. + /// + /// - Note: To disable expiration extending entirely, use ``ExpirationExtending/none``. + case memoryCacheAccessExtendingExpiration(ExpirationExtending) + + /// When set, use the associated ``StorageExpiration`` value for the disk cache to determine the expiration date. + /// + /// By default, the underlying ``DiskStorage/Backend`` uses the expiration defined in its ``DiskStorage/Config`` + /// for all items. If this option is set, the ``DiskStorage/Backend`` will utilize the associated value to override + /// the configuration setting for this caching item. + case diskCacheExpiration(StorageExpiration) + + /// When set, use the associated ``ExpirationExtending`` value for the disk cache to determine the extending policy + /// when setting the next expiration date. + /// + /// The item's expiration date will be extended after access to keep the "most recently accessed" items alive for a + /// longer duration in the cache. + /// + /// By default, the underlying ``DiskStorage/Backend`` uses the initial cache expiration as the extending value, + /// which is ``ExpirationExtending/cacheTime``. + /// + /// - Note: To disable expiration extending entirely, use ``ExpirationExtending/none``. + case diskCacheAccessExtendingExpiration(ExpirationExtending) + + /// Determines the queue on which image processing should occur. + /// + /// By default, Kingfisher uses an internal pre-defined serial queue to process images. Use this option to modify + /// this behavior. For instance, you can specify ``CallbackQueue/mainCurrentOrAsync`` to process the image on the + /// main queue, preventing potential flickering (but with the risk of blocking the UI, especially if the processor + /// is time-consuming). + case processingQueue(CallbackQueue) + + /// Enables progressive image loading. + /// + /// Kingfisher will use the associated ``ImageProgressive`` value to process progressive JPEG data and display + /// it progressively, if the image supports it. + case progressiveJPEG(ImageProgressive) + + /// Sets a set of alternative sources when the original input `Source` fails to load. + /// + /// The `Source`s in the associated + /// array will be used to start a new image loading task if the previous task fails due to an error. The image + /// source loading process will stop as soon as a source is loaded successfully. If all `[Source]`s are used but + /// the loading is still failing, an `imageSettingError` with `alternativeSourcesExhausted` as its reason will be + /// thrown out. + /// + /// This option is useful if you want to implement a fallback solution for setting image. + /// + /// User cancellation will not trigger the alternative source loading. + /// + + /// Specifies a set of alternative sources to use when the original input ``Source`` fails to load. + /// + /// The ``Source``s in the associated array will be used to start a new image loading task if the previous task + /// fails due to an error. The image source loading process will halt as soon as a source is loaded successfully. + /// If all ``Source``s are used, but loading still fails, a + /// ``KingfisherError/ImageSettingErrorReason/alternativeSourcesExhausted(_:)``will be used as the error in the + /// result. + /// + /// This option is useful for implementing a fallback solution for image setting. + /// + /// - Note: User cancellation will not trigger the loading of alternative sources. + case alternativeSources([Source]) + + /// Provides a retry strategy to use when something goes wrong during the image retrieval process from + /// ``KingfisherManager``. + /// + /// You can define a strategy by creating a type that conforms to the ``RetryStrategy`` protocol. When Kingfisher + /// encounters a loading failure, it follows the defined retry strategy and retries until a ``RetryDecision/stop`` + /// is received. + /// + /// - Note: All extension methods of Kingfisher (the `kf` extensions on `UIImageView` or `UIButton`, for example) + /// retrieve images through ``KingfisherManager``, so the retry strategy also applies when using them. However, + /// this option does not apply when passed to an ``ImageDownloader`` or an ``ImageCache`` directly. + case retryStrategy(any RetryStrategy) + + /// Specifies the `Source` to load when the user enables Low Data Mode and the original source fails due to the data + /// constraint. + /// + /// When the user enables Low Data Mode in the system settings, and the original source fails with an + /// `NSURLErrorNetworkUnavailableReason.constrained` error, Kingfisher uses this source instead to load an image + /// for Low Data Mode. + /// + /// When this option is set, the `allowsConstrainedNetworkAccess` property of the request for the original source + /// will be set to `false`, and the ``Source`` in the associated value will be used to retrieve the image for Low + /// Data Mode. Typically, you can provide a low-resolution version of your image or a local image provider to + /// display a placeholder to save data usage. + /// + /// If not set or if the associated optional ``Source`` value is `nil`, the device's Low Data Mode will be ignored, + /// and the original source will be loaded following the system default behavior. + case lowDataMode(Source?) + + case forcedCacheFileExtension(String?) +} + +// MARK: - KingfisherParsedOptionsInfo + +// Improve performance by parsing the input `KingfisherOptionsInfo` (self) first. +// So we can prevent the iterating over the options array again and again. + +/// Represents the parsed options info used throughout Kingfisher methods. +/// +/// Each property in this type corresponds to a case member in ``KingfisherOptionsInfoItem``. When a +/// ``KingfisherOptionsInfo`` is sent to Kingfisher-related methods, it will be parsed and converted to a +/// ``KingfisherParsedOptionsInfo`` first before passing through the internal methods. +public struct KingfisherParsedOptionsInfo: Sendable { + + public var targetCache: ImageCache? = nil + public var originalCache: ImageCache? = nil + public var downloader: ImageDownloader? = nil + public var transition: ImageTransition = .none + public var downloadPriority: Float = URLSessionTask.defaultPriority + public var forceRefresh = false + public var fromMemoryCacheOrRefresh = false + public var forceTransition = false + public var cacheMemoryOnly = false + public var waitForCache = false + public var onlyFromCache = false + public var backgroundDecode = false + public var preloadAllAnimationData = false + public var callbackQueue: CallbackQueue = .mainCurrentOrAsync + public var scaleFactor: CGFloat = 1.0 + public var requestModifier: (any AsyncImageDownloadRequestModifier)? = nil + public var redirectHandler: (any ImageDownloadRedirectHandler)? = nil + public var processor: any ImageProcessor = DefaultImageProcessor.default + public var imageModifier: (any ImageModifier)? = nil + public var cacheSerializer: any CacheSerializer = DefaultCacheSerializer.default + public var keepCurrentImageWhileLoading = false + public var onlyLoadFirstFrame = false + public var cacheOriginalImage = false + public var onFailureImage: Optional = .none + public var alsoPrefetchToMemory = false + public var loadDiskFileSynchronously = false + public var diskStoreWriteOptions: Data.WritingOptions = [] + public var memoryCacheExpiration: StorageExpiration? = nil + public var memoryCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime + public var diskCacheExpiration: StorageExpiration? = nil + public var diskCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime + public var processingQueue: CallbackQueue? = nil + public var progressiveJPEG: ImageProgressive? = nil + public var alternativeSources: [Source]? = nil + public var retryStrategy: (any RetryStrategy)? = nil + public var lowDataModeSource: Source? = nil + public var forcedExtension: String? = nil + + var onDataReceived: [any DataReceivingSideEffect]? = nil + + public init(_ info: KingfisherOptionsInfo?) { + guard let info = info else { return } + for option in info { + switch option { + case .targetCache(let value): targetCache = value + case .originalCache(let value): originalCache = value + case .downloader(let value): downloader = value + case .transition(let value): transition = value + case .downloadPriority(let value): downloadPriority = value + case .forceRefresh: forceRefresh = true + case .fromMemoryCacheOrRefresh: fromMemoryCacheOrRefresh = true + case .forceTransition: forceTransition = true + case .cacheMemoryOnly: cacheMemoryOnly = true + case .waitForCache: waitForCache = true + case .onlyFromCache: onlyFromCache = true + case .backgroundDecode: backgroundDecode = true + case .preloadAllAnimationData: preloadAllAnimationData = true + case .callbackQueue(let value): callbackQueue = value + case .scaleFactor(let value): scaleFactor = value + case .requestModifier(let value): requestModifier = value + case .redirectHandler(let value): redirectHandler = value + case .processor(let value): processor = value + case .imageModifier(let value): imageModifier = value + case .cacheSerializer(let value): cacheSerializer = value + case .keepCurrentImageWhileLoading: keepCurrentImageWhileLoading = true + case .onlyLoadFirstFrame: onlyLoadFirstFrame = true + case .cacheOriginalImage: cacheOriginalImage = true + case .onFailureImage(let value): onFailureImage = .some(value) + case .alsoPrefetchToMemory: alsoPrefetchToMemory = true + case .loadDiskFileSynchronously: loadDiskFileSynchronously = true + case .diskStoreWriteOptions(let options): diskStoreWriteOptions = options + case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration + case .memoryCacheAccessExtendingExpiration(let expirationExtending): memoryCacheAccessExtendingExpiration = expirationExtending + case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration + case .diskCacheAccessExtendingExpiration(let expirationExtending): diskCacheAccessExtendingExpiration = expirationExtending + case .processingQueue(let queue): processingQueue = queue + case .progressiveJPEG(let value): progressiveJPEG = value + case .alternativeSources(let sources): alternativeSources = sources + case .retryStrategy(let strategy): retryStrategy = strategy + case .lowDataMode(let source): lowDataModeSource = source + case .forcedCacheFileExtension(let ext): forcedExtension = ext + } + } + + if originalCache == nil { + originalCache = targetCache + } + } +} + +extension KingfisherParsedOptionsInfo { + var imageCreatingOptions: ImageCreatingOptions { + return ImageCreatingOptions( + scale: scaleFactor, + duration: 0.0, + preloadAll: preloadAllAnimationData, + onlyFirstFrame: onlyLoadFirstFrame) + } +} + +protocol DataReceivingSideEffect: AnyObject, Sendable { + var onShouldApply: () -> Bool { get set } + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) +} + +class ImageLoadingProgressSideEffect: DataReceivingSideEffect, @unchecked Sendable { + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageLoadingProgressSideEffectPropertyQueue") + + private var _onShouldApply: () -> Bool = { return true } + + var onShouldApply: () -> Bool { + get { propertyQueue.sync { _onShouldApply } } + set { propertyQueue.sync { _onShouldApply = newValue } } + } + + let block: DownloadProgressBlock + + init(_ block: @escaping DownloadProgressBlock) { + self.block = block + } + + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { + DispatchQueue.main.async { + guard self.onShouldApply() else { return } + guard let expectedContentLength = task.task.response?.expectedContentLength, + expectedContentLength != -1 else + { + return + } + + let dataLength = Int64(task.mutableData.count) + self.block(dataLength, expectedContentLength) + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/Filter.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/Filter.swift new file mode 100644 index 00000000..fc32a6ee --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/Filter.swift @@ -0,0 +1,173 @@ +// +// Filter.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/31. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if !os(watchOS) + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +import CoreImage + +// Reuses the same CI Context for all CI drawings. +struct SendableBox: @unchecked Sendable { + let value: T +} + +private let ciContext = SendableBox(value: CIContext(options: nil)) + +/// Represents the type of transformer method, which will be used to provide a ``Filter``. +public typealias Transformer = (CIImage) -> CIImage? + +/// Represents an ``ImageProcessor`` based on a ``Filter``, for images of `CIImage`. +/// +/// You can use any ``Filter``, or in other words, a ``Transformer`` to convert a `CIImage` to another, to create a +/// ``ImageProcessor`` type easily. +public protocol CIImageProcessor: ImageProcessor { + var filter: Filter { get } +} + +extension CIImageProcessor { + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.apply(filter) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// A wrapper struct for a `Transformer` of CIImage filters. +/// +/// A ``Filter`` value can be used to create an ``ImageProcessor`` for `CIImage`s. +public struct Filter { + + let transform: Transformer + + /// Creates a ``Filter`` from a given ``Transformer``. + /// + /// - Parameter transform: The value defines how a `CIImage` can be converted to another one. + public init(transform: @escaping Transformer) { + self.transform = transform + } + + /// Tint filter that applies a tint color to images. + public static let tint: @Sendable (KFCrossPlatformColor) -> Filter = { + color in + Filter { + input in + + let colorFilter = CIFilter(name: "CIConstantColorGenerator")! + colorFilter.setValue(CIColor(color: color), forKey: kCIInputColorKey) + + let filter = CIFilter(name: "CISourceOverCompositing")! + + let colorImage = colorFilter.outputImage + filter.setValue(colorImage, forKey: kCIInputImageKey) + filter.setValue(input, forKey: kCIInputBackgroundImageKey) + + return filter.outputImage?.cropped(to: input.extent) + } + } + + /// Represents color control elements. + /// + /// It contains necessary variables which can be applied as a filter to `CIImage.applyingFilter` feature as + /// "CIColorControls". + public struct ColorElement { + public let brightness: CGFloat + public let contrast: CGFloat + public let saturation: CGFloat + public let inputEV: CGFloat + + /// Creates a ``ColorElement`` value with given parameters. + /// - Parameters: + /// - brightness: The brightness change applied to the image. + /// - contrast: The contrast change applied to the image. + /// - saturation: The saturation change applied to the image. + /// - inputEV: The EV (F-stops brighter or darker) change applied to the image. + public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) { + self.brightness = brightness + self.contrast = contrast + self.saturation = saturation + self.inputEV = inputEV + } + } + + /// Color control filter that applies color control changes to images. + public static let colorControl: @Sendable (ColorElement) -> Filter = { arg -> Filter in + return Filter { input in + let paramsColor = [kCIInputBrightnessKey: arg.brightness, + kCIInputContrastKey: arg.contrast, + kCIInputSaturationKey: arg.saturation] + let blackAndWhite = input.applyingFilter("CIColorControls", parameters: paramsColor) + let paramsExposure = [kCIInputEVKey: arg.inputEV] + return blackAndWhite.applyingFilter("CIExposureAdjust", parameters: paramsExposure) + } + } +} + +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// Applies a `Filter` containing a `CIImage` transformer to `self`. + /// + /// - Parameters: + /// - filter: The filter used to transform `self`. + /// - Returns: A transformed image by the input `Filter`. + /// + /// > Important: Only CG-based images are supported. If an error occurs during transformation, + /// ``KingfisherWrapper/base`` will be returned. + public func apply(_ filter: Filter) -> KFCrossPlatformImage { + + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Tint image only works for CG-based image.") + return base + } + + let inputImage = CIImage(cgImage: cgImage) + guard let outputImage = filter.transform(inputImage) else { + return base + } + + guard let result = ciContext.value.createCGImage(outputImage, from: outputImage.extent) else { + assertionFailure("[Kingfisher] Can not make an tint image within context.") + return base + } + + #if os(macOS) + return fixedForRetinaPixel(cgImage: result, to: size) + #else + return KFCrossPlatformImage(cgImage: result, scale: base.scale, orientation: base.imageOrientation) + #endif + } + +} + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift new file mode 100644 index 00000000..77e7a324 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift @@ -0,0 +1,205 @@ +// +// AnimatedImage.swift +// Kingfisher +// +// Created by onevcat on 2018/09/26. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +import ImageIO + +/// Represents a set of image creation options used in Kingfisher. +public struct ImageCreatingOptions: Equatable { + + /// The target scale of the image that needs to be created. + public var scale: CGFloat + + /// The expected animation duration if an animated image is being created. + public var duration: TimeInterval + + /// For an animated image, indicates whether or not all frames should be loaded before displaying. + public var preloadAll: Bool + + /// For an animated image, indicates whether only the first image should be + /// loaded as a static image. It is useful for previewing an animated image. + public var onlyFirstFrame: Bool + + /// Creates an `ImageCreatingOptions` object. + /// + /// - Parameters: + /// - scale: The target scale of the image that needs to be created. Default is `1.0`. + /// - duration: The expected animation duration if an animated image is being created. + /// A value less than or equal to `0.0` means the animated image duration will + /// be determined by the frame data. Default is `0.0`. + /// - preloadAll: For an animated image, whether or not all frames should be loaded before displaying. + /// Default is `false`. + /// - onlyFirstFrame: For an animated image, whether only the first image should be + /// loaded as a static image. It is useful for previewing an animated image. + /// Default is `false`. + public init( + scale: CGFloat = 1.0, + duration: TimeInterval = 0.0, + preloadAll: Bool = false, + onlyFirstFrame: Bool = false + ) + { + self.scale = scale + self.duration = duration + self.preloadAll = preloadAll + self.onlyFirstFrame = onlyFirstFrame + } +} + +/// Represents the decoding for a GIF image. This class extracts frames from an ``ImageFrameSource``, and then +/// holds the images for later use. +public class GIFAnimatedImage { + let images: [KFCrossPlatformImage] + let duration: TimeInterval + + init?(from frameSource: any ImageFrameSource, options: ImageCreatingOptions) { + let frameCount = frameSource.frameCount + var images = [KFCrossPlatformImage]() + var gifDuration = 0.0 + + for i in 0 ..< frameCount { + guard let imageRef = frameSource.frame(at: i) else { + return nil + } + + if frameCount == 1 { + gifDuration = .infinity + } else { + // Get current animated GIF frame duration + gifDuration += frameSource.duration(at: i) + } + images.append(KingfisherWrapper.image(cgImage: imageRef, scale: options.scale, refImage: nil)) + if options.onlyFirstFrame { break } + } + self.images = images + self.duration = gifDuration + } + + convenience init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) { + let frameSource = CGImageFrameSource(data: nil, imageSource: imageSource, options: info) + self.init(from: frameSource, options: options) + } + + /// Calculates the frame duration for a GIF frame out of the `kCGImagePropertyGIFDictionary` dictionary. + public static func getFrameDuration(from gifInfo: [String: Any]?) -> TimeInterval { + let defaultFrameDuration = 0.1 + guard let gifInfo = gifInfo else { return defaultFrameDuration } + + let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber + let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber + let duration = unclampedDelayTime ?? delayTime + + guard let frameDuration = duration else { return defaultFrameDuration } + return frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration + } + + /// Calculates the frame duration at a specific index for a GIF from an `CGImageSource`. + /// + /// - Parameters: + /// - imageSource: The image source where the animated image information should be extracted from. + /// - index: The index of the target frame in the image. + /// - Returns: The time duration of the frame at given index in the image. + public static func getFrameDuration(from imageSource: CGImageSource, at index: Int) -> TimeInterval { + guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil) + as? [String: Any] else { return 0.0 } + + let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] + return getFrameDuration(from: gifInfo) + } +} + +/// Represents a frame source for an animated image. +public protocol ImageFrameSource { + + /// Source data associated with this frame source. + var data: Data? { get } + + /// Count of the total frames in this frame source. + var frameCount: Int { get } + + /// Retrieves the frame at a specific index. + /// + /// The resulting image is expected to be no larger than `maxSize`. If the index is invalid, + /// implementors should return `nil`. + func frame(at index: Int, maxSize: CGSize?) -> CGImage? + + /// Retrieves the duration at a specific index. If the index is invalid, implementors should return `0.0`. + func duration(at index: Int) -> TimeInterval + + /// Creates a copy of the current `ImageFrameSource` instance. + /// + /// - Returns: A new instance of the same type as `self` with identical properties. + /// If not overridden by conforming types, this default implementation + /// simply returns `self`, which may not create an actual copy if the type is a reference type. + func copy() -> Self +} + +public extension ImageFrameSource { + + /// Retrieves the frame at a specific index. If the index is invalid, implementors should return `nil`. + func frame(at index: Int) -> CGImage? { + return frame(at: index, maxSize: nil) + } + + func copy() -> Self { + return self + } +} + +struct CGImageFrameSource: ImageFrameSource { + let data: Data? + let imageSource: CGImageSource + let options: [String: Any]? + + var frameCount: Int { + return CGImageSourceGetCount(imageSource) + } + + func frame(at index: Int, maxSize: CGSize?) -> CGImage? { + var options = self.options as? [CFString: Any] + if let maxSize = maxSize, maxSize != .zero { + options = (options ?? [:]).merging([ + kCGImageSourceCreateThumbnailFromImageIfAbsent: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceThumbnailMaxPixelSize: max(maxSize.width, maxSize.height) + ], uniquingKeysWith: { $1 }) + } + return CGImageSourceCreateImageAtIndex(imageSource, index, options as CFDictionary?) + } + + func duration(at index: Int) -> TimeInterval { + return GIFAnimatedImage.getFrameDuration(from: imageSource, at: index) + } + + func copy() -> Self { + guard let data = data, let source = CGImageSourceCreateWithData(data as CFData, options as CFDictionary?) else { + return self + } + return CGImageFrameSource(data: data, imageSource: source, options: options) + } +} + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/GraphicsContext.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/GraphicsContext.swift new file mode 100644 index 00000000..4ff0f11d --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/GraphicsContext.swift @@ -0,0 +1,98 @@ +// +// GraphicsContext.swift +// Kingfisher +// +// Created by taras on 19/04/2021. +// +// Copyright (c) 2021 Wei Wang +// +// 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. + +#if os(macOS) || os(watchOS) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +enum GraphicsContext { + static func begin(size: CGSize, scale: CGFloat) { + #if os(macOS) + NSGraphicsContext.saveGraphicsState() + #elseif os(watchOS) + UIGraphicsBeginImageContextWithOptions(size, false, scale) + #else + assertionFailure("This method is deprecated on the current platform and should not be used.") + #endif + } + + static func current(size: CGSize, scale: CGFloat, inverting: Bool, cgImage: CGImage?) -> CGContext? { + #if os(macOS) + guard let rep = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: Int(size.width), + pixelsHigh: Int(size.height), + bitsPerSample: cgImage?.bitsPerComponent ?? 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .calibratedRGB, + bytesPerRow: 0, + bitsPerPixel: 0) else + { + assertionFailure("[Kingfisher] Image representation cannot be created.") + return nil + } + rep.size = size + guard let context = NSGraphicsContext(bitmapImageRep: rep) else { + assertionFailure("[Kingfisher] Image context cannot be created.") + return nil + } + + NSGraphicsContext.current = context + return context.cgContext + #elseif os(watchOS) + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + if inverting { // If drawing a CGImage, we need to make context flipped. + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: 0, y: -size.height) + } + return context + #else + assertionFailure("This method is deprecated on the current platform and should not be used.") + return nil + #endif + } + + static func end() { + #if os(macOS) + NSGraphicsContext.restoreGraphicsState() + #elseif os(watchOS) + UIGraphicsEndImageContext() + #else + assertionFailure("This method is deprecated on the current platform and should not be used.") + #endif + } +} + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/Image.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/Image.swift new file mode 100644 index 00000000..f773d5b8 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/Image.swift @@ -0,0 +1,462 @@ +// +// Image.swift +// Kingfisher +// +// Created by Wei Wang on 16/1/6. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + + +#if os(macOS) +import AppKit +#else // os(macOS) +import UIKit +import MobileCoreServices +#endif // os(macOS) + +#if !os(watchOS) +import CoreImage +#endif + +import CoreGraphics +import ImageIO + +#if canImport(UniformTypeIdentifiers) +import UniformTypeIdentifiers +#endif + +#if compiler(>=5.10) +nonisolated(unsafe) private let animatedImageDataKey = malloc(1)! +nonisolated(unsafe) private let imageFrameCountKey = malloc(1)! +nonisolated(unsafe) private let imageSourceKey = malloc(1)! +nonisolated(unsafe) private let imageCreatingOptionsKey = malloc(1)! +#if os(macOS) +nonisolated(unsafe) private let imagesKey = malloc(1)! +nonisolated(unsafe) private let durationKey = malloc(1)! +#endif // os(macOS) +#else // compiler(>=5.10) +private let animatedImageDataKey = malloc(1)! +private let imageFrameCountKey = malloc(1)! +private let imageSourceKey = malloc(1)! +private let imageCreatingOptionsKey = malloc(1)! +#if os(macOS) +private let imagesKey = malloc(1)! +private let durationKey = malloc(1)! +#endif // os(macOS) +#endif // compiler(>=5.10) + +// MARK: - Image Properties +extension KingfisherWrapper where Base: KFCrossPlatformImage { + private(set) var animatedImageData: Data? { + get { return getAssociatedObject(base, animatedImageDataKey) } + set { setRetainedAssociatedObject(base, animatedImageDataKey, newValue) } + } + + private(set) var imageCreatingOptions: ImageCreatingOptions? { + get { return getAssociatedObject(base, imageCreatingOptionsKey) } + set { setRetainedAssociatedObject(base, imageCreatingOptionsKey, newValue) } + } + + public var imageFrameCount: Int? { + get { return getAssociatedObject(base, imageFrameCountKey) } + set { setRetainedAssociatedObject(base, imageFrameCountKey, newValue) } + } + + #if os(macOS) + var cgImage: CGImage? { + return base.cgImage(forProposedRect: nil, context: nil, hints: nil) + } + + var scale: CGFloat { + return 1.0 + } + + private(set) var images: [KFCrossPlatformImage]? { + get { return getAssociatedObject(base, imagesKey) } + set { setRetainedAssociatedObject(base, imagesKey, newValue) } + } + + private(set) var duration: TimeInterval { + get { return getAssociatedObject(base, durationKey) ?? 0.0 } + set { setRetainedAssociatedObject(base, durationKey, newValue) } + } + + var size: CGSize { + // Prefer to use pixel size of the image + let pixelSize = base.representations.reduce(.zero) { size, rep in + CGSize( + width: max(size.width, CGFloat(rep.pixelsWide)), + height: max(size.height, CGFloat(rep.pixelsHigh)) + ) + } + // If the pixel size is zero (SVG or PDF, for example), use the size of the image. + return pixelSize == .zero ? base.representations.reduce(.zero) { size, rep in + CGSize( + width: max(size.width, CGFloat(rep.size.width)), + height: max(size.height, CGFloat(rep.size.height)) + ) + } : pixelSize + } + #else + var cgImage: CGImage? { return base.cgImage } + var scale: CGFloat { return base.scale } + var images: [KFCrossPlatformImage]? { return base.images } + var duration: TimeInterval { return base.duration } + var size: CGSize { return base.size } + + /// The source reference for the current image. + public var imageSource: CGImageSource? { + get { + guard let frameSource = frameSource as? CGImageFrameSource else { return nil } + return frameSource.imageSource + } + } + #endif + + /// The custom frame source for the current image. + public private(set) var frameSource: (any ImageFrameSource)? { + get { return getAssociatedObject(base, imageSourceKey) } + set { setRetainedAssociatedObject(base, imageSourceKey, newValue) } + } + + // Bitmap memory cost with bytes. + var cost: Int { + let pixel = Int(size.width * size.height * scale * scale) + guard let cgImage = cgImage else { + return pixel * 4 + } + let bytesPerPixel = cgImage.bitsPerPixel / 8 + guard let imageCount = images?.count else { + return pixel * bytesPerPixel + } + return pixel * bytesPerPixel * imageCount + } +} + +// MARK: - Image Conversion +extension KingfisherWrapper where Base: KFCrossPlatformImage { + #if os(macOS) + static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage { + return KFCrossPlatformImage(cgImage: cgImage, size: .zero) + } + + /// The normalized image. On macOS, this getter returns the image itself without performing any additional operations. + public var normalized: KFCrossPlatformImage { return base } + #else + + /// Create an image from a given `CGImage` with specified scale and orientation, tailored for `refImage`. This + /// method signature is designed for compatibility with macOS versions. + /// + /// - Parameters: + /// - cgImage: The `CGImage` which is used to create the `UIImage` object. + /// - scale: The scale. + /// - refImage: The ref image which is used to determine the image orientation. + /// - Returns: The created image object. + static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage { + return KFCrossPlatformImage(cgImage: cgImage, scale: scale, orientation: refImage?.imageOrientation ?? .up) + } + + /// The normalized image for the current `base` image. + /// + /// This method attempts to redraw the image, taking orientation and scale into account. + public var normalized: KFCrossPlatformImage { + // prevent animated image (GIF) lose it's images + guard images == nil else { return base.copy() as! KFCrossPlatformImage } + // No need to do anything if already up + guard base.imageOrientation != .up else { return base.copy() as! KFCrossPlatformImage } + + return draw(to: size, inverting: true, refImage: KFCrossPlatformImage()) { + fixOrientation(in: $0) + return true + } + } + + func fixOrientation(in context: CGContext) { + guard let cgImage else { return } + + var transform = CGAffineTransform.identity + let orientation = base.imageOrientation + + switch orientation { + case .down, .downMirrored: + transform = transform.translatedBy(x: size.width, y: size.height) + transform = transform.rotated(by: .pi) + case .left, .leftMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.rotated(by: .pi / 2.0) + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: size.height) + transform = transform.rotated(by: .pi / -2.0) + case .up, .upMirrored: + break + @unknown default: + break + } + + // Flip image one more time if needed for mirrored images. This is to prevent the flipped image. + switch orientation { + case .upMirrored, .downMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .leftMirrored, .rightMirrored: + transform = transform.translatedBy(x: size.height, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .up, .down, .left, .right: + break + @unknown default: + break + } + + context.concatenate(transform) + switch orientation { + case .left, .leftMirrored, .right, .rightMirrored: + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) + default: + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + } + } + #endif +} + +// MARK: - Image Representation +extension KingfisherWrapper where Base: KFCrossPlatformImage { + /// Returns a data object that contains the specified image in PNG format. + /// + /// - Returns: PNG data of image. + public func pngRepresentation() -> Data? { + #if os(macOS) + guard let cgImage = cgImage else { + return nil + } + let rep = NSBitmapImageRep(cgImage: cgImage) + return rep.representation(using: .png, properties: [:]) + #else + return base.pngData() + #endif + } + + /// Returns a data object that contains the specified image in JPEG format. + /// + /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. + /// - Returns: JPEG data of image. + public func jpegRepresentation(compressionQuality: CGFloat) -> Data? { + #if os(macOS) + guard let cgImage = cgImage else { + return nil + } + let rep = NSBitmapImageRep(cgImage: cgImage) + return rep.representation(using:.jpeg, properties: [.compressionFactor: compressionQuality]) + #else + return base.jpegData(compressionQuality: compressionQuality) + #endif + } + + /// Returns GIF representation of `base` image. + /// + /// - Returns: Original GIF data of image. + public func gifRepresentation() -> Data? { + return animatedImageData + } + + /// Returns a data representation for the `base` image with the specified `format`. + /// + /// - Parameters: + /// - format: The desired format for the output data. If set to `unknown`, the `base` image will be + /// converted to PNG representation. + /// - compressionQuality: The compression quality when converting the image to a lossy format data. + /// + /// - Returns: The resulting data representation. + public func data(format: ImageFormat, compressionQuality: CGFloat = 1.0) -> Data? { + return autoreleasepool { () -> Data? in + let data: Data? + switch format { + case .PNG: data = pngRepresentation() + case .JPEG: data = jpegRepresentation(compressionQuality: compressionQuality) + case .GIF: data = gifRepresentation() + case .unknown: data = normalized.kf.pngRepresentation() + } + + return data + } + } +} + +// MARK: - Creating Images +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// Creates an animated image from provided data and options. + /// + /// - Parameters: + /// - data: The data containing the animated image. + /// - options: Options to be used when creating the animated image. + /// - Returns: An `Image` object representing the animated image. It's structured as an array of image frames, + /// each with a specific duration. Returns `nil` if any issues occur during animated image creation. + /// + /// - Note: Currently, only GIF data is supported. + public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { + #if os(visionOS) + let info: [String: Any] = [ + kCGImageSourceShouldCache as String: true, + kCGImageSourceTypeIdentifierHint as String: UTType.gif.identifier + ] + #else + let info: [String: Any] = [ + kCGImageSourceShouldCache as String: true, + kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF + ] + #endif + + guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else { + return nil + } + let frameSource = CGImageFrameSource(data: data, imageSource: imageSource, options: info) + #if os(macOS) + let baseImage = KFCrossPlatformImage(data: data) + #else + let baseImage = KFCrossPlatformImage(data: data, scale: options.scale) + #endif + return animatedImage(source: frameSource, options: options, baseImage: baseImage) + } + + /// Creates an animated image from a given frame source. + /// + /// - Parameters: + /// - source: The frame source from which to create the animated image. + /// - options: Options to be used during animated image creation. + /// - baseImage: An optional image object to serve as the key frame of the animated image. If `nil`, the first + /// frame of the `source` will be used. + /// - Returns: An `Image` object representing the animated image. It consists of an array of image frames, each with a + /// specific duration. Returns `nil` if any issues arise during animated image creation. + public static func animatedImage(source: any ImageFrameSource, options: ImageCreatingOptions, baseImage: KFCrossPlatformImage? = nil) -> KFCrossPlatformImage? { + #if os(macOS) + guard let animatedImage = GIFAnimatedImage(from: source, options: options) else { + return nil + } + var image: KFCrossPlatformImage? + if options.onlyFirstFrame { + image = animatedImage.images.first + } else { + if let baseImage = baseImage { + image = baseImage + } else { + image = animatedImage.images.first + } + var kf = image?.kf + kf?.images = animatedImage.images + kf?.duration = animatedImage.duration + } + image?.kf.animatedImageData = source.data + image?.kf.imageFrameCount = source.frameCount + image?.kf.frameSource = source + image?.kf.imageCreatingOptions = options + return image + #else + + var image: KFCrossPlatformImage? + if options.preloadAll || options.onlyFirstFrame { + // Use `images` image if you want to preload all animated data + guard let animatedImage = GIFAnimatedImage(from: source, options: options) else { + return nil + } + if options.onlyFirstFrame { + image = animatedImage.images.first + } else { + let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration + image = .animatedImage(with: animatedImage.images, duration: duration) + } + image?.kf.animatedImageData = source.data + } else { + if let baseImage = baseImage { + image = baseImage + } else { + guard let firstFrame = source.frame(at: 0) else { + return nil + } + image = KFCrossPlatformImage(cgImage: firstFrame, scale: options.scale, orientation: .up) + } + var kf = image?.kf + kf?.frameSource = source + kf?.animatedImageData = source.data + } + + image?.kf.imageFrameCount = source.frameCount + image?.kf.imageCreatingOptions = options + return image + #endif + } + + /// Creates an image from provided data and options. Supported formats include `.JPEG`, `.PNG`, or `.GIF`. For + /// other image formats, the system's image initializer will be used. If no image object can be created from the + /// given `data`, `nil` will be returned. + /// + /// - Parameters: + /// - data: The data representing the image. + /// - options: Options to be used when creating the image. + /// - Returns: An `Image` object representing the image if successfully created. If the `data` is invalid or + /// unsupported, `nil` will be returned. + public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? { + var image: KFCrossPlatformImage? + switch data.kf.imageFormat { + case .JPEG: + image = KFCrossPlatformImage(data: data, scale: options.scale) + case .PNG: + image = KFCrossPlatformImage(data: data, scale: options.scale) + case .GIF: + image = KingfisherWrapper.animatedImage(data: data, options: options) + case .unknown: + image = KFCrossPlatformImage(data: data, scale: options.scale) + } + return image + } + + /// Creates a downsampled image from the given data to a specified size and scale. + /// + /// - Parameters: + /// - data: The image data containing a JPEG or PNG image. + /// - pointSize: The target size in points to which the image should be downsampled. + /// - scale: The scale of the resulting image. + /// - Returns: A downsampled `Image` object adhering to the specified conditions. + /// + /// Unlike image `resize` methods, downsampling does not render the original input image in pixel format. + /// Instead, it downsamples directly from the image data, making it more memory-efficient and friendly. Whenever + /// possible, consider using downsampling. + /// + /// > Important: The `pointSize` should be smaller than the size of the input image. If it is larger than the original image + /// > size, the resulting image will have the same dimensions as the input without downsampling. + public static func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> KFCrossPlatformImage? { + let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary + guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else { + return nil + } + + let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale + let downsampleOptions: [CFString : Any] = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels + ] + guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions as CFDictionary) else { + return nil + } + return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageDrawing.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageDrawing.swift new file mode 100644 index 00000000..b01a8a57 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageDrawing.swift @@ -0,0 +1,713 @@ +// +// ImageDrawing.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Accelerate + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +extension KingfisherWrapper where Base: KFCrossPlatformImage { + // MARK: - Image Transforming + + // MARK: Blend Mode + +#if !os(macOS) + /// Create an image from the `base` image and apply a blend mode. + /// + /// - Parameters: + /// - blendMode: The blend mode to be applied to the image. + /// - alpha: The alpha value to be used for the image. + /// - backgroundColor: The background color for the output image. + /// - Returns: An image with the specified blend mode applied. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + public func image(withBlendMode blendMode: CGBlendMode, + alpha: CGFloat = 1.0, + backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size, inverting: false) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + UIRectFill(rect) + } + + base.draw(in: rect, blendMode: blendMode, alpha: alpha) + return false + } + } +#endif + +#if os(macOS) + // MARK: Compositing + + /// Create an image from the `base` image and apply a compositing operation. + /// + /// - Parameters: + /// - compositingOperation: The compositing operation to be applied to the image. + /// - alpha: The alpha value to be used for the image. + /// - backgroundColor: The background color for the output image. + /// - Returns: An image with the specified compositing operation applied. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + public func image(withCompositingOperation compositingOperation: NSCompositingOperation, + alpha: CGFloat = 1.0, + backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Compositing Operation image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size, inverting: false) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + rect.fill() + } + base.draw(in: rect, from: .zero, operation: compositingOperation, fraction: alpha) + return false + } + } +#endif + + // MARK: Round Corner + + /// Create a rounded corner image from the `base` image. + /// + /// - Parameters: + /// - radius: The radius for rounding the corners of the image. + /// - size: The target size of the resulting image. + /// - corners: The corners to which rounding will be applied. + /// - backgroundColor: The background color for the output image. + /// - Returns: An image with rounded corners based on `self`. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + public func image( + withRadius radius: Radius, + fit size: CGSize, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> KFCrossPlatformImage + { + + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Round corner image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + return draw(to: size, inverting: false) { _ in + #if os(macOS) + if let backgroundColor = backgroundColor { + let rectPath = NSBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) + path.addClip() + base.draw(in: rect) + #else + guard let context = UIGraphicsGetCurrentContext() else { + assertionFailure("[Kingfisher] Failed to create CG context for image.") + return false + } + + if let backgroundColor = backgroundColor { + let rectPath = UIBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) + context.addPath(path.cgPath) + context.clip() + base.draw(in: rect) + #endif + return false + } + } + + /// Create a round corner image from the `base` image. + /// + /// - Parameters: + /// - radius: The radius for rounding the corners of the image. + /// - size: The target size of the resulting image. + /// - corners: The corners to which rounding will be applied. + /// - backgroundColor: The background color for the output image. + /// - Returns: An image with rounded corners based on `self`. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + public func image( + withRoundRadius radius: CGFloat, + fit size: CGSize, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> KFCrossPlatformImage + { + image(withRadius: .point(radius), fit: size, roundingCorners: corners, backgroundColor: backgroundColor) + } + + #if os(macOS) + func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> NSBezierPath { + let cornerRadius = radius.compute(with: rect.size) + let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: cornerRadius - offsetBase / 2) + path.windingRule = .evenOdd + return path + } + #else + func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> UIBezierPath { + let cornerRadius = radius.compute(with: rect.size) + return UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners.uiRectCorner, + cornerRadii: CGSize( + width: cornerRadius - offsetBase / 2, + height: cornerRadius - offsetBase / 2 + ) + ) + } + #endif + + #if os(iOS) || os(tvOS) || os(visionOS) + func resize(to size: CGSize, for contentMode: UIView.ContentMode) -> KFCrossPlatformImage { + switch contentMode { + case .scaleAspectFit: + return resize(to: size, for: .aspectFit) + case .scaleAspectFill: + return resize(to: size, for: .aspectFill) + default: + return resize(to: size) + } + } + #endif + + // MARK: Resizing + + /// Resize the `base` image to a new size. + /// + /// - Parameter size: The target size in points. + /// - Returns: An image with the new size. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + /// + /// > Tip: This method resizes the `base` image to a specified size by drawing it into that size. If you require a + /// smaller thumbnail of the image, consider using ``downsampledImage(data:to:scale:)`` instead, as it offers + /// improved efficiency. + public func resize(to size: CGSize) -> KFCrossPlatformImage { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Resize only works for CG-based image.") + return base + } + + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + return draw(to: size, inverting: false) { _ in + #if os(macOS) + base.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + #else + base.draw(in: rect) + #endif + return false + } + } + + /// Resize the `base` image to a new size while respecting the specified content mode. + /// + /// - Parameters: + /// - targetSize: The target size in points. + /// - contentMode: The desired content mode for the output image. + /// - Returns: An image with the new size. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + /// + /// > Tip: This method resizes the `base` image to a specified size by drawing it into that size. If you require a + /// smaller thumbnail of the image, consider using ``downsampledImage(data:to:scale:)`` instead, as it offers + /// improved efficiency. + public func resize(to targetSize: CGSize, for contentMode: ContentMode) -> KFCrossPlatformImage { + let newSize = size.kf.resize(to: targetSize, for: contentMode) + return resize(to: newSize) + } + + // MARK: Cropping + + /// Crop the `base` image to a new size with a specified anchor point. + /// + /// - Parameters: + /// - size: The target size. + /// - anchor: The anchor point from which the size should be calculated. + /// - Returns: An image with the new size. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + public func crop(to size: CGSize, anchorOn anchor: CGPoint) -> KFCrossPlatformImage { + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Crop only works for CG-based image.") + return base + } + + let rect = self.size.kf.constrainedRect(for: size, anchor: anchor) + guard let image = cgImage.cropping(to: rect.scaled(scale)) else { + assertionFailure("[Kingfisher] Cropping image failed.") + return base + } + + return KingfisherWrapper.image(cgImage: image, scale: scale, refImage: base) + } + + // MARK: Blur + + /// Create an image with a blur effect based on the `base` image. + /// + /// - Parameter radius: The blur radius to be used when creating the blur effect. + /// - Returns: An image with the blur effect applied. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + public func blurred(withRadius radius: CGFloat) -> KFCrossPlatformImage { + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Blur only works for CG-based image.") + return base + } + + // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement + // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) + // if d is odd, use three box-blurs of size 'd', centered on the output pixel. + let s = max(radius, 2.0) + + // We will do blur on a resized image (*0.5), so the blur radius could be half as well. + // Fix the slow compiling time for Swift 3. + // See https://github.com/onevcat/Kingfisher/issues/611 + let pi2 = 2 * CGFloat.pi + let sqrtPi2 = sqrt(pi2) + var targetRadius = floor(s * 3.0 * sqrtPi2 / 4.0 + 0.5) + + if targetRadius.isEven { targetRadius += 1 } + + // Determine necessary iteration count by blur radius. + let iterations: Int + if radius < 0.5 { + iterations = 1 + } else if radius < 1.5 { + iterations = 2 + } else { + iterations = 3 + } + + func createEffectBuffer(_ context: CGContext) -> vImage_Buffer { + let data = context.data + let width = vImagePixelCount(context.width) + let height = vImagePixelCount(context.height) + let rowBytes = context.bytesPerRow + + return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes) + } + + guard let inputContext = CGContext.fresh(cgImage: cgImage) else { + return base + } + inputContext.draw( + cgImage, + in: CGRect( + x: 0, + y: 0, + width: size.width * scale, + height: size.height * scale + ) + ) + var inBuffer = createEffectBuffer(inputContext) + + guard let outContext = CGContext.fresh(cgImage: cgImage) else { + return base + } + var outBuffer = createEffectBuffer(outContext) + + for _ in 0 ..< iterations { + let flag = vImage_Flags(kvImageEdgeExtend) + vImageBoxConvolve_ARGB8888( + &inBuffer, &outBuffer, nil, 0, 0, UInt32(targetRadius), UInt32(targetRadius), nil, flag) + // Next inBuffer should be the outButter of current iteration + (inBuffer, outBuffer) = (outBuffer, inBuffer) + } + + #if os(macOS) + let result = outContext.makeImage().flatMap { + fixedForRetinaPixel(cgImage: $0, to: size) + } + #else + let result = outContext.makeImage().flatMap { + KFCrossPlatformImage(cgImage: $0, scale: base.scale, orientation: base.imageOrientation) + } + #endif + guard let blurredImage = result else { + assertionFailure("[Kingfisher] Can not make an blurred image within this context.") + return base + } + return blurredImage + } + + public func addingBorder(_ border: Border) -> KFCrossPlatformImage + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size, inverting: false) { context in + + #if os(macOS) + base.draw(in: rect) + #else + base.draw(in: rect, blendMode: .normal, alpha: 1.0) + #endif + + + let strokeRect = rect.insetBy(dx: border.lineWidth / 2, dy: border.lineWidth / 2) + context.setStrokeColor(border.color.cgColor) + context.setAlpha(border.color.rgba.a) + + let line = pathForRoundCorner( + rect: strokeRect, + radius: border.radius, + corners: border.roundingCorners, + offsetBase: border.lineWidth + ) + line.lineCapStyle = .square + line.lineWidth = border.lineWidth + line.stroke() + + return false + } + } + + // MARK: Overlay + + /// Create an image from the `base` image with a color overlay layer. + /// + /// - Parameters: + /// - color: The color to be used for the overlay. + /// - fraction: The fraction of the input color to apply, ranging from 0.0 (solid color) to 1.0 (transparent overlay). + /// - Returns: An image with a color overlay applied. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + public func overlaying(with color: KFCrossPlatformColor, fraction: CGFloat) -> KFCrossPlatformImage { + + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Overlaying only works for CG-based image.") + return base + } + + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + return draw(to: rect.size, inverting: false) { context in + #if os(macOS) + base.draw(in: rect) + if fraction > 0 { + color.withAlphaComponent(1 - fraction).set() + rect.fill(using: .sourceAtop) + } + #else + color.set() + UIRectFill(rect) + base.draw(in: rect, blendMode: .destinationIn, alpha: 1.0) + + if fraction > 0 { + base.draw(in: rect, blendMode: .sourceAtop, alpha: fraction) + } + #endif + return false + } + } + + // MARK: Tint + + /// Create an image from the `base` image with a color tint. + /// + /// - Parameter color: The color to be used for tinting the `base` image. + /// - Returns: An image with a color tint applied. + /// + /// > Important: This method does not work on watchOS, where the original image is returned. + public func tinted(with color: KFCrossPlatformColor) -> KFCrossPlatformImage { +#if os(watchOS) + return base +#else + return apply(.tint(color)) +#endif + } + + // MARK: Color Control + + /// Create an image from `self` with color control adjustments. + /// + /// - Parameters: + /// - brightness: The degree of brightness adjustment to apply to the image. + /// - contrast: The degree of contrast adjustment to apply to the image. + /// - saturation: The degree of saturation adjustment to apply to the image. + /// - inputEV: The exposure value (EV) adjustment to apply to the image. + /// - Returns: An image with color control adjustments applied. + /// + /// > Important: This method does not work on watchOS, where the original image is returned. + public func adjusted(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) -> KFCrossPlatformImage { +#if os(watchOS) + return base +#else + let colorElement = Filter.ColorElement( + brightness: brightness, + contrast: contrast, + saturation: saturation, + inputEV: inputEV + ) + return apply(.colorControl(colorElement)) +#endif + } + + /// Return an image with the specified scale. + /// + /// - Parameter scale: The target scale factor for the new image. + /// - Returns: The image with the target scale. If the base image is already at the target scale, the `base` image + /// will be returned. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image, the `base` image itself is returned. + public func scaled(to scale: CGFloat) -> KFCrossPlatformImage { + guard scale != self.scale else { + return base + } + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Scaling only works for CG-based image.") + return base + } + return KingfisherWrapper.image(cgImage: cgImage, scale: scale, refImage: base) + } +} + +// MARK: - Decoding Image +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// Returns the decoded image of the `base` image. + /// + /// On iOS 15 or later, this is identical to the `UIImage.preparingForDisplay` method. + /// + /// In previous versions, this method draws the image in a plain context and returns the data from it. Using this + /// method can improve drawing performance when an image is created from data but hasn't been displayed for the + /// first time. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image or animated image, the `base` image itself is returned. + public var decoded: KFCrossPlatformImage { return decoded(scale: scale) } + + /// Returns the decoded image of the `base` image at a given `scale`. + /// + /// On iOS 15 or later, this is identical to the `UIImage.preparingForDisplay` method. + /// + /// In previous versions, this method draws the image in a plain context and returns the data from it. Using this + /// method can improve drawing performance when an image is created from data but hasn't been displayed for the + /// first time. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image or animated image, the `base` image itself is returned. + public func decoded(scale: CGFloat) -> KFCrossPlatformImage { + + // Prevent animated image (GIF) losing it's images + #if os(iOS) || os(visionOS) + if frameSource != nil { return base } + #else + if images != nil { return base } + #endif + + // For older system versions, revert to the drawing for decoding. + guard let imageRef = cgImage else { + assertionFailure("[Kingfisher] Decoding only works for CG-based image.") + return base + } + + #if !os(watchOS) && !os(macOS) + // In newer system versions, use `preparingForDisplay`. + if #available(iOS 15.0, tvOS 15.0, visionOS 1.0, *) { + if base.scale == scale, let image = base.preparingForDisplay() { + return image + } + let scaledImage = KFCrossPlatformImage(cgImage: imageRef, scale: scale, orientation: base.imageOrientation) + if let image = scaledImage.preparingForDisplay() { + return image + } + } + #endif + + let size = CGSize(width: CGFloat(imageRef.width) / scale, height: CGFloat(imageRef.height) / scale) + return draw(to: size, inverting: true, scale: scale) { context in + context.draw(imageRef, in: CGRect(origin: .zero, size: size)) + return true + } + } + + /// Returns the decoded image of the `base` image on a given `context`. + /// + /// This method draws the image in the given context and returns the data from it. Using this + /// method can improve drawing performance when an image is created from data but hasn't been displayed for the + /// first time. + /// + /// > This method is only applicable to CG-based images. The current image scale is preserved. + /// > For any non-CG-based image or animated image, the `base` image itself is returned. + public func decoded(on context: CGContext) -> KFCrossPlatformImage { + // Prevent animated image (GIF) losing it's images + if frameSource != nil { return base } + + guard let refImage = cgImage, + let decodedRefImage = refImage.decoded(on: context, scale: scale) else + { + assertionFailure("[Kingfisher] Decoding only works for CG-based image.") + return base + } + return KingfisherWrapper.image(cgImage: decodedRefImage, scale: scale, refImage: base) + } +} + +extension CGImage { + func decoded(on context: CGContext, scale: CGFloat) -> CGImage? { + let size = CGSize(width: CGFloat(self.width) / scale, height: CGFloat(self.height) / scale) + context.draw(self, in: CGRect(origin: .zero, size: size)) + guard let decodedImageRef = context.makeImage() else { + return nil + } + return decodedImageRef + } + + static func create(ref: CGImage) -> CGImage? { + guard let space = ref.colorSpace, let provider = ref.dataProvider else { + return nil + } + return CGImage( + width: ref.width, + height: ref.height, + bitsPerComponent: ref.bitsPerComponent, + bitsPerPixel: ref.bitsPerPixel, + bytesPerRow: ref.bytesPerRow, + space: space, + bitmapInfo: ref.bitmapInfo, + provider: provider, + decode: ref.decode, + shouldInterpolate: ref.shouldInterpolate, + intent: ref.renderingIntent + ) + } +} + +extension KingfisherWrapper where Base: KFCrossPlatformImage { + func draw( + to size: CGSize, + inverting: Bool, + scale: CGFloat? = nil, + refImage: KFCrossPlatformImage? = nil, + draw: (CGContext) -> Bool // Whether use the refImage (`true`) or ignore image orientation (`false`) + ) -> KFCrossPlatformImage + { + #if os(macOS) || os(watchOS) + let targetScale = scale ?? self.scale + GraphicsContext.begin(size: size, scale: targetScale) + guard let context = GraphicsContext.current(size: size, scale: targetScale, inverting: inverting, cgImage: cgImage) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + defer { GraphicsContext.end() } + let useRefImage = draw(context) + guard let cgImage = context.makeImage() else { + return base + } + let ref = useRefImage ? (refImage ?? base) : nil + return KingfisherWrapper.image(cgImage: cgImage, scale: targetScale, refImage: ref) + #else + + let format = UIGraphicsImageRendererFormat.preferred() + format.scale = scale ?? self.scale + let renderer = UIGraphicsImageRenderer(size: size, format: format) + + var useRefImage: Bool = false + let image = renderer.image { rendererContext in + + let context = rendererContext.cgContext + if inverting { // If drawing a CGImage, we need to make context flipped. + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: 0, y: -size.height) + } + + useRefImage = draw(context) + } + if useRefImage { + guard let cgImage = image.cgImage else { + return base + } + let ref = refImage ?? base + return KingfisherWrapper.image(cgImage: cgImage, scale: format.scale, refImage: ref) + } else { + return image + } + #endif + } + + #if os(macOS) + func fixedForRetinaPixel(cgImage: CGImage, to size: CGSize) -> KFCrossPlatformImage { + + let image = KFCrossPlatformImage(cgImage: cgImage, size: base.size) + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + + return draw(to: self.size, inverting: false) { context in + image.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + return false + } + } + #endif +} + +extension CGContext { + fileprivate static func fresh(cgImage: CGImage) -> CGContext? { + CGContext( + data: nil, + width: cgImage.width, + height: cgImage.height, + bitsPerComponent: 8, + bytesPerRow: 4 * cgImage.width, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageFormat.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageFormat.swift new file mode 100644 index 00000000..104243fb --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageFormat.swift @@ -0,0 +1,127 @@ +// +// ImageFormat.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +/// Represents the image format. +public enum ImageFormat: Sendable { + /// The format cannot be recognized or not supported yet. + case unknown + /// PNG image format. + case PNG + /// JPEG image format. + case JPEG + /// GIF image format. + case GIF + + struct HeaderData { + static let PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] + static let JPEG_SOI: [UInt8] = [0xFF, 0xD8] + static let JPEG_IF: [UInt8] = [0xFF] + static let GIF: [UInt8] = [0x47, 0x49, 0x46] + } + + /// JPEG marker of each sequence of segments. + /// + /// See also [here](https://www.digicamsoft.com/itu/itu-t81-36.html). + public enum JPEGMarker { + case SOF0 //baseline + case SOF2 //progressive + case DHT //Huffman Table + case DQT //Quantization Table + case DRI //Restart Interval + case SOS //Start Of Scan + case RSTn(UInt8) //Restart + case APPn //Application-specific + case COM //Comment + case EOI //End Of Image + + var bytes: [UInt8] { + switch self { + case .SOF0: return [0xFF, 0xC0] + case .SOF2: return [0xFF, 0xC2] + case .DHT: return [0xFF, 0xC4] + case .DQT: return [0xFF, 0xDB] + case .DRI: return [0xFF, 0xDD] + case .SOS: return [0xFF, 0xDA] + case .RSTn(let n): return [0xFF, 0xD0 + n] + case .APPn: return [0xFF, 0xE0] + case .COM: return [0xFF, 0xFE] + case .EOI: return [0xFF, 0xD9] + } + } + } +} + + +extension Data: KingfisherCompatibleValue {} + +// MARK: - Misc Helpers +extension KingfisherWrapper where Base == Data { + /// Gets the image format corresponding to the data. + public var imageFormat: ImageFormat { + guard base.count > 8 else { return .unknown } + + var buffer = [UInt8](repeating: 0, count: 8) + base.copyBytes(to: &buffer, count: 8) + + if buffer == ImageFormat.HeaderData.PNG { + return .PNG + + } else if buffer[0] == ImageFormat.HeaderData.JPEG_SOI[0], + buffer[1] == ImageFormat.HeaderData.JPEG_SOI[1], + buffer[2] == ImageFormat.HeaderData.JPEG_IF[0] + { + return .JPEG + + } else if buffer[0] == ImageFormat.HeaderData.GIF[0], + buffer[1] == ImageFormat.HeaderData.GIF[1], + buffer[2] == ImageFormat.HeaderData.GIF[2] + { + return .GIF + } + + return .unknown + } + + public func contains(jpeg marker: ImageFormat.JPEGMarker) -> Bool { + guard imageFormat == .JPEG else { + return false + } + + let bytes = [UInt8](base) + let markerBytes = marker.bytes + for (index, item) in bytes.enumerated() where bytes.count > index + 1 { + guard + item == markerBytes.first, + bytes[index + 1] == markerBytes[1] else { + continue + } + return true + } + return false + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageProcessor.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageProcessor.swift new file mode 100644 index 00000000..fd033b88 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageProcessor.swift @@ -0,0 +1,874 @@ +// +// ImageProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/26. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +import CoreGraphics + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#else +import UIKit +#endif + +/// Represents an item which could be processed by an `ImageProcessor`. +public enum ImageProcessItem: Sendable { + + /// Input image. The processor should provide a method to apply + /// processing to this `image` and return the resulting image. + case image(KFCrossPlatformImage) + + /// Input data. The processor should provide a method to apply + /// processing to this `data` and return the resulting image. + case data(Data) +} + +/// An `ImageProcessor` is used to convert downloaded data into an image. +public protocol ImageProcessor: Sendable { + + /// Identifier for the processor. + /// + /// This identifier is used to distinguish the processor when caching and retrieving an image. Ensure that + /// processors with the same properties or functionality share the same identifier so that processed images can be + /// retrieved with the correct key. + /// + /// > Important: Avoid using an empty string for a custom processor, as it is already reserved by the + /// > `DefaultImageProcessor`. It is recommended to use a reverse domain name notation string for your identifier. + var identifier: String { get } + + /// Process the input `ImageProcessItem` using this processor. + /// + /// - Parameters: + /// - item: The input item to be processed by `self`. + /// - options: The parsed options for processing the item. + /// - Returns: The processed image. + /// + /// You should return `nil` if processing fails when converting an input item to an image. If the processing + /// caller receives `nil`, an error will be reported, and the processing flow will stop. If processing flow is not + /// critical for your use case, and the input item is already an image (`.image` case), you can also choose to + /// return the input image itself to continue the processing pipeline. + /// + /// > Important: Most processors only support CG-based images. The watchOS is not supported for processors + /// > containing a filter, and the input image will be returned directly on watchOS. + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? +} + +extension ImageProcessor { + + /// Append an `ImageProcessor` to another. The identifier of the new `ImageProcessor` will + /// be `"\(self.identifier)|>\(another.identifier)"`. + /// + /// - Parameter another: An `ImageProcessor` to be appended to `self`. + /// - Returns: The new `ImageProcessor` that will process the image in the order of the two processors concatenated. + public func append(another: any ImageProcessor) -> any ImageProcessor { + let newIdentifier = identifier.appending("|>\(another.identifier)") + return GeneralProcessor(identifier: newIdentifier) { + item, options in + if let image = self.process(item: item, options: options) { + return another.process(item: .image(image), options: options) + } else { + return nil + } + } + } +} + +func ==(left: any ImageProcessor, right: any ImageProcessor) -> Bool { + return left.identifier == right.identifier +} + +func !=(left: any ImageProcessor, right: any ImageProcessor) -> Bool { + return !(left == right) +} + +typealias ProcessorImp = (@Sendable (ImageProcessItem, KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?) +struct GeneralProcessor: ImageProcessor { + let identifier: String + let p: ProcessorImp + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return p(item, options) + } +} + +/// The default processor. It converts the input data into a valid image. +/// +/// Supported image formats include .PNG, .JPEG, and .GIF. If an image item is provided as the +/// ``ImageProcessItem/image(_:)`` case, ``DefaultImageProcessor`` will leave it unchanged and return the associated +/// image. +public struct DefaultImageProcessor: ImageProcessor { + + /// A default instance of ``DefaultImageProcessor`` can be used across the framework. + public static let `default` = DefaultImageProcessor() + + public let identifier = "" + + /// Create a ``DefaultImageProcessor``. + /// + /// Use ``DefaultImageProcessor/default`` to obtain an instance if you have no specific reason to create your own + /// ``DefaultImageProcessor``. + public init() {} + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + case .data(let data): + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } + } +} + +/// Represents the rect corner setting when processing a round corner image. +public struct RectCorner: OptionSet, Sendable { + + /// Raw value for the corner radius. + public let rawValue: Int + + /// Represents the top left corner. + public static let topLeft = RectCorner(rawValue: 1 << 0) + + /// Represents the top right corner. + public static let topRight = RectCorner(rawValue: 1 << 1) + + /// Represents the bottom left corner. + public static let bottomLeft = RectCorner(rawValue: 1 << 2) + + /// Represents the bottom right corner. + public static let bottomRight = RectCorner(rawValue: 1 << 3) + + /// Represents all corners. + public static let all: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight] + + /// Create a `RectCorner` option set with a specified value. + /// + /// - Parameter rawValue: The value representing a specific corner option. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + var cornerIdentifier: String { + if self == .all { + return "" + } + return "_corner(\(rawValue))" + } +} + +#if !os(macOS) +/// Processor for applying a blend mode to images. +/// +/// Supported for CG-based images only. +public struct BlendImageProcessor: ImageProcessor { + + public let identifier: String + + /// The blend mode used to blend the input image. + public let blendMode: CGBlendMode + + /// The alpha value used when blending the image. + public let alpha: CGFloat + + /// The background color of the output image. + /// + /// If `nil`, the background will remain transparent. + public let backgroundColor: KFCrossPlatformColor? + + /// Create a `BlendImageProcessor`. + /// + /// - Parameters: + /// - blendMode: The blend mode to be used for blending the input image. + /// - alpha: The alpha value to be used when blending the image, ranging from 0.0 (completely transparent) to + /// 1.0 (completely solid). Default is 1.0. + /// - backgroundColor: The background color to apply to the output image. Default is `nil`. + public init(blendMode: CGBlendMode, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) { + self.blendMode = blendMode + self.alpha = alpha + self.backgroundColor = backgroundColor + var identifier = "com.onevcat.Kingfisher.BlendImageProcessor(\(blendMode.rawValue),\(alpha))" + if let color = backgroundColor { + identifier.append("_\(color.rgbaDescription)") + } + self.identifier = identifier + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.image(withBlendMode: blendMode, alpha: alpha, backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} +#endif + +#if os(macOS) +/// Processor for applying a compositing operation to images. +/// +/// Supported for CG-based images on macOS. +public struct CompositingImageProcessor: ImageProcessor { + + public let identifier: String + + /// The compositing operation applied to the input image. + public let compositingOperation: NSCompositingOperation + + /// The alpha value used when compositing the image. + public let alpha: CGFloat + + /// The background color of the output image. If `nil`, the background will remain transparent. + public let backgroundColor: KFCrossPlatformColor? + + /// Create a `CompositingImageProcessor`. + /// + /// - Parameters: + /// - compositingOperation: The compositing operation to be applied to the input image. + /// - alpha: The alpha value to be used when compositing the image, ranging from 0.0 (completely transparent) to + /// 1.0 (completely solid). Default is 1.0. + /// - backgroundColor: The background color to apply to the output image. Default is `nil`. + public init(compositingOperation: NSCompositingOperation, + alpha: CGFloat = 1.0, + backgroundColor: KFCrossPlatformColor? = nil) + { + self.compositingOperation = compositingOperation + self.alpha = alpha + self.backgroundColor = backgroundColor + var identifier = "com.onevcat.Kingfisher.CompositingImageProcessor(\(compositingOperation.rawValue),\(alpha))" + if let color = backgroundColor { + identifier.append("_\(color.rgbaDescription)") + } + self.identifier = identifier + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.image( + withCompositingOperation: compositingOperation, + alpha: alpha, + backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} +#endif + +/// Represents a radius specified in a ``RoundCornerImageProcessor``. +public enum Radius: Sendable { + + /// The radius should be calculated as a fraction of the image width. Typically, the associated value should be + /// between 0 and 0.5, where 0 represents no radius, and 0.5 represents using half of the image width. + case widthFraction(CGFloat) + + /// The radius should be calculated as a fraction of the image height. Typically, the associated value should be + /// between 0 and 0.5, where 0 represents no radius, and 0.5 represents using half of the image height. + case heightFraction(CGFloat) + + /// Use a fixed point value as the round corner radius. + case point(CGFloat) + + var radiusIdentifier: String { + switch self { + case .widthFraction(let f): + return "w_frac_\(f)" + case .heightFraction(let f): + return "h_frac_\(f)" + case .point(let p): + return p.description + } + } + + public func compute(with size: CGSize) -> CGFloat { + let cornerRadius: CGFloat + switch self { + case .point(let point): + cornerRadius = point + case .widthFraction(let widthFraction): + cornerRadius = size.width * widthFraction + case .heightFraction(let heightFraction): + cornerRadius = size.height * heightFraction + } + return cornerRadius + } +} + +/// Processor for creating round corner images. +/// +/// Supported for CG-based images on macOS. If a non-CG image is passed in, the processor will have no effect. +/// +/// > Tip: The input image will be rendered with round corner pixels removed. If the image itself does not contain an +/// > alpha channel (for example, a JPEG image), the processed image will contain an alpha channel in memory for +/// > correct rendering. However, when cached to disk, Kingfisher defaults to preserving the original image format. +/// > This means the alpha channel will be removed for these images. If you load the processed image from the cache +/// > again, you will lose the transparent corners. +/// > +/// > You can use ``FormatIndicatedCacheSerializer/png`` to force Kingfisher to serialize the image to PNG format in this +/// > case. + +public struct RoundCornerImageProcessor: ImageProcessor { + + public let identifier: String + + /// The radius to be applied during processing. + /// + /// Specify a specific point value with ``Radius/point(_:)``, or a fraction of the target image with + /// ``Radius/widthFraction(_:)`` or ``Radius/heightFraction(_:)``. For example, if you have a square image with + /// equal width and height, `.widthFraction(0.5)` means using half of the width of the size to create a round image. + public let radius: Radius + + /// The target corners to round. + public let roundingCorners: RectCorner + + /// The target size for the output image. If `nil`, the image will retain its original size after processing. + public let targetSize: CGSize? + + /// The background color for the output image. If `nil`, it will use a transparent background. + public let backgroundColor: KFCrossPlatformColor? + + /// Create a ``RoundCornerImageProcessor`` with given parameters. + /// + /// - Parameters: + /// - cornerRadius: The corner radius in points to be applied during processing. + /// - targetSize: The target size for the output image. If `nil`, the image will retain its original size after + /// processing. Default is `nil`. + /// - corners: The target corners to round. Default is ``RectCorner/all``. + /// - backgroundColor: The background color to apply to the output image. Default is `nil`. + /// + /// This initializer accepts a specific point value for `cornerRadius`. If you don't know the image size but still + /// want to apply a full round corner (making the final image round), or specify the corner radius as a fraction of + /// one dimension of the target image, use the ``init(radius:targetSize:roundingCorners:backgroundColor:)`` + /// instead. + public init( + cornerRadius: CGFloat, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) + { + let radius = Radius.point(cornerRadius) + self.init(radius: radius, targetSize: targetSize, roundingCorners: corners, backgroundColor: backgroundColor) + } + + /// Create a `RoundCornerImageProcessor`. + /// + /// - Parameters: + /// - radius: The radius to be applied during processing. + /// - targetSize: The target size for the output image. If `nil`, the image will retain its original size after + /// processing. Default is `nil`. + /// - corners: The target corners to round. Default is ``RectCorner/all``. + /// - backgroundColor: The background color to apply to the output image. Default is `nil`. + public init( + radius: Radius, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) + { + self.radius = radius + self.targetSize = targetSize + self.roundingCorners = corners + self.backgroundColor = backgroundColor + + self.identifier = { + var identifier = "" + + if let size = targetSize { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(radius.radiusIdentifier)_\(size)\(corners.cornerIdentifier))" + } else { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(radius.radiusIdentifier)\(corners.cornerIdentifier))" + } + if let backgroundColor = backgroundColor { + identifier += "_\(backgroundColor)" + } + + return identifier + }() + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + let size = targetSize ?? image.kf.size + return image.kf.scaled(to: options.scaleFactor) + .kf.image( + withRadius: radius, + fit: size, + roundingCorners: roundingCorners, + backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Represents a border to be added to the image. +/// +/// Typically used with ``BorderImageProcessor``, which adds the border to the image. +public struct Border: Sendable { + + /// The color of the border to create. + public var color: KFCrossPlatformColor + + /// The line width of the border to create. + public var lineWidth: CGFloat + + /// The radius to be applied during processing. + /// + /// Specify a specific point value with ``Radius/point(_:)``, or a fraction of the target image with + /// ``Radius/widthFraction(_:)`` or ``Radius/heightFraction(_:)``. For example, if you have a square image with + /// equal width and height, `.widthFraction(0.5)` means using half of the width of the size to create a round image. + public var radius: Radius + + /// The target corners which will be applied rounding. + public var roundingCorners: RectCorner + + /// Creates a border. + /// - Parameters: + /// - color: The color will be used to render the border. + /// - lineWidth: The line width of the border. + /// - radius: The radius of the border corner. + /// - roundingCorners: The target corners type. + public init( + color: KFCrossPlatformColor = .black, + lineWidth: CGFloat = 4, + radius: Radius = .point(0), + roundingCorners: RectCorner = .all + ) { + self.color = color + self.lineWidth = lineWidth + self.radius = radius + self.roundingCorners = roundingCorners + } + + var identifier: String { + "\(color.rgbaDescription)_\(lineWidth)_\(radius.radiusIdentifier)_\(roundingCorners.cornerIdentifier)" + } +} + +/// Processor for creating bordered images. +public struct BorderImageProcessor: ImageProcessor { + + public var identifier: String { "com.onevcat.Kingfisher.RoundCornerImageProcessor(\(border)" } + + /// The border to be added to the image. + public let border: Border + + /// Create a `BorderImageProcessor` with a given `Border`. + /// + /// - Parameter border: The border to be added to the image. + public init(border: Border) { + self.border = border + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.addingBorder(border) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Represents how a size of content adjusts itself to fit a target size. +public enum ContentMode: Sendable { + /// Does not scale the content. + case none + /// Scales the content to fit the size of the view while maintaining the aspect ratio. + case aspectFit + /// Scales the content to fill the size of the view. + case aspectFill +} + +/// Processor for resizing images. +/// +/// If you need to resize an image represented by data to a smaller size, use ``DownsamplingImageProcessor`` instead, +/// which is more efficient and uses less memory. +public struct ResizingImageProcessor: ImageProcessor { + + public let identifier: String + + /// The reference size for the resizing operation in points. + public let referenceSize: CGSize + + /// The target content mode of the output image. + public let targetContentMode: ContentMode + + /// Create a ``ResizingImageProcessor``. + /// + /// - Parameters: + /// - referenceSize: The reference size for the resizing operation in points. + /// - mode: The target content mode of the output image. + /// + /// The instance of ``ResizingImageProcessor`` will follow the `mode` argument and attempt to resize the input + /// images to fit or fill the `referenceSize`. This means if you are using a `mode` besides `.none`, you may get an + /// image with a size that is not the same as the `referenceSize`. + /// + /// For example, with an input image size of {100, 200}, `referenceSize` of {100, 100}, and `mode` of `.aspectFit`, + /// you will get an output image with a size of {50, 100} that "fits" the `referenceSize`. + /// + /// > If you need an output image to be exactly a specified size, append or use a ``CroppingImageProcessor``. + public init(referenceSize: CGSize, mode: ContentMode = .none) { + self.referenceSize = referenceSize + self.targetContentMode = mode + + if mode == .none { + self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize))" + } else { + self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize), \(mode))" + } + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.resize(to: referenceSize, for: targetContentMode) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for adding a blur effect to images. +/// +/// Uses `Accelerate.framework` under the hood for better performance. Applies a simulated Gaussian blur with the +/// specified blur radius. +public struct BlurImageProcessor: ImageProcessor { + + public let identifier: String + + /// The blur radius for the simulated Gaussian blur. + public let blurRadius: CGFloat + + /// Create a `BlurImageProcessor`. + /// + /// - Parameter blurRadius: The blur radius for the simulated Gaussian blur. + public init(blurRadius: CGFloat) { + self.blurRadius = blurRadius + self.identifier = "com.onevcat.Kingfisher.BlurImageProcessor(\(blurRadius))" + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + let radius = blurRadius * options.scaleFactor + return image.kf.scaled(to: options.scaleFactor) + .kf.blurred(withRadius: radius) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for adding an overlay to images. +/// +/// > Only CG-based images are supported. +public struct OverlayImageProcessor: ImageProcessor { + + public let identifier: String + + /// The overlay color used to overlay the input image. + public let overlay: KFCrossPlatformColor + + /// The fraction used when overlaying the color to the image. + public let fraction: CGFloat + + /// Create an ``OverlayImageProcessor``. + /// + /// - Parameters: + /// - overlay: The overlay color used to overlay the input image. + /// - fraction: The fraction used when overlaying the color to the image. + /// Ranges from 0.0 to 1.0. 0.0 means a solid color, and 1.0 means a transparent overlay. + public init(overlay: KFCrossPlatformColor, fraction: CGFloat = 0.5) { + self.overlay = overlay + self.fraction = fraction + self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.rgbaDescription)_\(fraction))" + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.overlaying(with: overlay, fraction: fraction) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for tinting images with color. +/// +/// > Only CG-based images are supported. +/// +/// > Important: On watchOS, there is no tint support and the original image will be returned. +public struct TintImageProcessor: ImageProcessor { + + public let identifier: String + + /// The tint color used to tint the input image. + public let tint: KFCrossPlatformColor + + /// Create a ``TintImageProcessor``. + /// + /// - Parameter tint: The tint color used to tint the input image. + public init(tint: KFCrossPlatformColor) { + self.tint = tint + self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.rgbaDescription))" + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.tinted(with: tint) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for applying color control to images. +/// +/// > Only CG-based images are supported. +/// +/// > Important: On watchOS, there is no color control support and the original image will be returned. +public struct ColorControlsProcessor: ImageProcessor { + + public let identifier: String + + /// The brightness change applied to the image. + public let brightness: CGFloat + + /// The contrast change applied to the image. + public let contrast: CGFloat + + /// The saturation change applied to the image. + public let saturation: CGFloat + + /// The EV (F-stops brighter or darker) change applied to the image. + public let inputEV: CGFloat + + /// Create a ``ColorControlsProcessor``. + /// + /// - Parameters: + /// - brightness: The brightness change applied to the image. + /// - contrast: The contrast change applied to the image. + /// - saturation: The saturation change applied to the image. + /// - inputEV: The EV (F-stops brighter or darker) change applied to the image. + public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) { + self.brightness = brightness + self.contrast = contrast + self.saturation = saturation + self.inputEV = inputEV + self.identifier = "com.onevcat.Kingfisher.ColorControlsProcessor(\(brightness)_\(contrast)_\(saturation)_\(inputEV))" + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.adjusted(brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for applying black and white effect to images. Only CG-based images are supported. +/// +/// > Only CG-based images are supported. +/// +/// > Important: On watchOS, there is no color control support and the original image will be returned. +public struct BlackWhiteProcessor: ImageProcessor { + + public let identifier = "com.onevcat.Kingfisher.BlackWhiteProcessor" + + /// Creates a ``BlackWhiteProcessor`` + public init() {} + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7) + .process(item: item, options: options) + } +} + +/// Processor for cropping an image. +public struct CroppingImageProcessor: ImageProcessor { + + public let identifier: String + + /// The target size of the output image. + public let size: CGSize + + /// Anchor point from which the output size should be calculate. + /// + /// The anchor point is consisted by two values between 0.0 and 1.0. + /// It indicates a related point in current image. + /// + /// See ``CroppingImageProcessor/init(size:anchor:)`` for more. + public let anchor: CGPoint + + /// Create a ``CroppingImageProcessor``. + /// + /// - Parameters: + /// - size: The target size of the output image. + /// - anchor: The anchor point from which the size should be calculated. Default is `CGPoint(x: 0.5, y: 0.5)`, + /// which represents the center of the input image. + /// + /// The anchor point is composed of two values between 0.0 and 1.0. It indicates a relative point in the current + /// image, e.g: + /// - (0.0, 0.0) for the top-left corner + /// - (0.5, 0.5) for the center + /// - (1.0, 1.0) for the bottom-right corner + /// + /// The ``CroppingImageProcessor/size`` property will be used along with ``CroppingImageProcessor/anchor`` to + /// calculate a target rectangle in the image size. + /// + /// > The target size will be automatically calculated with a reasonable behavior. For example, when you have an + /// > image size of `CGSize(width: 100, height: 100)` and a target size of `CGSize(width: 20, height: 20)`: + /// > + /// > - with a (0.0, 0.0) anchor (top-left), the crop rect will be `{0, 0, 20, 20}`; + /// > - with a (0.5, 0.5) anchor (center), it will be `{40, 40, 20, 20}`; + /// > - while with a (1.0, 1.0) anchor (bottom-right), it will be `{80, 80, 20, 20}`. + public init(size: CGSize, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) { + self.size = size + self.anchor = anchor + self.identifier = "com.onevcat.Kingfisher.CroppingImageProcessor(\(size)_\(anchor))" + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.crop(to: size, anchorOn: anchor) + case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +/// Processor for downsampling an image. +/// +/// Compared to ``ResizingImageProcessor``, this processor does not render the images to resize. Instead, it +/// downsamples the input data directly to an image. It is more efficient than ``ResizingImageProcessor``. +/// +/// > Tip: It is preferable to use ``DownsamplingImageProcessor`` whenever possible rather than the +/// > ``ResizingImageProcessor``. +/// +/// > Important: Only CG-based images are supported. Animated images (such as GIFs) are not supported. +public struct DownsamplingImageProcessor: ImageProcessor { + + /// The target size of the output image. + /// + /// It should be smaller than the size of the input image. If it is larger, the resulting image will be the same + /// size as the input data without downsampling. + public let size: CGSize + + public let identifier: String + + /// Creates a `DownsamplingImageProcessor`. + /// + /// - Parameters: + /// - size: The target size of the downsampling operation. + /// + /// > Important: The size should be smaller than the size of the input image. If it is larger, the resulting image + /// will be the same size as the input data without downsampling. + public init(size: CGSize) { + self.size = size + self.identifier = "com.onevcat.Kingfisher.DownsamplingImageProcessor(\(size))" + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + guard let data = image.kf.data(format: .unknown) else { + return nil + } + return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) + case .data(let data): + return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) + } + } +} + +// This is an internal processor to provide the same interface for Live Photos. +// It is not intended to be open and used from external. +struct LivePhotoImageProcessor: ImageProcessor { + + public static let `default` = LivePhotoImageProcessor() + private init() { } + + public let identifier = "com.onevcat.Kingfisher.LivePhotoImageProcessor" + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image + case .data: + return KFCrossPlatformImage() + } + } +} + +infix operator |>: AdditionPrecedence + +/// Concatenates two `ImageProcessor`s to create a new one, in which the `left` and `right` are combined in order to +/// process the image. +/// +/// - Parameters: +/// - left: The first processor. +/// - right: The second processor that follows the `left`. +/// - Returns: The new processor that processes the image or the image data in left-to-right order. +public func |>(left: any ImageProcessor, right: any ImageProcessor) -> any ImageProcessor { + return left.append(another: right) +} + +extension KFCrossPlatformColor { + + var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + #if os(macOS) + (usingColorSpace(.extendedSRGB) ?? self).getRed(&r, green: &g, blue: &b, alpha: &a) + #else + getRed(&r, green: &g, blue: &b, alpha: &a) + #endif + + return (r, g, b, a) + } + + var rgbaDescription: String { + let components = self.rgba + return String(format: "(%.2f,%.2f,%.2f,%.2f)", components.r, components.g, components.b, components.a) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageProgressive.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageProgressive.swift new file mode 100644 index 00000000..f2e1da13 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageProgressive.swift @@ -0,0 +1,373 @@ +// +// ImageProgressive.swift +// Kingfisher +// +// Created by lixiang on 2019/5/10. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +import CoreGraphics +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +private let sharedProcessingQueue: CallbackQueue = + .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) + +/// Represents a progressive loading for images which supports this feature. +public struct ImageProgressive: Sendable { + + /// The updating strategy when an intermediate progressive image is generated and about to be set to the hosting view. + public enum UpdatingStrategy { + + /// Use the progressive image as it is. + /// + /// > It is the standard behavior when handling the progressive image. + case `default` + + /// Discard this progressive image and keep the current displayed one. + case keepCurrent + + /// Replace the image to a new one. + /// + /// If the progressive loading is initialized by a view extension in Kingfisher, the replacing image will be + /// used to update the view. + case replace(KFCrossPlatformImage?) + } + + /// A default `ImageProgressive` could be used across. It blurs the progressive loading with the fastest + /// scan enabled and scan interval as 0. + @available(*, deprecated, message: "Getting a default `ImageProgressive` is deprecated due to its syntax semantic is not clear. Use `ImageProgressive.init` instead.", renamed: "init()") + public static let `default` = ImageProgressive( + isBlur: true, + isFastestScan: true, + scanInterval: 0 + ) + + /// Indicates whether to enable blur effect processing. + public var isBlur: Bool + + /// Indicates whether to enable the fastest scan. + public var isFastestScan: Bool + + /// The minimum time interval for each scan. + public var scanInterval: TimeInterval + + /// Called when an intermediate image is prepared and about to be set to the image view. + /// + /// If implemented, you should return an ``UpdatingStrategy`` value from this delegate. This value will be used to + /// update the hosting view, if any. Otherwise, if there is no hosting view (i.e., the image retrieval is not + /// happening from a view extension method), the returned ``UpdatingStrategy`` is ignored. + public let onImageUpdated = Delegate() + + /// Creates an `ImageProgressive` value with default settings. + /// + /// It enables progressive loading with the fastest scan enabled and a scan interval of 0, resulting in a blurred + /// effect. + public init() { + self.init(isBlur: true, isFastestScan: true, scanInterval: 0) + } + + /// Creates an `ImageProgressive` value with the given values. + /// + /// - Parameters: + /// - isBlur: Indicates whether to enable blur effect processing. + /// - isFastestScan: Indicates whether to enable the fastest scan. + /// - scanInterval: The minimum time interval for each scan. + public init( + isBlur: Bool, + isFastestScan: Bool, + scanInterval: TimeInterval + ) + { + self.isBlur = isBlur + self.isFastestScan = isFastestScan + self.scanInterval = scanInterval + } +} + +// A data receiving provider to update the image. Working with an `ImageProgressive`, it helps to implement the image +// progressive effect. +final class ImageProgressiveProvider: DataReceivingSideEffect, @unchecked Sendable { + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageProgressiveProviderPropertyQueue") + + private var _onShouldApply: () -> Bool = { return true } + + var onShouldApply: () -> Bool { + get { propertyQueue.sync { _onShouldApply } } + set { propertyQueue.sync { _onShouldApply = newValue } } + } + + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { + + DispatchQueue.main.async { + guard self.onShouldApply() else { return } + self.update(data: task.mutableData, with: task.callbacks) + } + } + + private let progressive: ImageProgressive + private let refresh: (KFCrossPlatformImage) -> Void + + private let decoder: ImageProgressiveDecoder + private let queue = ImageProgressiveSerialQueue() + + init?( + options: KingfisherParsedOptionsInfo, + refresh: @escaping (KFCrossPlatformImage) -> Void + ) { + guard let progressive = options.progressiveJPEG else { return nil } + + self.progressive = progressive + self.refresh = refresh + self.decoder = ImageProgressiveDecoder( + progressive, + processingQueue: options.processingQueue ?? sharedProcessingQueue, + creatingOptions: options.imageCreatingOptions + ) + } + + func update(data: Data, with callbacks: [SessionDataTask.TaskCallback]) { + guard !data.isEmpty else { return } + queue.add(minimum: progressive.scanInterval) { completion in + + @Sendable func decode(_ data: Data) { + self.decoder.decode(data, with: callbacks) { image in + defer { completion() } + guard self.onShouldApply() else { return } + guard let image = image else { return } + self.refresh(image) + } + } + + Task { @MainActor in + let applyFlag = self.onShouldApply() + guard applyFlag else { + self.queue.clean() + completion() + return + } + + if self.progressive.isFastestScan { + decode(self.decoder.scanning(data) ?? Data()) + } else { + self.decoder.scanning(data).forEach { decode($0) } + } + } + } + } +} + +private final class ImageProgressiveDecoder: @unchecked Sendable { + + private let option: ImageProgressive + private let processingQueue: CallbackQueue + private let creatingOptions: ImageCreatingOptions + private(set) var scannedCount = 0 + private(set) var scannedIndex = -1 + + init(_ option: ImageProgressive, + processingQueue: CallbackQueue, + creatingOptions: ImageCreatingOptions) { + self.option = option + self.processingQueue = processingQueue + self.creatingOptions = creatingOptions + } + + func scanning(_ data: Data) -> [Data] { + guard data.kf.contains(jpeg: .SOF2) else { + return [] + } + guard scannedIndex + 1 < data.count else { + return [] + } + + var datas: [Data] = [] + var index = scannedIndex + 1 + var count = scannedCount + + while index < data.count - 1 { + scannedIndex = index + // 0xFF, 0xDA - Start Of Scan + let SOS = ImageFormat.JPEGMarker.SOS.bytes + if data[index] == SOS[0], data[index + 1] == SOS[1] { + if count > 0 { + datas.append(data[0 ..< index]) + } + count += 1 + } + index += 1 + } + + // Found more scans this the previous time + guard count > scannedCount else { return [] } + scannedCount = count + + // `> 1` checks that we've received a first scan (SOS) and then received + // and also received a second scan (SOS). This way we know that we have + // at least one full scan available. + guard count > 1 else { return [] } + return datas + } + + func scanning(_ data: Data) -> Data? { + guard data.kf.contains(jpeg: .SOF2) else { + return nil + } + guard scannedIndex + 1 < data.count else { + return nil + } + + var index = scannedIndex + 1 + var count = scannedCount + var lastSOSIndex = 0 + + while index < data.count - 1 { + scannedIndex = index + // 0xFF, 0xDA - Start Of Scan + let SOS = ImageFormat.JPEGMarker.SOS.bytes + if data[index] == SOS[0], data[index + 1] == SOS[1] { + lastSOSIndex = index + count += 1 + } + index += 1 + } + + // Found more scans this the previous time + guard count > scannedCount else { return nil } + scannedCount = count + + // `> 1` checks that we've received a first scan (SOS) and then received + // and also received a second scan (SOS). This way we know that we have + // at least one full scan available. + guard count > 1 && lastSOSIndex > 0 else { return nil } + return data[0 ..< lastSOSIndex] + } + + func decode(_ data: Data, + with callbacks: [SessionDataTask.TaskCallback], + completion: @escaping @Sendable (KFCrossPlatformImage?) -> Void) { + guard data.kf.contains(jpeg: .SOF2) else { + CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } + return + } + + @Sendable func processing(_ data: Data) { + let processor = ImageDataProcessor( + data: data, + callbacks: callbacks, + processingQueue: processingQueue + ) + processor.onImageProcessed.delegate(on: self) { (self, result) in + guard let image = try? result.0.get() else { + CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } + return + } + + CallbackQueue.mainCurrentOrAsync.execute { completion(image) } + } + processor.process() + } + + // Blur partial images. + let count = scannedCount + + if option.isBlur, count < 6 { + processingQueue.execute { + // Progressively reduce blur as we load more scans. + let image = KingfisherWrapper.image( + data: data, + options: self.creatingOptions + ) + let radius = max(2, 14 - count * 4) + let temp = image?.kf.blurred(withRadius: CGFloat(radius)) + processing(temp?.kf.data(format: .JPEG) ?? data) + } + + } else { + processing(data) + } + } +} + +private final class ImageProgressiveSerialQueue: @unchecked Sendable { + typealias ClosureCallback = @Sendable ((@escaping @Sendable () -> Void)) -> Void + + private let queue: DispatchQueue + private var items: [DispatchWorkItem] = [] + private var notify: (() -> Void)? + private var lastTime: TimeInterval? + + init() { + self.queue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageProgressive.SerialQueue") + } + + func add(minimum interval: TimeInterval, closure: @escaping ClosureCallback) { + let completion = { @Sendable [weak self] in + guard let self = self else { return } + + self.queue.async { [weak self] in + guard let self = self else { return } + guard !self.items.isEmpty else { return } + + self.items.removeFirst() + + if let next = self.items.first { + self.queue.asyncAfter( + deadline: .now() + interval, + execute: next + ) + + } else { + self.lastTime = Date().timeIntervalSince1970 + self.notify?() + self.notify = nil + } + } + } + + queue.async { [weak self] in + guard let self = self else { return } + + let item = DispatchWorkItem { + closure(completion) + } + if self.items.isEmpty { + let difference = Date().timeIntervalSince1970 - (self.lastTime ?? 0) + let delay = difference < interval ? interval - difference : 0 + self.queue.asyncAfter(deadline: .now() + delay, execute: item) + } + self.items.append(item) + } + } + + func clean() { + queue.async { [weak self] in + guard let self = self else { return } + self.items.forEach { $0.cancel() } + self.items.removeAll() + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageTransition.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageTransition.swift new file mode 100644 index 00000000..60f1c254 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/ImageTransition.swift @@ -0,0 +1,112 @@ +// +// ImageTransition.swift +// Kingfisher +// +// Created by Wei Wang on 15/9/18. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +#if os(iOS) || os(tvOS) || os(visionOS) +import UIKit + +/// Transition effect to be used when an image is downloaded and set using the `UIImageView` extension API in Kingfisher. +/// +/// You can assign an enum value with a transition duration as an item in `KingfisherOptionsInfo` to enable the animation +/// transition. Apple's `UIViewAnimationOptions` are used under the hood. +/// +/// For custom transitions, you should specify your own transition options, animations, and completion handler as well. +public enum ImageTransition: Sendable { + /// No animation transition. + case none + /// Fade effect to the loaded image over a specified duration. + case fade(TimeInterval) + /// Flip from left transition. + case flipFromLeft(TimeInterval) + /// Flip from right transition. + case flipFromRight(TimeInterval) + /// Flip from top transition. + case flipFromTop(TimeInterval) + /// Flip from bottom transition. + case flipFromBottom(TimeInterval) + /// Custom transition defined by a general animation block. + /// + /// - Parameters: + /// - duration: The duration of this custom transition. + /// - options: The `UIView.AnimationOptions` to use in the transition. + /// - animations: The animation block to apply when setting the image. + /// - completion: A block called when the transition animation finishes. + case custom(duration: TimeInterval, + options: UIView.AnimationOptions, + animations: (@Sendable @MainActor (UIImageView, UIImage) -> Void)?, + completion: (@Sendable (Bool) -> Void)?) + + var duration: TimeInterval { + switch self { + case .none: return 0 + case .fade(let duration): return duration + + case .flipFromLeft(let duration): return duration + case .flipFromRight(let duration): return duration + case .flipFromTop(let duration): return duration + case .flipFromBottom(let duration): return duration + + case .custom(let duration, _, _, _): return duration + } + } + + var animationOptions: UIView.AnimationOptions { + switch self { + case .none: return [] + case .fade: return .transitionCrossDissolve + + case .flipFromLeft: return .transitionFlipFromLeft + case .flipFromRight: return .transitionFlipFromRight + case .flipFromTop: return .transitionFlipFromTop + case .flipFromBottom: return .transitionFlipFromBottom + + case .custom(_, let options, _, _): return options + } + } + + @MainActor + var animations: ((UIImageView, UIImage) -> Void)? { + switch self { + case .custom(_, _, let animations, _): return animations + default: return { $0.image = $1 } + } + } + + var completion: ((Bool) -> Void)? { + switch self { + case .custom(_, _, _, let completion): return completion + default: return nil + } + } +} +#else +// Just a placeholder for compiling on macOS. +public enum ImageTransition: Sendable { + case none + /// This is a placeholder on macOS now. It is for SwiftUI (KFImage) to identify the fade option only. + case fade(TimeInterval) +} +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/Placeholder.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/Placeholder.swift new file mode 100644 index 00000000..f45862f5 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Image/Placeholder.swift @@ -0,0 +1,100 @@ +// +// Placeholder.swift +// Kingfisher +// +// Created by Tieme van Veen on 28/08/2017. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if !os(watchOS) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +#if canImport(UIKit) +import UIKit +#endif + +/// Represents a placeholder type that could be set during loading as well as when loading is finished without +/// getting an image. +public protocol Placeholder { + + /// Called when the placeholder needs to be added to a given image view. + /// + /// To conform to ``Placeholder``, you implement this method and add your own placeholder view to the + /// given `imageView`. + /// + /// - Parameter imageView: The image view where the placeholder should be added to. + @MainActor func add(to imageView: KFCrossPlatformImageView) + + /// Called when the placeholder needs to be removed from a given image view. + /// + /// To conform to ``Placeholder``, you implement this method and remove your own placeholder view from the + /// given `imageView`. + /// + /// - Parameter imageView: The image view where the placeholder is already added to and now should be removed from. + @MainActor func remove(from imageView: KFCrossPlatformImageView) +} + +@MainActor +extension KFCrossPlatformImage: Placeholder { + public func add(to imageView: KFCrossPlatformImageView) { + imageView.image = self + } + + public func remove(from imageView: KFCrossPlatformImageView) { + imageView.image = nil + } + + public func add(to base: any KingfisherHasImageComponent) { + base.image = self + } + + public func remove(from base: any KingfisherHasImageComponent) { + base.image = nil + } +} + +/// Default implementation of an arbitrary view as a placeholder. The view will be +/// added as a subview when adding and removed from its superview when removing. +/// +/// To use your customized View type as a placeholder, simply have it conform to +/// `Placeholder` using an extension: `extension MyView: Placeholder {}`. +@MainActor +extension Placeholder where Self: KFCrossPlatformView { + + public func add(to imageView: KFCrossPlatformImageView) { + imageView.addSubview(self) + translatesAutoresizingMaskIntoConstraints = false + + centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true + centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true + heightAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true + widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true + } + + public func remove(from imageView: KFCrossPlatformImageView) { + removeFromSuperview() + } +} + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift new file mode 100644 index 00000000..0ff4d1b3 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift @@ -0,0 +1,99 @@ +// +// AuthenticationChallengeResponsable.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +@available(*, deprecated, message: "Typo. Use `AuthenticationChallengeResponsible` instead", renamed: "AuthenticationChallengeResponsible") +public typealias AuthenticationChallengeResponsable = AuthenticationChallengeResponsible + +/// Protocol indicates that an authentication challenge could be handled. +public protocol AuthenticationChallengeResponsible: AnyObject { + + /// Called when a session level authentication challenge is received. + /// + /// This method provides a chance to handle and respond to the authentication challenge before the downloading can + /// start. + /// + /// - Parameters: + /// - downloader: The downloader that receives this challenge. + /// - challenge: An object that contains the request for authentication. + /// - Returns: The challenge disposition on how the challenge should be handled, and the credential if the + /// disposition is `.useCredential`. + /// + /// > This method is a forward from `URLSessionDelegate.urlSession(_:didReceive:completionHandler:)`. + /// > Please refer to the documentation of it in `URLSessionDelegate`. + func downloader( + _ downloader: ImageDownloader, + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) + + /// Called when a task level authentication challenge is received. + /// + /// This method provides a chance to handle and respond to the authentication challenge before the downloading can + /// start. + /// + /// - Parameters: + /// - downloader: The downloader that receives this challenge. + /// - task: The task whose request requires authentication. + /// - challenge: An object that contains the request for authentication. + /// - Returns: The challenge disposition on how the challenge should be handled, and the credential if the + /// disposition is `.useCredential`. + /// + /// > This method is a forward from `URLSessionDataDelegate.urlSession(_:dataTask:didReceive:completionHandler:)`. + /// > Please refer to the documentation of it in `URLSessionDataDelegate`. + func downloader( + _ downloader: ImageDownloader, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) +} + +extension AuthenticationChallengeResponsible { + + public func downloader( + _ downloader: ImageDownloader, + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) + { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) { + let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!) + return (.useCredential, credential) + } + } + + return (.performDefaultHandling, nil) + } + + public func downloader( + _ downloader: ImageDownloader, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + (.performDefaultHandling, nil) + } + +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift new file mode 100644 index 00000000..845e9513 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift @@ -0,0 +1,76 @@ +// +// ImageDataProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +private let sharedProcessingQueue: CallbackQueue = + .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) + +// Handles image processing work on an own process queue. +final class ImageDataProcessor: Sendable { + let data: Data + let callbacks: [SessionDataTask.TaskCallback] + let queue: CallbackQueue + + // Note: We have an optimization choice there, to reduce queue dispatch by checking callback + // queue settings in each option... + let onImageProcessed = Delegate<(Result, SessionDataTask.TaskCallback), Void>() + + init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) { + self.data = data + self.callbacks = callbacks + self.queue = processingQueue ?? sharedProcessingQueue + } + + func process() { + queue.execute { + self.doProcess() + } + } + + private func doProcess() { + var processedImages = [String: KFCrossPlatformImage]() + for callback in callbacks { + let processor = callback.options.processor + var image = processedImages[processor.identifier] + if image == nil { + image = processor.process(item: .data(data), options: callback.options) + processedImages[processor.identifier] = image + } + + let result: Result + if let image = image { + let finalImage = callback.options.backgroundDecode ? image.kf.decoded : image + result = .success(finalImage) + } else { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: .data(data))) + result = .failure(error) + } + onImageProcessed.call((result, callback)) + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDownloader+LivePhoto.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDownloader+LivePhoto.swift new file mode 100644 index 00000000..22ea082c --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDownloader+LivePhoto.swift @@ -0,0 +1,101 @@ +// +// ImageDownloader+LivePhoto.swift +// Kingfisher +// +// Created by onevcat on 2024/10/01. +// +// Copyright (c) 2024 Wei Wang +// +// 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. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +public struct LivePhotoResourceDownloadingResult: Sendable { + + /// The original URL of the image request. + public let url: URL? + + /// The raw data received from the downloader. + public let originalData: Data + + /// Creates an `ImageDownloadResult` object. + /// + /// - Parameters: + /// - url: The URL from which the image was downloaded. + /// - originalData: The binary data of the image. + public init(originalData: Data, url: URL? = nil) { + self.url = url + self.originalData = originalData + } +} + +extension ImageDownloader { + + public func downloadLivePhotoResource( + with url: URL, + options: KingfisherParsedOptionsInfo + ) async throws -> LivePhotoResourceDownloadingResult { + let task = CancellationDownloadTask() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + let downloadTask = downloadLivePhotoResource(with: url, options: options) { result in + continuation.resume(with: result) + } + if Task.isCancelled { + downloadTask.cancel() + } else { + Task { + await task.setTask(downloadTask) + } + } + } + } onCancel: { + Task { + await task.task?.cancel() + } + } + } + + @discardableResult + public func downloadLivePhotoResource( + with url: URL, + options: KingfisherParsedOptionsInfo, + completionHandler: (@Sendable (Result) -> Void)? = nil + ) -> DownloadTask { + var checkedOptions = options + if options.processor == DefaultImageProcessor.default { + // The default processor is a default behavior so we replace it silently. + checkedOptions.processor = LivePhotoImageProcessor.default + } else if options.processor != LivePhotoImageProcessor.default { + assertionFailure("[Kingfisher] Using of custom processors during loading of live photo resource is not supported.") + checkedOptions.processor = LivePhotoImageProcessor.default + } + return downloadImage(with: url, options: checkedOptions) { result in + guard let completionHandler else { + return + } + let newResult = result.map { LivePhotoResourceDownloadingResult(originalData: $0.originalData, url: $0.url) } + completionHandler(newResult) + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift new file mode 100644 index 00000000..ef1c2899 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift @@ -0,0 +1,671 @@ +// +// ImageDownloader.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +typealias DownloadResult = Result + +/// Represents a successful result of an image downloading process. +public struct ImageLoadingResult: Sendable { + + /// The downloaded image. + public let image: KFCrossPlatformImage + + /// The original URL of the image request. + public let url: URL? + + /// The raw data received from the downloader. + public let originalData: Data + + /// Creates an `ImageDownloadResult` object. + /// + /// - Parameters: + /// - image: The image of the download result. + /// - url: The URL from which the image was downloaded. + /// - originalData: The binary data of the image. + public init(image: KFCrossPlatformImage, url: URL? = nil, originalData: Data) { + self.image = image + self.url = url + self.originalData = originalData + } +} + +/// Represents a task in the image downloading process. +/// +/// When a download starts in Kingfisher, the involved methods always return you an instance of ``DownloadTask``. If you +/// need to cancel the task during the download process, you can keep a reference to the instance and call ``cancel()`` +/// on it. +public final class DownloadTask: @unchecked Sendable { + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.DownloadTaskPropertyQueue") + + init(sessionTask: SessionDataTask, cancelToken: SessionDataTask.CancelToken) { + _sessionTask = sessionTask + _cancelToken = cancelToken + } + + init() { } + + private var _sessionTask: SessionDataTask? = nil + + /// The ``SessionDataTask`` object associated with this download task. Multiple `DownloadTask`s could refer to the + /// same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading tasks for the same + /// URL resource simultaneously. + /// + /// When you call ``DownloadTask/cancel()``, this ``SessionDataTask`` and its cancellation token will be passed + /// along. You can use them to identify the cancelled task. + public private(set) var sessionTask: SessionDataTask? { + get { propertyQueue.sync { _sessionTask } } + set { propertyQueue.sync { _sessionTask = newValue } } + } + + private var _cancelToken: SessionDataTask.CancelToken? = nil + + /// The cancellation token used to cancel the task. + /// + /// This is solely for identifying the task when it is cancelled. To cancel a ``DownloadTask``, call + /// ``DownloadTask/cancelToken``. + public private(set) var cancelToken: SessionDataTask.CancelToken? { + get { propertyQueue.sync { _cancelToken } } + set { propertyQueue.sync { _cancelToken = newValue } } + } + + /// Cancel this single download task if it is running. + /// + /// This method will do nothing if this task is not running. + /// + /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is currently + /// being downloaded. However, even when internally no new session task is created, a ``DownloadTask`` will still + /// be created and returned when you call related methods. It will share the session downloading task with a + /// previous task. + /// + /// In this case, if multiple ``DownloadTask``s share a single session download task, calling this method + /// does not cancel the actual download process, since there are other `DownloadTask`s need it. It only removes + /// `self` from the download list. + /// + /// > Tip: If you need to cancel all on-going ``DownloadTask``s of a certain URL, use + /// ``ImageDownloader/cancel(url:)``. If you need to cancel all downloading tasks of an ``ImageDownloader``, + /// use ``ImageDownloader/cancelAll()``. + public func cancel() { + guard let sessionTask, let cancelToken else { return } + sessionTask.cancel(token: cancelToken) + } + + public var isInitialized: Bool { + propertyQueue.sync { + _sessionTask != nil && _cancelToken != nil + } + } + + func linkToTask(_ task: DownloadTask) { + self.sessionTask = task.sessionTask + self.cancelToken = task.cancelToken + } +} + +actor CancellationDownloadTask { + var task: DownloadTask? + func setTask(_ task: DownloadTask?) { + self.task = task + } +} + +extension DownloadTask { + enum WrappedTask { + case download(DownloadTask) + case dataProviding + + func cancel() { + switch self { + case .download(let task): task.cancel() + case .dataProviding: break + } + } + + var value: DownloadTask? { + switch self { + case .download(let task): return task + case .dataProviding: return nil + } + } + } +} + +/// Represents a download manager for requesting an image with a URL from the server. +open class ImageDownloader: @unchecked Sendable { + + // MARK: Singleton + + /// The default downloader. + public static let `default` = ImageDownloader(name: "default") + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloaderPropertyQueue") + + // MARK: Public Properties + + private var _downloadTimeout: TimeInterval = 15.0 + + /// The duration before the download times out. + /// + /// If the download does not complete before this duration, the URL session will raise a timeout error, which + /// Kingfisher wraps and forwards as a ``KingfisherError/ResponseErrorReason/URLSessionError(error:)``. + /// + /// The default timeout is set to 15 seconds. + open var downloadTimeout: TimeInterval { + get { propertyQueue.sync { _downloadTimeout } } + set { propertyQueue.sync { _downloadTimeout = newValue } } + } + + /// A set of trusted hosts when receiving server trust challenges. + /// + /// A challenge with host name contained in this set will be ignored. You can use this set to specify the + /// self-signed site. It only will be used if you don't specify the + /// ``ImageDownloader/authenticationChallengeResponder``. + /// + /// > If ``ImageDownloader/authenticationChallengeResponder`` is set, this property will be ignored and the + /// implementation of ``ImageDownloader/authenticationChallengeResponder`` will be used instead. + open var trustedHosts: Set? + + /// Use this to supply a configuration for the downloader. + /// + /// By default, `URLSessionConfiguration.ephemeral` will be used. + /// + /// You can modify the configuration before a downloading task begins. A configuration without persistent storage + /// for caches is necessary for the downloader to function correctly. + /// + /// > Setting a new session delegate to the downloader will invalidate the existing session and create a new one + /// > with the new value and the ``sessionDelegate``. + open var sessionConfiguration = URLSessionConfiguration.ephemeral { + didSet { + session.invalidateAndCancel() + session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + } + } + + /// The session delegate which is used to handle the session related tasks. + /// + /// > Setting a new session delegate to the downloader will invalidate the existing session and create a new one + /// > with the new value and the ``sessionConfiguration``. + open var sessionDelegate: SessionDelegate { + didSet { + session.invalidateAndCancel() + session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + setupSessionHandler() + } + } + + /// Whether the download requests should use pipeline or not. + /// + /// It sets the `httpShouldUsePipelining` of the `URLRequest` for the download task. Default is false. + open var requestsUsePipelining = false + + /// The delegate of this `ImageDownloader` object. + /// + /// See the ``ImageDownloaderDelegate`` protocol for more information. + open weak var delegate: (any ImageDownloaderDelegate)? + + /// A responder for authentication challenges. + /// + /// The downloader forwards the received authentication challenge for the downloading session to this responder. + /// See ``AuthenticationChallengeResponsible`` for more. + open weak var authenticationChallengeResponder: (any AuthenticationChallengeResponsible)? + + // The downloader name. + private let name: String + + // The session bound to the downloader. + private var session: URLSession + + // MARK: Initializers + + /// Creates a downloader with a given name. + /// + /// - Parameter name: The name for the downloader. It should not be empty. + public init(name: String) { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the downloader. " + + "A downloader with empty name is not permitted.") + } + + self.name = name + + sessionDelegate = SessionDelegate() + session = URLSession( + configuration: sessionConfiguration, + delegate: sessionDelegate, + delegateQueue: nil) + + authenticationChallengeResponder = self + setupSessionHandler() + } + + deinit { session.invalidateAndCancel() } + + private func setupSessionHandler() { + sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in + await (self.authenticationChallengeResponder ?? self).downloader(self, didReceive: invoke.1) + } + sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in + await (self.authenticationChallengeResponder ?? self).downloader(self, task: invoke.1, didReceive: invoke.2) + } + sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in + (self.delegate ?? self).isValidStatusCode(code, for: self) + } + sessionDelegate.onResponseReceived.delegate(on: self) { (self, response) in + await (self.delegate ?? self).imageDownloader(self, didReceive: response) + } + sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in + let (url, result) = value + do { + let value = try result.get() + self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil) + } catch { + self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error) + } + } + sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in + (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task) + } + } + + // Wraps `completionHandler` to `onCompleted` respectively. + private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate? { + completionHandler.map { block -> Delegate in + let delegate = Delegate, Void>() + delegate.delegate(on: self) { (self, callback) in + block(callback) + } + return delegate + } + } + + private func createTaskCallback( + _ completionHandler: ((DownloadResult) -> Void)?, + options: KingfisherParsedOptionsInfo + ) -> SessionDataTask.TaskCallback + { + SessionDataTask.TaskCallback( + onCompleted: createCompletionCallBack(completionHandler), + options: options + ) + } + + private func createDownloadContext( + with url: URL, + options: KingfisherParsedOptionsInfo, + done: @escaping (@Sendable (Result) -> Void) + ) + { + @Sendable func checkRequestAndDone(r: URLRequest) { + // There is a possibility that request modifier changed the url to `nil` or empty. + // In this case, throw an error. + guard let url = r.url, !url.absoluteString.isEmpty else { + done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r)))) + return + } + done(.success(DownloadingContext(url: url, request: r, options: options))) + } + + // Creates default request. + var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout) + request.httpShouldUsePipelining = requestsUsePipelining + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil { + request.allowsConstrainedNetworkAccess = false + } + + guard let requestModifier = options.requestModifier else { + checkRequestAndDone(r: request) + return + } + + // Modifies request before sending. + // FIXME: A temporary solution for keep the sync `ImageDownloadRequestModifier` behavior as before. + // We should be able to combine two cases once the full async support can be introduced to Kingfisher. + if let m = requestModifier as? any ImageDownloadRequestModifier { + guard let result = m.modified(for: request) else { + done(.failure(KingfisherError.requestError(reason: .emptyRequest))) + return + } + checkRequestAndDone(r: result) + } else { + Task { [request] in + guard let result = await requestModifier.modified(for: request) else { + done(.failure(KingfisherError.requestError(reason: .emptyRequest))) + return + } + checkRequestAndDone(r: result) + } + } + } + + private func addDownloadTask( + context: DownloadingContext, + callback: SessionDataTask.TaskCallback + ) -> DownloadTask + { + // Ready to start download. Add it to session task manager (`sessionHandler`) + let downloadTask: DownloadTask + if let existingTask = sessionDelegate.task(for: context.url) { + downloadTask = sessionDelegate.append(existingTask, callback: callback) + } else { + let sessionDataTask = session.dataTask(with: context.request) + sessionDataTask.priority = context.options.downloadPriority + downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback) + } + return downloadTask + } + + private func reportWillDownloadImage(url: URL, request: URLRequest) { + delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request) + } + + private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) { + var response: URLResponse? + var err: (any Error)? + do { + response = try result.get().1 + } catch { + err = error + } + self.delegate?.imageDownloader( + self, + didFinishDownloadingImageForURL: url, + with: response, + error: err + ) + } + + private func reportDidProcessImage( + result: Result, url: URL, response: URLResponse? + ) + { + if let image = try? result.get() { + self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response) + } + } + + private func startDownloadTask( + context: DownloadingContext, + callback: SessionDataTask.TaskCallback + ) -> DownloadTask + { + let downloadTask = addDownloadTask(context: context, callback: callback) + + guard let sessionTask = downloadTask.sessionTask, !sessionTask.started else { + return downloadTask + } + + sessionTask.onTaskDone.delegate(on: self) { (self, done) in + // Underlying downloading finishes. + // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback] + let (result, callbacks) = done + + // Before processing the downloaded data. + self.reportDidDownloadImageData(result: result, url: context.url) + + switch result { + // Download finished. Now process the data to an image. + case .success(let (data, response)): + let processor = ImageDataProcessor( + data: data, callbacks: callbacks, processingQueue: context.options.processingQueue + ) + processor.onImageProcessed.delegate(on: self) { (self, done) in + // `onImageProcessed` will be called for `callbacks.count` times, with each + // `SessionDataTask.TaskCallback` as the input parameter. + // result: Result, callback: SessionDataTask.TaskCallback + let (result, callback) = done + + self.reportDidProcessImage(result: result, url: context.url, response: response) + + let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) } + let queue = callback.options.callbackQueue + queue.execute { callback.onCompleted?.call(imageResult) } + } + processor.process() + + case .failure(let error): + callbacks.forEach { callback in + let queue = callback.options.callbackQueue + queue.execute { callback.onCompleted?.call(.failure(error)) } + } + } + } + + reportWillDownloadImage(url: context.url, request: context.request) + sessionTask.resume() + return downloadTask + } + + // MARK: Downloading Task + /// Downloads an image with a URL and options. + /// + /// - Parameters: + /// - url: The target URL. + /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter. + /// + /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherParsedOptionsInfo, + completionHandler: (@Sendable (Result) -> Void)? = nil) -> DownloadTask + { + let downloadTask = DownloadTask() + createDownloadContext(with: url, options: options) { result in + switch result { + case .success(let context): + // `downloadTask` will be set if the downloading started immediately. This is the case when no request + // modifier or a sync modifier (`ImageDownloadRequestModifier`) is used. Otherwise, when an + // `AsyncImageDownloadRequestModifier` is used the returned `downloadTask` of this method will be `nil` + // and the actual "delayed" task is given in `AsyncImageDownloadRequestModifier.onDownloadTaskStarted` + // callback. + let actualDownloadTask = self.startDownloadTask( + context: context, + callback: self.createTaskCallback(completionHandler, options: options) + ) + downloadTask.linkToTask(actualDownloadTask) + if let modifier = options.requestModifier { + modifier.onDownloadTaskStarted?(downloadTask) + } + case .failure(let error): + options.callbackQueue.execute { + completionHandler?(.failure(error)) + } + } + } + + return downloadTask + } + + /// Downloads an image with a URL and options. + /// + /// - Parameters: + /// - url: The target URL. + /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. + /// - progressBlock: Called when the download progress is updated. This block will always be called on the main + /// queue. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter. + /// + /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@Sendable (Result) -> Void)? = nil) -> DownloadTask + { + var info = KingfisherParsedOptionsInfo(options) + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return downloadImage( + with: url, + options: info, + completionHandler: completionHandler) + } + + /// Downloads an image with a URL and options. + /// + /// - Parameters: + /// - url: The target URL. + /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter. + /// + /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@Sendable (Result) -> Void)? = nil) -> DownloadTask + { + downloadImage( + with: url, + options: KingfisherParsedOptionsInfo(options), + completionHandler: completionHandler + ) + } +} + +// Concurrency +extension ImageDownloader { + /// Downloads an image with a URL and option. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. + /// - Returns: The image loading result. + /// + /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in. + public func downloadImage( + with url: URL, + options: KingfisherParsedOptionsInfo + ) async throws -> ImageLoadingResult { + let task = CancellationDownloadTask() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + let downloadTask = downloadImage(with: url, options: options) { result in + continuation.resume(with: result) + } + if Task.isCancelled { + downloadTask.cancel() + } else { + Task { + await task.setTask(downloadTask) + } + } + } + } onCancel: { + Task { + await task.task?.cancel() + } + } + } + + /// Downloads an image with a URL and option. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. + /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue. + /// - Returns: The image loading result. + /// + /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in. + public func downloadImage( + with url: URL, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil + ) async throws -> ImageLoadingResult + { + var info = KingfisherParsedOptionsInfo(options) + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return try await downloadImage(with: url, options: info) + } + + /// Downloads an image with a URL and option. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``. + /// - Returns: The image loading result. + /// + /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in. + public func downloadImage( + with url: URL, + options: KingfisherOptionsInfo? = nil + ) async throws -> ImageLoadingResult + { + try await downloadImage(with: url, options: KingfisherParsedOptionsInfo(options)) + } +} + +// MARK: Cancelling Task +extension ImageDownloader { + + /// Cancel all downloading tasks for this ``ImageDownloader``. + /// + /// It will trigger the completion handlers for all not-yet-finished downloading tasks with a cancellation error. + /// + /// If you need to only cancel a certain task, call ``DownloadTask/cancel()`` on the task returned by the + /// downloading methods. If you need to cancel all ``DownloadTask``s of a certain URL, use + /// ``ImageDownloader/cancel(url:)``. + public func cancelAll() { + sessionDelegate.cancelAll() + } + + /// Cancel all downloading tasks for a given URL. + /// + /// It will trigger the completion handlers for all not-yet-finished downloading tasks for the URL with a + /// cancellation error. + /// + /// - Parameter url: The URL for which you want to cancel downloading. + public func cancel(url: URL) { + sessionDelegate.cancel(url: url) + } +} + +// Use the default implementation from extension of `AuthenticationChallengeResponsible`. +extension ImageDownloader: AuthenticationChallengeResponsible {} + +// Use the default implementation from extension of `ImageDownloaderDelegate`. +extension ImageDownloader: ImageDownloaderDelegate {} + +extension ImageDownloader { + struct DownloadingContext { + let url: URL + let request: URLRequest + let options: KingfisherParsedOptionsInfo + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift new file mode 100644 index 00000000..6faf05ba --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift @@ -0,0 +1,188 @@ +// +// ImageDownloaderDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Protocol for handling events for ``ImageDownloader``. +/// +/// This delegate protocol provides a set of methods related to the stages and rules of the image downloader. You use +/// the provided methods to inspect the downloader working phases or respond to some events to make decisions. +public protocol ImageDownloaderDelegate: AnyObject { + + /// Called when the ``ImageDownloader`` object is about to start downloading an image from a specified URL. + /// + /// - Parameters: + /// - downloader: The ``ImageDownloader`` object used for the downloading operation. + /// - url: The URL of the starting request. + /// - request: The request object for the download process. + func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) + + /// Called when the ``ImageDownloader`` completes a downloading request with success or failure. + /// + /// - Parameters: + /// - downloader: The ``ImageDownloader`` object used for the downloading operation. + /// - url: The URL of the original request. + /// - response: The response object of the downloading process. + /// - error: The error in case of failure. + func imageDownloader( + _ downloader: ImageDownloader, + didFinishDownloadingImageForURL url: URL, + with response: URLResponse?, + error: (any Error)?) + + /// Called when the ``ImageDownloader`` object successfully downloads image data with a specified task. + /// + /// This is your last chance to verify or modify the downloaded data before Kingfisher attempts to perform + /// additional processing on the image data. + /// + /// - Parameters: + /// - downloader: The ``ImageDownloader`` object used for the downloading operation. + /// - data: The original downloaded data. + /// - task: The data task containing request and response information for the download. + /// - Returns: The data that Kingfisher should use to create an image. You need to provide valid data that is in + /// one of the supported image file formats. Kingfisher will process this data and attempt to convert it into an + /// image object. + func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? + + /// Called when the ``ImageDownloader`` object successfully downloads image data from a specified URL. + /// + /// This is your last chance to verify or modify the downloaded data before Kingfisher attempts to perform + /// additional processing on the image data. + /// + /// - Parameters: + /// - downloader: The ``ImageDownloader`` object used for the downloading operation. + /// - data: The original downloaded data. + /// - url: The URL of the original request. + /// + /// - Returns: The data that Kingfisher should use to create an image. You need to provide valid data that is in + /// one of the supported image file formats. Kingfisher will process this data and attempt to convert it into an + /// image object. + /// + /// This method can be used to preprocess raw image data before the creation of the `Image` instance (e.g., + /// decrypting or verification). If `nil` is returned, the processing is interrupted and a + /// ``KingfisherError/ResponseErrorReason/dataModifyingFailed(task:)`` error will be raised. You can use this fact + /// to stop the image processing flow if you find that the data is corrupted or malformed. + /// + /// > If the ``SessionDataTask`` version of `imageDownloader(_:didDownload:with:)` is implemented, this method will + /// > not be called anymore. + func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? + + /// Called when the ``ImageDownloader`` object successfully downloads and processes an image from a specified URL. + /// + /// - Parameters: + /// - downloader: The ``ImageDownloader`` object used for the downloading operation. + /// - image: The downloaded and processed image. + /// - url: The URL of the original request. + /// - response: The original response object of the downloading process. + func imageDownloader( + _ downloader: ImageDownloader, + didDownload image: KFCrossPlatformImage, + for url: URL, + with response: URLResponse?) + + /// Checks if a received HTTP status code is valid or not. + /// + /// By default, a status code in the range `200..<400` is considered as valid. If an invalid code is received, + /// the downloader will raise a ``KingfisherError/ResponseErrorReason/invalidHTTPStatusCode(response:)`` error. + /// + /// - Parameters: + /// - code: The received HTTP status code. + /// - downloader: The ``ImageDownloader`` object requesting validation of the status code. + /// - Returns: A value indicating whether this HTTP status code is valid or not. + /// + /// > If the default range of `200..<400` as valid codes does not suit your needs, you can implement this method to + /// change that behavior. + func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool + + /// Called when the task has received a valid HTTP response after passing other checks such as the status code. + /// You can perform additional checks or verifications on the response to determine if the download should be + /// allowed or cancelled. + /// + /// For example, this is useful if you want to verify some header values in the response before actually starting + /// the download. + /// + /// If implemented, you have to return a proper response disposition, such as `.allow` to start the actual + /// downloading or `.cancel` to cancel the task. If `.cancel` is used as the disposition, the downloader will raise + /// a ``KingfisherError/ResponseErrorReason/cancelledByDelegate(response:)`` error. If not implemented, any response + /// that passes other checks will be allowed, and the download will start. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object used for the downloading operation. + /// - response: The original response object of the downloading process. + /// + /// - Returns: The disposition for the download task. You have to return either `.allow` or `.cancel`. + func imageDownloader( + _ downloader: ImageDownloader, + didReceive response: URLResponse + ) async -> URLSession.ResponseDisposition +} + +// Default implementation for `ImageDownloaderDelegate`. +extension ImageDownloaderDelegate { + public func imageDownloader( + _ downloader: ImageDownloader, + willDownloadImageForURL url: URL, + with request: URLRequest?) {} + + public func imageDownloader( + _ downloader: ImageDownloader, + didFinishDownloadingImageForURL url: URL, + with response: URLResponse?, + error: (any Error)?) {} + + public func imageDownloader( + _ downloader: ImageDownloader, + didDownload image: KFCrossPlatformImage, + for url: URL, + with response: URLResponse?) {} + + public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool { + return (200..<400).contains(code) + } + + public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? { + guard let url = task.originalURL else { + return data + } + return imageDownloader(downloader, didDownload: data, for: url) + } + + public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? { + return data + } + + public func imageDownloader( + _ downloader: ImageDownloader, + didReceive response: URLResponse + ) async -> URLSession.ResponseDisposition { + .allow + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageModifier.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageModifier.swift new file mode 100644 index 00000000..65a36c82 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImageModifier.swift @@ -0,0 +1,120 @@ +// +// ImageModifier.swift +// Kingfisher +// +// Created by Ethan Gill on 2017/11/28. +// +// Copyright (c) 2019 Ethan Gill +// +// 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. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// An ``ImageModifier`` can be used to change properties on an image between cache serialization and the actual use of +/// the image. +/// +/// The ``ImageModifier/modify(_:)`` method will be called after the image is retrieved from its source and before it +/// is returned to the caller. This modified image is expected to be used only for rendering purposes; any changes +/// applied by the ``ImageModifier`` will not be serialized or cached. +public protocol ImageModifier: Sendable { + + /// Modify an input `Image`. + /// + /// - Parameter image: The image which will be modified by `self`. + /// + /// - Returns: The modified image. + /// + /// > Important: The return value will be unmodified if modification is not possible on the current platform. + func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage +} + +/// A wrapper that simplifies the creation of an ``ImageModifier``. +/// +/// This type conforms to ``ImageModifier`` and encapsulates an image modification block. If the `block` throws an +/// error, the original image will be used. +public struct AnyImageModifier: ImageModifier { + + /// A block that modifies images, or returns the original image if modification cannot be performed, along with an + /// error. + let block: @Sendable (KFCrossPlatformImage) throws -> KFCrossPlatformImage + + /// Creates an ``AnyImageModifier`` with a given `modify` block. + /// - Parameter modify: A block which is used to modify the input image. + public init(modify: @escaping @Sendable (KFCrossPlatformImage) throws -> KFCrossPlatformImage) { + block = modify + } + + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return (try? block(image)) ?? image + } +} + +#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +import UIKit + +/// Modifier for setting the rendering mode of images. +public struct RenderingModeImageModifier: ImageModifier { + + /// The rendering mode to apply to the image. + public let renderingMode: UIImage.RenderingMode + + /// Creates a ``RenderingModeImageModifier``. + /// + /// - Parameter renderingMode: The rendering mode to apply to the image. The default is `.automatic`. + public init(renderingMode: UIImage.RenderingMode = .automatic) { + self.renderingMode = renderingMode + } + + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.withRenderingMode(renderingMode) + } +} + +/// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images. +public struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier { + + /// Creates a ``FlipsForRightToLeftLayoutDirectionImageModifier``. + public init() {} + + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.imageFlippedForRightToLeftLayoutDirection() + } +} + +/// Modifier for setting the `alignmentRectInsets` property of images. +public struct AlignmentRectInsetsImageModifier: ImageModifier { + + /// The alignment insets to apply to the image. + public let alignmentInsets: UIEdgeInsets + + /// Creates a ``AlignmentRectInsetsImageModifier``. + /// - Parameter alignmentInsets: The alignment insets to apply to the image. + public init(alignmentInsets: UIEdgeInsets) { + self.alignmentInsets = alignmentInsets + } + + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.withAlignmentRectInsets(alignmentInsets) + } +} +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift new file mode 100644 index 00000000..3b97d5e7 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift @@ -0,0 +1,386 @@ +// +// ImagePrefetcher.swift +// Kingfisher +// +// Created by Claire Knight on 24/02/2016 +// +// Copyright (c) 2019 Wei Wang +// +// 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. + + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Progress update block of prefetcher when initialized with a list of resources. +/// +/// - Parameters: +/// - skippedResources: An array of resources that are already cached before the prefetching begins. +/// - failedResources: An array of resources that fail to be downloaded. This could be because of being cancelled while downloading, encountering an error during downloading, or the download not being started at all. +/// - completedResources: An array of resources that are downloaded and cached successfully. +public typealias PrefetcherProgressBlock = + ((_ skippedResources: [any Resource], _ failedResources: [any Resource], _ completedResources: [any Resource]) -> Void) + +/// Progress update block of prefetcher when initialized with a list of resources. +/// +/// - Parameters: +/// - skippedSources: An array of sources that are already cached before the prefetching begins. +/// - failedSources: An array of sources that fail to be fetched. +/// - completedResources: An array of sources that are fetched and cached successfully. +public typealias PrefetcherSourceProgressBlock = + ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void) + +/// Completion block of prefetcher when initialized with a list of sources. +/// +/// - Parameters: +/// - skippedResources: An array of resources that are already cached before the prefetching begins. +/// - failedResources: An array of resources that fail to be downloaded. This could be because of being cancelled while downloading, encountering an error during downloading, or the download not being started at all. +/// - completedResources: An array of resources that are downloaded and cached successfully. +public typealias PrefetcherCompletionHandler = + ((_ skippedResources: [any Resource], _ failedResources: [any Resource], _ completedResources: [any Resource]) -> Void) + +/// Completion block of prefetcher when initialized with a list of sources. +/// +/// - Parameters: +/// - skippedSources: An array of sources that are already cached before the prefetching begins. +/// - failedSources: An array of sources that fail to be fetched. +/// - completedSources: An array of sources that are fetched and cached successfully. +public typealias PrefetcherSourceCompletionHandler = + ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void) + +/// ``ImagePrefetcher`` represents a downloading manager for requesting many images via URLs and then caching them. +/// +/// Use this class when you know a list of image resources and want to download them before showing. It also works with +/// some Cocoa prefetching mechanisms like table view or collection view `prefetchDataSource` to start image downloading +/// and caching before they are displayed on screen. +public class ImagePrefetcher: CustomStringConvertible, @unchecked Sendable { + + public var description: String { + return "\(Unmanaged.passUnretained(self).toOpaque())" + } + + /// The maximum concurrent downloads to use when prefetching images. + /// + /// The default is 5. + public var maxConcurrentDownloads = 5 + + private let prefetchSources: [Source] + private let optionsInfo: KingfisherParsedOptionsInfo + + private var progressBlock: PrefetcherProgressBlock? + private var completionHandler: PrefetcherCompletionHandler? + + private var progressSourceBlock: PrefetcherSourceProgressBlock? + private var completionSourceHandler: PrefetcherSourceCompletionHandler? + + private var tasks = [String: DownloadTask.WrappedTask]() + + private var pendingSources: ArraySlice + private var skippedSources = [Source]() + private var completedSources = [Source]() + private var failedSources = [Source]() + + private var stopped = false + + // A manager used for prefetching. We will use the helper methods in manager. + private let manager: KingfisherManager + + private let prefetchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.prefetchQueue") + private static let requestingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue") + + private var finished: Bool { + let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count + return totalFinished == prefetchSources.count && tasks.isEmpty + } + + /// Creates an image prefetcher with an array of URLs. + /// + /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable. + /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the + /// prefetching process. The images that are already cached will be skipped without being downloaded again. + /// + /// - Parameters: + /// - urls: The URLs to be prefetched. + /// - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information. + /// - progressBlock: Called every time a resource is downloaded, skipped, or canceled. + /// - completionHandler: Called when the whole prefetching process is finished. + /// + /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache + /// targets, respectively. You can specify other downloaders or caches by using a customized + /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The + /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method. + public convenience init( + urls: [URL], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherProgressBlock? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + let resources: [any Resource] = urls.map { $0 } + self.init( + resources: resources, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Creates an image prefetcher with an array of ``Resource``s. + /// + /// The prefetcher should be initiated with a list of prefetching targets. The resource list is immutable. + /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the + /// prefetching process. The images that are already cached will be skipped without being downloaded again. + /// + /// - Parameters: + /// - resources: An array of resource to be prefetched. See ``ImageResource``. + /// - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information. + /// - progressBlock: Called every time a resource is downloaded, skipped, or canceled. + /// - completionHandler: Called when the whole prefetching process is finished. + /// + /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache + /// targets, respectively. You can specify other downloaders or caches by using a customized + /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The + /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method. + public convenience init( + resources: [any Resource], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherProgressBlock? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + self.init(sources: resources.map { $0.convertToSource() }, options: options) + self.progressBlock = progressBlock + self.completionHandler = completionHandler + } + + /// Creates an image prefetcher with an array of ``Source``s. + /// + /// The prefetcher should be initiated with a list of prefetching targets. The source list is immutable. + /// After you get a valid ``ImagePrefetcher`` object, you can call ``ImagePrefetcher/start()`` on it to begin the + /// prefetching process. The images that are already cached will be skipped without being downloaded again. + /// + /// - Parameters: + /// - sources: An array of resource to be prefetched. See ``Source``. + /// - options: Options that can control some behaviors. See ``KingfisherOptionsInfo`` for more information. + /// - progressBlock: Called every time a resource is downloaded, skipped, or canceled. + /// - completionHandler: Called when the whole prefetching process is finished. + /// + /// By default, the ``ImageDownloader/default`` and ``ImageCache/default`` will be used as the downloader and cache + /// targets, respectively. You can specify other downloaders or caches by using a customized + /// ``KingfisherOptionsInfo``. Both the progress and completion blocks will be invoked on the main thread. The + /// ``KingfisherOptionsInfoItem/callbackQueue(_:)`` value in `optionsInfo` will be ignored in this method. + public convenience init(sources: [Source], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherSourceProgressBlock? = nil, + completionHandler: PrefetcherSourceCompletionHandler? = nil) + { + self.init(sources: sources, options: options) + self.progressSourceBlock = progressBlock + self.completionSourceHandler = completionHandler + } + + init(sources: [Source], options: KingfisherOptionsInfo?) { + var options = KingfisherParsedOptionsInfo(options) + prefetchSources = sources + pendingSources = ArraySlice(sources) + + // We want all callbacks from our prefetch queue, so we should ignore the callback queue in options. + // Add our own callback dispatch queue to make sure all internal callbacks are + // coming back in our expected queue. + options.callbackQueue = .dispatch(prefetchQueue) + optionsInfo = options + + let cache = optionsInfo.targetCache ?? .default + let downloader = optionsInfo.downloader ?? .default + manager = KingfisherManager(downloader: downloader, cache: cache) + } + + /// Starts downloading the resources and caching them. + /// + /// This can be useful for the background downloading of assets that are required for later use in an app. This + /// code will not try to update any UI with the results of the process. + public func start() { + prefetchQueue.async { + guard !self.stopped else { + assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.") + self.handleComplete() + return + } + + guard self.maxConcurrentDownloads > 0 else { + assertionFailure("There should be concurrent downloads value should be at least 1.") + self.handleComplete() + return + } + + // Empty case. + guard self.prefetchSources.count > 0 else { + self.handleComplete() + return + } + + let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads) + for _ in 0 ..< initialConcurrentDownloads { + if let resource = self.pendingSources.popFirst() { + self.startPrefetching(resource) + } + } + } + } + + /// Stops the current downloading progress and cancels any future prefetching activity that might be occurring. + public func stop() { + prefetchQueue.async { + if self.finished { return } + self.stopped = true + self.tasks.values.forEach { $0.cancel() } + } + } + + private func downloadAndCache(_ source: Source) { + + let downloadTaskCompletionHandler: (@Sendable (Result) -> Void) = { + result in + + self.tasks.removeValue(forKey: source.cacheKey) + do { + let _ = try result.get() + self.completedSources.append(source) + } catch { + self.failedSources.append(source) + } + + self.reportProgress() + if self.stopped { + if self.tasks.isEmpty { + self.failedSources.append(contentsOf: self.pendingSources) + self.handleComplete() + } + } else { + self.reportCompletionOrStartNext() + } + } + + var downloadTask: DownloadTask.WrappedTask? + ImagePrefetcher.requestingQueue.sync { + let context = RetrievingContext( + options: optionsInfo, originalSource: source + ) + downloadTask = manager.loadAndCacheImage( + source: source, + context: context, + completionHandler: downloadTaskCompletionHandler) + } + + if let downloadTask = downloadTask { + tasks[source.cacheKey] = downloadTask + } + } + + private func append(cached source: Source) { + skippedSources.append(source) + + reportProgress() + reportCompletionOrStartNext() + } + + private func startPrefetching(_ source: Source) + { + if optionsInfo.forceRefresh { + downloadAndCache(source) + return + } + + let cacheType = manager.cache.imageCachedType( + forKey: source.cacheKey, + processorIdentifier: optionsInfo.processor.identifier + ) + switch cacheType { + case .memory: + append(cached: source) + case .disk: + if optionsInfo.alsoPrefetchToMemory { + let context = RetrievingContext(options: optionsInfo, originalSource: source) + _ = manager.retrieveImageFromCache( + source: source, + context: context) + { + _ in + self.append(cached: source) + } + } else { + append(cached: source) + } + case .none: + downloadAndCache(source) + } + } + + private func reportProgress() { + + if progressBlock == nil && progressSourceBlock == nil { + return + } + + let skipped = self.skippedSources + let failed = self.failedSources + let completed = self.completedSources + CallbackQueue.mainCurrentOrAsync.execute { + self.progressSourceBlock?(skipped, failed, completed) + self.progressBlock?( + skipped.compactMap { $0.asResource }, + failed.compactMap { $0.asResource }, + completed.compactMap { $0.asResource } + ) + } + } + + private func reportCompletionOrStartNext() { + if let resource = self.pendingSources.popFirst() { + // Loose call stack for huge amount of sources. + prefetchQueue.async { self.startPrefetching(resource) } + } else { + guard allFinished else { return } + self.handleComplete() + } + } + + var allFinished: Bool { + return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count + } + + private func handleComplete() { + + if completionHandler == nil && completionSourceHandler == nil { + return + } + + // The completion handler should be called on the main thread + CallbackQueue.mainCurrentOrAsync.execute { + self.completionSourceHandler?(self.skippedSources, self.failedSources, self.completedSources) + self.completionHandler?( + self.skippedSources.compactMap { $0.asResource }, + self.failedSources.compactMap { $0.asResource }, + self.completedSources.compactMap { $0.asResource } + ) + self.completionHandler = nil + self.progressBlock = nil + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift new file mode 100644 index 00000000..dbefcace --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift @@ -0,0 +1,81 @@ +// +// RedirectHandler.swift +// Kingfisher +// +// Created by Roman Maidanovych on 2018/12/10. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +/// The ``ImageDownloadRedirectHandler`` is used to modify the request before redirection. +/// +/// This allows you to customize the image download request during redirection. You can make modifications for +/// purposes such as adding an authentication token to the header, performing basic HTTP authentication, or URL +/// mapping. +/// +/// Typically, you pass an ``ImageDownloadRedirectHandler`` as the associated value of +/// ``KingfisherOptionsInfoItem/redirectHandler(_:)`` and use it as the `options` parameter in relevant methods. +/// +/// If you do not make any changes to the input `request` and return it as is, the downloading process will redirect +/// using it. +/// +public protocol ImageDownloadRedirectHandler: Sendable { + + /// Called when a redirect is received and the downloader waiting for the request to continue the download task. + /// + /// - Parameters: + /// - task: The current ``SessionDataTask`` that triggers this redirect. + /// - response: The response received during redirection. + /// - newRequest: The new request received from the URL session for redirection that can be modified. + /// - Returns: The modified request. + func handleHTTPRedirection( + for task: SessionDataTask, + response: HTTPURLResponse, + newRequest: URLRequest + ) async -> URLRequest? +} + +/// A wrapper for creating an ``ImageDownloadRedirectHandler`` instance more easily. +/// +/// This type conforms to ``ImageDownloadRedirectHandler`` and wraps an image modification block. +public struct AnyRedirectHandler: ImageDownloadRedirectHandler { + + let block: @Sendable (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void + + public func handleHTTPRedirection( + for task: SessionDataTask, response: HTTPURLResponse, newRequest: URLRequest + ) async -> URLRequest? { + return await withCheckedContinuation { continuation in + block(task, response, newRequest, { urlRequest in + continuation.resume(returning: urlRequest) + }) + } + } + + /// Creates a value of ``ImageDownloadRedirectHandler`` that executes the `modify` block. + /// + /// - Parameter handle: The block that modifies the request when a request modification task is triggered. + public init(handle: @escaping @Sendable (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void) { + block = handle + } + +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/RequestModifier.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/RequestModifier.swift new file mode 100644 index 00000000..c774e45c --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/RequestModifier.swift @@ -0,0 +1,142 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/05. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +/// Represents and wraps a method for modifying a request before an image download request starts asynchronously. +/// +/// Usually, you pass an ``AsyncImageDownloadRequestModifier`` instance as the associated value of +/// ``KingfisherOptionsInfoItem/requestModifier(_:)`` and use it as the `options` parameter in related methods. +/// +/// For example, the code below defines a modifier to add a header field and its value to the request. +/// +/// ```swift +/// class HeaderFieldModifier: AsyncImageDownloadRequestModifier { +/// var onDownloadTaskStarted: ((DownloadTask?) -> Void)? = nil +/// func modified(for request: URLRequest) async -> URLRequest? { +/// var r = request +/// let token = await service.fetchToken() +/// r.setValue(token, forHTTPHeaderField: "token") +/// return r +/// } +/// } +/// +/// imageView.kf.setImage(with: url, options: [.requestModifier(HeaderFieldModifier())]) +/// ``` +public protocol AsyncImageDownloadRequestModifier: Sendable { + + /// This method will be called just before the `request` is sent. + /// + /// This is the last chance to modify the image download request. You can modify the request for some customizing + /// purposes, such as adding an auth token to the header, performing basic HTTP auth, or something like URL mapping. + /// + /// After making the modification, you should return the modified request, and the data will be downloaded with + /// this modified request. + /// + /// > If you do nothing with the input `request` and return it as-is, the download process will start with it as the + /// modifier doesn't exist. If you return `nil`, the downloading will be interrupted with an + /// ``KingfisherError/RequestErrorReason/emptyRequest`` error. + /// + /// - Parameter request: The input request contains necessary information like `url`. This request is generated + /// according to your resource URL as a GET request. + /// - Returns: The modified request which should be used to trigger the download. + func modified(for request: URLRequest) async -> URLRequest? + + /// A block that will be called when the download task starts. + /// + /// If an ``AsyncImageDownloadRequestModifier`` and asynchronous modification occur before the download, the + /// related download method will not return a valid ``DownloadTask`` value. Instead, you can get one from this + /// method. + /// + /// User the ``DownloadTask`` value to track the task, or cancel it when you need to. + var onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)? { get } +} + +/// Represents and wraps a method for modifying a request before an image download request starts synchronously. +/// +/// Usually, you pass an ``ImageDownloadRequestModifier`` instance as the associated value of +/// ``KingfisherOptionsInfoItem/requestModifier(_:)`` and use it as the `options` parameter in related methods. +/// +/// For example, the code below defines a modifier to add a header field and its value to the request. +/// +/// ```swift +/// class HeaderFieldModifier: AsyncImageDownloadRequestModifier { +/// func modified(for request: URLRequest) -> URLRequest? { +/// var r = request +/// r.setValue("value", forHTTPHeaderField: "key") +/// return r +/// } +/// } +/// +/// imageView.kf.setImage(with: url, options: [.requestModifier(HeaderFieldModifier())]) +/// ``` +public protocol ImageDownloadRequestModifier: AsyncImageDownloadRequestModifier { + + /// This method will be called just before the `request` is sent. + /// + /// This is the last chance to modify the image download request. You can modify the request for some customizing + /// purposes, such as adding an auth token to the header, performing basic HTTP auth, or something like URL mapping. + /// + /// After making the modification, you should return the modified request, and the data will be downloaded with + /// this modified request. + /// + /// > If you do nothing with the input `request` and return it as-is, the download process will start with it as the + /// modifier doesn't exist. If you return `nil`, the downloading will be interrupted with an + /// ``KingfisherError/RequestErrorReason/emptyRequest`` error. + /// + /// > Tip: If you are trying to execute an async operation during the modify, choose to conform the + /// ``AsyncImageDownloadRequestModifier`` instead. + /// + /// - Parameter request: The input request contains necessary information like `url`. This request is generated + /// according to your resource URL as a GET request. + /// - Returns: The modified request which should be used to trigger the download. + func modified(for request: URLRequest) -> URLRequest? +} + +extension ImageDownloadRequestModifier { + /// This is `nil` for a sync `ImageDownloadRequestModifier` by default. You can get the `DownloadTask` from the + /// return value of downloader method. + public var onDownloadTaskStarted: (@Sendable (DownloadTask?) -> Void)? { return nil } +} + +/// A wrapper for creating an ``ImageDownloadRequestModifier`` instance more easily. +/// +/// This type conforms to ``ImageDownloadRequestModifier`` and wraps an image modification block. +public struct AnyModifier: ImageDownloadRequestModifier { + + let block: @Sendable (URLRequest) -> URLRequest? + + public func modified(for request: URLRequest) -> URLRequest? { + return block(request) + } + + /// Creates a value of ``ImageDownloadRequestModifier`` that runs the `modify` block. + /// + /// - Parameter modify: The request modifying block runs when a request modifying task comes. + public init(modify: @escaping @Sendable (URLRequest) -> URLRequest?) { + block = modify + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift new file mode 100644 index 00000000..08202694 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift @@ -0,0 +1,186 @@ +// +// RetryStrategy.swift +// Kingfisher +// +// Created by onevcat on 2020/05/04. +// +// Copyright (c) 2020 Wei Wang +// +// 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. + +import Foundation + +/// Represents a retry context that could be used to determine the current retry status. +/// +/// The instance of this type can be shared between different retry attempts. +public class RetryContext: @unchecked Sendable { + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.RetryContextPropertyQueue") + + /// The source from which the target image should be retrieved. + public let source: Source + + /// The source from which the target image should be retrieved. + public let error: KingfisherError + + private var _retriedCount: Int + + /// The number of retries attempted before the current retry happens. + /// + /// This value is `0` if the current retry is for the first time. + public var retriedCount: Int { + get { propertyQueue.sync { _retriedCount } } + set { propertyQueue.sync { _retriedCount = newValue } } + } + + private var _userInfo: Any? = nil + + /// A user-set value for passing any other information during the retry. + /// + /// If you choose to use ``RetryDecision/retry(userInfo:)`` as the retry decision for + /// ``RetryStrategy/retry(context:retryHandler:)``, the associated value of ``RetryDecision/retry(userInfo:)`` will + /// be delivered to you in the next retry. + public internal(set) var userInfo: Any? { + get { propertyQueue.sync { _userInfo } } + set { propertyQueue.sync { _userInfo = newValue } } + } + + init(source: Source, error: KingfisherError) { + self.source = source + self.error = error + _retriedCount = 0 + } + + @discardableResult + func increaseRetryCount() -> RetryContext { + retriedCount += 1 + return self + } +} + +/// Represents the decision on the behavior for the current retry. +public enum RetryDecision { + /// A retry should happen. The associated `userInfo` value will be passed to the next retry in the + /// ``RetryContext`` parameter. + case retry(userInfo: Any?) + /// There should be no more retry attempts. The image retrieving process will fail with an error. + case stop +} + +/// Defines a retry strategy that can be applied to the ``KingfisherOptionsInfoItem/retryStrategy(_:)`` option. +public protocol RetryStrategy: Sendable { + + /// Kingfisher calls this method if an error occurs during the image retrieving process from ``KingfisherManager``. + /// + /// You implement this method to provide the necessary logic based on the `context` parameter. Then you need to call + /// `retryHandler` to pass the retry decision back to Kingfisher. + /// + /// - Parameters: + /// - context: The retry context containing information of the current retry attempt. + /// - retryHandler: A block you need to call with a decision on whether the retry should happen or not. + func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void) +} + +/// A retry strategy that guides Kingfisher to perform retry operation with some delay. +/// +/// When an error of ``KingfisherError/ResponseErrorReason`` happens, Kingfisher uses the retry strategy in its option +/// to retry. This strategy defines a specified maximum retry count and a certain interval mechanism. +public struct DelayRetryStrategy: RetryStrategy { + + /// Represents the interval mechanism used in a ``DelayRetryStrategy``. + public enum Interval : Sendable{ + + /// The next retry attempt should happen in a fixed number of seconds. + /// + /// For example, if the associated value is 3, the attempt happens 3 seconds after the previous decision is + /// made. + case seconds(TimeInterval) + + /// The next retry attempt should happen in an accumulated duration. + /// + /// For example, if the associated value is 3, the attempts happen with intervals of 3, 6, 9, 12, ... seconds. + case accumulated(TimeInterval) + + /// Uses a block to determine the next interval. + /// + /// The current retry count is given as a parameter. + case custom(block: @Sendable (_ retriedCount: Int) -> TimeInterval) + + func timeInterval(for retriedCount: Int) -> TimeInterval { + let retryAfter: TimeInterval + switch self { + case .seconds(let interval): + retryAfter = interval + case .accumulated(let interval): + retryAfter = Double(retriedCount + 1) * interval + case .custom(let block): + retryAfter = block(retriedCount) + } + return retryAfter + } + } + + /// The maximum number of retries allowed by the retry strategy. + public let maxRetryCount: Int + + /// The interval between retry attempts in the retry strategy. + public let retryInterval: Interval + + /// Creates a delayed retry strategy. + /// + /// - Parameters: + /// - maxRetryCount: The maximum number of retries allowed. + /// - retryInterval: The mechanism defining the interval between retry attempts. + /// + /// By default, ``Interval/seconds(_:)`` with an associated value `3` is used to establish a constant retry + /// interval. + public init(maxRetryCount: Int, retryInterval: Interval = .seconds(3)) { + self.maxRetryCount = maxRetryCount + self.retryInterval = retryInterval + } + + public func retry(context: RetryContext, retryHandler: @escaping @Sendable (RetryDecision) -> Void) { + // Retry count exceeded. + guard context.retriedCount < maxRetryCount else { + retryHandler(.stop) + return + } + + // User cancel the task. No retry. + guard !context.error.isTaskCancelled else { + retryHandler(.stop) + return + } + + // Only retry for a response error. + guard case KingfisherError.responseError = context.error else { + retryHandler(.stop) + return + } + + let interval = retryInterval.timeInterval(for: context.retriedCount) + if interval == 0 { + retryHandler(.retry(userInfo: nil)) + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + interval) { + retryHandler(.retry(userInfo: nil)) + } + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift new file mode 100644 index 00000000..520c51c5 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift @@ -0,0 +1,142 @@ +// +// SessionDataTask.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +/// Represents a session data task in ``ImageDownloader``. +/// +/// Essentially, a ``SessionDataTask`` wraps a `URLSessionDataTask` and manages the download data. +/// It uses a ``SessionDataTask/CancelToken`` to track the task and manage its cancellation. +public class SessionDataTask: @unchecked Sendable { + + /// Represents the type of token used for canceling a task. + public typealias CancelToken = Int + + struct TaskCallback { + let onCompleted: Delegate, Void>? + let options: KingfisherParsedOptionsInfo + } + + private var _mutableData: Data + /// The downloaded raw data of the current task. + public var mutableData: Data { + lock.lock() + defer { lock.unlock() } + return _mutableData + } + + // This is a copy of `task.originalRequest?.url`. It is for obtaining race-safe behavior for a pitfall on iOS 13. + // Ref: https://github.com/onevcat/Kingfisher/issues/1511 + public let originalURL: URL? + + /// The underlying download task. + /// + /// It is only for debugging purposes when you encounter an error. You should not modify the content of this task + /// or start it yourself. + public let task: URLSessionDataTask + + private var callbacksStore = [CancelToken: TaskCallback]() + + var callbacks: [SessionDataTask.TaskCallback] { + lock.lock() + defer { lock.unlock() } + return Array(callbacksStore.values) + } + + private var currentToken = 0 + private let lock = NSLock() + + let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>() + let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>() + + var started = false + var containsCallbacks: Bool { + // We should be able to use `task.state != .running` to check it. + // However, in some rare cases, cancelling the task does not change + // task state to `.cancelling` immediately, but still in `.running`. + // So we need to check callbacks count to for sure that it is safe to remove the + // task in delegate. + return !callbacks.isEmpty + } + + init(task: URLSessionDataTask) { + self.task = task + self.originalURL = task.originalRequest?.url + _mutableData = Data() + } + + func addCallback(_ callback: TaskCallback) -> CancelToken { + lock.lock() + defer { lock.unlock() } + callbacksStore[currentToken] = callback + defer { currentToken += 1 } + return currentToken + } + + func removeCallback(_ token: CancelToken) -> TaskCallback? { + lock.lock() + defer { lock.unlock() } + if let callback = callbacksStore[token] { + callbacksStore[token] = nil + return callback + } + return nil + } + + @discardableResult + func removeAllCallbacks() -> [TaskCallback] { + lock.lock() + defer { lock.unlock() } + let callbacks = callbacksStore.values + callbacksStore.removeAll() + return Array(callbacks) + } + + func resume() { + guard !started else { return } + started = true + task.resume() + } + + func cancel(token: CancelToken) { + guard let callback = removeCallback(token) else { + return + } + onCallbackCancelled.call((token, callback)) + } + + func forceCancel() { + for token in callbacksStore.keys { + cancel(token: token) + } + } + + func didReceiveData(_ data: Data) { + lock.lock() + defer { lock.unlock() } + _mutableData.append(data) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift new file mode 100644 index 00000000..63470896 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift @@ -0,0 +1,270 @@ +// +// SessionDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +/// Represents the delegate object of the downloader session. +/// +/// It also behaves like a task manager for downloading. +@objc(KFSessionDelegate) // Fix for ObjC header name conflicting. https://github.com/onevcat/Kingfisher/issues/1530 +open class SessionDelegate: NSObject, @unchecked Sendable { + + typealias SessionChallengeFunc = ( + URLSession, + URLAuthenticationChallenge + ) + + typealias SessionTaskChallengeFunc = ( + URLSession, + URLSessionTask, + URLAuthenticationChallenge + ) + + private var tasks: [URL: SessionDataTask] = [:] + private let lock = NSLock() + + let onValidStatusCode = Delegate() + let onResponseReceived = Delegate() + let onDownloadingFinished = Delegate<(URL, Result), Void>() + let onDidDownloadData = Delegate() + + let onReceiveSessionChallenge = Delegate() + let onReceiveSessionTaskChallenge = Delegate() + + func add( + _ dataTask: URLSessionDataTask, + url: URL, + callback: SessionDataTask.TaskCallback) -> DownloadTask + { + lock.lock() + defer { lock.unlock() } + + // Create a new task if necessary. + let task = SessionDataTask(task: dataTask) + task.onCallbackCancelled.delegate(on: self) { [weak task] (self, value) in + guard let task = task else { return } + + let (token, callback) = value + + let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token)) + task.onTaskDone.call((.failure(error), [callback])) + // No other callbacks waiting, we can clear the task now. + if !task.containsCallbacks { + let dataTask = task.task + + self.cancelTask(dataTask) + self.remove(task) + } + } + let token = task.addCallback(callback) + tasks[url] = task + return DownloadTask(sessionTask: task, cancelToken: token) + } + + private func cancelTask(_ dataTask: URLSessionDataTask) { + lock.lock() + defer { lock.unlock() } + dataTask.cancel() + } + + func append( + _ task: SessionDataTask, + callback: SessionDataTask.TaskCallback) -> DownloadTask + { + let token = task.addCallback(callback) + return DownloadTask(sessionTask: task, cancelToken: token) + } + + private func remove(_ task: SessionDataTask) { + lock.lock() + defer { lock.unlock() } + + guard let url = task.originalURL else { + return + } + task.removeAllCallbacks() + tasks[url] = nil + } + + private func task(for task: URLSessionTask) -> SessionDataTask? { + lock.lock() + defer { lock.unlock() } + + guard let url = task.originalRequest?.url else { + return nil + } + guard let sessionTask = tasks[url] else { + return nil + } + guard sessionTask.task.taskIdentifier == task.taskIdentifier else { + return nil + } + return sessionTask + } + + func task(for url: URL) -> SessionDataTask? { + lock.lock() + defer { lock.unlock() } + return tasks[url] + } + + func cancelAll() { + lock.lock() + let taskValues = tasks.values + lock.unlock() + for task in taskValues { + task.forceCancel() + } + } + + func cancel(url: URL) { + lock.lock() + let task = tasks[url] + lock.unlock() + task?.forceCancel() + } +} + +extension SessionDelegate: URLSessionDataDelegate { + + open func urlSession( + _ session: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse + ) async -> URLSession.ResponseDisposition { + guard let httpResponse = response as? HTTPURLResponse else { + let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response)) + onCompleted(task: dataTask, result: .failure(error)) + return .cancel + } + + let httpStatusCode = httpResponse.statusCode + guard onValidStatusCode.call(httpStatusCode) == true else { + let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse)) + onCompleted(task: dataTask, result: .failure(error)) + return .cancel + } + + guard let disposition = await onResponseReceived.callAsync(response) else { + return .cancel + } + + if disposition == .cancel { + let error = KingfisherError.responseError(reason: .cancelledByDelegate(response: response)) + self.onCompleted(task: dataTask, result: .failure(error)) + } + + return disposition + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + guard let task = self.task(for: dataTask) else { + return + } + + task.didReceiveData(data) + + task.callbacks.forEach { callback in + callback.options.onDataReceived?.forEach { sideEffect in + sideEffect.onDataReceived(session, task: task, data: data) + } + } + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { + guard let sessionTask = self.task(for: task) else { return } + + if let url = sessionTask.originalURL { + let result: Result + if let error = error { + result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) + } else if let response = task.response { + result = .success(response) + } else { + result = .failure(KingfisherError.responseError(reason: .noURLResponse(task: sessionTask))) + } + onDownloadingFinished.call((url, result)) + } + + let result: Result<(Data, URLResponse?), KingfisherError> + if let error = error { + result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) + } else { + if let data = onDidDownloadData.call(sessionTask) { + result = .success((data, task.response)) + } else { + result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask))) + } + } + onCompleted(task: task, result: result) + } + + open func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) + { + await onReceiveSessionChallenge.callAsync((session, challenge)) ?? (.performDefaultHandling, nil) + } + + open func urlSession( + _ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) + { + await onReceiveSessionTaskChallenge.callAsync((session, task, challenge)) ?? (.performDefaultHandling, nil) + } + + + open func urlSession( + _ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest + ) async -> URLRequest? + { + guard let sessionDataTask = self.task(for: task), + let redirectHandler = Array(sessionDataTask.callbacks).last?.options.redirectHandler else + { + return request + } + return await redirectHandler.handleHTTPRedirection( + for: sessionDataTask, + response: response, + newRequest: request + ) + } + + private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?), KingfisherError>) { + guard let sessionTask = self.task(for: task) else { + return + } + let callbacks = sessionTask.removeAllCallbacks() + sessionTask.onTaskDone.call((result, callbacks)) + remove(sessionTask) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/PrivacyInfo.xcprivacy b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..993e1f60 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/PrivacyInfo.xcprivacy @@ -0,0 +1,25 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + NSPrivacyCollectedDataTypes + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift new file mode 100644 index 00000000..dac336b1 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift @@ -0,0 +1,168 @@ +// +// ImageBinder.swift +// Kingfisher +// +// Created by onevcat on 2019/06/27. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + + /// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs + /// image downloading and progress reporting based on `KingfisherManager`. + @MainActor + class ImageBinder: ObservableObject { + + init() {} + + var downloadTask: DownloadTask? + private var loading = false + + var loadingOrSucceeded: Bool { + return loading || loadedImage != nil + } + + // Do not use @Published due to https://github.com/onevcat/Kingfisher/issues/1717. Revert to @Published once + // we can drop iOS 12. + private(set) var loaded = false + + private(set) var animating = false + + var loadedImage: KFCrossPlatformImage? = nil { willSet { objectWillChange.send() } } + var progress: Progress = .init() + + func markLoading() { + loading = true + } + + func markLoaded(sendChangeEvent: Bool) { + loaded = true + if sendChangeEvent { + objectWillChange.send() + } + } + + func start(context: Context) { + guard let source = context.source else { + CallbackQueueMain.currentOrAsync { + context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource)) + if let image = context.options.onFailureImage { + self.loadedImage = image + } + self.loading = false + self.markLoaded(sendChangeEvent: false) + } + return + } + + loading = true + + progress = .init() + downloadTask = KingfisherManager.shared + .retrieveImage( + with: source, + options: context.options, + progressBlock: { size, total in + self.updateProgress(downloaded: size, total: total) + context.onProgressDelegate.call((size, total)) + }, + progressiveImageSetter: { image in + CallbackQueueMain.currentOrAsync { + self.markLoaded(sendChangeEvent: true) + self.loadedImage = image + } + }, + completionHandler: { [weak self] result in + + guard let self else { return } + + CallbackQueueMain.currentOrAsync { + self.downloadTask = nil + self.loading = false + } + + switch result { + case .success(let value): + CallbackQueueMain.currentOrAsync { + if let fadeDuration = context.fadeTransitionDuration(cacheType: value.cacheType) { + self.animating = true + let animation = Animation.linear(duration: fadeDuration) + withAnimation(animation) { + // Trigger the view render to apply the animation. + self.markLoaded(sendChangeEvent: true) + } + } else { + self.markLoaded(sendChangeEvent: false) + } + self.loadedImage = value.image + self.animating = false + } + + CallbackQueueMain.async { + context.onSuccessDelegate.call(value) + } + case .failure(let error): + CallbackQueueMain.currentOrAsync { + if let image = context.options.onFailureImage { + self.loadedImage = image + } + self.markLoaded(sendChangeEvent: false) + } + + CallbackQueueMain.async { + context.onFailureDelegate.call(error) + } + } + }) + } + + private func updateProgress(downloaded: Int64, total: Int64) { + progress.totalUnitCount = total + progress.completedUnitCount = downloaded + objectWillChange.send() + } + + /// Cancels the download task if it is in progress. + func cancel() { + downloadTask?.cancel() + downloadTask = nil + loading = false + } + + /// Restores the download task priority to default if it is in progress. + func restorePriorityOnAppear() { + guard let downloadTask = downloadTask, loading == true else { return } + downloadTask.sessionTask?.task.priority = URLSessionTask.defaultPriority + } + + /// Reduce the download task priority if it is in progress. + func reducePriorityOnDisappear() { + guard let downloadTask = downloadTask, loading == true else { return } + downloadTask.sessionTask?.task.priority = URLSessionTask.lowPriority + } + } +} +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift new file mode 100644 index 00000000..bdd1df8a --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift @@ -0,0 +1,142 @@ +// +// ImageContext.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 Wei Wang +// +// 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + public class Context: @unchecked Sendable { + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.KFImageContextPropertyQueue") + + let source: Source? + var _options = KingfisherParsedOptionsInfo( + KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously] + ) + var options: KingfisherParsedOptionsInfo { + get { propertyQueue.sync { _options } } + set { propertyQueue.sync { _options = newValue } } + } + + var _configurations: [(HoldingView) -> HoldingView] = [] + var configurations: [(HoldingView) -> HoldingView] { + get { propertyQueue.sync { _configurations } } + set { propertyQueue.sync { _configurations = newValue } } + } + + var _renderConfigurations: [(HoldingView.RenderingView) -> Void] = [] + var renderConfigurations: [(HoldingView.RenderingView) -> Void] { + get { propertyQueue.sync { _renderConfigurations } } + set { propertyQueue.sync { _renderConfigurations = newValue } } + } + + var _contentConfiguration: ((HoldingView) -> AnyView)? = nil + var contentConfiguration: ((HoldingView) -> AnyView)? { + get { propertyQueue.sync { _contentConfiguration } } + set { propertyQueue.sync { _contentConfiguration = newValue } } + } + + var _cancelOnDisappear: Bool = false + var cancelOnDisappear: Bool { + get { propertyQueue.sync { _cancelOnDisappear } } + set { propertyQueue.sync { _cancelOnDisappear = newValue } } + } + + var _reducePriorityOnDisappear: Bool = false + var reducePriorityOnDisappear: Bool { + get { propertyQueue.sync { _reducePriorityOnDisappear } } + set { propertyQueue.sync { _reducePriorityOnDisappear = newValue } } + } + + var _placeholder: ((Progress) -> AnyView)? = nil + var placeholder: ((Progress) -> AnyView)? { + get { propertyQueue.sync { _placeholder } } + set { propertyQueue.sync { _placeholder = newValue } } + } + + var _startLoadingBeforeViewAppear: Bool = false + var startLoadingBeforeViewAppear: Bool { + get { propertyQueue.sync { _startLoadingBeforeViewAppear } } + set { propertyQueue.sync { _startLoadingBeforeViewAppear = newValue } } + } + + let onFailureDelegate = Delegate() + let onSuccessDelegate = Delegate() + let onProgressDelegate = Delegate<(Int64, Int64), Void>() + + init(source: Source?) { + self.source = source + } + + func shouldApplyFade(cacheType: CacheType) -> Bool { + options.forceTransition || cacheType == .none + } + + func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? { + shouldApplyFade(cacheType: cacheType) + ? options.transition.fadeDuration + : nil + } + } +} + +extension ImageTransition { + // Only for fade effect in SwiftUI. + fileprivate var fadeDuration: TimeInterval? { + switch self { + case .fade(let duration): + return duration + default: + return nil + } + } +} + + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage.Context: Hashable { + public static func == (lhs: KFImage.Context, rhs: KFImage.Context) -> Bool { + lhs.source == rhs.source && + lhs.options.processor.identifier == rhs.options.processor.identifier + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(source) + hasher.combine(options.processor.identifier) + } +} + +#if !os(watchOS) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) +extension KFAnimatedImage { + public typealias Context = KFImage.Context + typealias ImageBinder = KFImage.ImageBinder +} +#endif + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift new file mode 100644 index 00000000..3abc072f --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift @@ -0,0 +1,128 @@ +// +// KFAnimatedImage.swift +// Kingfisher +// +// Created by wangxingbin on 2021/4/29. +// +// Copyright (c) 2021 Wei Wang +// +// 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. + +#if canImport(SwiftUI) && canImport(Combine) && !os(watchOS) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, *) +public struct KFAnimatedImage: KFImageProtocol { + public typealias HoldingView = KFAnimatedImageViewRepresenter + public var context: Context + public init(context: KFImage.Context) { + self.context = context + } + + /// Configures current rendering view with a `block`. This block will be applied when the under-hood + /// `AnimatedImageView` is created in `UIViewRepresentable.makeUIView(context:)` + /// + /// - Parameter block: The block applies to the animated image view. + /// - Returns: A `KFAnimatedImage` view that being configured by the `block`. + public func configure(_ block: @escaping (HoldingView.RenderingView) -> Void) -> Self { + context.renderConfigurations.append(block) + return self + } +} + +#if os(macOS) +@available(macOS 11.0, *) +typealias KFCrossPlatformViewRepresentable = NSViewRepresentable +#else +@available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) +typealias KFCrossPlatformViewRepresentable = UIViewRepresentable +#endif + +/// A wrapped `UIViewRepresentable` of `AnimatedImageView` +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFAnimatedImageViewRepresenter: KFCrossPlatformViewRepresentable, KFImageHoldingView { + public typealias RenderingView = AnimatedImageView + public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> KFAnimatedImageViewRepresenter { + KFAnimatedImageViewRepresenter(image: image, context: context) + } + + var image: KFCrossPlatformImage? + let context: KFImage.Context + + #if os(macOS) + public func makeNSView(context: Context) -> AnimatedImageView { + return makeImageView() + } + + public func updateNSView(_ nsView: AnimatedImageView, context: Context) { + updateImageView(nsView) + } + #else + public func makeUIView(context: Context) -> AnimatedImageView { + return makeImageView() + } + + public func updateUIView(_ uiView: AnimatedImageView, context: Context) { + updateImageView(uiView) + } + #endif + + @MainActor + private func makeImageView() -> AnimatedImageView { + let view = AnimatedImageView() + + #if !os(macOS) + view.isUserInteractionEnabled = true + #endif + + self.context.renderConfigurations.forEach { $0(view) } + + view.image = image + + // Allow SwiftUI scale (fit/fill) working fine. + view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + view.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + return view + } + + @MainActor + private func updateImageView(_ imageView: AnimatedImageView) { + imageView.image = image + } +} + +#if DEBUG +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFAnimatedImage_Previews: PreviewProvider { + static var previews: some View { + Group { + KFAnimatedImage(source: .network(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/1.gif")!)) + .onSuccess { r in + print(r) + } + .placeholder { + ProgressView() + } + .padding() + } + } +} +#endif +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift new file mode 100644 index 00000000..7a4119bf --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift @@ -0,0 +1,150 @@ +// +// KFImage.swift +// Kingfisher +// +// Created by onevcat on 2019/06/26. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +/// Represents an image view in SwiftUI that manages its content using Kingfisher. +/// +/// This view asynchronously loads the content. You can set a ``Source`` to load for the ``KFImage`` through +/// its ``KFImage/init(source:)`` or ``KFImage/init(_:)`` initializers or other relevant methods in ``KF`` Builder type. +/// Kingfisher will first look for the required image in the cache. If it is not found, it will load it via the +/// ``Source`` and provide the result for display, following sending the result to cache and for the future use. +/// +/// When using a `URL` valve as the ``Source``, it is similar to SwiftUI's `AsyncImage` but with additional support +/// for caching. +/// +/// Here is a basic example of using ``KFImage``: +/// +/// ```swift +/// var body: some View { +/// KFImage(URL(string: "https://example.com/image.png")!) +/// } +/// ``` +/// +/// Usually, you can also use the value by calling additional modifiers defined on it, to configure the view: +/// +/// ```swift +/// var body: some View { +/// KFImage.url(url) +/// .placeholder(placeholderImage) +/// .setProcessor(processor) +/// .loadDiskFileSynchronously() +/// .cacheMemoryOnly() +/// .onSuccess { result in } +/// } +/// ``` +/// Here only very few are listed as demonstration. To check other available modifiers, see ``KFOptionSetter`` and its +/// extension methods. +/// +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFImage: KFImageProtocol { + + /// Represent the wrapping context of the image view. + /// + /// Inside ``KFImage`` it is using the `SwiftUI.Image` to render the image. + public var context: Context + + /// Initializes the ``KFImage`` with a context. + /// + /// This should be only used internally in Kingfisher. Do not use this initializer yourself. Instead, use + /// ``KFImage/init(source:)`` or ``KFImage/init(_:)`` initializers or other relevant methods in ``KF`` Builder + /// type. + /// - Parameter context: The context value that the image view should wrap. + public init(context: Context) { + self.context = context + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension Image: KFImageHoldingView { + public typealias RenderingView = Image + public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Image { + Image(crossPlatformImage: image) + } +} + +// MARK: - Image compatibility. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + + public func resizable( + capInsets: EdgeInsets = EdgeInsets(), + resizingMode: Image.ResizingMode = .stretch) -> KFImage + { + configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) } + } + + public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFImage { + configure { $0.renderingMode(renderingMode) } + } + + public func interpolation(_ interpolation: Image.Interpolation) -> KFImage { + configure { $0.interpolation(interpolation) } + } + + public func antialiased(_ isAntialiased: Bool) -> KFImage { + configure { $0.antialiased(isAntialiased) } + } + + /// Starts the loading process of `self` immediately. + /// + /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading + /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a + /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once + /// could help avoiding the flickering, with some performance trade-off. + /// + /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. + /// It does nothing now and please just remove it. + /// + /// - Returns: The `Self` value with changes applied. + @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") + public func loadImmediately(_ start: Bool = true) -> KFImage { + return self + } +} + +#if DEBUG +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFImage_Previews: PreviewProvider { + static var previews: some View { + Group { + KFImage.url(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!) + .onSuccess { r in + print(r) + } + .placeholder { p in + ProgressView(p) + } + .resizable() + .aspectRatio(contentMode: .fit) + .padding() + } + } +} +#endif +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift new file mode 100644 index 00000000..fc6099cd --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift @@ -0,0 +1,181 @@ +// +// KFImageOptions.swift +// Kingfisher +// +// Created by onevcat on 2020/12/20. +// +// Copyright (c) 2020 Wei Wang +// +// 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +// MARK: - KFImage creating. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + + /// Creates a Kingfisher-compatible image view with a given ``Source``. + /// + /// - Parameters: + /// - source: The ``Source`` object that defines data information from the network or a data provider. + /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. + public static func source( + _ source: Source? + ) -> Self + { + Self.init(source: source) + } + + /// Creates a Kingfisher-compatible image view with a given ``Resource``. + /// + /// - Parameters: + /// - resource: The ``Resource`` object that defines data information such as a key or URL. + /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. + public static func resource( + _ resource: (any Resource)? + ) -> Self + { + source(resource?.convertToSource()) + } + + /// Creates a Kingfisher-compatible image view with a given `URL`. + /// + /// - Parameters: + /// - url: The `URL` from which the image should be downloaded. + /// - cacheKey: The key used to store the downloaded image in the cache. If `nil`, the `absoluteString` of `url` + /// is used as the cache key. + /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. + public static func url( + _ url: URL?, cacheKey: String? = nil + ) -> Self + { + source(url?.convertToSource(overrideCacheKey: cacheKey)) + } + + /// Creates a Kingfisher-compatible image view with a given ``ImageDataProvider``. + /// + /// - Parameters: + /// - provider: The ``ImageDataProvider`` object that contains information about the data. + /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. + + public static func dataProvider( + _ provider: (any ImageDataProvider)? + ) -> Self + { + source(provider?.convertToSource()) + } + + /// Creates a builder for the provided raw data and a cache key. + /// + /// - Parameters: + /// - data: The data object from which the image should be created. + /// - cacheKey: The key used to store the downloaded image in the cache. + /// - Returns: A Kingfisher-compatible image view for future configuration or embedding into another `SwiftUI.View`. + public static func data( + _ data: Data?, cacheKey: String + ) -> Self + { + if let data = data { + return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) + } else { + return dataProvider(nil) + } + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + + /// Sets a placeholder `View` that is displayed during the image loading, with a progress parameter as input. + /// + /// - Parameter content: A view that represents the placeholder. + /// - Returns: A Kingfisher-compatible image view that includes the provided `content` as its placeholder. + public func placeholder(@ViewBuilder _ content: @escaping (Progress) -> P) -> Self { + context.placeholder = { progress in + return AnyView(content(progress)) + } + return self + } + + /// Sets a placeholder `View` that is displayed during the image loading. + /// + /// - Parameter content: A view that represents the placeholder. + /// - Returns: A Kingfisher-compatible image view that includes the provided `content` as its placeholder. + public func placeholder(@ViewBuilder _ content: @escaping () -> P) -> Self { + placeholder { _ in content() } + } + + /// Enables canceling the download task associated with `self` when the view disappears. + /// + /// - Parameter flag: A boolean value indicating whether to cancel the task. + /// - Returns: A Kingfisher-compatible image view that cancels the download task when it disappears. + public func cancelOnDisappear(_ flag: Bool) -> Self { + context.cancelOnDisappear = flag + return self + } + + /// Sets reduce priority of the download task to low, bound to `self` when the view disappearing. + /// - Parameter flag: Whether reduce the priority task or not. + /// - Returns: A `KFImage` view that reduces downloading task priority when disappears. + public func reducePriorityOnDisappear(_ flag: Bool) -> Self { + context.reducePriorityOnDisappear = flag + return self + } + + + /// Sets a fade transition for the image task. + /// + /// - Parameter duration: The duration of the fade transition. + /// - Returns: A Kingfisher-compatible image view with the applied changes. + /// + /// Kingfisher will use the fade transition to animate the image if it is downloaded from the web. The transition + /// will not occur when the image is retrieved from either memory or disk cache by default. If you need the + /// transition to occur even when the image is retrieved from the cache, also call + /// ``KFOptionSetter/forceRefresh(_:)`` on the returned view. + public func fade(duration: TimeInterval) -> Self { + context.options.transition = .fade(duration) + return self + } + + /// Sets whether to start the image loading before the view actually appears. + /// + /// - Parameter flag: A boolean value indicating whether the image loading should happen before the view appears. The default is `true`. + /// - Returns: A Kingfisher-compatible image view with the applied changes. + /// + /// By default, Kingfisher performs lazy loading for `KFImage`. The image loading won't start until the view's + /// `onAppear` is called. However, sometimes you may want to trigger aggressive loading for the view. By enabling + /// this, the `KFImage` will attempt to load the view when its `body` is evaluated if the image loading has not + /// yet started or if a previous loading attempt failed. + /// + /// > Important: This was a temporary workaround for an issue that arose in iOS 16, where the SwiftUI view's + /// > `onAppear` was not called when it was deeply embedded inside a `List` or `ForEach`. This is no longer necessary + /// > if built with Xcode 14.3 and deployed to iOS 16.4 or later. So, it is not needed anymore. + /// > + /// > Enabling this may cause performance regression, especially if you have a lot of images to load in the view. + /// > Use it at your own risk. + /// > + /// > Please refer to [#1988](https://github.com/onevcat/Kingfisher/issues/1988) for more information. + public func startLoadingBeforeViewAppear(_ flag: Bool = true) -> Self { + context.startLoadingBeforeViewAppear = flag + return self + } +} +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift new file mode 100644 index 00000000..7b144fea --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift @@ -0,0 +1,128 @@ +// +// KFImageProtocol.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 Wei Wang +// +// 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + + +/// Represents a view that is compatible with Kingfisher in SwiftUI. +/// +/// As a framework user, you do not need to know the details of this protocol. As the public types, ``KFImage`` and +/// ``KFAnimatedImage`` conform this type and should be used in your app to represent an image view with network and +/// cache support in SwiftUI. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +@MainActor +public protocol KFImageProtocol: View, KFOptionSetter { + associatedtype HoldingView: KFImageHoldingView + var context: KFImage.Context { get set } + init(context: KFImage.Context) +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + @MainActor + public var body: some View { + ZStack { + KFImageRenderer( + context: context + ).id(context) + } + } + + /// Creates an image view compatible with Kingfisher for loading an image from the provided `Source`. + /// + /// - Parameters: + /// - source: The `Source` of the image that specifies where to load the target image. + public init(source: Source?) { + let context = KFImage.Context(source: source) + self.init(context: context) + } + + /// Creates an image view compatible with Kingfisher for loading an image from the provided `URL`. + /// + /// - Parameters: + /// - url: The `URL` defining the location from which to load the target image. + public init(_ url: URL?) { + self.init(source: url?.convertToSource()) + } + + /// Configures the current image with a `block` and returns another `Image` to use as the final content. + /// + /// This block will be lazily applied when creating the final `Image`. + /// + /// If multiple `configure` modifiers are added to the image, they will be evaluated in order. + /// + /// - Parameter block: The block that applies to the loaded image. The block should return an `Image` that is + /// configured. + /// - Returns: A ``KFImage`` or ``KFAnimatedImage`` view that configures the internal `Image` with the provided + /// `block`. + /// + /// > If you want to configure the input image (which is usually an `Image` value) and use a non-`Image` value as + /// > the configured result, use ``KFImageProtocol/contentConfigure(_:)`` instead. + public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self { + context.configurations.append(block) + return self + } + + /// Configures the current image with a `block` and returns a `View` to use as the final content. + /// + /// This block will be lazily applied when creating the final `Image`. + /// + /// If multiple `contentConfigure` modifiers are added to the image, only the last one will be stored and used. + /// + /// - Parameter block: The block applies to the loaded image. The block should return a `View` that is configured. + /// - Returns: A ``KFImage`` or ``KFAnimatedImage`` view that configures the internal `Image` with the provided + /// `block`. + public func contentConfigure(_ block: @escaping (HoldingView) -> V) -> Self { + context.contentConfiguration = { AnyView(block($0)) } + return self + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +@MainActor +public protocol KFImageHoldingView: View { + associatedtype RenderingView + static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Self +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + public var options: KingfisherParsedOptionsInfo { + get { context.options } + nonmutating set { context.options = newValue } + } + + public var onFailureDelegate: Delegate { context.onFailureDelegate } + public var onSuccessDelegate: Delegate { context.onSuccessDelegate } + public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.onProgressDelegate } + + public var delegateObserver: AnyObject { context } +} + + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift new file mode 100644 index 00000000..e526e8aa --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift @@ -0,0 +1,135 @@ +// +// KFImageRenderer.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 Wei Wang +// +// 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`. +/// Declaring a `KFImage` in a `View`'s body to trigger loading from the given `Source`. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFImageRenderer : View where HoldingView: KFImageHoldingView { + + @StateObject var binder: KFImage.ImageBinder = .init() + let context: KFImage.Context + + var body: some View { + if context.startLoadingBeforeViewAppear && !binder.loadingOrSucceeded && !binder.animating { + binder.markLoading() + DispatchQueue.main.async { binder.start(context: context) } + } + + return ZStack { + renderedImage().opacity(binder.loaded ? 1.0 : 0.0) + if binder.loadedImage == nil { + ZStack { + if let placeholder = context.placeholder { + placeholder(binder.progress) + } else { + Color.clear + } + } + .onAppear { [weak binder = self.binder] in + guard let binder = binder else { + return + } + if !binder.loadingOrSucceeded { + binder.start(context: context) + } else { + if context.reducePriorityOnDisappear { + binder.restorePriorityOnAppear() + } + } + } + .onDisappear { [weak binder = self.binder] in + guard let binder = binder else { + return + } + if context.cancelOnDisappear { + binder.cancel() + } else if context.reducePriorityOnDisappear { + binder.reducePriorityOnDisappear() + } + } + } + } + // Workaround for https://github.com/onevcat/Kingfisher/issues/1988 + // on iOS 16 there seems to be a bug that when in a List, the `onAppear` of the `ZStack` above in the + // `binder.loadedImage == nil` not get called. Adding this empty `onAppear` fixes it and the life cycle can + // work again. + // + // There is another "fix": adding an `else` clause and put a `Color.clear` there. But I believe this `onAppear` + // should work better. + // + // It should be a bug in iOS 16, I guess it is some kinds of over-optimization in list cell loading caused it. + .onAppear() + } + + @ViewBuilder + private func renderedImage() -> some View { + let configuredImage = context.configurations + .reduce(HoldingView.created(from: binder.loadedImage, context: context)) { + current, config in config(current) + } + if let contentConfiguration = context.contentConfiguration { + contentConfiguration(configuredImage) + } else { + configuredImage + } + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension Image { + // Creates an Image with either UIImage or NSImage. + init(crossPlatformImage: KFCrossPlatformImage?) { + #if canImport(UIKit) + self.init(uiImage: crossPlatformImage ?? KFCrossPlatformImage()) + #elseif canImport(AppKit) + self.init(nsImage: crossPlatformImage ?? KFCrossPlatformImage()) + #endif + } +} + +#if canImport(UIKit) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension UIImage.Orientation { + func toSwiftUI() -> Image.Orientation { + switch self { + case .down: return .down + case .up: return .up + case .left: return .left + case .right: return .right + case .upMirrored: return .upMirrored + case .downMirrored: return .downMirrored + case .leftMirrored: return .leftMirrored + case .rightMirrored: return .rightMirrored + @unknown default: return .up + } + } +} +#endif +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Box.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Box.swift new file mode 100644 index 00000000..df3d50a4 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Box.swift @@ -0,0 +1,45 @@ +// +// Box.swift +// Kingfisher +// +// Created by Wei Wang on 2018/3/17. +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +class Box { + var value: T + + init(_ value: T) { + self.value = value + } +} + +actor ActorBox { + var value: T + init(_ value: T) { + self.value = value + } + + func setValue(_ value: T) { + self.value = value + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift new file mode 100644 index 00000000..e91b6d72 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift @@ -0,0 +1,98 @@ +// +// CallbackQueue.swift +// Kingfisher +// +// Created by onevcat on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +public typealias ExecutionQueue = CallbackQueue + +/// Represents the behavior of the callback queue selection when a closure is dispatched. +public enum CallbackQueue: Sendable { + + /// Dispatches the closure to `DispatchQueue.main` with an `async` behavior. + case mainAsync + + /// Dispatches the closure to `DispatchQueue.main` with an `async` behavior if the current queue is not `.main`. + /// Otherwise, it calls the closure immediately on the current main queue. + case mainCurrentOrAsync + + /// Does not change the calling queue for the closure. + case untouch + + /// Dispatches the closure to a specified `DispatchQueue`. + case dispatch(DispatchQueue) + + /// Executes the `block` in a dispatch queue defined by `self`. + /// - Parameter block: The block needs to be executed. + public func execute(_ block: @Sendable @escaping () -> Void) { + switch self { + case .mainAsync: + CallbackQueueMain.async { block() } + case .mainCurrentOrAsync: + CallbackQueueMain.currentOrAsync { block() } + case .untouch: + block() + case .dispatch(let queue): + queue.async { block() } + } + } + + var queue: DispatchQueue { + switch self { + case .mainAsync: return .main + case .mainCurrentOrAsync: return .main + case .untouch: return OperationQueue.current?.underlyingQueue ?? .main + case .dispatch(let queue): return queue + } + } +} + +enum CallbackQueueMain { + static func currentOrAsync(_ block: @MainActor @Sendable @escaping () -> Void) { + if Thread.isMainThread { + MainActor.runUnsafely { block() } + } else { + DispatchQueue.main.async { block() } + } + } + + static func async(_ block: @MainActor @Sendable @escaping () -> Void) { + DispatchQueue.main.async { block() } + } +} + +extension MainActor { + @_unavailableFromAsync + static func runUnsafely(_ body: @MainActor () throws -> T) rethrows -> T { +#if swift(>=5.10) + return try MainActor.assumeIsolated(body) +#else + dispatchPrecondition(condition: .onQueue(.main)) + return try withoutActuallyEscaping(body) { fn in + try unsafeBitCast(fn, to: (() throws -> T).self)() + } +#endif + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Delegate.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Delegate.swift new file mode 100644 index 00000000..429f4592 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Delegate.swift @@ -0,0 +1,160 @@ +// +// Delegate.swift +// Kingfisher +// +// Created by onevcat on 2018/10/10. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +/// A class that maintains a weak reference to `self` when implementing `onXXX` behaviors. +/// Instead of manually ensuring that `self` is kept as weak in a stored closure: +/// +/// ```swift +/// // MyClass.swift +/// var onDone: (() -> Void)? +/// func done() { +/// onDone?() +/// } +/// +/// // ViewController.swift +/// var obj: MyClass? +/// +/// func doSomething() { +/// obj = MyClass() +/// obj!.onDone = { [weak self] in +/// self?.reportDone() +/// } +/// } +/// ``` +/// +/// You can create a `Delegate` and observe it on `self`. This ensures there is no retain cycle: +/// +/// ```swift +/// // MyClass.swift +/// let onDone = Delegate<(), Void>() +/// func done() { +/// onDone.call() +/// } +/// +/// // ViewController.swift +/// var obj: MyClass? +/// +/// func doSomething() { +/// obj = MyClass() +/// obj!.onDone.delegate(on: self) { (self, _) in +/// // The `self` here is shadowed and does not retain a strong reference. +/// // Thus, both the `MyClass` instance and the `ViewController` instance can be released. +/// self.reportDone() +/// } +/// } +/// +public class Delegate: @unchecked Sendable { + public init() {} + + private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.DelegateQueue") + + private var _block: ((Input) -> Output?)? + private var block: ((Input) -> Output?)? { + get { propertyQueue.sync { _block } } + set { propertyQueue.sync { _block = newValue } } + } + + private var _asyncBlock: ((Input) async -> Output?)? + private var asyncBlock: ((Input) async -> Output?)? { + get { propertyQueue.sync { _asyncBlock } } + set { propertyQueue.sync { _asyncBlock = newValue } } + } + + public func delegate(on target: T, block: ((T, Input) -> Output)?) { + self.block = { [weak target] input in + guard let target = target else { return nil } + return block?(target, input) + } + } + + public func delegate(on target: T, block: ((T, Input) async -> Output)?) { + self.asyncBlock = { [weak target] input in + guard let target = target else { return nil } + return await block?(target, input) + } + } + + public func call(_ input: Input) -> Output? { + return block?(input) + } + + public func callAsFunction(_ input: Input) -> Output? { + return call(input) + } + + public func callAsync(_ input: Input) async -> Output? { + return await asyncBlock?(input) + } + + public var isSet: Bool { + block != nil || asyncBlock != nil + } +} + +extension Delegate where Input == Void { + public func call() -> Output? { + return call(()) + } + + public func callAsFunction() -> Output? { + return call() + } +} + +extension Delegate where Input == Void, Output: OptionalProtocol { + public func call() -> Output { + return call(()) + } + + public func callAsFunction() -> Output { + return call() + } +} + +extension Delegate where Output: OptionalProtocol { + public func call(_ input: Input) -> Output { + if let result = block?(input) { + return result + } else { + return Output._createNil + } + } + + public func callAsFunction(_ input: Input) -> Output { + return call(input) + } +} + +public protocol OptionalProtocol { + static var _createNil: Self { get } +} +extension Optional : OptionalProtocol { + public static var _createNil: Optional { + return nil + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/DisplayLink.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/DisplayLink.swift new file mode 100644 index 00000000..61dc559e --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/DisplayLink.swift @@ -0,0 +1,178 @@ +// +// DisplayLink.swift +// Kingfisher +// +// Created by yeatse on 2024/1/9. +// +// Copyright (c) 2024 Wei Wang +// +// 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. + +#if !os(watchOS) +#if canImport(UIKit) +import UIKit +#else +import AppKit +import CoreVideo +#endif + +protocol DisplayLinkCompatible: AnyObject, Sendable { + var isPaused: Bool { get set } + + var preferredFramesPerSecond: NSInteger { get } + var timestamp: CFTimeInterval { get } + var duration: CFTimeInterval { get } + + func add(to runLoop: RunLoop, forMode mode: RunLoop.Mode) + func remove(from runLoop: RunLoop, forMode mode: RunLoop.Mode) + + func invalidate() +} + +#if !os(macOS) +extension UIView { + func compatibleDisplayLink(target: Any, selector: Selector) -> any DisplayLinkCompatible { + return CADisplayLink(target: target, selector: selector) + } +} + +#if compiler(>=6) +extension CADisplayLink: DisplayLinkCompatible, @retroactive @unchecked Sendable {} +#else +extension CADisplayLink: DisplayLinkCompatible, @unchecked Sendable {} +#endif + +#else +extension NSView { + func compatibleDisplayLink(target: Any, selector: Selector) -> any DisplayLinkCompatible { +#if swift(>=5.9) // macOS 14 SDK is included in Xcode 15, which comes with swift 5.9. Add this check to make old compilers happy. + if #available(macOS 14.0, *) { + return displayLink(target: target, selector: selector) + } else { + return DisplayLink(target: target, selector: selector) + } +#else + return DisplayLink(target: target, selector: selector) +#endif + } +} + +#if swift(>=5.9) +@available(macOS 14.0, *) +extension CADisplayLink: DisplayLinkCompatible { + var preferredFramesPerSecond: NSInteger { return 0 } +} +#if compiler(>=6) +@available(macOS 14.0, *) +extension CADisplayLink: @retroactive @unchecked Sendable { } +#else // compiler(>=6) +@available(macOS 14.0, *) +extension CADisplayLink: @unchecked Sendable { } +#endif // compiler(>=6) +#endif // swift(>=5.9) + +final class DisplayLink: DisplayLinkCompatible, @unchecked Sendable { + private var link: CVDisplayLink? + private var target: Any? + private var selector: Selector? + + private var schedulers: [RunLoop: [RunLoop.Mode]] = [:] + + var preferredFramesPerSecond: NSInteger = 0 + var timestamp: CFTimeInterval = 0 + var duration: CFTimeInterval = 0 + + init(target: Any, selector: Selector) { + self.target = target + self.selector = selector + CVDisplayLinkCreateWithActiveCGDisplays(&link) + if let link = link { + CVDisplayLinkSetOutputHandler(link) { displayLink, inNow, inOutputTime, flagsIn, flagsOut in + self.displayLinkCallback( + displayLink, inNow: inNow, inOutputTime: inOutputTime, flagsIn: flagsIn, flagsOut: flagsOut + ) + } + } + } + + deinit { + self.invalidate() + } + + private func displayLinkCallback(_ link: CVDisplayLink, + inNow: UnsafePointer, + inOutputTime: UnsafePointer, + flagsIn: CVOptionFlags, + flagsOut: UnsafeMutablePointer) -> CVReturn + { + let outputTime = inOutputTime.pointee + DispatchQueue.main.async { + guard let selector = self.selector, let target = self.target else { return } + if outputTime.videoTimeScale != 0 { + self.duration = CFTimeInterval(Double(outputTime.videoRefreshPeriod) / Double(outputTime.videoTimeScale)) + } + if self.timestamp != 0 { + for scheduler in self.schedulers { + scheduler.key.perform(selector, target: target, argument: nil, order: 0, modes: scheduler.value) + } + } + self.timestamp = CFTimeInterval(Double(outputTime.hostTime) / 1_000_000_000) + } + return kCVReturnSuccess + } + + var isPaused: Bool = true { + didSet { + guard let link = link else { return } + if isPaused { + if CVDisplayLinkIsRunning(link) { + CVDisplayLinkStop(link) + } + } else { + if !CVDisplayLinkIsRunning(link) { + CVDisplayLinkStart(link) + } + } + } + } + + func add(to runLoop: RunLoop, forMode mode: RunLoop.Mode) { + assert(runLoop == .main) + schedulers[runLoop, default: []].append(mode) + } + + func remove(from runLoop: RunLoop, forMode mode: RunLoop.Mode) { + schedulers[runLoop]?.removeAll { $0 == mode } + if let modes = schedulers[runLoop], modes.isEmpty { + schedulers.removeValue(forKey: runLoop) + } + } + + func invalidate() { + schedulers = [:] + isPaused = true + target = nil + selector = nil + if let link = link { + CVDisplayLinkSetOutputHandler(link) { _, _, _, _, _ in kCVReturnSuccess } + } + } +} +#endif +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift new file mode 100644 index 00000000..22b20638 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift @@ -0,0 +1,117 @@ +// +// ExtensionHelpers.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +extension CGFloat { + var isEven: Bool { + return truncatingRemainder(dividingBy: 2.0) == 0 + } +} + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +extension NSBezierPath { + convenience init(roundedRect rect: NSRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, + bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) + { + self.init() + + let maxCorner = min(rect.width, rect.height) / 2 + + let radiusTopLeft = min(maxCorner, max(0, topLeftRadius)) + let radiusTopRight = min(maxCorner, max(0, topRightRadius)) + let radiusBottomLeft = min(maxCorner, max(0, bottomLeftRadius)) + let radiusBottomRight = min(maxCorner, max(0, bottomRightRadius)) + + guard !rect.isEmpty else { + return + } + + let topLeft = NSPoint(x: rect.minX, y: rect.maxY) + let topRight = NSPoint(x: rect.maxX, y: rect.maxY) + let bottomRight = NSPoint(x: rect.maxX, y: rect.minY) + + move(to: NSPoint(x: rect.midX, y: rect.maxY)) + appendArc(from: topLeft, to: rect.origin, radius: radiusTopLeft) + appendArc(from: rect.origin, to: bottomRight, radius: radiusBottomLeft) + appendArc(from: bottomRight, to: topRight, radius: radiusBottomRight) + appendArc(from: topRight, to: topLeft, radius: radiusTopRight) + close() + } + + convenience init(roundedRect rect: NSRect, byRoundingCorners corners: RectCorner, radius: CGFloat) { + let radiusTopLeft = corners.contains(.topLeft) ? radius : 0 + let radiusTopRight = corners.contains(.topRight) ? radius : 0 + let radiusBottomLeft = corners.contains(.bottomLeft) ? radius : 0 + let radiusBottomRight = corners.contains(.bottomRight) ? radius : 0 + + self.init(roundedRect: rect, topLeftRadius: radiusTopLeft, topRightRadius: radiusTopRight, + bottomLeftRadius: radiusBottomLeft, bottomRightRadius: radiusBottomRight) + } +} + +extension KFCrossPlatformImage { + // macOS does not support scale. This is just for code compatibility across platforms. + convenience init?(data: Data, scale: CGFloat) { + self.init(data: data) + } +} +#endif + +#if canImport(UIKit) +import UIKit +extension RectCorner { + var uiRectCorner: UIRectCorner { + + var result: UIRectCorner = [] + + if contains(.topLeft) { result.insert(.topLeft) } + if contains(.topRight) { result.insert(.topRight) } + if contains(.bottomLeft) { result.insert(.bottomLeft) } + if contains(.bottomRight) { result.insert(.bottomRight) } + + return result + } +} +#endif + +extension Date { + var isPast: Bool { + return isPast(referenceDate: Date()) + } + + func isPast(referenceDate: Date) -> Bool { + return timeIntervalSince(referenceDate) <= 0 + } + + // `Date` in memory is a wrap for `TimeInterval`. But in file attribute it can only accept `Int` number. + // By default the system will `round` it. But it is not friendly for testing purpose. + // So we always `ceil` the value when used for file attributes. + var fileAttributeDate: Date { + return Date(timeIntervalSince1970: ceil(timeIntervalSince1970)) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Result.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Result.swift new file mode 100644 index 00000000..12c80a4d --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Result.swift @@ -0,0 +1,50 @@ +// +// Result.swift +// Kingfisher +// +// Created by onevcat on 2018/09/22. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +// These helper methods are not public since we do not want them to be exposed or cause any conflicting. +// However, they are just wrapper of `ResultUtil` static methods. +extension Result where Failure: Error { + + /// Evaluates the given transformation closures to create a single output value. + /// + /// - Parameters: + /// - onSuccess: A closure that transforms the success value. + /// - onFailure: A closure that transforms the error value. + /// - Returns: A single `Output` value. + func match( + onSuccess: (Success) -> Output, + onFailure: (Failure) -> Output) -> Output + { + switch self { + case let .success(value): + return onSuccess(value) + case let .failure(error): + return onFailure(error) + } + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Runtime.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Runtime.swift new file mode 100644 index 00000000..944de47a --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/Runtime.swift @@ -0,0 +1,39 @@ +// +// Runtime.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/12. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import Foundation + +func getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> T? { + if #available(iOS 14, macOS 11, watchOS 7, tvOS 14, *) { // swift 5.3 fixed this issue (https://github.com/swiftlang/swift/issues/46456) + return objc_getAssociatedObject(object, key) as? T + } else { + return objc_getAssociatedObject(object, key) as AnyObject as? T + } +} + +func setRetainedAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: T) { + objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift new file mode 100644 index 00000000..e5cd52e1 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift @@ -0,0 +1,111 @@ +// +// SizeExtensions.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +import CoreGraphics + +extension CGSize: KingfisherCompatibleValue {} +extension KingfisherWrapper where Base == CGSize { + + /// Returns a size by resizing the `base` size to a target size under a given content mode. + /// + /// - Parameters: + /// - size: The target size to resize to. + /// - contentMode: The content mode of the target size when resizing. + /// - Returns: The resized size under the given ``ContentMode``. + public func resize(to size: CGSize, for contentMode: ContentMode) -> CGSize { + switch contentMode { + case .aspectFit: + return constrained(size) + case .aspectFill: + return filling(size) + case .none: + return size + } + } + + /// Returns a size by resizing the `base` size to make it aspect-fit the given `size`. + /// + /// - Parameter size: The size in which the `base` should fit. + /// - Returns: The size that fits the `base` within the input `size`, while keeping the `base` aspect. + public func constrained(_ size: CGSize) -> CGSize { + let aspectWidth = round(aspectRatio * size.height) + let aspectHeight = round(size.width / aspectRatio) + + return aspectWidth > size.width ? + CGSize(width: size.width, height: aspectHeight) : + CGSize(width: aspectWidth, height: size.height) + } + + /// Returns a size by resizing the `base` size to make it aspect-fill the given `size`. + /// + /// - Parameter size: The size that the `base` should fill. + /// - Returns: The size filled by the input `size`, while keeping the `base` aspect. + public func filling(_ size: CGSize) -> CGSize { + let aspectWidth = round(aspectRatio * size.height) + let aspectHeight = round(size.width / aspectRatio) + + return aspectWidth < size.width ? + CGSize(width: size.width, height: aspectHeight) : + CGSize(width: aspectWidth, height: size.height) + } + + /// Returns a `CGRect` in which the `base` size is constrained to fit within a specified `size`, anchored at a + /// particular `anchor` point. + /// + /// - Parameters: + /// - size: The size to which the `base` should be constrained. + /// - anchor: The anchor point where the size constraint is applied. + /// - Returns: A `CGRect` that results from the constraint operation. + public func constrainedRect(for size: CGSize, anchor: CGPoint) -> CGRect { + + let unifiedAnchor = CGPoint(x: anchor.x.clamped(to: 0.0...1.0), + y: anchor.y.clamped(to: 0.0...1.0)) + + let x = unifiedAnchor.x * base.width - unifiedAnchor.x * size.width + let y = unifiedAnchor.y * base.height - unifiedAnchor.y * size.height + let r = CGRect(x: x, y: y, width: size.width, height: size.height) + + let ori = CGRect(origin: .zero, size: base) + return ori.intersection(r) + } + + private var aspectRatio: CGFloat { + return base.height == 0.0 ? 1.0 : base.width / base.height + } +} + +extension CGRect { + func scaled(_ scale: CGFloat) -> CGRect { + return CGRect(x: origin.x * scale, y: origin.y * scale, + width: size.width * scale, height: size.height * scale) + } +} + +extension Comparable { + func clamped(to limits: ClosedRange) -> Self { + return min(max(self, limits.lowerBound), limits.upperBound) + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/String+SHA256.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/String+SHA256.swift new file mode 100644 index 00000000..65738d65 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Utility/String+SHA256.swift @@ -0,0 +1,59 @@ +// +// String+SHA256.swift +// Kingfisher +// +// Created by kaimaschke on 28.07.23. +// +// Copyright (c) 2023 Wei Wang +// +// 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. + +import Foundation +import CryptoKit +import CommonCrypto + +extension String: KingfisherCompatibleValue { } +extension KingfisherWrapper where Base == String { + var sha256: String { + guard let data = base.data(using: .utf8) else { return base } + if #available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, macCatalyst 13.0, *) { + let hashed = SHA256.hash(data: data) + return hashed.compactMap { String(format: "%02x", $0) }.joined() + } else { + var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { bytes in + _ = CC_SHA256(bytes.baseAddress, UInt32(data.count), &digest) + } + return digest.makeIterator().compactMap { String(format: "%02x", $0) }.joined() + } + } + + var ext: String? { + guard let firstSeg = base.split(separator: "@").first else { + return nil + } + + var ext = "" + if let index = firstSeg.lastIndex(of: ".") { + let extRange = firstSeg.index(index, offsetBy: 1).. 0 ? ext : nil + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift new file mode 100644 index 00000000..ed646363 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift @@ -0,0 +1,861 @@ +// +// AnimatedImageView.swift +// Kingfisher +// +// Created by bl4ckra1sond3tre on 4/22/16. +// +// The AnimatedImageView, AnimatedFrame and Animator is a modified version of +// some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu) +// +// The MIT License (MIT) +// +// Copyright (c) 2019 Reda Lemeden. +// +// 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. +// +// The name and characters used in the demo of this software are property of their +// respective owners. + +#if !os(watchOS) +#if canImport(UIKit) +import UIKit +import ImageIO +typealias KFCrossPlatformContentMode = UIView.ContentMode +#elseif canImport(AppKit) +import AppKit +typealias KFCrossPlatformContentMode = NSImageScaling +#endif + +/// Delegate of the ``AnimatedImageView``. +/// +/// It reports back some events of the animated image view. +public protocol AnimatedImageViewDelegate: AnyObject { + + /// Called after the ``AnimatedImageView`` has finished each animation loop. + /// + /// - Parameters: + /// - imageView: The ``AnimatedImageView`` that is being animated. + /// - count: The loop count. + func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) + + /// Called after the ``AnimatedImageView`` has reached the maximum repeat count. + /// + /// - Parameter imageView: The ``AnimatedImageView`` that is being animated. + func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) +} + +extension AnimatedImageViewDelegate { + public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {} + public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {} +} + +let KFRunLoopModeCommon = RunLoop.Mode.common + +/// Represents a subclass of `UIImageView` for displaying animated images. +/// +/// Different from showing an animated image in a normal `UIImageView` (which loads all frames at one time), +/// ``AnimatedImageView`` only tries to load several frames (defined by ``AnimatedImageView/framePreloadCount``) to +/// reduce memory usage. It provides a tradeoff between memory usage and CPU time. If you have a memory issue when +/// using a normal image view to load GIF data, you could give this class a try. +/// +/// Kingfisher supports setting GIF animated data to either `UIImageView` or ``AnimatedImageView`` out of the box. So +/// it would be fairly easy to switch between them. +open class AnimatedImageView: KFCrossPlatformImageView { + /// Proxy object for preventing a reference cycle between the `CADDisplayLink` and `AnimatedImageView`. + class TargetProxy { + private weak var target: AnimatedImageView? + + init(target: AnimatedImageView) { + self.target = target + } + + @MainActor @objc func onScreenUpdate() { + target?.updateFrameIfNeeded() + } + } + + /// An enumeration that specifies the repeat count of a GIF. + public enum RepeatCount: Equatable { + /// The animated image should be only played once. + case once + /// The animated image should be played by a finite times defined in the associated value. + case finite(count: UInt) + /// The animated image should be played infinitely. + case infinite + + public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool { + switch (lhs, rhs) { + case let (.finite(l), .finite(r)): + return l == r + case (.once, .once), + (.infinite, .infinite): + return true + case (.once, .finite(let count)), + (.finite(let count), .once): + return count == 1 + case (.once, _), + (.infinite, _), + (.finite, _): + return false + } + } + } + + // MARK: - Public property + /// Whether to automatically play the animation when the view becomes visible. + /// + /// The default is `true`. + public var autoPlayAnimatedImage = true + + /// The count of frames that should be preloaded before being shown. + public var framePreloadCount = 10 + + /// Specifies whether the GIF frames should be pre-scaled to the image view's size or not. + /// + /// If the downloaded image is larger than the image view's size, it will help reduce some memory usage. + /// + /// The default is `true`. + public var needsPrescaling = true + + /// Decode the GIF frames in background thread before using. It will decode frames data and do a off-screen + /// rendering to extract pixel information in background. This can reduce the main thread CPU usage. + /// + @available(*, deprecated, message: """ + This property does not perform as declared and may lead to performance degradation. + It is currently obsolete and scheduled for removal in a future version. + """) + public var backgroundDecode = true + + /// The animation timer's run loop mode. The default is `RunLoop.Mode.common`. + /// + /// Setting this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling. + public var runLoopMode = KFRunLoopModeCommon { + willSet { + guard runLoopMode != newValue else { return } + stopAnimating() + displayLink.remove(from: .main, forMode: runLoopMode) + displayLink.add(to: .main, forMode: newValue) + startAnimating() + } + } + + /// The repeat count. The animated image will keep animating until the loop count reaches this value. + /// + /// Setting this value to another one will reset the current animation. + /// + /// The default is ``RepeatCount/infinite``, which means the animation will last forever. + public var repeatCount = RepeatCount.infinite { + didSet { + if oldValue != repeatCount { + reset() + #if os(macOS) + needsDisplay = true + layer?.setNeedsDisplay() + #else + setNeedsDisplay() + layer.setNeedsDisplay() + #endif + } + } + } + + /// The delegate of this `AnimatedImageView` object. + /// + /// See the ``AnimatedImageViewDelegate`` protocol for more information. + public weak var delegate: (any AnimatedImageViewDelegate)? + + /// The ``Animator`` instance that holds the frames of a specific image in memory. + public private(set) var animator: Animator? + + // MARK: - Private property + // Dispatch queue used for preloading images. + private lazy var preloadQueue: DispatchQueue = { + return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") + }() + + // A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. + private var isDisplayLinkInitialized: Bool = false + + // A display link that keeps calling the `updateFrame` method on every screen refresh. + private lazy var displayLink: any DisplayLinkCompatible = { + isDisplayLinkInitialized = true + let displayLink = self.compatibleDisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate)) + displayLink.add(to: .main, forMode: runLoopMode) + displayLink.isPaused = true + return displayLink + }() + + // MARK: - Override + @MainActor + override open var image: KFCrossPlatformImage? { + didSet { + if image != oldValue { + reset() + } + #if os(macOS) + needsDisplay = true + layer?.setNeedsDisplay() + #else + setNeedsDisplay() + layer.setNeedsDisplay() + #endif + } + } + + open override var isHighlighted: Bool { + get { + super.isHighlighted + } + set { + // Highlighted image is unsupported for animated images. + // See https://github.com/onevcat/Kingfisher/issues/1679 + if displayLink.isPaused { + super.isHighlighted = newValue + } + } + } + +// Workaround for Apple xcframework creating issue on Apple TV in Swift 5.8. +// https://github.com/swiftlang/swift/issues/66015 +#if os(tvOS) + public override init(image: UIImage?, highlightedImage: UIImage?) { + super.init(image: image, highlightedImage: highlightedImage) + } + + required public init?(coder: NSCoder) { + super.init(coder: coder) + } + + init() { + super.init(frame: .zero) + } +#endif + + deinit { + if isDisplayLinkInitialized { + // We have to assume this UIView deinit is called on main thread. + MainActor.runUnsafely { displayLink.invalidate() } + } + } + +#if os(macOS) + public override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + commonInit() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + super.animates = false + wantsLayer = true + } + + open override var animates: Bool { + get { + if isDisplayLinkInitialized { + return !displayLink.isPaused + } else { + return super.animates + } + } + set { + if newValue { + startAnimating() + } else { + stopAnimating() + } + } + } + + open func startAnimating() { + guard let animator = animator else { return } + guard !animator.isReachMaxRepeatCount else { return } + + displayLink.isPaused = false + } + + open func stopAnimating() { + if isDisplayLinkInitialized { + displayLink.isPaused = true + } + } + + open override var wantsUpdateLayer: Bool { + return true + } + + open override func updateLayer() { + if let frame = animator?.currentFrameImage ?? currentFrame, let layer = layer { + layer.contents = frame.kf.cgImage + layer.contentsScale = frame.kf.scale + layer.contentsGravity = determineContentsGravity(for: frame) + currentFrame = frame + } + } + + private func determineContentsGravity(for image: NSImage) -> CALayerContentsGravity { + switch imageScaling { + case .scaleProportionallyDown: + if image.size.width > bounds.width || image.size.height > bounds.height { + return .resizeAspect + } else { + return .center + } + case .scaleProportionallyUpOrDown: + return .resizeAspect + case .scaleAxesIndependently: + return .resize + case .scaleNone: + return .center + default: + return .resizeAspect + } + } + + open override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + didMove() + } + + open override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + didMove() + } +#else + override open var isAnimating: Bool { + if isDisplayLinkInitialized { + return !displayLink.isPaused + } else { + return super.isAnimating + } + } + + override open func startAnimating() { + guard !isAnimating else { return } + guard let animator = animator else { return } + guard !animator.isReachMaxRepeatCount else { return } + + displayLink.isPaused = false + } + + override open func stopAnimating() { + super.stopAnimating() + if isDisplayLinkInitialized { + displayLink.isPaused = true + } + } + + override open func display(_ layer: CALayer) { + layer.contents = animator?.currentFrameImage?.cgImage ?? image?.cgImage + } + + override open func didMoveToWindow() { + super.didMoveToWindow() + didMove() + } + + override open func didMoveToSuperview() { + super.didMoveToSuperview() + didMove() + } +#endif + + // This is for back compatibility that using regular `UIImageView` to show animated image. + override func shouldPreloadAllAnimation() -> Bool { + return false + } + + // Reset the animator. + private func reset() { + animator = nil + currentFrame = nil + if let image = image, let frameSource = image.kf.frameSource { + #if os(visionOS) + let scale = UITraitCollection.current.displayScale + #elseif os(macOS) + let scale = image.recommendedLayerContentsScale(window?.backingScaleFactor ?? 0.0) + let contentMode = imageScaling + #else + var scale: CGFloat = 0 + if #available(iOS 13.0, tvOS 13.0, *) { + scale = UITraitCollection.current.displayScale + } else { + scale = UIScreen.main.scale + } + #endif + currentFrame = image + let targetSize = bounds.scaled(scale).size + let animator = Animator( + frameSource: frameSource, + contentMode: contentMode, + size: targetSize, + imageSize: image.kf.size, + imageScale: image.kf.scale, + framePreloadCount: framePreloadCount, + repeatCount: repeatCount, + preloadQueue: preloadQueue) + animator.delegate = self + animator.needsPrescaling = needsPrescaling + animator.prepareFramesAsynchronously() + self.animator = animator + } + didMove() + } + + private func didMove() { + if autoPlayAnimatedImage && animator != nil { + if let _ = superview, let _ = window { + startAnimating() + } else { + stopAnimating() + } + } + } + + /// If the Animator cannot prepare the next frame in time, `animator.currentFrameImage` will return nil. + /// To prevent unexpected blinking in the ImageView, we maintain a cache of the currently displayed frame + /// to use as a fallback in such scenarios. + private var currentFrame: KFCrossPlatformImage? + + /// Update the current frame with the displayLink duration. + @MainActor + private func updateFrameIfNeeded() { + guard let animator = animator else { + return + } + + guard !animator.isFinished else { + stopAnimating() + delegate?.animatedImageViewDidFinishAnimating(self) + return + } + + let duration: CFTimeInterval + + // CA based display link is opt-out from ProMotion by default. + // So the duration and its FPS might not match. + // See [#718](https://github.com/onevcat/Kingfisher/issues/718) + // By setting CADisableMinimumFrameDuration to YES in Info.plist may + // cause the preferredFramesPerSecond being 0 + let preferredFramesPerSecond = displayLink.preferredFramesPerSecond + if preferredFramesPerSecond == 0 { + duration = displayLink.duration + } else { + // Some devices (like iPad Pro 10.5) will have a different FPS. + duration = 1.0 / TimeInterval(preferredFramesPerSecond) + } + + if animator.shouldChangeFrame(with: duration) { + #if os(macOS) + layer?.setNeedsDisplay() + #else + layer.setNeedsDisplay() + #endif + } + } +} + +@MainActor +protocol AnimatorDelegate: AnyObject { + func animator(_ animator: AnimatedImageView.Animator, didPlayAnimationLoops count: UInt) +} + +extension AnimatedImageView: AnimatorDelegate { + func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) { + delegate?.animatedImageView(self, didPlayAnimationLoops: count) + } +} + +extension AnimatedImageView { + + // Represents a single frame in a GIF. + struct AnimatedFrame { + + // The image to display for this frame. Its value is nil when the frame is removed from the buffer. + let image: KFCrossPlatformImage? + + // The duration that this frame should remain active. + let duration: TimeInterval + + // A placeholder frame with no image assigned. + // Used to replace frames that are no longer needed in the animation. + var placeholderFrame: AnimatedFrame { + return AnimatedFrame(image: nil, duration: duration) + } + + // Whether this frame instance contains an image or not. + var isPlaceholder: Bool { + return image == nil + } + + // Returns a new instance from an optional image. + // + // - parameter image: An optional `UIImage` instance to be assigned to the new frame. + // - returns: An `AnimatedFrame` instance. + func makeAnimatedFrame(image: KFCrossPlatformImage?) -> AnimatedFrame { + return AnimatedFrame(image: image, duration: duration) + } + } +} + +extension AnimatedImageView { + + // MARK: - Animator + + // TODO: Check the thread-safety of `Animator` for Sendable again. + /// An animator which is used to drive the data behind ``AnimatedImageView``. + public class Animator: @unchecked Sendable { + private let size: CGSize + + private let imageSize: CGSize + private let imageScale: CGFloat + + /// The maximum count of image frames that need to be preloaded. + public let maxFrameCount: Int + + private let frameSource: any ImageFrameSource + private let maxRepeatCount: RepeatCount + + private let maxTimeStep: TimeInterval = 1.0 + private let animatedFrames = SafeArray() + private var frameCount = 0 + private var timeSinceLastFrameChange: TimeInterval = 0.0 + private var currentRepeatCount: UInt = 0 + + var isFinished: Bool = false + + var needsPrescaling = true + + weak var delegate: (any AnimatorDelegate)? + + // Total duration of one animation loop + var loopDuration: TimeInterval = 0 + + /// The image of the current frame. + public var currentFrameImage: KFCrossPlatformImage? { + return frame(at: currentFrameIndex) + } + + /// The duration of the current active frame. + public var currentFrameDuration: TimeInterval { + return duration(at: currentFrameIndex) + } + + /// The index of the current animation frame. + public internal(set) var currentFrameIndex = 0 { + didSet { + previousFrameIndex = oldValue + } + } + + var previousFrameIndex = 0 { + didSet { + preloadQueue.async { + self.updatePreloadedFrames() + } + } + } + + var isReachMaxRepeatCount: Bool { + switch maxRepeatCount { + case .once: + return currentRepeatCount >= 1 + case .finite(let maxCount): + return currentRepeatCount >= maxCount + case .infinite: + return false + } + } + + /// Whether the current frame is the last frame or not in the animation sequence. + public var isLastFrame: Bool { + return currentFrameIndex == frameCount - 1 + } + + var preloadingIsNeeded: Bool { + return maxFrameCount < frameCount - 1 + } + + #if os(macOS) + var contentMode = NSImageScaling.scaleAxesIndependently + #else + var contentMode = UIView.ContentMode.scaleToFill + #endif + + private lazy var preloadQueue: DispatchQueue = { + return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") + }() + + /// Creates an animator with image source reference. + /// + /// - Parameters: + /// - source: The reference of animated image. + /// - mode: Content mode of the `AnimatedImageView`. + /// - size: Size of the `AnimatedImageView`. + /// - imageSize: Size of the `KingfisherWrapper`. + /// - imageScale: Scale of the `KingfisherWrapper`. + /// - count: Count of frames needed to be preloaded. + /// - repeatCount: The repeat count should this animator uses. + /// - preloadQueue: Dispatch queue used for preloading images. + convenience init(imageSource source: CGImageSource, + contentMode mode: KFCrossPlatformContentMode, + size: CGSize, + imageSize: CGSize, + imageScale: CGFloat, + framePreloadCount count: Int, + repeatCount: RepeatCount, + preloadQueue: DispatchQueue) { + let frameSource = CGImageFrameSource(data: nil, imageSource: source, options: nil) + self.init(frameSource: frameSource, + contentMode: mode, + size: size, + imageSize: imageSize, + imageScale: imageScale, + framePreloadCount: count, + repeatCount: repeatCount, + preloadQueue: preloadQueue) + } + + /// Creates an animator with a custom image frame source. + /// + /// - Parameters: + /// - frameSource: The reference of animated image. + /// - mode: Content mode of the `AnimatedImageView`. + /// - size: Size of the `AnimatedImageView`. + /// - imageSize: Size of the `KingfisherWrapper`. + /// - imageScale: Scale of the `KingfisherWrapper`. + /// - count: Count of frames needed to be preloaded. + /// - repeatCount: The repeat count should this animator uses. + /// - preloadQueue: Dispatch queue used for preloading images. + init(frameSource source: any ImageFrameSource, + contentMode mode: KFCrossPlatformContentMode, + size: CGSize, + imageSize: CGSize, + imageScale: CGFloat, + framePreloadCount count: Int, + repeatCount: RepeatCount, + preloadQueue: DispatchQueue) { + self.frameSource = source.copy() + self.contentMode = mode + self.size = size + self.imageSize = imageSize + self.imageScale = imageScale + self.maxFrameCount = count + self.maxRepeatCount = repeatCount + self.preloadQueue = preloadQueue + } + + /// Gets the image frame of a given index. + /// - Parameter index: The index of the desired image. + /// - Returns: The decoded image at the frame. `nil` if the index is out of bounds or the image is not yet loaded. + public func frame(at index: Int) -> KFCrossPlatformImage? { + return animatedFrames[index]?.image + } + + /// Gets the duration of an image for the given frame index. + /// - Parameter index: The index of the desired image. + /// - Returns: The duration of that frame. + public func duration(at index: Int) -> TimeInterval { + return animatedFrames[index]?.duration ?? .infinity + } + + func prepareFramesAsynchronously() { + frameCount = frameSource.frameCount + animatedFrames.reserveCapacity(frameCount) + preloadQueue.async { [weak self] in + self?.setupAnimatedFrames() + } + } + + @MainActor + func shouldChangeFrame(with duration: CFTimeInterval) -> Bool { + incrementTimeSinceLastFrameChange(with: duration) + + if currentFrameDuration > timeSinceLastFrameChange { + return false + } else { + resetTimeSinceLastFrameChange() + incrementCurrentFrameIndex() + return true + } + } + + private func setupAnimatedFrames() { + resetAnimatedFrames() + + var duration: TimeInterval = 0 + + (0.. maxFrameCount { return } + animatedFrames[index] = animatedFrames[index]?.makeAnimatedFrame(image: loadFrame(at: index)) + } + + self.loopDuration = duration + } + + private func resetAnimatedFrames() { + animatedFrames.removeAll() + } + + private func loadFrame(at index: Int) -> KFCrossPlatformImage? { + let resize = needsPrescaling && size != .zero + let maxSize = resize ? size : nil + guard let cgImage = frameSource.frame(at: index, maxSize: maxSize) else { + return nil + } + + #if os(macOS) + return KFCrossPlatformImage(cgImage: cgImage, size: .zero) + #else + if #available(iOS 15, tvOS 15, *) { + // From iOS 15, a plain image loading causes iOS calling `-[_UIImageCGImageContent initWithCGImage:scale:]` + // in ImageIO, which holds the image ref on the creating thread. + // To get a workaround, create another image ref and use that to create the final image. This leads to + // some performance loss, but there is little we can do. + // https://github.com/onevcat/Kingfisher/issues/1844 + // https://github.com/onevcat/Kingfisher/pulls/2194 + guard let unretainedImage = CGImage.create(ref: cgImage) else { + return KFCrossPlatformImage(cgImage: cgImage) + } + + return KFCrossPlatformImage(cgImage: unretainedImage).preparingForDisplay() + } else { + return KFCrossPlatformImage(cgImage: cgImage) + } + #endif + } + + private func updatePreloadedFrames() { + guard preloadingIsNeeded else { + return + } + + let previousFrame = animatedFrames[previousFrameIndex] + animatedFrames[previousFrameIndex] = previousFrame?.placeholderFrame + // ensure the image dealloc in main thread + defer { + if let image = previousFrame?.image { + DispatchQueue.main.async { + _ = image + } + } + } + + preloadIndexes(start: currentFrameIndex).forEach { index in + guard let currentAnimatedFrame = animatedFrames[index] else { return } + if !currentAnimatedFrame.isPlaceholder { return } + animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index)) + } + } + + @MainActor private func incrementCurrentFrameIndex() { + let wasLastFrame = isLastFrame + currentFrameIndex = increment(frameIndex: currentFrameIndex) + if isLastFrame { + currentRepeatCount += 1 + if isReachMaxRepeatCount { + isFinished = true + + // Notify the delegate here because the animation is stopping. + delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) + } + } else if wasLastFrame { + + // Notify the delegate that the loop completed + delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) + } + } + + private func incrementTimeSinceLastFrameChange(with duration: TimeInterval) { + timeSinceLastFrameChange += min(maxTimeStep, duration) + } + + private func resetTimeSinceLastFrameChange() { + timeSinceLastFrameChange -= currentFrameDuration + } + + private func increment(frameIndex: Int, by value: Int = 1) -> Int { + return (frameIndex + value) % frameCount + } + + private func preloadIndexes(start index: Int) -> [Int] { + let nextIndex = increment(frameIndex: index) + let lastIndex = increment(frameIndex: index, by: maxFrameCount) + + if lastIndex >= nextIndex { + return [Int](nextIndex...lastIndex) + } else { + return [Int](nextIndex.. { + private var array: Array = [] + private let lock = NSLock() + + subscript(index: Int) -> Element? { + get { + lock.lock() + defer { lock.unlock() } + return array.indices ~= index ? array[index] : nil + } + + set { + lock.lock() + defer { lock.unlock() } + if let newValue = newValue, array.indices ~= index { + array[index] = newValue + } + } + } + + var count : Int { + lock.lock() + defer { lock.unlock() } + return array.count + } + + func reserveCapacity(_ count: Int) { + lock.lock() + defer { lock.unlock() } + array.reserveCapacity(count) + } + + func append(_ element: Element) { + lock.lock() + defer { lock.unlock() } + array += [element] + } + + func removeAll() { + lock.lock() + defer { lock.unlock() } + array = [] + } +} +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Views/Indicator.swift b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Views/Indicator.swift new file mode 100644 index 00000000..3e4f207a --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Kingfisher/Sources/Views/Indicator.swift @@ -0,0 +1,248 @@ +// +// Indicator.swift +// Kingfisher +// +// Created by João D. Moreira on 30/08/16. +// +// Copyright (c) 2019 Wei Wang +// +// 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. + +#if !os(watchOS) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +public typealias IndicatorView = NSView +#else +import UIKit +public typealias IndicatorView = UIView +#endif + +/// Represents the activity indicator type that should be added to an image view when an image is being downloaded. +public enum IndicatorType { + + /// No indicator. + case none + + /// Uses the system activity indicator. + case activity + + /// Uses an image as an indicator. GIF is supported. + case image(imageData: Data) + + /// Uses a custom indicator. + /// + /// The type of the associated value should conform to the ``Indicator`` protocol. + case custom(indicator: any Indicator) +} + +/// An indicator type which can be used to show that the download task is in progress. +@MainActor +public protocol Indicator: Sendable { + + /// Called when the indicator should start animating. + func startAnimatingView() + + /// Called when the indicator should stop animating. + func stopAnimatingView() + + /// Center offset of the indicator. + /// + /// Kingfisher will use this value to determine the position of the indicator in the superview. + var centerOffset: CGPoint { get } + + /// The indicator view which would be added to the superview. + var view: IndicatorView { get } + + /// The size strategy used when adding the indicator to the image view. + /// - Parameter imageView: The superview of the indicator. + /// - Returns: An ``IndicatorSizeStrategy`` that determines how the indicator should be sized. + func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy +} + +/// The idicator size strategy used when sizing the indicator in the image view. +public enum IndicatorSizeStrategy { + /// Uses the intrinsic size of the indicator. + case intrinsicSize + /// Match the size of the super view of the indicator. + case full + /// Uses the associated `CGSize` to set the indicator size. + case size(CGSize) +} + +extension Indicator { + + /// Default implementation of ``Indicator/centerOffset-7jxdw`` of the ``Indicator``. + /// + /// The default value is `.zero`, which means that there is no offset for the indicator view. + public var centerOffset: CGPoint { + .zero + } + + /// Default implementation of ``Indicator/sizeStrategy(in:)-5x0b4`` of the ``Indicator``. + /// + /// The default value is ``IndicatorSizeStrategy/full``, means that the indicator will pin to the same height and + /// width as the image view. + /// - Parameter imageView: The image view which holds the indicator. + /// - Returns: The desired ``IndicatorSizeStrategy`` + public func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { + .full + } +} + +// Displays a NSProgressIndicator / UIActivityIndicatorView +@MainActor +final class ActivityIndicator: Indicator { + + #if os(macOS) + private let activityIndicatorView: NSProgressIndicator + #else + private let activityIndicatorView: UIActivityIndicatorView + #endif + private var animatingCount = 0 + + var view: IndicatorView { + return activityIndicatorView + } + + func startAnimatingView() { + if animatingCount == 0 { + #if os(macOS) + activityIndicatorView.startAnimation(nil) + #else + activityIndicatorView.startAnimating() + #endif + activityIndicatorView.isHidden = false + } + animatingCount += 1 + } + + func stopAnimatingView() { + animatingCount = max(animatingCount - 1, 0) + if animatingCount == 0 { + #if os(macOS) + activityIndicatorView.stopAnimation(nil) + #else + activityIndicatorView.stopAnimating() + #endif + activityIndicatorView.isHidden = true + } + } + + func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { + return .intrinsicSize + } + + init() { + #if os(macOS) + activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) + activityIndicatorView.controlSize = .small + activityIndicatorView.style = .spinning + #else + let indicatorStyle: UIActivityIndicatorView.Style + + #if os(tvOS) + if #available(tvOS 13.0, *) { + indicatorStyle = UIActivityIndicatorView.Style.large + } else { + indicatorStyle = UIActivityIndicatorView.Style.white + } + #elseif os(visionOS) + indicatorStyle = UIActivityIndicatorView.Style.medium + #else + if #available(iOS 13.0, * ) { + indicatorStyle = UIActivityIndicatorView.Style.medium + } else { + indicatorStyle = UIActivityIndicatorView.Style.gray + } + #endif + + activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle) + #endif + } +} + +#if canImport(UIKit) +extension UIActivityIndicatorView.Style { + #if compiler(>=5.1) + #else + static let large = UIActivityIndicatorView.Style.white + #if !os(tvOS) + static let medium = UIActivityIndicatorView.Style.gray + #endif + #endif +} +#endif + +// MARK: - ImageIndicator +// Displays an ImageView. Supports gif +final class ImageIndicator: Indicator { + private let animatedImageIndicatorView: KFCrossPlatformImageView + + var view: IndicatorView { + return animatedImageIndicatorView + } + + init?( + imageData data: Data, + processor: any ImageProcessor = DefaultImageProcessor.default, + options: KingfisherParsedOptionsInfo? = nil) + { + var options = options ?? KingfisherParsedOptionsInfo(nil) + // Use normal image view to show animations, so we need to preload all animation data. + if !options.preloadAllAnimationData { + options.preloadAllAnimationData = true + } + + guard let image = processor.process(item: .data(data), options: options) else { + return nil + } + + animatedImageIndicatorView = KFCrossPlatformImageView() + animatedImageIndicatorView.image = image + + #if os(macOS) + // Need for gif to animate on macOS + animatedImageIndicatorView.imageScaling = .scaleNone + animatedImageIndicatorView.canDrawSubviewsIntoLayer = true + #else + animatedImageIndicatorView.contentMode = .center + #endif + } + + func startAnimatingView() { + #if os(macOS) + animatedImageIndicatorView.animates = true + #else + animatedImageIndicatorView.startAnimating() + #endif + animatedImageIndicatorView.isHidden = false + } + + func stopAnimatingView() { + #if os(macOS) + animatedImageIndicatorView.animates = false + #else + animatedImageIndicatorView.stopAnimating() + #endif + animatedImageIndicatorView.isHidden = true + } +} + +#endif diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Local Podspecs/SDCycleScrollView.podspec.json b/SwiftDemo/SDCycleSrollViewDemo/Pods/Local Podspecs/SDCycleScrollView.podspec.json new file mode 100644 index 00000000..9c791d0f --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Local Podspecs/SDCycleScrollView.podspec.json @@ -0,0 +1,44 @@ +{ + "name": "SDCycleScrollView", + "version": "1.82", + "summary": "简单易用的图片无限轮播器. 1.82版本更新内容:修复iOS14上系统自带pagecontrol显示不出来bug", + "homepage": "https://github.com/gsdios/SDCycleScrollView", + "license": "MIT", + "authors": { + "GSD_iOS": "gsdios@126.com" + }, + "platforms": { + "ios": "11.0" + }, + "source": { + "git": "https://github.com/gsdios/SDCycleScrollView.git", + "tag": "1.82" + }, + "requires_arc": true, + "default_subspecs": "Core", + "swift_versions": [ + "5.0" + ], + "subspecs": [ + { + "name": "Core", + "source_files": "SDCycleScrollView/Lib/SDCycleScrollView/**/*.{h,m}", + "dependencies": { + "SDWebImage": [ + ">= 5.0.0" + ] + } + }, + { + "name": "SwiftNOSD", + "source_files": [ + "SDCycleScrollView/Lib/SDCycleScrollView/**/*.{h,m}", + "SDCycleScrollView/Lib/SDCycleScrollView/**/*.swift" + ], + "dependencies": { + "Kingfisher": [] + } + } + ], + "swift_version": "5.0" +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Manifest.lock b/SwiftDemo/SDCycleSrollViewDemo/Pods/Manifest.lock new file mode 100644 index 00000000..f89365ea --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Manifest.lock @@ -0,0 +1,23 @@ +PODS: + - Kingfisher (8.3.3) + - SDCycleScrollView/SwiftNOSD (1.82): + - Kingfisher + +DEPENDENCIES: + - SDCycleScrollView/SwiftNOSD (from `../../`) + +SPEC REPOS: + trunk: + - Kingfisher + +EXTERNAL SOURCES: + SDCycleScrollView: + :path: "../../" + +SPEC CHECKSUMS: + Kingfisher: ff82cb91d9266ddb56cbb2f72d32c26f00d3e5be + SDCycleScrollView: 357d9df5ad77352f6e36b9c94ab8e34f0eba1513 + +PODFILE CHECKSUM: 43fcfd2a94ca076de4190913c33da62fffa2a25f + +COCOAPODS: 1.16.2 diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/project.pbxproj b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 00000000..49a23a8d --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,1326 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 013D0B0A8786F5B34F039FB3704E12AB /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E2B17600C7A345991B9AA15A7C35582 /* Indicator.swift */; }; + 019E2E60370D97793F78BFA5B82E7F70 /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA268B90610EC9332A023E0652EBC0BE /* ImageProgressive.swift */; }; + 0217BD2D535BBE17D651A40D117783C5 /* Kingfisher-Kingfisher in Resources */ = {isa = PBXBuildFile; fileRef = C298ABB78D9B05529B89D8322DB2E7B0 /* Kingfisher-Kingfisher */; }; + 104898BA44C8449A302C7E2F8620DAEB /* Pods-SDCycleSrollViewDemo-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 8CA8E94E503AB54105C70F29695898A6 /* Pods-SDCycleSrollViewDemo-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 12CB26635E98555061F17851DBFCBA45 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A3BFAF850432D08D1C6D10D5BA02F14 /* Foundation.framework */; }; + 17B994AB4C64CCCD4C944087FB56BB32 /* SDCollectionViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D0CD87524DC0C02263D3876547A19E1 /* SDCollectionViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 183189655EA72780910B9A83E8208BC4 /* TADotView.m in Sources */ = {isa = PBXBuildFile; fileRef = E760997E58D6213236EF01ADFDC5EF04 /* TADotView.m */; }; + 1C4D430B867251402232CD36BA776B17 /* ImageView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 155818C90F023801A94628C7D6A82B08 /* ImageView+Kingfisher.swift */; }; + 1D74158DCC81C4E83C0A8945EDE69C86 /* SizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E09CE59C2E4A707801BA1699B64C1AD /* SizeExtensions.swift */; }; + 1DE5A65531D35FBB1E317481CD316342 /* ImageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9040AF0C1319C45E038E43519E9BBB0 /* ImageModifier.swift */; }; + 25C27C7AEBD561EBF70CFC413F4CD755 /* TAAnimatedDotView.m in Sources */ = {isa = PBXBuildFile; fileRef = CA24FD4EB19F02F46938A28177F81A9A /* TAAnimatedDotView.m */; }; + 2703C0DC58FB460C01ADE8E8D22C9F62 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BDE2F9B149AD480FEBCC4023C85806 /* SessionDataTask.swift */; }; + 2D8245FE7A22ED672D3DF95E77A96E46 /* UIButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A941E03CBAE776400D3FAF9A46CCFE8 /* UIButton+Kingfisher.swift */; }; + 32D1F8F5EFDA36D786DD8CA2D1DFC732 /* SDCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 86A4F3BFB02311B52B33A9760EE23EEF /* SDCollectionViewCell.m */; }; + 34EAFE241E4CAFF1BE5D8D7AC6EDD1B8 /* TAAnimatedDotView.h in Headers */ = {isa = PBXBuildFile; fileRef = 7DAE4BC5188A1176F0A73F2ED6BC75FA /* TAAnimatedDotView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3CF9247B7112973208E98C5B8A3EFD08 /* ImageDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA49DFE81879C1CF8A37DC2652F6515D /* ImageDrawing.swift */; }; + 3F749E95FFA3640649B8252D9B2CFC10 /* KFImageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC9CB7A52E863154B2924D9E3837FDC /* KFImageProtocol.swift */; }; + 420ADE5F222F9DB15F15F59991EEFDC1 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 721CC8813F811F38062E2DC12C356423 /* CacheSerializer.swift */; }; + 420C49E6734283B87E7D206694344112 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A3BFAF850432D08D1C6D10D5BA02F14 /* Foundation.framework */; }; + 442F3EF780ACE70BAF124C9E3E5E8E15 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C90E96D8B2C8DF15E8E42AC6E13ABD /* ImageDataProcessor.swift */; }; + 4BFEF1EA0FD8911E9EF55A4484F7F1E6 /* KF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D584A19F29BD30693DEC1F332EB4F2F /* KF.swift */; }; + 4C016BE1DC259AB2A379AC81DF581C49 /* SDCycleScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E048F1CAE639581C7BCE5863399A4CE /* SDCycleScrollView.m */; }; + 54FCB2A0877468FCA03245D4FBB0F9D2 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0493054DB65F299F9EF390FA362C44AF /* ImageCache.swift */; }; + 576C5FD65E8E35A7EF189EBB20CE01C6 /* LivePhotoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54454B75E9B89D7455492F797CD55490 /* LivePhotoSource.swift */; }; + 588426DC3CDADF722F9363F76E77B835 /* TADotView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B5214A60F9C1964EC7EEF076A82A4D2 /* TADotView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 59E1BF0AB3F19C50E578D398E26EB227 /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C741AF2FD004ADC53D21318D6A58EF6C /* KFAnimatedImage.swift */; }; + 5DF2E03715129AA0C77ADD8BAFFBDDFC /* KingfisherOptionsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088A6991D69CFD4C9CC370EC49D40307 /* KingfisherOptionsInfo.swift */; }; + 5E6BFEA8AF2EE6BEEAEB1BE2346A33AE /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 650D0CDEB1097DF8DF7ED97736C0BAF0 /* Documentation.docc */; }; + 5EE23E0B4A7175D504F4C7E20505DDE7 /* KFImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8302757A435AAF85003AF57DC70CD843 /* KFImageRenderer.swift */; }; + 5F09107998BED88EC6FCDFC5305031B1 /* String+SHA256.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AAC734F320AEFC0A79730431548512 /* String+SHA256.swift */; }; + 6659D83530A30CBBC03DC8CBA9366062 /* SDCycleScrollView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A9621F6AB328AE5820D7D1D2816A1E8 /* SDCycleScrollView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 66E4088C9C894EB11899240BC3F5A3C7 /* TAPageControl.h in Headers */ = {isa = PBXBuildFile; fileRef = DCF2742416B59B7BCEA502D4761D5DD8 /* TAPageControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 678EDDCD6ED116BC5100C5924A9BB0C6 /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F9AD5A832789E211525CE279FF4D099 /* DiskStorage.swift */; }; + 6C71098ECC662C8B37BDA8243820F070 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0835F411AD796930557E6D6FD2D393A6 /* Accelerate.framework */; }; + 6CAB59AA86D67BD46B82E326058258A1 /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 119E0D751FF4848197C9CBCBFD54278D /* ImageDownloaderDelegate.swift */; }; + 6CABA18CE8DB786EFDA231AF69A7D4B2 /* KFImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EDFF538F75518BF577FE7A6B6F23D6 /* KFImage.swift */; }; + 6DA8B916CBDCDABB31D0DB7A0926CB55 /* SDCycleScrollView-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 50065895D82A55BECDCA8C0F2AFBACC7 /* SDCycleScrollView-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7168A1F86699F21734478EC9596F0D74 /* NSTextAttachment+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92E55CD65C474E2E8E54D591AB7E8F41 /* NSTextAttachment+Kingfisher.swift */; }; + 77F2E509688A124F02FD040516154751 /* GraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A105C7E11616A4F871DA64B13B03C648 /* GraphicsContext.swift */; }; + 799EAD4947C3902EA490A7282B738C25 /* AVAssetImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8F6C4C1958434B423D9D400A4366D4E /* AVAssetImageDataProvider.swift */; }; + 7A2DDEA900883E39D33468469FC14AEB /* ImageDownloader+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF7BFB9771159D81EF8C9333335D237 /* ImageDownloader+LivePhoto.swift */; }; + 7D98386A54F28BA3F27CBC8334D0CADA /* Pods-SDCycleSrollViewDemo-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 137DCF57626054B77775E42E0C2BA8E4 /* Pods-SDCycleSrollViewDemo-dummy.m */; }; + 7FDE5BBEB2E3E5E79A686FB6F496E566 /* KingfisherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BF51D75A1D7CF174DEC319B717C9E5 /* KingfisherError.swift */; }; + 88E76DE06F81A6B173F029A37BD0BE7F /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71F8832F0E3C04949146C7474CE031C /* Runtime.swift */; }; + 89C457ACFD60011F394C5BD42800BDC6 /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86EDC7AE0ED35F162F025AD406A91D7 /* ImagePrefetcher.swift */; }; + 8B7560712BC596AD4790CDCB38D629BC /* Kingfisher-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 0AFC9EA03DAB681E0715BF92EDD2A735 /* Kingfisher-dummy.m */; }; + 919299610C2CA6AB6B58C902F9FA91F5 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09AF893526C785EFD23D179CA2A86759 /* AnimatedImageView.swift */; }; + 9198B564D6DF28D1F6AC58E1E068607A /* TAPageControl.m in Sources */ = {isa = PBXBuildFile; fileRef = DD05D9AF7DA7FFF7C618E11BB1D5126A /* TAPageControl.m */; }; + 92A43BFC165F5595D1CF24AD2C65D6C0 /* ImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82CEE4C7A2EC49613D78115E9722FC7B /* ImageDataProvider.swift */; }; + 92D50FCEBF77BFACA64AB481BF6EB687 /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE6891D619E7BAD9069B7828102C291 /* CallbackQueue.swift */; }; + 9534119EDE1FB3F9DECE76D8130D6397 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF622FA5D10ACC4E837A454A8D17E36 /* Storage.swift */; }; + 990A4E31ADF23A8BC1C5044ABD45B979 /* ImageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA46282C19C34798D334579E48903844 /* ImageContext.swift */; }; + 9981C32E56FF09688D9FBCBF45FB93B2 /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31CDCCDB90175D14F2FB958365EE308 /* NSButton+Kingfisher.swift */; }; + 9E4FDDFBA5C3856E7C540B560910C2C0 /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1683A06B1CDA5014470D609382D85A8 /* CPListItem+Kingfisher.swift */; }; + A1B70BEB9DCBE081F5B39F596F2596C5 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA079D53D3957F27C01640994AFDBDE1 /* RedirectHandler.swift */; }; + A2D48CC012DADC29CC924512A5F4513B /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1145E8A94DDE8391F3FAE545D4948D05 /* CFNetwork.framework */; }; + A7E562FC9C10C5003534E91160C69330 /* Kingfisher-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 8369F9C9E9BE220D765E2F61F1A46EF4 /* Kingfisher-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7FF322DF74002F7C9F855A5F95B166A /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6E83F91A43F9187E5162107B8409090 /* ImageFormat.swift */; }; + AB1F732806BA88499D9556A1DF9C7147 /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 727D3A7D27282403AF9D7939AEE852D2 /* DisplayLink.swift */; }; + AD860938D35585A99549790933C165E5 /* HasImageComponent+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35CB00E19921B56505D287E03603DF61 /* HasImageComponent+Kingfisher.swift */; }; + AE1B484501E4ECD99787770A6545775E /* SDCycleScrollView-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B3F7386065BCFC1FE7C8AEBE7F3BF31 /* SDCycleScrollView-dummy.m */; }; + AEF253E11EE8937E3A5ABC5922DDF289 /* UIView+SDExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = CB047120502ADF821B937FEE15D06FCE /* UIView+SDExtension.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AF1D366BDF7AA8C015A0283550C197AB /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E292D71D89B9147EAA550A1F58CC6FB5 /* ImageDownloader.swift */; }; + AF34FDA62958AD154C005C924C808165 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 703C928F054A329427FDFAAFAEB6C157 /* Delegate.swift */; }; + AFE232EEDAC5B035CDB331A5CEA18E32 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866DDAAFAB87B93333D5EBDA00D61377 /* Placeholder.swift */; }; + B01BF1940094224E6A48D83E53BA6AC8 /* ImageBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B722F196FAA4116C77E5668969C9F72 /* ImageBinder.swift */; }; + B126EF2264809CA8F7A70EB1F227030B /* ExtensionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83DDEDA09F47013DD4844B67277E2D4F /* ExtensionHelpers.swift */; }; + B13477F1A5A0A88616148C1205D8EC29 /* TAAbstractDotView.m in Sources */ = {isa = PBXBuildFile; fileRef = 611C9292D3D0D977C87178C1341D39AC /* TAAbstractDotView.m */; }; + B1F7B96741F92ADC2F98417FFC33DB96 /* PHLivePhotoView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B341DD334EE921BAE33DD0EE99890BB9 /* PHLivePhotoView+Kingfisher.swift */; }; + BB8CAA589F0F85C385509FEC4F65DCE6 /* UIImageView+KF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF85BF23361A3CFD4A9178D3CB06556 /* UIImageView+KF.swift */; }; + C10B1478C4AB3693D886D94ADFAEFDFB /* KFImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA15FADA159925DA5B7F647988750CC /* KFImageOptions.swift */; }; + C2A21354D5DB755E780A9ABC1752E74A /* PHPickerResultImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58165CEAC074F84BB6C04DAE85F6694F /* PHPickerResultImageDataProvider.swift */; }; + C4754374D4DAE6B06A96F2920989D911 /* TAAbstractDotView.h in Headers */ = {isa = PBXBuildFile; fileRef = 341EB2B318A479F2EC5CDBC14F6D4344 /* TAAbstractDotView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C59E3FD52D5408C08FDC0E6FE6266C3B /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C15430CA3CDC10040305E2E58824CEC /* Source.swift */; }; + CC63E9DDAC7230E3EA8ECB73B9E0DA13 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8718F88D170D177F8B0E6232DE630FB7 /* Filter.swift */; }; + D548E4E0D9121E18DA31F2150791F9AA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81DB7AA223A72FB28AE16D52395FF971 /* Image.swift */; }; + DB17C9319D2226DB80AFB60844D61209 /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9793A0714B13CE1E984C9374014D1752 /* MemoryStorage.swift */; }; + DDEDEE8B19E94E46A2D70C78127236E5 /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92F740DB14C8A11B5071110CC6CEA113 /* Kingfisher.swift */; }; + DDF61BEA9AFF4E1CFD53D4B84D119DF4 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7155C1450AF4C012F7F7D475C17E46 /* AuthenticationChallengeResponsable.swift */; }; + E274BA92F9C6AA938EF04CE7EA38CEAD /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41AB8FE22E1CBB75C162F9AC7AEB1310 /* RequestModifier.swift */; }; + E7D06F2DBFA16E219746D0E11E60A501 /* KFOptionsSetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFE377AB83F00D5C992C6496C9EFC21 /* KFOptionsSetter.swift */; }; + E9AEBBA488E8832DA6CBE9881DF9DE95 /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D636476819E91DCF512663277AD05056 /* ImageProcessor.swift */; }; + E9C2DC4FA2FFD17B934AC840DB7F7799 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C053CED8760749D077840E910D307068 /* PrivacyInfo.xcprivacy */; }; + EAAA0041F6A0DB1039D328B35FF5EA09 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497915090ABEF76673CD7B888BE455F2 /* SessionDelegate.swift */; }; + EE1368EC3C3660565A82A9842E57124F /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52DC76A10E877360B29DA1E4E2085999 /* Box.swift */; }; + EE4E733231F8656BB065F4FBA5793FF8 /* SDSwiftCycleScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515CF0BCE420016D38B2950FC4147250 /* SDSwiftCycleScrollView.swift */; }; + F0B0B551A13101A1F5906D38E5863E8A /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9D9FE4308C75E6CD57F08EA223FA41 /* Resource.swift */; }; + F469D3446C362E18EF1F7D714E2E64E0 /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9758A207D7BADEF5C130D8D6933FE61D /* KingfisherManager.swift */; }; + F510058AE05060128BA765E991B787E2 /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5B59F022ECBE3A0E291927F7279670 /* ImageTransition.swift */; }; + F5E62DAD60E5C311FE1EC1333D44F194 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F33A6A87BEAEC452B66E51172CE523 /* FormatIndicatedCacheSerializer.swift */; }; + F819B1541A0195B89BEBAB3CCC51DCC6 /* RetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7BD9C5BEFA3742345FC3410A7EE3D5 /* RetryStrategy.swift */; }; + FA190258BD504A02FD26844DF48C5FAF /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770BCFB2284AB21643F892B69643E65 /* Result.swift */; }; + FA1D2FDB9DE836CEF3DAD8FB122D67B6 /* UIView+SDExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CD6E246215955CE7D4E27EED78C764 /* UIView+SDExtension.m */; }; + FD08990C503D242E5A70B010B704C755 /* KingfisherManager+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3B376C93B00DDDF2549BB513A4BDCC /* KingfisherManager+LivePhoto.swift */; }; + FE9BA3316B14AC042881E3601745E44A /* GIFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1626175E5A5CC9D5E26F40AAE33EFA0 /* GIFAnimatedImage.swift */; }; + FEBA8B25086C2BB38C74F5F748F001F2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A3BFAF850432D08D1C6D10D5BA02F14 /* Foundation.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 43956C431C0DB174165BE8B6923B7A80 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = CBED833AAD6266F3AEFE9BE31C68E094; + remoteInfo = SDCycleScrollView; + }; + 4B1BD609E38336B14B1A7C81F0212402 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9828BBC09E9FB1238624113D7456E59E; + remoteInfo = "Kingfisher-Kingfisher"; + }; + 7C0DEB136D71DA321B593C1BB8EFE59C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E8022D22FAA6690B5E1C379C1BCE1491; + remoteInfo = Kingfisher; + }; + F87F72A9CB4AE83D6B0E63FD060D3A1F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E8022D22FAA6690B5E1C379C1BCE1491; + remoteInfo = Kingfisher; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 024F0799DFFCAD855FED55AE5F947FD9 /* SDCycleScrollView.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SDCycleScrollView.debug.xcconfig; sourceTree = ""; }; + 0493054DB65F299F9EF390FA362C44AF /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/Cache/ImageCache.swift; sourceTree = ""; }; + 0835F411AD796930557E6D6FD2D393A6 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Accelerate.framework; sourceTree = DEVELOPER_DIR; }; + 088A6991D69CFD4C9CC370EC49D40307 /* KingfisherOptionsInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherOptionsInfo.swift; path = Sources/General/KingfisherOptionsInfo.swift; sourceTree = ""; }; + 09AF893526C785EFD23D179CA2A86759 /* AnimatedImageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedImageView.swift; path = Sources/Views/AnimatedImageView.swift; sourceTree = ""; }; + 0A941E03CBAE776400D3FAF9A46CCFE8 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIButton+Kingfisher.swift"; path = "Sources/Extensions/UIButton+Kingfisher.swift"; sourceTree = ""; }; + 0AFC9EA03DAB681E0715BF92EDD2A735 /* Kingfisher-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Kingfisher-dummy.m"; sourceTree = ""; }; + 0E09CE59C2E4A707801BA1699B64C1AD /* SizeExtensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SizeExtensions.swift; path = Sources/Utility/SizeExtensions.swift; sourceTree = ""; }; + 0EE6891D619E7BAD9069B7828102C291 /* CallbackQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CallbackQueue.swift; path = Sources/Utility/CallbackQueue.swift; sourceTree = ""; }; + 0EF622FA5D10ACC4E837A454A8D17E36 /* Storage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Storage.swift; path = Sources/Cache/Storage.swift; sourceTree = ""; }; + 1145E8A94DDE8391F3FAE545D4948D05 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; }; + 119E0D751FF4848197C9CBCBFD54278D /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloaderDelegate.swift; path = Sources/Networking/ImageDownloaderDelegate.swift; sourceTree = ""; }; + 11F82C874A277B48A172935DFE86483D /* Pods-SDCycleSrollViewDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SDCycleSrollViewDemo.debug.xcconfig"; sourceTree = ""; }; + 137DCF57626054B77775E42E0C2BA8E4 /* Pods-SDCycleSrollViewDemo-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-SDCycleSrollViewDemo-dummy.m"; sourceTree = ""; }; + 155818C90F023801A94628C7D6A82B08 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ImageView+Kingfisher.swift"; path = "Sources/Extensions/ImageView+Kingfisher.swift"; sourceTree = ""; }; + 17AAC734F320AEFC0A79730431548512 /* String+SHA256.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String+SHA256.swift"; path = "Sources/Utility/String+SHA256.swift"; sourceTree = ""; }; + 19C90E96D8B2C8DF15E8E42AC6E13ABD /* ImageDataProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProcessor.swift; path = Sources/Networking/ImageDataProcessor.swift; sourceTree = ""; }; + 1B3F7386065BCFC1FE7C8AEBE7F3BF31 /* SDCycleScrollView-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SDCycleScrollView-dummy.m"; sourceTree = ""; }; + 1E2AEDEC24FCC884BD06DF0400D86241 /* Kingfisher-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-prefix.pch"; sourceTree = ""; }; + 27CD6E246215955CE7D4E27EED78C764 /* UIView+SDExtension.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIView+SDExtension.m"; path = "SDCycleScrollView/Lib/SDCycleScrollView/UIView+SDExtension.m"; sourceTree = ""; }; + 2AF85BF23361A3CFD4A9178D3CB06556 /* UIImageView+KF.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIImageView+KF.swift"; path = "SDCycleScrollView/Lib/SDCycleScrollView/UIImageView+KF.swift"; sourceTree = ""; }; + 2BF7BFB9771159D81EF8C9333335D237 /* ImageDownloader+LivePhoto.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ImageDownloader+LivePhoto.swift"; path = "Sources/Networking/ImageDownloader+LivePhoto.swift"; sourceTree = ""; }; + 2E9D9FE4308C75E6CD57F08EA223FA41 /* Resource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Resource.swift; path = Sources/General/ImageSource/Resource.swift; sourceTree = ""; }; + 341EB2B318A479F2EC5CDBC14F6D4344 /* TAAbstractDotView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = TAAbstractDotView.h; sourceTree = ""; }; + 35CB00E19921B56505D287E03603DF61 /* HasImageComponent+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "HasImageComponent+Kingfisher.swift"; path = "Sources/Extensions/HasImageComponent+Kingfisher.swift"; sourceTree = ""; }; + 3B722F196FAA4116C77E5668969C9F72 /* ImageBinder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageBinder.swift; path = Sources/SwiftUI/ImageBinder.swift; sourceTree = ""; }; + 3D0CD87524DC0C02263D3876547A19E1 /* SDCollectionViewCell.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDCollectionViewCell.h; path = SDCycleScrollView/Lib/SDCycleScrollView/SDCollectionViewCell.h; sourceTree = ""; }; + 3FA15FADA159925DA5B7F647988750CC /* KFImageOptions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageOptions.swift; path = Sources/SwiftUI/KFImageOptions.swift; sourceTree = ""; }; + 41AB8FE22E1CBB75C162F9AC7AEB1310 /* RequestModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestModifier.swift; path = Sources/Networking/RequestModifier.swift; sourceTree = ""; }; + 48196E8A2F320585E031F229E5F76B0F /* Pods-SDCycleSrollViewDemo-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-SDCycleSrollViewDemo-Info.plist"; sourceTree = ""; }; + 497915090ABEF76673CD7B888BE455F2 /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Sources/Networking/SessionDelegate.swift; sourceTree = ""; }; + 4A3B376C93B00DDDF2549BB513A4BDCC /* KingfisherManager+LivePhoto.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "KingfisherManager+LivePhoto.swift"; path = "Sources/General/KingfisherManager+LivePhoto.swift"; sourceTree = ""; }; + 4A9621F6AB328AE5820D7D1D2816A1E8 /* SDCycleScrollView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDCycleScrollView.h; path = SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.h; sourceTree = ""; }; + 4B5214A60F9C1964EC7EEF076A82A4D2 /* TADotView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = TADotView.h; sourceTree = ""; }; + 4C15430CA3CDC10040305E2E58824CEC /* Source.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Source.swift; path = Sources/General/ImageSource/Source.swift; sourceTree = ""; }; + 50065895D82A55BECDCA8C0F2AFBACC7 /* SDCycleScrollView-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SDCycleScrollView-umbrella.h"; sourceTree = ""; }; + 515CF0BCE420016D38B2950FC4147250 /* SDSwiftCycleScrollView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SDSwiftCycleScrollView.swift; path = SDCycleScrollView/Lib/SDCycleScrollView/SDSwiftCycleScrollView.swift; sourceTree = ""; }; + 52DC76A10E877360B29DA1E4E2085999 /* Box.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Box.swift; path = Sources/Utility/Box.swift; sourceTree = ""; }; + 53BC042143F20D062EAC307186393CB3 /* Kingfisher.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.debug.xcconfig; sourceTree = ""; }; + 54454B75E9B89D7455492F797CD55490 /* LivePhotoSource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LivePhotoSource.swift; path = Sources/General/ImageSource/LivePhotoSource.swift; sourceTree = ""; }; + 58165CEAC074F84BB6C04DAE85F6694F /* PHPickerResultImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PHPickerResultImageDataProvider.swift; path = Sources/General/ImageSource/PHPickerResultImageDataProvider.swift; sourceTree = ""; }; + 5C059A6815ABC498E0FC68C01A4C3415 /* Kingfisher.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Kingfisher.modulemap; sourceTree = ""; }; + 5CF908FA7A531A297954B16394DC1EAF /* Pods-SDCycleSrollViewDemo */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-SDCycleSrollViewDemo"; path = Pods_SDCycleSrollViewDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 611C9292D3D0D977C87178C1341D39AC /* TAAbstractDotView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = TAAbstractDotView.m; sourceTree = ""; }; + 63EDFF538F75518BF577FE7A6B6F23D6 /* KFImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImage.swift; path = Sources/SwiftUI/KFImage.swift; sourceTree = ""; }; + 650D0CDEB1097DF8DF7ED97736C0BAF0 /* Documentation.docc */ = {isa = PBXFileReference; includeInIndex = 1; name = Documentation.docc; path = Sources/Documentation.docc; sourceTree = ""; }; + 703C928F054A329427FDFAAFAEB6C157 /* Delegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Delegate.swift; path = Sources/Utility/Delegate.swift; sourceTree = ""; }; + 721CC8813F811F38062E2DC12C356423 /* CacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CacheSerializer.swift; path = Sources/Cache/CacheSerializer.swift; sourceTree = ""; }; + 727D3A7D27282403AF9D7939AEE852D2 /* DisplayLink.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DisplayLink.swift; path = Sources/Utility/DisplayLink.swift; sourceTree = ""; }; + 73DA22E071C96E19748840D7C2A689B2 /* SDCycleScrollView.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SDCycleScrollView.modulemap; sourceTree = ""; }; + 7770BCFB2284AB21643F892B69643E65 /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Sources/Utility/Result.swift; sourceTree = ""; }; + 7A3BFAF850432D08D1C6D10D5BA02F14 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 7DAE4BC5188A1176F0A73F2ED6BC75FA /* TAAnimatedDotView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = TAAnimatedDotView.h; sourceTree = ""; }; + 7E048F1CAE639581C7BCE5863399A4CE /* SDCycleScrollView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDCycleScrollView.m; path = SDCycleScrollView/Lib/SDCycleScrollView/SDCycleScrollView.m; sourceTree = ""; }; + 7F9AD5A832789E211525CE279FF4D099 /* DiskStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DiskStorage.swift; path = Sources/Cache/DiskStorage.swift; sourceTree = ""; }; + 7FE4205E6E4DD87BF8828A905B4FA703 /* ResourceBundle-Kingfisher-Kingfisher-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-Kingfisher-Kingfisher-Info.plist"; sourceTree = ""; }; + 81DB7AA223A72FB28AE16D52395FF971 /* Image.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.swift; path = Sources/Image/Image.swift; sourceTree = ""; }; + 82CEE4C7A2EC49613D78115E9722FC7B /* ImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProvider.swift; path = Sources/General/ImageSource/ImageDataProvider.swift; sourceTree = ""; }; + 8302757A435AAF85003AF57DC70CD843 /* KFImageRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageRenderer.swift; path = Sources/SwiftUI/KFImageRenderer.swift; sourceTree = ""; }; + 8369F9C9E9BE220D765E2F61F1A46EF4 /* Kingfisher-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-umbrella.h"; sourceTree = ""; }; + 83DDEDA09F47013DD4844B67277E2D4F /* ExtensionHelpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExtensionHelpers.swift; path = Sources/Utility/ExtensionHelpers.swift; sourceTree = ""; }; + 866DDAAFAB87B93333D5EBDA00D61377 /* Placeholder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Placeholder.swift; path = Sources/Image/Placeholder.swift; sourceTree = ""; }; + 86A4F3BFB02311B52B33A9760EE23EEF /* SDCollectionViewCell.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDCollectionViewCell.m; path = SDCycleScrollView/Lib/SDCycleScrollView/SDCollectionViewCell.m; sourceTree = ""; }; + 8718F88D170D177F8B0E6232DE630FB7 /* Filter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Filter.swift; path = Sources/Image/Filter.swift; sourceTree = ""; }; + 893A28D81696D6330FBC61237061981E /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE; sourceTree = ""; }; + 8B6CF5C20C32EE9F7F0862FF892524DE /* SDCycleScrollView */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SDCycleScrollView; path = SDCycleScrollView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8CA8E94E503AB54105C70F29695898A6 /* Pods-SDCycleSrollViewDemo-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-SDCycleSrollViewDemo-umbrella.h"; sourceTree = ""; }; + 8CD8F2AB433203CC87D3AC5C6BEDD15C /* Pods-SDCycleSrollViewDemo-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SDCycleSrollViewDemo-frameworks.sh"; sourceTree = ""; }; + 8E2B17600C7A345991B9AA15A7C35582 /* Indicator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Indicator.swift; path = Sources/Views/Indicator.swift; sourceTree = ""; }; + 920D31D615D8C0B848AC4E3682BC504D /* Pods-SDCycleSrollViewDemo-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-SDCycleSrollViewDemo-acknowledgements.plist"; sourceTree = ""; }; + 92E55CD65C474E2E8E54D591AB7E8F41 /* NSTextAttachment+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSTextAttachment+Kingfisher.swift"; path = "Sources/Extensions/NSTextAttachment+Kingfisher.swift"; sourceTree = ""; }; + 92F740DB14C8A11B5071110CC6CEA113 /* Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Kingfisher.swift; path = Sources/General/Kingfisher.swift; sourceTree = ""; }; + 9758A207D7BADEF5C130D8D6933FE61D /* KingfisherManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherManager.swift; path = Sources/General/KingfisherManager.swift; sourceTree = ""; }; + 9793A0714B13CE1E984C9374014D1752 /* MemoryStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MemoryStorage.swift; path = Sources/Cache/MemoryStorage.swift; sourceTree = ""; }; + 9C06040DCF060D3E4458FD23C4D04233 /* SDCycleScrollView.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SDCycleScrollView.release.xcconfig; sourceTree = ""; }; + 9D584A19F29BD30693DEC1F332EB4F2F /* KF.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KF.swift; path = Sources/General/KF.swift; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 9E7BD9C5BEFA3742345FC3410A7EE3D5 /* RetryStrategy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryStrategy.swift; path = Sources/Networking/RetryStrategy.swift; sourceTree = ""; }; + A105C7E11616A4F871DA64B13B03C648 /* GraphicsContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GraphicsContext.swift; path = Sources/Image/GraphicsContext.swift; sourceTree = ""; }; + A1683A06B1CDA5014470D609382D85A8 /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CPListItem+Kingfisher.swift"; path = "Sources/Extensions/CPListItem+Kingfisher.swift"; sourceTree = ""; }; + A54130D361CDE65C7FCE7B7AEEE8E428 /* Pods-SDCycleSrollViewDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SDCycleSrollViewDemo.release.xcconfig"; sourceTree = ""; }; + AA079D53D3957F27C01640994AFDBDE1 /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Sources/Networking/RedirectHandler.swift; sourceTree = ""; }; + AA46282C19C34798D334579E48903844 /* ImageContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageContext.swift; path = Sources/SwiftUI/ImageContext.swift; sourceTree = ""; }; + B341DD334EE921BAE33DD0EE99890BB9 /* PHLivePhotoView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "PHLivePhotoView+Kingfisher.swift"; path = "Sources/Extensions/PHLivePhotoView+Kingfisher.swift"; sourceTree = ""; }; + B3BF51D75A1D7CF174DEC319B717C9E5 /* KingfisherError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherError.swift; path = Sources/General/KingfisherError.swift; sourceTree = ""; }; + B9420292B9769FED1D6F8711A051A6DD /* Kingfisher.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.release.xcconfig; sourceTree = ""; }; + BE7A6EE3713C50D6FD1FEEA0586D8E50 /* SDCycleScrollView-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SDCycleScrollView-Info.plist"; sourceTree = ""; }; + BF5B59F022ECBE3A0E291927F7279670 /* ImageTransition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageTransition.swift; path = Sources/Image/ImageTransition.swift; sourceTree = ""; }; + C053CED8760749D077840E910D307068 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Sources/PrivacyInfo.xcprivacy; sourceTree = ""; }; + C29203D370C8D13366A705688ABE9AAA /* Pods-SDCycleSrollViewDemo.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-SDCycleSrollViewDemo.modulemap"; sourceTree = ""; }; + C298ABB78D9B05529B89D8322DB2E7B0 /* Kingfisher-Kingfisher */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = "Kingfisher-Kingfisher"; path = Kingfisher.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + C3BDE2F9B149AD480FEBCC4023C85806 /* SessionDataTask.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDataTask.swift; path = Sources/Networking/SessionDataTask.swift; sourceTree = ""; }; + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Kingfisher; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C66163B02F5485E62884BA732C256F40 /* Pods-SDCycleSrollViewDemo-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-SDCycleSrollViewDemo-acknowledgements.markdown"; sourceTree = ""; }; + C71F8832F0E3C04949146C7474CE031C /* Runtime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Runtime.swift; path = Sources/Utility/Runtime.swift; sourceTree = ""; }; + C741AF2FD004ADC53D21318D6A58EF6C /* KFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFAnimatedImage.swift; path = Sources/SwiftUI/KFAnimatedImage.swift; sourceTree = ""; }; + CA24FD4EB19F02F46938A28177F81A9A /* TAAnimatedDotView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = TAAnimatedDotView.m; sourceTree = ""; }; + CAC9CB7A52E863154B2924D9E3837FDC /* KFImageProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageProtocol.swift; path = Sources/SwiftUI/KFImageProtocol.swift; sourceTree = ""; }; + CB047120502ADF821B937FEE15D06FCE /* UIView+SDExtension.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIView+SDExtension.h"; path = "SDCycleScrollView/Lib/SDCycleScrollView/UIView+SDExtension.h"; sourceTree = ""; }; + D1626175E5A5CC9D5E26F40AAE33EFA0 /* GIFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GIFAnimatedImage.swift; path = Sources/Image/GIFAnimatedImage.swift; sourceTree = ""; }; + D31CDCCDB90175D14F2FB958365EE308 /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSButton+Kingfisher.swift"; path = "Sources/Extensions/NSButton+Kingfisher.swift"; sourceTree = ""; }; + D636476819E91DCF512663277AD05056 /* ImageProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProcessor.swift; path = Sources/Image/ImageProcessor.swift; sourceTree = ""; }; + D86EDC7AE0ED35F162F025AD406A91D7 /* ImagePrefetcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImagePrefetcher.swift; path = Sources/Networking/ImagePrefetcher.swift; sourceTree = ""; }; + DA49DFE81879C1CF8A37DC2652F6515D /* ImageDrawing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDrawing.swift; path = Sources/Image/ImageDrawing.swift; sourceTree = ""; }; + DA8961E7907C3F9151A69250E58146AE /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = ""; }; + DCF2742416B59B7BCEA502D4761D5DD8 /* TAPageControl.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = TAPageControl.h; sourceTree = ""; }; + DD05D9AF7DA7FFF7C618E11BB1D5126A /* TAPageControl.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = TAPageControl.m; sourceTree = ""; }; + E292D71D89B9147EAA550A1F58CC6FB5 /* ImageDownloader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloader.swift; path = Sources/Networking/ImageDownloader.swift; sourceTree = ""; }; + E2F33A6A87BEAEC452B66E51172CE523 /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FormatIndicatedCacheSerializer.swift; path = Sources/Cache/FormatIndicatedCacheSerializer.swift; sourceTree = ""; }; + E760997E58D6213236EF01ADFDC5EF04 /* TADotView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = TADotView.m; sourceTree = ""; }; + E8F6C4C1958434B423D9D400A4366D4E /* AVAssetImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AVAssetImageDataProvider.swift; path = Sources/General/ImageSource/AVAssetImageDataProvider.swift; sourceTree = ""; }; + E9040AF0C1319C45E038E43519E9BBB0 /* ImageModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageModifier.swift; path = Sources/Networking/ImageModifier.swift; sourceTree = ""; }; + EDBE48AD49064E2D7B99418609D70957 /* SDCycleScrollView-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SDCycleScrollView-prefix.pch"; sourceTree = ""; }; + F2FB654EA77A598B70374157AF17D073 /* SDCycleScrollView.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = SDCycleScrollView.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + F5F089F10DF87678E7A78B7C47480EAA /* Kingfisher-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Kingfisher-Info.plist"; sourceTree = ""; }; + F6E83F91A43F9187E5162107B8409090 /* ImageFormat.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageFormat.swift; path = Sources/Image/ImageFormat.swift; sourceTree = ""; }; + FA268B90610EC9332A023E0652EBC0BE /* ImageProgressive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProgressive.swift; path = Sources/Image/ImageProgressive.swift; sourceTree = ""; }; + FB7155C1450AF4C012F7F7D475C17E46 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationChallengeResponsable.swift; path = Sources/Networking/AuthenticationChallengeResponsable.swift; sourceTree = ""; }; + FCFE377AB83F00D5C992C6496C9EFC21 /* KFOptionsSetter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFOptionsSetter.swift; path = Sources/General/KFOptionsSetter.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 04C139C841E5A69C3B73E4E7788DB3C3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6C71098ECC662C8B37BDA8243820F070 /* Accelerate.framework in Frameworks */, + A2D48CC012DADC29CC924512A5F4513B /* CFNetwork.framework in Frameworks */, + 420C49E6734283B87E7D206694344112 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5E949F9F341F81DAB038FB5070293A02 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FEBA8B25086C2BB38C74F5F748F001F2 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 987389D579A7E84C638BFFF6CBCC48E8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 998291F38E5F490E4F0074FC23149450 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 12CB26635E98555061F17851DBFCBA45 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03C5C200A0787E300053CFA8F53CA094 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A79BE4628FF0D743431D06580DF47AE5 /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; + 2C49200257795985A235CE462744CCF9 /* SwiftNOSD */ = { + isa = PBXGroup; + children = ( + 3D0CD87524DC0C02263D3876547A19E1 /* SDCollectionViewCell.h */, + 86A4F3BFB02311B52B33A9760EE23EEF /* SDCollectionViewCell.m */, + 4A9621F6AB328AE5820D7D1D2816A1E8 /* SDCycleScrollView.h */, + 7E048F1CAE639581C7BCE5863399A4CE /* SDCycleScrollView.m */, + 515CF0BCE420016D38B2950FC4147250 /* SDSwiftCycleScrollView.swift */, + 2AF85BF23361A3CFD4A9178D3CB06556 /* UIImageView+KF.swift */, + CB047120502ADF821B937FEE15D06FCE /* UIView+SDExtension.h */, + 27CD6E246215955CE7D4E27EED78C764 /* UIView+SDExtension.m */, + 337372642D4569F0315D77C8D24D22C0 /* PageControl */, + ); + name = SwiftNOSD; + sourceTree = ""; + }; + 337372642D4569F0315D77C8D24D22C0 /* PageControl */ = { + isa = PBXGroup; + children = ( + 341EB2B318A479F2EC5CDBC14F6D4344 /* TAAbstractDotView.h */, + 611C9292D3D0D977C87178C1341D39AC /* TAAbstractDotView.m */, + 7DAE4BC5188A1176F0A73F2ED6BC75FA /* TAAnimatedDotView.h */, + CA24FD4EB19F02F46938A28177F81A9A /* TAAnimatedDotView.m */, + 4B5214A60F9C1964EC7EEF076A82A4D2 /* TADotView.h */, + E760997E58D6213236EF01ADFDC5EF04 /* TADotView.m */, + DCF2742416B59B7BCEA502D4761D5DD8 /* TAPageControl.h */, + DD05D9AF7DA7FFF7C618E11BB1D5126A /* TAPageControl.m */, + ); + name = PageControl; + path = SDCycleScrollView/Lib/SDCycleScrollView/PageControl; + sourceTree = ""; + }; + 61B448B79208C42D7A21CFF4DDDA475E /* Resources */ = { + isa = PBXGroup; + children = ( + C053CED8760749D077840E910D307068 /* PrivacyInfo.xcprivacy */, + ); + name = Resources; + sourceTree = ""; + }; + 78D7360455FF649EAD9ACE5D468B487F /* Pods */ = { + isa = PBXGroup; + children = ( + C468ABE76962E0BE2929432C1C0FEAED /* Kingfisher */, + ); + name = Pods; + sourceTree = ""; + }; + 7F0439ECD3E836A199562DEC71D4888E /* Development Pods */ = { + isa = PBXGroup; + children = ( + 812B2DFBECB147CE0BE0B84FCE09787E /* SDCycleScrollView */, + ); + name = "Development Pods"; + sourceTree = ""; + }; + 812B2DFBECB147CE0BE0B84FCE09787E /* SDCycleScrollView */ = { + isa = PBXGroup; + children = ( + 8237F1D73FBB69E3F9581834EC9F89F9 /* Pod */, + C022D73960E7E90D0E05C6E45C832BE7 /* Support Files */, + 2C49200257795985A235CE462744CCF9 /* SwiftNOSD */, + ); + name = SDCycleScrollView; + path = ../../..; + sourceTree = ""; + }; + 81FF8BDA67FD1B5AE0CF892246415389 /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + D6283149C2E8531AEB014C77EF7C618F /* Pods-SDCycleSrollViewDemo */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; + 8237F1D73FBB69E3F9581834EC9F89F9 /* Pod */ = { + isa = PBXGroup; + children = ( + 893A28D81696D6330FBC61237061981E /* LICENSE */, + DA8961E7907C3F9151A69250E58146AE /* README.md */, + F2FB654EA77A598B70374157AF17D073 /* SDCycleScrollView.podspec */, + ); + name = Pod; + sourceTree = ""; + }; + 82ABC8DC035038E29AC7AAE1B2AF317D /* Support Files */ = { + isa = PBXGroup; + children = ( + 5C059A6815ABC498E0FC68C01A4C3415 /* Kingfisher.modulemap */, + 0AFC9EA03DAB681E0715BF92EDD2A735 /* Kingfisher-dummy.m */, + F5F089F10DF87678E7A78B7C47480EAA /* Kingfisher-Info.plist */, + 1E2AEDEC24FCC884BD06DF0400D86241 /* Kingfisher-prefix.pch */, + 8369F9C9E9BE220D765E2F61F1A46EF4 /* Kingfisher-umbrella.h */, + 53BC042143F20D062EAC307186393CB3 /* Kingfisher.debug.xcconfig */, + B9420292B9769FED1D6F8711A051A6DD /* Kingfisher.release.xcconfig */, + 7FE4205E6E4DD87BF8828A905B4FA703 /* ResourceBundle-Kingfisher-Kingfisher-Info.plist */, + ); + name = "Support Files"; + path = "../Target Support Files/Kingfisher"; + sourceTree = ""; + }; + A79BE4628FF0D743431D06580DF47AE5 /* iOS */ = { + isa = PBXGroup; + children = ( + 0835F411AD796930557E6D6FD2D393A6 /* Accelerate.framework */, + 1145E8A94DDE8391F3FAE545D4948D05 /* CFNetwork.framework */, + 7A3BFAF850432D08D1C6D10D5BA02F14 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + C022D73960E7E90D0E05C6E45C832BE7 /* Support Files */ = { + isa = PBXGroup; + children = ( + 73DA22E071C96E19748840D7C2A689B2 /* SDCycleScrollView.modulemap */, + 1B3F7386065BCFC1FE7C8AEBE7F3BF31 /* SDCycleScrollView-dummy.m */, + BE7A6EE3713C50D6FD1FEEA0586D8E50 /* SDCycleScrollView-Info.plist */, + EDBE48AD49064E2D7B99418609D70957 /* SDCycleScrollView-prefix.pch */, + 50065895D82A55BECDCA8C0F2AFBACC7 /* SDCycleScrollView-umbrella.h */, + 024F0799DFFCAD855FED55AE5F947FD9 /* SDCycleScrollView.debug.xcconfig */, + 9C06040DCF060D3E4458FD23C4D04233 /* SDCycleScrollView.release.xcconfig */, + ); + name = "Support Files"; + path = "SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView"; + sourceTree = ""; + }; + C468ABE76962E0BE2929432C1C0FEAED /* Kingfisher */ = { + isa = PBXGroup; + children = ( + 09AF893526C785EFD23D179CA2A86759 /* AnimatedImageView.swift */, + FB7155C1450AF4C012F7F7D475C17E46 /* AuthenticationChallengeResponsable.swift */, + E8F6C4C1958434B423D9D400A4366D4E /* AVAssetImageDataProvider.swift */, + 52DC76A10E877360B29DA1E4E2085999 /* Box.swift */, + 721CC8813F811F38062E2DC12C356423 /* CacheSerializer.swift */, + 0EE6891D619E7BAD9069B7828102C291 /* CallbackQueue.swift */, + A1683A06B1CDA5014470D609382D85A8 /* CPListItem+Kingfisher.swift */, + 703C928F054A329427FDFAAFAEB6C157 /* Delegate.swift */, + 7F9AD5A832789E211525CE279FF4D099 /* DiskStorage.swift */, + 727D3A7D27282403AF9D7939AEE852D2 /* DisplayLink.swift */, + 650D0CDEB1097DF8DF7ED97736C0BAF0 /* Documentation.docc */, + 83DDEDA09F47013DD4844B67277E2D4F /* ExtensionHelpers.swift */, + 8718F88D170D177F8B0E6232DE630FB7 /* Filter.swift */, + E2F33A6A87BEAEC452B66E51172CE523 /* FormatIndicatedCacheSerializer.swift */, + D1626175E5A5CC9D5E26F40AAE33EFA0 /* GIFAnimatedImage.swift */, + A105C7E11616A4F871DA64B13B03C648 /* GraphicsContext.swift */, + 35CB00E19921B56505D287E03603DF61 /* HasImageComponent+Kingfisher.swift */, + 81DB7AA223A72FB28AE16D52395FF971 /* Image.swift */, + 3B722F196FAA4116C77E5668969C9F72 /* ImageBinder.swift */, + 0493054DB65F299F9EF390FA362C44AF /* ImageCache.swift */, + AA46282C19C34798D334579E48903844 /* ImageContext.swift */, + 19C90E96D8B2C8DF15E8E42AC6E13ABD /* ImageDataProcessor.swift */, + 82CEE4C7A2EC49613D78115E9722FC7B /* ImageDataProvider.swift */, + E292D71D89B9147EAA550A1F58CC6FB5 /* ImageDownloader.swift */, + 2BF7BFB9771159D81EF8C9333335D237 /* ImageDownloader+LivePhoto.swift */, + 119E0D751FF4848197C9CBCBFD54278D /* ImageDownloaderDelegate.swift */, + DA49DFE81879C1CF8A37DC2652F6515D /* ImageDrawing.swift */, + F6E83F91A43F9187E5162107B8409090 /* ImageFormat.swift */, + E9040AF0C1319C45E038E43519E9BBB0 /* ImageModifier.swift */, + D86EDC7AE0ED35F162F025AD406A91D7 /* ImagePrefetcher.swift */, + D636476819E91DCF512663277AD05056 /* ImageProcessor.swift */, + FA268B90610EC9332A023E0652EBC0BE /* ImageProgressive.swift */, + BF5B59F022ECBE3A0E291927F7279670 /* ImageTransition.swift */, + 155818C90F023801A94628C7D6A82B08 /* ImageView+Kingfisher.swift */, + 8E2B17600C7A345991B9AA15A7C35582 /* Indicator.swift */, + 9D584A19F29BD30693DEC1F332EB4F2F /* KF.swift */, + C741AF2FD004ADC53D21318D6A58EF6C /* KFAnimatedImage.swift */, + 63EDFF538F75518BF577FE7A6B6F23D6 /* KFImage.swift */, + 3FA15FADA159925DA5B7F647988750CC /* KFImageOptions.swift */, + CAC9CB7A52E863154B2924D9E3837FDC /* KFImageProtocol.swift */, + 8302757A435AAF85003AF57DC70CD843 /* KFImageRenderer.swift */, + FCFE377AB83F00D5C992C6496C9EFC21 /* KFOptionsSetter.swift */, + 92F740DB14C8A11B5071110CC6CEA113 /* Kingfisher.swift */, + B3BF51D75A1D7CF174DEC319B717C9E5 /* KingfisherError.swift */, + 9758A207D7BADEF5C130D8D6933FE61D /* KingfisherManager.swift */, + 4A3B376C93B00DDDF2549BB513A4BDCC /* KingfisherManager+LivePhoto.swift */, + 088A6991D69CFD4C9CC370EC49D40307 /* KingfisherOptionsInfo.swift */, + 54454B75E9B89D7455492F797CD55490 /* LivePhotoSource.swift */, + 9793A0714B13CE1E984C9374014D1752 /* MemoryStorage.swift */, + D31CDCCDB90175D14F2FB958365EE308 /* NSButton+Kingfisher.swift */, + 92E55CD65C474E2E8E54D591AB7E8F41 /* NSTextAttachment+Kingfisher.swift */, + B341DD334EE921BAE33DD0EE99890BB9 /* PHLivePhotoView+Kingfisher.swift */, + 58165CEAC074F84BB6C04DAE85F6694F /* PHPickerResultImageDataProvider.swift */, + 866DDAAFAB87B93333D5EBDA00D61377 /* Placeholder.swift */, + AA079D53D3957F27C01640994AFDBDE1 /* RedirectHandler.swift */, + 41AB8FE22E1CBB75C162F9AC7AEB1310 /* RequestModifier.swift */, + 2E9D9FE4308C75E6CD57F08EA223FA41 /* Resource.swift */, + 7770BCFB2284AB21643F892B69643E65 /* Result.swift */, + 9E7BD9C5BEFA3742345FC3410A7EE3D5 /* RetryStrategy.swift */, + C71F8832F0E3C04949146C7474CE031C /* Runtime.swift */, + C3BDE2F9B149AD480FEBCC4023C85806 /* SessionDataTask.swift */, + 497915090ABEF76673CD7B888BE455F2 /* SessionDelegate.swift */, + 0E09CE59C2E4A707801BA1699B64C1AD /* SizeExtensions.swift */, + 4C15430CA3CDC10040305E2E58824CEC /* Source.swift */, + 0EF622FA5D10ACC4E837A454A8D17E36 /* Storage.swift */, + 17AAC734F320AEFC0A79730431548512 /* String+SHA256.swift */, + 0A941E03CBAE776400D3FAF9A46CCFE8 /* UIButton+Kingfisher.swift */, + 61B448B79208C42D7A21CFF4DDDA475E /* Resources */, + 82ABC8DC035038E29AC7AAE1B2AF317D /* Support Files */, + ); + name = Kingfisher; + path = Kingfisher; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + 7F0439ECD3E836A199562DEC71D4888E /* Development Pods */, + 03C5C200A0787E300053CFA8F53CA094 /* Frameworks */, + 78D7360455FF649EAD9ACE5D468B487F /* Pods */, + E4091C70C81AE3C17FF090C35D444B4B /* Products */, + 81FF8BDA67FD1B5AE0CF892246415389 /* Targets Support Files */, + ); + sourceTree = ""; + }; + D6283149C2E8531AEB014C77EF7C618F /* Pods-SDCycleSrollViewDemo */ = { + isa = PBXGroup; + children = ( + C29203D370C8D13366A705688ABE9AAA /* Pods-SDCycleSrollViewDemo.modulemap */, + C66163B02F5485E62884BA732C256F40 /* Pods-SDCycleSrollViewDemo-acknowledgements.markdown */, + 920D31D615D8C0B848AC4E3682BC504D /* Pods-SDCycleSrollViewDemo-acknowledgements.plist */, + 137DCF57626054B77775E42E0C2BA8E4 /* Pods-SDCycleSrollViewDemo-dummy.m */, + 8CD8F2AB433203CC87D3AC5C6BEDD15C /* Pods-SDCycleSrollViewDemo-frameworks.sh */, + 48196E8A2F320585E031F229E5F76B0F /* Pods-SDCycleSrollViewDemo-Info.plist */, + 8CA8E94E503AB54105C70F29695898A6 /* Pods-SDCycleSrollViewDemo-umbrella.h */, + 11F82C874A277B48A172935DFE86483D /* Pods-SDCycleSrollViewDemo.debug.xcconfig */, + A54130D361CDE65C7FCE7B7AEEE8E428 /* Pods-SDCycleSrollViewDemo.release.xcconfig */, + ); + name = "Pods-SDCycleSrollViewDemo"; + path = "Target Support Files/Pods-SDCycleSrollViewDemo"; + sourceTree = ""; + }; + E4091C70C81AE3C17FF090C35D444B4B /* Products */ = { + isa = PBXGroup; + children = ( + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */, + C298ABB78D9B05529B89D8322DB2E7B0 /* Kingfisher-Kingfisher */, + 5CF908FA7A531A297954B16394DC1EAF /* Pods-SDCycleSrollViewDemo */, + 8B6CF5C20C32EE9F7F0862FF892524DE /* SDCycleScrollView */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 1EA27A001802BD1E91846F3CF061B128 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 17B994AB4C64CCCD4C944087FB56BB32 /* SDCollectionViewCell.h in Headers */, + 6659D83530A30CBBC03DC8CBA9366062 /* SDCycleScrollView.h in Headers */, + 6DA8B916CBDCDABB31D0DB7A0926CB55 /* SDCycleScrollView-umbrella.h in Headers */, + C4754374D4DAE6B06A96F2920989D911 /* TAAbstractDotView.h in Headers */, + 34EAFE241E4CAFF1BE5D8D7AC6EDD1B8 /* TAAnimatedDotView.h in Headers */, + 588426DC3CDADF722F9363F76E77B835 /* TADotView.h in Headers */, + 66E4088C9C894EB11899240BC3F5A3C7 /* TAPageControl.h in Headers */, + AEF253E11EE8937E3A5ABC5922DDF289 /* UIView+SDExtension.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 79ACEA4C767737CC336F92F33BBF3AAA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 104898BA44C8449A302C7E2F8620DAEB /* Pods-SDCycleSrollViewDemo-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B9D06F7443086FD18D2F6596BEFA3BD5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A7E562FC9C10C5003534E91160C69330 /* Kingfisher-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 0C592F52B4BC528BE2B2703032C1EDCA /* Pods-SDCycleSrollViewDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 891F4598EC783831DC51B9EAD978B1B3 /* Build configuration list for PBXNativeTarget "Pods-SDCycleSrollViewDemo" */; + buildPhases = ( + 79ACEA4C767737CC336F92F33BBF3AAA /* Headers */, + 4B70C9877EDA230A81F98E8DE8DF085D /* Sources */, + 998291F38E5F490E4F0074FC23149450 /* Frameworks */, + EA117A16CF6C12E4786585528722BA94 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F77D537FC0241881AF671D39233A7212 /* PBXTargetDependency */, + C0BC52940D353060E81A3D27AC722190 /* PBXTargetDependency */, + ); + name = "Pods-SDCycleSrollViewDemo"; + productName = Pods_SDCycleSrollViewDemo; + productReference = 5CF908FA7A531A297954B16394DC1EAF /* Pods-SDCycleSrollViewDemo */; + productType = "com.apple.product-type.framework"; + }; + 9828BBC09E9FB1238624113D7456E59E /* Kingfisher-Kingfisher */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8C8E3A9FA953C638DFE9BB5B21CDE302 /* Build configuration list for PBXNativeTarget "Kingfisher-Kingfisher" */; + buildPhases = ( + A95934576EA543C2F0211A0E7066A420 /* Sources */, + 987389D579A7E84C638BFFF6CBCC48E8 /* Frameworks */, + FE45E9B336EF1DE506776C05A9D7DCB2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Kingfisher-Kingfisher"; + productName = Kingfisher; + productReference = C298ABB78D9B05529B89D8322DB2E7B0 /* Kingfisher-Kingfisher */; + productType = "com.apple.product-type.bundle"; + }; + CBED833AAD6266F3AEFE9BE31C68E094 /* SDCycleScrollView */ = { + isa = PBXNativeTarget; + buildConfigurationList = 618954CF75E26FCF3A35DF4A2A0F1691 /* Build configuration list for PBXNativeTarget "SDCycleScrollView" */; + buildPhases = ( + 1EA27A001802BD1E91846F3CF061B128 /* Headers */, + E42B2389456B6406033016EFB71AEE7E /* Sources */, + 5E949F9F341F81DAB038FB5070293A02 /* Frameworks */, + 33BEADACE96E0BB6C03CEB8F28728A35 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7F27F51AA81CA8614C508AE030BBD412 /* PBXTargetDependency */, + ); + name = SDCycleScrollView; + productName = SDCycleScrollView; + productReference = 8B6CF5C20C32EE9F7F0862FF892524DE /* SDCycleScrollView */; + productType = "com.apple.product-type.framework"; + }; + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6EBF6B1B58A2645B3AC41D7865B9A3FC /* Build configuration list for PBXNativeTarget "Kingfisher" */; + buildPhases = ( + B9D06F7443086FD18D2F6596BEFA3BD5 /* Headers */, + 3EAAC5090CC9D7A004F60C6A2FD0307D /* Sources */, + 04C139C841E5A69C3B73E4E7788DB3C3 /* Frameworks */, + 1250FC97299C545C7651BC907254327A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6F4D6632EB2B63F4A3D77B0A84CD6893 /* PBXTargetDependency */, + ); + name = Kingfisher; + productName = Kingfisher; + productReference = C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 16.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + minimizedProjectReferenceProxies = 0; + preferredProjectObjectVersion = 77; + productRefGroup = E4091C70C81AE3C17FF090C35D444B4B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */, + 9828BBC09E9FB1238624113D7456E59E /* Kingfisher-Kingfisher */, + 0C592F52B4BC528BE2B2703032C1EDCA /* Pods-SDCycleSrollViewDemo */, + CBED833AAD6266F3AEFE9BE31C68E094 /* SDCycleScrollView */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1250FC97299C545C7651BC907254327A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0217BD2D535BBE17D651A40D117783C5 /* Kingfisher-Kingfisher in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33BEADACE96E0BB6C03CEB8F28728A35 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EA117A16CF6C12E4786585528722BA94 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FE45E9B336EF1DE506776C05A9D7DCB2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E9C2DC4FA2FFD17B934AC840DB7F7799 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3EAAC5090CC9D7A004F60C6A2FD0307D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 919299610C2CA6AB6B58C902F9FA91F5 /* AnimatedImageView.swift in Sources */, + DDF61BEA9AFF4E1CFD53D4B84D119DF4 /* AuthenticationChallengeResponsable.swift in Sources */, + 799EAD4947C3902EA490A7282B738C25 /* AVAssetImageDataProvider.swift in Sources */, + EE1368EC3C3660565A82A9842E57124F /* Box.swift in Sources */, + 420ADE5F222F9DB15F15F59991EEFDC1 /* CacheSerializer.swift in Sources */, + 92D50FCEBF77BFACA64AB481BF6EB687 /* CallbackQueue.swift in Sources */, + 9E4FDDFBA5C3856E7C540B560910C2C0 /* CPListItem+Kingfisher.swift in Sources */, + AF34FDA62958AD154C005C924C808165 /* Delegate.swift in Sources */, + 678EDDCD6ED116BC5100C5924A9BB0C6 /* DiskStorage.swift in Sources */, + AB1F732806BA88499D9556A1DF9C7147 /* DisplayLink.swift in Sources */, + 5E6BFEA8AF2EE6BEEAEB1BE2346A33AE /* Documentation.docc in Sources */, + B126EF2264809CA8F7A70EB1F227030B /* ExtensionHelpers.swift in Sources */, + CC63E9DDAC7230E3EA8ECB73B9E0DA13 /* Filter.swift in Sources */, + F5E62DAD60E5C311FE1EC1333D44F194 /* FormatIndicatedCacheSerializer.swift in Sources */, + FE9BA3316B14AC042881E3601745E44A /* GIFAnimatedImage.swift in Sources */, + 77F2E509688A124F02FD040516154751 /* GraphicsContext.swift in Sources */, + AD860938D35585A99549790933C165E5 /* HasImageComponent+Kingfisher.swift in Sources */, + D548E4E0D9121E18DA31F2150791F9AA /* Image.swift in Sources */, + B01BF1940094224E6A48D83E53BA6AC8 /* ImageBinder.swift in Sources */, + 54FCB2A0877468FCA03245D4FBB0F9D2 /* ImageCache.swift in Sources */, + 990A4E31ADF23A8BC1C5044ABD45B979 /* ImageContext.swift in Sources */, + 442F3EF780ACE70BAF124C9E3E5E8E15 /* ImageDataProcessor.swift in Sources */, + 92A43BFC165F5595D1CF24AD2C65D6C0 /* ImageDataProvider.swift in Sources */, + AF1D366BDF7AA8C015A0283550C197AB /* ImageDownloader.swift in Sources */, + 7A2DDEA900883E39D33468469FC14AEB /* ImageDownloader+LivePhoto.swift in Sources */, + 6CAB59AA86D67BD46B82E326058258A1 /* ImageDownloaderDelegate.swift in Sources */, + 3CF9247B7112973208E98C5B8A3EFD08 /* ImageDrawing.swift in Sources */, + A7FF322DF74002F7C9F855A5F95B166A /* ImageFormat.swift in Sources */, + 1DE5A65531D35FBB1E317481CD316342 /* ImageModifier.swift in Sources */, + 89C457ACFD60011F394C5BD42800BDC6 /* ImagePrefetcher.swift in Sources */, + E9AEBBA488E8832DA6CBE9881DF9DE95 /* ImageProcessor.swift in Sources */, + 019E2E60370D97793F78BFA5B82E7F70 /* ImageProgressive.swift in Sources */, + F510058AE05060128BA765E991B787E2 /* ImageTransition.swift in Sources */, + 1C4D430B867251402232CD36BA776B17 /* ImageView+Kingfisher.swift in Sources */, + 013D0B0A8786F5B34F039FB3704E12AB /* Indicator.swift in Sources */, + 4BFEF1EA0FD8911E9EF55A4484F7F1E6 /* KF.swift in Sources */, + 59E1BF0AB3F19C50E578D398E26EB227 /* KFAnimatedImage.swift in Sources */, + 6CABA18CE8DB786EFDA231AF69A7D4B2 /* KFImage.swift in Sources */, + C10B1478C4AB3693D886D94ADFAEFDFB /* KFImageOptions.swift in Sources */, + 3F749E95FFA3640649B8252D9B2CFC10 /* KFImageProtocol.swift in Sources */, + 5EE23E0B4A7175D504F4C7E20505DDE7 /* KFImageRenderer.swift in Sources */, + E7D06F2DBFA16E219746D0E11E60A501 /* KFOptionsSetter.swift in Sources */, + DDEDEE8B19E94E46A2D70C78127236E5 /* Kingfisher.swift in Sources */, + 8B7560712BC596AD4790CDCB38D629BC /* Kingfisher-dummy.m in Sources */, + 7FDE5BBEB2E3E5E79A686FB6F496E566 /* KingfisherError.swift in Sources */, + F469D3446C362E18EF1F7D714E2E64E0 /* KingfisherManager.swift in Sources */, + FD08990C503D242E5A70B010B704C755 /* KingfisherManager+LivePhoto.swift in Sources */, + 5DF2E03715129AA0C77ADD8BAFFBDDFC /* KingfisherOptionsInfo.swift in Sources */, + 576C5FD65E8E35A7EF189EBB20CE01C6 /* LivePhotoSource.swift in Sources */, + DB17C9319D2226DB80AFB60844D61209 /* MemoryStorage.swift in Sources */, + 9981C32E56FF09688D9FBCBF45FB93B2 /* NSButton+Kingfisher.swift in Sources */, + 7168A1F86699F21734478EC9596F0D74 /* NSTextAttachment+Kingfisher.swift in Sources */, + B1F7B96741F92ADC2F98417FFC33DB96 /* PHLivePhotoView+Kingfisher.swift in Sources */, + C2A21354D5DB755E780A9ABC1752E74A /* PHPickerResultImageDataProvider.swift in Sources */, + AFE232EEDAC5B035CDB331A5CEA18E32 /* Placeholder.swift in Sources */, + A1B70BEB9DCBE081F5B39F596F2596C5 /* RedirectHandler.swift in Sources */, + E274BA92F9C6AA938EF04CE7EA38CEAD /* RequestModifier.swift in Sources */, + F0B0B551A13101A1F5906D38E5863E8A /* Resource.swift in Sources */, + FA190258BD504A02FD26844DF48C5FAF /* Result.swift in Sources */, + F819B1541A0195B89BEBAB3CCC51DCC6 /* RetryStrategy.swift in Sources */, + 88E76DE06F81A6B173F029A37BD0BE7F /* Runtime.swift in Sources */, + 2703C0DC58FB460C01ADE8E8D22C9F62 /* SessionDataTask.swift in Sources */, + EAAA0041F6A0DB1039D328B35FF5EA09 /* SessionDelegate.swift in Sources */, + 1D74158DCC81C4E83C0A8945EDE69C86 /* SizeExtensions.swift in Sources */, + C59E3FD52D5408C08FDC0E6FE6266C3B /* Source.swift in Sources */, + 9534119EDE1FB3F9DECE76D8130D6397 /* Storage.swift in Sources */, + 5F09107998BED88EC6FCDFC5305031B1 /* String+SHA256.swift in Sources */, + 2D8245FE7A22ED672D3DF95E77A96E46 /* UIButton+Kingfisher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B70C9877EDA230A81F98E8DE8DF085D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D98386A54F28BA3F27CBC8334D0CADA /* Pods-SDCycleSrollViewDemo-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A95934576EA543C2F0211A0E7066A420 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E42B2389456B6406033016EFB71AEE7E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 32D1F8F5EFDA36D786DD8CA2D1DFC732 /* SDCollectionViewCell.m in Sources */, + 4C016BE1DC259AB2A379AC81DF581C49 /* SDCycleScrollView.m in Sources */, + AE1B484501E4ECD99787770A6545775E /* SDCycleScrollView-dummy.m in Sources */, + EE4E733231F8656BB065F4FBA5793FF8 /* SDSwiftCycleScrollView.swift in Sources */, + B13477F1A5A0A88616148C1205D8EC29 /* TAAbstractDotView.m in Sources */, + 25C27C7AEBD561EBF70CFC413F4CD755 /* TAAnimatedDotView.m in Sources */, + 183189655EA72780910B9A83E8208BC4 /* TADotView.m in Sources */, + 9198B564D6DF28D1F6AC58E1E068607A /* TAPageControl.m in Sources */, + BB8CAA589F0F85C385509FEC4F65DCE6 /* UIImageView+KF.swift in Sources */, + FA1D2FDB9DE836CEF3DAD8FB122D67B6 /* UIView+SDExtension.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 6F4D6632EB2B63F4A3D77B0A84CD6893 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Kingfisher-Kingfisher"; + target = 9828BBC09E9FB1238624113D7456E59E /* Kingfisher-Kingfisher */; + targetProxy = 4B1BD609E38336B14B1A7C81F0212402 /* PBXContainerItemProxy */; + }; + 7F27F51AA81CA8614C508AE030BBD412 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Kingfisher; + target = E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */; + targetProxy = F87F72A9CB4AE83D6B0E63FD060D3A1F /* PBXContainerItemProxy */; + }; + C0BC52940D353060E81A3D27AC722190 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = SDCycleScrollView; + target = CBED833AAD6266F3AEFE9BE31C68E094 /* SDCycleScrollView */; + targetProxy = 43956C431C0DB174165BE8B6923B7A80 /* PBXContainerItemProxy */; + }; + F77D537FC0241881AF671D39233A7212 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Kingfisher; + target = E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */; + targetProxy = 7C0DEB136D71DA321B593C1BB8EFE59C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 0AF5C5997CB8535E68D79709E3259BF5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9C06040DCF060D3E4458FD23C4D04233 /* SDCycleScrollView.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/SDCycleScrollView/SDCycleScrollView-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/SDCycleScrollView/SDCycleScrollView-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/SDCycleScrollView/SDCycleScrollView.modulemap"; + PRODUCT_MODULE_NAME = SDCycleScrollView; + PRODUCT_NAME = SDCycleScrollView; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 306DAAEFF20B03A10C67BA72D29BB53C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 024F0799DFFCAD855FED55AE5F947FD9 /* SDCycleScrollView.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/SDCycleScrollView/SDCycleScrollView-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/SDCycleScrollView/SDCycleScrollView-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/SDCycleScrollView/SDCycleScrollView.modulemap"; + PRODUCT_MODULE_NAME = SDCycleScrollView; + PRODUCT_NAME = SDCycleScrollView; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 4C12F8779C0A7FCC937B093F9317D1D9 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 53BC042143F20D062EAC307186393CB3 /* Kingfisher.debug.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Kingfisher"; + ENABLE_BITCODE = NO; + IBSC_MODULE = Kingfisher; + INFOPLIST_FILE = "Target Support Files/Kingfisher/ResourceBundle-Kingfisher-Kingfisher-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Debug; + }; + 79669D80747D67F6A70B7CFD7B11E5BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 11F82C874A277B48A172935DFE86483D /* Pods-SDCycleSrollViewDemo.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 903A0004D3E6651EFD5D2E16214D101B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = ""; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; + A1BEDCEF2CAAEBAF299D256F525A3382 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 53BC042143F20D062EAC307186393CB3 /* Kingfisher.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; + PRODUCT_MODULE_NAME = Kingfisher; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + B4EFE046ACF8F37157F6E322C7FCFC28 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = ""; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + BB1E4591BB6D79FA90D12D61F3BD4E74 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B9420292B9769FED1D6F8711A051A6DD /* Kingfisher.release.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/Kingfisher"; + ENABLE_BITCODE = NO; + IBSC_MODULE = Kingfisher; + INFOPLIST_FILE = "Target Support Files/Kingfisher/ResourceBundle-Kingfisher-Kingfisher-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; + CB3020DFC843BCE565F5FA4024538D17 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B9420292B9769FED1D6F8711A051A6DD /* Kingfisher.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; + PRODUCT_MODULE_NAME = Kingfisher; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + D7AB34A54B0908B6394D1982FAF92F2A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A54130D361CDE65C7FCE7B7AEEE8E428 /* Pods-SDCycleSrollViewDemo.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B4EFE046ACF8F37157F6E322C7FCFC28 /* Debug */, + 903A0004D3E6651EFD5D2E16214D101B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 618954CF75E26FCF3A35DF4A2A0F1691 /* Build configuration list for PBXNativeTarget "SDCycleScrollView" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 306DAAEFF20B03A10C67BA72D29BB53C /* Debug */, + 0AF5C5997CB8535E68D79709E3259BF5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6EBF6B1B58A2645B3AC41D7865B9A3FC /* Build configuration list for PBXNativeTarget "Kingfisher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1BEDCEF2CAAEBAF299D256F525A3382 /* Debug */, + CB3020DFC843BCE565F5FA4024538D17 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 891F4598EC783831DC51B9EAD978B1B3 /* Build configuration list for PBXNativeTarget "Pods-SDCycleSrollViewDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 79669D80747D67F6A70B7CFD7B11E5BC /* Debug */, + D7AB34A54B0908B6394D1982FAF92F2A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8C8E3A9FA953C638DFE9BB5B21CDE302 /* Build configuration list for PBXNativeTarget "Kingfisher-Kingfisher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4C12F8779C0A7FCC937B093F9317D1D9 /* Debug */, + BB1E4591BB6D79FA90D12D61F3BD4E74 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/Kingfisher-Kingfisher.xcscheme b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/Kingfisher-Kingfisher.xcscheme new file mode 100644 index 00000000..dd7f78d6 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/Kingfisher-Kingfisher.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/Kingfisher.xcscheme b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/Kingfisher.xcscheme new file mode 100644 index 00000000..a74d0494 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/Kingfisher.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/Pods-SDCycleSrollViewDemo.xcscheme b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/Pods-SDCycleSrollViewDemo.xcscheme new file mode 100644 index 00000000..ad276c66 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/Pods-SDCycleSrollViewDemo.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/SDCycleScrollView.xcscheme b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/SDCycleScrollView.xcscheme new file mode 100644 index 00000000..a03bf3fc --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/SDCycleScrollView.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..6339b8bf --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Pods.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,39 @@ + + + + + SchemeUserState + + Kingfisher-Kingfisher.xcscheme + + isShown + + orderHint + 1 + + Kingfisher.xcscheme + + isShown + + orderHint + 0 + + Pods-SDCycleSrollViewDemo.xcscheme + + isShown + + orderHint + 2 + + SDCycleScrollView.xcscheme + + isShown + + orderHint + 3 + + + SuppressBuildableAutocreation + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist new file mode 100644 index 00000000..c0cbadfd --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 8.3.3 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m new file mode 100644 index 00000000..1b89d0ed --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Kingfisher : NSObject +@end +@implementation PodsDummy_Kingfisher +@end diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch new file mode 100644 index 00000000..beb2a244 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h new file mode 100644 index 00000000..75a79961 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double KingfisherVersionNumber; +FOUNDATION_EXPORT const unsigned char KingfisherVersionString[]; + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig new file mode 100644 index 00000000..a8d81614 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig @@ -0,0 +1,16 @@ +BUILD_LIBRARY_FOR_DISTRIBUTION = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap new file mode 100644 index 00000000..2a20d91c --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap @@ -0,0 +1,6 @@ +framework module Kingfisher { + umbrella header "Kingfisher-umbrella.h" + + export * + module * { export * } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig new file mode 100644 index 00000000..a8d81614 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig @@ -0,0 +1,16 @@ +BUILD_LIBRARY_FOR_DISTRIBUTION = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/ResourceBundle-Kingfisher-Kingfisher-Info.plist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/ResourceBundle-Kingfisher-Kingfisher-Info.plist new file mode 100644 index 00000000..e164be3a --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Kingfisher/ResourceBundle-Kingfisher-Kingfisher-Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + BNDL + CFBundleShortVersionString + 8.3.3 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-Info.plist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-Info.plist new file mode 100644 index 00000000..19cf209d --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-acknowledgements.markdown b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-acknowledgements.markdown new file mode 100644 index 00000000..044c724f --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-acknowledgements.markdown @@ -0,0 +1,55 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## Kingfisher + +The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +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. + + + +## SDCycleScrollView + +The MIT License (MIT) + +Copyright (c) 2015 GSD_iOS + +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. + + +Generated by CocoaPods - https://cocoapods.org diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-acknowledgements.plist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-acknowledgements.plist new file mode 100644 index 00000000..10e1db5e --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-acknowledgements.plist @@ -0,0 +1,93 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2019 Wei Wang + +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. + + + License + MIT + Title + Kingfisher + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2015 GSD_iOS + +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. + + + License + MIT + Title + SDCycleScrollView + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-dummy.m b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-dummy.m new file mode 100644 index 00000000..e86cbe1e --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_SDCycleSrollViewDemo : NSObject +@end +@implementation PodsDummy_Pods_SDCycleSrollViewDemo +@end diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Debug-input-files.xcfilelist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 00000000..bc094463 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,3 @@ +${PODS_ROOT}/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks.sh +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework +${BUILT_PRODUCTS_DIR}/SDCycleScrollView/SDCycleScrollView.framework \ No newline at end of file diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Debug-output-files.xcfilelist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 00000000..3ec47ce2 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1,2 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDCycleScrollView.framework \ No newline at end of file diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Release-input-files.xcfilelist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Release-input-files.xcfilelist new file mode 100644 index 00000000..bc094463 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,3 @@ +${PODS_ROOT}/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks.sh +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework +${BUILT_PRODUCTS_DIR}/SDCycleScrollView/SDCycleScrollView.framework \ No newline at end of file diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Release-output-files.xcfilelist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Release-output-files.xcfilelist new file mode 100644 index 00000000..3ec47ce2 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-Release-output-files.xcfilelist @@ -0,0 +1,2 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDCycleScrollView.framework \ No newline at end of file diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks.sh b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks.sh new file mode 100755 index 00000000..7ed2b8b5 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks.sh @@ -0,0 +1,188 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +BCSYMBOLMAP_DIR="BCSymbolMaps" + + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink -f "${source}")" + fi + + if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then + # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied + find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do + echo "Installing $f" + install_bcsymbolmap "$f" "$destination" + rm "$f" + done + rmdir "${source}/${BCSYMBOLMAP_DIR}" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + warn_missing_arch=${2:-true} + if [ -r "$source" ]; then + # Copy the dSYM into the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .dSYM "$source")" + binary_name="$(ls "$source/Contents/Resources/DWARF")" + binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" + + # Strip invalid architectures from the dSYM. + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" "$warn_missing_arch" + fi + if [[ $STRIP_BINARY_RETVAL == 0 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + mkdir -p "${DWARF_DSYM_FOLDER_PATH}" + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" + fi + fi +} + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + warn_missing_arch=${2:-true} + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + if [[ "$warn_missing_arch" == "true" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + fi + STRIP_BINARY_RETVAL=1 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=0 +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/SDCycleScrollView/SDCycleScrollView.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/SDCycleScrollView/SDCycleScrollView.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-umbrella.h b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-umbrella.h new file mode 100644 index 00000000..0f3a1424 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_SDCycleSrollViewDemoVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_SDCycleSrollViewDemoVersionString[]; + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.debug.xcconfig b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.debug.xcconfig new file mode 100644 index 00000000..2903028b --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.debug.xcconfig @@ -0,0 +1,16 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView/SDCycleScrollView.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -framework "SDCycleScrollView" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "-F${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.modulemap b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.modulemap new file mode 100644 index 00000000..a9ffa490 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.modulemap @@ -0,0 +1,6 @@ +framework module Pods_SDCycleSrollViewDemo { + umbrella header "Pods-SDCycleSrollViewDemo-umbrella.h" + + export * + module * { export * } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.release.xcconfig b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.release.xcconfig new file mode 100644 index 00000000..2903028b --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.release.xcconfig @@ -0,0 +1,16 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView/SDCycleScrollView.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -framework "SDCycleScrollView" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "-F${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-Info.plist b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-Info.plist new file mode 100644 index 00000000..7f2f697a --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.82.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-dummy.m b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-dummy.m new file mode 100644 index 00000000..3a541b56 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_SDCycleScrollView : NSObject +@end +@implementation PodsDummy_SDCycleScrollView +@end diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-prefix.pch b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-prefix.pch new file mode 100644 index 00000000..beb2a244 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-umbrella.h b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-umbrella.h new file mode 100644 index 00000000..a12b2c9d --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView-umbrella.h @@ -0,0 +1,23 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "TAAbstractDotView.h" +#import "TAAnimatedDotView.h" +#import "TADotView.h" +#import "TAPageControl.h" +#import "SDCollectionViewCell.h" +#import "SDCycleScrollView.h" +#import "UIView+SDExtension.h" + +FOUNDATION_EXPORT double SDCycleScrollViewVersionNumber; +FOUNDATION_EXPORT const unsigned char SDCycleScrollViewVersionString[]; + diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView.debug.xcconfig b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView.debug.xcconfig new file mode 100644 index 00000000..5cb91f02 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView.debug.xcconfig @@ -0,0 +1,16 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../.. +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView.modulemap b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView.modulemap new file mode 100644 index 00000000..e334dc7a --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView.modulemap @@ -0,0 +1,6 @@ +framework module SDCycleScrollView { + umbrella header "SDCycleScrollView-umbrella.h" + + export * + module * { export * } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView.release.xcconfig b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView.release.xcconfig new file mode 100644 index 00000000..5cb91f02 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/Pods/Target Support Files/SDCycleScrollView/SDCycleScrollView.release.xcconfig @@ -0,0 +1,16 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SDCycleScrollView +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "Kingfisher" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../.. +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/project.pbxproj b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/project.pbxproj new file mode 100644 index 00000000..229ff480 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/project.pbxproj @@ -0,0 +1,443 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 1919DB712C3A30F723970D6D /* Pods_SDCycleSrollViewDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 120018BA8B80BCB8EF5E4F95 /* Pods_SDCycleSrollViewDemo.framework */; }; + 980C9E4B2E0D46FA00B74484 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 980C9E422E0D46FA00B74484 /* Assets.xcassets */; }; + 980C9E4D2E0D46FA00B74484 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 980C9E452E0D46FA00B74484 /* LaunchScreen.storyboard */; }; + 980C9E4E2E0D46FA00B74484 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 980C9E472E0D46FA00B74484 /* Main.storyboard */; }; + 980C9E4F2E0D46FA00B74484 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980C9E412E0D46FA00B74484 /* AppDelegate.swift */; }; + 980C9E502E0D46FA00B74484 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980C9E482E0D46FA00B74484 /* SceneDelegate.swift */; }; + 980C9E512E0D46FA00B74484 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980C9E492E0D46FA00B74484 /* ViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 120018BA8B80BCB8EF5E4F95 /* Pods_SDCycleSrollViewDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SDCycleSrollViewDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 16B12D0D9A1B903416B3241D /* Pods-SDCycleSrollViewDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDCycleSrollViewDemo.release.xcconfig"; path = "Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.release.xcconfig"; sourceTree = ""; }; + 58BA47FF477033A34C8295EA /* Pods-SDCycleSrollViewDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDCycleSrollViewDemo.debug.xcconfig"; path = "Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo.debug.xcconfig"; sourceTree = ""; }; + 980C9E292E0D46F400B74484 /* SDCycleSrollViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SDCycleSrollViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 980C9E412E0D46FA00B74484 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 980C9E422E0D46FA00B74484 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 980C9E432E0D46FA00B74484 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 980C9E442E0D46FA00B74484 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 980C9E462E0D46FA00B74484 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 980C9E482E0D46FA00B74484 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 980C9E492E0D46FA00B74484 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 980C9E262E0D46F400B74484 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1919DB712C3A30F723970D6D /* Pods_SDCycleSrollViewDemo.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 980C9E202E0D46F400B74484 = { + isa = PBXGroup; + children = ( + 980C9E4A2E0D46FA00B74484 /* SDCycleSrollViewDemo */, + 980C9E2A2E0D46F400B74484 /* Products */, + A5F7882428C8C9483D87EE91 /* Pods */, + CB77F090A080282F2F9C1D67 /* Frameworks */, + ); + sourceTree = ""; + }; + 980C9E2A2E0D46F400B74484 /* Products */ = { + isa = PBXGroup; + children = ( + 980C9E292E0D46F400B74484 /* SDCycleSrollViewDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + 980C9E4A2E0D46FA00B74484 /* SDCycleSrollViewDemo */ = { + isa = PBXGroup; + children = ( + 980C9E412E0D46FA00B74484 /* AppDelegate.swift */, + 980C9E422E0D46FA00B74484 /* Assets.xcassets */, + 980C9E432E0D46FA00B74484 /* Info.plist */, + 980C9E452E0D46FA00B74484 /* LaunchScreen.storyboard */, + 980C9E472E0D46FA00B74484 /* Main.storyboard */, + 980C9E482E0D46FA00B74484 /* SceneDelegate.swift */, + 980C9E492E0D46FA00B74484 /* ViewController.swift */, + ); + path = SDCycleSrollViewDemo; + sourceTree = ""; + }; + A5F7882428C8C9483D87EE91 /* Pods */ = { + isa = PBXGroup; + children = ( + 58BA47FF477033A34C8295EA /* Pods-SDCycleSrollViewDemo.debug.xcconfig */, + 16B12D0D9A1B903416B3241D /* Pods-SDCycleSrollViewDemo.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + CB77F090A080282F2F9C1D67 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 120018BA8B80BCB8EF5E4F95 /* Pods_SDCycleSrollViewDemo.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 980C9E282E0D46F400B74484 /* SDCycleSrollViewDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 980C9E3C2E0D46F500B74484 /* Build configuration list for PBXNativeTarget "SDCycleSrollViewDemo" */; + buildPhases = ( + BD1CDC77B9FC85065E0F8B02 /* [CP] Check Pods Manifest.lock */, + 980C9E252E0D46F400B74484 /* Sources */, + 980C9E262E0D46F400B74484 /* Frameworks */, + 980C9E272E0D46F400B74484 /* Resources */, + A51E68765E58698DAC707431 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SDCycleSrollViewDemo; + productName = SDCycleSrollViewDemo; + productReference = 980C9E292E0D46F400B74484 /* SDCycleSrollViewDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 980C9E212E0D46F400B74484 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 980C9E282E0D46F400B74484 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = 980C9E242E0D46F400B74484 /* Build configuration list for PBXProject "SDCycleSrollViewDemo" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 980C9E202E0D46F400B74484; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 980C9E2A2E0D46F400B74484 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 980C9E282E0D46F400B74484 /* SDCycleSrollViewDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 980C9E272E0D46F400B74484 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 980C9E4B2E0D46FA00B74484 /* Assets.xcassets in Resources */, + 980C9E4D2E0D46FA00B74484 /* LaunchScreen.storyboard in Resources */, + 980C9E4E2E0D46FA00B74484 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + A51E68765E58698DAC707431 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SDCycleSrollViewDemo/Pods-SDCycleSrollViewDemo-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + BD1CDC77B9FC85065E0F8B02 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SDCycleSrollViewDemo-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 980C9E252E0D46F400B74484 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 980C9E4F2E0D46FA00B74484 /* AppDelegate.swift in Sources */, + 980C9E502E0D46FA00B74484 /* SceneDelegate.swift in Sources */, + 980C9E512E0D46FA00B74484 /* ViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 980C9E452E0D46FA00B74484 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 980C9E442E0D46FA00B74484 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 980C9E472E0D46FA00B74484 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 980C9E462E0D46FA00B74484 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 980C9E3D2E0D46F500B74484 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 58BA47FF477033A34C8295EA /* Pods-SDCycleSrollViewDemo.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = YQ2BBNYL5U; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SDCycleSrollViewDemo/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 11; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yeqiu.SDCycleSrollViewDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 980C9E3E2E0D46F500B74484 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 16B12D0D9A1B903416B3241D /* Pods-SDCycleSrollViewDemo.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = YQ2BBNYL5U; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SDCycleSrollViewDemo/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 11; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yeqiu.SDCycleSrollViewDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 980C9E3F2E0D46F500B74484 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 980C9E402E0D46F500B74484 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 980C9E242E0D46F400B74484 /* Build configuration list for PBXProject "SDCycleSrollViewDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 980C9E3F2E0D46F500B74484 /* Debug */, + 980C9E402E0D46F500B74484 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 980C9E3C2E0D46F500B74484 /* Build configuration list for PBXNativeTarget "SDCycleSrollViewDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 980C9E3D2E0D46F500B74484 /* Debug */, + 980C9E3E2E0D46F500B74484 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 980C9E212E0D46F400B74484 /* Project object */; +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/project.xcworkspace/xcuserdata/york.xcuserdatad/UserInterfaceState.xcuserstate b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/project.xcworkspace/xcuserdata/york.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..be12d15c Binary files /dev/null and b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/project.xcworkspace/xcuserdata/york.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..40efef48 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcodeproj/xcuserdata/york.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + SDCycleSrollViewDemo.xcscheme_^#shared#^_ + + orderHint + 4 + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcworkspace/contents.xcworkspacedata b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..990392dc --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcworkspace/xcuserdata/york.xcuserdatad/UserInterfaceState.xcuserstate b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcworkspace/xcuserdata/york.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..020356e8 Binary files /dev/null and b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcworkspace/xcuserdata/york.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcworkspace/xcuserdata/york.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcworkspace/xcuserdata/york.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 00000000..72372e81 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo.xcworkspace/xcuserdata/york.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/AppDelegate.swift b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/AppDelegate.swift new file mode 100644 index 00000000..60b4677f --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// SDCycleSrollViewDemo +// +// Created by york on 2025/6/26. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Assets.xcassets/AccentColor.colorset/Contents.json b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Assets.xcassets/Contents.json b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Base.lproj/LaunchScreen.storyboard b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Base.lproj/Main.storyboard b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Base.lproj/Main.storyboard new file mode 100644 index 00000000..6947709b --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Base.lproj/Main.storyboard @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Info.plist b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Info.plist new file mode 100644 index 00000000..c0ae0b42 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/Info.plist @@ -0,0 +1,30 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/SceneDelegate.swift b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/SceneDelegate.swift new file mode 100644 index 00000000..aee9b828 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// SDCycleSrollViewDemo +// +// Created by york on 2025/6/26. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/ViewController.swift b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/ViewController.swift new file mode 100644 index 00000000..7da2b947 --- /dev/null +++ b/SwiftDemo/SDCycleSrollViewDemo/SDCycleSrollViewDemo/ViewController.swift @@ -0,0 +1,22 @@ +// +// ViewController.swift +// SDCycleSrollViewDemo +// +// Created by york on 2025/6/26. +// +import SDCycleScrollView +import UIKit + +class ViewController: UIViewController { + + @IBOutlet weak var cycleView: SDSwiftCycleScrollView! + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + cycleView.bannerImageViewContentMode = .scaleAspectFill + cycleView.imageURLStringsGroup = ["https://ww2.sinaimg.cn/mw690/007ut4Uhly1hx4v37mpxcj30u017cgrv.jpg"] + } + + +} +