Swift Talk #07 Stack Views with Enums

我们使用申明式的方式使用枚举定义UI元素来创建一个抽象视图栈。

我们经常使用栈来存储视图,特别是在原型模式中,因为这样很方便地把视图堆到一起。然而,因我们使用代码创建视图(可以看一下S01E05那一集将为什么我们要用代码创建)我们需要写很多代码来建立栈视图。因此通过抽象化来简化这个过程是有意义的。

在代码中使用UIStackView

我们从创建一个简单的View Controller开始,这也是在viewDidLoad中传统的建立Stack View的方式。

final class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .whiteColor()

        let stack = UIStackView()
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.axis = .Vertical
        stack.spacing = 10
        view.addSubview(stack)

        stack.constrainEqual(.Width, to: view)
        stack.center(in: view)

        let image = UIImageView(image: [#Image(imageLiteral: "objc-logo-white.png")#])
        stack.addArrangedSubview(image)

        let text1 = UILabel()
        text1.numberOfLines = 0
        text1.text = "To use the Swift Talk app please login as a subscriber"
        stack.addArrangedSubview(text1)

        let button = UIButton(type: .System)
        button.setTitle("Login with GitHub", forState: .Normal)
        stack.addArrangedSubview(button)

        let text2 = UILabel()
        text2.numberOfLines = 0
        text2.text = "If you're not registered yet, please visit http://objc.io for more information"
        stack.addArrangedSubview(text2)
    }
}

对于这个图片,我们使用playground's image literals,这个能很方便的从playground's资源中加载图片。这个button还没有链接到action,我们一会做这个。

我们在playground中实例化这个View Controller并且预览这个视图。

let vc = ViewController()
vc.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
vc.view
s01e07-stackview.png

用枚举描述视图

像这样使用栈视图是很方便的,因为这可以替你处理布局,不过还有很多代码要写。这样在创建视图的时候还有很多重复,比如设置UILabel上的numberOfLines属性,尤其是我们app中有很多label的配置是相似的。

一个实现抽象化栈视图的方法是给想要显示的不同的内容类型定义一个枚举。

enum ContentElement {
    case label(String)
    case button(String) // TODO: Add an action
    case image(UIImage)
}

现在我们可以使用ContentElements来创建我们想要显示的描述,并且一会儿把他们转换成view。在我们这么做之前,我们需要记住button需要一个方式和一个action关联。最后我们来看这个。

给不同的ContentElement的不同的元素创建视图,我们用计算属性UIView类型的view来增加一个扩展。我们通过对self的判断来处理不同的情况。对于.label我们返回一个UILabel.我们可以从之前在viewDidLoad中复制一下代码。我们就必须要改变text1名字为label并且使用给.label的关联值替换写死的字符串。我们用同样的方式创建另外两种枚举值.button和.image。

extension ContentElement {
    var view: UIView {
        switch self {
        case .label(let text):
            let label = UILabel()
            label.numberOfLines = 0
            label.text = text
            return label
        case .button(let title):
            let button = UIButton(type: .System)
            button.setTitle(title, forState: .Normal)
            return button
        case .image(let image):
            return UIImageView(image: image)
        }
    }
}

让我们用ContentElement来创建我们要添加到Stack View上面的视图。

let image = ContentElement.image([#Image(imageLiteral: "objc-logo-white.png")#]).view
stack.addArrangedSubview(image)

let text1 = ContentElement.label("To use the Swift Talk app please login as a subscriber").view
stack.addArrangedSubview(text1)

let button = ContentElement.button("Login with GitHub").view
stack.addArrangedSubview(button)

let text2 = ContentElement.label("If you're not registered yet, please visit http://objc.io for more information").view
stack.addArrangedSubview(text2)

在这个image枚举中,代码并没有得到很大的改进。尽管label和button的步骤是好些了。总体来说,这是一个谦虚的进步,但是我们在ContentElements上我们可以做很多。

从枚举创建栈视图

下一步,我们给UIStackView增加构造器。这个构造器使用ContetnElement数组作为参数,并且使用遍历把视图添加到栈视图中。不使用子类化,我们仅仅能给已有的类添加一个便利构造器。

extension UIStackView {
    convenience init(elements: [ContentElement]) {
        self.init()
        for element in elements {
            addArrangedSubview(element.view)
        }
    }
}

现在我们可以删除viewDidLoad中addArrangeSubview的调用并且把内容元素传给新的构造器。

let image = ContentElement.image([#Image(imageLiteral: "objc-logo-white.png")#])
let text1 = ContentElement.label("To use the Swift Talk app please login as a subscriber")
let button = ContentElement.button("Login with GitHub")
let text2 = ContentElement.label("If you're not registered yet, please visit http://objc.io for more information")

let stack = UIStackView(elements: [image, text1, button, text2])

如果我们把元素数据偶读提取出来并且指定变量的一个类型。我们可以把所有的ContentElement的.前缀去掉。

let elements: [ContentElement] = [
    .image([#Image(imageLiteral: "objc-logo-white.png")#]),
    .label("To use the Swift Talk app please login as a subscriber"),
    .button("Login with GitHub"),
    .label("If you're not registered yet, please visit http://objc.io for more information")
]

let stack = UIStackView(elements: elements)

这是一个描述我们的接口显示申明的方式,那样的话可读性很高。另外我们可以把stack View的配置移到构造器里。因为在我们的项目中多数的栈视图的配置是相似的。这样就可以删掉viewDidLoad方法中的一堆代码。

extension UIStackView {
    convenience init(elements: [ContentElement]) {
        self.init()
        translatesAutoresizingMaskIntoConstraints = false
        axis = .Vertical
        spacing = 10

        for element in elements {
            addArrangedSubview(element.view)
        }
    }
}

创建一个栈视图控制器

为了移除更多代码,我们可以用content elementes数组来初始化一整个视图控制器。这样的话,我们避免了重复我们在view Controller的viewDidLoad方法中必要的步骤。

我买的StackViewController类有一个使用[ContentElement]参数的自定义构造器,就像刚才在UIStack中的便利构造器一样。这边我们调用指定的父类构造器,并把Content Elements存到一个属性里。

final class StackViewController: UIViewController {
    let elements: [ContentElement]

    init(elements: [ContentElement]) {
        self.elements = elements
        super.init(nibName: nil, bundle: nil)
    }
    // ...
}

为了建立这个栈视图,我们可以从我们已经实现了的viewDidLoad方法可似乎。我们就必须剪切elements数组的定义出来。我们一会儿把这个粘贴到我们实例化stack View Controller的地方。最后我们也需要增加要求构造器的默认实现才能开始编译。

final class StackViewController: UIViewController {
    let elements: [ContentElement]

    init(elements: [ContentElement]) {
        self.elements = elements
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .whiteColor()

        let stack = UIStackView(elements: elements)
        view.addSubview(stack)
        stack.constrainEqual(.Width, to: view)
        stack.center(in: view)
    }
}

为了测试StackViewController这个类,我们把elements数组粘贴到我们实例化view Controller的地方。

let elements: [ContentElement] = [
    .image([#Image(imageLiteral: "objc-logo-white.png")#]),
    .label("To use the Swift Talk app please login as a subscriber"),
    .button("Login with GitHub", {
        print("Button tapped")
    }),
    .label("If you're not registered yet, please visit http://objc.io for more information")
]

let vc = StackViewController(elements: elements)

结果视图还没有动,但是创建他的代码简短且明确。StackViewController的抽象化让我们可以快速的用原型创建出一个屏幕内容。这个对于与其他一起参与的人沟通来说很有用。我们一会儿继续优化。

一个回调的按钮

我们还必须写一下ContentElement.button。到这它还什么都没做,而且我们没有指定action的方式。简单的部分是给枚举值增加一个回调,这个回调将在用户点击按钮的时候执行。

enum ContentElement {
    // ...
    case button(String, () -> ())
}

然而让回调和UIButton关联是不确定的。我们可以试试子类化UIButton并且在那里引用闭包,但是那样也不行。举个栗子,构造器的文档告诉我们UIButton(type:)不能返回一个自定义子类的实例,所以这个办法有点困难。

另一个方法是简单的包装UIButton来接收.TouchUpInside事件并且调用回调。我们调用继承自UIView的CallbackButton类。构造器定义了button的文字和回调。他们用属性来存储回调,建立button实例作为一个子视图,并且给button增加约束。

final class CallbackButton: UIView {
    let onTap: () -> ()
    let button: UIButton

    init(title: String, onTap: () -> ()) {
        self.onTap = onTap
        self.button = UIButton(type: .System)
        super.init(frame: .zero)
        addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.constrainEdges(to: self)
        button.setTitle(title, forState: .Normal)
        button.addTarget(self, action: #selector(tapped), forControlEvents: .TouchUpInside)
    }
    // ...
}

现在我们可以增加tapped方法最终调用我们传进构造器的回调。

func tapped(sender: AnyObject) {
    onTap()
}

最后我们再一次的添加要求构造器的默认实现。完整的CallbackButton类如下:

final class CallbackButton: UIView {
    let onTap: () -> ()
    let button: UIButton

    init(title: String, onTap: () -> ()) {
        self.onTap = onTap
        self.button = UIButton(type: .System)
        super.init(frame: .zero)
        addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.constrainEdges(to: self)
        button.setTitle(title, forState: .Normal)
        button.addTarget(self, action: #selector(tapped), forControlEvents: .TouchUpInside)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func tapped(sender: AnyObject) {
        onTap()
    }
}

CallbackButton是一种支撑,因为我们必须要给我们的按钮回调关联UIKit的target/action机制。但是至少成功了,而且安全。

现在我们就必须在ContentElement的view属性中使用CallbackButton。我们改变.button枚举像下面这样:

extension ContentElement {
    var view: UIView {
        switch self {
        // ...
        case .button(let title, let callback):
            return CallbackButton(title: title, onTap: callback)
        }
    }
}

我们成功的用声明式的方式来构建我们的栈视图,我们发现这是个很有用的原型工具。

我们可以扩展我们的实现在后面很多明显的方式。举个栗子,增加一个自顶一个枚举值来显示UIView随意的实例。增加一个异步的枚举值也很有意思。这让我们可以从网络加载数据,并且一旦数据到了可以替换视图。另一方面,增加越来越多的枚举值会使得这种简单抽象变得逐渐复杂。所以我们就止步于在所有UIKit中实现这些。毕竟总归要按需求编程。

另一个有趣的使用抽象化的案例,是可以用我们收到的数据构建视图层级,比如从服务器上。我们可以简单的把JSON字典转换成ContentElement并且创建StackViewController出来。

总是有很多有趣的可能,但是即使实在这个简单的示例中,抽象化也帮助我们快速迭代和代码整洁。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,871评论 25 707
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,135评论 30 470
  • 1、小逸的暑假语文预习有一些文言文阅读,作为不好读书、读书亦不求甚解的老妈我好些题也不会,就微信上偷偷求助。他看到...
    大巷阅读 321评论 0 2
  • 什么是信息流广告?信息流广告是指出现在社交媒体用户好友动态中的广告。它的特点是个性化投放,可以通过标签控制自己的需...
    张亚利SHE阅读 539评论 0 0
  • 开了心智军训感受不一样 这已经是大学军训第6天,这次军训和以往高中初中军训不同,在过去都是带着一颗不懂得思考与反思...
    陈脑院阅读 242评论 0 0