第65天 项目 13 第四部分
今天我们将把你刚刚学到的一些技巧付诸实践,包括如何创建自定义绑定、如何将用户选择的图像导入应用程序,以及如何对图像应用Core Filter效果。
希望你能注意到一点:我经常会带你回顾之前学过的主题。这并非偶然——正如自助类书籍作家拿破仑·希尔所写:“任何想法、计划或目标都可以通过反复思考植入脑海”。这种重复是我帮助你理解所有代码如何整合在一起的多种方法之一。
即便你今天未能完全理解这些概念,也没关系——我们在未来还会再次探讨它们。
今天你需要完成三个主题的实践,在这些主题中,你将练习使用PhotosPicker、通过Core Image过滤图像等内容。
- 构建基础用户界面(UI)
- 使用PhotosPicker将图像导入SwiftUI
- 使用Core Image进行基础图像过滤
构建基础用户界面(UI)
作者:Paul Hudson 2023年12月12日
我们项目的第一步是构建基础用户界面,该界面包含以下元素:
- 一个
NavigationStack,用于在顶部显示应用名称。 - 一个提示用户选择照片的区域,用户导入的图片将显示在该区域上方。
- 一个“强度”(Intensity)滑块,用于控制Core Image滤镜的应用强度,取值范围为0.0到1.0。
- 一个分享按钮,用于将处理后的图像导出应用程序。
一开始,我们不会把所有这些元素都添加进去,先添加足够的内容,让你理解各部分如何配合工作即可。
最初,用户尚未选择图像,因此我们将使用一个@State可选图像属性来表示这种状态。
首先,在ContentView中添加以下两个属性:
@State private var processedImage: Image?
@State private var filterIntensity = 0.5然后,将其body属性的内容修改为:
NavigationStack {
VStack {
Spacer()
// 图像区域
Spacer()
HStack {
Text("Intensity")
Slider(value: $filterIntensity)
}
.padding(.vertical)
HStack {
Button("Change Filter") {
// 切换滤镜
}
Spacer()
// 分享图片
}
}
.padding([.horizontal, .bottom])
.navigationTitle("Instafilter")
}这里使用了两个Spacer,确保图像区域上下都有空间,同时也能让滤镜控制项固定在屏幕底部。
对于// 图像区域注释所在的位置,需要根据情况显示两种内容:如果用户已选择图像,则显示该图像;否则,显示一个简单的ContentUnavailableView,让用户知道该区域并非意外空白:
if let processedImage {
processedImage
.resizable()
.scaledToFit()
} else {
ContentUnavailableView("No Picture", systemImage: "photo.badge.plus", description: Text("Tap to import a photo"))
}我很喜欢在SwiftUI布局中直接放置可选视图,而且我认为将其与ContentUnavailableView配合使用效果特别好,因为一次只会显示其中一个视图。当然,目前点击该视图还不会有任何反应,但我们很快就会解决这个问题。
由于这段代码相对简单,我想简要说明如何进一步简化body属性——目前body中包含了大量布局代码,还有一个按钮动作。
虽然“切换滤镜”(Change Filter)按钮暂时不会包含复杂功能,但将按钮动作分离出来是一种良好的实践。
现在,我们只需在ContentView中添加一个空方法:
func changeFilter() {
}然后在“切换滤镜”按钮中调用该方法:
Button("Change Filter", action: changeFilter)学习过程中,将按钮动作等代码直接写在视图中是很常见的做法,但一旦进入实际项目开发,花些时间整理代码是很有必要的——相信我,从长远来看,这会让你的工作更轻松!
接下来,我还会分享更多类似的代码整理技巧,这样当你接近课程尾声时,会更有信心确保自己的代码规范整洁。
使用PhotosPicker将图像导入SwiftUI
作者:Paul Hudson 2023年12月12日
要让这个项目真正可用,我们需要允许用户从图库中选择照片,然后在ContentView中显示该照片。这需要一些思考,主要是因为Core Image的工作方式与SwiftUI相比有些迂回。
首先,我们需要在ContentView顶部导入PhotosUI框架,然后添加一个额外的@State属性,用于跟踪用户选择的图片:
@State private var selectedItem: PhotosPickerItem?其次,我们需要在希望触发图像选择的位置放置一个PhotosPicker视图。在这个应用中,我们可以将整个if let processedImage判断语句包裹在PhotosPicker中——可以将选中的图像或ContentUnavailableView作为PhotosPicker的标签。
具体代码如下:
PhotosPicker(selection: $selectedItem) {
if let processedImage {
processedImage
.resizable()
.scaledToFit()
} else {
ContentUnavailableView("No Picture", systemImage: "photo.badge.plus", description: Text("Import a photo to get started"))
}
}提示: 这样会给ContentUnavailableView添加蓝色,以表明它是可交互的。如果不希望这样,可以给PhotosPicker添加.buttonStyle(.plain)修饰符来取消蓝色效果。
第三,我们需要一个方法,在用户选择图像后调用。
之前我已经向你展示过如何从PhotosPicker的选择结果中加载数据,也单独展示过如何将UIImage传入Core Image进行过滤。现在,我们需要将这两部分结合起来:不能直接加载简单的SwiftUI图像,因为它们无法传入Core Image,所以我们需要先加载纯Data对象,再将其转换为UIImage。
现在,在ContentView中添加以下方法:
func loadImage() {
Task {
guard let imageData = try await selectedItem?.loadTransferable(type: Data.self) else { return }
guard let inputImage = UIImage(data: imageData) else { return }
// 后续代码待添加
}
}然后,我们可以在selectedItem属性发生变化时调用该方法,只需在ContentView的某个位置添加onChange()修饰符即可——具体位置并不重要,但将其附加到PhotosPicker上似乎更为合理。
.onChange(of: selectedItem, loadImage)现在可以再次运行应用程序了——虽然目前选择图像后不会有太多操作,但至少可以调出照片选择界面并浏览图片。
使用Core Image进行基础图像过滤
作者:Paul Hudson 2023年12月12日
现在我们的项目已经能够让用户选择图像,下一步是允许用户对图像应用不同强度的Core Image滤镜。首先,我们先使用单一滤镜,之后会通过确认对话框扩展滤镜种类。
如果要在应用中使用Core Image,首先需要在ContentView.swift顶部导入两个框架:
import CoreImage
import CoreImage.CIFilterBuiltins接下来,我们需要一个上下文(context)和一个滤镜(filter)。Core Image上下文是一个负责将CIImage渲染为CGImage的对象,通俗地说,就是将图像的“生成配方”转换为我们可以使用的实际像素序列的对象。
创建上下文的成本较高,因此如果需要渲染大量图像,最好创建一个上下文并长期保留。至于滤镜,我们将默认使用CIFilter.sepiaTone()(棕褐色滤镜),但由于之后会扩展滤镜种类,我们会将滤镜设为@State属性,以便后续修改。
因此,在ContentView中添加以下两个属性:
@State private var currentFilter = CIFilter.sepiaTone()
let context = CIContext()有了这两个属性后,我们就可以编写一个处理导入图像的方法了——该方法会根据filterIntensity的值设置棕褐色滤镜的强度,从滤镜中读取输出图像,让CIContext渲染该图像,然后将结果放入processedImage属性中,以便在屏幕上显示。
func applyProcessing() {
currentFilter.intensity = Float(filterIntensity)
guard let outputImage = currentFilter.outputImage else { return }
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { return }
let uiImage = UIImage(cgImage: cgImage)
processedImage = Image(uiImage: uiImage)
}提示: 遗憾的是,棕褐色滤镜背后的Core Image代码要求强度值为Float类型,而非Double类型。这确实让Core Image显得有些老旧,但不用担心——我们很快就会解决这个问题!
接下来,需要修改loadImage()方法的工作方式。目前该方法的末尾有一个// 后续代码待添加注释,实际上,这里需要将用户选择的图像传入棕褐色滤镜,然后调用applyProcessing()方法来实现滤镜效果。
Core Image滤镜有一个专门的inputImage属性,用于传入待处理的CIImage,但这个属性常常存在问题,可能导致应用崩溃——更安全的做法是使用滤镜的setValue()方法,并传入键kCIInputImageKey。
因此,将// 后续代码待添加注释替换为以下代码:
let beginImage = CIImage(image: inputImage)
currentFilter.setValue(beginImage, forKey: kCIInputImageKey)
applyProcessing()现在运行代码,你会发现应用的基础流程可以正常工作:选择图像后,就能看到应用了棕褐色效果的图像。但我们之前添加的强度滑块却没有任何作用,尽管它绑定的filterIntensity值正是滤镜读取的强度值。
出现这种情况其实并不奇怪:虽然滑块会改变filterIntensity的值,但改变这个属性并不会自动重新触发applyProcessing()方法。相反,我们需要通过onChange()告诉SwiftUI监听filterIntensity的变化,从而手动触发该方法。
同样,onChange()修饰符可以放在SwiftUI视图层级的任何位置,但由于滑块直接改变了该值,我会将其附加在滑块上:
Slider(value: $filterIntensity)
.onChange(of: filterIntensity, applyProcessing)提示: 如果有多个视图修改同一个值,或者不确定具体是哪个视图修改了该值,那么可以将这个修饰符添加到视图的末尾。
现在你可以运行应用程序了,但需要注意:尽管Core Image在所有iPhone上的运行速度都非常快,但在模拟器中通常会非常慢。因此,你可以尝试运行应用以确保所有功能正常,但如果代码运行速度极慢,也不必感到惊讶。