SwiftUI 照片/视频选择 PhotosPicker

所属分类:ios | 发布于 2025-04-08

SwiftUI提供了PhotosPicker组件用来选择照片/视频,在iOS16又提供了.photosPicker()修饰符来简化照片旋转流程。

1、PhotosPicker组件初体验

1.1、导入PhotosUI

import PhotosUI

1.2、创建PhotosPickerItem变量

@State private var photosPickerItem: PhotosPickerItem? = nil

1.3、调用PhotosPicker

PhotosPicker(selection: $photosPickerItem) {
    Image(systemName: "plus")
}

1.4、完整代码

import SwiftUI
import PhotosUI

struct MeetPhotosPicker: View {
    @State private var photosPickerItem: PhotosPickerItem? = nil
    var body: some View {
        PhotosPicker(selection: $photosPickerItem) {
            Image(systemName: "plus")
        }
    }
}

这样点击➕就可以选择照片了。

2、.photosPicker()修饰符

在iOS16、macOS13增加了.photosPicker()修饰符,作用同PhotosPicker()组件

2.1、创建一个presented变量

@State private var isPickerPresented: Bool = false
@State private var photosPickerItem: PhotosPickerItem? = nil

2.2、调用.photosPicker修饰符

Button(action: {
    isPickerPresented.toggle()
}, label: {
    Image(systemName: "plus")
})
.photosPicker(isPresented: $isPickerPresented, selection: $photosPickerItem)

2.3、完整代码

import SwiftUI
import PhotosUI

struct MeetPhotosPicker: View {
    @State private var isPickerPresented: Bool = false
    @State private var photosPickerItem: PhotosPickerItem? = nil
    
    var body: some View {
        Button(action: {
            isPickerPresented.toggle()
        }, label: {
            Image(systemName: "plus")
        })
        .photosPicker(isPresented: $isPickerPresented, selection: $photosPickerItem)
    }
}

3、matching参数

matching的参数类型是 PHPickerFilter

public struct PHPickerFilter : Equatable, Hashable, @unchecked Sendable {

    public static let images: PHPickerFilter
    public static let videos: PHPickerFilter
    public static let livePhotos: PHPickerFilter
    public static let depthEffectPhotos: PHPickerFilter
    public static let bursts: PHPickerFilter
    public static let panoramas: PHPickerFilter
    public static let screenshots: PHPickerFilter
    public static let screenRecordings: PHPickerFilter
    public static let slomoVideos: PHPickerFilter
    public static let timelapseVideos: PHPickerFilter
    public static let cinematicVideos: PHPickerFilter
    public static let spatialMedia: PHPickerFilter

    public static func playbackStyle(_ playbackStyle: PHAsset.PlaybackStyle) -> PHPickerFilter

    public static func any(of subfilters: [PHPickerFilter]) -> PHPickerFilter
    public static func all(of subfilters: [PHPickerFilter]) -> PHPickerFilter
    public static func not(_ filter: PHPickerFilter) -> PHPickerFilter

    public static func == (a: PHPickerFilter, b: PHPickerFilter) -> Bool
    public func hash(into hasher: inout Hasher)
    public var hashValue: Int { get }
}

4、选择照片处理

4.1、选择照片基本结构

import SwiftUI
import PhotosUI

struct MeetPhotosPicker: View {
    @State private var photosPickerItem: PhotosPickerItem? = nil
    @State private var pickerImage: Image?
    
    var body: some View {
        pickerImage?
            .resizable()
            .scaledToFit()
            .frame(width: 300, height: 300)
        
        PhotosPicker(selection: $photosPickerItem) {
            Label("Choose Photo", systemImage: "plus")
        }
        .onChange(of: photosPickerItem) {
            Task {
                // 处理选择的图片
                // todo
            }
        }
}

后面的几个分支只是图片处理的方式不同,都写在Task{ }代码块里面。

4.2、方式一:Transferable选择Image

// 方式一
if let loaded = try? await photosPickerItem?.loadTransferable(type: Image.self) {
    pickerImage = loaded
}

代码非常简单,多试几张图片之后,就会发现有的图片方向上下反了。实际上普通的照片会正常显示,如果是HDR照片,显示方向则会颠倒。

结论:SwiftUI 的 Image Transferable 只支持 PNG 类型的照片,如果是其它类型的照片,则可能丢失一些信息,比如 orientation 信息。

4.3、方式二:Transferable选择Data

把pickerImage类型由Image更改为UIImage

struct MeetPhotosPicker: View {
    @State private var photosPickerItem: PhotosPickerItem? = nil
    @State private var pickerImage: UIImage?
    
    var body: some View {
        if let pickerImage = pickerImage {
            Image(uiImage: pickerImage)
                .resizable()
                .scaledToFit()
                .frame(width: 300, height: 300)
        }
        
        
        PhotosPicker(selection: $photosPickerItem) {
            Label("Choose Photo", systemImage: "plus")
        }
        .onChange(of: photosPickerItem) {
            Task {
                // 处理选择的图片
            }
        }
        
    }
}

图片处理代码:

// 方式二
if let photosPickerItem,
   let data = try? await photosPickerItem.loadTransferable(type: Data.self) {
    if let image = UIImage(data: data) {
        self.pickerImage = image
    }
}

这种方式已经可以了,但是我们还可以再封装一下。

4.4、进阶,自定义Transferable, 自定义UIImageTransferable

struct UIImageTransferable: Transferable {
    let image: UIImage
    
    enum TransferError: Error {
        case importFailed
    }
    
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(importedContentType: .image) { data in
            guard let uiImage = UIImage(data: data) else {
                throw TransferError.importFailed
            }
            return UIImageTransferable(image: uiImage)
        }
    }
}

调用:

// 方式三
if let photosPickerItem,
   let pickerData = try? await photosPickerItem.loadTransferable(type: UIImageTransferable.self) {
    self.pickerImage = pickerData.image
}

4.5、进阶,自定义ImageTransferable

struct ImageTransferable: Transferable {
    let image: Image
    
    enum TransferError: Error {
        case importFailed
    }
    
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(importedContentType: .image) { data in
            guard let uiImage = UIImage(data: data) else {
                throw TransferError.importFailed
            }
            let image = Image(uiImage: uiImage)
            return ImageTransferable(image: image)
        }
    }
}

这个Transferable只是对上一个的进一步封装,将返回由UIImage变成了Image。

6、选择视频处理

视频需要自定义一个Transferable。

6.1、自定义VideoTransferable

import SwiftUI

struct VideoTransferable: Transferable {
    let url: URL
    
    static var transferRepresentation: some TransferRepresentation {
        FileRepresentation(importedContentType: .movie) { receivedTransferredFile in
            let originalFile = receivedTransferredFile.file
//          let copiedFile = URL.documentsDirectory.appending(path: "videoPicker.mov")
            let copiedFile = URL.documentsDirectory
                .appendingPathComponent("movie")
                .appendingPathExtension("mp4")
            
            /// Checking fi aleady file Exists at the Path
            if FileManager.default.fileExists(atPath: copiedFile.path()) {
                /// Deleting Old File
                try FileManager.default.removeItem(at: copiedFile)
            }
            /// Copyiing File
            try FileManager.default.copyItem(at: originalFile, to: copiedFile)
            /// Passing the Copied File Path
            return .init(url: copiedFile)
        }
    }
}

6.2、完整调用

import SwiftUI
import PhotosUI

struct MeetPhotosPicker: View {
    @State private var isPickerPresented: Bool = false
    @State private var photosPickerItem: PhotosPickerItem? = nil
    @State private var pickerVideoURL: URL?

    var body: some View {
        if let url = pickerVideoURL {
            Text("Url: \(url.absoluteString)")
        }

        Button(action: {
            isPickerPresented.toggle()
        }, label: {
            Label("Choose Video", systemImage: "plus")
        })
        .photosPicker(isPresented: $isPickerPresented, selection: $photosPickerItem)
        .onChange(of: photosPickerItem) {
            Task {
                if let photosPickerItem,
                   let pickerData = try? await photosPickerItem.loadTransferable(type: VideoTransferable.self) {
                    pickerVideoURL = pickerData.url
                }
            }
        }
    }
}

6.3、删除临时文件(如果需要)

.onDisappear {
    Task {
        if let url = pickerVideoURL {
            try? FileManager().removeItem(at: url)
        }
    }
}

文哥博客(https://wenge365.com)属于文野个人博客,欢迎浏览使用

联系方式:qq:52292959 邮箱:52292959@qq.com

备案号:粤ICP备18108585号 友情链接