前言

最新突然突发奇想先做 Objective-C版本的SwiftUI这个仿生库 OCUI。发现其实困难还是很多的,比如基于Swift语法的语法特性可以让语法特别简洁,但是OC就显的十分的臃肿了。

  • 一个简单的横向文本居中

    HStack(^{
        Text(@"Hello World!");
    });
    

虽然居中大家都知道,可以用Masonry直接设置Center居中即可,但是却不适合其他的场景。

对于SwiftUI布局通常有下面四个

  • VStack

    纵向布局

  • HStack

    横向布局

  • ZStack

    垂直布局

  • Spacer

    空隙填充

目前我做的是横向布局HStackSpacer

布局框架示意图

image-20190829093331823

布局基本上是按照上面图所示进行填充的。Spacer只在框架存在,在页面上已经变成布局的约束了。

这个布局思路,不知道从什么地方开始将,不如从 例子从简单到复杂的开始说起吧。

单个文本居中显示

HStack(^{
    Text(@"Hello World!");
});
image-20190829093912352
image-20190829094101704

其实看我们的代码里面是没有出现任何的Spacer布局的,我们可以在内部布局的时候如果缺失左右填充时候,可以进行添加。

我们此时生成布局方案的时候不能按照直接设置下面的伪代码

[label mas_makeConstraints:^(MASConstraintMaker *make) {
	make.center(superView);
}];

我们正常的会按照这样布局的,但是作为一个框架,不可能写无数个布局方案处理,应该有一个通用的布局方案。

既然左侧和右侧的Spacer的长度是按照中间显示文本的宽度自动计算的,那么我们可以在布局之前优先的算出Spacer的具体宽度。

CGFloat spacerWidth = (HStackWidth - LabelWidth) / 2;

我们先拿到父试图的宽度,目前按照会自动充满屏幕,就是屏幕的宽度ScreenWidth。因为UILabel是可以自动计算出合适大小的。我们可以使用UIViewintrinsicContentSize属性得到适合的宽度。

关于生成布局的思路可以把布局拆解出来

  • 布局上下的位置
  • 布局大小
  • 布局左侧或者右侧的位置

布局上下位置

对于HStack支持三种布局分别是下面

  • Top

  • Center

    默认布局类型

  • Bottom

我们可以根据设置的三种类型进行上下布局

if (top) {
	make.top.equal(superView);
} else if (center) {
	make.centerY.equal(superView);
} else {
	make.bottom.equal(superView);
}

关于大小布局

我们对于intrinsicContentSize可以计算出大小的不进行布局,让系统自动约束。对于计算不出来的大小的UIView可以单独调用renderSize协议方法获取试图的大小。

如果renderView的宽度为0,就引出了自动约束试图大小的布局,这个下面讲述。宽度不为0,我们可以设置下面约束

make.width.mas_equalTo(width);

如果renderView的高度不存在,我们就设置为父试图的全部高度。

if (renderViewHeight > 0) {
	make.height.mas_equalTo(renderViewHeight);
} else {
  make.height.equal(superView);
}

关于左侧和右侧布局。

我的思路是这样的

  • 如果和左侧的试图间距已知就按照左侧的进行布局
  • 如果和左侧的试图间距可变就按照右侧已知间距的进行布局
  • 如果和左侧的试图间距可变就按照右侧 如果右侧间距未知就按照右侧约束走
/// 是否存在左侧固定间距
if (leftSpacerFlxedOffset) {
	if (isSuperView) {
		make.leading.equeal(superView).offset(leftSpacerFlxedOffset.value)
	} else {
		make.leading.equeal(superView.mas_trailing).offset(leftSpacerFlxedOffset.value)
	}
} else if (rightSpacerFlxedOffset) {
	/// 是否存在右侧的固定间距
	if (isSuperView) {
		make.trailing.equeal(superView).offset(-leftSpacerFlxedOffset.value)
	} else {
		make.trailing.equeal(superView.mas_leading).offset(-leftSpacerFlxedOffset.value)
	}
} else {
	if (isSuperView) {
		make.trailing.lessThanOrEqualTo(superView).offset(-leftSpacerFlxedOffset.value)
	} else {
		make.trailing.lessThanOrEqualTo(superView.mas_leading).offset(-leftSpacerFlxedOffset.value)
	}
}

两个文本横向居中对齐

HStack(^{
    Text(@"Hello World!");
    Text(@"Hello 张行!");
});
image-20190829112454402
image-20190829112611122

此时我们的代码仅仅是比上一个例子多了下面代码

Text(@"Hello 张行!");

按照上面的布局思路,我们可以想象一下中间存在一个固定间隙为0Spacer。按照这个思路,我们可以给Spacer新增一个固定间隙的属性。从而可以实现下面的效果。

两个文本间隙10并且居中显示

HStack(^{
    Text(@"Hello World!");
    Spacer(@10);
    Text(@"Hello 张行!");
});
image-20190829113255984
image-20190829113319194

两个文本左右对齐

HStack(^{
    Text(@"Hello World!");
    Spacer(nil);
    Text(@"Hello 张行!");
});
image-20190829113437202
image-20190829113553088

我们可以设置最左侧和做右侧的Spacer的固定间隙为0。

一个复杂一点的设置 Cell 布局

HStack(^{
    Spacer(@15);
    Image(nil)
        .size(CGSizeMake(25, 25))
        .backgroundColor([UIColor redColor]);
    Spacer(@10);
    Text(@"WIFI");
    Spacer(nil);
    Text(@"未连接");
    Spacer(@15);
});
image-20190829113847342
image-20190829114334862

可能此时也已经发现了问题,当我设置左侧文本的时候,应该中间的Spacer的间隙需要自动变小。但是其实事实上并没有。

因为开始的时候我们计算出了浮动间隙的具体值,我们设置了最小值。

目前的方案是做了KVO的监听文本Text的变化,从而重新计算间隙的大小,更新约束。

对于重新计算间隙的大小,还有一个复杂的逻辑。

四个文本左右间距为0 怎么让中间的间隙一直保持不变。

HStack(^{
    Text(@"文本1");
    Spacer(nil);
    Text(@"文本2");
    Spacer(nil);
    Text(@"文本3");
    Spacer(nil);
    Text(@"文本4");
});
image-20190829114911218
image-20190829115046184

我们还存在一种情况,就是可能两个文本之间有最小间隙。

假设三个Spacer的最小间隙分别是10 20 30

我们获取可以浮动的宽度就等于

CGFloat floatWidth = width - text1Width - text2Width - text3Width - text4Width;

我们可以算出间隙最小承受的值

CGFloat minFloatWidth = 10 + 20 + 30;

我们可以算出间隙最大的值

CGFloat maxFloatWidth = 30 * 3; /// MAX(10,20,30..) * count;

这个最大的值其实就是大于或者等于就可以把浮动的间隙平分。

if (floatWidth <= minFloatWidth) {
	// 按照最小的间隙更新布局
} else if (floatWidth > minFloatWidth && minFloatWidth < maxFloatWidth) {
	// 就按照优先级最低的按照最小间距布局 默认为最后一个优先级最小
} else {
	// 平分计算进行更新布局
}

一个全部充满的红色试图

HStack(^{
    View()
    .backgroundColor([UIColor redColor]);
});
image-20190829120236761
image-20190829120324371

这个布局其实和刚才Spacer的思路是一样的,但是存在不确定大小的试图,就不允许存在不确定间隙的Spacer存在。

三个试图平分

HStack(^{
    View()
        .backgroundColor([UIColor redColor]);
    View()
        .backgroundColor([UIColor blueColor]);
    View()
        .backgroundColor([UIColor grayColor]);
});
image-20190829121259682
image-20190829121336235

三个试图其中一个设置具体大小

HStack(^{
    View()
        .backgroundColor([UIColor redColor]);
    View()
        .backgroundColor([UIColor blueColor])
        .size(CGSizeMake(40, 40));
    View()
        .backgroundColor([UIColor grayColor]);
});
image-20190829121526417
image-20190829121606143

OCUI 正在测试开发阶段,Api 会尽量和SwiftUI保持一致。但是最后的 Api 按照最后发布为准。欢迎有志之士加入这个项目。