本文从新手常犯的布局bug,推导出更加适用移动客户端的开发框架,从而得出Flutter和SwiftUI是更加适合客户端开发的结论。

(一)问题描述

在开发过程中,我们经常会遇到这样的逻辑,A和B两个UI,他们之间的布局是有关联的,

比如A如果要展示,B的位置就要相应的调整。

(二)bad case新手示范

如果是新手,很容易根据产品文档的描述写出类似下面逻辑的代码:

if A.isHidden == NO
    B.frame = xxx

这类代码初看是没有问题的,但实际其背后隐藏着巨大的隐患。

我们对上面的方法进行扩展如下:

- (void)udpateAState:(BOOL)hidden {
    A.hidden = hidden;
}

- (void)updateBState {
    if A.isHidden == NO
        B.top = A.top
    else 
        B.top = Navigationbar.height
}

这样你就会发现,本来没什么关联的两个方法: udpateAStateupdateBState,在我们的逻辑安排下必须要求先后顺序了,
也即:调用 udpateAState 之后,必须调用 updateBState ,当然你可能说把 updateBState 放在 udpateAState 方法里不就行了吗?

但实际review代码的时候,竟然遇到忘记绑定导致的各种时序问题。

所以这个方案本质上就是行不通的,即使暂时行得通,其维护成本和后续复杂度也是非常高的(我们只举例了2个UI的关联,如果是3个、4个甚至10个)。

那这类问题一般怎么处理呢?

这类问题比较有效的处理成套方案是: 【数据驱动 + UI约束】。

(三)数据驱动 + UI约束

如果你是Flutter或者SwiftUI开发者,你会很熟悉【数据驱动 + UI约束】这一套开发模式。

相比我们上面描述的写法,A和B这两个UI将不再进行数据上的依赖,而是同事和数据进行关联起来,比如:

// KVO showAState state
self.showAState = NO

if self.showAState
    B.top = A.top
else
    B.top = Navigationbar.height

这样写的好处是只要我们修改数据的状态,那么就会同步更新有相关依赖的UI控件,也就是【数据驱动】。

【数据驱动】这一套在Flutter中是默认的设计方式,Flutter中相关代码如下:

setState {
    showAState = false
}

setState 中包裹的状态量如果有变化,那么Flutter就会自动刷新UI。

解决了UI之间状态依赖的问题,但接下来还有个问题,就是如下的逻辑:

if self.showAState
    B.top = A.top
else
    B.top = Navigationbar.height

你会发现UI之间存在着布局的依赖,对于布局依赖我们自然要拿出【UI约束】这套对口的解决方案。

如果你写过Flutter或者SwiftUI的布局代码,你会发现他们号称的声明式UI的写法,本质上是在底层完成了UI的自动约束。

(四)小结

可以发现,移动客户端发展到现在这个阶段,Flutter和SwiftUI的诞生并不只是语言上更加安全,在设计模式上也更加契合【数据+UI】的客户端布局方式。