这是一个怎样的应用?

这是一个这样的移动端App。

What is Unsplash?

Unsplash是我的非常推荐的一个高质量无版权照片共享网站。(具有一般意义上的共产主义精神)

当时读研究生的时候,总是苦恼于一些高分辨率的装饰图片从哪里下载比较好。尝试了Flicker,Pexels,StockVault等站点后,总觉得不够满意,他们要么图片质量不够严格,要么具有广告标识,有的还具有严格的版权要求。

直到遇到了它 - Unsplash.com。
在这里引用一段网站上关于版权的描述:

Unsplash photos are made to be used freely. Our license reflects that.

All photos can be downloaded and used for free

Commercial and non-commercial purposes

No permission needed (though attribution is appreciated!)

所以,目前Unsplash的图片资源都是可以无偿使用的,并且可使用在商业领域(这点很关键,以免邮箱里出现很多律师函)。来自世界各地的摄影师,拍摄各种各样的高质量高清图片(低质量的图片是通过不了后台审核的),并在这里免费共享给互联网上的所有人。您在我的博客的记事中看到的图片,都大多出自于Unsplash。

由此,Unsplash成为了我最喜欢的图片资源站点。
我也上传过一些自己拍摄的照片,其中有一些获得过“Editorial”标识。标记为“Editorial”是后台的编辑们选出的一些优质图片,并周期性的呈现在Unsplash的首页上。

欢迎各位看官姥爷在这里戳这进入我的链接

Monotone

Monotone,这个想法最早萌生于Dribbble(追波,设计师都知道)上的一份Unsplash UIKit作品,点这里可以看到。
在看到这副作品后,就想要把它从设计稿做成一个真正的App。

最让我惊叹的是:

  1. 它包含了整个APP所需要的40个页面,要知道,Dribbble以纯展示为主,上大多数都是一些一两个页面,甚至一两个组件的设计作品,这份具有40个页面的设计作品绝对上是一位重量级选手;
  2. 符合工业化流程标准,在审阅了这副设计作品中的各个页面和组件后,设计理念自洽,很多部分的重用性很高,符合软件开发的设计规范,不会给后续设计与开发造成不一致的困扰;
  3. 这位设计师显然具有深厚的实践经验,界面交互也点到即止,恰当好处得找到了一个设计与开发的平衡点。

面对这么一份完美的作业,作为一位移动端的开发者,这简直是梦寐以求的设计师递交出的一份设计稿,没有任何可以挑剔的地方。

于是,命名为Monotone的开源项目,正式立项动工了。

从第一个Commit开始

当时的最初的想法是,相比以前学生时代的APP(虽然我已经尽可能地去尝试采用正式的流程完成了),我想在Monotone的开发作业中,实践一个完整的,工业化的流程,前期规划,质量控制,版本迭代,设计保真都需要去踩一踩。

Monotone的开发最早是从2020年的10月开始,在一个风和日丽(?)的上午,创建了第一个commit:

 

简单规划了一下项目中需要用到的类库,询问了自己数个问题:

  • 网络请求使用什么?Alamofire
  • 界面布局使用什么?因为我经常在工作中使用Masonry,所以这里我也选择SnapKit
  • 决定使用响应式编程处理数据流,所以我会引入RxSwift(虽然之前我没有正式使用过它);
  • 会使用到网络图片缓存,稍后会引入Kingfisher
  • 会使用到JSON序列化,稍后会引入SwfityJSON + ObjectMapper(这个也没有用过);
  • 一些其他的工具类库,使用的时候查找到了再加也没有关系。

遵于设计

上面提到的Unsplash UIKit,作者说明是可以免费下载并随意使用的,我们将会基于这份设计稿来进行APP的搭建工作。

标注稿

首先,作为开发者比较关心的是各个元素的属性,比如绝对位置,相对位置,尺寸大小,颜色,字体等。对于元素的标注,在这里使用的是Zeplin,这是一款交互软件,可以为开发者呈现更多的设计信息,从单纯的图片格式或者Sketc源文件中解放出来,进行设计和开发之间的协同作业。

它的官网在这里,并且提供了免费版的订阅,虽然只支持单人项目,不过目前对于不需要跨团队作业的项目,已经远远满足了要求。

Zeplin可以导入Sketch格式的文稿,并且进行呈现:

 

以上就是由Unsplash UIKit导入Zeplin后展示的内容,接下来比较重要的只有一点:对于Sketch中被标记为“Exported”的图层元素,在Zeplin中都可以被选中查阅其属性。

就像这样:

 

这样做的好处很简单:

  • 标注这一操作的步骤少,设计师基本不需要很多工作量来完成;
  • 需要关注的元素少,开发者其实不需要关注这么多元素(并且他们有相似甚至相同的属性的情况下)。

把鼠标移动到这个元素上,可以查阅元素的尺寸,与其他元素的距离,样式等。对于开发者,这些都是必要的信息,因为标注稿不一定是响应式的,但在面对不同尺寸的设备屏幕大小时,为了遵循响应式设计使用AutoLayout等进行布局,单个元素上呈现的位置关系,大小关系有助于开发者更好的梳理元素层级,提高从设计稿到应用程序的还原度。

 

还在读研究生的时候,我使用的是这样的版本:

 

上面这张图是PS+Specking(一个标注用的脚本)做出来的标注稿,基本操作都在PS中进行。就是选中元素,然后对应生成样式块,或者选中两个元素,生成两个元素之间的距离等等。后面的绿色一列一列的是boostrap前端框架的网格系统,用来参考元素放置的位置。

老实说,这样的标注稿极其麻烦。作为设计师时的操作繁琐,且PS对于UI设计稿真的很难用(又卡)。输出标注稿的时候,漏了什么还得补上,有时候信息也放不下。好在生产力的进步,有了更为方便的应用程序。

Style Guide

刚才提到过的,这份Unsplash UIKit的作者显然是一位与开发合作过数年的老练的设计师,具有很多可重用的样式和元素。

Zeplin可以通过命名来进行颜色的语义化(Semantic)归档,经过简单的整理,颜色表示的范围就变成了如下寥寥数种:

 

为了同时照顾到后续的深色模式(尽管这篇Unsplash UIKit中没有涉及深色模式),需要预先建立起色彩方案(这里命名颜色盘,ColorPalette)类型。参考之前的写的文章,我们很容易书写出以下代码:

 

100行代码,就包含了本项目工程中所有用到的颜色,并且支持了深色模式的自动切换。
除了颜色部分,还会有一些可以重用的UI元素(TableViewCell,SegmentedControl等),他们会跟随整个工程项目的推进逐渐建立。

选择代码还是StoryBoard?

我是一个喜欢新技术,新方案的开发者,但是这次我选择纯代码布局的形式
在之前的个人项目中,我使用过StoryBoard进行界面布局,但却极其灾难。Xcode中的StoryBoard的支持并不能让我满意,特别是针对这种定制化要求高的App而言,是省不了需要写UI代码的,不光是布局,还有样式,甚至是状态切换,动画等。如果使用StoryBoard,大部分场景下一半的样式设置会在StoryBoard中,而另一半就放在VC中,我不太喜欢这样做。

同时,工作中使用的也是纯代码布局,毕竟StoryBoard在Git中解决冲突也是很愁人的。而纯代码布局虽然看上去繁琐且与Apple的推荐不太相符,但是在大型项目中的优点实在是太多了。SwiftUI应该可以较好的缓解这个问题,但是目前还没有进行应用。

子视图

在这个项目的搭建中(很多项目也一样),基本由业务实际自动归类了两种类型的子视图:

  1. 重用性高的:UITableViewCell,UIButton,UITextField等;
  2. 重用性低的:UIView等。

重用性高的视图当然是越多越好,但是免不了有些重用性低的视图,他们大多数直接或者间接地继承自UIView,而他们的存在也更好的组织起了ViewController中的代码,而避免ViewController变成厚厚的一层,以缓解压力。

并且,需要注意的是,为了更好的遵循RxSwift的设计原则,子视图的对外交互基本由RxSwift进行实现:

架构

整体基本架构是大家耳熟能详的MVVM,使用数据去驱动视图。

ViewModel怎么写?

其实我最早接触ViewModel这个概念的时候,是巨硬家的ASP.NET MVC 5.0(8年前是5.0,现在已经被叫做ASP.NET Core了)。这是一个优秀的后台框架,具有很多先进的设计思想,其中有个概念就是“强类型视图”。对于每个需要渲染的.cshtml(=ViewController),在上方声明一个ViewModel,通过Controller灌装数据给ViewModel,然后用ViewModel内的数据来页面的行为,就是“强类型视图”。

把这个行为放到iOS中,也就是说:

  1. 对于每一个ViewController,对应一个ViewModel,不管数据量的多少,One2One
  2. 对于每一个ViewModel,数据流向永远是Controller -> ViewModel -> View

 

而基于RxSwift,与参考了这篇优质的文章 - RxSwift + MVVM:How to feed ViewModels?,我选择把ViewModel写成下面这样的结构:

 

每个ViewModel都有输入输出,根据业务本身来决定。
输入(Input)中的数据结构非常简单(必须要做到简单),一般是id,page之类的基本数据结构,或者是上层传进来的较复杂数据结构,并搭配一些Action来触发一些特定的行为,当数据产生变动或者Action被触发时,在输出(Output)中便会有相应的值生成。这种特定的模式使数据流向变为了单一的形式,以免把ViewModel或者ViewController的交互变得复杂了。

规范化的网络请求

上面说道,网络请求使用了Alamofire行处理,当然这是最基本的,我们还需要根据自身业务情况做出一些变动。

在这里,我参考了一下工作中使用的工程项目中的设计结构:

 

所有的请求都走这一个Request函数就可以,请注意到这是一个典型的网络异步且遵循了RxSwift的设计规范的函数。这是一个较为底层的函数,在服务端的响应返回后,做出一些基本的数据整理,错误处理,然后直接返回给调用方,让业务决定到底怎样处理返回的数据。

这是我写过的一个比较干净的的网络请求函数了,为什么能这么做呢?
因为官方API的做得十分规范:Unsplash API Documentation

官方提供的API基于OAuth2.0进行验证,每个对应的接口的返回数据格式也非常统一,基本没有特例状况。因此仅仅需要在网络层进行初步整理即可使用。

具体怎么调用呢?举个例子,现在我们要调用接口请求图片列表。

这里表现的就是一个请求存在的形式,他们分散于这三个文件中(为了方便给各位观众老爷观察,这里先写在一起):

  • ListPhotosRequest.swift
  • ListPhotosResponse.swift
  • PhotoService.swift

于是,很容易梳理出它们之间的结构与调用关系:

  1. ListPhotosRequest和ListPhotosResponse是一一对应的;
  2. PhotoService中的listPhotos函数负责调用它们;
  3. 进行网络请求时,实例化一个Request,把数据灌装进去传递给上面的Request函数;
  4. 返回网络请求时,实例化一个Response,把JSON灌装进去进行解析并返回。

对于每个网络请求,只要新建几个文件,并执行调用(它们的结构都很相似)就可以了,非常简单。

路由跳转

路由管理一直是我之前比较陌生的一块内容,因为到目前为止的移动端应用开发,都没有较好的实践过规范化的路由管理。
一直是仅仅通过调用navigationController.push之类的(虽然他们也能满足业务需求)。

刚好前几个月,阅读并翻译了这篇文章:RxSwift — Coordinator Pattern done right™。这篇文章的主要描述就是Coordinator Pattern该怎么书写代码,怎样应用到自己的工程项目中。
于是想着Best Practice的原则,在新开的坑中好好地实践了一把。

Coordinator Pattern,把松散在各地的navigationController.push,pop以及present等调用进行了集中管理。文章中在SceneCoordinator同级声明了Scene和SceneContent枚举,对应各个Scene(ViewController)。并且,在该类中实例化对应的ViewModel实例对象,并灌装给了ViewController,并再交给NavigationController来进行进行呈现。

至于呈现,方式也可以进行划分,比如有push,pop,present(以FullScreen或者Modal的方式),root(整个替换最上层的VC)等。路由相关的代码内容比较多,这里就不贴了,具体请参考Coordinators目录下的内容。

其他内容

本地化

在这个项目内,主要支持了英语与中文这两项的本地化,总数大概是140个字符串左右。
这些字符串是用来在UILabel等控件内表现的NSLocalizedString的内容。

但是,对于隐私&许可证,Unsplash API没有提供一个接口来获取本地化的内容。它们的存在是以静态网页的形式,所以我的方案是以静态资源的内容将其放在App中,并以WebView的形式进行呈现。这是我在工作中学到的一个小技巧,对于可控数量的静态H5内容,是可以这么做的。

于是,我机翻(┌(。Д。)┐,因为太长了)了一下英语版本的许可证内容,并以代码的方式进行动态调用。

Animation & Transition

优雅的App总是有精心设计的过渡效果。
而Animation与Transition又是两个截然不同的概念。

  • Animation:在页面内的,由用户的行为主动触发的动画效果。
  • Transition:在页面与页面之间的,由跳转而自然发生的转场效果。

在这里,我使用了anim来简化一些控制Animation的语句,但是还是需要手动书写的,这是制作动画效果所不可避免的。
至于Transition,我非常推荐ViewAnimator这个第三方库,这是一个高度自动化的提供页面转场动画的库,与Hero有些相似。

在Unsplash UIKit之前,这位设计师制作了数个视频来对App和其动画效果进行了整体的展示,点击这里可以看到链接。因此我使用了这几个视频中的内容进行了参考,并补充了一些自己的理解。

总结

不知不觉这篇文章已经写了6千多个字了(还不包括一些代码!)。总的来说,对于一个现代化的应用程序,作为一个单独的开发者需要实打实做的工作量是非常多的,并且要求开发者了解不同领域(设计,资源等)的前置知识。

这是一篇概览文章,主要用来描述Montone的开发过程中,主要工作的分布点。
而接下来的文章中,我会更详细的拆解一些Monotone中的设计理念,并且进行复盘,思考一下如何设计的更为优雅(时臣脸)。