[译]纯代码创建 UIView
翻译自:
https://medium.com/written-code/creating-uiviews-programmatically-in-swift-55f5d14502ae/
读完这篇文章,你能得到啥?
- 了解 iOS 的屏幕构成
- 视图关系
- 何时适合使用代码方式构建视图
- MVC 模式下,该如何组织代码
- 自定义 UIView
- 一个 Twitter iOS App 作为示例
- 避免构建 Massive View Controller
- 使用 PureLayout 构建约束
了解 iOS 屏幕
iOS App 由许多视图组成。视图的显示依赖四个值:x,y,width,height。
三种方式构建视图:Storyboards
,Nib files
和编码实现
。
UIKit 包含许多标准组件,从简单的按钮,到复杂的表格。他们用处广泛,如 UILabel 对象绘制文本字符串,UIImageView 对象绘制图像。
视图可以被嵌入到其他视图,从而在视图之间产生父子视图关系,一个视图的父视图被称为superview
,子视图被称为subview
。
视图关系
如何组织你的视图关系着你的应用程序的视觉效果和事件行为。举一个例子,有两个视图,他们的父子关系决定了如何捕获事件及响应事件的顺序。类似的,当手机方向发生变化,视图的父子关系也决定了他们做如何修改。
何时使用代码形式构建视图
以下情景,通常都是适合使用代码构建视图的情况:
- 动态布局
- 视图需要实现一些效果,如圆角,阴影这类
- 任何你感觉使用 Storyboard 实现会复杂的时候
如何组织代码(MVC 模式)
Model-View-Controller 是最常用的设计模式。然而在 iOS App 开发过程中,通常要面临一个问题:视图控制器常常变得过于庞大,修改和重构都很痛苦。所以 MVC 也被戏称为Massive View Controller
。
遵循此模式,我们应该尽量确保项目中的每个类都是Controller、Model或者View。这能有效避免代码失控。我们也可以创建其他的分组和类,但 App 的核心部分应该是这三种组成。
准备
创建项目的时候,Xcode 会自动为我们增加一个 storyboard。为了展示自定义视图,我们干掉他先。
然后,创建两个文件:ProfileView 继承自 UIView,放到 View 分类中。ProfileViewController,继承自 UIViewController,放在 Controller 分类中。
Auto Layout
Auto Layout 决定了屏幕上视图的 frame。每个视图都包含约束条件,通过这些条件来计算出视图的 width,height,x,y。直接编写 Auto Layout 代码并不容易,这里我们使用 PureLayout,它提供了功能强大,使用友好的接口来帮助我们编写 Auto Layout
首先添加 PureLayout 到你的项目中。我使用 CocoaPods 进行包管理,它依赖 Podfile 文件:
platform :ios, '8.0'
use_frameworks!
pod 'PureLayout', '~> 2.0.5'
执行代码以安装依赖:
pod install
这条命令将创建一个以.xcworkspace
为扩展名的新的工程文件。现在,使用 Xcode 打开它。
Building custom classes
ProfileView.swift
文件当前是一个自定义 UIView 类的模板:
import UIKit
class ProfileView: UIView {
}
我们需要初始化它。初始化在 Swift 中是值得重视的事,你将在这里了解到更多关于它的事。现在,我们只需要知道有一个主要的初始化器负责初始化当前类所有的属性。这是一个典型的实现:
import UIKit
import PureLayout
class ProfileView: UIView {
var shouldSetupConstraints = true
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func updateConstraints() {
if(shouldSetupConstraints) {
// AutoLayout constraints
shouldSetupConstraints = false
}
super.updateConstraints()
}
}
给自定义的视图添加约束之前,我们要覆盖 updateConstraints
方法。这个方法在运行期间可能会被调用多次。为了避免多次添加约束给视图,我们需要立一个 flag(shouldSetupContraints)来标示是否已经添加过约束。这个方法的最后,我们也必须调用父类中的同名方法。(如果你在约束发生改变之前就调用,可能会 crash)。
Example
我们来仿写一个 Twitter iOS app 的个人信息视图。在下面的图片中,可以看到顶部视图包括一个 Banner 图,用户头像,以及用户信息。然后下方是所有推展示在列表中以及tabbar。所有的 UIView 元素都拿紫色标注了起来。接下来我们聚焦在 header view,橙色区域。
我们先看主要的三部分。banner 和 用户头像使用 UIImageViews 进行展示,button 区域使用 UISegmentedControl。
首先,我们定义三个元素在我们的 ProfileView 类。bannerView,profileView 和 segmentedControl。
//ProfileView.swift
import UIKit
import PureLayout
class ProfileView: UIView {
var shouldSetupConstraints = true
var bannerView: UIImageView!
var profileView: UIImageView!
var segmentedControl: UISegmentedControl!
override init(frame: CGRect){
super.init(frame: frame)
}
...
在 init 方法中,我们初始化这些视图元素的属性。背景颜色、边框颜色和其他基本的视觉属性。初始化他们的 frame 为 zero,AutoLayout 会自动调整大小和位置。
将这些视图元素添加为 ProfileView 的子视图使用 addSubview 方法。这个方法将被操作的视图放在其他子元素的最上面。代码如下:
//ProfileView.swift
import UIKit
import PureLayout
class ProfileView: UIView {
var shouldSetupConstraints = true
var bannerView: UIImageView!
var profileView: UIImageView!
var segmentedControl: UISegmentedControl!
let screenSize = UIScreen.main.bounds
override init(frame: CGRect){
super.init(frame: frame)
bannerView = UIImageView(frame: CGRect.zero)
bannerView.backgroundColor = UIColor.gray
bannerView.autoSetDimension(.height, toSize: screenSize.width / 3)
self.addSubview(bannerView)
profileView = UIImageView(frame: CGRect.zero)
profileView.backgroundColor = UIColor.gray
profileView.layer.borderColor = UIColor.white.cgColor
profileView.layer.borderWidth = 1.0
profileView.layer.cornerRadius = 5.0
profileView.autoSetDimension(.width, toSize: 124.0)
profileView.autoSetDimension(.height, toSize: 124.0)
self.addSubview(profileView)
segmentedControl = UISegmentedControl(items: ["Tweets", "Media", "Likes"])
self.addSubview(segmentedControl)
}
...
使用 PureLayout 设置约束
布局约束
布局约束用来描述视图与其他视图的关系和属性。通过 NSLayoutConstraint 类来使用。
约束有以下几种:
- 尺寸约束 - 如描述一个图片的宽为200px
- 对齐约束 - 如描述一个 label 垂直居中在屏幕
- 间隙约束 - 如描述两个元素之间的间隙
Attributes
PureLayout 定义了用来创建约束的视图属性,见图:
开始搞事吧!
屏幕上有三个巨星元素,我们要把他们的位置大小调整如 Twitter 个人页面。
//ProfileView.swift
...
override func updateConstraints() {
if(shouldSetupConstraints) {
let edgesInset: CGFloat = 10.0
let centerOffset: CGFloat = 62.0
bannerView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .bottom)
profileView.autoPinEdge(toSuperviewEdge: .left, withInset: edgesInset)
// 👇🏽 profileView.autoAlignAxis(.horizontal, toSameAxisOf: bannerView, withOffset: centerOffset)
profileView.autoPinEdge(.bottom, to: .bottom, of: bannerView, withOffset: centerOffset)
segmentedControl.autoPinEdge(toSuperviewEdge: .bottom, withInset: edgesInset)
segmentedControl.autoPinEdge(toSuperviewEdge: .left, withInset: edgesInset)
segmentedControl.autoPinEdge(toSuperviewEdge: .right, withInset: edgesInset)
shouldSetupConstraints = false
}
super.updateConstraints()
...
}
避免 MVC 成为 Massive View Controllers
下面进行完成 ProfileView 的最后一步。我们需要在我们的 Controller(ProfileViewController)中调用。Xcode 已经创建了一个 Controller 模板,并包含 viewDidLoad: 方法。这个方法会在 Controller 显示前进行回调。接下来我们需要实例化我们的 ProfileView 并展示它。
//ProfileViewController.swift
import UIKit
class ViewController: UIViewController {
var profile: ProfileView!
override func viewDidLoad() {
super.viewDidLoad()
profile = ProfileView(frame: CGRect.zero)
self.view.addSubview(profile)
// AutoLayout
profile.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
(最后:本文中的代码用于展示 Auto Layout。你需要补充其他代码才能使其成为一个完整的项目,加油!💃🏽👋)