- 原文及英文演讲地址 - 做Swift式的UI
- 本文为翻译文
原英文视频演讲内容:在更加了解swift语言结构及属性的前提下,简化UI代码。此演讲回顾了UI作业中经常遇到的问题,并提出避免失误的方法。特别提及了一些枚举类型,及比较有用的第三方框架Swift库、模型化等,并介绍了把我View状态的方法。
介绍
我是-------,希望通过本次演讲向大家分享我在工作中总结的代码模式,并分享利用swift语言本身的特性,改善代码结构的方法。我将通过一个利用Star Wars API显示star wars电影列表的示例程序来做具体说明。
schrodinger's result -- 薛定谔结果
从服务器获取数据
func getFilms(completion: @escaping ([Film]?, APIError? -> Void) {
let url = SWAPI.baseURL.appendingPathComponent(Endpoint.films.rawValue)
let task = self.session.dataTask(with: url) { (data, response, error) in
if let data = data {
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
if let films = SWAPI.decodeFilms(jsonObject: jsonObject) {
completion(films, nil)
} else {
completion(nil, .decoding)
}
} catch {
completion(nil, .server(originalError: error))
}
} else {
completion(nil, .server(originalError: error))
}
}
task.resume()
}
上述代码是一个普通的从后台获取数据的代码,若获取成功,则回调complete函数 ; 或获取失败时将数据放入nil,调用fail函数。
一般都是从后台获取数据或发生错误,但下列UI代码,没有与之相符的情况。有四种结果,其中2中说不通。
override func viewDidLoad() {
super.viewDidLoad()
apiClient.getFilms() { films, error in
if let films = films{
//Show film UI
if let error = error {
//Log warning ... this is weird
}
} else if let error = error {
// Show error UI
} else {
// No results at all? Show error UI I guess?
}
}
}
解决方法便是像成功、失败函数一样,改变与后台交互的模型方式。
下面通过Rob Rix名为 Result的框架来为大家进行说明,此框架即简单又能正确捕捉我们的意图。
public enum Result<T, Error: Swift.Error>: ResultProtocol {
case success(T)
case failure(Error)
}
枚举型及相关值的参考
在没举类型中,有成功和失败两种可能,若获取成功,得到的变不是可选类型,而是结果T的对象,无论是哪种类型的数据都一样;获取失败时,得到的也不是可选类型,而是error对象, 只处理着两种情况即可。
func getFilms(completion: @escaping ([Film]?, APIError? -> Void) {
let task = self.session
.dataTask(with: SWAPI.baseURL.appendingPathComponent(Endpoint.films.rawValue)) { (data, response, error) in
let result = Result(data, failWith: APIError.server(originalError: error!))
flatMap { data in
Result<Any, AnyError>(attempt: { try JSONSerialization.jsonObject(with: data, options: []) })
.mapError { _ in APIError.decoding }
}
.flatMap { Result(SWAPI.decodeFilms(jsonObject: $0), failWith: .decoding) }
completion(result)
}
}
task.resume()
}
UI部分做如下处理即可
override func viewDidLoad() {
super.viewDidLoad()
apiClient.getFilms() { result in
switch result {
case .success(let films): print(films) // Show my UI!
case .failure(let error): print(error) // Show some error UI!
}
}
}
利用结果枚举类型,可以更加简介方便的以模型的方式处理与后台交互结果,因此UI界面代码也可以更加简便。
小型layout 引擎
storyboard
一般我在写程序或者团队合作时不怎么用storyboard。�理由有很多,首先在团队合作时,用storyboard会比较困难,光看XML diff里变更项目时有很多并不能明确的显示出来;再有就是解决合并时产生的冲突着实很让人头疼。
其次就是在布局UI时,颜色、字体、样式、间距等要重复设置,这些值都设为固定的常数比较方便,但sotryborad却不支持。
interface builder文件与在代码中的outlets并不是被强制性编译的,所以拖线时稍有不甚可能产生各种崩溃的问题。
纯代码布局
我目前并未用sotryboard而是用纯代码在做自动布局,我在开篇提到的start wars app的主页面是一个tableView,其大小与父控件相同。
以下代码展示了我如何用ios 9 layout anchors布局。
为使代码更加简便,可读性更高,我使用了另一个框架Robb Bohnke的 Cartography.
使用Cartography做布局,可以让约束像声明一样,代码简单明了。
init() {
super.init(frame: .zero)
addSubview(tableView)
// Autolayout: Table same size as parent
constrain(tableView, self) { table, parent in
table.edges == parent.edges
}
}
下面是使用Cartography做一些更加复杂的布局,此处为对tableview cell进行的设置,通过观察下列代表,我们可以迅速把我通过代码对UI做出了哪些约束,这也是我觉得使用纯代码布局的一个优点。
private let margin: CGFloat = 16
private let episodeLeftPadding: CGFloat = 8
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(episodeLabel)
contentView.addSubview(titleLabel)
constrain(episodeLabel, titleLabel, contentView) { episode, title, parent in
episode.leading == parent.leading + margin
episode.top == parent.top + margin
episode.bottom == parent.bottom - margin
title.leading == episode.trailing + episodeLeftPadding
title.trailing <= parent.trailing - margin
title.centerY == episode.centerY
}
}
使用episode参数来调节UI的间距。
利用Cartography布局不仅可以发挥swift operator overload的特性,而且可以用纯代码流畅简洁的布局。
View 状态
使用view来加载数据时,普通可以分为3种情况
- 数据加载中
- 成功获取数据
- error时,在()中写在error时view的状态
以下代码展示了如何处理不同的view状态。
/// MainView.swift
var isLoading: Bool = false {
didSet {
errorView.isHidden = true
loadingView.isHidden = !isLoading
}
}
var isError: Bool = false {
didSet {
errorView.isHidden = !isError
loadingView.isHidden = true
}
}
var items: [MovieItem]? {
didSet {
tableView.reloadData()
}
}
为区分view状态,一般使用isLoading或者isError来区分表示。但这个方法并不是那么理想;首先会使我们多加载不必要的数据。
假如把isError和isLoading设置为true,我们其实并不清楚现在view处于哪种状态,是在加载还是发生错误。view有三种状态,其中两种与数据有关。
解决方法是,可以使用与相关值有关的枚举类型
final class MainView: UIView {
enum State {
case loading
case loaded(items: [MovieItem])
case error(message: String)
}
init(state: State) { ... }
// the rest of my class...
}
可以在初始化需要的view状态,初始化后view一般只能有一个状态。
所有的view管理都从这里开始。在调用getFilms之间,设定 ViewState的 loading状态,之后在根据结果设置 loaded或 error即可。在一个地方集中处理view的逻辑。