SwiftUI 使用笔记

5,318 阅读3分钟

摘要

2021年最后一个月,项目组接到个ipad-app演示demo的需求,因为不涉及大规模toC上线,所以使用了SwiftUI练练手,有什么比用最新的工具来解决问题更最快乐的工作方式了呢。

历时两个星期,边看文档边实现功能,再没有UI参与的情况下,用SwiftUI实现了一个让需求方非常满意的demo-app。作者自认为自己的审美有点问题,让我用swift搞个app没有UI是行不通的,但是用SwiftUI, 没有UI也能有一个较为合适的视觉效果(满满的苹果风)。

大致yy下SwiftUI的体感吧

  • 没有UI也终于能上手了。
  • UI即时显示的canvas虽然没有H5,Flutter那么智能,但是相比需要重新build看效果有了更大的提升,虽然有时也不太即时
  • 图层变得好多,运行时点开图层可以看到view图层变得很复杂,可以看出SwiftUI是在UIKit上面做的封装。
  • View的状态监听机制需要着重关注,乱监听会存在性能问题,比如一个页面统一监听了很多个状态对象,每个对象发生了变化都会导致页面刷新。可以考虑把页面拆分成子页面分别监听对应的对象。

后续就是笔记了,内容较少,后续遇到了再加。

State、StateObject、ObservedObject

State 基于值类型的状态管理,只在当前页面所拥有

StateObjectObservedObject类似,都是基于对象的状态管理,差别在于State是对对象的强引用,Observerd是弱引用。如果监听的对象已被其它对象强引用或者是个单例,用ObservedObject即可,否则用StateObject

切记: 在应该使用StateObject的地方使用ObservedObject将会引发灵异事件。

AppStorage SwiftUI版本的UserDefaults

使用方式简单粗暴

@AppStorage("hehe_key") var hehe = ""

// 修改直接赋值即可

hehe = "hehe"

DB之Realm

realm与swiftUI的配合简直天造地设,具体使用可看官网

import SwiftUI
import RealmSwift

class RealmSampleSetting: Object, ObjectKeyIdentifiable {
    @Persisted var sampleRate = "1k"
    @Persisted var HPFEnable = false
    @Persisted var hpfValue = 30
    
    static func get() -> RealmSampleSetting {
        let realm = try! Realm()
        if let item = realm.objects(RealmSampleSetting.self).first {
            return item
        }
        let setting = RealmSampleSetting()
        try? realm.write {
            realm.add(setting)
        }
        return setting
    }
}

该配置在数据表里只会有一条记录

以下是修改的案例


struct SampleRateView: View {
    @ObservedRealmObject var ds: RealmSampleSetting = RealmSampleSetting.get()
    private var realm = try! Realm()
    var body: some View {
        VStack {
            Text("采样参数设置").bold().padding(20)
            List {
                HStack {
                    Text("采样率")
                    Spacer()
                    Picker("", selection: Binding.init(get: {
                        return  RatingSpeed(rawValue: ds.sampleRate) ?? .one
                    }, set: { v in
                        let tmp = ds.thaw()
                        try? realm.write {
                            tmp?.sampleRate = v.rawValue
                        }
                    })) {
                        ForEach(RatingSpeed.allCases, id: \.rawValue) { speed in
                            Text(speed.rawValue).tag(speed)
                        }
                    }
                    .pickerStyle(.segmented)
                    .frame(width: 300)
                }
                HStack {
                    Toggle("", isOn: $ds.HPFEnable).labelsHidden()
                    Text("HPF(HZ)")
                    Spacer()
                    TextField.init("", value: $ds.hpfValue, format:.number.precision(.integerLength(2...3)))
                        .keyboardType(.numberPad)
                        .multilineTextAlignment(.trailing)
                        .frame(width: 200)
                        .disabled(!ds.HPFEnable)
                }
            }
        }
    }
}

简单的修改,直接使用对应的属性即可,复杂的Binding需要自己调用保存

控件篇

Segment

enum RatingSpeed: String, CaseIterable {
    case one = "1k"
    case three = "3.9k"
    case five = "5k"
}

@State private var speedPicker = RatingSpeed.one

body中UI

Picker("速度选择", selection: $speedPicker) {
    ForEach(RatingSpeed.allCases, id: \.rawValue) { speed in
        Text(speed.rawValue).tag(speed)
    }
}.pickerStyle(.segmented)
    .onChange(of: speedPicker) { picker in
        BluetoothCommand.speed(picker)
                            }

更优的方案-不用定义speedPicker变量,取名总是编程中最耗时的工作

Picker("", selection: Binding(get: {
                        return  RatingSpeed(rawValue: ds.sampleRate) ?? .one
                    }, set: { v in
                        let tmp = ds.thaw()
                        try? realm.write {
                            tmp?.sampleRate = v.rawValue
                        }
                    })) {
                        ForEach(RatingSpeed.allCases, id: \.rawValue) { speed in
                            Text(speed.rawValue).tag(speed)
                        }
                    }
                    .pickerStyle(.segmented)
                    .frame(width: 300)

Binding 赋值 get set. get获取初始值。set即修改后的处理

性能优化篇

工具 instruments 里面的 SwiftUI

  1. 通过View Body 里面的页面刷新次数可以获知哪些页面在刷新。

优化方案

  1. 判断该页面是不是应该刷新。一般都是使用了数据监听。
  2. 把数据监听的控件封装为独立的View去监听数据。这样就不会对其它数据源的数据刷新造成影响。
struct MainControlView: View {
    @ObservedObject var blueCentral = BluetoothCentral.shared
    var body: some View {
        VStack(alignment: .center, spacing: 32) {
            TimelineView(.periodic(from: Date(), by: 1)) { context in
                TimeShow(date: context.date, durationTime: $durationTime, timeRefresh: $timeRefresh)
            }
            Text("每秒点数: \(String(format: "%.1f", blueCentral.pointsPerSecond))")
            Text("丢包率: \(String(format: "%.2f", blueCentral.dropRate*100))%")
            Text("采样速度: \(String(format: "%.2f", blueCentral.rattingSpeed))")
            }
        }
    }

优化之前,每次blueCentral数据发生变更,就会影响TimelineView里面的时间显示。然而时间显示与blueCentral无关。 这样情况可以把 blueCentral有关的控件封装成独立的控件

struct DataShowView: View {
    
    @ObservedObject var blueCentral = BluetoothCentral.shared
    var body: some View {
        Text("每秒点数: \(String(format: "%.1f", blueCentral.pointsPerSecond))")
        Text("丢包率: \(String(format: "%.2f", blueCentral.dropRate*100))%")
        Text("采样速度: \(String(format: "%.2f", blueCentral.rattingSpeed))")
    }
}

struct MainControlView: View {
    var body: some View {
        VStack(alignment: .center, spacing: 32) {
            TimelineView(.periodic(from: Date(), by: 1)) { context in
                TimeShow(date: context.date, durationTime: $durationTime, timeRefresh: $timeRefresh)
            }
            DataShowView()
            }
        }
    }

这样bluecentral的数据刷新就不会影响TimelineView了

后续有新的感悟将持续更新