Skip to content

第62天 项目 13 第一部分

这是两个项目中的第一个,旨在探讨如何突破SwiftUI的界限,将其与Apple的其他框架连接起来。Apple已经为我们提供了实现这一目标的方法,但这需要一些思考——这与你迄今为止编写的常规SwiftUI代码有很大不同,甚至与UIKit开发者习惯的代码类型也有很大差异!

别担心,我们会一步一步来解决。我会提供教程和一个目标项目;你只需要动动脑筋,并有坚持下去的毅力就行。记住,正如作家约翰·奥特伯格所说:“如果你想在水上行走,就必须走出船外!”

今天你需要学习四个主题,在这些主题中,你将了解如何响应状态变化、显示确认对话框等内容。

  • Instafilter:简介
  • 属性包装器如何成为结构体
  • 使用onChange()响应状态变化
  • 使用confirmationDialog()显示多个选项

Instafilter:简介

作者:Paul Hudson 2022年5月12日

在这个项目中,我们将构建一个应用程序,允许用户从他们的图库中导入照片,然后使用各种图像效果对照片进行修改。我们将涵盖许多新技巧,但核心是两项实用的应用开发技能——使用Apple的Core Image框架,以及一项重要的SwiftUI技能——与UIKit集成。当然还有其他内容,但这两项是最关键的知识点。

Core Image是Apple用于处理图像的高性能框架,功能极其强大。Apple为我们设计了数十种示例图像滤镜,提供了模糊、颜色偏移、像素化等效果,并且所有这些滤镜都经过优化,能充分利用iOS设备上的图形处理单元(GPU)。

提示: 虽然你可以在模拟器中运行Core Image应用程序,但不要惊讶于大多数操作会非常缓慢——只有在物理设备上运行时,你才能获得出色的性能。

至于与UIKit集成,你可能会疑惑为什么需要这样做——毕竟,SwiftUI的设计目的就是取代UIKit,对吧?嗯,某种程度上是这样。在SwiftUI推出之前,几乎所有的iOS应用程序都是用UIKit构建的,这意味着世界上可能存在数十亿行UIKit代码。因此,如果你想将SwiftUI集成到现有项目中,就需要学习如何让两者良好协作。

但还有另一个原因,希望这个原因将来不再存在:Apple框架的许多部分目前还没有SwiftUI包装器,这意味着如果你想集成MapKit、Safari或其他重要的API,就需要知道如何包装它们的代码以便在SwiftUI中使用。说实话,实现这种集成所需的代码并不美观,但在你SwiftUI学习生涯的这个阶段,你已经完全有能力掌握它了。

和往常一样,在开始项目之前,我们需要先学习一些技巧,请使用App模板创建一个新的iOS应用程序,并将其命名为“Instafilter”。

属性包装器如何成为结构体

作者:Paul Hudson 2023年12月5日

你已经了解到,SwiftUI允许我们通过使用@State属性包装器在视图结构体中存储变化的数据,如何使用$将该状态绑定到UI控件的值,以及状态的变化如何自动促使SwiftUI重新调用视图的body属性。

综合以上所有特性,我们可以编写如下代码:

swift
struct ContentView: View {
    @State private var blurAmount = 0.0

    var body: some View {
        VStack {
            Text("Hello, World!")
                .blur(radius: blurAmount)

            Slider(value: $blurAmount, in: 0...20)

            Button("Random Blur") {
                blurAmount = Double.random(in: 0...20)
            }
        }
    }
}

运行这段代码,你会发现左右拖动滑块可以调整文本标签的模糊程度,完全符合预期,点击按钮则会立即跳转到一个随机的模糊程度。

现在,假设我们希望这个绑定不仅处理模糊效果的半径。也许我们想运行一个方法,或者只是为了调试目的打印出该值。你可能会尝试像这样更新属性:

swift
@State private var blurAmount = 0.0 {
    didSet {
        print("New value is \(blurAmount)")
    }
}

如果你运行这段代码,可能会感到失望:当你拖动滑块时,会看到模糊程度发生变化,但不会看到print()语句被触发——实际上,根本不会有任何输出。但是,如果你尝试按下按钮,就看到打印出一条消息。

要理解这背后的原因,我们需要稍微探究一下@State的工作原理,以及属性包装器实际上在为我们做什么。

属性包装器之所以叫这个名字,是因为它们会将我们的属性包装在另一个结构体中。这意味着,当我们使用@State包装一个字符串时,最终得到的属性的实际类型是State<String>。同样,当我们使用@Environment等属性包装器时,最终会得到一个Environment类型的结构体,其中包含了其他某个值。

之前我解释过,我们不能修改视图中的属性,因为它们是结构体,因此是不可变的。但是,现在你知道@State本身会生成一个结构体,这就产生了一个矛盾:为什么那个结构体可以被修改呢?

Xcode有一个非常有用的命令叫做“快速打开”(通过Cmd+Shift+O访问),它可以让你找到项目中或已导入的任何框架中的任何文件或类型。现在激活这个命令,输入“State”——希望第一个结果下方显示SwiftUI,如果没有,请找到并选择它。

你会看到SwiftUI的生成接口,本质上就是SwiftUI向我们公开的所有部分。其中没有实现代码,只有大量的协议、结构体、修饰符等的定义。

我们请求查看State,所以应该会看到以下这行代码:

swift
@propertyWrapper public struct State<Value> : DynamicProperty {

正是@propertyWrapper这个属性,使得我们可以使用@State

再往下看几行,你应该会看到:

swift
public var wrappedValue: Value { get nonmutating set }

这个被包装的值(wrappedValue)就是我们试图存储的实际值,比如一个字符串。这个生成的接口告诉我们,该属性可以被读取(get),也可以被写入(set),但当我们设置该值时,并不会实际改变结构体本身。在幕后,它会将该值发送给SwiftUI,存储在一个可以自由修改的位置,因此结构体本身确实从未改变。

现在你已经了解了这些知识,让我们回到之前有问题的代码:

swift
@State private var blurAmount = 0.0 {
    didSet {
        print("New value is \(blurAmount)")
    }
}

从表面上看,这段代码的意思是“当blurAmount发生变化时,打印出它的新值”。但是,由于@State实际上会包装它的内容,所以它真正的含义是“当包装blurAmountState结构体发生变化时,打印出模糊程度的新值”。

还能跟上吗?现在让我们再深入一步:你刚刚了解到State使用非可变设置器(non-mutating setter)来包装它的值,这意味着blurAmount和包装它的State结构体都不会发生变化——我们的绑定是直接修改内部存储的值,这就导致属性观察器从未被触发。

因此,使用按钮直接修改属性是可行的,因为它会通过非可变设置器,并触发didSet观察器,但使用绑定则不行,因为绑定会绕过设置器,直接调整值。

那么,我们该如何解决这个问题呢——如何确保无论绑定以何种方式发生变化,都能运行某些代码?其实,有一个专门用于此目的的修饰符……

使用onChange()响应状态变化

作者:Paul Hudson 2024年3月4日

由于SwiftUI向属性包装器发送绑定更新的方式,与属性包装器一起使用的属性观察器往往不会按预期工作,这意味着即使模糊半径发生变化,以下这类代码也不会打印任何内容:

swift
struct ContentView: View {
    @State private var blurAmount = 0.0 {
        didSet {
            print("New value is \(blurAmount)")
        }
    }

    var body: some View {
        VStack {
            Text("Hello, World!")
                .blur(radius: blurAmount)

            Slider(value: $blurAmount, in: 0...20)
        }
    }
}

要解决这个问题,我们需要使用onChange()修饰符,它会告诉SwiftUI当某个特定值发生变化时,运行我们选择的函数。SwiftUI会自动将旧值和新值传递给你附加的任何函数,因此我们可以这样使用它:

swift
struct ContentView: View {
    @State private var blurAmount = 0.0

    var body: some View {
        VStack {
            Text("Hello, World!")
                .blur(radius: blurAmount)

            Slider(value: $blurAmount, in: 0...20)
                .onChange(of: blurAmount) { oldValue, newValue in
                    print("New value is \(newValue)")
                }
        }
    }
}

现在,这段代码会在滑块变化时正确打印出值,因为onChange()正在监视它。注意,其他大部分内容都保持不变:我们仍然使用@State private var来声明blurAmount属性,并且仍然使用blur(radius: blurAmount)作为文本视图的修饰符。

提示: 你可以在视图层次结构中的任何位置附加onChange(),但我更喜欢将它放在实际发生变化的元素附近。

这一切意味着,你可以在onChange()函数内部执行任何你想做的操作:调用方法、运行算法以确定如何应用变化,或者其他任何你可能需要的操作。

onChange()修饰符还有另外两种常见的变体:

  • 一种完全不接受参数,适用于当你只想在值变化时运行一个函数,但并不关心新值是什么的情况。
  • 一种只接受新值,不传递旧值。从iOS 17开始,这种变体已被弃用(deprecated),这是Apple在表示“除非你需要支持iOS 16及更早版本,否则请不要使用它”。

使用confirmationDialog()显示多个选项

作者:Paul Hudson 2023年12月5日

SwiftUI为我们提供了alert()用于呈现重要的选择,sheet()用于在当前视图上方呈现完整视图,此外还提供了confirmationDialog():它是alert()的替代方案,允许我们添加多个按钮。

从视觉上看,警告框(alert)和确认对话框(confirmation dialog)有很大不同:在iPhone上,警告框会出现在屏幕中央,必须通过选择一个按钮主动关闭;而确认对话框会从屏幕底部滑入,可以包含许多按钮,并且可以通过点击“取消”(Cancel)或点击选项外部来关闭。

尽管它们看起来非常不同,但创建确认对话框和警告框的方式几乎相同:

  • 两者都是通过向视图层次结构附加一个修饰符来创建的——alert()用于警告框,confirmationDialog()用于确认对话框。
  • 两者都会在某个条件为true时由SwiftUI自动显示。
  • 两者都可以添加多个按钮以执行各种操作。
  • 两者都可以附加第二个闭包来提供额外的消息。

为了演示如何使用确认对话框,我们首先需要一个基本按钮来切换某种条件:

swift
struct ContentView: View {
    @State private var showingConfirmation = false
    @State private var backgroundColor = Color.white

    var body: some View {
        Button("Hello, World!") {
            showingConfirmation = true
        }
        .frame(width: 300, height: 300)
        .background(backgroundColor)
    }
}

现在是关键部分:我们需要向按钮添加另一个修饰符,以便在准备好时创建并显示确认对话框。

就像alert()一样,confirmationDialog()修饰符接受三个参数:标题、一个用于确定对话框当前是否显示的绑定,以及一个用于提供应显示的按钮的闭包——通常作为尾随闭包提供。

我们为确认对话框提供一个标题,还可以选择提供一条消息,然后是一个按钮数组。这些按钮会按照你提供的顺序在屏幕上垂直排列,通常最好在末尾包含一个“取消”按钮——是的,你可以通过点击屏幕其他地方来取消,但为用户提供明确的取消选项会好得多。

因此,向你的文本视图添加以下修饰符:

swift
.confirmationDialog("Change background", isPresented: $showingConfirmation) {
    Button("Red") { backgroundColor = .red }
    Button("Green") { backgroundColor = .green }
    Button("Blue") { backgroundColor = .blue }
    Button("Cancel", role: .cancel) { }
} message: {
    Text("Select a new color")
}

运行应用程序后,你会发现点击文本会使确认对话框滑入,点击其中的选项会导致文本的背景颜色发生变化。

本站使用 VitePress 制作