Hacking with iOS: SwiftUI Editio
生成并放大二维码
Core Image 使我们能够从任何输入字符串生成二维码,并且非常快。但是,存在一个问题:它生成的图像非常小,因为它仅与显示其数据所需的像素一样大。增大二维码是很简单的,但是要使其看起来更好,我们还需要调整SwiftUI的图像插值。因此,在这一步中,我们将要求用户以表格形式输入其姓名和电子邮件地址,使用这两条信息生成一个可识别它们的二维码,并在不使其变得模糊的情况下扩展代码。
我们已经有了一个较早的作为占位符的简单MeView
结构,因此我们的第一项工作是添加几个文本输入框并与其字符串绑定。
首先,添加以下两个新的状态来保存名称和电子邮件地址:
@State private var name = "Anonymous"
@State private var emailAddress = "you@yoursite.com"
对于视图主体,我们将使用两个带有大字体的文本输入框,然后使用一个空格将所有内容顶到屏幕顶部。这次,我们将在文本字段上附加一个小的但有用的修饰符,称为textContentType()
,它告诉iOS我们要求用户提供什么样的信息。这应该允许iOS代表用户提供自动完成数据,这使该应用程序更易于使用。
以下面代码替换您当前的body
内容:
NavigationView {
VStack {
TextField("Name", text: $name)
.textContentType(.name)
.font(.title)
.padding(.horizontal)
TextField("Email address", text: $emailAddress)
.textContentType(.emailAddress)
.font(.title)
.padding([.horizontal, .bottom])
Spacer()
}
.navigationBarTitle("Your code")
}
我们将使用名称和电子邮件地址字段来生成QR码,这是黑白像素的正方形集合,可以通过电话和其他设备进行扫描。Core Image内置了一个过滤器,并且您先前已经学习了如何使用Core Image过滤器,因此会发现它非常相似。
首先,我们需要使用新的导入功能来引入所有Core Image过滤器:
import CoreImage.CIFilterBuiltins
其次,我们需要两个属性来存储活动的 Core Image 上下文和 Core Image 的二维码生成器过滤器的实例。因此,将这两个添加到MeView
中:
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
现在介绍有趣的部分:制作二维码本身。如果您还记得,使用Core Image过滤器需要我们使用setValue(_forKey:)
一次或多次以提供输入数据,然后将输出 CIImage
转换为 CGImage
,然后将CGImage
转换为UIImage
。除了以下内容外,我们将按照相同的步骤进行操作:
- 我们对该方法的输入将是一个字符串,但是过滤器的输入是
Data
,因此我们需要对其进行转换。 - 如果转换由于任何原因失败,我们将从SF Symbols发回“xmark.circle”图片。
- 如果无法读取——从理论上来说这是可能的,因为SF Symbols是强类型的——然后我们将发回一个空的
UIImage
。
因此,现在将此方法添加到MeView
结构体中:
func generateQRCode(from string: String) -> UIImage {
let data = Data(string.utf8)
filter.setValue(data, forKey: "inputMessage")
if let outputImage = filter.outputImage {
if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgimg)
}
}
return UIImage(systemName: "xmark.circle") ?? UIImage()
}
在方法中将所有功能隔离在SwiftUI之外确实可以很好地工作,因为这意味着我们放入body
属性中的代码将保持尽可能简单。实际上,我们可以直接使用Image(uiImage:)
来调用generateQRCode(from:)
,然后将其放大到屏幕上的合理大小——swiftUI将确保每次name
或emailAddress
更改时都会调用该方法。
根据要传递给generateQRCode(from:)
的字符串,我们将使用用户输入的名称和电子邮件地址,以换行符分隔。这是一种非常好用的简单格式,以后在扫描这些代码时很容易解析。
将这个新的Image
视图直接添加到Spacer
之前:
Image(uiImage: generateQRCode(from: "\(name)\n\(emailAddress)"))
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
如果您运行该代码,您会发现它工作得很好——您将看到一个默认的二维码,但是您也可以在两个文本字段中键入任何一个,以使二维码动态更改。
像二维码和条形码这样的线条艺术是禁用图像插值的理想选择。尝试将修饰符添加到图像中以了解我的意思:
.interpolation(.none)
PS: 注意它的位置
现在,二维码将变得清晰漂亮,因为SwiftUI只会重复像素,而不是尝试将像素整齐地混合。我会想象相机不在乎会用到什么,但是对用户来说看起来更好!
使用SwiftUI扫描二维码
扫描二维码——或实际上任何类型的可见代码,例如条形码——都可以通过Apple的AVFoundation
库进行扫描。这并不能很顺利地集成到 SwiftUI中,因此,为了避免很多麻烦,我将二维码阅读器打包成一个Swift包,我们可以在Xcode中直接添加和使用它。
我的程序包称为CodeScanner
,可在MIT许可证下的GitHub上找到它,网址为 https://github.com/twostraws/CodeScanner ——欢迎您随意检查或编辑源代码。不过,在这里,我们将按照以下步骤将其添加到 Xcode:
- 转到 File > Swift Packages > Add Package Dependency。
- 输入 https://github.com/twostraws/CodeScanner 作为软件包存储库URL。
- 对于版本规则,请选中“Up to Next Major”,这意味着您将获得所有错误修复和其他功能,但没有任何重大更改。
- 点击 Finish 将完成的包导入到您的项目中。
CodeScanner
软件包为我们提供了一个CodeScanner
SwiftUI视图,该视图可以显示在表单中,并以干净,隔离的方式处理代码扫描。我知道我一直在重复自己的观点,但我希望您能看到一个连续的主题:编写SwiftUI的最佳方法是将功能隔离在分离方法和包装器中,以便您在SwiftUI布局中展示的所有内容都是干净,清晰且明确的。
ProspectsView
中已经有一个“扫码”按钮,我们将使用该按钮来触发QR扫描。因此,首先将这个新的@State
属性添加到ProspectsView
中:
@State private var isShowingScanner = false
之前,我们在“扫码”按钮中添加了一些测试功能,以便我们可以插入一些示例数据,但由于我们将要扫描实际的QR码,因此不再需要这些数据。因此,将导航栏按钮项的操作代码替换为:
self.isShowingScanner = true
在处理QR扫描的结果时,我已经使CodeScanner
程序包完成了所有工作,弄清了什么是代码以及如何将其返回,因此我们在这里要做的就是捕获结果并以某种方式处理它。
当CodeScannerView
找到代码时,它将使用结果实例调用完成闭包,该结果实例包含找到的代码字符串或CodeScannerView.ScanError
指出问题所在。只会发生两个错误:相机不可用,或者相机无法扫描代码。无论返回什么代码或错误,我们都将隐藏视图;我们将很快添加更多代码以完成更多工作。
首先在ProspectsView.swift顶部附近添加以下新导入:
import CodeScanner
现在将此方法添加到ProspectsView
中:
func handleScan(result: Result<String, CodeScannerView.ScanError>) {
self.isShowingScanner = false
// more code to come
}
在显示扫描器并尝试处理其结果之前,我们需要先征询用户使用相机的许可:
- 打开Info.plist。
- 右键单击某些空间,然后选择Add Row。
- 选择 “Privacy - Camera Usage Description” .
- 对于该值,请输入“我们需要扫描二维码”。
现在,我们准备扫描一些二维码了!我们已经具有isShowingScanner
状态,该状态确定是否显示代码扫描器,因此我们现在可以附加sheet()
修饰符以显示扫描器UI。
创建CodeScanner
视图需要三个参数:
- 我们要扫描的代码类型的数组。我们仅在此应用程序中扫描二维码,因此
[.qr]
就很好,但iOS也支持许多其他类型。 - 用作模拟数据的字符串。Xcode的模拟器不支持使用相机扫描代码,因此
CodeScannerView
会自动显示替换用户界面,以便我们仍然可以测试一切正常。此替换UI将自动发送回我们作为模拟数据传递的所有内容。 - 要使用的完成功能。这可能是一个闭包,但是我们只是编写了
handleScan()
方法,所以我们将使用它。
因此,将其添加到ProspectsView
中现有的navigationBarItems()
修改器的下方:
.sheet(isPresented: $isShowingScanner) {
CodeScannerView(
codeTypes: [.qr],
simulatedData: "韦弦zhy\nzhy@jianshu.com",
completion: self.handleScan
)
}
这足以使大部分屏幕正常工作,但还有最后一步:用一些实际功能替换// more code to come
以在handleScan()
中以处理我们拿到的数据。
如果您还记得的话,我们生成的二维码是一个名称,然后是一个换行符,然后是一个电子邮件地址,因此,如果我们的扫描结果成功返回,则可以将代码字符串分解为这些组件,并使用它们创建一个新的Prospect
。如果代码扫描失败,我们只会打印一个错误——如果需要,欢迎您显示一些更有趣的UI!
替换// more code to come
:
switch result {
case .success(let code):
let details = code.components(separatedBy: "\n")
guard details.count == 2 else { return }
let person = Prospect()
person.name = details[0]
person.emailAddress = details[1]
self.prospects.people.append(person)
case .failure(let error):
print("Scanning failed")
}
继续并立即运行代码。如果您使用的是模拟器,则会看到一个测试界面,点击任何地方都将关闭该视图并发送回我们的模拟数据。如果您使用的是真实设备,则会看到一条权限消息,要求用户允许使用相机,并且您同意查看扫描仪视图。要测试在真实设备上进行的扫描,请同时在模拟器中启动该应用程序并切换到“ Me”——您的手机应能够扫描计算机上的模拟器屏幕。
使用上下文菜单添加选项
我们需要一种在“联系”和“未联系”选项卡之间移动人员的方法,最简单的方法是在ProspectsView
中向VStack
添加上下文菜单。这将允许用户长按列表中的任何人,然后点击一个选项以在选项卡之间移动他们。
现在,请记住,此视图在三个地方共享,因此我们需要确保此上下文菜单无论在哪里使用都看起来正确。一种简单的选择是在设置按钮标题时使用三元运算符,因此我们可以将这样的上下文菜单附加到VStack
:
.contextMenu {
Button(prospect.isContacted ? "未联系" : "已联系" ) {
prospect.isContacted.toggle()
}
}
尽管该文本没问题,并且上下文菜单正确显示,但该操作没有任何作用。嗯,这并非完全正确:它确实切换了布尔值,但实际上并未更新用户界面。
发生此问题的原因是Prospects
中的人员数组标记有@Published
,这意味着如果我们从该数组中添加或删除项目,则会发出更改通知。但是,如果我们悄悄地更改数组中的项目,则SwiftUI将不会检测到该更改,并且不会刷新视图。
首先将此方法添加到Prospects
类中:
func toggle(_ prospect: Prospect) {
objectWillChange.send()
prospect.isContacted.toggle()
}
重要提示:更改属性之前,应调用objectWillChange.send()
,以确保SwiftUI正确获得其动画。
现在,您可以使用以下命令替换profise.isContacted.toggle()
操作:
self.prospects.toggle(prospect)
如果您现在运行该应用程序,您会发现它的效果要好得多——扫描用户,然后调出上下文菜单并点击其操作以查看用户在“已联系”和“未联系”选项卡之间移动。
我们可以到此为止,但是我还想做一个改变。如您所见,更改isContacted
会直接导致问题,因为尽管布尔值在内部已更改,但我们的UI仍然过时。如果我们将代码保持原样,则我们(或其他开发人员)可能会忘记此问题,并尝试直接从其他地方翻转布尔值,这只会导致更多错误。
Swift可以通过阻止我们在 Prospects.swift 之外修改布尔值来帮助我们缓解此问题。有一个名为fileprivate
的特定访问控制选项,表示“此属性只能由当前文件中的代码使用。”当然,我们仍然想读取该属性,因此我们可以部署另一个有用的Swift功能:fileprivate(set)
,这意味着“可以从任何地方读取该属性,但只能从当前文件写入”——我们的确切组合需要确保布尔值可以安全使用。
因此,将Prospect
中的isContacted
布尔值修改为如下形式:
fileprivate(set) var isContacted = false
它并没有影响我们在这里的项目,但确实有助于确保我们将来的安全。如果您想知道为什么我们将Prospect
和Prospects
类放在同一文件中,现在您知道了!
译自
Generating and scaling up a QR code
Scanning QR codes with SwiftUI
Adding options with a context menu