Sucha's Blog ~ Welcome

21年7月17日 周六 21:10

定制 UINavigationBar 的转场动画

最近 app UI 改版,交互设计师给出了 UINavigationController 下 Push/Pop 的转场动画,Push 的时候,fromView 会被缓慢往左推,toView 往左慢慢全部盖住 fromView,盖住之前,fromView 大概往左侧移动了 1/3 个页面;Pop 的时候则相反。

单单这样的转场动画是容易实现的,网上的教程一大堆,但难点在 UINavigationBar 也需要实现同样的交互。问题是 UINavigationBar 的动画是系统提供的,UIViewController 仅仅提供左、右、中的 CustomView,而且就我们的 App 来说,首页是隐藏的,在二级页面才会显示出来。由于 UINavigationBar 是系统管理的,转场动画的框架是没有对 UINavigationBar 提供支持的,得自己做。

网上搜索了一下,美团的技术博客提供了思路,具体我用的方案,在 iOS 14 模拟器上验证了蛮久,加上一些调试,如下:

Push 时,对 fromView 的 UINavigationBar 截图;如果是 hidden 状态则不用截图。toView 的 NavigationBar 是假的,但却是用真的 UINavigationBarItem 来构建的,真的 UINavigationBar,系统还未开始绘制呢。假的 NavigationBar 使用了 UINavigationBarItem 的 leftBarItem、rightBarItem 和 titleView 或者 title string。

这里需要注意将 fromViewController 上面对真的 UINavigationBarItem 的链接进行切断,比如 titleView 给他设置一个空的 UIView,leftBarItems、rightBarItems 给设置空的数组来填充。

fromView 截图的 UINavigationBar 按照转场动画的时长 Push 到左侧,toView 带的假的 NavigationBar 则从右侧 Push 进来,等动画结束,将 UINavigationBarItem 上的数据再拿给 fromViewController 使用,将假的 NavigationBar removeFromSuperview。

Pop 的时候需要区分是按返回按钮的 Pop,还是全局右划可中断的 interactive Pop。

先说简单的按返回按钮的 Pop,fromView 在对 UINavigationBar 截图后往右,toView 带着截图的 NavigationBar 出来,等动画结束,截图 NavigationBar removeFromSuperview 就好,这里比较简单。

可中断的 interactive Pop 复杂一些,举例我们用的是 FDFullscreenPopGesture,它其实是截获系统提供的全局右划返回功能。但系统的全局右划返回,当 UINavigationBar 的 backItem 被设置为 nil 等时候,是不起作用的。这个库是拿到系统手势检测 view,添加手势,检测到后调用系统的全局右划返回接口,发起是可中断的右划返回动画。

这个库暴露出来的检测手势 gesture 实例,我们后面会用到。

可中断的全局右划返回 Pop 跟直接调用 UINavigationController 的 popViewController 很不一样,由于动画可以中断,因此需要在 fromView 上建立假的 NavigationBar,截图就好。

手势的开始,来自于监听上面说到的 gesture 的 target action,可以监听 begin、change、end 事件,并计算右滑的百分比,用于调整截图 NavigationBar 跟随 fromView。难点来了,手势结束的时候,实际上右划动画也许结束,也许会中断返回,具体会如何,其实是系统判定的,我们并不知道后面动画要怎么样的,包括时长多久也都不知道,因此这里,需要建立一个跟踪机制,我用的是 DisplayLink 来做的,模拟器上看,比较跟手,但是太极端的情况,也会偶尔拉跨。

另外上面说到的假的 NavigationBar 将 UINavigationBarItem 给真的 UINavigationBar 时,其实会闪一下;以及截图 NavigationBar 的时机,实际上比转场动画要早,也就是截图代码不能放在转场动画开始的地方,那个地方才截图的话,太晚了。

因此需要在 UINavigationController 的 push、pop 接口地方做截图,另外全局右划的可中断 pop,截图是在手势开始的 begin,因为流程是 begin 开始后,才会调用到 UINavigationController 的 pop。

太多细节细节,不现场调试根本说不清楚,也不晓得会不会在后面的系统更新中变化,这里大体说了思路,以及需要注意的地方。

当然上面也提到过最后的效果,其实蛮不错的,

说一下前提,如果 UINavigationBar 设置了 translant 且真的用上了半透的话,效果应该是不行的,因为系统真实的 UINavigationBar 动画在底下会动,得实际遮住才行,因此这套东西,我是觉得只能用在非 translant 的场景下,当然,如果只是设置了 translant,但实际上又带了 opaque 的背景,仅仅是为了带一丢丢高斯模糊背景的话,是没问题的。

21年7月11日 周日 12:08

PinStackView

目前工程主要使用 AutoLayout,但一些复杂页面上面,先不说各种约束优先级的问题,单单就因为约束不对造成的约束失败,在 debug console 里面打印一大堆,都不好定位在哪里,别说去修复了。

总觉得大部分的场景,其实使用固定 frame 的 layout 就足够了,不需要那么复杂,于是我在工程里面引进了 PinLayout

因为使用 PinLayout 构建复杂页面确实太辛苦,其只用于搭建不复杂的元素,之前使用的是 StackViewLayout,但测试反馈问题比较多,我自己也发现,不大稳定,偶尔还有 crash,毕竟这个库是未完成的状态,然后还自带内存泄漏。

于是就想着自己写一个,毕竟 StackViewLayout 也用了这么久,接口已经蛮熟悉的了,而在功能上面,需要的又不多。

于是就有了 PinStackView,这个库相比 PinStackLayout 来说,简单很多,其实只有一个文件,另外,其处理流程上也是很简单的。

配置

最后增加了一个 autoSizeChangedCallback 的回调函数,如果 style 设置为了 auto,PinStackView 在 axis 上的长度由其 item 动态长度计算得到,每次对 bounds 有更改,都会 callback 通知。

另外上面的设置其实隐含了一些前提,在 fixed style 时,PinStackView 的大小是外部设置的,此时才有 start、end、equal 这些 distribution;而在 auto style 下,只有 start distribution,其实很好理解。

管理接口

下面是管理 view 的接口,添加、插入都会返回 PinStackItemInfo

item 配置

对于每个管理 view,在添加、插入的时候,都绑定了一个 PinStackItemInfo 用来描述其 layout 信息,下面的接口都可以进行链式配置

上面第二点,如果设置了 alignment 或者 alignSelf 为 stretch,会覆盖 item 在 cross axis 的长度

计算流程

建议看代码吧,其实没啥好说的,fxied style 下面,equal 就是平均 axis 的长度,start、end 的计算是最复杂的,因为可以带 grow(),需要先计算固定长度的,后面再分配动态长度的。

auto style 其实相对好计算,计算完所有管理 view 沿着 axis 的长度后,对比 PinStackView 在 axis 的长度,有变化的话就修改 PinStackView 的长度,然后 callback。说一下 callback,这个通知,可以回调到外部依赖 PinStackView frame 的地方,重新设定相关的 layout。

这个当然有可能造成 layout 的循环,之前的 StackViewLayout 就有这样的场景,我在这里将这个依赖交给了用户,用户自己来控制。

PinStackView 里面对于管理 view 的宽高计算是一个焦点,代码在 calcViewSize() 里面

将 StackViewLayout 换成 PinStackView 后,循环 layout 的场景不再出现,layout 也挺正常的,但是公司人员变动太大,貌似这个需求测完了,但上线遥遥无期,再说吧。

21年6月15日 周一 21:15

MoonCake Programming Language

从 4 月底开始就很少玩游戏,因为迷上了另外一件事情,基于 Lua,创建一个语法上类似 Swift 的编程语言,一方面学习 LPeg, 另外一方面,按照自己的想法,对 Lua 的语法修修改改。

基本上,Lua 用了很多的词来分割语意单元,比如 C/C++ 里面的语法块,是放在 '{' 和 '}' 包裹中的,在 Lua 中是 do 和 end 中,或者 then 和 end 中, 当内容一多的时候,都是词;还有就是 function,连 Swift 都是 func 就可以了,为何要一个完整的单词呢。

还有少不了的 local,避免全局变量污染,就只好都是 local 了,我觉得默认就行,总是 local 有点多余。

其实最开始就是上面这 3 点,当然也参考了一下有名的 MoonScript,因为它是 MIT 协议开源的嘛,还想抄来着,结果实际上,从头到尾几乎都是重新构建的, 只有 global name,真的是抄的,加上了 LuaJIT 的 jit 等不多的几个而已。

LPeg 的 P、S、Cg、Ct、Cmt、Cp、Cb、Cf,刚开始的一两天,真的头大,后面就变成肌肉记忆了。

LPeg 的匹配规则有两种,一种是全文完全匹配,得到的是完整的 AST,另外一种是寻找匹配,拿到部分匹配的内容。对于一门编程语言,只能选择第一种。

语言中的 class / struct 当然是基于 metamethod 的,其中,class 被设计为跟普通的 table 一样,可以随机创建 key / value,当然少不了继承;而 struct,想学 Swift,没有继承, 且 key 不能为 nil,为了效率考虑,子类以及实例,都会读取自己的类或者父类,如果不为 nil,就 rawset 到本地,下次访问就快了,但是对于父类不断变动的信息,实际上是获取不到的, 这种情况只有通过类方法来解决。

实际上还加了 extension 的逻辑,使得 struct 不能继承这点其实是可以被突破的,而且 class 和 struct 两者就单单使用上来说,区分没有那么大。

我还加了 guard、switch 关键字,都是 if 的语法糖。

还有 defer 这个关键字,处理比较有意思,实际上我是能做到在任何 scope 都是起作用的,但是我嫌麻烦,而且还读到某 Swift 大 V 直觉认为 Swift 的 defer 只支持 function scope 而导致某开源软件的 bug,当然我之前也是这么认为的,所以最后在这个语言中,我将 defer 限定为了 function scope,毕竟能用就行了,直觉理解不好的我觉得也不大好。

说一下 continue,我在这个语言中也实现了,基于 goto,我的理解 continue 其实就是 goto 的语法糖,为何 Lua 不选择支持不理解。

语言鼓励偏向 Swift 一样基于 class / struct 的数据和方法来编程,也有相关 VSCode 的 extension 做简单的支持,比如高亮、outline,至于 LSP 嘛,那是没有的。

最后爆地址,在 MoonCake.

21年5月24日 周一 22:10

编写 VSCode Extension

如果只是搭一个框架,VSCode Extension 的编写没有想象中那么无从入手,照着文档 VSCode插件开发全攻略 一步一步往下学就是了。

其实我的要求有 2 个

至于 language server 的支持,自然是不敢想的。

语法高亮

先说语法高亮吧,用不到 typescript 编程,只需要会正则就好,但是这个正则的规则,是跟 TextMate 一样的,文档在这里

这个正则默认是行匹配的,不会跨行,如果需要跨行,就需要设置 begin、end capture,另外,这个正则匹配,在设置的时候,就需要指定高亮颜色,偏偏 VSCode 提供的几种高亮颜色其实不怎么够用,我是想抄 EmmyLua 的一些高亮,比如全局变量的高亮,没学会人家是怎么搞的。

一些实践来的经验:

"strings_long": {
	"name": "string.quoted.single.name",
	"begin": "\\[(=*)\\[",
	"end": "\\]\\1\\]",
	"patterns": [
		{
			"name": "constant.character.escape.name",
			"match": "\\\\."
		}
	]
}

侧边栏大纲

这个需要 typescript 编程了,我的 typescript 小学生水平都可以搞出来 outline,想必是不难的。

先吐槽一下,typescript 是要编译成 javascript 才能用的,另外有引入库的话,记得 npm install,否则会像我一样,浪费了大量的时间在这些基础上面。

outline 其实已经是 language server 的范畴了,只不过我们简单处理,将这个抽象借口部署在本地,就是简单的文本处理就好。

不贴代码了,反正可以在网上找到大量的的例子,比如一个行处理,split 空格拿到 token 关键字的 LS,其实不堪用,但是我是没办法,暂时先这么用着了,毕竟海没学会更好的处理方法。

然后呢,最外层的循环用的是变量 i,我是刚开始不小心,里面一层小循环也用变量 i 了,然后这个 typescript 停不下来了,也是无语,没有语法 scope 的吗,难过。

分析 token 语法的结构,是提供给 VSCode 一个带层次的 node 列表,首层是 outline 第一级,如果需要二级的画,比如 class node 里面再塞入 funciton、property 之类的,就是在这个一级 node 的 children 列表里面塞入 node,VSCode 就会帮你将这个 outline 画出来。

对了,这些 node 需要指定类型,比如是 class,还是 function,还是 field,其实还有很多,property,namescope 之类的,但是实话说,只有前面这 3 个是比较好看的,其他的应该是我比较少见,不明白这些个 icon 是想干嘛。

有了上面这些基础,我也终于理解了好些 Lua extension 为何只用上面这 3 个 outline 类型了,另外,我还知道了为何这些 Lua extension 列出了一大堆 local 变量,明明更重要的应该是全局变量呀。

有个坑必须要说一下。

不晓得是不是 outline 默认了一个 node 就是一行的原因,同一行塞入多个 node,会造成接下来的行拿不到内容,我猜测是我小学生水平的 typescript 哪里搞错了,但具体到底是哪里,目前无从知晓。

先这样吧。

21年4月11日 周日 21:23

iOS 的异步刷新

技术细节大概描述一下就好,因为网上能搜到的内容蛮多的,这里主要说一下工作中涉及到的方面。

项目之前因为要 0.5 秒刷新一下部分界面,主要是数据更新,涉及到文字、以及用于标记数值程度的背景色。之前的负责人估计第一版用的是 UILabel,或者就是直接用了脸书的 Texture,就为了更新这一小块内容,居然用了 Texture,不可思议。

整个页面除了这里,应该也有其他的原因,反正有点卡,在 i7 32G 的 14.4 模拟器上,CPU 占用率大概 20%,其他部分算法的原因,在这个范围 -+4% 浮动。

我用 instrument 的 time profile 看了一下,Texture 的 layout 部分占了大头,因为需要更新标记数值程度的背景色,因此 cell node 整个 layout 更新,因为又是 RxSwift drive 了整个 table view items,所以实际上是整个 table view node 不停 layout 更新了。

即便 Texture 用的是 Yoga,但是就这两个变更点的更新,用得着 layout 整个 table view node 吗。

于是上周搜索了一下异步更新的方案,基本上就是 CPU 方案 vs GPU 方案,CPU 方案其实就是 Core Graphics,UIKit 其实就是 CPU 的,而 Core Animation 是 GPU 的,比如 CATextLayer、CAShapeLayer、CAGradientLayer,而且 CALayer 有 drawsAsynchronously 这个属性,设置为 true 后,就是异步线程刷新,文档建议自己试试看。

那么方案差不多出来了,选用 GPU 的方案,固定 frame,数据 drive 比如背景色,只更新相关 cell 背景 layer 的 frame,打开异步线程刷新,这样 layout 的负载会低一些。

另外实操后发现,CATextLayer 其实能力是比较有限的,跟 UILabel 有很大不同

因为需要配置 CTFont,以及自己计算行高后做 vertical center,于是再封装了一层普通 CALayer,将 UIFont 暴露出来,以及默认 vertical center,这个项目暂时不需要 vertical 的其他配置,就先不做这个部分的可选项了。

标记数值变化的背景色更新,用的就是普通的 CALayer,因为 frame 设置后,本来就有 tween 补间动画,只是配置颜色输入时不是 UIColor,而是 CGColor 罢了。

类似 tableview 的 UI 框架部分,用了之前介绍的 StackViewLayout,cell view 可以是任何的普通 view,不一定是需要同一种 view,只要支持 sizeToFits 就可以自动配置行高了。

配置了 rx 数据接口后,塞数据进去也很方便,上述的修改,在 i7 32G 的 14.4 模拟器上,CPU 降了大概 8%,在 6 plus 上降了有 10% 多。

是属于比较明显的改进了。

21年4月02日 周五 20:55

iOS 14 NavigationBar

工程在我进公司之前就遇到了这样的问题,导航栏左右 bar item 在 iOS 13 是好好的,但在 iOS 14 上面,从一级页面 push 进入到二级页面,进入到三级页面,或者从二级页面 pop 到一级页面,都会往中间跳动缩进一下,再回归到正常位置。

这种跳动感觉很突兀,很别扭,但是很难定位原因。新建 demo 工程自己试了一下,iOS 14 下是没问题的,我们有用一些第三方库,其中 QMUI 是涉及比较广的 UI 库,但用 QMUI 官方的 demo 试了一下,iOS 14 下也是好好的。

因为用的 QMUI 版本有点旧了,是 4.1.3 在 2020 年 5 月 release 的,于是早上更新到了 4.2.3,但还是没用,问题依旧。

模拟器断点看了 UI 层级,我们是用 custom view 放到 bar button item 里面的,custom view 的 superview 是一个 _UITAMICView 之类的,它的 superview 是 _UIButtonStackView 大概这个名字,它的 superview 是 _UINavigationBarContentView 这样的,最后才是 NavigationBar。

问题是除了 custom view,其他都是系统的 view,一开始观察,push、pop 的时候,其实是 _UIButtonStackView 和 _UITAMICView 都的 frame 有变化,一开始 frame x 是 0,稳定后 stack frame x 变为 -8,tamic frame x 变为 8,然后我们的 custom view frame x 是 0,刚好跟 navgation bar 对齐。

于是我就用 rxswift 的 observ 监听 _UITAMICView 的 frame,并做修改,发现不管用,因为总是断断续续在忙这个项目(对,其他插入的任务更紧急),第二天才发现无法直接监听的原因,是我自己对 navigation bar 了解不够所致。

navigation bar 的显示,开放给用户的接口是配置 navigation item,其实是个堆栈,最顶层的就是当前的显示配置。而 navigation item 可以配置左右 navigation bar item,还可以配置多个,其中左边是从左到右序列号增大排列,右边是从右到左序列号增大排列。

其 UI 层次规则,是每一个 navigation bar item 对应一组 _UIButtonStackView 和 _UITAMICView。因为 navigation item 左右两边 bar item 是可以动态配置的,因此这一组 view 层级也是动态显示的。

即便一开始就配置好了顶层 navigation item,但是 push 了新的 view controller 进来,顶层的 navigation item 更新后,新的 bar item custom view 会使用新建里的一组 stack view 和 tamic view。pop 后也类似,顶层 navigation item 更新,刚成为堆栈顶层 navigation item 左右 bar item 的 custom view 才建立 stack view 和 tamic view。

所以看到的缩进、跳动然后回归正常的左右 bar item custom view 动画,就是因为这种动态创建容器 view,然后 layout 造成的。

这就解释了之前为什么监听这两个 view 不成功,因为 pop 后才新建立的,之前建立的 observ 被 dispose 了。

继续观察,发现在 push、pop 的过程中,custom view frame 其实也会被设置的,一开始被设置为宽度更小的 frame,最后才设置为正常的 frame,原因我是搞不懂,但是 custom view 一直没有被释放,可以作为一个监听的基点。

于是做了改进,监听 custom view 的 frame,同时监听 .initial 和 .new,根据其在左右 bar items 的位置,配置对于 navigation bar 的偏移,这样就对了。

于是整套方案定下来了,最后结果也还说的过去,大概是继承 UINavigationBar,在建立 navigation controller 的时候,有初始化方法是可以传递 navigation bar class 进去的。

然后在 pushItem,以及 layoutSubviews 的时候,检查顶层 navigation item 的左右 bar items custom view,并将其左边前一个,或右边前一个的 view 记录下来。我是建立了一个 weak to weak 的 NSHashMap 来做记录的,然后建立对 custom view frame 的监听。

每次 frame 有更新,就检查 custom view 的 super view,其实就是 _UITAMICView,同时根据 custom view,找到排在它前面的另一个 custom view 的 _UITAMICView,根据前一个 tamic view 在 navigation bar 的位置,加上 8,作为当前 custom view _UITAMICView frame 的 x。

有点绕口了,但思路是没问题的,UI 表现上也说得过去。

说一下不好定位根本问题的原因,一方面不想做得更复杂了,系统类没有 hook,不好定位其修改点,且另外一方面,custom view frame 的修改,是在 QMUI hook 掉所有 view 的 layoutSubview 里面做的,这个到底是谁修改的,说不通了。

很无奈,不得不找一种半调子的解决办法。

21年3月25日 周四 20:33

Swift AVL Tree 和 SortedDictionary

苹果的 Collection 集合类型,我是找不到链表和树的,至于为什么,也许是底层封装好后,上层透露出来给大伙用,就不用担心了吧。

实际上没有那么美好,举个例子,一个不断更新的整数序列,新进来的整数有可能是做插入,或删除(比如元素不存在,就插入,存在就删除),且每次更新完后,都取最小的几个数字做输出,也就是一个不断求 Least(N) 的可变序列,这样的需求应该不难理解。

如果用苹果提供的数据结构,应该怎么做呢,不管是用 Array、Set 或是 Dictionary,最后的输出,总是免不了做一个 O(N * LogN) 的 sorted,其实我只是取最小的几个而已,这个全局的 sort 未免太奢侈了。

即便 Dictionary 是类似红黑树这样的结构(当然不是),对于所有元素 sorted,拿到所有元素的排序,再取 prefix,其实也是不必要的。

上面的需求,是我抄袭 skywind3000 miniavl 的原因,我将其翻译到了 Swift,然后呢,跟 Mutable Dictionary 结合,就变成了 SwiftSortedDictionary,AVL 树在 SwiftAvlTree

分析一下吧,AVL 树的插入、删除,查找都是 O(LogN),不过因为有 Dictionary 的加持,查找相关节点其实只需要 O(1),所以删除、查找变成了 O(1),当然删除比较特别,也就是做两、三个旋转而已,算 O(1) 了。

最后 Least(N) 的排序是交给 AVL 树来做的,一个闭包循环,加入一个表示序列进度的 index,一个可以控制结束的 inout 修饰的 stop 参数,就行了。

下面是测试的环境,我是在一台 i5 的老 MacOS 上测试的,基数是 256,添加了 512 个数,每次添加取 Least 16,同样的逻辑,循环了 10 次,结果对比如下:

with amount:256 addition:512 prefix:16 loop:10

Test Dicionary:
round 1: 111ms, avg: 111ms
round 2: 109ms, avg: 110ms
round 3: 109ms, avg: 110ms
round 4: 109ms, avg: 109ms
round 5: 109ms, avg: 109ms
round 6: 109ms, avg: 109ms
round 7: 109ms, avg: 109ms
round 8: 108ms, avg: 109ms
round 9: 109ms, avg: 109ms
round 10: 112ms, avg: 109ms

Test SortedDicionary:
round 1: 21ms, avg: 21ms
round 2: 18ms, avg: 19ms
round 3: 18ms, avg: 19ms
round 4: 19ms, avg: 19ms
round 5: 19ms, avg: 19ms
round 6: 19ms, avg: 19ms
round 7: 19ms, avg: 19ms
round 8: 19ms, avg: 19ms
round 9: 19ms, avg: 19ms
round 10: 19ms, avg: 19ms

当然现实中使用也是有收益的,负责的一个不具名的项目,因为要做上述类似的逻辑,之前单独使用 dictionary 结构做插入、删除,最后 sorted 函数在 time profile 排前面,使用了 sorted dictionary 后,耗时在 profile 中是完全看不到了。

不过就我负责的那个项目来说,因为还有其他的问题如频繁 UI 刷新以及 layout 的制约,肉眼可见的观感的改进不明显,但从 time profile 的结果来看,是有改进的,算达到了预期吧。

21年3月24日 周三 22:22

PageEventBus 大改动

上一周,PageEventBus 第一次用在了其他场景,让我不得不重新思考了一下使用 responder chain 隐含传递、关联 event bus 的优势、劣势,最后决定是将这个重要的特性去掉。

先说一下问题,使用了系统 UISearchViewController,然后使用了 result view controller,但是 result view controler 无法通过 responder chain 拿到 event bus,猜测是 search view controller 使用了其他的 window,或是其他的问题。

这样的话,估计如果有应用到其他的一些系统类,使用 responder chain 来拿到 event bus 这种想法,也是行不通的。

于是,做成了一个中心化的架构,默认使用带类型的 event bus 做 key,将新建立的 bus 缓存到一个中心节点中,其他同样类型的 event bus,通过查询中心节点,就能拿到 bus,然后 bus retain count +1,最后当所有持有 bus 的实例销毁后,这个类型的 bus 也会被自动销毁掉的。

这样的修改,对于单页面是没问题的,但是对于使用同样的事件类型,但具有多个实例,在不同的 UI 层次上,需要区分 bus 的情况,就需要特别处理一下了。这个时候,需要修改 agent 中默认的 event bus name,来建立多个不同的 event bus 实例。

21年3月16日 周二 21:04

PageEventBus 实践心得

在这次重构中实践了一把上个月说到的 PageEventBus,说到重构,其实只完成了一半,但是目前的心得,感觉是挺不错的。

因为页面交互很多,然后 UI 层次也比较复杂,有了 PageEventBus 后,少了很多传递数据的逻辑,一些有 UI 层次的 view,比如各种 StackView 包裹的有复杂业务功能的 view,不需要单独开业务参数接口了,整洁,同样的相关 view model 也不需要单独开业务接口了,整洁哈。

因为是在 didMovedToWindow 时候才去找 superview 持有的 bus,之前担心许多 view 下来会不会有什么影响,但实际上需要 findBus 的 view model 没有很多,所以 UI 绘制效率一点都不影响,况且只有第一次需要通过 next responder 来找,找到后就挂接上了,稳得很。

不过也有在之前没有考虑好的,比如 view controller 这个部分,因为 page model 继承于 view model,对于 controller 及 controller 上的 view 是 unowned let 的方式,意味着初始化的时候,就开始访问 controller 及其 view 了,这很不好。

因为 controller 的 view 不应该那么早被导入到内存,一般都在实际需要展示前再让它加载到内存,所以实际上 page model 没法完全独立于 controller,需要在 controller viewDidLoad 后才进行初始化。

所以这算个小遗憾吧,我现在的做法是,做一个可选闭包,在 viewDidLoad 里面执行,确保有了 viewDiDLoad 后,才初始化 page model,这样就好很多了,估计后面会改到 BlockViewController 里面做例子。

另外,还有些遗憾的点是,不复杂的业务,实际上根本用不到这个 event bus;还有就是,如果需要在不同的业务 controller 之间做数据传递,当前的 PageEventBus 又不可以拓展,比如没有相关路由的接口,可以连接两个不同的 event bus。

连接两个不同的 event bus,有时候还是很必要的,当然这个完全可以交给系统的 notification 来完成,但是接口感觉又麻烦了一点,后续感觉可以在这点上继续推进的,虽然现在的工程不一定能够用上,缺少实践心得还是感觉稍微有点不爽。

21年3月15日 周一 22:27

PinLayout 及 StackViewLayout

虽然现在 iOS 开发几乎全部都是使用 AutoLayout 了,诸如 OC 时代的 Masonry 或者 Swift 时代的 Snapit,或者 TinyConstraint,但其实基于 frame layout 还是有一些可以介绍的,就比如 PinLayout 以及 StackViewLayout

这里注意 StackViewLayout 在 dev 分支下才有内容,另外官方的 StackViewLayout 其实有循环引用,我提了一个 pull request,解决了循环引用,并加入了 distribution 的方案,估计没人看了。

PinLayout 很有意思,做为 UIView 的一个计算属性,chainable 方式不断接下来描述这个 layout 的位置、大小,最后在 deinit 的时候,才真正将传递进来的 UIView 进行设置,设置的属性是 center 及 bounds。

因为已经在一个重构的项目中使用了这两套方案,可以说一下了,相对于 AutoLayout,其实是不够方便的,但有些限制还是不错的。不够方便的是,PinLayout 描述的其实是有先后顺序的,就跟 AutoLayout 的 priority 一样,先描述的,PinLayout 会先 deinit,layout 完成后就成了其他 view layout 描述的输入。

自认为虽然不方便,但也算不错的限制是,不像 AutoLayout,只要有 superview 就可以描述,PinLayout 及 StackViewLayout,描述 subview 时,只能在 layout 的过程中,比如 layoutSubviews 或者是 sizeThatFits 里面。

另外在自动计算宽度、高度方面,PinLayout 相比 AutoLayout,要麻烦许多,需要 view 重写 sizeThatFits,限定宽度、高度后,基于这个计算另外的方向。

StackViewLayout 按照作者的说法是大量参考了 Yoga,这个组织其实也有个基于 Yoga 的 FlexLayout。大部分的功能有了,但是却没有 distribution,蛮可惜的,后面我自己加上了一个搓实现,不管怎么样,先有得用再说。

这俩个库的优势当然是计算速度极快,而且库很小,基于 frame 计算,如果没有自动宽高的话,是很安全的。

加上自动宽高的限定后,按照官网的描述,layout 逻辑被单独隔离在一个函数中,在 layoutSubivews 有使用,在 sizeThatFits 也有使用,另外 AutoLayout 用的是 intrinsicContentSize 来计算内容大小。

如果需要适配 AutoLayout,有时是得考虑重写这个函数的,这个函数默认不会刷新,需要单独刷新一下,看 UIView 有介绍。

StackViewLayout 相比 UIStackView 可以做到一些特别的方案,比如因为有 grow 和 shrink,可以玩剩余比例。而且在搭建的时候,因为 chainable 很方便,就都在一个 lazy 属性中全部描述完了,我说的是描述多层嵌套的 StackViewLayout,看起来是很赏心悦目的。

只是具体 Layout 真的得多动一下脑筋,毕竟不是 AutoLayout 是多项式计算的,这里是固定流程的,特别是一些限定了宽度高度的场合,我可是将 subview 高度自动计算完后,设置给 StackViewLayout 的,StackViewLayout 再拉伸其他的 subview。

大概就是这样了吧,还可以用下的,不晓得后面有了 SwiftUI 后会怎么样了,SwiftUI 毕竟还没有用过。

21年3月15日 周一 20:46

西行漫记读后感

也许是从某个微博用户上面读到了有关西行漫记的点点信息,于是就找来看了,读的是微信读书上面的版本,试用了几天无限卡才读完。

这本书名字实际是《红星照耀中国》,但我搜时用的就是西行漫记,到底是从哪里读来的标题忘记了,之前命名为西行漫记,是因为在解放前,国统区其实不能出版红色读物,所以命名为西行漫记是为了好低调传播。

在这之前,有在 B 站上看了沙盘推演里面李得胜的四渡赤水,徐海东等出神入化的战争艺术。

话题转回来,这本书是西方第一次了解到红色中国的窗口,当时斯诺也是费了不少劲才从白区去到红区,因为红区被完全封锁了,留有少许的时间窗口,刚好也应该是在西安事变之前的一小段窗口期,东北军不鸟常凯申,没有围剿红军,而是准备秘密合作,于是有了空隙,斯诺终于找了一个机会去了红色根据地,不过当时还有大财主的民团各地流窜,被碰到会没命,也是很危险的。

斯诺去之前是满腹孤疑,对根据地、红军的方方面面一堆的疑问,第一天就见到了周总理,被总理给安排了 90 天的行程,心里还觉得时间太长了,后面实际花了 120 天,走的时候还恋恋不舍。

里面篇幅其实挺多的,最精彩的,当然是访问李得胜的记录,以及对于红军如何得到广大农民支持的描述。

里面的不少论述在论持久战里面其实也有,当时就很客观,深入的讨论了中国人民抵抗日本的必然胜利,所需要的条件,什么是主要条件,什么是次要条件,什么是内部条件,什么是外部条件,为什么这些外部条件会成立,另外战争会分几个阶段(应该是论持久战里面的了,都混在一起说了吧),然后中国的优势和劣势,日本的优势和劣势,等等。

李得胜说这些话是很有资本的,毕竟国共内战已经 10 年了,经历了五次大围剿,还发展壮大了。

还讨论了具体会用的战略,“战略应该是一种在一条很长、流动的、不定的战线上 进行运动战的战略,快速进攻、快速退却,是一种大规模的运动战”,“我们的战略和战术应该注意避免在战争初期阶段进行大决战,而应该逐步打击敌军有生力量的志气、斗志和军事效率”。

说到得到农民拥护,当时军阀割据,为了养兵,各地都横征暴敛,征税都预征了几十年,国民党代表的是大地主、大资本家、以及国外资本的势力,广大的农民们只能是越来越穷。

为何会拥护共产党呢,是因为土地革命,革了大地主的命,将地分给广大农民,少征税,因此各地都收到广大群众的欢迎,这个国民党是做不到的。这也是红军为何是初期很穷的原因,也是为什么如燎原之火的原因,真的因为各地都是军阀,广大农民活不下去了。

对比一下,李得胜在回答斯诺对印度的看法时,说”印度不经过土地革命,是永远不会实现独立的“。当然印度在 1947 年就独立了,不过应该是类似国民党上台一样吧,代表的是大地主、大资产阶级的利益,现在种性、各地利益分割就可以看到,显然生产力远未得到解放。

估计是年龄大了吧,现在都喜欢看这样的书了。