少写代码多做事
Swift UI是一个又大又复杂的框架,尽管使用它很有趣,但也有很大的犯错空间。在这篇文章中,我将介绍Swift UI学习者犯的八个常见错误,以及如何修复它们。
其中一些错误是简单的误解,Swift UI如此之大,这些错误很容易犯。其他的是关于更深入地理解Swift UI是如何工作的,还有一些更像是遗留思维的标志——有时你花了很多时间写视图和修饰符,却没有花时间简化最终结果。
不管怎样,我不会让你对这八个错误有疑问,所以在我们深入研究它们之前,这里有一个简短的总结:
-
在不需要的地方添加视图和修饰符
-
当它们表示@State Object时使用@Object
-
把修饰符放在错误的顺序
-
将属性观察员附加到属性包装器
-
抚摸形状,当它们意味着抚摸边界时
-
使用带有选项的警报和工作表
-
试图“支持”他们的Swift UI视图
-
使用无效范围创建动态视图
如果你更喜欢看视频,我已经帮你搞定了!
还在这儿?好吧,让我们开始吧…
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!