Просмотр исходного кода

Merge pull request #2256 from onevcat/nuomi1-feature/PHPickerResultImageDataProvider

Nuomi1 feature/ph picker result image data provider
Wei Wang 1 год назад
Родитель
Сommit
8bf3571223

+ 70 - 3
Demo/Demo/Kingfisher-Demo/Base.lproj/Main.storyboard

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22113.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="peg-r0-mlo">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="peg-r0-mlo">
     <device id="retina5_5" orientation="portrait" appearance="dark"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22089.1"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -467,9 +467,33 @@
                                             <segue destination="m5P-35-yHH" kind="show" id="iY4-PO-rZO"/>
                                         </connections>
                                     </tableViewCell>
-                                    <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="TIF-8x-GLM">
+                                    <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="5xh-yt-GCq">
                                         <rect key="frame" x="0.0" y="666" width="414" height="44"/>
                                         <autoresizingMask key="autoresizingMask"/>
+                                        <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5xh-yt-GCq" id="RYS-B1-kkb">
+                                            <rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
+                                            <autoresizingMask key="autoresizingMask"/>
+                                            <subviews>
+                                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Picker Result" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wm0-0Z-bjC">
+                                                    <rect key="frame" x="20.000000000000007" y="11.666666666666664" width="98.666666666666686" height="21"/>
+                                                    <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                                    <nil key="textColor"/>
+                                                    <nil key="highlightedColor"/>
+                                                </label>
+                                            </subviews>
+                                            <constraints>
+                                                <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="wm0-0Z-bjC" secondAttribute="trailing" constant="20" symbolic="YES" id="B3I-gk-g0y"/>
+                                                <constraint firstItem="wm0-0Z-bjC" firstAttribute="leading" secondItem="RYS-B1-kkb" secondAttribute="leading" constant="20" symbolic="YES" id="drj-Dz-zub"/>
+                                                <constraint firstItem="wm0-0Z-bjC" firstAttribute="centerY" secondItem="RYS-B1-kkb" secondAttribute="centerY" id="wFd-yU-MrL"/>
+                                            </constraints>
+                                        </tableViewCellContentView>
+                                        <connections>
+                                            <segue destination="wco-eY-gNu" kind="show" id="KCP-ab-diO"/>
+                                        </connections>
+                                    </tableViewCell>
+                                    <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="TIF-8x-GLM">
+                                        <rect key="frame" x="0.0" y="710" width="414" height="44"/>
+                                        <autoresizingMask key="autoresizingMask"/>
                                         <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="TIF-8x-GLM" id="ykx-Ds-PkP">
                                             <rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
                                             <autoresizingMask key="autoresizingMask"/>
@@ -1127,6 +1151,49 @@
             </objects>
             <point key="canvasLocation" x="1851" y="-1114"/>
         </scene>
+        <!--Picker Result View Controller-->
+        <scene sceneID="JMu-F0-c9u">
+            <objects>
+                <viewController id="wco-eY-gNu" customClass="PHPickerResultViewController" customModule="Kingfisher_Demo" customModuleProvider="target" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="22H-rk-RMJ">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="1LX-Oj-TiN">
+                                <rect key="frame" x="87" y="114" width="240" height="240"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="240" id="bGC-o1-bQV"/>
+                                    <constraint firstAttribute="width" constant="240" id="bya-2p-YYh"/>
+                                </constraints>
+                            </imageView>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ySI-TE-gHl">
+                                <rect key="frame" x="167.33333333333334" y="404" width="79.666666666666657" height="34.333333333333314"/>
+                                <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                <state key="normal" title="Tap Me"/>
+                                <buttonConfiguration key="configuration" style="plain" title="Tap Me"/>
+                                <connections>
+                                    <action selector="onTapButton" destination="wco-eY-gNu" eventType="touchUpInside" id="QyB-8Y-1Oc"/>
+                                </connections>
+                            </button>
+                        </subviews>
+                        <viewLayoutGuide key="safeArea" id="BKk-eJ-kIo"/>
+                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+                        <constraints>
+                            <constraint firstItem="ySI-TE-gHl" firstAttribute="centerX" secondItem="1LX-Oj-TiN" secondAttribute="centerX" id="Sm9-p7-uMP"/>
+                            <constraint firstItem="ySI-TE-gHl" firstAttribute="top" secondItem="1LX-Oj-TiN" secondAttribute="bottom" constant="50" id="gbM-rb-Ok8"/>
+                            <constraint firstItem="1LX-Oj-TiN" firstAttribute="centerX" secondItem="BKk-eJ-kIo" secondAttribute="centerX" id="qyZ-NL-vmv"/>
+                            <constraint firstItem="1LX-Oj-TiN" firstAttribute="top" secondItem="BKk-eJ-kIo" secondAttribute="top" constant="50" id="x3D-dH-ynU"/>
+                        </constraints>
+                    </view>
+                    <navigationItem key="navigationItem" id="r2f-l4-Rbs"/>
+                    <connections>
+                        <outlet property="imageView" destination="1LX-Oj-TiN" id="ni0-hA-iSX"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="hjI-mE-I0S" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="1850.7246376811595" y="1735.5978260869567"/>
+        </scene>
     </scenes>
     <resources>
         <systemColor name="secondaryLabelColor">

+ 69 - 0
Demo/Demo/Kingfisher-Demo/ViewControllers/PHPickerResultViewController.swift

@@ -0,0 +1,69 @@
+//
+//  PHPickerResultViewController.swift
+//  Kingfisher
+//
+//  Created by nuomi1 on 2024-04-17.
+//
+//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+import Foundation
+import Kingfisher
+import PhotosUI
+import UIKit
+
+class PHPickerResultViewController: UIViewController {
+    @IBOutlet var imageView: UIImageView!
+
+    @IBAction func onTapButton() {
+        if #available(iOS 14.0, *) {
+            presentPickerViewController()
+        } else {
+            presentAlertController()
+        }
+    }
+
+    private func presentAlertController() {
+        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
+        let alertController = UIAlertController(title: "Warning!", message: "Only supports iOS 14+", preferredStyle: .alert)
+        alertController.addAction(cancelAction)
+        present(alertController, animated: true)
+    }
+
+    @available(iOS 14.0, *)
+    private func presentPickerViewController() {
+        var configuration = PHPickerConfiguration(photoLibrary: .shared())
+        configuration.filter = .images
+        configuration.selectionLimit = 1
+        let viewController = PHPickerViewController(configuration: configuration)
+        viewController.delegate = self
+        present(viewController, animated: true)
+    }
+}
+
+@available(iOS 14, *)
+extension PHPickerResultViewController: PHPickerViewControllerDelegate {
+    public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
+        picker.dismiss(animated: true)
+        guard let result = results.first else { return }
+        let provider = PHPickerResultImageDataProvider(pickerResult: result)
+        imageView.kf.setImage(with: .provider(provider))
+    }
+}

+ 4 - 0
Demo/Kingfisher-Demo.xcodeproj/project.pbxproj

@@ -8,6 +8,7 @@
 
 /* Begin PBXBuildFile section */
 		072922432638639D0089E810 /* AnimatedImageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072922422638639D0089E810 /* AnimatedImageDemo.swift */; };
+		078DCB512BCFEFB40008114E /* PHPickerResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078DCB502BCFEFB40008114E /* PHPickerResultViewController.swift */; };
 		277EAE8D2045B39C00547CD3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE892045B39C00547CD3 /* Assets.xcassets */; };
 		277EAE8E2045B39C00547CD3 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE8A2045B39C00547CD3 /* Interface.storyboard */; };
 		277EAE9D2045B4D500547CD3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 277EAE962045B4D500547CD3 /* Assets.xcassets */; };
@@ -156,6 +157,7 @@
 
 /* Begin PBXFileReference section */
 		072922422638639D0089E810 /* AnimatedImageDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImageDemo.swift; sourceTree = "<group>"; };
+		078DCB502BCFEFB40008114E /* PHPickerResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultViewController.swift; sourceTree = "<group>"; };
 		277EAE892045B39C00547CD3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		277EAE8B2045B39C00547CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
 		277EAE8C2045B39C00547CD3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -398,6 +400,7 @@
 				C959EEE522874DC600467A10 /* ProgressiveJPEGViewController.swift */,
 				D12EB83D24DD902300329EE1 /* TextAttachmentViewController.swift */,
 				D16CC3D724E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift */,
+				078DCB502BCFEFB40008114E /* PHPickerResultViewController.swift */,
 				D1F78A5E2589F0AA00930759 /* SwiftUIViewController.swift */,
 			);
 			path = ViewControllers;
@@ -721,6 +724,7 @@
 				D1F06F3321AA4292000B1C38 /* DetailImageViewController.swift in Sources */,
 				D198F42225EDC4B900C53E0D /* GridDemo.swift in Sources */,
 				4B1C7A3D21A256E300CE9D31 /* InfinityCollectionViewController.swift in Sources */,
+				078DCB512BCFEFB40008114E /* PHPickerResultViewController.swift in Sources */,
 				D1A1CCA321A1879600263AD8 /* MainViewController.swift in Sources */,
 				4BC0ED4A29A6EE78003E9CD1 /* Issue2035View.swift in Sources */,
 				D1F06F3721AAEACF000B1C38 /* GIFViewController.swift in Sources */,

+ 4 - 0
Kingfisher.xcodeproj/project.pbxproj

@@ -8,6 +8,7 @@
 
 /* Begin PBXBuildFile section */
 		07292245263B02F00089E810 /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07292244263B02F00089E810 /* KFAnimatedImage.swift */; };
+		078DCB4F2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078DCB4E2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift */; };
 		22FDCE0E2700078B0044D11E /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */; };
 		388F37382B4D9CDB0089705C /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388F37372B4D9CDB0089705C /* DisplayLink.swift */; };
 		4B10480D216F157000300C61 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B10480C216F157000300C61 /* ImageDataProcessor.swift */; };
@@ -148,6 +149,7 @@
 
 /* Begin PBXFileReference section */
 		07292244263B02F00089E810 /* KFAnimatedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFAnimatedImage.swift; sourceTree = "<group>"; };
+		078DCB4E2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultImageDataProvider.swift; sourceTree = "<group>"; };
 		185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSButtonExtensionTests.swift; sourceTree = "<group>"; };
 		22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CPListItem+Kingfisher.swift"; sourceTree = "<group>"; };
 		388F37372B4D9CDB0089705C /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = "<group>"; };
@@ -655,6 +657,7 @@
 				D12AB69E215D2BB50013BA68 /* Resource.swift */,
 				D1E56444219B16330057AAE3 /* ImageDataProvider.swift */,
 				D16CC3D524E02E9500F1A515 /* AVAssetImageDataProvider.swift */,
+				078DCB4E2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift */,
 			);
 			path = ImageSource;
 			sourceTree = "<group>";
@@ -850,6 +853,7 @@
 				D12AB714215D2BB50013BA68 /* ImageCache.swift in Sources */,
 				4B88CEB02646C056009EBB41 /* KFImageProtocol.swift in Sources */,
 				D12AB6D0215D2BB50013BA68 /* ImagePrefetcher.swift in Sources */,
+				078DCB4F2BCFEB7D0008114E /* PHPickerResultImageDataProvider.swift in Sources */,
 				388F37382B4D9CDB0089705C /* DisplayLink.swift in Sources */,
 				D12AB6F4215D2BB50013BA68 /* ImageView+Kingfisher.swift in Sources */,
 				D12AB6FC215D2BB50013BA68 /* UIButton+Kingfisher.swift in Sources */,

+ 84 - 0
Sources/General/ImageSource/PHPickerResultImageDataProvider.swift

@@ -0,0 +1,84 @@
+//
+//  PHPickerResultImageDataProvider.swift
+//  Kingfisher
+//
+//  Created by nuomi1 on 2024-04-17.
+//
+//  Copyright (c) 2024 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+import Foundation
+
+#if os(iOS) || os(macOS)
+
+import PhotosUI
+
+/// 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 {
+        /// 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 (Result<Data, Error>) -> Void) {
+        pickerResult.itemProvider.loadDataRepresentation(forTypeIdentifier: contentType.identifier) { data, error in
+            if let error {
+                handler(.failure(error))
+                return
+            }
+
+            guard let data else {
+                handler(.failure(PHPickerResultImageDataProviderError.invalidImage))
+                return
+            }
+
+            handler(.success(data))
+        }
+    }
+}
+
+#endif