iOS开发:屏幕适配浅谈

前端开发的屏幕适配其实算是基本功,每个码农在长期实践中都有自己的总结。

在iOS平台上,苹果爸爸对适配的支持个人感觉很不人性化,提供了autoLayout、sizeClass等技术,感觉没有前端类似flexBox这样的技术来得灵活。像是点歪了技能树,过于重视使用xib配置UI,但很多码农还是习惯纯代码编程。Cocoa没有css这样的纯布局文件,导致很多时候我们将布局、UI、逻辑写在一起,十分混乱,冗长。

下面简单介绍下在实践中适配屏幕的方向思路,抛砖引玉。

从设计到代码:沟通与标准

App的UI界面是由设计人员(产品,UI)绘制的,然后由开发实现,双方要有良好的沟通,并且把设计内容标准化,文档化。

对设计方来说,适配的规则总是在设计师心中的,是按比例的缩放,还是固定的间距,是公用一套规则,还是在大屏下有特殊的布局,都需要有明确方式传达给耿直的码农们。

良好的设计文档是沟通第一步

一般常见的布局方式有:

  • 固定间距:在不同尺寸下,间距总是固定
  • 流式布局:文字,图片等在不同屏幕下流式排布,比如大屏下一行显示四张图片,小屏一行三张,图片尺寸固定
  • 比例放大:间距,文字大小,图片大小等比例放大
  • 保持比值:两个UI元素或者图片的长宽等属性保持一定的比值
  • 对齐:元素间按某个方向对齐

设计师需要将这些布局规则标注清楚,有利沟通,也方便文档化日后追溯。

对于一些通用UI组件,要进行标准化,设计上有利于app风格统一,实现上也方便开发进行必要的封装。

平面设计要标准化

UI的搭建:xib VS 纯代码

苹果一直用xib来标榜他们家app开发的简单易上手:将各种你需要的东西往屏幕上一拖一放,一个UI界面就搞定了,这很cool不是嘛!

xib的优点显而易见:

  • 易上手、可视化,所见即所得
  • 减少代码量
  • 快,适合小app快速开发

但是在我们的实际项目中,是不推荐使用xib的,首先,xib本身过于笨拙,只能搭建一些简单的UI,动态性很差,难以满足app复杂的UI交互需求。

其次,做过性能优化的同学都知道,xib(or StoryBoard)的性能是很差的,相对于用纯代码alloc的组件来说,xib加载慢,而且会占用app包的体积。不仅仅是app的性能,使用老mac打开较大的xib文件,有时候会卡的你怀疑人生,严重影响开发效率(心情)。

除此以外,对于团队协作来说,xib也不是一个好选项:阅读困难,无法在git上查看历史改动,容易造成冲突,造成冲突后难以解决,元素通过outlets与代码的链接难以维护,容易在改动中造成错漏等等。

另外,对于我这种中途转到前端的工程师来说,对一切在IDE界面上配置的东西都有种迷之不信任,感觉不如一行行黑底白字的代码来的靠谱。

当然我们不是完全禁用了xib,用代码码UI的缺点也很明显:繁琐,代码量大,因此对一些元素较多,又比较固定的UI组件,我们可以用xib来减少代码量:

固定的UI组件可以使用xib

针对UI代码繁琐,重复编码多的情况,我们可以通过适当封装(UI工厂类),组织结构(MVC,分离UI代码)等手段,清晰逻辑。

// label 工厂方法
+ (UILabel *)labelWithFont:(UIFont *)font
                     color:(UIColor *)
                      text:(NSString *)text
             attributeText:(NSAttributeString *)attributeText
                 alignment:(NSTextAlignment)alignment;

布局:返璞归真

从iOS7开始苹果在Cocoa平台引入AutoLayout进行UI的基本布局,但是AutoLayout非常反人类,不仅代码繁琐而且使用不灵活限制很多。

比如我想要把三个元素等间距地展示在屏幕上,用AutoLayout写完基本蛋都碎了,更别说动态地在两套布局间切换这种高级需求。

后来苹果推出sizeClass,试图解决多套布局的问题,但是仍然没有触及到码农的痛点,而且依赖xib使它泛用性不好。

看起来很美好的sizeClass

一段典型的AutoLayout代码如下所示:

    _topViewTopPositionConstraint = [NSLayoutConstraint
                                     constraintWithItem:_topInfoView
                                     attribute:NSLayoutAttributeTop
                                     relatedBy:NSLayoutRelationEqual
                                     toItem:self.view
                                     attribute:NSLayoutAttributeTop
                                     multiplier:1.0
                                     constant:self.navigationController.navigationBar.frame.size.height + self.navigationController.navigationBar.frame.origin.y];
    
    [self.view addConstraint:topViewLeftPositionConstraint];
    
    (这里省略上述类似结构*4)

上面省略了很多代码,实际上一页都放不下,干了什么呢,只是将一个元素紧贴屏幕上边缘放置。项目中我们会使用三方autoLayout的封装:PureLayout ,简化代码,也有其它实用功能。

AutoLayout比较适合:

  • 基本的对齐(上下左右对齐,居中对齐等)
  • 固定的布局,固定的间距,动态性不高的页面
  • 简单且数量较少的UI元素

不擅长:

  • 比例布局
  • 动态性较强的页面局部
  • 不同屏幕大小比例的适配
  • 复杂的UI

另外有一点,autoLayout对性能是有损耗的,所以对性能有要求的场景,比如列表中的cell,我们会用代码计算frame,提高滑动帧率。

所以在实际工程中,我们根据具体需要来选择布局方式。

下面是app中首页新闻Feeds的布局代码片段:

- (void)layoutSubviews {
    
    [super layoutSubviews];
    
    CGFloat cellWidth = CGRectGetWidth(self.bounds);
    CGFloat currentY = 0.f;
    
    // 0.content
    CGFloat cellHeight = CGRectGetHeight(self.bounds);
    CGFloat contentHeigth = cellHeight - kCellPaddingHeight;
    _mainContentView.frame = CGRectMake(0, 0, cellWidth, contentHeigth);
    
    // 1. topic
    CGFloat topicLabelWidth = [_topicLabel.text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:_topicLabel.font} context:nil].size.width;
    
    CGFloat topicLabelHeight = [@"测高度" boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:_topicLabel.font} context:nil].size.height;
    
    CGFloat topicLogoLeftPadding = 3.f;
    CGFloat topicLogoWidth = 10.f;
    CGFloat topicLeftPadding = 13.f;
    
    _topicView.frame = CGRectMake(topicLeftPadding, currentY + kTopicUpPadding, topicLogoWidth + topicLogoLeftPadding + topicLabelWidth, topicLabelHeight);
    _topicLogo.frame = CGRectMake(topicLabelWidth + topicLogoLeftPadding, CGRectGetHeight(_topicView.frame) / 2.0 - topicLogoWidth / 2.0, topicLogoWidth, topicLogoWidth);
    _topicLabel.frame = CGRectMake(0, 0, topicLabelWidth, topicLabelHeight);
    
    (省略大量代码……)
    
    // 10._sourceLabel
    CGSize sourceSize = [_sourceLabel.text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:_sourceLabel.font} context:nil].size;
    
    _sourceLabel.frame = CGRectMake(kEdgeHorizontalPadding, currentY + kLeadingUpPading, sourceSize.width, sourceSize.height);
}

可以看到,为了确定每个元素的位置,我们需要进行大量的计算,代码可读性也不好,繁琐难读。如果引入动态性,比如不同屏幕字体大小改变,元素大小按比例扩大等,则计算量又要上一个数量级。

动态布局:清晰独立

UI界面是动态的,在不同状态,不同尺寸或者手机的横竖屏情况下,我们往往需要在多套布局方案中切换,或者对布局进行微调。如果使用xib布局的话,可以使用SizeClass+AutoLayout的方案;如果是代码实现的页面,则没有官方提供的工具,只能用逻辑去判断。

一般来说,我们写复杂的UI页面,需要遵循两个原则:

  • UI布局代码,要清晰:这是最重要的,要一眼就知道在调整那一块,怎么调整,如果不能,适当拆分,优化命名。
  • 布局代码要和业务逻辑独立:在一些常用设计模式下,我们会将UI和数据模型解耦,在UI内部,同样要将交互,配置这些逻辑和布局解耦,独立出类似前端css这样的纯布局文件。

将布局代码提炼出来,在不同尺寸下调用不同的实现:

    if (IS_IPHONE_6){  
        self.layout = [MyLayout iPhone6Layout];
    }else if (IS_IPHONE_6_PLUS){  
        self.layout = [MyLayout iPhone6PlusLayout]; 
    }

    // 实现小屏幕布局
    + (MyLayout *)iPhone6Layout {...}
    // 实现大屏幕布局
    + (MyLayout *)iPhone6PlusLayout {...}

字体适配:字体集

在开发中我们经常会遇到需要动态设置字体的情况:

  • 不同屏幕尺寸,或者横竖屏,需要展示不同的字体大小。
  • 为用户提供了文章调节字体选项。
  • App的不同语言版本,需要显示的字体不一样。
字体大小调节

较为简单的做法是用宏或者枚举定义字体参数,针对不同尺寸的屏幕,我们拿到不同的值:

#ifdef IPHONE6
#define kChatFontSize 16.f
#else IPHONE6Plus
#define kChatFontSize 18.f
#endif

在对一些旧代码做字体适配扩展的时候,直接修改源码改动太多,容易混乱,可以采用runTime方法hack Label等控件的展示,替换原有的setFont方法:

+ (void)load{  
    
    Method newMethod = class_getClassMethod([self class], @selector(mySystemFontOfSize:));  
    Method method = class_getClassMethod([self class], @selector(systemFontOfSize:));  
    method_exchangeImplementations(newMethod, method);  
}  
  
+ (UIFont *)mySystemFontOfSize:(CGFloat)fontSize{  
    UIFont *newFont=nil;  
    if (IS_IPHONE_6){  
        newFont = [UIFont adjustFont:fontSize * IPHONE6_INCREMENT];  
    }else if (IS_IPHONE_6_PLUS){  
        newFont = [UIFont adjustFont:fontSize * IPHONE6PLUS_INCREMENT];  
    }else{  
        newFont = [UIFont adjustFont:fontSize];  
    }  
    return newFont;  
}  

以上套路缺点显而易见:不够灵活,将逻辑分散,不便于维护,扩展性也不好。

一种比较好的实践是引入字体集(Font Collection)的概念,什么是字体集呢,我们在用Keynote或者Office的时候,软件会提供一些段落样式,定义了段落、标题、说明等文字的字体,我们可以在不同的段落样式中切换,来直接改变整个文章的字体风格。

Keynote中的字体集

听上去和我们的需求是不是很像呢,我们在代码中也是做类似的事情,将不同场景下的字体定义到一个Font Collection中:

@protocol XRFontCollectionProtocol <NSObject>

- (UIFont *)bodyFont; // 文章
- (UIFont *)chatFont; // 聊天
- (UIFont *)titleFont; // 标题
- (UIFont *)noteFont; // 说明
......
@end

不同的场景,灵活选择不同的字体集:

+ (id<XRFontCollectionProtocol>)currentFontCollection {
    
#ifdef IS_IPhone6
    return [self collectionForIPhone6];
#elif IS_IPhone6p
    return [self collectionForIPhone6Plus];
#endif
    return nil;
}

// set font
titleLabel.font = [[XRFontManager currentFontCollection] titleFont];

适配新的屏幕或者场景,我们只需要简单地增加一套字体集就好了,可以很方便的管理app中的字体样式,做动态切换也很简单。

总结来说,用代码在一个尺寸实现设计稿是比较简单的,但是要在各种尺寸下忠实反应设计的想法需要合理的代码设计以及一定的代码量。

UI的还原其实也是大前端开发非常重要的部分,作为程序员,往往重视代码的稳定,业务的正常使用而忽略软件界面这个同样重要的用户体验因素。设身处地地想,如果设计看到自己精心调配的比例、字体、色号在不同尺寸手机上显示得歪七倒八,一定会气的要死吧哈哈哈哈哈。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345