布局法则
面对任何布局,记住以下3个法则:
- 父view为子view提供一个建议的size
- 子view根据自身的特性,返回一个size
- 父view根据子view返回的size为其进行布局
举个简单的例子:
struct ContentView: View {
var body: some View {
Text("Hello, world")
.border(Color.green)
}
}
ContentView作为Text的父view,它为Text提供一个建议的size,在本例中,这个size为全屏幕的尺寸,然后Text根据自身的特性,返回了它实际需要的size,注意:Text的特性是尽可能的只使用必要的空间,也就是说能够刚好展示完整文本的空间,然后ContentView根据Text返回的size在其内部对Text进行布局,在SwiftUI中,容器默认的布局方式为居中对齐。
frame
在UIKit中,Frame算是一种绝对布局,它的位置是相对于父view左上角的绝对坐标。但SwiftUI中,frame的概念完全不同。
我们先看个例子:
struct ContentView: View {
var body: some View {
Text("Hello, world")
.background(Color.green)
.frame(width: 200, height: 50)
}
}
根据之前UIKit的惯性思维,我们认为.frame是设置Text的宽高,我们想象的页面呈现是这样的:
但实际效果是这样的:
理由:.background
并不会直接去修改原来的Text,而是在Text图层的下方新建了一个新的view,在SwiftUI中中,View是廉价的。background
就相当于Text
的父view,.frame
就是这个background
建议的size,background还需要去问它的child,也就是Text, Text返回了一个自身需要的size(根据文字只使用必要的最小空间),于是background也返回了Text的实际尺寸,这就造成了绿色背景跟文本同样大小的效果。
要想得到理想的效果,需要修改一下上边的代码:
struct ContentView: View {
var body: some View {
Text("Hello, world")
.frame(width: 200, height: 50)
.background(Color.green)
}
}
只是调整了frame
和backgroud
的位置,理由:background
相当于frame
的父view,frame
返回的实际大小是20050,则绿色背景的frame就是20050,而Text
还是实际大小,在frame
中居中显示。
实际布局中,我们需要考虑不同控件的影响:
Text
,会返回自身需要的size
,像Shape
,则会返回父view
建议的size
。
frame
还有一个属性alignment
:
frame中的alignment会对其内部的views做整体的对齐处理,在平时的开发中,如果你发现,在frame中设置了alignment,但并没有起作用,主要原因是外边的容器,比如说HStack,VStack等他们自身的尺寸刚好等于其子views的尺寸,这种情况下的alignment效果都是一样的。
struct ContentView: View {
var body: some View {
HStack {
Text("Good job.")
.background(Color.orange)
}
.frame(width: 300, height: 200, alignment: .topLeading)
.border(Color.green)
}
}
frame
有两种定义方式:
第一种:
func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center) -> some View
width和height都可以为nil,如果为nil,就直接使用父view的size
第二种:
public func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center) -> some View
这个函数的参数比较多,但总体分为3类:
- minWidth,idealWidth,maxWidth
- minHeight,idealHeight,maxHeight
- alignment
关于min和max的内容,大家看下边的这张关系图就可以了:
简单讲一讲idealWidth和idealHeight,按照字面意思,ideal是理想的意思,那么当我们为某个view设置了idealWidth后会怎样呢?
struct ContentView: View {
var body: some View {
Text("Good job.")
.frame(idealWidth: 200, idealHeight: 100)
.border(Color.green)
}
}
运行后,我们发现,Text并没有使用我们给出的ideal尺寸:
实际上,这个ideal必须跟.fixedSize(horizontal: true, vertical: true)
一起使用才行:
- horizontal:表示固定水平方向,也就是idealWidth
- vertical: 表示固定垂直方向,也就是idealHeight
我们再次验证一下:
struct ContentView: View {
var body: some View {
HStack {
Text("horizontal")
.frame(idealWidth: 200, idealHeight: 100)
.fixedSize(horizontal: true, vertical: false)
.border(Color.green)
Text("vertical")
.frame(idealWidth: 200, idealHeight: 100)
.fixedSize(horizontal: false, vertical: true)
.border(Color.green)
Text("horizontal & vertical")
.frame(idealWidth: 200, idealHeight: 100)
.fixedSize(horizontal: true, vertical: true)
.border(Color.green)
}
}
}
这项技术,在真实的开发中很有用,我们可以直接固定死某个view的尺寸,不会因为外部条件的改变而改变。
更多关于frame
详解,请看 juejin.cn/post/684490…
GeometryReader
我们已经知道了,修改frame,就相当于修改了父view建议的size,然后,子view会非常聪明的根据这个size做一些事情,但是,这个size到目前为止还是隐性的,所谓的隐性表示我们不能显式的获取到这个size。
如果子view想显式的得到这个建议的size,则需要用到GeometryReader
。这就是GeometryReader的核心功能之一:获取到父view的size。
GeometryReader
包裹在Text
外边,则能获取到Text
的父viewContentView
的大小,便是整个屏幕的大小。
struct ContentView: View {
var body: some View {
GeometryReader { geo in
Text("Hello, World!")
.frame(width: geo.size.width * 0.9)
.background(Color.red)
}
}
}
GeometryReader
的另一个作用就是让childview
知道自己在parent view
中的坐标,这个parent view
并不一定是父view,也可能是任一直系祖先view。
看例子:
struct ContentView: View {
var body: some View {
VStack {
GeometryReader{ proxy in
Text("I'm in (x: \(proxy.frame(in: .local).minX), y: \(proxy.frame(in: .local).minY))")
.font(.title2)
}
}
.frame(height: 200)
.background(Color.yellow)
}
}
proxy
提供了 frame(in:)
方法,通过这个方法的返回值,Rectangle
可以知道自己在 VStack
中的坐标。(同样,这里 Text
实际 parent view 是 GeometryReader
,不过 GeometryReader
和 VStack
大小位置一样,这里就直接这么说了)。
VStack
区域是黄色部分,以左上角为原点建立坐标系,看到 Text
正好在黄色区域的左上角,起始坐标是 (0, 0)
,这和 frame(in:)
的返回值验证上了。
注意到 frame(in:)
有一个参数,并且我们上面传递的是 .local
,实际上 frame(in:)
可以传递 3 个不同的参数:
frame(in: .local)
,这个参数正如上面所演示的,child view 可以获取到「以 parent view 区域为坐标系的位置」。frame(in: .global)
,使用这个参数会返回 child view 在整个屏幕的位置。frame(in: .named())
,自定义坐标参考系,返回以自定义坐标系的位置。
其中 1 和 2 还能理解,但是 3 是什么意思?这么想一下:现在使用 .local
和 .global
可以获取在 parent view 和 整个屏幕中的位置,那如果希望 child view 获取在「grandfather view」的位置呢?这个时候就可以使用 frame(in: .named())
了。具体作用如下:
- 在 child view 的任意直系祖先中使用
.coordinateSpace(name: "custom")
自定义坐标系。 - child view 中使用
proxy.frame(in :.named("custom"))
就可以获取到在该坐标系中的位置。
具体代码如下
struct ContentView: View {
var body: some View {
HStack{
VStack {
VStack{
GeometryReader{proxy in
Text("I'm in (x: \(Int(proxy.frame(in: .named("custom")).minX)), y: \(Int(proxy.frame(in: .named("custom")).minY)))")
}
}.frame(width: 200, height: 200)
.background(Color.yellow)
}
.frame(width: 300, height: 300)
.background(Color.blue)
.coordinateSpace(name: "custom") //自定义一个坐标系
}
}
}
运行可以看到,Text
坐标是 (50,50)
,这个坐标正是相对蓝色区域的坐标系,蓝色区域为 HStack
,是 Text
的 grandfather view。
但是要注意一点:frame(in: .named())
一定是 child view 的直系祖先,类似叔叔之类的是不行的,获取到的坐标是 .global
类型的。代码如下:
struct ContentView: View {
var body: some View {
HStack{
VStack{
}.frame(width: 150, height: 150)
.background(Color.blue)
.coordinateSpace(name: "custom")
VStack{
GeometryReader{proxy in
Text("I'm in (x: \(Int(proxy.frame(in: .global).minX)), y: \(Int(proxy.frame(in: .named("custom")).minY)))")
}
}.frame(width: 200, height: 200)
.background(Color.yellow)
}
}
}
总结一下:
- 使用
GeometryReader
暴露出来的proxy.width && proxy.height
可以获取到 parent view 的大小。 - 使用
GeometryReader
暴露出来的proxy.frame(in:)
方法可以获取 child view 的位置。
安全区域 SafeArea
swiftui默认都是在安全区域中布局,如果想突破安全区域,则可以使用GeometryReader
获取safeAreaInsets
,以及使用edgesIgnoringSafeArea()
具体例子:
mlog.club/article/535…
Alignment Guide
详见这篇: juejin.cn/post/684490…
Padding
详见这篇: www.jianshu.com/p/fbaa83e13…
状态管理
状态管理详见:www.jianshu.com/p/345b80f0e… SwiftUI视图生命周期:www.fatbobman.com/posts/swift…
SwiftUI异步编程框架Combine
Combine 中的三大支柱
Publisher
,负责发布事件;Operator
,负责转换事件和数据;Subscribe
,负责订阅事件。
一个Combine的例子:github.com/ra1028/Swif…
Swift也可以使用Redux的方式进行数据流的管理;一个好用的SwiftUI redux框架:
Swift @escaping 逃逸闭包
函数接收闭包作为其参数,如果在函数中进行了异步操作(例如耗时操作,网络请求)后执行该闭包,则该闭包需要定义为逃逸闭包。
在swift2中,有@noescape属性,对应非逃逸闭包,也就是在return之前就被调用,因为闭包在方法调用完成之后就会销毁,不会存在强引用,因此非逃逸闭包中不需要做weak self操作。
在swift3中去掉了@noescape,引入了@escaping,并给将所有的闭包默认作为非逃逸闭包。
public func myFunction(myHandler: @escaping () -> Void) {
print("this is my Function")
DispatchQueue.main.async { //异步操作
myHandler()
}
}
Swift in 关键字
说白了,这个in
就是为了分开函数的参数
和函数体
的一个固定语法,这个语法只在回调函数中使用。
Alamofire.request("https://api.openweathermap.org/xxx").responseJSON {
response in
//xxx
}