50万奖金+官方证书,深圳国际金融科技大赛正式启动,点击报名 了解详情
写点什么

写了 3 万行 SwiftUI 生产代码后的经验:凡事皆有利弊

  • 2023-02-27
    北京
  • 本文字数:2881 字

    阅读完需:约 9 分钟

写了3万行SwiftUI生产代码后的经验:凡事皆有利弊

上手SwiftUI才几个小时,我们就爱上它了,甚至立刻就决定放弃跨平台代码库,并在 iOS 上改成完全原生的架构。1 月份上架 AppStore 的 timing.is 这个应用完全是用SwiftUI构建的。它的开发过程历时数月,要不是 SwiftUI 除了优点外还有很多缺点的话,这个时间还可以更短。(补充一下,这个应用开发阶段就上架 TestFlight 了,基于用户反馈的迭代过程也消耗了我们一部分开发时间。)在开发后期,我们甚至重新审视了当初的决策。最后,出于几个原因我们没有放弃它:我们已经走的太远了,整个计划还严重超期,重启的成本是无法接受的。但这不是主要原因——尽管小问题不断,但我们还是很喜欢它。起码过半的时间它很好用,所以这个决策还是正确的。但本文要谈的是剩下的那不到一半的情况。

 

首先声明,本文的主题并不是“SwiftUI准备好投入生产了吗?”因为我的答案很明确,这是肯定的!起码我们的情况是这样。我们的应用还是要满足非常复杂的需求而设计的(日历应用一般很简单,但我们不想做那种没什么用的东西)。换句话说,如果你构建的是没什么负载压力、比较省事的东西,那我打赌它用起来基本上会更舒服。我们最终成功了,但也做出了很多妥协。SwiftUI 本可以——而且真的应该——变得更易用,特别是考虑到它自 2019 年发布以来已经有过三个重大更新了。问题在于我们遇到的问题并不像我们的需求那样复杂,其实都是些基础的东西。

 

免责声明:我们面临的一些问题完全有可能存在我们不知道的解决方案。但总之我们已经查过了,也没找到办法。这里的重点是这些基础的细节本来应该可以正常用下来的。

ScrollView 地狱

 

深吸一口气吧。这是最让我们纠结的控件。日历应用中需要无限滚动功能,在 SwiftUI 里执行这个操作相对简单,但只能平滑向下滚动,因为尝试按需加载项目时向上滚动会导致明显的抖动。我在 StackOverflow 上问过这个问题,浏览量有两千次,很明显没有有效的原生方案。其实我去年在 WWDC 实验室和一位 SwiftUI 工程师说过这个问题,他们的建议是:1)创建一个 LazyVStack,这就要在两个方向上都做一个大得离谱的数据集;2)滚动到 Today 的 onAppear。这个办法很有创意,不幸的是 scrollTo 在 LazyVStack 中表现不可靠。它甚至经常会远离目标,偶尔会稍微偏离目标,很少落到正确的位置。还好我们最后找到了 Marc Palmer 的这篇精彩文章《你的 SwiftUI ScrollView 是否滚动到了错误的位置?》。引用其中一段:

 

最后我把它隔离了。如果 ScrollView 中有 ForEach,并且要滚动到的视图的 ForEach 主体结果包含其他视图,则 scrollTo(id...)不会滚动到有 id 的视图框架。

 

竟然是这样?

 

他的办法能用,但还是不够可靠(我们会用非常繁忙的日历数据做用户体验压力测试)。我们不得不重新设计,接受现实:在 Agenda 视图里,很遗憾目前你无法回到过去。

 

遇到这种情况的时候你有三种选择:战斗、认输或重新评估。我们很顽固,不肯认输。我们的建议是为战斗设定时间限制,不要拖延,确保开发可控。建议大家基于当前的限制重新调整设计,这样用户就不会察觉到出现了什么局限,而且你的解决方案会让他们感觉是有意为之的!我们解决这个问题的办法就是引入了追溯元素。

 

但 SwiftUI 的 ScrollView 还有其他一些痛点,其根源在于它无法优雅地处理冲突手势,也就是说滚动可能会因为干扰手势而中断,反之亦然。例如,我们希望每个日历项都可以点击和长按。这是开箱即用的功能,但 onLongPressGesture 的最短持续时间被框架忽略了,结果打开速度慢得发指,也没有干净的解决方案。还好 Daniel Saidi 最近提出了一个方案:他的 ScrollViewGestureButton 用了一个隐蔽的 ButtonStyle 来搞定了。类似地,我们的 Day 屏幕有一个水平分页器,该分页器由多个相邻的垂直滚动视图组成。为了让滚动和水平滑动同时工作,Ciaran O’Brien 还巧妙地使用了一个 ButtonStyle 来创建人为延迟,因为在 UIScrollView 上没有 UIKit 的 delaysContentTouches 等效项。

 

除了 ScrollView,我们还经常面临性能和状态方面的问题。当然我们的麻烦更有可能是自己造成的,我们严重依赖 Observables。所以说如果你的架构基础没那么牢固的话,中等复杂度的应用很容易遇到问题。视图经常无意义地刷新,在可以无限滚动的日历中这将导致明显的卡顿。你离触发一个 @Published 属性总是一步之遥。对于这个问题,Martin Mitrevski 的《SwiftUI 性能技巧》这篇文章是必读的。Oskar Groth 最近发的一则技巧推文也引起了共鸣。我们的办法是不再默认随意创建 Publisher,而是加了一堆条件:

 

  1. 如果在更改特定属性的值后必须刷新视图,请考虑为其提供一个 @Published 属性 wrapper。

  2. 如果没有其他可能同时更改的属性也需要刷新视图,则继续执行 @Published 分配。

  3. 如果有这样的属性,则寻找是否有更统一的方法来发布更改,比如说一个单独的 Publisher,一旦导致对象属性发生变化的活动完成,其值就会发生变化。或者采用 Martin 当时对 objectWillChange.send()的建议。

 

同样,@EnvironmentObject 很好用,但这种便利本身也带来了麻烦。如果一个视图引用任何内容,请确保它需要响应其中包含的每个 Publisher 的更改。否则,考虑把引用替换成对它完全关联的一个新对象。

 


之前:当我们大量使用 Publisher 时,滚动时有一大堆时间线插入内容

 


之后:时间线上的 Publisher 数量显著减少

 

一般来说,如果你像我之前那几次一样让自己陷入困境,请不要重蹈我的覆辙:就像我一开始天真地设置一堆不必要/非最优选择的 Publisher 一样,我的做法跟正确路径背道而驰,把那些看起来不重要的东西都删掉了。在你移除任何东西之前,请先研究这个属性的轨迹。我有几个移除示例并没有立即产生什么明显的后果,结果一段时间后视图没有响应特定场景中的变化,这时候问题才浮出水面。最后强调一下,请记住 @EnvironmentObject 将触发一个视图更新,即使视图没有引用其任何属性也是如此。一种找出不必要的重绘的廉价方法是将视图的背景颜色设置为 Color.random,这是 Peter Steinberger 给出的巧妙方法

 

TextField 也值得一提。我们遇到的一些新的磨合例子如下所述。

 

  1. 我们想移动入口 UI,打开一个表并立即关注标题 TextField,但是在弹出键盘之前有明显的延迟,所以这个操作取消了。

  2. 我们支持在不失去键盘焦点的情况下键入一个新条目并点击[Next]添加另一个条目。iOS 16 中的一个新错误意味着当你这样做时,键盘会在滑回原位之前就出现关闭动画。当前版本中保留了这个设计,因为我们觉得放弃添加连续项目的能力会比这个 UI 问题更让用户感到不爽。与此同时我们正在寻找解决方法,目前预计要到夏天才能解决问题。

  3. 当你在编辑条目时,我们希望标题字段的光标位置在开头,结果也做不到。

 

虽然有这些问题,但我们还是坚持使用 SwiftUI,对它也抱有敬意。我们经验丰富,不会让一些挫折,哪怕是频繁出现的挫折影响我们对整体体验的判断。考虑到我们的问题都很明确,相信它们最终都会被解决掉,只是具体的时间就没法预测了。与此同时,每当我们遇到障碍时,我们都会好好战斗。

 

timing.is 现已上架 AppStore,由 SwiftUI 打造。可在Twitter网站上关注作者了解更多开发经验。

 

原文链接https://blog.timing.is/swiftui-production-experience-problems-solutions-performance-tips/

 

2023-02-27 19:2712796

评论 1 条评论

发布
用户头像
Reflecting on the valuable insights from this article about SwiftUI production code, it's essential to recognize the significance of choosing the right tools for your software development needs. For businesses considering a robust ERP solution, exploring options like those offered by [ImpactFirst](https://www.impactfirst.co/id/erp/software-erp) can lead to better integration and efficiency. Balancing pros and cons is crucial in both development and enterprise resource planning.
展开
2024-07-25 16:28 · 印尼
回复
没有更多了
发现更多内容

极客时间算法训练营Week01

jjn0703

算法训练营

架构实战营-模块五

瓜子葫芦侠

「架构实战营」

微博评论的高性能高可用计算架构

deng

架构实战

字节跳动面试官:SpringBoot统一接口返回和全局异常处理怎么玩?

热爱java的分享家

Java 架构 程序人生 编程语言 经验分享

Elasticsearch写入数据的过程是什么?以及是如何更新索引数据的

热爱java的分享家

Java 架构 程序人生 编程语言 经验分享

模块四作业-redis 存储方案设计 - 学生考试试卷

Geek_cb2b43

Pulsar VS. Kafka(2): 以Segment为中心的架构

Apache Pulsar

kafka 架构 分布式 Apache Pulsar 消息中间件

微博评论的高性能高可用计算架构

deng

架构实战

【LeetCode】N 叉树的最大深度Java题解

Albert

算法 LeetCode 11月日更

明道云对接企查查,一键矫正客户信息

明道云

架构实战营模块四作业

孙志强

架构实战营

微服务架构中,二次浅封装实践

架构 分布式 微服务 技术栈 二次封装

考试试卷存储方案

天天向上

架构实战营

如何通过抓包来查看Kubernetes API流量

Robert Lu

golang #Kubernetes#

调优达到上限?这份尊享版性能实战套餐,让你领先别人好几个级别

热爱java的分享家

Java 面试 程序人生 编程语言 经验分享

遇到了几道关于作用域的前端小题

空城机

JavaScript 大前端 11月日更

nginx配置详解

小鲍侃java

11月日更

System.Text.Json自定义Conveter

喵叔

11月日更

Hive基本理论和常用函数

犟马骝

架构实战营 - 模块四作业

危险游戏

架构实战营

🏆【Alibaba中间件技术系列】「RocketMQ技术专题」让我们一起实践RocketMQ的服务搭建及配置操作

码界西柚

RocketMQ 11月日更 Apache RocketMQ 集群搭建

你敢相信?我用了3个月成功破茧成蝶,从简历被拒到收割8个大厂offer

热爱java的分享家

Java 面试 程序人生 编程语言 经验分享

Pulsar VS. Kafka(1): 统一的消息消费模型(Queue + Stream)

Apache Pulsar

kafka 架构 云原生 Apache Pulsar 消息中间件

先睹为快即将到来的HTML6

devpoint

JavaScript html5 11月日更

记一次提升18倍的性能优化

捉虫大师

性能优化 Go 语言

真香!Github一夜爆火,阿里性能优化不传之秘终于开源

热爱java的分享家

Java 程序人生 性能优化 编程语言 经验分享

终于有人将Github星标百万的Spring技术精髓收录成册

热爱java的分享家

Java 面试 程序人生 springboot 经验分享

不可思议,阿里巴巴首发:Java核心框架指导手册,竟1小时点击量破千万

热爱java的分享家

Java 面试 程序人生 编程语言 经验分享

【高并发】深度解析ScheduledThreadPoolExecutor类的源代码

冰河

Java 并发编程 多线程 高并发 异步编程

redis sentinel 设计考试试卷

云里雾花

redis sentinel

模块四作业

Asha

「架构实战营」

写了3万行SwiftUI生产代码后的经验:凡事皆有利弊_语言 & 开发_Bardi Golriz_InfoQ精选文章