摘要
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 基于值类型的状态管理,只在当前页面所拥有
StateObject 与ObservedObject类似,都是基于对象的状态管理,差别在于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
- 通过View Body 里面的页面刷新次数可以获知哪些页面在刷新。
优化方案
- 判断该页面是不是应该刷新。一般都是使用了数据监听。
- 把数据监听的控件封装为独立的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了