swiftui
介绍 (Introduction)
SwiftUI introduced us to a whole new way of designing and coding interfaces. Gone are the old ways of subclassing UIKit (or AppKit) classes and hardwiring layout constraints. Instead, we now have a nice, declarative way of structuring and styling our controls and making sure the interface updates whenever new information or events arrive.
SwiftUI向我们介绍了一种全新的界面设计和编码方式。 继承UIKit(或AppKit)类和固定布局约束的旧方法已经一去不复返了。 取而代之的是,我们现在有了一种很好的声明式方法来构造和样式化控件,并确保只要有新信息或事件到达,接口就会更新。
To facilitate this new architecture, the good people at Apple took some of Swift’s best features (e.g. protocols, generics, opaque types) and combined them into SwiftUI. However, this comes at a hidden cost: If you’re not already well-versed in these features, there will be a bit of a learning curve and most likely a lot of cryptic error messages that will send you off to your favorite search engine. This article will look at some of these error messages and explain what they mean and what you can do to prevent them.
为了促进这种新架构的发展,Apple的好人采用了Swift的一些最佳功能(例如协议,泛型,不透明类型),并将它们组合到SwiftUI中。 但是,这需要付出一些隐性的代价:如果您还不熟悉这些功能,将会有一些学习过程,并且很可能会出现很多隐含的错误消息,这些错误消息会将您带到您最喜欢的搜索引擎。 本文将研究其中一些错误消息,并解释它们的含义以及如何防止这些错误消息。
建立视图 (Building a View)
When implementing a new SwiftUI view, you typically start small. You add some components to the body
, you style them, and handle any interactions. At some point, your simple view starts to get too big or you get a lot of conditional logic or duplication in your body
. So, you decide to move some of the logic out of the body
and into a separate function. This function will take care of building some complex components for you, and since everything is a View
in SwiftUI, you simply define the function type signature like this:
在实现新的SwiftUI视图时,通常从小处着手。 您将一些组件添加到body
,对其进行样式设置并处理任何交互。 在某个时候,您的简单视图开始变得太大,或者您的body
了很多条件逻辑或重复项。 因此,您决定将某些逻辑移出body
并移至单独的函数中。 该函数将为您构建一些复杂的组件,并且由于一切都是SwiftUI中的View
,因此您只需定义函数类型签名即可,如下所示:
private func buildComplexButton() -> View
Great! Well… apart from the compiler, which complains,
大! 好吧……除了编译器抱怨
“Protocol ‘View’ can only be used as a generic constraint because it has Self or associated type requirements.”
“协议“视图”只能用作通用约束,因为它具有“自身”或关联的类型要求。”
The problem here lies in the last part of the error message: Any object conforming to the View
protocol will need to have an associated type Body
that determines how the view is actually implemented. Attempting to return just a plain View
from your function results in the compiler throwing up its hands and saying, “I don’t know what the return type will be without any additional information on the actual type that is conforming to this protocol.” It’s a bit like returning a generic type (such as Array
) without specifying the type parameter list (What does the Array
contain?). But that is exactly the point! We don’t want to pin ourselves down to a concrete type just yet. Our function might generate a variety of different views with different concrete types. Luckily, Swift 5.1 introduced the keyword some
to help with this, and you’ve already seen it when creating a new view:
这里的问题出在错误消息的最后一部分:任何符合View
协议的对象都需要具有一个 关联类型决定视图实际实现方式的Body
。 尝试从函数中仅返回普通View
导致编译器举起手来,说:“如果不提供有关符合此协议的实际类型的任何其他信息,我将不知道返回类型将是什么。” 这有点像不指定类型参数列表( Array
包含什么?)而返回泛型类型(例如Array
)。 但这就是重点! 我们现在还不想将自己固定在一个具体的类型上。 我们的函数可能会生成具有不同具体类型的各种不同视图。 幸运的是,Swift 5.1引入了关键字some
来解决这个问题,在创建新视图时您已经看到了它:
var body: some View
Naively, this means what you think it means: We return some View
and we don’t really care what kind. This is commonly referred to as an opaque type: a type that has some capabilities (it’s a View
), but we don’t know exactly what kind of view. So, we’ll update our function to the new signature and give it an implementation:
天真的,这意味着您认为它意味着什么:我们返回一些 View
而我们实际上并不关心哪种类型。 通常将其称为不透明类型:具有某些功能的类型(它是View
),但是我们不确切知道哪种视图。 因此,我们将功能更新为新的签名并为其提供实现:
And all is well again! Well… as long as you make sure that every possible View
that you return from this function has the exact same type. The restriction on opaque types is that the compiler will only allow them if every available code path will return the same concrete type. We’re only returning identical buttons, so no issues here. However, suppose we are implementing a user interface for a keypad.
而且一切都很好! 好吧……只要确保从此函数返回的每个可能的View
都具有完全相同的类型。 对不透明类型的限制是,仅当每个可用代码路径都返回相同的具体类型时,编译器才允许它们。 我们只返回相同的按钮,因此这里没有问题。 但是,假设我们正在实现键盘的用户界面。
We’ve chosen to implement this as a grid of Buttons
. Since all the buttons are more or less identical and we don’t want to hardcode each and every one of them, we use a builder function to create them. There are two main types of buttons: ones with a text label (the digits) and ones with an image (in this case, the delete and Face ID symbols coming from SF Symbols). Simplified, it looks like this:
我们选择将其实现为Buttons
的网格。 由于所有按钮或多或少都是相同的,并且我们不想对每个按钮进行硬编码,因此我们使用了一个builder函数来创建它们。 按钮主要有两种类型:带有文本标签(数字)的按钮和带有图像的按钮(在这种情况下,为SF Symbols的Delete和Face ID符号)。 简化后,它看起来像这样:
We’re still returning Buttons
, so this must work, right? Well, the compiler unfortunately says no:
我们仍在返回Buttons
,所以这必须工作,对吗? 好吧,编译器不幸地拒绝了:
“Function declares an opaque return type, but the return statements in its body do not have matching underlying types.”
“函数声明了不透明的返回类型,但是其主体中的return语句没有匹配的基础类型。”
Odd. A button is a button, right? But if we examine the documentation, we will see that Button
is actually a generic type and not a plain struct like Text
:
奇。 一个按钮就是一个按钮,对不对? 但是,如果我们仔细阅读文档 ,将会发现Button
实际上是一个通用类型,而不是像Text
这样的普通结构:
struct Button<Label> where Label : View
And this holds for a lot of the SwiftUI built-in types — most notably the ones that can contain other views or content. So, we are trying to return either a Button<Text>
or a Button<Image>
that the compiler (correctly) identifies as two different types and hence refuses to cooperate. This is one of those situations where the rigorous typing of Swift is working against us.
这适用于许多SwiftUI内置类型-最值得注意的是可以包含其他视图或内容的类型。 因此,我们试图返回Button<Text>
或Button<Image>
,编译器正确地将它们标识为两种不同的类型,因此拒绝合作。 这是Swift严格键入对我们不利的情况之一。
Fortunately, there are two ways to solve this issue, and both deal with satisfying the compiler just enough that it’ll allow us to compile and run our code:
幸运的是,有两种方法可以解决此问题,并且两种方法都足以使编译器满意,从而使我们能够编译和运行代码:
Embedding our views in a
Group
, preserving as much type information as possible.将我们的意见嵌入到
Group
,并保留尽可能多的类型信息。Wrapping our views in
AnyView
, effectively removing type information.将我们的视图包装在
AnyView
,可以有效地删除类型信息。
Both methods have their peculiarities and it’s ultimately up to you to decide which one suits you best.
两种方法都有其独特性,最终由您决定哪种方法最适合您。
嵌入组 (Embedding in a Group)
This is what some people consider the “cleanest” approach because embedding your mixed content in a Group
preserves all typing information. However, it introduces some types you might not expect and you’re currently limited to only the simple if
statements for any conditional switching. This means no if case let
or switch
statements. If that’s not an issue, then go right ahead. It looks something like this:
这就是某些人认为的“最干净”的方法,因为将您的混合内容嵌入到Group
保留所有键入信息。 但是,它引入了一些您可能不会想到的类型,并且当前您仅限于用于任何条件切换的简单if
语句。 这意味着没有if case let
或switch
语句。 如果这不是问题,那就继续吧。 看起来像这样:
Now, this isn’t some “magic” fix that changes the way opaque types work. It merely introduces some additional types that make sure that from a compiler perspective, this function always returns the same type. If we inspect it, we see that the type returned is:
现在,这不是改变不透明类型工作方式的“魔术”解决方案。 它只是引入了一些其他类型,这些类型可以确保从编译器的角度来看,此函数始终返回相同的类型。 如果我们检查它,我们看到返回的类型是:
Group<_ConditionalContent<Button<Text>, Button<Image>>>
Again, Group
is a generic type, but it introduces an additional (generic) type _ConditionalContent
that has our button types (again generics) in the type parameter list. And this is actually the trick up SwiftUI’s sleeve: By being smart and introducing additional types, it can preserve all the original types and still make the compiler happy because we’re always returning the same type to satisfy the some View
return type. But as I’ve mentioned, you’re limited to what SwiftUI can actually express. So, for example, any complex logic switching is off the table for now. Also, understand that this is a very simple case and it’s already generating a complex result type. Now imagine having a lot of nested logic and generic types, and this will soon become very hard to read and comprehend.
同样, Group
是泛型类型,但它引入了一个附加的(泛型)类型_ConditionalContent
,该类型在类型参数列表中具有我们的按钮类型(再次为泛型)。 这实际上是SwiftUI的窍门:通过聪明并引入其他类型,它可以保留所有原始类型,并使编译器满意,因为我们总是返回相同的类型以满足some View
返回类型。 但是正如我已经提到的那样,您仅限于SwiftUI可以实际表达的内容。 因此,例如,任何复杂的逻辑切换都暂时不在讨论之列。 另外,请了解这是一个非常简单的案例,并且已经在生成一个复杂的结果类型。 现在想象一下,有很多嵌套的逻辑和泛型类型,而这很快将变得很难阅读和理解。
So, the upside is that we maintain all our type information, but the downside is that we will be generating a lot of complex types and we’re limited to the expressiveness of the SwiftUI view builders.
因此,好处是我们保留了所有类型信息,但缺点是我们将生成许多复杂的类型,并且仅限于SwiftUI视图构建器的表现力。
在AnyView中包装 (Wrapping in AnyView)
Wrapping in AnyView is the other method, and it involves something called type erasure to effectively strip away information regarding the types of the views and making it seem like they’re all the same. It looks something like this:
在AnyView中包装是另一种方法,它涉及一种称为类型擦除的方法,可以有效地剥离有关视图类型的信息,并使它们看起来都一样。 看起来像这样:
We are wrapping our views here in an AnyView
that conforms itself to the View
protocol and will delegate any calls to it to the wrapped view (our buttons). To the outside world (i.e. the compiler), our function now always returns the exact same type (AnyView
) and it will not complain.
我们在这里将视图包装在符合View
协议的AnyView
,并将对它的所有调用委派给包装的视图(我们的按钮)。 对于外界(即编译器),我们的函数现在始终返回完全相同的类型( AnyView
),并且不会抱怨。
We can make this even easier by introducing an extension to View
to provide a function that can return the type-erased view for us and make it work like many of the other modifiers:
我们可以通过向View
引入扩展来提供一个函数,该函数可以为我们返回经过类型擦除的视图并使它像许多其他修饰符一样工作,从而使此操作变得更加容易:
The upside here is that we can use the full expressiveness of Swift (and not just whatever SwiftUI has implemented) with regards to control logic: if case let
or switch
or even other complex logic — it’s all possible. The downside is that you effectively lose access to the regular types and can only access the parts that AnyView
exposes to you. Since, most of the time, the wrapping in AnyView
will be the last thing you do, it’s not a very big issue and you can still access all the properties provided by the View
protocol (since AnyView
conforms to View
).
这里的好处是,我们可以在控制逻辑方面使用Swift的完整表达能力(而不仅仅是SwiftUI实现的功能): if case let
或switch
或什至其他复杂的逻辑-一切皆有可能。 缺点是您实际上无法访问常规类型,并且只能访问AnyView
公开给您的部分。 因为在大多数情况下, AnyView
的包装将是您要做的最后一件事,所以这不是一个很大的问题,并且您仍然可以访问View
协议提供的所有属性(因为AnyView
符合View
)。
There have been some concerns about performance due to the fact that SwiftUI has to destroy and rebuild the view hierarchy whenever the wrapped View
inside the AnyView
changes, but if you’re not constantly doing this (and most user interfaces don’t), there should not be an issue.
已经有大约性能,因为这样的事实,SwiftUI具有摧毁并重建视图层次每当包裹有些担忧View
里面AnyView
变化,但如果你不经常这样做(和大多数的用户界面没有),有应该不是问题。
结论 (Conclusion)
Building complex user interfaces in SwiftUI can quite rapidly become a frustrating experience due to the way the compiler dictates how we can handle generic types, protocols with associated types, and opaque types. Sooner or later, you’ll run into some of the aforementioned issues. We’ve seen two ways to circumvent these issues: one by embedding your content in a Group
(type-preserving, but with the caveat that you’re limited to what SwiftUI can express) and one by wrapping in AnyView
(effectively hiding type information from the compiler, but gaining more expressiveness). Both methods are valid and can be considered for use in your own apps, and now you should have an idea of why you might choose one over the other.
由于编译器指示我们如何处理通用类型,具有关联类型的协议和不透明类型的方式,因此在SwiftUI中构建复杂的用户界面会很快变得令人沮丧。 迟早,您都会遇到一些上述问题。 我们已经看到了两种方法来解决这些问题:一种方法是将您的内容嵌入到一个Group
(保留类型,但是需要注意的是,您限于SwiftUI可以表达的内容),另一种方法是通过包装在AnyView
(有效地隐藏类型信息)从编译器,但获得更多的表现力)。 这两种方法都是有效的,可以考虑在自己的应用程序中使用,现在您应该知道为什么可能要选择一种方法了。
As a closing note, it is impressive how Swift preserves all the typing information when building views and how it works “most of the time” given the rigorous type checking that the compiler does. If you’re interested in this, I suggest you look at how ViewBuilder
works. This is used under the hood to build SwiftUI views containing one or more child views and provide functionality to support basic logic in your view templates using, for example, TupleView
and _ConditionalContent
(the latter unfortunately being marked private). Swift by Sundell has a nice overview of many of the Swift 5.1 features that power SwiftUI/ViewBuilder.
作为结束语,令人印象深刻的是,在编译器进行严格的类型检查的情况下,Swift如何在构建视图时保留所有类型的信息,以及“大部分时间”如何工作。 如果您对此感兴趣,建议您查看ViewBuilder
工作方式。 它在后台用于构建包含一个或多个子视图的SwiftUI视图,并使用TupleView
和_ConditionalContent
(不幸的是后者被标记为私有)提供功能来支持视图模板中的基本逻辑。 Sundell的Swift很好地概述了支持SwiftUI / ViewBuilder的许多Swift 5.1功能 。
We’ve also sort of glossed over how type erasure exactly works in Swift, but it is actually used in more places in Swift, such as AnySequence
and AnyPublisher
. In the latter case, it is actually helpful to hide some type information not just from the compiler but also from others.
我们还对类型擦除在Swift中的工作原理进行了一些AnySequence
,但实际上它在Swift中的更多地方都得到了使用,例如AnySequence
和AnyPublisher
。 在后一种情况下,不仅对编译器而且对其他类型隐藏一些类型信息实际上是有帮助的。
“When you use type erasure this way, you can change the underlying publisher implementation over time without affecting existing clients.” — Apple’s official documentation
“当您以这种方式使用类型擦除时,您可以随时间更改基础发布者实现,而不会影响现有客户端。” — 苹果官方文档
Again, I recommend an article by Swift by Sundell to get to grips with type erasure.
再次,我推荐Sundell的Swift撰写的一篇文章来处理类型擦除。
翻译自: https://medium.com/better-programming/a-mixed-bag-of-swiftui-11e018a280b7
swiftui
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/275421.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!