swift和swiftui
您一直在等待的完整SwiftUI 2文档 (The Complete SwiftUI 2 Documentation You’ve Been Waiting For)
At the start of 2020, I wrote a long Medium article called “The Complete SwiftUI Documentation You’ve Been Waiting For.”
在2020年初,我写了一篇很长的中型文章,名为“ 您一直在等待的完整SwiftUI文档 。”
This was my way of sharing what I learned when I tried to fill in the gaps left by the insufficient documentation provided by Apple.
这是我分享我尝试填补Apple提供的文档不足所留下的空白时所学到的知识的方式。
Although my article seemed to help a lot of people, I also wrote it eight months late.
尽管我的文章似乎对很多人有帮助,但我也写了八个月。
Now that Apple’s 2020 developer conference is over, SwiftUI has been given some new capabilities, so hopefully, this update will make my documentation more helpful than ever before. This will be released as a series, with one chapter per article. The names of these chapters correspond with the chapter names in Apple’s SwiftUI documentation. They can be read in any order, so that’s why I’m not numbering them.
现在,Apple的2020年开发人员大会已经结束,SwiftUI已获得一些新功能,因此希望此更新将使我的文档比以往任何时候都更有帮助。 这将作为系列发布,每篇文章一个章节。 这些章节的名称与Apple的SwiftUI文档中的章节名称相对应。 可以按任何顺序读取它们,所以这就是为什么我不给它们编号。
As I promised, the current chapter isn’t as long as “Views and Controls,” which was longer than my original documentation!
如我所言,本章不如“视图和控件”长,它比我的原始文档还要长!
View Layout and Presentation
查看布局和演示
Views and Controls
视图和控件
- App Structure and Behavior 应用结构和行为
- Drawing and Animation 绘画与动画
- Framework Integration 框架整合
- State and Data Flow 状态和数据流
- Gestures 手势
- Preview 预习
I encourage you to contact me in a response below if you spot any mistakes or a subject you think I should cover in more detail.
如果您发现任何错误或您认为我应该更详细介绍的主题,建议您在以下答复中与我联系。
LazyHStack and LazyVStack (New in 2.0)LazyVGrid (New in 2.0)LazyHGrid (New in 2.0)GridItem (New in 2.0)List (Updated in 2.0)ForEach & DynamicViewContent (Updated in 2.0)ScrollViewReader (New in 2.0)ScrollViewProxy (New in 2.0)Group (Updated in 2.0)Groupbox (Updated), OutlineGroup (NEW), & DisclosureGroup (NEW)NavigationView (Updated in 2.0)TabView (Updated in 2.0)
LazyHStack和LazyVStack(2.0中的新增功能) (LazyHStack and LazyVStack (New in 2.0))
Back to contents ↑
返回目录↑
One thing that was pretty ambiguous in the first iteration of SwiftUI was whether the rows of a List
are queued or not.
在SwiftUI的第一次迭代中,一个很不明确的事情是List
的行是否排队。
When you scroll on a UITableView
, cells that leave the bottom or top of the screen are added to a queue, meaning that every cell in the table does not have to be stored in memory at once. When a cell is about to be scrolled into view, a method like func dequeueReusableCell(withIdentifier: String) -> UITableViewCell?
is called. The cells are considered to be reusable, since they can be destroyed and recreated, and removing them from the queue is called dequeueing.
在UITableView
上滚动时,离开屏幕底部或顶部的单元格将添加到队列中,这意味着表中的每个单元格不必一次存储在内存中。 当一个单元格要滚动到视图中时,类似func dequeueReusableCell(withIdentifier: String) -> UITableViewCell?
叫做。 单元被认为是可重用的,因为它们可以被销毁和重新创建,并且将它们从队列中删除称为出队 。
Anyway, it turns out that List
does reuse cells. But if you want to use a ScrollView
instead, you’re back to everything loading at once and not queuing when they leave the top or bottom of the screen. You might be okay with using List
instead of a verticalScrollView
, but what happens if you want to scroll horizontally?
无论如何,事实证明List
确实重用了cell 。 但是,如果您要使用ScrollView
,则可以返回到一次加载的所有内容,并且当它们离开屏幕顶部或底部时不会排队。 使用List
而不是垂直的ScrollView
可能会没事,但是如果要水平滚动会怎样?
If you try it, you’ll notice that List
has no option to scroll horizontally.
如果尝试,您会发现List
没有水平滚动的选项。
struct ContentView: View {
@State var text = ""
var body: some View {
VStack {
WhatJustHappenedView(text: text)
ScrollView(.horizontal) {
MyLazyHStack(text: $text)
}
.frame(height: 50)
ScrollView(.vertical) {
MyLazyVStack(text: $text)
}
}
}
}
struct WhatJustHappenedView: View {
let text: String
@State var toggleIsOn = true
var body: some View {
Group {
Toggle(isOn: $toggleIsOn) {
Text("Show what just happened")
}
.padding(.top)
if toggleIsOn {
Text("What just happened?")
Text("\(text)")
}
}
.padding(.horizontal)
}
}
struct MyLazyHStack: View {
@Binding var text: String
var body: some View {
LazyHStack {
ForEach(0..<150, id: \.self) {
index in
Text("LazyHStack \(index)")
.onAppear {
text = "LazyHStack \(index) appeared"
print(text)
}
.onDisappear {
text = "LazyHStack \(index) disappeared"
print(text)
}
}
}
}
}
struct MyLazyVStack: View {
@Binding var text: String
var body: some View {
LazyVStack {
ForEach(0..<150, id: \.self) {
index in
Text("LazyVStack \(index)")
.onAppear {
text = "LazyVStack \(index) appeared"
print(text)
}
.onDisappear {
text = "LazyVStack \(index) disappeared"
print(text)
}
}
}
}
}
In my example, we have an aptly named WhatJustHappenedView
, which prints the most recent queueing event. If the stacks weren’t lazy, every Text
cell inside them would appear once at the beginning, and they would never disappear when they are queued.
在我的示例中,我们有一个恰当地命名为WhatJustHappenedView
,它打印最近的排队事件。 如果堆栈不是惰性的,则其中的每个Text
单元格将在开始时出现一次,并且在排队时它们永远不会消失。
Instead, we see the events that prove that our memory is being allocated dynamically and not all at once.
相反,我们看到的事件证明了我们的内存是动态分配的,而不是一次分配。
LazyVGrid(2.0中的新增功能) (LazyVGrid (New in 2.0))
Back to contents ↑
返回目录↑
You can apply the same logic of the LazyVStack
and LazyHStack
section above to a grid. What if we want to lay views out in rows and columns in SwiftUI? In the original version, there was no way to go about this other than manually coding your own logic, of course! The LazyVGrid
bears a lot of visual similarity to the UICollectionView
from UIKit, but it’s a lot easier to implement. You can construct these grids using an array of GridItem
objects, which can act as rows in your layout.
您可以将上面的LazyVStack
和LazyHStack
部分的相同逻辑应用于网格。 如果我们想在SwiftUI中按行和列布局视图怎么办? 当然,在原始版本中,除了手动编码自己的逻辑外,别无他法! UICollectionView
与UIKit中的LazyVGrid
具有很多视觉相似性,但是实现起来容易UICollectionView
。 您可以使用GridItem
对象数组构造这些网格,这些对象可以在布局中充当行。
To make it easier to see the effect of changing properties of your grids, I’ve created a convenient way to lay out six steppers called SteppersView. I’m going to be providing examples that are sized using .fixed
, .adaptive
, and .relative
sizing types. These are all cases of the enum GridItem.Size
, and while .fixed
requires only one CGFloat
value, the other two require a minimum and maximum for the system to choose a value between.
为了更轻松地查看更改网格属性的效果,我创建了一种方便的方法来布置六个名为SteppersView的步进器。 我将要提供使用大小的例子.fixed
, .adaptive
和.relative
大小类型。 这些都是枚举GridItem.Size
所有情况,而.fixed
仅需要一个CGFloat
值,而其他两个则需要最小值和最大值,系统才能在其中选择一个值。
First we have .fixed
, which gives an explicit width to the columns of the LazyVGrid
:
首先,我们有.fixed
,这给出了一个明确的宽度到的列LazyVGrid
:
// Requires SteppersView which can be found here:
// https://gist.github.com/sturdysturge/eed04e007cef3222729663d9eed0d7d6
import SwiftUI
struct ContentView: View {
@State var column1Width: CGFloat = 20.0
@State var column2Width: CGFloat = 20.0
@State var column3Width: CGFloat = 20.0
@State var column1Spacing: CGFloat = 50.0
@State var column2Spacing: CGFloat = 50.0
@State var column3Spacing: CGFloat = 50.0
let rows = 50
let columns = 3
var body: some View {
VStack {
SteppersView(
control1A: ("Column 1 Width", $column1Width),
control1B: ("Column 1 Spacing", $column1Spacing),
control2A: ("Column 2 Width", $column2Width),
control2B: ("Column 2 Spacing", $column2Spacing),
control3A: ("Column 3 Width", $column3Width),
control3B: ("Column 3 Spacing", $column3Spacing)
)
ScrollView(.vertical) {
LazyVGrid(columns: [
GridItem(.fixed(column1Width), spacing: column1Spacing),
GridItem(.fixed(column2Width), spacing: column2Spacing),
GridItem(.fixed(column3Width), spacing: column3Spacing)
], alignment: .center, spacing: 19) {
ForEach(0..<(columns * rows), id: \.self) {
index in
Rectangle()
.foregroundColor(.red)
.frame(height: 25)
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical)
Spacer()
}
}
}
Now we have .flexible
, which allows the columns to grow to the maximum width they have available. This is similar to using the .frame(maxWidth: .infinity)
modifier on any other view. Although columns can grow or shrink according to the requirements of those around them, they cannot change the number of columns in a row. This means that we still end up with an appropriate number of rows, as is seen if you scroll to the bottom and see that the bottom row has the same number as all previous rows.
现在我们有了.flexible
,它可以使列增长到可用的最大宽度。 这类似于在其他任何视图上使用.frame(maxWidth: .infinity)
修饰符。 尽管可以根据周围的列的要求来增加或缩小列,但是它们不能更改一行中的列数。 这意味着我们仍然可以得到适当数量的行,就像您滚动到底部并看到最底部的行与所有先前的行具有相同的行数一样。
// Requires SteppersView which can be found here:
// https://gist.github.com/sturdysturge/eed04e007cef3222729663d9eed0d7d6
import SwiftUI
struct LazyVGridFlexibleView: View {
@State var column1MinWidth: CGFloat = 50.0
@State var column2MinWidth: CGFloat = 50.0
@State var column3MinWidth: CGFloat = 50.0
@State var column1MaxWidth: CGFloat = 50.0
@State var column2MaxWidth: CGFloat = 50.0
@State var column3MaxWidth: CGFloat = 50.0
let rows = 50
let columns = 3
var body: some View {
VStack {
SteppersView(
control1A: ("Column 1 Min Width", $column1MinWidth),
control1B: ("Column 1 Max Width", $column1MaxWidth),
control2A: ("Column 2 Min Width", $column2MinWidth),
control2B: ("Column 2 Max Width", $column2MaxWidth),
control3A: ("Column 3 Min Width", $column3MinWidth),
control3B: ("Column 3 Max Width", $column3MaxWidth)
)
ScrollView(.vertical) {
LazyVGrid(columns: [
GridItem(.flexible(minimum: column1MinWidth, maximum: column1MaxWidth)),
GridItem(.flexible(minimum: column2MinWidth, maximum: column2MaxWidth)),
GridItem(.flexible(minimum: column3MinWidth, maximum: column3MaxWidth)),
]) {
ForEach(0..<(columns * rows), id: \.self) {
index in
Rectangle()
.foregroundColor(.red)
.frame(height: 25)
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical)
Spacer()
}
}
}
GridItem.Size.adaptive
is different from .flexible
in one simple way. While these cells still have a minimum and maximum width, they will not prevent cells from the row below moving up in order to occupy available space. This is assuming that the available space is larger than the minimum width that the cells can occupy, of course. The difference here can be observed most clearly when scrolling to the bottom, as it is easy to achieve a situation in which the last row has less cells in it than the previous rows.
GridItem.Size.adaptive
与.flexible
以一种简单的方式不同。 尽管这些单元格仍具有最小和最大宽度,但它们不会阻止下一行的单元格向上移动以占用可用空间。 当然,这是假定可用空间大于单元格可以占用的最小宽度。 滚动到底部时,可以最清楚地观察到此处的差异,因为很容易实现最后一行中的单元格少于前一行的情况。
This is because the number of cells we calculated using columns * rows
is no longer an accurate representation of the cells, as there are more items per row than previously expected.
这是因为我们使用columns * rows
计算的单元格数量不再是单元格的准确表示,因为每行中的项目比以前预期的要多。
// Requires SteppersView which can be found here:
// https://gist.github.com/sturdysturge/eed04e007cef3222729663d9eed0d7d6
import SwiftUI
struct ContentView: View {
@State var column1MinWidth: CGFloat = 50.0
@State var column2MinWidth: CGFloat = 50.0
@State var column3MinWidth: CGFloat = 50.0
@State var column1MaxWidth: CGFloat = 50.0
@State var column2MaxWidth: CGFloat = 50.0
@State var column3MaxWidth: CGFloat = 50.0
let rows = 50
let columns = 3
var body: some View {
VStack {
SteppersView(
control1A: ("Column 1 Min Width", $column1MinWidth),
control1B: ("Column 1 Max Width", $column1MaxWidth),
control2A: ("Column 2 Min Width", $column2MinWidth),
control2B: ("Column 2 Max Width", $column2MaxWidth),
control3A: ("Column 3 Min Width", $column3MinWidth),
control3B: ("Column 3 Max Width", $column3MaxWidth)
)
ScrollView(.vertical) {
LazyVGrid(columns: [
GridItem(.adaptive(minimum: column1MinWidth, maximum: column1MaxWidth)),
GridItem(.adaptive(minimum: column2MinWidth, maximum: column2MaxWidth)),
GridItem(.adaptive(minimum: column3MinWidth, maximum: column3MaxWidth)),
]) {
ForEach(0..<(columns * rows), id: \.self) {
index in
Rectangle()
.foregroundColor(.red)
.frame(height: 25)
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical)
Spacer()
}
}
}
LazyHGrid(2.0中的新增功能) (LazyHGrid (New in 2.0))
Back to contents ↑
返回目录↑
Like LazyVGrid
above, the examples here require controls so that you can play around with them in the subsequent examples. All of the examples use six different steppers, so I’ve provided SteppersView, which allows you to lay them out for each example.
与上面的LazyVGrid
一样,此处的示例也需要控件,以便您可以在后续示例中使用它们。 所有示例都使用六个不同的步进器,因此我提供了SteppersView ,它允许您为每个示例布置它们。
// Requires StepperView which can be found here:
// https://gist.github.com/sturdysturge/eed04e007cef3222729663d9eed0d7d6
import SwiftUI
struct LazyHGridFixedView: View {
@State var row1Height: CGFloat = 20.0
@State var row2Height: CGFloat = 20.0
@State var row3Height: CGFloat = 20.0
@State var row1Spacing: CGFloat = 50.0
@State var row2Spacing: CGFloat = 50.0
@State var row3Spacing: CGFloat = 50.0
let columns = 50
let rows = 3
var body: some View {
VStack {
SteppersView(
control1A: ("Row 1 Height", $row1Height),
control1B: ("Row 1 Spacing", $row1Spacing),
control2A: ("Row 2 Height", $row2Height),
control2B: ("Row 2 Spacing", $row2Spacing),
control3A: ("Row 3 Height", $row3Height),
control3B: ("Row 3 Spacing", $row3Spacing)
)
ScrollView(.horizontal) {
LazyHGrid(rows: [
GridItem(.fixed(row1Height), spacing: row1Spacing),
GridItem(.fixed(row2Height), spacing: row2Spacing),
GridItem(.fixed(row3Height), spacing: row3Spacing)
], alignment: .center, spacing: 19) {
ForEach(0..<(columns * rows), id: \.self) {
index in
Rectangle()
.foregroundColor(.red)
.frame(width: 25)
}
}
}
.frame(maxHeight: .infinity)
.padding(.vertical)
Spacer()
}
}
}
Now we have .flexible
, which allows the rows to grow to the maximum height they have available. This is similar to using the .frame(maxHeight: .infinity)
modifier on any other view. Although columns can grow or shrink according to the requirements of those around them, they cannot change the number of rows in a column. This means that we still end up with an appropriate number of columns, as is seen if you scroll to the right and see that the last column has the same number as all previous columns.
现在我们有了.flexible
,它可以使行增长到可用的最大高度。 这类似于在其他任何视图上使用.frame(maxHeight: .infinity)
修饰符。 尽管可以根据周围的列的要求来增加或缩小列,但是它们不能更改列中的行数。 这意味着我们仍然可以得到适当数量的列,就像您向右滚动并看到最后一列具有与所有先前列相同的列数一样。
// Requires SteppersView which can be found here:
// https://gist.github.com/sturdysturge/eed04e007cef3222729663d9eed0d7d6
import SwiftUI
struct LazyHGridAdaptiveView: View {
@State var row1MinHeight: CGFloat = 50.0
@State var row2MinHeight: CGFloat = 50.0
@State var row3MinHeight: CGFloat = 50.0
@State var row1MaxHeight: CGFloat = 50.0
@State var row2MaxHeight: CGFloat = 50.0
@State var row3MaxHeight: CGFloat = 50.0
let columns = 50
let rows = 3
var body: some View {
VStack {
SteppersView(
control1A: ("Row 1 Min Height", $row1MinHeight),
control1B: ("Row 1 Max Height", $row1MaxHeight),
control2A: ("Row 2 Min Height", $row2MinHeight),
control2B: ("Row 2 Max Height", $row2MaxHeight),
control3A: ("Row 3 Min Height", $row3MinHeight),
control3B: ("Row 3 Max Height", $row3MaxHeight)
)
ScrollView(.horizontal) {
LazyHGrid(rows: [
GridItem(.adaptive(minimum: row1MinHeight, maximum: row1MaxHeight)),
GridItem(.adaptive(minimum: row2MinHeight, maximum: row2MaxHeight)),
GridItem(.adaptive(minimum: row3MinHeight, maximum: row3MaxHeight)),
], alignment: .center, spacing: 19) {
ForEach(0..<(columns * rows), id: \.self) {
index in
Rectangle()
.foregroundColor(.red)
.frame(width: 25)
}
}
}
.frame(maxHeight: .infinity)
.padding(.vertical)
Spacer()
}
}
}
GridItem.Size.adaptive
is different from .flexible
in one simple way. While these cells still have a minimum and maximum height, they will not prevent cells from the column to the right moving left in order to occupy available space. This is assuming that the available space is larger than the minimum height that the cells can occupy, of course. The difference here can be observed most clearly when scrolling to the right, as it is easy to achieve a situation in which the last column has less cells in it than the previous columns.
GridItem.Size.adaptive
与.flexible
以一种简单的方式不同。 尽管这些单元格仍具有最小和最大高度,但它们不会阻止从列到右侧的单元格向左移动以占用可用空间。 当然,这是假定可用空间大于单元格可以占用的最小高度。 向右滚动时,可以最清楚地观察到此处的差异,因为很容易实现最后一列的单元格少于前一列的情况。
This is because the number of cells we calculated using columns * rows
is no longer an accurate representation of the cells, as there are more items per column than previously expected.
这是因为我们使用columns * rows
计算的单元格数量不再是单元格的准确表示,因为每列中的项比以前预期的要多。
// Requires SteppersView which can be found here:
// https://gist.github.com/sturdysturge/eed04e007cef3222729663d9eed0d7d6
import SwiftUI
struct LazyHGridFlexibleView: View {
@State var row1MinHeight: CGFloat = 50.0
@State var row2MinHeight: CGFloat = 50.0
@State var row3MinHeight: CGFloat = 50.0
@State var row1MaxHeight: CGFloat = 50.0
@State var row2MaxHeight: CGFloat = 50.0
@State var row3MaxHeight: CGFloat = 50.0
let columns = 50
let rows = 3
var body: some View {
VStack {
SteppersView(
control1A: ("Row 1 Min Height", $row1MinHeight),
control1B: ("Row 1 Max Height", $row1MaxHeight),
control2A: ("Row 2 Min Height", $row2MinHeight),
control2B: ("Row 2 Max Height", $row2MaxHeight),
control3A: ("Row 3 Min Height", $row3MinHeight),
control3B: ("Row 3 Max Height", $row3MaxHeight)
)
ScrollView(.horizontal) {
LazyHGrid(rows: [
GridItem(.flexible(minimum: row1MinHeight, maximum: row1MaxHeight)),
GridItem(.flexible(minimum: row2MinHeight, maximum: row2MaxHeight)),
GridItem(.flexible(minimum: row3MinHeight, maximum: row3MaxHeight))
], alignment: .center, spacing: 19) {
ForEach(0..<(columns * rows), id: \.self) {
index in
Rectangle()
.foregroundColor(.red)
.frame(width: 25)
}
}
}
.frame(maxHeight: .infinity)
.padding(.vertical)
Spacer()
}
}
}
GridItem(2.0中的新增功能) (GridItem (New in 2.0))
Back to contents ↑
返回目录↑
You can see some great examples of GridItem
in action above, in LazyHGrid
and LazyVGrid
.
您可以在上面的LazyHGrid
和LazyVGrid
看到一些出色的GridItem
示例。
A GridItem
must be given a size, but spacing and alignment are optional.
必须给GridItem
指定大小,但是间距和对齐方式是可选的。
The GridItem.Size
enum has three cases:
GridItem.Size
枚举有以下三种情况:
case adaptive(minimum: CGFloat, maximum: CGFloat)
case adaptive(minimum: CGFloat, maximum: CGFloat)
case fixed(CGFloat)
case fixed(CGFloat)
case flexible(minimum: CGFloat, maximum: CGFloat)
case flexible(minimum: CGFloat, maximum: CGFloat)
Bear in mind that failing to give a value for the spacing property allows your columns (in LazyVGrid
) or your rows (in LazyHGrid
) to potentially end up touching one another if they are not given enough space.
请记住,如果不给LazyVGrid
属性指定值,则如果没有足够的空间,则您的列(在LazyVGrid
)或行(在LazyHGrid
)可能最终彼此接触。
Being explicit about spacing gives you more control about how you want them to adapt, assuming that the size they were given was not of type .fixed
.
明确指定间距可以让您更好地控制它们的适应方式,假设给出的尺寸不是.fixed
类型。
列表(在2.0中更新) (List (Updated in 2.0))
Back to contents ↑
返回目录↑
List
, the vertical ScrollView
that allows lazy loading of content only when it is visible on the screen, has some new initialisers in 2.0.
List
是一种垂直ScrollView
,仅在屏幕上可见时才允许延迟加载内容,它在2.0中具有一些新的初始化程序。
init(Data, children: KeyPath
, selection: Binding ?, rowContent: (Data.Element) -> RowContent) init(Data, children: KeyPath
, selection: Binding ?, rowContent: (Data.Element) -> RowContent) init(Data, children: KeyPath
, selection: Binding >?, rowContent: (Data.Element) -> RowContent) init(Data, children: KeyPath
, selection: Binding >?, rowContent: (Data.Element) -> RowContent) init(Data, id: KeyPath
, children: KeyPath , selection: Binding >?, rowContent: (Data.Element) -> RowContent) init(Data, id: KeyPath
, children: KeyPath , selection: Binding >?, rowContent: (Data.Element) -> RowContent) init(Data, id: KeyPath
, children: KeyPath , selection: Binding ?, rowContent: (Data.Element) -> RowContent) init(Data, id: KeyPath
, children: KeyPath , selection: Binding ?, rowContent: (Data.Element) -> RowContent)
These initialisers all have one thing in common. They were all available when SwiftUI launched, but they were only available on tvOS and watchOS.
这些初始化程序有一个共同点。 它们在SwiftUI启动时都可用,但是仅在tvOS和watchOS上可用。
All of these initialisers have now been added iOS, macOS and Mac Catalyst.
所有这些初始化程序现已添加到iOS,macOS和Mac Catalyst。
ForEach和DynamicViewContent(在2.0中更新) (ForEach & DynamicViewContent (Updated in 2.0))
Back to contents ↑
返回目录↑
In the “Views and Controls” chapter of this documentation, I talked about the new UTType
structure that had replaced a rather confusing method. Instead of being able to create objects that represent data types, we had to resort to passing an array of strings that represented data types.
在本文档的“ 视图和控件 ”一章中,我谈到了新的UTType
结构,该结构已替代了一个相当混乱的方法。 除了能够创建代表数据类型的对象外,我们不得不诉诸于传递代表数据类型的字符串数组。
This is not obvious in the initialiser for ForEach
, but it conforms to the DynamicViewContent
protocol. This happens when the generic Content
conforms to View
, which confusingly isn’t required by the ForEach
structure itself. Every initialiser exists in an extension that does require that Content
conforms to View
though, so don’t go thinking you can use ForEach
for any other purpose.
这在ForEach
的初始化程序中并不明显,但它符合DynamicViewContent
协议。 当通用Content
符合View
,就会发生这种情况,而ForEach
结构本身并不需要混淆性的要求。 每个扩展程序都存在于一个扩展中,该扩展确实要求Content
符合View
,所以不要以为您可以将ForEach
用于任何其他目的。
DynamicViewContent
requires a Collection
of data, the particular type of which is inferred by the data that it is given. What does it do, you ask. It provides methods such as onDelete
, which gives you the ability to run a closure when the user deletes a row of a List
. While onDelete
hasn’t changed since last year, onInsert
has. This occurs when an item is dragged using the onDrag
modifier, as List
uses onInsert
instead of the more conventional onDrop
modifier.
DynamicViewContent
需要数据Collection
,其特定类型由给出的数据推断。 您会问,它是做什么的。 它提供了诸如onDelete
方法,该方法使您能够在用户删除List
的一行时运行闭包。 虽然onDelete
还没有从去年开始改变, onInsert
了。 当使用onDrag
修改器拖动项目时会发生这种情况,因为List
使用onInsert
而不是更常规的onDrop
修改器。
More information on drag and drop was contained in the “Views and Controls” chapter, so the main thing to point out is that onInsert
now takes a UTType
structure instead of the previous array of strings representing the UTType
s. This allows us to specify what kind of data can be dragged and dropped into a List
, as otherwise we would not know whether we can add that data to the underlying Collection
or not.
有关拖放的更多信息包含在“视图和控件”一章中,因此主要要指出的是, onInsert
现在采用UTType
结构,而不是之前的表示UTType
的字符串数组。 这使我们可以指定可以将哪种数据拖放到List
,否则我们将不知道是否可以将该数据添加到基础Collection
。
But that’s not all that’s changed.
但这还不是全部更改。
If you look at the new initialiser for ForEach
, you might notice something is different:
如果您查看ForEach
的新初始化程序,您可能会发现有所不同:
init(_ data: Data, id: KeyPath, @ViewBuilder content: @escaping (Data.Element) -> Content)
Like the body: some View
property of a View
struct, the initialiser now takes a @ViewBuilder
closure. Why does this matter? This is is effectively like wrapping our layout in a Group
in the first iteration of SwiftUI. We did this because we wanted to be able to return one concrete type that conforms to the view protocol, and adding multiple values in the closure made it impossible to do that.
就像body: some View
View
结构的body: some View
属性一样,初始化程序现在使用@ViewBuilder
闭包。 为什么这么重要? 这实际上就像在SwiftUI的第一次迭代中将布局包装在Group
中一样。 之所以这样做,是因为我们希望能够返回一种符合视图协议的具体类型,并且在闭包中添加多个值使其无法实现。
Now you can add whatever you want inside a ForEach
, as long as it is less than ten views in size.
现在,您可以在ForEach
添加所需的任何内容,只要它的大小小于十个视图即可。
Obviously this excludes the underlying data, so you could for instance have a List
row with ten views in it, but that row is one of 100 or more rows that get their data from an array or other data structure.
显然,这不包括基础数据,因此,例如,您可以有一个包含十个视图的List
行,但是该行是从数组或其他数据结构获取其数据的100个或更多行中的一个。
The power of ForEach
is the ability to effectively treat as many items as you want as if they were one view in your hierarchy.
ForEach
是能够有效地处理ForEach
数量的项目,就好像它们是层次结构中的一个视图一样。
ScrollViewReader(2.0中的新增功能) (ScrollViewReader (New in 2.0))
Back to contents ↑
返回目录↑
There is some similarity between the existing GeometryReader
and the new ScrollViewReader
.
现有的GeometryReader
与新的ScrollViewReader
之间存在一些相似之处。
They are both closures that pass in a single parameter.
它们都是传递单个参数的闭包。
A GeometryReader
passes a GeometryProxy
which has two properties: safeAreaInsets: EdgeInsets
and size: CGSize
. This proxy comes with a method that will return a CGRect
for the frame, but it requires a coordinate space in which to calculate this frame. The most obvious one is .global
, as this gives a frame that is relative to the entire screen. But you can create custom coordinateSpace
with a name that you specify, allowing you to get a frame relative to another View in the hierarchy.
一个GeometryReader
传递一个GeometryProxy
,它具有两个属性: safeAreaInsets: EdgeInsets
和size: CGSize
。 该代理附带有一种方法,该方法将为框架返回CGRect
,但是它需要一个坐标空间来计算该框架。 最明显的是.global
,因为它提供了相对于整个屏幕的框架。 但是你可以创建自定义 coordinateSpace
与您指定的名称 ,让您获得相对于层次结构中的另一个View的框架。
ScrollViewProxy
has no properties, but it has a single method that performs an action instead of returning a value. When we specify an id
for Views in a ScrollView
, we can provide any Hashable
type. With this we are telling Swift which part of our type is unique so that it can differentiate between instances of that type.
ScrollViewProxy
没有属性,但它具有执行操作而不是返回值的单个方法。 当我们在ScrollView
为Views指定一个id
,我们可以提供任何Hashable
类型。 这样我们告诉Swift我们类型的哪一部分是唯一的,以便可以区分该类型的实例。
In my example, I’m just using the index for each row in my List
as an ID.
在我的示例中,我只是将List
每一行的索引用作ID。
Many provided Swift types already conform to Hashable, so this is easier than making a Hashable type yourself. Here’s how to conform to the Hashable protocol if you’re interested, and you’ll see there that it isn’t a lot of effort at all. Now that I can identify the rows of my List
, I provided a TextField
that you can type a number into and a Button
that will send the ScrollView
to that row automatically.
许多提供的Swift类型已经符合Hashable,因此这比自己制作Hashable类型要容易。 如果您感兴趣的话,这里是如何遵循Hashable协议的方法 ,您会发现它根本不需要花费很多精力。 现在,我可以识别List
的行了,我提供了一个TextField
可以在其中键入数字)和一个Button
,它将ScrollView
自动发送到该行。
struct Contentview: View {
@State var target = 0
var body: some View {
ScrollViewReader { proxy in
VStack {
Group {
Text("Type a number using lower case words like 'thirty-four' and press return on the keyboard")
HStack {
TargetTextField(target: $target)
GoToButton(target: target, proxy: proxy)
}
}
.padding()
List {
ForEach(0..<100, id: \.self) {
index in
Text("Item \(index)")
.id(index)
}
Button("Back to top") {
proxy.scrollTo(0)
}
}
}
}
}
}
struct GoToButton: View {
let target: Int
let proxy: ScrollViewProxy
var body: some View {
Button("Go to \(target)") {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
withAnimation {
proxy.scrollTo(target)
}
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(15)
}
}
struct TargetTextField: View {
static var formatter: NumberFormatter {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
return formatter
}
@Binding var target: Int
var body: some View {
TextField("Enter a number", value: $target, formatter: Self.formatter)
.padding()
.background(Color.gray)
.cornerRadius(15)
}
}
I’ve made my example more difficult to use by using a NumberFormatter
which uses a numberStyle
called spellOut
. This requires you to spell your numbers as lowercase words, putting a hyphen between a number like thirty-four and omitting words like and. Have a play around with it, and if you get bored of it, you can always change the NumberFormatter
to something more sensible if you prefer.
我做了我的例子比较难用用NumberFormatter
其采用了numberStyle
称为spellOut
。 这需要你拼出你的号码为小写的话,把一个连字符数量之间像34和省略的话就像和 。 试一试,如果您对它感到厌烦,可以随时将NumberFormatter
更改为更明智的选择。
Notice that the scrolling is animated, but at the bottom of the List
there is a Button that says “Back to top.” Unlike the GoToButton
at the top, which puts its proxy.scrollTo(_:)
inside a withAnimation
block, this Button
does not add an explicit animation. This is the default behaviour of the scrollTo(_:)
action, instantly scrolling without any animation. Keep this in mind if you want to animate any changes to the scroll location.
请注意,滚动是动画的,但是在List
的底部有一个按钮,显示“返回页首”。 与顶部的GoToButton
不同,该Button
将其proxy.scrollTo(_:)
放在withAnimation
块内,此Button
不会添加显式动画。 这是scrollTo(_:)
操作的默认行为,无需任何动画即可立即滚动。 如果要对滚动位置进行动画处理,请记住这一点。
Notice how I was able to pass the ScrollViewProxy
as a parameter to GoToButton
, so that the ability to change the scroll location can be passed between views.
请注意,我如何能够将ScrollViewProxy
作为参数传递给GoToButton
,以便可以在视图之间传递更改滚动位置的功能。
ScrollViewProxy(2.0中的新增功能) (ScrollViewProxy (New in 2.0))
Back to contents ↑
返回目录↑
See ScrollViewReader
above, which passes a ScrollViewProxy
as a parameter into its closure the same way as GeometryReader
passes a GeometryProxy
.
参见上面的ScrollViewReader
,它将ScrollViewProxy
作为参数传递到它的闭包中,就像GeometryReader
传递GeometryProxy
。
组(在2.0中更新) (Group (Updated in 2.0))
Back to contents ↑
返回目录↑
Now that more structures take a @ViewBuilder
closure, and therefore return a TupleView
that contains up to ten children that all conform to View, you might think that Group
no longer has much purpose.
现在,更多结构采用@ViewBuilder
闭包,并因此返回一个TupleView
,其中包含最多十个都符合View的子级,您可能会认为Group
不再具有太大的用途。
After all this “affordance for grouping view content,” as Apple calls it, did little else at that point.
就像苹果公司所说的那样,在完成了所有“为视图内容分组后的功能”之后,其他事情就没有了。
But now we have new possibilities, as we can now group anything conforming to these protocols too:
但是现在有了新的可能性,因为我们现在也可以将符合这些协议的所有内容归为一组:
Scene
Scene
Widget
Widget
Commands
Commands
ToolbarContent
ToolbarContent
I’ll go into a lot more detail about what these are in a later chapter called “App Structure and Behavior,” but the important thing to know is that Group
has new capabilities.
在后面的“应用程序结构和行为”一章中,我将详细介绍这些内容,但重要的是要知道Group
具有新功能。
In much the same way that @ViewBuilder
allows Group
to combine up to ten views, @_WidgetBuilder
allows a combination of up to ten widgets. When macOS has commands that it will display in the menus at the top of the screen, up to ten can be added with @CommandBuilder
.
@ViewBuilder
允许Group
最多组合十个视图,而@_WidgetBuilder
可以组合十个小部件。 当macOS具有将显示在屏幕顶部菜单中的命令时, @CommandBuilder
最多可以添加@CommandBuilder
。
Building a toolbar?
建立工具列?
You guessed it:@ToolbarBuilder
will allow up to ten children.
您猜对了: @ToolbarBuilder
最多允许十个孩子。
Now that SwiftUI apps can be created without an AppDelegate
, we use a structure that conforms to the App
protocol, which in turn requires a body that conforms to the Scene
protocol.
现在,无需使用AppDelegate
即可创建SwiftUI应用程序,我们使用符合App
协议的结构,而该结构又需要符合Scene
协议的主体。
When multiple scenes are provided within a Group
, @SceneBuilder
allows us to add up to ten children.
当一个Group
中提供多个场景时, @SceneBuilder
允许我们最多添加十个孩子。
This differs from WindowGroup
, which specifically provides views that will be given identically structured yet separate windows. Since WindowGroup
conforms to the Scene
protocol itself, it can be at the top of the hierarchy inside the body of an App
structure. If a Group
only has children that conform to the View
protocol, it cannot be used in the same way.
这不同于WindowGroup
,后者专门提供了视图,这些视图将具有相同的结构,但具有独立的窗口。 由于WindowGroup
符合Scene
协议本身,因此它可以位于App
结构主体内部的层次结构的顶部。 如果Group
仅具有符合View
协议的子级,则不能以相同的方式使用它。
In other words, a structure conforming to App
can contain:
换句话说,符合App
的结构可以包含:
A
Group
made up of up to tenWindowGroup
children一
Group
由多达十WindowGroup
儿童A group made up of up to ten
Scene
-conforming children由最多十个符合
Scene
孩子组成的小组A
WindowGroup
made up of up to ten Group- or other View-conforming children一个
WindowGroup
由最多十个符合WindowGroup
或其他符合View的子项组成
If this is confusing, don’t worry. It’ll be covered in way more detail in the “App Structure and Behavior” chapter.
如果这令人困惑,请不要担心。 “应用程序的结构和行为”一章将对此进行更详细的介绍。
Groupbox,OutlineGroup和DisclosureGroup (Groupbox, OutlineGroup, & DisclosureGroup)
Of these three, GroupBox
is the only one that isn’t new in 2.0.
在这三个组件中, GroupBox
是2.0版中唯一不新增的组件。
When Groupbox
was made available when SwiftUI was originally released, it was only available on macOS, and the main change is that it is now cross-platform. This is an easy way of grouping content together with an optional label. OutlineGroup
provides an ability to reveal additional information about an item that would otherwise be hidden. DisclosureGroup
has a similar purpose, with the addition of a Binding
that can control whether or not the additional information is shown.
在最初发布Groupbox
时使Groupbox
可用时,它仅在macOS上可用,主要的变化是现在它是跨平台的。 这是将内容与可选标签一起分组的一种简便方法。 OutlineGroup
提供了显示有关可能会隐藏的项目的其他信息的功能。 DisclosureGroup
具有类似的目的,增加了一个Binding
,它可以控制是否显示其他信息。
You can find examples of this, as well as the new OutlineGroup
and DisclosureGroup
, in “SwiftUI’s GroupBox, OutlineGroup, and DisclosureGroup in iOS 14” by Anupam Chugh.
您可以在 Anupam Chugh 撰写的 “ iOS 14中的SwiftUI的GroupBox,OutlineGroup和DisclosureGroup中 ”找到此示例以及新的OutlineGroup
和DisclosureGroup
。
NavigationView(在2.0中更新) (NavigationView (Updated in 2.0))
Back to contents ↑
返回目录↑
I thought this was already available on watchOS, as I had previously released a watchOS app that lets you choose pictures of dogs from a List
. But it turns out that despite using a NavigationLink
in that app, I was not embedding it inside a NavigationView
. This would compile for iOS and macOS, but it would not allow navigation due to the lack of NavigationView
. Presumably something about the way watchOS always works on the basis of stacked navigation makes this unnecessary, but other platforms have no expectation that this would be the case.
我以为它已经在watchOS上可用了,因为我以前发布了一个watchOS应用程序,可以从List
选择狗的图片。 但事实证明,尽管在该应用程序中使用了NavigationLink
,但我并未将其嵌入到NavigationView
。 这将针对iOS和macOS进行编译,但由于缺少NavigationView
,因此将不允许导航。 可能有关watchOS始终基于堆叠导航的工作方式的某些事情使此操作变得不必要,但是其他平台并不期望会出现这种情况。
WatchOS now has the ability to use .navigationViewStyle
, but it seems the only provided value for it is StackNavigationViewStyle
.
WatchOS现在可以使用.navigationViewStyle
,但是似乎唯一提供的值是StackNavigationViewStyle
。
The only other option on any platform isDoubleColumnNavigationViewStyle
, and you can bet that's not coming to WatchOS any time soon!
在任何平台上,唯一的其他选项是DoubleColumnNavigationViewStyle
,您可以打赌,很快就不会在WatchOS上使用它了!
TabView(在2.0中更新) (TabView (Updated in 2.0))
Back to contents ↑
返回目录↑
I already mentioned this when I went through the new standard View Modifiers in the “Views and Controls” chapter. That was when I was covering the .tabItem
modifier, which has changed in the same way as the TabView
it applies to.
我在“ 视图和控件 ”一章中通过新的标准视图修改器时已经提到了这一点。 那是我讨论.tabItem
修饰符的时候,该修饰符的更改方式与其应用于的TabView
相同。
To recap what I said then, I’ll post Apple’s example with the addition of the @available
attribute at the top.
回顾一下我刚才讲的内容,我将在顶部添加@available
属性的情况下发布Apple的示例。
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 7.0, *)
struct TabItem: View {
var body: some View {
TabView {
View1()
.tabItem {
Image(systemName: "list.dash")
Text("Menu")
}
View2()
.tabItem {
Image(systemName: "square.and.pencil")
Text("Order")
}
}
}
}
struct View1: View {
var body: some View {
Text("View 1")
}
}
struct View2: View {
var body: some View {
Text("View 2")
}
}
Notice anything?
注意到什么了吗?
TabView
, along with the modifier .tabItem
that allows you to create the icon that represents that page on the tab bar, is new to watchOS. Although it was available on Mac, iOS, iPadOS and tvOS last year, it has only just come to the watch this year. What form could it possibly take, you might ask? It resembles a UIPageViewController
from UIKit, with each page requiring you to swipe horizontally from one to the other. The although the .tabItem
modifier exists, neither the Text
nor the Image
that Apple’s example provides are visible.
TabView
和修改器.tabItem
一起使您可以在选项卡栏上创建代表该页面的图标,这是watchOS的新增功能。 尽管它去年在Mac,iOS,iPadOS和tvOS上可用,但今年才出现。 您可能会问,它可能采用什么形式? 它类似于UIKit中的UIPageViewController
,每个页面都需要您从一个页面水平滑动到另一个页面。 尽管存在.tabItem
修饰符,但Apple示例提供的Text
和Image
都不可见。
Instead we get dots, much in the same way that UIPageViewController
makes use of a UIPageControl
, which Apple describes as "a horizontal series of dots, each of which corresponds to a page in the app’s document or other data-model entity.”
相反,我们得到点,就像UIPageViewController
使用UIPageControl
,Apple将其描述为“水平的点序列,每个点对应于应用程序文档或其他数据模型实体中的页面”。
下一步 (Next Steps)
SwiftUI is only a year old as I’m writing this, and there are already a wealth of resources out there. My writing would not be possible without the following websites:
在我撰写本文时,SwiftUI才刚成立一年,并且那里已经有很多资源。 没有以下网站,我的写作将是不可能的:
LOSTMOA Blog
LOSTMOA博客
Hacking with Swift
用Swift入侵
Swift UI Lab
Swift UI实验室
Swift with Majid
斯威夫特与马吉德
WWDC by Sundell
WWDC,桑德尔
Swift by Sundell
迅捷的桑德尔
If you’ve got a great resource to share with the community, let me know and I’ll gladly add it to this list.
如果您有很多资源可以与社区分享,请告诉我,我们很乐意将其添加到此列表中。
As I said at the start of the article, If you have requests for more detail on a subject, or if you think I’ve made a mistake, let me know in a response below.
就像我在文章开头所说的那样,如果您要求提供有关某个主题的更多详细信息,或者您认为自己犯了一个错误,请在下面的回复中告诉我。
Thanks for reading!
谢谢阅读!
翻译自: https://medium.com/better-programming/view-layout-and-presentation-in-swiftui-705b7d81f03
swift和swiftui