【译】8个常见的Swift UI错误-以及如何修复它们

295 阅读10分钟

原文地址:8 Common SwiftUI Mistakes - and how to fix them

少写代码多做事

Swift UI是一个又大又复杂的框架,尽管使用它很有趣,但也有很大的犯错空间。在这篇文章中,我将介绍Swift UI学习者犯的八个常见错误,以及如何修复它们。

其中一些错误是简单的误解,Swift UI如此之大,这些错误很容易犯。其他的是关于更深入地理解Swift UI是如何工作的,还有一些更像是遗留思维的标志——有时你花了很多时间写视图和修饰符,却没有花时间简化最终结果。

不管怎样,我不会让你对这八个错误有疑问,所以在我们深入研究它们之前,这里有一个简短的总结:

  1. 在不需要的地方添加视图和修饰符

  2. 当它们表示@State Object时使用@Object

  3. 把修饰符放在错误的顺序

  4. 将属性观察员附加到属性包装器

  5. 抚摸形状,当它们意味着抚摸边界时

  6. 使用带有选项的警报和工作表

  7. 试图“支持”他们的Swift UI视图

  8. 使用无效范围创建动态视图

如果你更喜欢看视频,我已经帮你搞定了!

还在这儿?好吧,让我们开始吧…

赞助与Swift黑客,并达到世界上最大的Swift社区!

1.在不需要的地方添加视图和修饰符

让我们从最常见的一个开始,即编写比实际需要更多的Swift UI代码。这很常见,部分原因是我们在解决问题时经常写很多代码,但很容易忘记事后清理代码。有时候,这也是一个回到旧习惯的问题,尤其是如果你来自UI Kit或其他用户界面框架。

作为一个开始的例子,你如何用红色矩形填充屏幕?你可以这样写:

Rectangle()    .fill(Color.red)

老实说,这很有效——它能得到你想要的确切结果。但是一半的代码是不需要的,因为你可以这样说:

Color.red

这是因为Swift UI的所有颜色和形状都自动符合视图协议,所以您可以直接将它们用作视图。

您通常也会在剪辑形状中看到这一点,因为用您想要的任何形状应用剪辑形状()是非常自然的。例如,我们可以让我们的红色矩形有这样的圆角:

Color.red    .clipShape(RoundedRectangle(cornerRadius: 50))

但这不是必需的——您可以通过使用角半径()修饰符来大幅简化代码,如下所示:

Color.red    .cornerRadius(50)

删除这些冗余代码需要时间,因为您需要稍微改变一下您的心态。当你刚刚学习Swift UI的时候,这也更难做到,所以如果你在学习的时候使用更长的版本,不要感到难过。

2.当它们表示@State Object时使用@Object

Swift UI提供了许多属性包装器来帮助我们构建数据响应用户界面,其中三个最重要的是@State、@State Object和@Object。知道什么时候使用这些非常重要,如果弄错了会导致代码中的各种问题。

第一个简单明了:当您拥有当前视图拥有的值类型属性时,应该使用@State。因此,整数、字符串、数组等都是@State的最佳候选对象。

但是后两者会引起一些混乱,通常会看到这样的代码:

class DataModel: ObservableObject {    @Published var username = "@twostraws"}struct ContentView: View {    @ObservedObject var model = DataModel()    var body: some View {        Text(model.username)    }}

这是绝对错误的,可能会在您的应用程序中导致问题。

正如我所说,@State是指当前视图拥有的价值类型属性,而“拥有”部分很重要。您看,上面的代码应该使用@State Object,因为“state”部分意味着“这是当前视图所拥有的”

所以,应该是这样的:

@StateObject model = DataModel()

当您使用@观察对象创建一个对象的新实例时,您的视图拥有该对象,这意味着它可以随时被销毁。巧妙的是,这只会在某些时候失败,所以您可能会认为您的代码非常好,但后来却在某个随机点失败了。

需要记住的重要一点是,@State和@State Object意味着“该视图拥有数据”,而其他属性包装器,如@观察对象和@环境对象则不拥有。

3.把修饰符放在错误的顺序

修改顺序在Swift UI中非常重要,弄错了会导致你的布局看起来错误,但也会表现不佳。

这个问题的典型例子是使用填充和背景,像这样:

Text("Hello, World!")    .font(.largeTitle)    .background(Color.green)    .padding()        

因为我们在背景颜色之后应用填充,所以颜色只会直接应用在文本周围,而不是填充文本周围。如果你想两者都是绿色的,你应该这样做:

Text("Hello, World!")    .font(.largeTitle)    .padding()        .background(Color.green)

当你试图调整视图的位置时,这变得特别有趣。

例如,偏移()修饰符会更改视图呈现的位置,不会更改视图的实际尺寸。这意味着在偏移后应用的修饰符就像偏移从未发生一样。

试试这个:

Text("Hello, World!")    .font(.largeTitle)    .offset(x: 15, y: 15)    .background(Color.green)

你会看到这抵消了文本,没有抵消背景颜色。现在尝试交换偏移量()和背景():

Text("Hello, World!")    .font(.largeTitle)    .background(Color.green)    .offset(x: 15, y: 15)

现在你会看到文本背景被移动。

或者,position()修饰符可以更改视图在其父视图中呈现的位置,但只能通过首先在其周围应用灵活大小的框架来实现。

试试这个:

Text("Hello, World!")    .font(.largeTitle)    .background(Color.green)    .position(x: 150, y: 150)

你会看到背景颜色非常适合文本,整个东西都位于左上角附近。现在尝试交换background()和position():

Text("Hello, World!")    .font(.largeTitle)    .position(x: 150, y: 150)    .background(Color.green)

这一次,你会看到整个屏幕变成绿色。同样,使用position()需要Swift UI在文本视图周围放置一个灵活大小的框架,这将自动占用所有可用空间。然后我们把它涂成绿色,这就是为什么整个屏幕看起来是绿色的。

所有这些都发生了,因为你应用的大多数修饰符都会创建新的视图——应用一个位置或背景颜色会将你在新视图中应用的修饰符包裹起来。这有一个重要的好处,我们可以应用修饰符多个计时器来创建新效果,例如添加多个填充和背景:

Text("Hello, World!")    .font(.largeTitle)    .padding()    .background(Color.green)    .padding()    .background(Color.blue)

或者应用多个阴影来创建超级密集的效果:

Text("Hello, World!")    .font(.largeTitle)    .foregroundColor(.white)    .shadow(color: .black, radius: 10)    .shadow(color: .black, radius: 10)    .shadow(color: .black, radius: 10)

4.将属性观察员附加到属性包装器

在某些情况下,您可以将属性观察员(如didSet)附加到属性包装器上,但通常情况下,这并不像您预期的那样有效。

例如,如果您正在使用滑块,并且希望在滑块值更改时采取一些操作,您可以尝试编写以下内容:

struct ContentView: View {    @State private var rating = 0.0 {        didSet {            print("Rating changed to \(rating)")        }    }    var body: some View {        Slider(value: $rating)    }}

但是,永远不会调用didSet属性观察器,因为绑定直接更改值,而不是每次都创建新值。

Swift UI原生方法是使用on Change()修饰符,如下所示:

struct ContentView: View {    @State private var rating = 0.0    var body: some View {        Slider(value: $rating)            .onChange(of: rating) { value in                print("Rating changed to \(value)")            }    }}

然而,我更喜欢一个稍微不同的解决方案:我在绑定本身上使用一个扩展,它像以前一样获取和设置包装的值,但也使用新值调用一个处理函数:

extension Binding {    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {        Binding(            get: { self.wrappedValue },            set: { newValue in                self.wrappedValue = newValue                handler(newValue)            }        )    }}

有了它,我们现在可以将绑定操作直接附加到滑块上:

struct ContentView: View {    @State private var rating = 0.0    var body: some View {        Slider(value: $rating.onChange(sliderChanged))    }    func sliderChanged(_ value: Double) {        print("Rating changed to \(value)")    }}

用哪个最适合你!

5.抚摸形状,当它们意味着抚摸边界时

这里有一个很简单的问题,很多人都会遇到:不理解运笔()和运笔边界()之间的区别。当您尝试描边一个大的形状时,您可以看到这个问题:

Circle()    .stroke(Color.red, lineWidth: 20)

注意到你怎么看不到圆的左右边缘了吗?这是因为描边()修饰符将描边集中在形状的边缘,所以一个20点的红色描边将在形状内部画10点,在形状外部画10点——导致它挂在屏幕上。

相比之下,运笔边框()在形状内部绘制它的整个笔画,所以它永远不会大于形状的框架:

Circle()    .strokeBorder(Color.red, lineWidth: 20)

使用描边()比描边()有一个优势,那就是如果您使用描边样式,描边()将返回一个新的形状,而不是一个新的视图。这允许您创建某些效果,否则是不可能的,例如抚摸形状两次:

Circle()    .stroke(style: StrokeStyle(lineWidth: 20, dash: [10]))    .stroke(style: StrokeStyle(lineWidth: 20, dash: [10]))    .frame(width: 280, height: 280)

6.使用带有选项的警报和工作表

当你学习呈现工作表和选项时,最简单的事情是将它们的呈现绑定到这样的布尔值:

struct User: Identifiable {    let id: String}struct ContentView: View {    @State private var selectedUser: User?    @State private var showingAlert = false    var body: some View {        VStack {            Button("Show Alert") {                selectedUser = User(id: "@twostraws")                showingAlert = true            }        }        .alert(isPresented: $showingAlert) {            Alert(title: Text("Hello, \(selectedUser!.id)"))        }    }}

这同样很有效——很容易理解,而且很有效。但是一旦你已经过了基础阶段,你应该考虑换一个可选版本,它完全删除布尔值,也删除一个力展开。唯一的要求是,无论你在看什么,都必须符合《可识别》。

例如,我们可以在选择用户更改时显示警报,如下所示:

struct ContentView: View {    @State private var selectedUser: User?    var body: some View {        VStack {            Button("Show Alert") {                selectedUser = User(id: "@twostraws")            }        }        .alert(item: $selectedUser) { user in            Alert(title: Text("Hello, \(user.id)"))        }    }}

它使您的代码更易于读写,并消除了由于某种原因强制打开失败的额外担忧。

7.试图“支持”你的Swift UI视图

人们用Swift UI遇到的最常见问题之一是试图改变Swift UI视图背后的内容。这通常始于这样的代码:

struct ContentView: View {    var body: some View {        Text("Hello, World!")            .background(Color.red)    }}

这显示了一个白色屏幕,红色背景颜色非常适合文本视图。许多人想要的是整个屏幕都有背景颜色,所以他们开始向上寻找Swift UI背后的UI Kit视图,这样他们就可以使用它了。

现在,你的代码背后绝对有一个UI Kit视图:它由UI Hosting Controller管理,这是一个UI Kit视图控制器,就像其他任何控制器一样。但是如果你试图进入UI Kit领域,你很可能会遇到问题,要么是你的修改导致Swift UI行为奇怪,要么是你试图在UI Kit不可用的iOS之外使用你的代码。

相反,如果您希望视图扩展以填充所有可用空间,只需在Swift UI中这样说:

Text("Hello, World!")    .frame(maxWidth: .infinity, maxHeight: .infinity)    .background(Color.red)    .ignoresSafeArea()

8.使用无效范围创建动态视图

Swift UI的几个初始化器允许我们传递范围,这使得许多类型的视图易于创建。

例如,如果我们想显示一个包含四个项目的列表,我们可以这样写:

struct ContentView: View {    @State private var rowCount = 4    var body: some View {        VStack {            List(0..<rowCount) { row in                Text("Row \(row)")            }        }    }}

虽然这很好,但如果您试图在运行时更改范围,就会出现问题。您可以看到我使用@State属性包装器使row Count可变,因此我们可以通过在VS tack的开头添加一个按钮来更改它:

Button("Add Row") {    rowCount += 1}.padding(.top)

如果您现在运行代码,您会看到按下按钮会在Xcode的调试输出中显示一条警告消息,实际上没有什么变化——它就是不起作用。

要解决这个问题,您应该始终使用可识别协议或提供您自己的特定id参数,以向Swift UI明确表示该范围将随着时间的推移而变化:

List(0..<rowCount, id: \.self) { row in    Text("Row \(row)")}

有了它,按钮将按预期工作。

现在怎么办?

我希望你觉得这篇文章有用,不管你对Swift UI有多少经验。正如我已经说过几次的那样,一些替代代码确实工作得很好,尽管是的,你可以改进它,但没关系——总有一些东西你可以改进,如果你只是在学习Swift UI,那么不要太纠结于使用任何有助于你完成工作的代码。

你在学习Swift UI时遇到了什么问题?让我知道在Twitter!

赞助与Swift黑客,并达到世界上最大的Swift社区!