UIkit
简介
UIkit框架提供了构建ios和tvOS应用的核心,我们可以使用UIkit去展示我们的app的界面,并且去管理与系统之间的交互。当然UIkit也提供了与用户之间的交互功能,在UIkit的官方文档当中将UIkit按照其具体的功能分成了好几个部分,包括app的结构,用户界面,用户交互,图层相关等。本文主要基于官方文档梳理在app开发中常见的一些控件,因此大部分讲述的是用户界面这一块。
官方文档
使用UIKit的app的代码结构
UIkit负责了许多的核心功能,包括与系统交互,运行app的主线程的事件循环(比如点击按钮等事件),以及展示内容。 使用UIkit的app 是基于MVC设计模式的,这种划分基于其职责,其中Model层负责管理app的数据和业务逻辑;view层则是通过数据展示出对应的界面。Controller层则是前面两者之间的桥梁,提供两者之间的数据流通。 对于MVC架构,苹果官方有这么一张图
通常来讲我们会在appDelegate里添加在app启动时的自己的一些需要的操作,除系统提供的VC或者View以外,有时我们也会自己设计一些VC和View.
常见的ios控件
UIControl
UIControl是可以在视觉上与用户进行交互的控件的基类,一般这些控件指UIButton,UISWitch等。UIControl继承自UIView.而如果需要使用UIControl的话,没有必要直接使用UIControl(当然也可以),最好通过子类继承的方式来自定义自己的控件。UIControl有许多的状态(stage),而这些状态决定了其展示与功能,这些stage如下图所示,比如我们可以通过设置disabled来关闭其交互功能。UIControl是通过目标-操作机制(The target-action mechanism)来简化我们所写的代码。使用的方法的声明如下
func addTarget(
_ target: Any?,
action: Selector,
for controlEvents: UIControl.Event
)
这里需要注意的是如果target为空的话,那么会顺着UIControl的响应链直到找到一个view能够响应Selector中的方法,比如如下代码,当设置target为nil时,顺着响应链最终还是执行了selectedEvent方法了。
class ViewController: UIViewController{
var control : UIControl!
override func viewDidLoad() {
super.viewDidLoad()
control = UIControl.init(frame: self.view.frame)
control.backgroundColor = .orange
control.addTarget(nil, action: #selector(selectedEvent), for: .touchUpInside)
self.view.addSubview(control)
// Do any additional setup after loading the view.
}
@objc
func selectedEvent() {
print("点击了control")
}
}
之前的UIkit提到过UIApplication会处理事件循环,这里当点击事件产生后,其事件会进入事件队列,当前的UIApplication会拿到该事件后分发事件并且根据响应链找到合适的对象去处理事件。
table views
UItableView可以说是在ios开发中经常可以见到的常客了。UItableView将垂直展示一系列简单的items,而在具体的界面划分上,UITableView划分为了Section,而Section进一步划分为了rows,其实总的来看UItableView是多个objects的一个集合体。其大致结构如上.
- Cells. 这个object是展示内容的一个主体
- ViewController. 实现了相关的代理方法用来处理相关的工作,比如cell的点击事件,cell的加载等
- 数据源. cell的展示来自于数据源
- 相关代理. 定义了一些table views 相关的方法
UITableView.Style
tableview的样式有三种
- plan
- grouped
- insetGrouped
初始化
这里使用代码进行初始化,设置tableView的大小和VC的view的大小相同,同时设置样式为grouped
class ViewController: UIViewController {
var tableView : UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView.init(frame:self.view.frame, style: UITableView.Style.grouped)
// Do any additional setup after loading the view.
}
}
UITableCell
官方为我们提供的一种最为简单的item,有上图的三种样式,我们也可以自定义一个cell去继承UITableCell,如下
class MyCell: UITableViewCell{
override func layoutSubviews() {
super.layoutSubviews()
/*
custom your item
*/
}
}
我们可以在layoutSubviews方法中自定义我们的cell,并且添加如Accessory view这样的部件。
UITableViewDataSource
在苹果官方文档中提到,UItableview不直接管理数据,而是通过一个实现了数据源代理的object来进行数据管理,这里将ViewController作为tableView的数据源,其中必须需要实现的是如下的两个方法。
class ViewController: UIViewController,UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "normalCell",for: indexPath)
cell.textLabel!.text = "Cell text"
return cell
}
var tableView : UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView.init(frame:self.view.frame, style: UITableView.Style.grouped)
// Do any additional setup after loading the view.
}
}
除这两种必须实现的方法以外,UItableView还提供了一些可选的方法,其中常见的有
func numberOfSections(in: UITableView) -> Int返回tableview的Sections数量func tableView(UITableView, titleForHeaderInSection: Int) -> String?提供指定Section的Header的标题func tableView(UITableView, titleForFooterInSection: Int) -> String?提供指定Section的footer的标题
在实现了数据源代理后我们的tableView其实就已经可以展示了,以下代码
import UIKit
class MyCell: UITableViewCell{
override func layoutSubviews() {
super.layoutSubviews()
}
}
class ViewController: UIViewController,UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "normalCell",for: indexPath)
cell.textLabel!.text = "Cell text"
return cell
}
var tableView : UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView.init(frame:view.frame, style: .grouped)
tableView.register(MyCell.self, forCellReuseIdentifier: "normalCell")
tableView.dataSource = self
self.view.addSubview(tableView)
// Do any additional setup after loading the view.
}
}
运行的结果为
但是可惜的是此时tableView作为一个常用的控件还不能响应事件,于是接下来需要实现第二个代理。
UITableViewDelegate
该代理是管理tableView中一系列action的方法的集合。包括items的select,section header和section footer的配置,以及对cell的删除和重排序等。UITableViewDelegate并没有什么必须实现的方法,因此这里只看一个row的点击事件其他方法可见官方文档
- 实现代理 ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate
- 在viewDidLoad设置代理 tableView.delegate = self
- 实现代理方法 tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
UITableView的复用
和UITableView相关的代理还有很多,以上两个是开发时经常用到的。接下来说的是UITableView的复用原理。对于一个有许多cell的UITableView而言,必然需要屏幕滑动展示Cell,而在屏幕滑动时滑出屏幕的cell并不会释放掉,而是保存在cell复用池中,滑入屏幕的cell如果在复用池有其相同类型的cell那么就会进行复用。这一做法能够有效地利用内存。
存在的问题
UITableView进入复用池中的cell不会被格式化,其样式和相关内容会被保留,这样如果后面有和其相同类型的cell进行复用的话如果没有设置内容的话就会沿用之前的内容。举例如下
class MyCell: UITableViewCell{
override func layoutSubviews() {
super.layoutSubviews()
}
}
class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "normalCell",for: indexPath)
if(indexPath.row == 0){
cell.textLabel?.text = "我是第0个"
}
return cell
}
var tableView : UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView.init(frame:view.frame, style: .grouped)
tableView.register(MyCell.self, forCellReuseIdentifier: "normalCell")
tableView.dataSource = self
tableView.delegate = self
self.view.addSubview(tableView)
// Do any additional setup after loading the view.
}
}
当屏幕往上滑动时会出现如上两种状态,显然此时复用时出现了问题。
解决的方法
- 取消复用. 其实就是为tableView的每一项创建一个cell.当cell过多时会过耗内存
- 数据源.对于同类型的cell,可以抽出其相关配置当做数据源,在cell重用时进行配置。
如果tableView的项不多,其实用方法1也就ok了,如果项过多可以考虑使用方法2
UIStackView
UIStackView是一种和tableView不同的容器,它提供在垂直或者水平方向上进行流式布局。UIStackView提供axis,distribution,alignment,spacing,isBaselineRelativeArrangement,isLayoutMarginsRelativeArrangement六个属性来对容器中的view进行约束,同时提供了addArrangedSubview往其中添加view.
axis
指定了UIStackView伸缩的朝向。其值有两个
- NSLayout.Constraint.Axis.vertical
- NSLayoutConstraint.Axis.horizontal
spacing
设置arrangedSubviews之间的间距
distribution
控制指定朝向的分布情况
UIStackView.Distribution.fill
让容器尽量充满,如果容器内的view的容量大于容器本身,那么就会根据view的compression resistance priority(抗压缩属性,每个view都可以设置)来压缩view从而保证容器充满。如果容器内的容量小于容器本身,那么就会通过hugging priority的情况来按照优先级对view进行放大。如果上述两种情况的优先级一样,那么就会根据加入arrangedSubviews的顺序而定。
UIStackView.Distribution.fillEqually
除设置的spacing外,其余空间平分
UIStackView.Distribution.fillProportionally
按照arrangedSubviews中的实际大小比例进行resize.
UIStackView.Distribution.equalSpacing
先按照arrangedSubviews中的实际大小进行布局,将剩下的空间均分为spacing,如果不小于stackView设置的spacing,那么ok,如果小于,那么根据compression resistance priority进行压缩。
UIStackView.Distribution.equalCentering
保证arrangedSubviews中的view之间的中心距离是相同的,这样的话spacing可能不相同,但是spacing仍然需要不小于stackView设置的spacing,如果小于,那么根据compression resistance priority进行压缩。
alignment
控制对齐方式,有很多种对齐方式,这里只讲常见的几个,其余请见官方文档
fill
填充整个stackView(在和axis相反的方向进行拉伸,默认情况)
center
中心对齐
leading
垂直往左,水平往上
示例代码
import UIKit
class MyUIStackViewController: UIViewController {
var stackView:UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
stackView = UIStackView.init(frame: view.frame)
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 10
stackView.alignment = .fill
self.view.addSubview(stackView)
let view0 = UIView.init(frame: .zero)
let view1 = UIView.init(frame: .zero)
let view2 = UIView.init(frame: .zero)
view0.backgroundColor = .orange
view1.backgroundColor = .blue
view2.backgroundColor = .green
stackView.addArrangedSubview(view0)
stackView.addArrangedSubview(view1)
stackView.addArrangedSubview(view2)
// Do any additional setup after loading the view.
}
}