0%

  • 两种设计模式

    • 每次在视图中读取数据时,都会在那个视图中创建一个依赖

      每次数据发生变化时,视图也会发生变化

    • 在视图层级中个,读取的每一条数据,都只有一个数据源

      例如,在同级视图中,用同一个数据,你的实现是这样的:

      03:40

      这种实现会在不同的View中维护同一个数据来源,这一个过程很容易造成数据同步错误导致的BUG。不妨改成这样来实现:

      4:10

  • @State

    1
    @State var isPlaying: Bool = false

    @State告诉系统isPlaying是一个可以变化的值,视图也会随之改变

    9:08

    当你声明一个@State变量时,SwiftUI框架会为之分配永久存储,SwiftUI可以在变量发生变化时,重新渲染其关联的视图和其所有的SubvIew(只会重新渲染发生改变的地方)

    11:02

    在SwiftUI中,数据流向是单一向的

  • @Binding

    保持不同视图的数据同步

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    struct PlayerView: View {
    let episode: Episode
    @State private var isPlaying: Bool = false

    var body: some View {
    VStack {
    Text(episode.title).foregroundColor(isPlaying ?.white : .gray)
    Text(episode.showTitle).font(.caption).foregroundColor(.gray)
    PlayButton(isPlaying: $isPlaying)
    }
    }
    }

    struct PlayButton: View {
    // 这里的isPlaying和👆的是同一个,所以无需同步数据
    @Binding var isPlaying: Bool
    var body: some View {
    Button(action: {
    self.isPlaying.toggle()
    }) {
    Image(systemName: isPlaying ? "pause.circle" : "play.circle")
    }
    }
    }
  • Publisher

    19:45

    在SwiftUI中,使用Publisher来表述这些例如Timer和通知的外部事件。

    1
    2
    3
    4
    // 接受来自currentTimePublisher的事件
    .onReceive(PodcastPlayer.currentTimePublisher, perform: { newCurrentTime in
    self.currentTime = newCurrentTime
    })
    • BindableObject

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class PodcastPlayerStore: BindableObject {

      var didChange = PassthroughSubject<Void, Never>()

      // ...

      func advance() {
      currentEpisode = nextEpisode
      currentTime = 0.0

      didChange.send()
      }
      }

      PassthroughSubject是一个Publisher,SwiftUI会订阅这个Publisher,来更新视图层级

      • @ObjectBinding

        当使用@ObjectBinding声明模型时,SwiftUI会识别出这个属性,并给它设置依赖,一旦模型发生变化,框架就会自动明白什么时候刷新视图。

        1
        2
        3
        4
        5
        6
        struct MyView: View {
        @ObjectBinding var model: MyModelObject
        // ...
        }

        MyView(model: modelInstance)

        26:08

        所有声明@ObjectBindng模型的视图,都会自动订阅模型,并在模型发生改变时,刷新视图。(即:依赖自动追踪)

    • 创建一个间接依赖

      上面声明的依赖关系,属于视图直接依赖,接下来看看如何生成一个视图的间接依赖

      27:01

SwiftUI中的一切都是视图。

文章来源:Building Custom Views with SwiftUI

SwiftUI布局基础

1
2
3
4
5
6
struct ContentView: View {
var body: some View {
Text("Hello, world!")
// .edgesIgnoringSafeArea(.all)
}
}

这段代码包含三个视图:

01:44

  • 视图等级底部的文本(图中Hello World)

  • 内容视图(和文本的布局一致,即图中Hello World四周白线内)

  • 根视图(屏幕Size - 安全区域)

    如果想将根视图扩展到安全区,可以使用edgesIgnoringSafeArea(.all)修饰器

当然,文本和文本的内容视图,我们通常当做同一个来操作

2:18

在SwiftUI中,不能给子视图强制规定一个尺寸,而是应该有父视图决定

布局步骤

  • 父视图提供给子视图一个Size
  • 子视图决定自身的Size(子视图也许不能完全使用父视图的Size)
  • 父视图将子视图放在其坐标系中
  • SwiftUI会让视图坐标像最接近的像素值取整

例一:查看一段代码的布局:

1
2
3
4
5
var body: some View {
Text("Avocado Toast")
.padding(10)
.background(Color.green)
}

在设置backgroundpadding修饰器时,会在Text视图和根视图中间插入对应的背景视图和边距视图

1

例二:图片的原尺寸为20x20,我们希望1.5倍尺寸展示图片

1
2
3
4
5
struct ContentView: View {
var body: some View {
Image("20x20_avoado")
}
}

做法:

1
.frame(width: 30, height: 30)

效果:图片尺寸不会发生变化,但是在图片周围会插入一个30x30尺寸的Frame视图

9:39

在SwiftUI中frame并不是一个重要的布局元素,它其实只是一个View。

例三:

1
2
3
4
5
6
7
// 子视图必须平等竞争一个空间
HStack {
Text("Delicious")
Image("20x20_avocado")
Text("Avocado Toast")
}
.lineLimit(1)

  • 设置文字底基线对齐

    19:51

  • 设置图片的底基线

    20:40

例四:让不同容器中的视图对齐

21:14

  • 自定义对齐方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    extension VerticalAlignment {
    private enum MidStarAndTitle : AlignmentID {
    // 告诉SwiftUI如何计算默认值
    static func defaultValue(in d: ViewDimensions) -> CGFloat {
    return d[.bottom]
    }
    }
    static let midStarAndTitle = VerticalAlignment(MidStarAndTitle.self)
    }
  • 设置文字的基线

    22:55

SwiftUI绘图

SwiftUI默认提供了多种样式的图形,比如圆形,胶囊和椭圆

26:08

  • 实现渐变色

    27:00

    • 角渐变色

      27:06

    • 使用角渐变色填充圆

      27:26

    • 使用渐变色填充圆环

      27:39

  • 实现复杂图形绘制

    完整代码可参见:官方Demo

    总体步骤主要包括:

    1. 创建单个楔形的数据模型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Ring: ObservableObject {
    /// A single wedge within a chart ring.
    struct Wedge: Equatable {
    /// 弧度值(所有楔形的弧度值之合最大为2π,即360°)
    var width: Double
    /// 横轴深度比例 [0,1]. (用来计算楔形的长度)
    var depth: Double
    /// 颜色值
    var hue: Double
    }
    }
    1. 绘制单个子图形
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    struct WedgeShape: Shape {
    func path(in rect: CGRect) -> Path {
    // WedgeGeometry是用来计算绘制信息的类,详细代码见Demo。
    let points = WedgeGeometry(wedge, in: rect)

    var path = Path()
    path.addArc(center: points.center, radius: points.innerRadius,
    startAngle: .radians(wedge.start), endAngle: .radians(wedge.end),
    clockwise: false)
    path.addLine(to: points[.bottomTrailing])
    path.addArc(center: points.center, radius: points.outerRadius,
    startAngle: .radians(wedge.end), endAngle: .radians(wedge.start),
    clockwise: true)
    path.closeSubpath()
    return path
    }
    // ···
    }
    1. 用ZStack组装所有的楔形
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let wedges = ZStack {
    ForEach(ring.wedgeIDs, id: \.self) { wedgeID in
    WedgeView(wedge: self.ring.wedges[wedgeID]!)

    // use a custom transition for insertions and deletions.
    .transition(.scaleAndFade)

    // remove wedges when they're tapped.
    .onTapGesture {
    withAnimation(.spring()) {
    self.ring.removeWedge(id: wedgeID)
    }
    }
    }

    // 如果不加这个Spacer(),会使Mac程序,在没添加任何楔形时,APP尺寸为0。
    Spacer()
    }

    为了更好的理解这个工程,你还需要一些知识:

    如何使用Animatable自定义复杂的动画

    假如我们想实现一个简单的SwiftUI动画,比如点击Button按钮渐变消失,我们可以这样来实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @State private var hidden = false

    var body: some View {
    Button("Tap Me") {
    self.hidden = true
    }
    .opacity(hidden ? 0 : 1)
    .animation(.easeInOut(duration: 2))
    }

    在这个例子中,我们使用@State修饰变量hidden,当hidden的值发生变化时,SwiftUI会自动为我们处理渐变动画。而SwiftUI能够为我们自动执行动画的前提是:SwiftUI已经知道要如果展示该动画效果,那什么情况下,SwiftUI不知道要如何展示动画呢?

    比如:我们通过下面代码绘制出了多角形。

    1
    Shape(sides: 3)

    该方法支持传入不同的值,生成不同的多边形。如果我们希望从三边形变成四边型,那么可以写如下代码:

    1
    2
    3
    Shape(sides: isSquare ? 4 : 3)
    .stroke(Color.blue, lineWidth: 3)
    .animation(.easeInOut(duration: duration))

    但是运行代码后发现,这段代码并无动画过渡效果。这是因为在执行动画的过程中,SwiftUI会从起始状态到终止状态分成不同的阶段来绘制,像opacity从0-1,可能会分成0,0.1,0.2,0.3,···,0.9,1.0,SwiftUI依次进行绘制,从而展示过渡状态。

    同理,从三角形变到四边形,SwiftUI也需要绘制中间状态,但SwiftUI并不知道该如何绘制3.5边形。这时就需要我们自己来告诉SwiftUI该如何绘制了。

    • animatableDataAnimatable协议中,唯一需要实现的方法,通过这个方法来告诉SwiftUI需要监听哪些属性的变化。

      1
      2
      3
      4
      5
      // 代表需要监听的值为Float类型
      var animatableData: Float {
      get { //··· }
      set { //··· }
      }
    • 然而并不是所有类型的属性都能够被SwiftUI所监听,只有遵循VectorArithmetic协议的对象AnimatablePair, CGFloat, Double, EmptyAnimatableData and Float才能被SwiftUI监听。

    • 要实现Demo中楔形图片变换的效果,需要监听的值有startenddepthhue这四个值。animatableData属性的返回值也应该包含这四个值。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      extension Ring.Wedge: Animatable {
      typealias AnimatableData = AnimatablePair<AnimatablePair<Double, Double>, AnimatablePair<Double, Double>>

      var animatableData: AnimatableData {
      get {
      .init(.init(start, end), .init(depth, hue))
      }
      set {
      start = newValue.first.first
      end = newValue.first.second
      depth = newValue.second.first
      hue = newValue.second.second
      }
      }
      }
    • 接下来在这四个属性发生变化时,SwiftUI会通过函数func path(in rect: CGRect) -> Path {}重新绘制,这样就可以展示动画的过渡效果了

    一些琐碎的知识点

    • 使用drawingGroup()提高复杂UI的渲染效率

      以往每创建一个楔形,都是一个单独的View,当楔形数量非常多时,再加上每个View都在执行动画,非常耗费性能。在SwiftUI中,可以通过drawingGroup()将相同类型的View通过Metal绘制在一张画布上,从而减少渲染耗费的性能,避免卡顿。

    • 使用Equatable防止视图的新值和旧值相同时,更新子视图

      1
      2
      3
      struct Wedge: Equatable { 
      // ···
      }
    • 使用PassthroughSubject通知SwiftUI值发生变化

      • PassthroughSubject

        • 使用PassthroughSubject通知绑定的属性的视图,属性发生变化,需要重新绘制。
        1
        2
        3
        4
        5
        6
        7
        let objectWillChange = PassthroughSubject<Void, Never>()

        private(set) var wedgeIDs = [Int]() {
        willSet {
        objectWillChange.send()
        }
        }
        • contentView中监听了Ring模型
        1
        @EnvironmentObject var ring: Ring

        因此在Ring模型的wedgeIDs发生变化时,会发出通知告知contentView使用其绘制的地方,需要重新绘制

      • CurrentValueSubject

        我们常用的@Published属性包装器,实际上就是一种CurrentValueSubject

      简单来说:PassthroughSubject用于表示事件。CurrentValueSubject用于表示状态。用现实世界的案例进行类比。

      PassthroughSubject = 门铃按钮,当有人按门时,只有在你在家时才会收到通知。CurrentValueSubject = 电灯开关,当你在外面时,有人打开了您家中的灯。你回到家,你知道有人打开了它们。

参考:

https://stackoverflow.com/questions/60482737/what-is-passthroughsubject-currentvaluesubject

https://swiftui-lab.com/swiftui-animations-part1/

4.1 一个源程序从写出到执行的过程

  • 一个汇编语言程序从写出到最终执行的简要过程:
    • 编写
    • 编译连接
    • 执行

4.2 源程序

  • 汇编指令

    有对应机器码的指令,可以被编译为机器指令,最终为CPU所执行

  • 伪指令

    没有对应的机器码的指令,最终不被CPU所执行。

    伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。

  1. 定义一个段

    • segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令

      • segment和ends的功能是定义一个段,segment说明一个段开始,ends说明一个段结束
    • 一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当做栈空间来使用。

      • 一个有意义的程序至少有一个段
  2. 寄存器与段的关联假设

    assume:含义为“假设”

    • 它假设某一段寄存器和程序中的某一个用segment…ends定义的段相关联。
    • 通过assume说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。
  • 汇编源程序

    • 伪指令(编译器处理)
    • 汇编指令(编译为机器码)
  • 程序:源程序中最终由计算机执行,处理的指令或数据。

    截屏2021-07-05 下午1.04.45

    截屏2021-07-05 下午1.05.07

学习SwifUI的最好方式是,用SwiftUI编写一个程序

文章来源:Introducing SwiftUI: Building Your First App

注:视频中使用的部分API,已经在后续版本中废弃,本文代码根据最新版API进行了调整,确保 Demo工程 能够运行

一:编写第一个SwiftUI程序

  1. 创建SwiftUI工程

截屏2021-07-04 上午11.04.58

  • 左侧是代码区

  • 右侧是Canvas

    编写代码时,右侧的Canvas能够实时显示出代码的UI预览效果

截屏2021-07-04 上午11.05.49

  1. 编写UI布局代码

    • 通过拖拽增加UI控件

      • VStack:SwiftUI常用的一种布局元素,可以用来垂直地叠加视图
      • HStack:水平叠加视图
    • cmd+点击VStack,插入HStack
      截屏2021-07-04 上午11.30.37

      1
      2
      3
      4
      5
      6
      HStack {
      VStack {
      Text("Rooms")
      Text("20 people")
      }
      }
    • 在文字左侧增加一个图片

      1
      2
      3
      4
      5
      6
      7
      8
      9
      HStack {
      // `photo`是系统自带资源库中的图片
      Image(systemName: "photo")

      VStack {
      Text("Rooms")
      Text("20 people")
      }
      }
    • 在Canvas中将VStack修改为左对齐

    • 设置Text的字号

      截屏2021-07-04 上午11.49.02

      1
      2
      3
      4
      5
      6
      7
      8
      9
      HStack {
      Image(systemName: "photo")

      VStack(alignment: .leading) {
      Text("Rooms")
      Text("20 people")
      .font(.subheadline)
      }
      }

      我们称.font(.subheadline)为修饰器(modifier),用来自定义视图的外观或行为

    • 设置Text的颜色为secondary

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      HStack {
      Image(systemName: "photo")

      VStack(alignment: .leading) {
      Text("Rooms")
      Text("20 people")
      .font(.subheadline)
      .foregroundColor(.secondary)
      }
      }
    • 将HStack替换为List

      截屏2021-07-04 上午11.55.23

      截屏2021-07-04 上午11.56.07

  2. 设置数据源

    • 增加Room模型

      在SwiftUI中,需要让模型遵循Identifiable协议,实现id属性

    截屏2021-07-04 下午12.34.18

    • 在ContentView中使用Room数据来展示UI

      截屏2021-07-04 下午12.40.15

      当代码发生重大改变(比如增加属性),Xcode会暂停预览,直到我们做好重新更新的准备(点击Resum按钮)

  3. 丰富UI内容

    • 设置图片圆角

      • 通过拖拽Modifier库来实现

    • 设置Navigation、NavigationTitle以及给每个单元格设置跳转

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      NavigationView {
      List(rooms) { room in
      NavigationLink(destination: Text(room.name)) {
      Image(systemName: "photo")
      .cornerRadius(8.0)


      VStack(alignment: .leading) {
      Text(room.name)
      Text("\(room.capacity) people")
      .font(.subheadline)
      .foregroundColor(.secondary)
      }
      }
      }
      .navigationTitle(Text("Rooms"))
      }
  4. 进入实时模式,查看效果

  5. 将子视图提成一个单独的视图

  6. 创建新的页面:RoomDetail

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct RoomDetail: View {
    let room: Room
    var body: some View {
    Image(room.imageName)
    .resizable()
    .aspectRatio(contentMode: .fit)
    }
    }

    struct RoomDetail_Previews: PreviewProvider {
    static var previews: some View {
    RoomDetail(room: testData[0])
    }
    }
    • 设置navigationBarTitle

      1
      2
      3
      4
      Image(room.imageName)
      .resizable()
      .aspectRatio(contentMode: .fit)
      .navigationBarTitle(Text(room.name))

      此时页面不会更新,因为RoomDetail中缺乏NavigationView作为上下文

    • 在预览中添加NavigationView上下文

      1
      2
      3
      4
      5
      6
      7
      struct RoomDetail_Previews: PreviewProvider {
      static var previews: some View {
      NavigationView {
      RoomDetail(room: testData[0])
      }
      }
      }
    • 调整navigationBarTitle的展示模式

      截屏2021-07-04 下午2.08.47

  7. 将RoomCell的跳转修改为前往RoomDetail页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct RoomCell: View {
    let room: Room

    var body: some View {
    NavigationLink(destination: RoomDetail(room: room)) {
    // ···
    }
    }
    }

二:Swift UI的工作方式

2.1 View

  • A View Defines a Piece of UI

    在SwiftUI中,视图是一种遵守View协议的结构,而不是继承自基础类的类

  • A View Defines its Dependencies

2.2 状态属性

  • @State

  • 当SwiftUI看到一个带@State状态变量的视图时,它会以视图的名义为那个变量分配存储空间。

    19:18

    • 绿色部分是APP的内存
    • 紫色是SwiftUI所管理的内存

    SwiftUI可以观察到@State变量合时被读写,同时SwiftUI知道zoom是从body中读取的,SwiftUI会在@State变量发生更改时,使用新的状态值,刷新渲染。

例:实现在RoomDetail中,点击图片修改填充模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct RoomDetail: View {
let room: Room
@State private var zoomed = false

var body: some View {
Image(room.imageName)
.resizable()
.aspectRatio(contentMode: zoomed ? .fit : .fill) // 根据zoomed值修改填充模式
.navigationBarTitle(Text(room.name), displayMode: .inline)
.onTapGesture {
self.zoomed.toggle() // 点击修改zoomed的值
}
}
}

2.3 事实来源

在SwiftUI中,UI可能因不同的数据,处于不同的状态,我们将这些用来绘制UI的数据称为“事实来源”,“事实来源“由状态变量模型共同组成。

  • 属性可以简单地分为:事实来源(Source of Truth)和衍生值(Derived Value)

21:49

zoomed变量是一个事实来源,contentMode衍生自它,当系统观察到zoomed变量发生变化时,SwiftUI框架会请求新的body,刷新渲染,重新生成一个新的宽高比视图,接下来覆盖contentMode

像RoomDetail中的,room属性,也是一个衍生值。

SwiftUI是数据驱动,而不是事件驱动

三:完善Rooms APP

  1. 增加动画

    1
    2
    3
    4
    5
    .onTapGesture {
    withAnimation {
    self.zoomed.toggle()
    }
    }
  2. 添加一个ZStack

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    ZStack(alignment: .topLeading) {
    Image(room.imageName)
    .resizable()
    .aspectRatio(contentMode: zoomed ? .fit : .fill)
    .navigationBarTitle(Text(room.name), displayMode: .inline)
    .onTapGesture {
    withAnimation {
    self.zoomed.toggle()
    }
    }

    Image(systemName: "video.fill")
    .font(.title)
    .padding(.all)
    }

  3. 固定图标的位置

    1
    2
    Image(room.imageName)
    .frame(minWidth:0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)

    更多关于SwiftUI布局相关的内容 - > Buiding Custom Views with SwiftUI

  4. 同时预览多个View

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct RoomDetail_Previews: PreviewProvider {
    static var previews: some View {

    Group {
    NavigationView {
    RoomDetail(room: testData[0])
    }
    NavigationView {
    RoomDetail(room: testData[1])
    }
    }

    }
    }
  5. 增加动效

    • 给视频图标增加过渡动效
    1
    2
    3
    4
    5
    6
    if room.hasVideo && !zoomed {
    Image(systemName: "video.fill")
    .font(.title)
    .padding(.all)
    .transition(.move(edge: .leading))
    }

    • 给图片的动效延长时间

      1
      2
      3
      4
      5
      .onTapGesture {
      withAnimation(.easeInOut(duration: 2)) {
      self.zoomed.toggle()
      }
      }
  6. 支持动态增加

    监测数据模型的改变,实时更新UI

    • 创建RoomStore储存Room模型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      import SwiftUI

      class RoomStore {
      var rooms: [Room]

      init(rooms: [Room] = []) {
      self.rooms = rooms
      }
      }
    • 遵守ObservableObject协议

      1
      2
      3
      4
      class RoomStore: ObservableObject {
      @Published var rooms: [Room]
      // ···
      }
    • 声明EnvironmentObject类型变量

      1
      @EnvironmentObject var store: RoomStore
    • 传入EnvironmentObject类型变量

      1
      2
      3
      4
      5
      6
      struct ContentView_Previews: PreviewProvider {
      static var previews: some View {
      ContentView()
      .environmentObject(RoomStore(rooms: testData))
      }
      }
    • 列表中增加一个按钮

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      List {
      Button(action:{

      }) {
      Text("Add Room")
      }
      // ForEach为它的每个集合项都创建一个视图
      ForEach(store.rooms) { room in
      RoomCell(room: room)
      }
      }

      截屏2021-07-04 下午11.22.36

      1
      2
      3
      4
      5
      6
      7
      Button(action:addRoom) {
      Text("Add Room")
      }

      func addRoom() {
      store.rooms.append(Room(name: "Hall 2", capacity: 2000))
      }

  7. 修改List的样式

    • 修改listStyle

      1
      2
      3
      4
      5
      6
      7
      NavigationView {
      List {
      // ···
      }
      .navigationBarTitle(Text("Rooms"))
      .listStyle(GroupedListStyle())
      }
    • 设置分组

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      List {
      Section {
      Button(action:addRoom) {
      Text("Add Room")
      }
      }

      Section {
      ForEach(store.rooms) { room in
      RoomCell(room: room)
      }
      }

      }
  8. 支持动态删除

    1
    2
    func delete(at offsets: IndexSet) {
    store.rooms.remove(atOffsets: offsets)
    1
    2
    3
    4
    ForEach(store.rooms)  { room in
    RoomCell(room: room)
    }
    .onDelete(perform: delete)

  9. 设置NavigationBarItem

    1
    2
    3
    4
    5
    6
    7
    NavigationView {
    List {


    }
    .navigationBarItems(trailing: EditButton())
    }
  10. 支持列表重新排序

    1
    2
    3
    func move(from source: IndexSet, to destination: Int) {
    store.rooms.move(fromOffsets: source, toOffset: destination)
    }
    1
    2
    3
    4
    5
    ForEach(store.rooms)  { room in
    RoomCell(room: room)
    }
    .onDelete(perform: delete)
    .onMove(perform: move)

  1. 设置预览环境

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Group {
    ContentView()
    .environmentObject(RoomStore(rooms: testData))
    // 大字号环境
    ContentView()
    .environmentObject(RoomStore(rooms: testData))
    .environment(\.sizeCategory, .extraExtraLarge)
    // 深色模式
    ContentView()
    .environmentObject(RoomStore(rooms: testData))
    .environment(\.colorScheme, .dark)
    // 布局方向
    ContentView()
    .environmentObject(RoomStore(rooms: testData))
    .environment(\.layoutDirection, .rightToLeft)
    .environment(\.locale, Locale(identifier: "ar"))
    }

总结

  • SwiftUI四个主要设计原则:

    • Declarative
    • Compositional
    • Automatic
    • Consistent
  • SwiftUI使用陈述性语法

  • 在SwiftUI中,Xcode预览可以让我们浏览、编辑和调试APP,我们甚至不需要运行项目工程

3.10 栈段

我们可以将长度为N(N <= 64k)的一组地址连续、起始地址为16的倍数的内存单元,当作栈来用,从而定义了一个栈段。

截屏2021-07-05 上午9.50.40

问题:3.11

如果我们将10000H~1FFFFH这段空间当做栈段,初始状态是空的,此时,SS=1000H,SP=?

换位思考:

栈为空,相当于栈中唯一的元素出栈,出栈后,SP=SP+2。

  • SP原来为FFFEH,加2后SP=0,所以,当栈为空的时候,SS=1000H,SP=0。

问题:3.12

一个栈段最大可以设为多少?

所以栈顶的变化范围是0~FFFFH,所以一个栈段的容量最大是64KB

(01H,容量为2, 0FFFF,容量为10000H,即64KB = 2^16)

截屏2021-07-05 下午12.33.47

  • 数据段:段地址放在DS中
  • 代码段:段地址放在CS中,第一条指令的偏移地址放在IP中。
  • 栈段:段地址放在SS中,栈顶单元的偏移地址放在SP中

3.8 栈顶越界的问题

push越界,上溢出;pop越界,下溢出

栈空间之前的空间里很可能存放了具有其他用途的数据、代码

  • 我们在编程时要自己操心栈顶越界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致越界
  • 执行出栈的操作时也要注意,以防止栈空的时候继续出栈而导致的越界

3.9 push、pop指令

push和pop指令是可以在寄存器和内存之间传送数据的。

栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊方式进行访问的内存空间

push和pop指令的格式

一:

  • push 寄存器:将一个寄存器中的数据入栈
  • pop 寄存器:用一个寄存器接受出栈的数据

二:

  • push 段寄存器:将一个段寄存器中的内容入栈
  • pop 段寄存器:用一个段寄存器接受出栈的数据

三:

  • push 内存单元:将一个内存单元处的字入栈(栈操作都是以字为单位)

  • pop 内存单元:用一个内存单元来接收出栈的数据

    push [0]pop [2]

指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。

问题:3.7

将10000H~1000FH这段空间当做栈,初始状态是空的,将AX、BX、DS中的数据入栈。

1
2
3
4
5
6
7
mov ax 1000H
mov ss, ax ; 设置栈的段地址,SS=1000H,不能直接向段寄存器SS送入数据
mov sp, 0010H ; 栈为空时,ss:sp在1000FH的下一位

push ax
push bx
push ds

问题:3.8

编程:

  • 将10000H~1000FH 这段空间当作栈,初始状态是空的;
  • 设置AX=001AH,BX=001BH;
  • 将AX、BX中的数据入栈;
  • 然后将AX、BX清零;
  • 从栈中恢复AX、BX原来的内容。

类似于函数调用过程:main函数调用a函数,初始时先将main函数的寄存器入栈,进行完操作后恢复寄存器到初始状态,回到main函数执行下面的内容

1
2
3
4
5
6
7
8
9
10
11
mov ax, 1000H   
mov ss, ax
mov sp, 1010H ;初始化栈顶
mov ax, 001AH
mov bx, 001BH
sub ax, ax ;将ax清零,也可以用赋值:mov ax, 0 异或: xor ax, ax
sub bx, bx ;sub ax,ax的机器码为2个字节
;mov ax,0 的机器码为3个字节

pop bx ;从栈中恢复ax,bx原来的数据,当前栈顶的内容是bx
pop ax ;中原来的内容

问题3.9

编程:

  • 将10000H~1000FH这段空间当作栈,初始状态是空的
  • 设置AX=002AH,BX=002BH
  • 利用栈,交换AX和BX中的数据
1
2
3
4
5
6
7
8
9
mov ax, 1000H
mov ss, ax
mov sp, 1010H
mov ax, 002AH
mov bx, 002BH
push ax
push bx
pop ax
pop bx

问题3.10

我们如果要在10000H处写入字型数据2266H,可以用以下的代码完成:

1
2
3
4
mov ax, 1000H
mov ds, ax
mov ax, 2266H
mov [0], ax

换一个写法:要求写入字型数据2266H

1
2
3
4
5
______
______
______
mov ax, 2266H
push ax

不能使用mov 内存单元, 寄存器这类指令

答:

1
2
3
mov ax, 1000H
mov ss, ax
mov sp, 2 ; push 会让 sp = sp - 2

截屏2021-06-26 下午3.26.03

执行push、pop指令需要两步:

  • 执行push时
    • 先改变sp,后向SS:SP处传送
      • SP=SP-2
      • 向SS:SP指向的字单元中送入数据
  • 执行pop时
    • 先读取SS:SP处的数据,后改变SP。
      • 向SS:SP指向的字单元中读取数据
      • SP=SP+2

push、pop等栈操作指令,修改的只是SP。也就是说,栈顶的变化范围最大为:0~FFFFH。

综述:

  • 任意时刻,SS:SP指向栈顶元素
  • 8086CPU只记录栈顶,栈空间的大小我们要自己管理

ARK

  • Pod Install 时间长
  • Build 较长
  • 成功率较低
  • IDE慢、内存占用高

500+ POD

思路一:子壳/Eample工程

  • 彻底模块化

  • 体验差异

    集成主壳后发现问题,仍需去子壳工程修复

  • 速度较慢

  • 容易劣化

思路二:

  • 精简化

  • 全链路稳定

    单节点的提升未必对全链路是正向的

动态化研发流程ARK

  • 基于动态库

    • 减少包体积

    • 进程间共享、降低内存

    • 动态加载应用内模块

      • 运行时(热修复)
      • 编译时

      编译时替换应用内模块

    ARK思路:

    平台研发流程:输出动态库文件

    本地研发流程:单独编译需要修改的Pod,编译成Framework并动态替换

    [2.57.45]

静态库转动态库

编译、链接、运行

不期而遇

某天我开开心心地写了一段代码:

1
2
3
NSInteger a = 10;
NSUInteger b = 9;
CGFloat c = -4 - (a - b - 1) * 13;

我预期c值为-4,但是编译器却输出了一个:

![截屏2021-06-17 上午11.56.32](/Users/Joshsone/Library/Application Support/typora-user-images/截屏2021-06-17 上午11.56.32.png)

WTF?这是啥,因为这莫名的缘分,我展开了一段追根溯源的路程。

寻觅之旅

为了厘清这段神秘的数字到底从何而来,我将Xcode调整为Always Show Disassembly,来看看编译器到底做了些啥?

![截屏2021-06-17 下午12.05.44](/Users/Joshsone/Library/Application Support/typora-user-images/截屏2021-06-17 下午12.05.44.png)

这段汇编代码对应到执行完-4 - (a - b - 1) * 13,但是还没有赋值给c,寄存器rax中存放的就是计算结果,我们打印来看下:

1
2
(lldb) expression $rax
(unsigned long) $0 = 18446744073709551612

可以看出rax中存放的是无符号长整形,其二进制形式是:

1
2
(lldb) p/t 18446744073709551612
(unsigned long long) $1 = 0b1111111111111111111111111111111111111111111111111111111111111100

可以看到,代码执行到这一步,已经出现了问题,我们期待结果是负数,那必然就是有符号整形,但是由于我们将b声明为NSUInteger,系统便将计算结果强转为了NSUInteger

另外提一句:有符号的-4,二进制格式也为0b1111111111111111111111111111111111111111111111111111111111111100

3.6栈

操作规则:LIFO(后进先出)

8086CPU提供的入栈和出栈的指令:

  • Push(入栈)

    push ax:将寄存器ax中的数据送入栈中

  • Pop(出栈)

    push ax:从栈顶取出数据送入ax。

8086CPU的入栈和出栈操作都是以字为单位进行的

截屏2021-06-16 上午8.13.13

  • CPU如何知道当前要执行的指令所在的位置?

    寄存器CS和IP中存放着当前指令的段地址和偏移地址

  • 执行push和pop的时候,如何知道哪个单元是栈顶单元?

    8086CPU中,有两个寄存器:

    • 段寄存器SS 存放栈顶的段地址
    • 寄存器SP 存放栈顶的偏移地址

    任何时刻,SS:SP指向栈顶元素

push ax时:

  • SP = SP - 2

  • 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶

    截屏2021-06-16 上午8.23.02

  • 将10000H~1000FH这段空间当做栈,初始状态栈是空的,此时,SS=1000H,SP=多少?

    截屏2021-06-16 上午8.25.46

    pop指令的执行过程

pop ax

  • 将SS:SP指向的内存单元处的数据放入ax中;
  • SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。

截屏2021-06-22 上午8.14.47

注意:

  • 出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素,1000CH处的2266H依然存在,但是,它已不再栈中。
  • 当再次执行push等入栈指令后,SS:SP移至1000CH,并在里面写入新的数据,它将被覆盖。

什么是Hang?

试想这样的场景:当用户在你的APP中进行交互时,APP却没有及时的响应用户的操作。

这种体验可以被描述为延时、慢、卡顿,在Apple开发中,我们称这种无响应的表现为Hang。

1:35

(温馨提示:本片内容来源自WWDC21:Understand and eliminate hangs from your app

想了解更多WWDC2021内容的小伙伴,可以阅读我以下文章,欢迎多多交流和指正

一文带你读完WWDC21核心(新)技术点

APP终极性能生存指南

检测和诊断内存问题

诊断APP的电源和性能回归

理解main runloop

用户的交互均在主线程的Runloop发生。当用户和APP交互,主线程会接受到这个事件,接下来处理这个事件,然后更新UI。

2:03

如果处理事件耗费的很长时间,就从用户交互到UI更新之间会发生延时(delay)。

2:31

Hang通常由什么引起?

3:52

  • 主线程任务忙(Busy)

    例如:过渡加载资源。当前页面中只需要展示前4个图片,但是却一次性加载了所有的图片。

    4:35

    例如:执行了与主线程不相关的任务。

    5:18

    再比如了使用次优的API。像绘制图片圆角有两种方法:

    • 方法一:

      6:23

    • 方法二:

      6:38

    比较这两种方法,法一是CPU密集型操作,会消耗大量内存。法二使用GPU进行绘制,更快更及时。

  • 主线程阻塞(Blocked)

    可能导致主线程阻塞的操作,例如:

    • 同步请求网络,并等待数据返回

    • 文件IO等访问系统资源的行为

      10:29

    • 数据库操作

    • 8:57

      9:02

如何诊断Hang

  • 使用Instruments,排查线下的问题

    • Time Profile

      13:00

  • 使用MetricKit,检测线上的情况

    13:38

如果治理Hang?

核心目标:减轻主线程的工作

  • 优化必需在主线程中执行的任务

    • 使用缓存

      例如:由其他线程负责保存和更新缓存,主线程只负责读

      16:29

    • 使用Notification

      在主线程中发送通知,其他线程接受通知并异步处理数据

      17:47

  • 移除主线程不必要的任务

    通常来说,为UI提供关键信息的任务,应该在主线程执行。此外,所有的View和View Controller必须在主线程创建、修改和销毁。

    而计算任务可以在其他线程执行,然后在主线程同步UI。一些不重要的维护、非时间敏感的任务应该在其他线程异步执行。

    例如:网络请求

    19:28

    • 使用GCD异步处理任务

      20:21

相关资料

System Trace in depth

What’s new in MetricKit

Diagnose power and performance regressions in your app

Improving battery life and performance

Modernizing Grand Central Dispatch