制作swift式的UI

  • 原文及英文演讲地址 - 做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函数。

data

一般都是从后台获取数据或发生错误,但下列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并不是被强制性编译的,所以拖线时稍有不甚可能产生各种崩溃的问题。

纯代码布局

main.png

我目前并未用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 mode.png

使用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来区分表示。但这个方法并不是那么理想;首先会使我们多加载不必要的数据。

view mode2.png

假如把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的逻辑。

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,093评论 4 62
  • 大学 我有一个同学 他整日喝酒 诅咒这操蛋的大学 我问他你为什么不努力 他冷哼一声 呷了一口啤酒 我的曼倩卡 我的...
    深绘里的小小人阅读 126评论 0 0
  • 很久很久以前,在美丽的大陆上,有一个不知名的国家。这个国家里面有一个国王和一位王后,当然了按照童话书来说,他们应...
    飒风蕊菊阅读 372评论 0 0
  • You are given two linked lists representing two non-negat...
    _SANTU_阅读 101评论 0 0