SwiftUI 学习

8,748 阅读8分钟

SwiftUI推出已经很久了,现在才有时间来学习一下,现在开始做一下笔记。 苹果官方学习 官网学习地址可以做到部分代码预演示效果,明显看出一些代码添加后的效果方便理解。

整体感知

1. 声明式语法

这种思想近年来比较流行,react、vue、flutter等都已经采用了这种先进的设计,大大的简化了代码的数量以及对UI管理的难度。你只需要描述界面应该是什么样子,而 SwiftUI 会处理实际的渲染过程。

2. 实时预览

这是一个用文字和图片很难通过本课程传递给大家的部分,只有实际去码代码的时候才能感受到这种预览带来的体验是颠覆性的。如果可以的话希望各位可以找一些视频课程体验一下,最好是能上手体验一下。 截屏2024-06-18 21.55.06.png

3.布局

在SwiftUI中,利用上面所说的声明式语法仅需要描述自己想要的界面就可以轻松地完成布局。配合上Xcode工具提供的实时预览功能,可以说写布局从痛苦变为了一种享受。 在 SwiftUI 中,布局主要由容器视图(如 HStackVStack 和 ZStack)和修饰符(如 paddingframe 和 offset)来控制。

以下是一些常用的容器视图:

  • HStack:在水平方向上排列其子视图。
  • VStack:在垂直方向上排列其子视图。
  • ZStack:在深度方向上堆叠其子视图。

以下是一些常用的修饰符:

  • padding:给视图添加内边距。
  • frame:设置视图的尺寸和对齐方式。
  • offset:移动视图的位置。

Spacer()添加可拉伸的空白空间

List()显示数据列表

var body: some View { 
    List(items, id: \.self) { item in 
        Text(item) 
        } 
    }

Form()创建表单界面

var body: some View { 
    Form { 
        Section(headerText("User Info")) { 
            TextField("Name", text: $name) 
            Toggle("Receive Notifications", isOn: $isOn) 
        } 
        Section { 
            Button("Submit") { 
                print("Form submitted") 
            } 
        } 
   } 
}

4. 跨平台

从目前苹果公布的情况来看,SwiftUI是跨苹果整个生态平台的,包括iOS,macOS,iPadOS,watchOS和tvOS。不太清楚苹果是否未来会有更进一步的规划将整个框架开源出来或者提供更多跨平台的比如web端,windows端或者Android端的开发能力。从目前来看跨iOS和iPadOS是没什么问题,但是直接将代码复用到watchOS还是有些难度,界面最好还是重新设计。 16e121396dd4edca~tplv-t2oaga2asx-jj-mark-3024-0-0-0-q75.png

5. 数据流

SwiftUI 引入了一些新的数据流概念,如 @State@Binding@ObservedObject 和 @EnvironmentObject,它们使得数据状态管理更加直观和一致。而在 UIKit 中,你通常需要手动管理数据状态和更新界面。

@Binding注解实现的双向绑定,在开发UI的时候,我们会遇到过很多种父子视图要传递数据的情况,通过使用@Binding注解,可以自动的帮我们实现双向绑定的功能,无论是父视图还是子视图对数据进行了修改,都可以同步给另一方。

16e12114dc194943~tplv-t2oaga2asx-jj-mark-3024-0-0-0-q75.png 从上图可以看出SwiftUI 的数据流转过程:

  • 用户对界面进行操作,产生一个操作行为 action
  • 该行为触发数据状态的改变
  • 数据状态的变化会触发视图重绘
  • SwiftUI 内部按需更新视图,最终再次呈现给用户,等待下次界面操作

@ObservedObject和@EnvironmentObject两种跨多个组件的数据传递方式。 更多时候一些数据需要跨多个view进行传递并保持一致,这种时候需要类似于消息总线的一个东西帮我们来储存这些数据,学过vue的同学就会想到这个和vuex的设计思路是一致的。在SwiftUI中我们有两种方式可以实现,分别是@ObservedObject和@EnvironmentObject。区别是引入子组件的时候@ObservedObject需要将类在SubView( )括号里面引入;而引入@EnvironmentObject的时候是用SubView().environmentObject( )在后面引入到组件树中,只需要引入一次。如下图所示如果view组件层级嵌套很长的话,那可能用@EnvironmentObject更方便访问全局数据而不用每个组件都传递一次。

16e1213283fd6f85~tplv-t2oaga2asx-jj-mark-3024-0-0-0-q75.png 被这两种修饰符所修饰的类必须继承ObservableObject协议,并且内部中需要被观察的属性需要打上@Published修饰符。

ObservableObject

16e12943b727e392~tplv-t2oaga2asx-jj-mark-3024-0-0-0-q75.png

  • 在应用开发过程中,很多数据其实并不是在 View 内部产生的,这些数据有可能是一些本地存储的数据,也有可能是网络请求的数据,这些数据默认是与 SwiftUI 没有依赖关系的,要想建立依赖关系就要用 ObservableObject,与之配合的是@ObservedObject@Published

  • @Published 是 Xcode11 beta5 之后新增的代理属性,此属性如果用在 ObservableObject 内,一旦修饰的属性发送了变化,会自动触发 ObservableObject 的objectWillChange 的 send方法,刷新页面,SwiftUI 已经默认帮我实现好了,但也可以自己手动触发这个行为。

  • ObservableObject 是一个协议,必须要去实现该协议。

  • ObservableObject 适用于多个 UI 之间的同步数据。

  • 基本使用

class User: ObservableObject {
    @Published var name = ""  // @Published修饰需要监听的属性,一旦变化就会发出通知,它是发布者
    @Published var address = ""
}
struct ContentView: View {
    @ObservedObject var User = User()  // @ObservedObject修饰ObservableObject
    var body: some View {
    }
}
  • 使用案例
class UserSettings: ObservableObject {
  // 有可能会有多个视图使用,所以属性未声明为私有
  @Published var score = 123
}

struct ContentView: View {
  @ObservedObject var settings = UserSettings()

  var body: some View {
      VStack {
          Text("人气值: (settings.score)").font(.title).padding()
          Button(action: {
              self.settings.score += 1
          }) {
              Text("增加人气")
          }
      }
  }
}
  • 手动发送状态更新
class UserSettings: ObservableObject {
    
    // 1.添加发布者,实现一个属性,名字不能乱写,否则没有效果
    let objectWillChange = ObservableObjectPublisher()
    
    // 2.只要name发生更改,属性观察器就会调用,告诉objectWillChange发布者发布有关我们的数据已更改的消息,以便所有订阅的视图都可以刷新的消息
    var name = "" {
        willSet {
            
            // 3.使用发布者
            objectWillChange.send()
        }
    }
}


struct ContentView: View {
    @ObservedObject var settings = UserSettings()
    
    var body: some View {
        VStack {
            TextField("姓名", text: $settings.name)
                .textFieldStyle(RoundedBorderTextFieldStyle()).padding()
            
            Text("你的姓名: (settings.name)")
        }
    }
}

EnvironmentObject

  • 主要是为了解决跨组件(跨应用)数据传递的问题。

  • 组件层级嵌套太深,就会出现数据逐层传递的问题, @EnvironmentObject可以帮助组件快速访问全局数据,避免不必要的组件数据传递问题。

  • 使用基本与@ObservedObject一样,但@EnvironmentObject突出强调此数据将由某个外部实体提供,所以不需要在具体使用的地方初始化,而是由外部统一提供。

  • 使用@EnvironmentObject,SwiftUI 将立即在环境中搜索正确类型的对象。如果找不到这样的对象,则应用程序将立即崩溃。

// 和@ObservableObject一样
class User: ObservableObject {
    @Published var name = ""
    @Published var address = ""
}


struct ContentView: View {
    @EnvironmentObject var User   // 注意这里不需要初始化
    var body: some View {
    }
}
  • 使用案例
class UserSettings: ObservableObject {
    @Published var score = 123
}

struct ContentView: View {
    
    @EnvironmentObject var settings: UserSettings
    
    var body: some View {        
        NavigationView{            
            VStack {
                // 显示score
                Text("人气值: (settings.score)").font(.title).padding()
                // 改变score
                Button(action: {
                    self.settings.score += 1
                }) {
                    Text("增加人气")
                }
                // 跳转下一个界面
                NavigationLink(destination: DetailView()) {
                    Text("下一个界面")
                }
            }
        }
    }
}

struct DetailView: View {
    
    @EnvironmentObject var settings: UserSettings
    
    var body: some View {
        VStack {
            Text("人气值: (settings.score)").font(.title).padding()
            Button(action: {
                self.settings.score += 1
            }) {
                Text("增加人气")
            }
        }
    }
}

// 需要注意此时需要修改SceneDelegate,传入environmentObject
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserSettings()))

注意

  1. 在 SwiftUI 中,开发者只需要构建一个视图可依赖的数据源,保持数据的单向有序流转即可,其他数据和视图的状态同步问题 SwiftUI 帮你管理,所以 ViewController 在这里也就不需要了,再也不存在臃肿瘦身的问题了。

  2. SwiftUI 的界面不再像 UIKit 那样,用 ViewController 承载各种 UIVew控件,而是一切皆 View,所以可以把 View 切分成各种细粒度的组件,然后通过组合的方式拼装成最终的界面,这种视图的拼装方式大大提高了界面开发的灵活性和复用性,视图组件化并任意组合的方式是 SwiftUI 官方非常鼓励的做法。

  3. Property、 @State、 @Binding 一般修饰的都是 View 内部的数据。

  4. @ObservedObject、 @EnvironmentObject 一般修饰的都是 View 外部的数据:

    • 系统级的消息
    • 网络或本地存储的数据
    • 界面之间互相传递的数据

6. 交互动画

在SwiftUI中,苹果提供了全新的方式来实现和动画效果,使用动画效果变成了一件简单的事情,任何view都可以通过.animation(.spring())来得到一个不错的效果。但是如果涉及到复杂的手势操作和动画效果的话,实现起来还是略有些困难。

常用手势Gesture

  1. TapGesture 用于处理点击事件
  2. LongPressGesture 用于处理长按事件
  3. DragGesture 用于处理拖拽事件
  4. MagnificationGesture 用于处理缩放手势
  5. RotationGesture 用于处理旋转手势
  6. Combining Gestures 组合多个手势来处理复杂的交互
@State private var offset = CGSize.zero 
@State private var scale: CGFloat = 1.0 

var body: some View { 
Text("Drag and Zoom me") 
    .padding() 
    .background(Color.yellow) 
    .foregroundColor(.black) 
    .offset(offset) 
    .scaleEffect(scale) 
    .gesture(
        SimultaneousGestureDragGesture() 
                .onChanged { gesture in 
                    self.offset = gesture.translation 
                    } .onEnded { _ in 
                        self.offset = .zero 
                        }, 
            MagnificationGesture() 
                .onChanged { value in 
                    self.scale = value 
                    } 
                .onEnded { _ in 
                    self.scale = 1.0 
                } 
       ) 
   ) 
 } 
}

常用动画animation

  1. 使用 .animation() 修饰符为状态变化添加动画。
  2. 自定义动画 使用 .animation(_:value:) 修饰符为特定值的变化添加动画
  3. Spring Animation 使用弹簧动画创建自然的弹性效果。
var body: some View {
        VStack { 
            Spacer() 
            Circle() 
                .fill(Color.purple) 
                .frame(width: isExpanded ? 200 : 100, height: isExpanded ? 200 : 100) 
                .animation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0), value: isExpanded)
                Spacer() 
                Button("Toggle") { 
                    isExpanded.toggle() 
                    } 
                .padding() 
          } 
    }
  1. 组合动画 同时执行多个动画
var body: some View { 
    VStack { 
    Spacer() 
    Circle() 
        .fill(isExpanded ? Color.orange : Color.blue) 
        .frame(width: isExpanded ? 200 : 100, height: isExpanded ? 200 : 100) 
        .scaleEffect(isExpanded ? 1.5 : 1) 
        .animation(.easeInOut(duration: 1), value: isExpanded) 
    Spacer() 
    Button("Toggle") { 
        isExpanded.toggle() 
    } 
    .padding() 
   } 
}
  1. 动画修饰符 使用 .transition() 为视图添加进出动画效果。
  2. 无限重复动画 使用 .repeatForever() 创建无限重复的动画。