Hacking with iOS: SwiftUI Edition - Hot Prospects项目(二)

时间:2022-07-25
本文章向大家介绍Hacking with iOS: SwiftUI Edition - Hot Prospects项目(二),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

生成并放大二维码

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。除了以下内容外,我们将按照相同的步骤进行操作:

  1. 我们对该方法的输入将是一个字符串,但是过滤器的输入是Data,因此我们需要对其进行转换。
  2. 如果转换由于任何原因失败,我们将从SF Symbols发回“xmark.circle”图片。
  3. 如果无法读取——从理论上来说这是可能的,因为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将确保每次nameemailAddress更改时都会调用该方法。

根据要传递给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:

  1. 转到 File > Swift Packages > Add Package Dependency。
  2. 输入 https://github.com/twostraws/CodeScanner 作为软件包存储库URL。
  3. 对于版本规则,请选中“Up to Next Major”,这意味着您将获得所有错误修复和其他功能,但没有任何重大更改。
  4. 点击 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
}

在显示扫描器并尝试处理其结果之前,我们需要先征询用户使用相机的许可:

  1. 打开Info.plist。
  2. 右键单击某些空间,然后选择Add Row。
  3. 选择 “Privacy - Camera Usage Description” .
  4. 对于该值,请输入“我们需要扫描二维码”。

现在,我们准备扫描一些二维码了!我们已经具有isShowingScanner状态,该状态确定是否显示代码扫描器,因此我们现在可以附加sheet()修饰符以显示扫描器UI。

创建CodeScanner视图需要三个参数:

  1. 我们要扫描的代码类型的数组。我们仅在此应用程序中扫描二维码,因此[.qr]就很好,但iOS也支持许多其他类型。
  2. 用作模拟数据的字符串。Xcode的模拟器不支持使用相机扫描代码,因此CodeScannerView会自动显示替换用户界面,以便我们仍然可以测试一切正常。此替换UI将自动发送回我们作为模拟数据传递的所有内容。
  3. 要使用的完成功能。这可能是一个闭包,但是我们只是编写了handleScan()方法,所以我们将使用它。

因此,将其添加到ProspectsView中现有的navigationBarItems()修改器的下方:

.sheet(isPresented: $isShowingScanner) {
    CodeScannerView(
        codeTypes: [.qr],
        simulatedData: "韦弦zhynzhy@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

它并没有影响我们在这里的项目,但确实有助于确保我们将来的安全。如果您想知道为什么我们将ProspectProspects类放在同一文件中,现在您知道了!

译自 Generating and scaling up a QR code Scanning QR codes with SwiftUI Adding options with a context menu