惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Stack Overflow Blog
Stack Overflow Blog
The GitHub Blog
The GitHub Blog
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
宝玉的分享
宝玉的分享
N
News and Events Feed by Topic
The Hacker News
The Hacker News
Google DeepMind News
Google DeepMind News
C
CERT Recently Published Vulnerability Notes
F
Full Disclosure
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Security @ Cisco Blogs
H
Hacker News: Front Page
L
LangChain Blog
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
B
Blog RSS Feed
H
Heimdal Security Blog
Google Online Security Blog
Google Online Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 三生石上(FineUI控件)
V2EX - 技术
V2EX - 技术
V
Vulnerabilities – Threatpost
Help Net Security
Help Net Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tailwind CSS Blog
W
WeLiveSecurity
T
Tenable Blog
D
DataBreaches.Net
Martin Fowler
Martin Fowler
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
S
Secure Thoughts
O
OpenAI News
L
LINUX DO - 热门话题
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
Jina AI
Jina AI
J
Java Code Geeks
Know Your Adversary
Know Your Adversary
IT之家
IT之家
Latest news
Latest news
Cloudbric
Cloudbric

ZDDHUB

PixelsMeasure 开发第二年总结 PixelsMeasure 开发一年总结 为什么说 GPT 利好程序员 ChatGPT 编程实现 Web 数字水印 Web 数字水印探究 Micro Frontends for Mobile URL 加载系统(URL Loading System) Protocol Buffers GraphQL 从 0 到 1 开发一款 IOS 应用 - Swift MV* 软件设计架构 学习一个新技巧需要多久? 不停机数据库迁移 Rspec 如何 mock update 方法更新自己? Rails 使用 mysql2 出现的段错误 使用 Docker-compose 部署 Rails 应用到生产环境 Cocoa troubleshooting 独孤九剑 Dit (0x05) - 终端篇 Gem-based Jekyll theme 开发小记 Miscellaneous 前端手记 TodoMVC 之 Redux 篇 前端手记 TodoMVC 之 Server 篇 前端手记 TodoMVC 之 React 篇 前端手记 TodoMVC 之 CSS 篇 独孤九剑 Dit (0x04) - 测试篇 独孤九剑 Dit (0x03) - 缓存篇 英语小抄 LLDB debug Golang Make mistakes 大牛俱乐部上线啦 独孤九剑 Dit (0x02) - 数据结构篇 独孤九剑 Dit (0x01) - 总决 独孤九剑 Dit (0x00) - 我为什么要做 Dit 零值强制类型转换的使用 终端颜色输出重定向 Go语法简略 - 正则表达式 Makefile Go语法简略 - Duck框架探索 Go语法简略 - 依赖注入 Go语法简略 - web应用框架 Go语法简略 - 反射 Go语法简略 - 面向对象 Go语法简略 - goroutine Go语法简略 - 方法和接口 Go语法简略 - 基础篇 大牛 轻松科研 未来这几年 为 Android Studio 创建图标 Shell Git Vim 金庸答百问 论拖延症 Flex, A fast scanner generator 有理想的人 从虚拟到现实 常用视频转接口 Recognizer configuration on CentOS 整个世界清静了 《Python源码剖析》读书笔记
Swift/SwiftUI 踩坑记
zddhub · 2023-09-07 · via ZDDHUB

这里记录 Swift/SwiftUI 开发过程中踩过的坑。

注:目前的项目混合使用 SwiftUI 和 UIKit,不能保证纯 SwiftUI 也有类似问题。

@State, @StateObject, @ObservedObject

这三兄弟是 SwiftUI 中最常用的三个属性,使用不当会得到奇怪的结果,也有可能导致性能问题。这里只记录如何正确的使用它们,来符合 SwiftUI 设计者的初衷。

@State

@State 定义在使用它的 View 的最外层,当改变时,SwiftUI 刷新依赖它的视图。@State 只对当前视图和子视图可见,因此通常标记成 private,它们永远成对出现,如果你发现不能标记成 private,那么可能不应该使用 @State。@StateObject 也一样。用法如下:

1. 经典用法
@State private var library: Library = Library()
// @State 属性的初始化在其生命周期内只被赋值一次,但是右值 `Library()` 会被创建多次,(在当前视图的输入参数变化时,init 会被调用。
// 即使在 init 里设置 library = Library(name: "new"), SwiftUI 也只保证 library 只被赋值一次。
// View 的 id 变化时,生命周期结束

2. 延迟初始化, 对于频繁刷新的视图为避免右值被创建多次并且创建右值代价太大时使用延迟调用
@State private var library: Library?

var body: some View {
  MyView()
    .task { // task 只调用一次
      library = Library()
    }
}

@State 自己改变时视图刷新,但是 @State 的属性改变时,视图并不会刷新,要做到这一点,就需要用 @StateObject

@StateObject

使用 @StateObject 配合 ObservableObject 对象的 @Published 属性来检测属性的改变。SwiftUI 会保证 @StateObject 属性在当前视图生命周期内只被改变一次。@StateObject 的用法如下:

1. 经典用法
@StateObject private var model = DataModel()

2. 延迟初始化 @State 一样

@StateObject private var dataModel: DataModel?

var body: some View {
  MyView()
    .task { // task 只调用一次
      dataModel = DataModel()
    }
}

3.  Init 中初始化即使 name 的变化会引起 init 被多次调用但是 _model 也只被赋值一次注意这种写法会使得 DataModel 被创建多次但没有赋值给 _model所以如果创建 DataModel 耗时选择使用延迟初始化
struct MyInitializableView: View {
  @StateObject private var model: DataModel

  init(name: String) {
      // SwiftUI ensures that the following initialization uses the
      // closure only once during the lifetime of the view, so
      // later changes to the view's name input have no effect.
      // State 和 StateObject 只能被初始化一次,但是 ObservedObject 每次都会被初始化
      // 如果想每次 name 更新时更新 _model, 需要更新 identifier,使用 .id(_) 方法。使用id有副作用,比如让动画失效。
      _model = StateObject(wrappedValue: DataModel(name: name))
      // model = DataModel(name: name) // 编译错误
  }

  var body: some View {
      VStack {
          Text("Name: \(model.name)")
      }
  }
}

@ObservedObject

如果视图的输入 (参数)是 ObservableObject 对象(通常是 @StateObject),使用 @ObservedObject 接收它,不要在声明 @ObservedObject 的视图里初始化它。用法如下:

class DataModel: ObservableObject {
    @Published var name = "Some Name"
    @Published var isEnabled = false
}


struct MyView: View {
    @StateObject private var model = DataModel()

    var body: some View {
        Text(model.name)
        MySubView(model: model)
    }
}

struct MySubView: View {
    @ObservedObject var model: DataModel

    init(model: DataModel) {
      self.model = model // 只应该被这样用

      // 不要像以下这样用,和 State,StateObject 表现不一样
      // self.model = DataMoel() // 此时 model 被重新初始化
      // self._model = ObservedObject(wrappedValue: DataModel()) // 此时 model 被重新初始化
    }

    var body: some View {
        Toggle("Enabled", isOn: $model.isEnabled)
    }
}

Don’t specify a default or initial value for the observed object. Use the attribute only for a property that acts as an input for a view, as in the above example.

总结

  • SwiftUI View 的 init 方法会被调用多次
  • SwiftUI 在 View 的输入(View 的参数)改变时调用 init 方法
  • 当状态改变或者 init 刷新时,body 会被调用
  • body 执行时,里面的所有对象/View都会被重新创建,但不是所有的 View hierachy 会更新,SwiftUI 只重绘那些改变的部分
  • 当 init 方法被重复调用时,@State, @StateObject 只会被初始化一次,@ObservedObject 每次都会被初始化
  • @State, @StateObject 应该永远被标记成 private,永远在当前 View 内初始化
  • @ObservedObject 只应该接受 ObservableObject 参数,不应该在当前 View 内被初始化
  • 改变 View 的 id (identifier),相当于创建一个全新的 View,会重置所有值。注意,动画可能也会收到影响而中断或者异常。

验证代码写在了 Github Gist 上。

弹出键盘导致布局混乱

坑:

使用 push 弹出全屏幕的 View,该 View 中有一个 TextField,获得焦点后返回,主屏布局混乱,底部多了一块空白区域,所有元素事件响应失效,主屏是一个 ScrollView。逐步盘查后发现,注释掉子页面的 view.becomeFirstResponder() 后问题不复现。

合理的行为:

子屏幕不应该影响主屏幕的行为。

修复方案:

禁用键盘的安全区域。

.ignoresSafeArea(.keyboard)

启发

  • 新开发的功能出问题后,应逐步盘查,用简单的 View 代替复杂的页面,逐步缩小范围,来定位问题。
  • 如果确定是事件响应引起的问题,可能与键盘相关。
  • 如果页面底部多了一块空白区域,可能是键盘收起后布局没有重新计算。

@AppStorage 的 key 不能以 @ 开头

坑:

@AppStorage 能非常方便的存取 UserDefaults 中的数据,但是如果 key 使用 @ 开头的话,会报 crash。

合理的行为:

不应该报 crash,或者文档中应该有类似的说明。

修复方案:

不使用 @ 开头的字符串做为 @AppStorage 的 key。如果是组合的字符串,保证 @ 之前的字符串不能为空。

启发

  • @ 是 objc 中字符串的关键字,目前 SwiftUI 的底层使用 UIKit,甚至 objc 的关键字
  • 这个问题很难发现,只有踩坑才能知道
  • 需要测试所有逻辑的路径,即使业务场景不存在

坑:

SwiftUI Menu 使用非常方便,但是在点开 Menu 菜单没有合上的时候,去点击/滑动同一个页面的其它元素 (使用 onTapGesture 响应),这时页面卡死。

合理的行为:

此时,应该自动收起 Menu 的菜单,然后响应接下来的点击或者滑动事件。

修复方案:

当 Menu 打开时,给整个页面添加透明的蒙层,在 Menu 开启的时候,点击/滑动其它元素,隐藏蒙层。代码片段如下:

YourScreen {}
    .overlay {
      if menuIsOpen {
        Color.white.opacity(0.001)
          .ignoresSafeArea()
          .frame(maxWidth: .infinity, maxHeight: .infinity)
          .onTapGesture {
            menuIsOpen = false
          }
          .gesture(DragGesture(minimumDistance: 0)
            .onEnded { _ in
              menuIsOpen = false
            }
          )
      }
    }

启发

  • 测试 Menu 时,记得在菜单打开的时候点击页面其它元素,或者滑动页面
  • 测试 Menu 时,记得在菜单打开的时候点击页面上的另一个 Menu 元素
  • 测试 Menu 时,记得旋转屏幕
  • 透明蒙层时一个好的方案

如果你喜欢这篇文章,欢迎赞赏作者以示鼓励