SWIFUI布局系统指南-第1部分

1,004 阅读11分钟

翻译地址

除了具有声明性的DSL和强大的数据绑定之外,SWIFUI还提供了一个全新的布局系统,它在许多方面将手工框架计算的显式性与自动布局的适应性结合起来。其结果是,这个系统乍一看可能看起来很简单,但一旦我们开始将它的各种构建块合并到越来越复杂的布局中,它就提供了巨大的灵活性和强大的功能。

本周,让我们从零开始构建一个全屏视图来探索SwiftUI布局系统。在此过程中,我们将使用许多不同的布局技术和API--这些技术和API将共同演示SwiftUI布局系统的基本规则是什么,以及这些规则如何相互关联。

设置视图的框架

让我们从一个简单的ContentView将日历图像呈现为body,通过引用苹果内置的一个SF符号:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
    }
}

默认情况下,SwiftUI允许每个视图根据呈现的容器选择自己的大小,然后将其集中在其父视图中。因此,上述代码的结果是在屏幕中心呈现一个小图标--而不是像我们所预期的那样,出现在左上角或左下角,这取决于UIKit和AppKit的工作方式。

接下来,让我们的图标更大一点,比如说50x50点。关于如何实现这一目标的最初想法可能是使用.frame() 视图修饰符告诉我们要采用这样的规模:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .frame(width: 50, height: 50)
    }
}

但是,当上面的代码将要结果观景这是50x50点,我们的图标的大小将保持与以前完全一样-这可能在一开始看起来有点奇怪。为了探究原因,让我们给我们的视图一个背景颜色,以便我们可以很容易地看到它的屏幕框架:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .frame(width: 50, height: 50)
            .background(Color.red)
    }
}

有了以上这些,我们可以看到我们的视图确实是正确的--只是我们的图标似乎完全不受.frame()修饰符,这实际上是真的。当将修饰符应用于视图时,我们通常不会改性视图,而是将其封装在一个新的透明视图中。所以当打电话的时候.background()上面,我们实际上是将这个背景修饰符应用于一个新的视图,包扎我们的形象,而不是形象本身。

因此,从布局的角度来看,我们的映像完全相同--它仍然以父视图为中心--只是这次它的父视图是一个新的50x50透明包装视图,而不是主宿主视图,但呈现的结果仍然相同。

由于SwiftUI视图负责确定自己的大小,因此我们需要告诉图像自身调整大小以占用所有可用空间,而不是坚持其默认大小。要实现这一点,我们只需应用.resizable()它的修饰符-如下所示:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .background(Color.red)
    }
}

我们现在有一个50x50日历图标呈现在屏幕的中心-完美!

应用填充

接下来,让我们看看填充物在SwiftUI中工作。和其他布局系统一样,比如CSS,填充使我们能够在自己的框架内抵消视图的内容。然而,取决于在我们的视图链修饰符中我们应用填充的位置,我们可以得到完全不同的结果。例如,让我们首先通过附加.padding()我们链尾处的修饰符:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .background(Color.red)
            .padding()
    }
}

同样,上述结果可能不是我们所期望的,因为我们实际上已经给出了日历图标外垫-附加的空白,不包括它的背景色。如果我们仔细考虑一下,这就是我们以前在应用.frame()修饰符调用.padding()实际上,它并没有改变我们以前的视图和修饰符,它只是在前面表达式的结果周围添加了空格。

事实上,如果我们加一秒钟.background()调用之后的修饰符.padding(),这种行为变得更加清晰--因为第二个背景色将在填充本身中呈现:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .background(Color.red)
            .padding()
            .background(Color.blue)
    }
}

所以如果我们想加入内垫考虑到视图的背景,我们需要应用这个填充以前添加背景-如下所示:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .padding()
            .background(Color.red)
    }
}

进一步说明,每个修饰符本质上封装了在另一个视图中调用的视图--如果我们要调用.padding() 以前应用我们.frame()修饰符,我们的图标会缩小,因为这个填充将应用在我们固定的50x50容器中--迫使我们的可调整大小的映像采用更小的大小:

struct ContentView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .padding()
            .frame(width: 50, height: 50)
            .background(Color.red)
    }
}

要完成我们的日历图标视图,我们也要对其应用一点角半径,并将其前景颜色变为白色--最后将所有这些代码提取到一个名为CalendarView,就像这样:

struct CalendarView: View {
    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .padding()
            .background(Color.red)
            .cornerRadius(10)
            .foregroundColor(.white)
    }
}

通常,每当我们定义了一个可以作为它自己的独立构建块的ui块时,将代码提取到一个新的代码中通常是个好主意。View执行-以便避免建立大量的视图.

堆栈和间隔

就像我们在SWIFT剪辑第三集,SwiftUI的各种堆栈和间隔最初可能看起来非常简单和有限,但实际上可以用来表示几乎无限的布局组合。要开始探索它们是如何工作的,让我们替换body我们的ContentView用我们的新CalendarView包装在垂直堆栈中的:

struct ContentView: View {
    var body: some View {
        VStack {
            CalendarView()
        }
    }
}

有趣的是上面提到的VStack实际上根本不会影响我们的布局,因为SwiftUI堆栈并不会扩展自己来占用他们的父级--相反,它们只是根据其子程序的总大小来调整自己的大小,在本例中,这只是我们的CalendarView从以前开始。

把我们的CalendarView,我们还必须添加一个Spacer到我们的堆里去。当放置在HStack或VStack,间隔器总是占用尽可能多的空间,在这种情况下,这将导致CalendarView被推到屏幕顶部:

struct ContentView: View {
    var body: some View {
        VStack {
            CalendarView()
            Spacer()
        }
    }
}

堆栈的酷之处在于,它们可以嵌套,以表达日益复杂的布局,而无需任何形式的手工框架计算。例如,下面是我们如何推动我们的CalendarView到屏幕的最上面的角,嵌套我们上面的VStack在HStack也包含Spacer(我们还将在视图层次结构中应用一些外部填充,以嵌入内容):

struct ContentView: View {
    var body: some View {
        HStack {
            VStack {
                CalendarView()
                Spacer()
            }
            Spacer()
        }.padding()
    }
}

接下来,让我们添加一个Text对于我们的视图,为了开始将其转换为一个屏幕,可以用来查看有关日历事件的一组详细信息。由于我们将只在本文中研究SwiftUI的布局系统,因此我们将对Text就目前而言:

struct ContentView: View {
    var body: some View {
        HStack {
            VStack {
                CalendarView()
                Spacer()
            }
            Text("Event title").font(.title)
            Spacer()
        }.padding()
    }
}

看看上面的代码,我们可能会期待我们的新代码Text就在我们的CalendarView-虽然在水平轴上是这样的,但在垂直轴上,它会根据屏幕的全部高度而居中。原因是我们Spacer只影响VStack我们的CalendarView,因此,为了获得相同的布局行为,Text同样,我们也必须将它封装在VStack包含一个间隔-或者我们可以简单地说出我们的根HStack把所有的孩子都对准顶端,就像这样:

struct ContentView: View {
    var body: some View {
        HStack(alignment: .top) {
            VStack {
                CalendarView()
                Spacer()
            }
            Text("Event title").font(.title)
            Spacer()
        }.padding()
    }
}

类似地,我们也可以调整VStack将其子节点水平地放置,例如,为了呈现一个Text显示我们想象中的日历事件在活动标题下的位置--同时根据我们的根视图的前沿保持这两个标签对齐:

struct ContentView: View {
    var body: some View {
        HStack(alignment: .top) {
            VStack {
                CalendarView()
                Spacer()
            }
            VStack(alignment: .leading) {
                Text("Event title").font(.title)
                Text("Location")
            }
            Spacer()
        }.padding()
    }
}

然而,虽然上面的布局工作,它可以被简化,以便更容易在心理上可视化。不太直观的是,我们视图的所有内容都被一个Spacer这是嵌套在两个堆栈中的,为了保持对视图的垂直迭代,我们在理想情况下也希望我们的根堆栈是一个VStack.

所以让我们再一次把我们的身体ContentView在重构时将其转换为专用组件。这一次,让我们调用我们的新视图EventHeader,并使其成为垂直中心。HStack这在它的子级之间增加了一些间隔--这将使我们实现更早布局的一个改进版本,同时也简化了我们的代码:

struct EventHeader: View {
    var body: some View {
        HStack(spacing: 15) {
            CalendarView()
            VStack(alignment: .leading) {
                Text("Event title").font(.title)
                Text("Location")
            }
            Spacer()
        }
    }
}

回到我们的ContentView,我们现在可以把它的身体变成一个VStack包含我们的新EventHeader组件,以及以前的垂直间隔--现在放置在一个更好的位置,使我们的布局代码更容易理解:

struct ContentView: View {
    var body: some View {
        VStack {
            EventHeader()
            Spacer()
        }.padding()
    }
}

再一次,我们遵循同样的原则,持续提取我们的身体ContentView尽可能地转换成专用组件。以这种方式工作通常可以使我们自然地将我们的UI分离成原子部分,而不需要我们预先做大量的架构设计工作。

#Zack和偏移量 最后,让我们更快地看看SwiftUI的ZStack类型,它使我们能够在深度上堆叠一系列视图,使用从后到前的顺序。

举个例子,假设我们想为显示一个小的“经核实的徽章”在我们的日历视图的顶部,从以前-通过在其顶部尾随角放置一个检查点图标。要以更通用的方式实现这一点,让我们扩展View使用允许我们将任何视图包装在ZStack(这本身不会影响视图的布局),也可以选择包含我们的复选标记图标-如下所示:

extension View {
    func addVerifiedBadge(_ isVerified: Bool) -> some View {
        ZStack(alignment: .topTrailing) {
            self

            if isVerified {
                Image(systemName: "checkmark.circle.fill")
                    .offset(x: 3, y: -3)
            }
        }
    }
}

注意一个ZStack给了我们对它的完全二维控制alignment,我们可以使用它在父视图的上尾随角中定位我们的图标。然后,我们还应用.offset()修改我们的徽章,它会将它稍微移出它的父视图的边界之外。

在上述情况下,我们现在可以有条件地将我们的新徽章添加到我们的CalendarView以防万一eventIsVerified属性设置为true(为了简单起见,我们目前默认为:

struct CalendarView: View {
    var eventIsVerified = true

    var body: some View {
        Image(systemName: "calendar")
            .resizable()
            .frame(width: 50, height: 50)
            .padding()
            .background(Color.red)
            .cornerRadius(10)
            .foregroundColor(.white)
            .addVerifiedBadge(eventIsVerified)
    }
}

使用ZStack连同.offset()修饰符可以很好地将各种类型的覆盖添加到视图中,而不会影响视图本身的布局。我们可以使用这种技术来实现加载旋转器、应用程序中的通知以及我们希望在现有的视图层次结构之上呈现的许多其他类型的视图。

结语

这是本指南中关于SwiftUI布局系统的第一部分的总结。在……里面第二部分,我们将继续查看构建完全自定义布局的稍微更强大的方法,但现在--让我们总结一下我们到目前为止已经介绍的内容:

  • SwiftUI的核心布局引擎的工作方式是要求每个子视图根据其父视图的边界确定自己的大小,然后要求每个父视图在自己的范围内定位其子视图。
  • 视图修饰符通常将当前视图包装在另一个视图中,这就是为什么我们可以根据调用修饰符的顺序得到完全不同的布局结果。
  • 使用.frame()和.padding()修饰符允许我们调整视图的大小和内部裕度,只要该视图被配置为相应地调整自身大小。
  • 使用HStack, VStack和ZStack我们可以将视图水平地、垂直地或深度地叠加在一起.
  • 使用offset()我们可以在不影响视图周围环境的情况下移动视图,这在实现重叠视图和其他类型的重叠视图时非常有用。

推荐👇:

如果你想一起进阶,不妨添加一下交流群1012951431

面试题资料或者相关学习资料都在群文件中 进群即可下载!