Swift + SwiftUI原生iOS开发 开发笔记1 – 实现摄像头调用和拍摄图片
背景
我的毕业设计是《Development of an AI-Powered Mobile Application for Animal Identification and Information》,开发一个可以识别动物的移动软件,在跟导师沟通后打算用Yolo作为深度学习识别模型,移动端软件使用原生的iOS开发,即在XCode上使用Swift+SwiftUI进行iOS开发,原本就一直对iOS开发很感兴趣,借这次机会好好学习一下原生的iOS开发。
软件构思
我借鉴了iOS上Ultralytics YOLO的设计,抛开繁琐的按钮,点开软件就是摄像头,直接进行识别,围绕识别结果增加其他功能。
除此之外,我还希望增加一个拍照按钮,获取当前的图片,这样就可以对这张特定的图片做分析,提升了用户的体验。
所以一开始的目标就是调用摄像头并显示,增加一个拍照按钮,实现点击按钮后获取当前的图片。
开始代码
主文件和主视图文件
首先先新建项目,在建完项目后,XCode自动生成了Animal_Recognition.swift作为主文件,里面的代码如下。
// 主文件,入口函数
import SwiftUI
@main
struct Animal_RecognitionApp: App {
// WindowGroup是SwiftUI中的一种Scene,用于管理多个窗口或者视图
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
在Swift当中,好像同一目录下的各个文件里的结构体或者类都可以直接用类名调用,具体没有测试过,但是目前来看好像是如此。
还有一个ContentView.swift就是对应的主视图,接下来就对ContentView视图进行修改,实现摄像头。
struct ContentView: View {
@State private var capturedImage: IdentifiableImage? = nil
@State private var isCameraActive = true
var body: some View {
NavigationView {
ZStack {
if isCameraActive {
CameraPreview(capturedImage: $capturedImage)
.edgesIgnoringSafeArea(.all)
}
NavigationLink(destination: ResultView()) {
Text("点击这里进入下一页")
}
VStack {
Spacer()
Button(action: {
NotificationCenter.default.post(name: .takePhoto, object: nil)
}) {
Circle()
.fill(Color.white)
.frame(width: 70, height: 70)
.overlay(Circle().stroke(Color.black, lineWidth: 2))
.shadow(radius: 10)
}
.padding(.bottom, 30)
}
}
.sheet(item: $capturedImage) { identifiableImage in
VStack {
Image(uiImage: identifiableImage.image)
.resizable()
.scaledToFit()
Button("关闭") {
isCameraActive = true
capturedImage = nil
}
}
}
}
}
}
对于这部分代码有若干个点是与其他常规代码不同的。
- @State
- <变量名>: <变量类型>? = nil
- ZStack, VStack, HStack
- .edgesIgnoringSafeArea(.all)
- NavigationLink(destination: <视图>)
- Spacer()
- NotificationCenetr.default.post(name: <关键词>, object: <关键字值>)
- .sheet(item: <变量>)
@State
在SwiftUI中,对于页面刷新的提供的方法是,给变量加上@State修饰,使得当这个变量更新后自动刷新页面。
<变量名>: <变量类型>? = nil
为变量类型后加上一个?
可以使其变成可选变量,可选变量是指可以为nil(NULL),即空值的变量,可以用于判断是否有值,若不是可选变量,则不可为nil。
ZStack, VStack, HStack
这三个玩意是SwiftUI里构建页面最基本的三个视图结构,分别是用于构建重叠视图(ZStack)、上下视图(VStack)、左右视图(HStack),类似css当中的flex方法,可以设定为row或column,但是对于重叠元素,SwiftUI提供了ZStack,我个人认为更加的方便。
图片来源:
https://developer.apple.com/cn/documentation/swiftui/building-layouts-with-stack-views/
.edgesIgnoringSafeArea(.all)
对视图的修饰和调整,SwiftUI都是采用在视图的最后添加.<修饰类(修饰值)>的方法,包括大小、内距等,在本项目中主页的摄像头需要占据整个屏幕,所以需要使用.edgesIgnoringSafeArea(.all)对摄像头视图进行修饰,使其占据整个屏幕。
NavigationLink(destination: <视图>)
在SwiftUI中提供了一个在不同页面跳转的方式,即Navigation视图,在Navigation视图中可以增加NavigationLink方法,并在参数destination中指定需要跳转的视图,之后在NavigationLink视图里的视图会被增加一个类似超链接的标志,点击后就会跳转到二级视图。
Spacer()
在SwiftUI中,所有元素默认都是垂直居中对齐的,要实现其他对齐方式就需要使用Spacer()方法,Spacer会使得当前所在的空间全部被占用,这样就可以把其他视图“挤”到其他地方去,从而实现其他对齐方式。
图片来源:
https://developer.apple.com/cn/documentation/swiftui/building-layouts-with-stack-views/
NotificationCenetr.default.post(name: <关键词>, object: <关键字值>)
在本项目中要实现按钮点击拍照,所以需要有按钮触发事件,为了项目的低耦合性,图像获取需要和主视图区分开来,所以需要在新的文件里的新的结构体或类中实现,而要实现和主视图的按钮事件结合起来就需要用类似ROS消息系统的工具实现。
在SwiftUI中就提供了NotificationCenter工具,实现信息发送和获取(实际上是Objective-C实现的,所以在应用的过程中需要声明Objective-C的相关代码)。
其中default.post意思是用default这个信息处理来发送值。
由于是自定义的关键字,所以需要对Notification的Name对象进行拓展
extension Notification.Name {
static let takePhoto = Notification.Name("takePhoto")
}
.sheet(item: <变量>)
本项目需要实现点击拍摄后转到拍摄的图片视图,SwiftUI提供了.sheet工具,当参数item所对应的变量与之前不同之后,就会跳出.sheet的视图。
但是运行后出现报错,UIImage不满足Equatable协议,这个原因是.sheet里的item对应的变量需要满足Equatable协议,即需要能够被比较出旧值和新值,即需要实现<变量> == <变量>,为了解决这个问题,我们可以新建一个结构体用于封装UIImage和实现可比较性,以此满足Equatable协议。
struct IdentifiableImage: Identifiable {
var id = UUID()
var image: UIImage
static func == (lhs: IdentifiableImage, rhs: IdentifiableImage) -> Bool {
return lhs.id == rhs.id && lhs.image == rhs.image
}
}
为UIImage添加一个id作为唯一标识符,并且实现==函数以满足Equtatble协议。
CameraView文件实现获取摄像头实时数据
主要有三个结构体:
- CameraView():用于实现实时摄像头数据的视图
- CameraPreview():用于实现拍照的图像的视图
- CameraViewController():用于获取摄像头数据
UIViewControllerRepresentable和UIViewController
在iOS开发中,SwiftUI是一个声明式框架,本身并不具有实现API调用等功能,就以本项目为例,获取摄像头数据还是需要基于Objective-C的AVCapturePhotoCaptureDelegate,来实现,而要使得Objective-C的数据能以SwiftUI视图显示出来,就需要UIViewControllerRepresentable类来通过Coordinate方法与UIViewController连接,所以才会需要三个结构体。
对于CameraView()和CameraPreview()结构体里需要做的事主要是定义好Coordinator和CameraViewController进行桥接以获取所需的数据,并且在Coordinator中使用对应的回调函数对获取的数据进行处理。
其中将CameraPreview()中获取的图片数据和主视图连接起来靠的是修饰词@Binding,@Binding修饰的变量会作为参数从主视图中传进来,在CameraPreview()结构体中,对该变量赋值就可以修改主视图中的同一个变量啦。
在CameraViewController中,主要用四个框架用于对摄像头进行操作。
private var captureSession: AVCaptureSession!
private var photoOutput: AVCapturePhotoOutput!
private var videoPreviewLayer: AVCaptureVideoPreviewLayer!
private var videoDataOutput: AVCaptureVideoDataOutput!
在其中通过NotificationObservers获取主视图的按钮事件,得到信号后对图片进行获取。
实现摄像头数据的实时获取可以启动一个AVCaptureSession,实时的获取图像,并且利用对应的回调函数实现后期的对图像进行处理。
成果
现在已经可以实现摄像头实时获取和点击拍照显示照片的功能了,但是还存在以下缺点:
启动摄像头缓慢,第一次启动摄像头会有一段时间的黑屏
点击拍照后会有一段时间的等待
总结
这次的实践很成功,在面向ChatGPT编程的开发后,成功完成了基本功能,也是为数不多的不用带着问题过夜的一次开发,接下来我会随着项目的开发继续记录我的经历,希望可以有所帮助。