SwiftUI 开发常用知识点总结

36 阅读4分钟

写在前面的话:在Swift中除了class和函数是引用类型,其它基本都是值类型;而在SwiftUI中更是将值类型用到极致,比如一个View都可以用一个Struct来创建,swift推荐大家能用Struct就用,尽量少用Class,因为Class的复杂程度很高。

1. ForEach

ForEach一般用在生成一组some View.

1.1 Identifiable

ForEach接受一个数组,且数组中的元素必须需要满足 Identifiable 协议,就是数组中的元素有唯一标识符,可以被区分。

struct Item:Identifiable{
    var id = UUID()
    var message: String
}

struct ContentView: View {
    let items = [Item(message: "hello"), Item(message: "world")]
    var body: some View {
        VStack {
            ForEach(items) {
                Text($0.message)
            }
        }
    }
}

1.2.Hashable

但是数组中的每一个元素都不遵循 Identifiable,我们可以使用 ForEach(_:id:) 来通过某个支持 Hashable 的 keyPath 来获取一个等效的元素的 Identifiable 的数组,其实就是用hash值来区分的。

struct Item {
    var message: String
}

struct ContentView: View {
    let items = [Item(message: "hello"), Item(message: "world")]
    var body: some View {
        VStack {
// 字符串,整数,浮点和布尔值,还有事件集合(even sets)都遵循Hashable。
其他类型(例如,选项(optionals),数组(Array)和范围(Range))在其类型参数实现符合Hashable时就会自动变为hashable
            ForEach(items,id: \.message) {
                Text($0.message)
            }
        }
    }
}

或者直接让数组中的元素遵循Hashable。

struct Item: Hashable {
    var message: String
}

struct ContentView: View {
    let items = [Item(message: "hello"), Item(message: "world")]
    var body: some View {
        VStack {
            ForEach(items,id: \.self) {
                Text($0.message)
            }
        }
    }
}

2. NavigationView 和 toolBar

NavigationView就类似于UIKit中的UINavigationController,而toolBar就是导航条上面的按钮元素(NavigationItem)

NavigationLink就是处理每一个cell的点击事件。

struct Item: Hashable {
    var message: String
}

struct ContentView: View {
    let items = [Item(message: "hello"), Item(message: "world")]

    var body: some View {
        NavigationView {
            List(items, id: \.self) { item in
                ZStack {
                    NavigationLink { // 这个NavigationLink里面的视图不会刷新
                        Text("click cell")
                    } label: {
                        EmptyView() // 为了去掉NavigtionLink右边的箭头
                    }.opacity(0)
                    Text(item.message)
                }
            }.listStyle(.plain)
                .navigationTitle("Jonathan")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Image(systemName: "plus.circle")
                    }
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
                        Image(systemName: "plus.circle")
                        Image(systemName: "plus.circle")
                    }
                }
        }
    }
}

注意:

如果想自定义左上角返回的按钮,则先要用.navigationBarBackButtonHidden(true)修饰符去隐藏掉默认的放回按钮,然后用ToolBarItem去加。

如果想要左滑删除等操作,需要在List中嵌套一层ForEach.

当然也可以用List中的每一个row的.swipActions修饰符来自定义左滑或者右滑按钮。

List {
    ForEach(items) { item in
        // do something
    }.onDelete { indexSet in
        // 做删除操作
    }
}

3. TabView 和 TabItem

TabView就类似于UIKit中UITabBarViewController,而TabItem就是TabView上面的一个元素。

var body: some View {
        TabView {
            NavigationView {
                List(items, id: \.self) { item in
                    Text(item.message)
                }.listStyle(.plain)
                    .navigationTitle("Jonathan")
                    .navigationBarTitleDisplayMode(.inline)
                    .toolbar {
                        ToolbarItem(placement: .navigationBarLeading) {
                            Image(systemName: "plus.circle")
                        }
                        ToolbarItemGroup(placement: .navigationBarTrailing) {
                            Image(systemName: "plus.circle")
                            Image(systemName: "plus.circle")
                        }
                    }
            }.navigationViewStyle(.stack) // 设置图片和文本的排放方式
            .tabItem {
                Image(systemName: "message.fill")
                Text("Message")
            }
            
            Text("Second Page").tabItem {
                Image(systemName: "person.2.fill")
                Text("User")
            }
        }.onAppear {
            // 设置navigationBar模糊的效果
            let navigationAppearance =  UINavigationBarAppearance()
            navigationAppearance.configureWithDefaultBackground()
            
            UINavigationBar.appearance().standardAppearance = navigationAppearance
            UINavigationBar.appearance().scrollEdgeAppearance = navigationAppearance
            
            // 设置tabBar的模糊效果
            let tabBarAppearance =  UITabBarAppearance()
            tabBarAppearance.configureWithDefaultBackground()
            
            // 设置tabbarItem的颜色
            let tabBarItemAppearance = UITabBarItemAppearance()
//            tabBarItemAppearance.normal.iconColor 设置为选中情况下的颜色
            tabBarItemAppearance.selected.iconColor = UIColor.green
            tabBarItemAppearance.selected.titleTextAttributes = [
                NSAttributedString.Key.foregroundColor: UIColor.green
            ]
            tabBarAppearance.stackedLayoutAppearance = tabBarItemAppearance
            
            UITabBar.appearance().standardAppearance = tabBarAppearance
            UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
            
        }
    }

4. 属性包装器 propertyWrapper

4.1 UIApplicationDelegateAdaptor

将UIApplicationDelegate的方法包装到SwiftUI.

import SwiftUI

@main
struct JonathanApp: App {
    
    //就是利用AppDelegate实例化了一个UIApplicationDelegateAdaptor 并给变量appDelegate
    //类似这行代码:private var appDelegate = UIApplicationDelegateAdaptor(AppDelegate.self)
    @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
    var body: some Scene {
         WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // Do something
        // 用户在上下拖动页面的时候  键盘会消失
        UIScrollView.appearance().keyboardDismissMode = .onDrag
        
        return true
    }
}

4.2 Environment中取scenePhase

scenePhase类似于UIKit中的SceneDelegate.

    @Environment(\.scenePhase) var scenePhase
    
    var body: some Scene {
         WindowGroup {
            ContentView()
         }.onChange(of: scenePhase) { newScenePhase in
             switch newScenePhase {
             case .background:
                 print("App entries background")
             case  .inactive:
                 print("App is inactive")
             case .active:
                 print("App is active")
             @unknown default:
                 break
             }
         }
    }

4.3 State(绑定数据到视图)

在Struct中,想要修改一个属性,则必须通过一个mutating的方法来进行修改;

而用State修饰的属性,则可以直接修改;

而且当state修饰的属性值改变的时候,将会自动刷新改属性相关联的视图。类似双向绑定。

    @State private var isOn = false
    var body: some View {
        Button {
            isOn.toggle() // 当isOn为true 执行toggle之后 isOn就为false了
        } label: {
            Text(isOn ? "on" : "off")
        }
    }

原理,当被state修饰的属性改变的时候,这个属性会被copy一份,进行修改,改完之后把新的重新赋给这个属性,然后触发didSet方法,进一步触发视图的刷新。

4.4 Binding

Binding用于绑定两个属性,如果父视图中的这个属性的value改变的时候,也会同事影响到子视图的这个属性,就是子视图的这个属性也会同时改变;相反,如果子视图的这个属性的value发生改变,其父视图这个属性的value也会发生改变;

// 绑定连个view的isOn属性
struct StateView: View {
    @State var isOn = false
    var body: some View {
        ZStack {
            ZStack(alignment: .bottomLeading) {
                Rectangle().frame(height: 500)
                Button {
                    isOn.toggle()
                } label: {
                    Text(isOn ? "on" : "off")
                }
            }
            SubStateView(isOn: $isOn) // 表示传的是isOn的引用
        }
        
    }
}


struct SubStateView: View {
    // single source of truth 单一数据源原则
    @Binding var isOn: Bool  // 且这里不能给初始值 而这个类型是 Binding<Bool>
    var body: some View {
        Text(isOn ? "Open" : "Close")
    }
}

在SwiftUI中初始化好多控件的时候,都会用到Binding类型的参数,不如Toggle,TextField,Picker等。

 struct TextFieldView: View {
    @State var content: String  = ""
    var body: some View {
        TextField("please input", text: $content)
    }
}

4.5 FocusState

用于设置光标是否在textFiled内部;

struct TextFieldView: View {
    @State var content: String  = ""
    @State var content2: String  = ""
    
    enum Field {
        case content, content2
    }
    @FocusState private var isFocused: Field? // 目前无初始值
    var body: some View {
        Form {
            Section {
                TextField("please input content", text: $content).focused($isFocused, equals: .content)
                TextField("please input content2", text: $content2).keyboardType(.default).focused($isFocused, equals: .content2)
            }
            Section {
                Button("Done") {
                    // do something
                    if content.isEmpty {
                        isFocused = .content
                    }else {
                        // do something
                    }
                }
//                .disabled(true)
            }
        }
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                Spacer()
                Button("Doen") {
                    isFocused = nil
                }
            }
        }
        .onAppear {
            // 延迟为了让页面完全加载
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                isFocused = .content
            }
        }
    }
}

4.6 ViewBuilder

当一个属性被ViewBuilder修饰的时候,则这个属性里面可以放多个视图,也可以进行一些逻辑判断,比如body.

通常属性、闭包都可以被@ViewBuilder修饰。

4.7 Publised,StateObject,ObservedObject

详情见后文:11.MVVM

4.8 EnviromentObject

一般用于夸页面传值,EnvironmentObject 就像把一个对象全局化了一样。

View A -> View B -> View C,现在在C中访问A的对象;

View A中:
用.environmentObject(thing)封装对象,注意调用在Body的根View.

var thing: Thing = Thing(tag: "e")
var body: some View {
    NavigationView {
        ViewB()
    }.environmentObject(thing)
}


View C中:
用@EnvironmentObject var thing: Thing直接访问即可

@EnvironmentObject var thing: Thing
var body: some View {
    // thing.tag: "e"
    Text(thing.tag)
}

5. Picker控件

struct PickerView: View {
    
    @State var content = 0
    let array = ["Seg one","Seg two","Seg three"]
    var body: some View {
        Picker("Select", selection: $content) {
            ForEach(0..<array.count, id: \.self) { index in
                // 下面的代码类似这行:Text(array[index]).tag(index),因为ForEach默认会将id赋给tag
                Text(array[index])
            }
        }.pickerStyle(.segmented)
    }
}

6. alert修饰符

struct AlertView: View {
    @State var alertValue: Bool = false
    var body: some View {
        Form {
            Section {
                Button("Done") {
                    alertValue = true
                }
            }
        }
        .alert("Alert", isPresented: $alertValue, actions: {
            Button("Done", role: .none) {
                // do something
            }
        }, message: {
            Text("This is message!")
        })
    }
}

7. group

类似stack,但是没有排序规则,用于包裹一组视图;

group里面最多放10个静态视图;

8. 常用动画

8.1 animation动画(显式和隐式)

注意给视图设置animation动画的时候,该视图必须已经加载到页面上。

struct AnimationView: View {
    
    @State private var isLike = false
    var body: some View {
        Image(systemName: isLike ? "heart.fill" : "heart")
            .foregroundColor(isLike ? .red : .black)
            .scaleEffect(isLike ? 1.5 : 1)
            //.animation(.default, value: isLike) // 当isLike变化的时候 触发这个动画, 
// 所以缺点就是很明显的 只能监听一个值得改变,这称为隐式动画implicate animation
            .onTapGesture {
                // 弹簧动画:response-表示刚度 dampingFraction-表示阻尼
                withAnimation(.spring(response: 0.2, dampingFraction: 0.2)) { // 这个就是显式动画 explicate Animation
                    isLike.toggle()
                }
            }
    }
}

8.2 stroke & overlay & opacity 修饰符

  • stroke 对视图进行勾勒描边;
  • overlay直接覆盖视图;
  • opacity 修改透明度;
struct AnimationView: View {
    @State private var count: CGFloat = 1
    var body: some View {
        Button("Click Me") {}
        .foregroundColor(.red)
        .background(.red)
        .clipShape(Circle())
        .overlay {
            Circle().stroke(.red, lineWidth: 10)
                .scaleEffect(count)
                .opacity(Double(2 - count))
                .animation(.easeOut(duration: 2).repeatForever(autoreverses: false), value: count)
        }
        .onAppear {
            count = 2
        }
    }
}

8.3 transition修饰符

transition 顾名思义,就是移动的动画;

    @State private var show = false
    var body: some View {
        VStack(spacing: 20) {
            Button(show ? "Hide" : "Show"){
                withAnimation(.linear(duration: 2)) {
                    show.toggle()
                }
            }
            
            VStack {
                if show {
                    Image(systemName: "sun")
                        .foregroundColor(.red)
                        .transition(.slide.combined(with: .opacity))
                }
            }
        }
    }
    

9. SwiftUI中的Self

Self表示当前类型本身;

@ViewBuilder var body: Self.Body { get }

10. present和dismiss

  • 页面的present方式跳转,需要用到一个sheet修饰符;
  • 如果想要全屏弹出,则用fullScreenCover修饰符;
    @State private var nextPage = false
    
    var body: some View {
        TabView {
            NavigationView {
                List(items, id: \.self) { item in
                    Text(item.message)
                }
            }.navigationViewStyle(.stack)
                .sheet(isPresented: $nextPage) {
                    // 放入目的View即可
                    Text("next page")
                }
     }

而手动dismiss的时候,需要从Environment中获取dismiss对象.

@Environment(\.dismiss) var dismiss

然后调用dismiss()即可关闭page. 注意直接在对象后面加(), 表示直接调用了这个对象的callAsFunction()方法。

而这个dismiss方法可以关闭push出来的页面,也可以关闭present出来的页面。

11. MVVM设计模式在SwiftUI中的使用

一般在ViewModel中做一些逻辑处理,比如取数据。而ViewModel一般是个Class. 而且要用@Published (表示这个数据被公开) 来修饰从服务器获取到的数据模型,且这个class必须遵循ObservableObjct (表示可以被监听)协议;

@Published 表示向外发布,其他地方有订阅(检测、观察)他的变化,一旦变化,则刷新相关视图;

class ViewModelObservableObject {
    @Published var modelContent = [Item(message: "one"), Item("two")] 
}


// 注意在ViewModel中可以主动调用 objectWillChange.send() 来刷新视图。

在View中用对象包装器StateObject (作用类似State包装器,第一次创建ViewModel的时候,当刷新当前视图的时候,这个被StateObject修饰的对象是不会被刷新的)修饰ViewModel对象。

@StateObject var viewModel = ViewModel()

而在其它用到ViewModel数据的Page,用ObservedObject (作用类似binding包装器,表示监听这个ViewModel对象,当刷新当前视图的时候,这个被ObservedObject修饰的对象是被刷新的) 来修饰ViewModel.

@ObservedObject var viewModel: ViewModel

当然:如果一个ViewModel如果要一层一层被传递好几个页面的时候,我们可以在viewModel的初始化页面(或者直接将@StateObject放到App page,岂不痛快,然后在其它page都可以取到这个viewModel)

使用.environmentObject(viewModel) 修饰符(注意这个修饰符是在body的根view上的),然后直接在它的子页面用@EnvironmentObject var viewModel: ViewModel 获得这个viewModel,而就不用使用@ObservedObject一层层的传值了。

12. 小零碎

12.1 获得屏幕的宽高(GeometryReader)

GeometryReader {
    Rectangle().frame(width: $0.size.width)
}

12.2 数据转模型

首先让Struct模型遵循Codable协议;

然后 JSONDecoder.decode([Model].self, from: data) 搞定。

12.3 自定义修饰符

// 自定义个修饰符 将font和padding放在一个修饰符里面

struct BoldTitleWithBottomPadding: ViewModifier {
    func body(content: Content) -> some View {
        content.font(.title.bold())
               .padding(.bottom, 5)
    }
}
extension View {
    func boldTitleWithBottomPadding() -> some View {
        modifier(BoldTitleWithBottomPadding())
    } 
}

// 调用
Text("Jonathan").boldTitleWithBottomPadding()

12.4 数据存储

数据存储的方式有UserDefaults(轻量的数据存储)、@AppStorage、CoreData.

@AppStorage这个属性包装器是会把简单数据存放到UserDefaults.

@AppStorage("key") var value = ""
// 这句话 结合了@State var value = ”“的定义,而且当value的值改变的时候,会自动将其存到UserDefault.

CoreData是封装了操作SQLite的方法,是其对象化,用对象的方式去操作数据库。

13. Concurrency

  • Swift5.5 的concurrency是专门来处理多线程的;
  • 首先要说的是 await 必须与 async 搭配使用;
  • 使用Task的原因是在同步线程和异步线程之间,我们需要一个桥接,我们需要告诉系统开辟一个异步环境;
0. 包装closure成一个 await - async
  func verifyData(id: String) async throws -> Bool {
    try await withCheckedThrowingContinuation { continuation in
      self.verify(by: id) { result in
        switch result {
        case let .success(isSucceed): continuation.resume(returning: isSucceed)
        case let .failure(err): continuation.resume(throwing: err)
        }
      }
    }
  }

// 使用
func test() {
  Task {
    do {
      let isSucceed: Bool = try await verifyData(id)
    } catch {// do something}
  }
}


1.单个异步
func downloadImage(url: String) async -> UIImage? {
  return await loadImage(url: url)
}

// 使用: 不用再function之后加 async, 在Task中执行完耗时操作之后,需要回到主线程,也可用@MainActor修饰。
func test() {
  Task {
    let image = await self.downloadImage(
    DispatchQueue.main.async {
      // do something
    }
  }
}

2. 并发下载多张图片
func multiImageLoad() async -> [UIImage]? {
  var results: [UIImage] = []
  for url in urls.enumerated() {
    results.append(await self.loadImage(url: url)!)
  }
  return results
}

// 使用:
Task {
  guard let images = await multiImageLoad() else { return }
  DispatchQueue.main.async {
    // do something
  }
}