大师学SwiftUI第18章Part1 - 图片选择器和相机

如今,个人设备主要用于处理图片、视频和声音,苹果的设备也不例外。SwiftUI可以通过Image视图显示图片,但需要其它框架的支持来处理图片、在屏幕上展示视频或是播放声音。本章中我们将展示Apple所提供的这类工具。

图片选择器

SwiftUI内置了一个PhotosPicker结构体用于生成一个视图,允许用户从图片库中选择一张或多张照片。以下为该视图的初始化方法。

  • PhotosPicker (selection : Binding, maxSelectionCount : Int?, selectionBehavior : PhotosPickerSelectionBehavior, matching : PHPickerFilter?, preferredItemEncoding : EncodingDisambiguationPolicy, photoLibrary : PHPhotoLibrary, label : Closure):这一初始化方法通过由参数所指定的配置创建一个PhotosPicker视图。selection参数是一个存储所选项指针的绑定属性。maxSelectionCount参数是我们希望用户选取的最大图片数。selectionBehavior参数指定如何进行选取。该结构体具有类型属性default(复选框选取)、ordered(数字选取)、continous(实时选取)和continousAndOrdered(实时数字选择)。matching参数指定视图所包含的资源类型。这个结构体的类型属性有bursts、cinematicVideos、depthEffectPhotos、images、livePhotos、panoramas、screenRecordings、screenshots、slomoVideos、timelapseVideos和videos。preferredItemEncoding参数指定用于处理资源的编码。这个结构体包含类型属性automatic(默认值)、current和compatible。photoLibrary参数提供对图片库的访问。该结构体带有类型方法shared()。label参数是一个闭包,提供视图所生成按钮的标签。

因获取资源会耗费时间,选择器并不直接返回图片和视频,而是一个稍后可供我们提取的资源指针。框架为此定义了PhotosPickerItem结构体。该结构体包含如下访问媒体资源的属性和方法。

  • itemIdentifier:该属性返回资源标识符的字符串。
  • loadTransferable (type : Type):这一异步方法加载资源并将其赋值给由type参数指定数据类型的实例。这个参数的数据类型必须遵循Transferable协议。

要访问ç结构体,我们必须导入PhotosUI 框架。此外,视图需要一个@State属性用于存储所选资源。要启用多选,该属性必须存储PhotosPickerItem结构体的数组,而对于单选,该属性只需要存储一个可选的PhotosPickerItem值。如下所示。

示例18-1:创建一个图片选择器

less 复制代码
import SwiftUI
import PhotosUI

struct ContentView: View {
    @State private var selected: PhotosPickerItem?
    @State private var picture: UIImage?
    var body: some View {
        NavigationStack {
            VStack {
                Image(uiImage: picture ?? UIImage(named: "nopicture")!)
                    .resizable()
                    .scaledToFit()
                Spacer()
                PhotosPicker(selection: $selected, matching: .images, photoLibrary: .shared()) {
                    Text("Select a photo")
                        .padding()
                        .buttonStyle(.borderedProminent)
                }
                .onChange(of: selected, initial: false) { old, item in
                    Task(priority: .background) {
                        if let data = try? await item?.loadTransferable(type: Data.self) {
                            picture = UIImage(data: data)
                        }
                    }
                }
            }
        }
    }
}

PhotosPicker初始化方法中的大部分参数都是可选的。本例中,我们只需要告诉选择在哪里存储所选资源的指针,需要对用户显示哪种资源(图片),以及从哪里获取(共享库)。

PhotosPicker结构体创建了一个打开选取资源视图的按钮。在视图中选中资源后,指针会存储到@State属性中。这意味着我们可以通过onChange()修饰符监控属性的变化。在选中新图片后,我们开启一个异步任务对所选资源调用loadTransferable()方法。该方法加载图片,将其转换成一个Data结构体返回。如果成功,我们使用这个数据初始化一个UIImage对象,并将其赋值给picture属性显示到屏幕上。

图18-1:图片库界面(中间图)

✍️跟我一起做:创建一个多平台项目。使用示例18-1 中的代码更新ContentView视图。下载nopicture.png并将其添加到Assets中。点击Select a photo 按钮。点击选中图片,图片会被赋给Image视图并显示到屏幕上,如图18-1所示(右图)。

注意 :本例中,我们使用了Data结构体通过loadTransferable()方法传输值。我们大可以使用Image视图,但它只能接收PNG图片。更多有关Transferable协议的信息,请阅读第12章拖放手势一节。

默认PhotosPicker视图创建一个在应用顶部打开视图的按钮,但我们也可以使用如下修饰符将视图嵌套到界面中。

  • photosPickerStyle (PhotosPickerStyle):这一修饰符指定视图的展现样式。参数是一个具有compact、inline和presentation(默认值)属性的结构体。

presentation样式以弹窗展示视图,上例正是如此。如果希望将视图嵌套到界面中,可以使用compact和inline样式。这两个样式很相似,但inline样式提供了更多的选项并且易于访问内容,如下例所示。

示例18-2:在界面中嵌套图片选择器

scss 复制代码
PhotosPicker(selection: $selected, matching: .images, photoLibrary: .shared()) {
    Text("Select a photo")
        .buttonStyle(.borderedProminent)
        .photosPickerStyle(.inline)
        .frame(height: 300)
}

使用compact和inline样式展示的图片选择器大小由可用空间决定。也就是说图片选择器在界面大小发生改变时会对新的空间进行适配。但我们可以使用frame()修饰符来设置固定大小,本例就是这么做的。结果如下所示。

图18-2:内联图片选择器

除frame()修饰符之外,我们也可变使用框架所提供的如下修饰符来配置视图。

  • photosPickerDisabledCapabilities (PHPickerCapabilities):这一修饰符指定对视图排除哪些能力。参数是用于表示能力的一个(或一组)结构体。该结构体包含的属性有collectionNavigation、selectionActions、search、sensitivityAnalysisIntervention和stagingArea。如果希望包含所有能力可以删除这一修饰符或是指定一个空集合。
  • photosPickerAccessoryVisibility (Visibility, edges : Edge):该修饰符指定是否显示控件。第一个参数指定可见性。它是一个值为的automatic、visible和hidden的枚举。edges参数是一组Edge值,用于指定应删除图片选择器哪一边的控件。Edge枚举的值有top、bottom、leading和trailing。

这些修饰符让我们可以选择希望包含或隐藏的控件。下例中我们删除了顶部的导航按钮。

示例18-3:隐藏控件

scss 复制代码
PhotosPicker(selection: $selected, matching: .images, photoLibrary: .shared()) {
    Text("Select a photo")}
    .buttonStyle(.borderedProminent)
    .photosPickerStyle(.inline)
    .frame(height: 300)
    .photosPickerDisabledCapabilities([.collectionNavigation])

图18-3:自定义控件的图片选择器

在上例中,用户一次仅能选择一张图片。通过将@State属性定义为PhotosPickerItem结构体数组,可以让用户选择多张图片。虽然我们启用多图选择只要这么做,但必须考虑在用户取消选择时如何从列表中删除图片。我们可以清空数组重新载入每张图片,但有些图片的加载可能要花上一些时间。另一个选择是将图片存在单独的数组中,比较它们值,这样只删除取消选择的,而保留其它的。下例中我们采用的正是这种方法。为此,我们需要一个带结构体的模型来存储图片及其ID。

示例18-4:定义用于多选的模型

typescript 复制代码
import SwiftUI
import Observation
import PhotosUI

struct ItemsData: Identifiable {
    var id: String
    var image: UIImage
}
@Observable class ApplicationData {
    var listPictures: [ItemsData] = []
    var selected: [PhotosPickerItem] = []
    
    func removeDeselectedItems() {
        listPictures = listPictures.filter{ value in
            if selected.contains(where: { $0.itemIdentifier == value.id }) {
                return true
            } else {
                return false
            }
        }
    }
    func addSelectedItems() {
        for item in selected {
            Task(priority: .background) {
                if let data = try? await item.loadTransferable(type: Data.self) {
                    if let id = item.itemIdentifier, let image = UIImage(data: data) {
                        if !listPictures.contains(where: { $0.id == id}) {
                            let newPicture = ItemsData(id: id, image: image)
                            await MainActor.run {
                                listPictures.append(newPicture)
                            }
                        }
                    }
                }
            }
        }
    }
}

以下模型包含两个observable属性,一个用于存储ItemsData结构体数组,将当前选中的图片发送给视图,另一个PhotosPickerItem结构体数组用于为PhotosPicker视图存储选中图片的指针。

模型中还有两个方法:removeDeselectedItems()和addSelectedItems()。两者都在用户修改选项时执行(即每当selected属性值发生改变时)。removeDeselectedItems()方法迭代listPictures数组中的各项,检查哪些是用户选中的图片,所以用户取消选择的图片就不再位于列表中。而addSelectedItems()方法将用户选中的图片添加到listPictures数组中。现在视图可以使用listPictures数组来显示在屏幕上选择的图片,在每次选项发生更改时调用这两个方法。

示例18-5:允许用户执行多图选择

scss 复制代码
struct ContentView: View {
    @Environment(ApplicationData.self) private var appData
    
    let guides = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
            VStack {
                ScrollView {
                    LazyVGrid(columns: guides) {
                        ForEach(appData.listPictures) { item in
                            Image(uiImage: item.image)
                                .resizable()
                                .scaledToFit()
                        }
                    }
                }
                .padding()
                Spacer()
                PhotosPicker(selection: Bindable(appData).selected, maxSelectionCount: 4, selectionBehavior: .continuous, matching: .images, photoLibrary: .shared()) {
                    Text("Select Photos")
                }
                .photosPickerStyle(.inline)
                .photosPickerDisabledCapabilities(.selectionActions)
            }
            .onChange(of: appData.selected, initial: false) { old, items in
                appData.removeDeselectedItems()
                appData.addSelectedItems()
            }
    }
}

本例中,我们配置了最多允许选择4张图片,但这么做没什么必要。如果不设置上限,用户可以选择希望添加的所有图片。注意因为图片选择器内嵌在了界面中,并不需要使用选择按钮,选择行为设置为了continous,这样选取的图片会实时更新(用户无需按添加按钮)。

图18-4:图片多选

✍️跟我一起做:使用示例18-4 中的代码创建一个ApplicationData.swift文件。再用示例18-5 中的代码更新ContentView视图。不要忘记把ApplicationData注入到应用的环境和预览中(参见第7章示例7-4 )。选择多张图片,会看到选中的图片实时更新,如图18-4所示。

相机

移动设备最常见的用途之一是拍摄、存储照片,因此现在设备都有带摄像头。因为应用访问相机和管理图片都是很常规的操作。UIKit内置了控制器为用户提供所有拍摄照片和视频所需的工具。用于创建这一控制器的类是UIImagePickerController。以下是用于配置该类的一些属性。

  • sourceType :该属性设置或返回希望用于获取图片出处的类型。它是UIImagePickerController类一个SourceType枚举。当前,可用值仅有camera。
  • mediaTypes :该属性设置或返回我们希望处理的媒体类型。它接收一个字符串数组,值表示希望使用的所有媒体。最常见的为用于图片的public.image和用于视频的public.movie。(这些值可通过常量kUTTypeImage和kUTTypeMovie进行表示。)
  • cameraCaptureMode :该属性设置或返回相机使用的捕获模式。它是UIImagePickerController类中的CameraCaptureMode枚举。可用值有photo和video。
  • cameraFlashMode :该属性设置或返回相机的闪光灯模式。它是UIImagePickerController类中的CameraFlashMode枚举。可用的值有on、off和auto。
  • allowsEditting:该属性设置或返回是否允许用户编辑图片的布尔值。
  • videoQuality :该属性设置或返回录制视频品质的值。这是UIImagePickerController类中的QualityType枚举。可用的值有typeHigh、typeMedium、typeLow、type640x480、typeIFrame960x540以及typeIFrame1280x720。

UIImagePickerController类还提供了如下类型方法用于检测可用数据源以及其可管理的媒体类型。

  • isSourceTypeAvailable (SourceType):该类型方法返回一个表明设备是否支持所指定数据源的布尔值。其中的参数是UIImagePickerController类中的SourceType枚举。当前可用值仅有camera。
  • availableMediaTypes (for : SourceType):该类型方法返回参数指定数据源所支持媒体类型的字符串数组。其中的参数是UIImagePickerController类中的SourceType枚举。当前可用值仅有camera。
  • isCameraDeviceAvailable (CameraDevice):该类型方法返回一个表明参数所指定摄像头是否可用的布尔值。其中的参数是UIImagePickerController类中的CameraDevice枚举。可用值有rear和front。

UIImagePickerController类创建一个用户可拍照或录制视频的视图。在创建完成图片或视频创建后,必须要释放该视图以及处理媒体资料。代码访问媒体资料以及知晓何时释放视图是借助于一个遵循UIImagePickerControllerDelegate协议的代理。

该协议包含如下方法。

  • imagePickerController (UIImagePickerController, didFinishPickingMediaWithInfo : Dictionary):该方法在用户完成拍照或录制视频后由代理调用。第二个参数包含一个有关媒体信息的字典。字典中的值通过UIImagePickerController类中的InfoKey结构体的属性进行标识。可用的属性有cropRect、editImage、imageURL、livePhoto、mediaMetadata、mediaType、mediaURL和originalImage。
  • imagePickerControllerDidCancel(UIImagePickerController):该方法在用户取消处理后由代理调用。

图片选择器放在弹窗中,但如果我们希望视图点满整个屏幕,可将其嵌套在NavigationStack视图中,通过NavigationLink进行打开。这正是我们在下面示例中采取的方法。界面中包含一个打开图片选择器的按钮以及一个显示用户所拍照片的Image视图。

图18-5:使用相机的界面

注意:访问相机必须要获得用户的授权。这个过程是自动的,但需要在应用配置的Info面板中添加Privacy - Camera Usage Description选项,设置向用户展示的信息(第5章 ,图5-34)。

图片选择控制器是一个UIKit视图控制器,因此通过representable视图控制器在SwiftUI界面中显示。为处理相机所捕获的图片,我们需要添加一个coordinator并实现代理方法。这个coordinator必须遵循两个协议:UINavigationControllerDelegate和UIImagePickerControllerDelegate,如下例所示。

示例18-6:创建图片选择控制器拍摄照片

swift 复制代码
import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var path: NavigationPath
    @Binding var picture: UIImage?
    
    func makeUIViewController(context: Context) ->  UIImagePickerController {
        let mediaPicker = UIImagePickerController()
        mediaPicker.delegate = context.coordinator
        if UIImagePickerController.isSourceTypeAvailable(.camera) {
            mediaPicker.sourceType = .camera
            mediaPicker.mediaTypes = ["public.image"]
            mediaPicker.allowsEditing = false
            mediaPicker.cameraCaptureMode = .photo
        } else {
            print("The media is not available")
        }
        return mediaPicker
    }
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
    func makeCoordinator() -> ImagePickerCoordinator {
        ImagePickerCoordinator(path: $path, picture: $picture)
    }
}
class ImagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    @Binding var path: NavigationPath
    @Binding var picture: UIImage?
    
    init(path: Binding<NavigationPath>, picture: Binding<UIImage?>) {
        self._path = path
        self._picture = picture
    }
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let newpicture = info[.originalImage] as? UIImage {
            picture = newpicture
        }
        path = NavigationPath()
    }
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        path = NavigationPath()
    }
}

这一representable视图控制器创建一个UIImagePickerController类的实例,并将ImagePickerCoordinator赋为其代理 。接着检查相机是否就绪,成功后配置控制器或是失败在控制台打印消息。将camera赋给sourceType属性来告诉控制器通过相机获取图像,对mediaTypes属性赋一个public.image数组,指定获取的图片,allowEditting设置为false阻止用户编辑图片,cameraCaptureMode赋了值photo来允许用户仅捕获图像。

相机界面包含控制相机和捕获图像的按钮。用户拍完照后,会出现一组新的按钮,允许用户选择图像或再拍一张。如果用户决定使用当前图片,控制器会对代理调用imagePickerController(didFinishPickingMediaWithInfo:)方法。该方法接收一个参数info,可读取它来获取控制器返回的媒体资源并进行处理(保存到文件、数据库或在屏幕上显示)。本例中,我们读取originalImage键的值来获取用户拍摄图片的UIImage对象,将对象赋给@State属性使其在视图中可用。注意我们还在coordinator中实现了imagePickerControllerDidCancel()方法来在用户点击Cancel按钮时释放控制器。

视图中必须包含一下打开图片选择控制器的按钮以及一个展示用户拍摄照片的Image视图。

示例18-7:定义拍照的界面

less 复制代码
struct ContentView: View {
    @State private var path = NavigationPath()
    @State private var picture: UIImage?
    
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                HStack {
                    Spacer()
                    NavigationLink("Get Picture", value: "Open Picker")
                }.navigationDestination(for: String.self, destination: { _ in
                    ImagePicker(path: $path, picture: $picture)
                })
                Image(uiImage: picture ?? UIImage(named: "nopicture")!)
                    .resizable()
                    .scaledToFit()
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                    .clipped()
                Spacer()
            }.padding()
        }.statusBarHidden()
    }
}

这个视图创建了一个ImagePicker结构体的实例,将其声明为NavigationLink按钮的目的地。点击按钮时打开视图。如果用户拍好照并决定使用它,该图片通过代理方法赋值给picture属性,屏幕上显示的Image视图也进行了更新。

✍️跟我一起做:创建一个多平台项目。使用示例18-6 中的代码创建ImagePicker.swift文件。使用示例18-7 中的代码更新ContentView.swift文件。下载nopicture.png文件放到资源目录中。在应用配置的Info面板中使用希望对用户显示的文字添加Privacy - Camera Usage Description选项。在设备上运行应用,点击按钮。拍照并按下按钮使用这张照片。会在屏幕中看到这张照片。

代码请见:GitHub仓库

本文首发地址:AlanHou的个人博客,整理自2023年10月版《SwiftUI for Masterminds》

相关推荐
wangruofeng1 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng1 小时前
给前端开发者的 100 个设计参考网站
前端·设计
星栈4 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技5 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569155 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马7 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng8 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding9 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js
鶴哥只手遮天9 小时前
《Electron 45.2.3 内核解剖》第 4 章 补丁体系:380 个补丁改了什么
前端·electron
FungLeo11 小时前
成为全栈·Vue 管理后台篇·分类树与标签管理:层级数据如何落到管理界面
前端·javascript·vue.js·分类树·naive ui·标签管理·前端数据结构