如今,个人设备主要用于处理图片、视频和声响,苹果的设备也不例外。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参数指定视图所包括的资源类型。这个结构体的类型特点有burstscinematicVideosdepthEffectPhotosimageslivePhotospanoramasscreenRecordingsscreenshotsslomoVideostimelapseVideosvideospreferredItemEncoding参数指定用于处理资源的编码。这个结构体包括类型特点automatic(默认值)、currentcompatiblephotoLibrary参数供给对图片库的拜访。该结构体带有类型办法shared()label参数是一个闭包,供给视图所生成按钮的标签。

因获取资源会消耗时间,挑选器并不直接回来图片和视频,而是一个稍后可供咱们提取的资源指针。结构为此界说了PhotosPickerItem结构体。该结构体包括如下拜访媒体资源的特点和办法。

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

要拜访结构体,咱们有必要导入PhotosUI结构。此外,视图需求一个@State特点用于存储所选资源。要启用多选,该特点有必要存储PhotosPickerItem结构体的数组,而关于单选,该特点只需求存储一个可选的PhotosPickerItem值。如下所示。

示例18-1:创立一个图片挑选器

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特点显现到屏幕上。

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

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

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

留意:本例中,咱们运用了Data结构体经过loadTransferable()办法传输值。咱们大能够运用Image视图,但它只能接纳PNG图片。更多有关Transferable协议的信息,请阅览第12章拖放手势一节。

默认PhotosPicker视图创立一个在运用顶部翻开视图的按钮,但咱们也能够运用如下修饰符将视图嵌套到界面中。

  • photosPickerStyle(PhotosPickerStyle):这一修饰符指定视图的展现款式。参数是一个具有compactinlinepresentation(默认值)特点的结构体。

presentation款式以弹窗展现视图,上例正是如此。假如期望将视图嵌套到界面中,能够运用compactinline款式。这两个款式很类似,但inline款式供给了更多的选项而且易于拜访内容,如下例所示。

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

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

运用compactinline款式展现的图片挑选器巨细由可用空间决议。也就是说图片挑选器在界面巨细产生改变时会对新的空间进行适配。但咱们能够运用frame()修饰符来设置固定巨细,本例就是这么做的。结果如下所示。

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

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

frame()修饰符之外,咱们也可变运用结构所供给的如下修饰符来装备视图。

  • photosPickerDisabledCapabilities(PHPickerCapabilities):这一修饰符指定对视图排除哪些能力。参数是用于标明能力的一个(或一组)结构体。该结构体包括的特点有collectionNavigationselectionActionssearchsensitivityAnalysisInterventionstagingArea。假如期望包括一切能力能够删去这一修饰符或是指定一个空集合。
  • photosPickerAccessoryVisibility(Visibility, edges: Edge):该修饰符指定是否显现控件。第一个参数指定可见性。它是一个值为的automaticvisiblehidden的枚举。edges参数是一组Edge值,用于指定应删去图片挑选器哪一边的控件。Edge枚举的值有topbottomleadingtrailing

这些修饰符让咱们能够挑选期望包括或躲藏的控件。下例中咱们删去了顶部的导航按钮。

示例18-3:躲藏控件

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

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

图18-3:自界说控件的图片挑选器

在上例中,用户一次仅能挑选一张图片。经过将@State特点界说为PhotosPickerItem结构体数组,能够让用户挑选多张图片。虽然咱们启用多图挑选只需这么做,但有必要考虑在用户撤销挑选时怎么从列表中删去图片。咱们能够清空数组从头载入每张图片,但有些图片的加载可能要花上一些时间。另一个挑选是将图片存在独自的数组中,比较它们值,这样只删去撤销挑选的,而保存其它的。下例中咱们选用的正是这种办法。为此,咱们需求一个带结构体的模型来存储图片及其ID。

示例18-4:界说用于多选的模型

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:答运用户履行多图挑选

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,这样选取的图片会实时更新(用户无需按增加按钮)。

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

图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。(这些值可经过常量kUTTypeImagekUTTypeMovie进行标明。)
  • cameraCaptureMode:该特点设置或回来相机运用的捕获形式。它是UIImagePickerController类中的CameraCaptureMode枚举。可用值有photovideo
  • cameraFlashMode:该特点设置或回来相机的闪光灯形式。它是UIImagePickerController类中的CameraFlashMode枚举。可用的值有onoffauto
  • allowsEditting:该特点设置或回来是否答运用户修改图片的布尔值。
  • videoQuality:该特点设置或回来录制视频质量的值。这是UIImagePickerController类中的QualityType枚举。可用的值有typeHightypeMediumtypeLowtype640x480typeIFrame960x540以及typeIFrame1280x720

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

  • isSourceTypeAvailable(SourceType):该类型办法回来一个标明设备是否支撑所指定数据源的布尔值。其中的参数是UIImagePickerController类中的SourceType枚举。当时可用值仅有camera
  • availableMediaTypes(for: SourceType):该类型办法回来参数指定数据源所支撑媒体类型的字符串数组。其中的参数是UIImagePickerController类中的SourceType枚举。当时可用值仅有camera
  • isCameraDeviceAvailable(CameraDevice):该类型办法回来一个标明参数所指定摄像头是否可用的布尔值。其中的参数是UIImagePickerController类中的CameraDevice枚举。可用值有rearfront

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

该协议包括如下办法。

  • imagePickerController(UIImagePickerController, didFinishPickingMediaWithInfo: Dictionary):该办法在用户完成拍照或录制视频后由署理调用。第二个参数包括一个有关媒体信息的字典。字典中的值经过UIImagePickerController类中的InfoKey结构体的特点进行标识。可用的特点有cropRecteditImageimageURLlivePhotomediaMetadatamediaTypemediaURLoriginalImage
  • imagePickerControllerDidCancel(UIImagePickerController):该办法在用户撤销处理后由署理调用。

图片挑选器放在弹窗中,但假如咱们期望视图点满整个屏幕,可将其嵌套在NavigationStack视图中,经过NavigationLink进行翻开。这正是咱们在下面示例中采纳的办法。界面中包括一个翻开图片挑选器的按钮以及一个显现用户所拍相片的Image视图。

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

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

留意:拜访相机有必要要取得用户的授权。这个进程是自动的,但需求在运用装备的Info面板中增加Privacy - Camera Usage Description选项,设置向用户展现的信息(第5章图5-34)。

图片挑选控制器是一个UIKit视图控制器,因而经过representable视图控制器在SwiftUI界面中显现。为处理相机所捕获的图片,咱们需求增加一个coordinator并完成署理办法。这个coordinator有必要遵循两个协议:UINavigationControllerDelegateUIImagePickerControllerDelegate,如下例所示。

示例18-6:创立图片挑选控制器拍照相片

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:界说拍照的界面

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》