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.pswp.cn/news/275421.shtml 繁體地址,請注明出處:http://hk.pswp.cn/news/275421.shtml 英文地址,請注明出處:http://en.pswp.cn/news/275421.shtml
如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!