SwiftUI笔记

163 阅读8分钟

布局法则

面对任何布局,记住以下3个法则:

  1. 父view为子view提供一个建议的size
  2. 子view根据自身的特性,返回一个size
  3. 父view根据子view返回的size为其进行布局

举个简单的例子:

struct ContentView: View {
    var body: some View {
        Text("Hello, world")
            .border(Color.green)
    }
}

image.png 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的宽高,我们想象的页面呈现是这样的:

image.png

但实际效果是这样的:

image.png

理由:.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)
    }
}

只是调整了framebackgroud的位置,理由: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的内容,大家看下边的这张关系图就可以了:

image.png

简单讲一讲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尺寸:

image.png

实际上,这个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)
        }
    }
}

image.png

这项技术,在真实的开发中很有用,我们可以直接固定死某个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)
    }
}

image.png

proxy 提供了 frame(in:) 方法,通过这个方法的返回值,Rectangle 可以知道自己在 VStack 中的坐标。(同样,这里 Text 实际 parent view 是 GeometryReader,不过 GeometryReaderVStack 大小位置一样,这里就直接这么说了)。

VStack 区域是黄色部分,以左上角为原点建立坐标系,看到 Text 正好在黄色区域的左上角,起始坐标是 (0, 0),这和 frame(in:) 的返回值验证上了。

注意到 frame(in:) 有一个参数,并且我们上面传递的是 .local,实际上 frame(in:) 可以传递 3 个不同的参数:

  1. frame(in: .local),这个参数正如上面所演示的,child view 可以获取到「以 parent view 区域为坐标系的位置」。
  2. frame(in: .global),使用这个参数会返回 child view 在整个屏幕的位置。
  3. frame(in: .named()),自定义坐标参考系,返回以自定义坐标系的位置。

其中 1 和 2 还能理解,但是 3 是什么意思?这么想一下:现在使用 .local.global 可以获取在 parent view 和 整个屏幕中的位置,那如果希望 child view 获取在「grandfather view」的位置呢?这个时候就可以使用 frame(in: .named()) 了。具体作用如下:

  1. 在 child view 的任意直系祖先中使用 .coordinateSpace(name: "custom") 自定义坐标系。
  2. 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。

image.png 但是要注意一点: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)
            
        }
    }
}

总结一下:

  1. 使用 GeometryReader 暴露出来的 proxy.width && proxy.height 可以获取到 parent view 的大小。
  2. 使用 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

详见:juejin.cn/post/709189…

Combine 中的三大支柱

  • Publisher,负责发布事件;
  • Operator,负责转换事件和数据;
  • Subscribe,负责订阅事件。

一个Combine的例子:github.com/ra1028/Swif…

Swift也可以使用Redux的方式进行数据流的管理;一个好用的SwiftUI redux框架:

image.png


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
}

zhuanlan.zhihu.com/p/83634904


Swift Image.resizable()

juejin.cn/post/697476…


Swift中 $0$1 的简写含义

blog.csdn.net/daiqiao_ios…