3 UIViewController
3.1什么是UIViewController?
iOS中经常看到MVC模式,因此代码中有的充当M,有的充当V,有的充当C的角色,但是 UIViewController
既有 View,又有 Controller,那么他到底是什么呢?
这个问题没有唯一答案,在iPhone早期, UIViewController
表示一屏的内容,例如,你的邮箱是一个视图控制器,当你阅读某条邮件,将显示另一个不同的视图控制器。
但是真实情况远比这个复杂,因为视图控制器的容器性,你可以将一个视图控制器放到另一个视图控制器中。结果,一屏的内容可能包含多个视图控制器一起协同工作。
视图控制器主要的不可推脱的角色是 响应视图生命周期事件。即当你的视图控制器将在视图被创建,显示,隐藏和销毁时被调用,你可以利用这些生命周期来实现自己的逻辑。
有些人将他们的视图控制器更偏向于视图部分(例如,处理布局),一些人则更偏向控制部分(例如,将布局代码放在 UIView
子类中,将粘合剂代码放在视图控制器中),还有一些人同时做2项(将视图代码和控制器代码放在同一个地方)。
3.2 如何使用视图控制器容器?
视图控制器容器允许你在一个视图控制器中插入另一个视图控制器,这能够简化和有利于你组织代码。可以按照下面4步:
- 在你的父视图控制器中调用
addChild()
,参数是子视图控制器 - 如果你使用frames,则设置你需要的子视图控制器的frame
- 将子视图添加到主视图中,添加自动布局约束
- 在子视图控制器中调用
didMove(toParentViewController:)
,参数使用你的主视图控制器
addChild(child)
child.view.frame = view.frame
view.addSubview(child.view)
child.didMove(toParent: self)
当你完成了上面这些,下面步骤理论上相似,但是是相反的:
- 调用
willMove(toParent:)
, 传入nil
- 从父视图控制器中移除子视图控制器
- 在子视图控制器中调用
removeFromParent()
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
为了方便,你可以考虑给 UIViewController
添加一个小的,私有的扩展来帮你完成这项任务,注意你需要按照顺序来运行,否则很容易出错:
// @nonobjc 用来避免和iOS自己的代码产生冲突
@nonobjc extension UIViewController {
func add(_ child: UIViewController, frame: CGRect? = nil) {
addChild(child)
if let frame = frame {
child.view.frame = view.frame
}
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
注意这里的代码的方法名和原文的不一样,此处使用的swift版本是4.2
示例:
// 创建一个子视图控制器
import UIKit
class ChildViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 创建一个视图控件
let bg = UIView()
bg.backgroundColor = .yellow
bg.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bg)
NSLayoutConstraint.activate([
bg.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
bg.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
bg.heightAnchor.constraint(equalToConstant: 200),
bg.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
])
// Do any additional setup after loading the view.
}
}
将子视图控制器添加到父视图控制器中:
// 父视图控制器
import UIKit
class ViewController: UIViewController {
let child = ChildViewController()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
addChild(child)
child.view.frame = view.frame
view.addSubview(child.view)
child.didMove(toParent: self)
}
}
4.UIView
4.1 如何强制UIView重绘: setNeedsDisplay()
所有的视图及其子类都是使用 drawRect()
方法进行渲染的,但是你不该直接自己调用这个方法。实际上,它是由系统在需要绘制时调用的,这可以避免多次绘制的发生。
但是,如果你想要一个视图立即重绘,你应该像这样调用 setNeedsDisplay()
:
myButton.setNeedsDisplay()
这个方法将会通知UIKit使用 drawRect()
重绘该button,但是需要重绘(redraw)不在队列中。
4.2 如何使用一个UIView遮挡另一个UIView?
所有的views都有一个 mask
(蒙版)属性,可以让你依据情况剪切部分视图。这个mask可以是任意的UIView,例如,用一个label遮挡一个image view
let redView = UIView(frame: CGRect(x: 50, y: 50, width: 128, height: 128))
redView.backgroundColor = .red
view.addSubview(redView)
接着创建一个mask作为单独的UIView。可能需要给视图一个背景色或者某些内容,因为mask alpha通道到决定原始视图上显示什么。
下面是一个和原始视图相同尺寸的mask,但是它向右偏移了64像素,以及一个64point的圆角。当用作先前视图的mask时,它会出现一个半圆的效果
// 注意这里的 x: 64 表示的是向右偏移64
let maskView = UIView(frame: CGRect(x: 64, y: 0, width: 128, height: 128))
maskView.backgroundColor = .blue
maskView.layer.cornerRadius = 64
redView.mask = maskView
蓝色背景并不可见,它只是用来当做遮挡的蒙版。
4.3 如何使用removeFromSuperview()从父视图中移除一个UIView?
如果你动态的创建一个视图,然后想移除,可以直接调用 removeFromSuperview()
方法。当你调用时,视图会立即被移除,可能被销毁(如果存在引用,可能不会被销毁):
yourView.removeFromSuperview()
4.4 如何把一个子视图放在一个UIView的前面?
UIKit 从后向前绘制视图,这表示在栈中比较高的视图将绘制在其它视图的上面。如果你想要把一个视图放在前面,可以使用 bringSubviewToFront(_ view: UIView)
:
parentView.bringSubviewToFront(childView)
这个方法可以将任何子视图放在前面,即使你不确定它在哪里:
childView.superView?.bringSubviewToFront(childView)
注意,此处使用的是swift4.2,和原文中的API有所不同
示例:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let orangeView = UIView(frame: CGRect(x: 64, y: 100, width: 128, height: 128))
orangeView.backgroundColor = .orange
view.addSubview(orangeView)
// 蓝色视图在栈的上面
// 因此会遮挡住上面的橘色视图
let blueView = UIView(frame: CGRect(x: 64, y: 100, width: 200, height: 200))
blueView.backgroundColor = .blue
view.addSubview(blueView)
// 使用下面方法将橘色视图放在父视图的最前面
// view.bringSubviewToFront(orangeView)
orangeView.superview?.bringSubviewToFront(orangeView)
}
}
4.5 如何使用自动布局锚点让一个UIView填充整个屏幕?
你可以将4个角和父视图容器的4个角对齐来填充整个屏幕,下面是扩展:
extension UIView {
func pinEdges(to ohter: UIView) {
leadingAnchor.constraint(equalTo, other.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true
topAnchor.constraint(equalTo: other.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: other.bottomAnchor).isActive = true
}
}
然后可以这样调用 pinEdges(to: someOtherView)
.
4.6 如何设置UIView的着色?
tintColor
属性可以改变着色效果,具体效果要看控件的类型:对导航条和tab bars表示的是按钮上的文字和图标,对文字视图表示选择光标和高亮文字,对进度条表示的是track的颜色。
tintColor
可以单独给某个视图设置颜色,对在视图控制器中的所有视图,甚至真个应用窗口都可以一次性设置颜色。
设置当前视图控制器的颜色,可以使用下面代码:
override func viewDidLoad() {
view.tintColor = .red
}
如果你想应用中所有的视图进行着色,可以在 AppDelegate.swift
:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window?.tintColor = UIColor.red
return true;
}
示例:
import UIKit
class ViewController: UIViewController {
let child = ChildViewController()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// let orangeView = UIView(frame: CGRect(x: 64, y: 100, width: 128, height: 128))
// orangeView.backgroundColor = .orange
// view.addSubview(orangeView)
//
// let blueView = UIView(frame: CGRect(x: 64, y: 100, width: 200, height: 200))
// blueView.backgroundColor = .blue
// view.addSubview(blueView)
//
//// view.bringSubviewToFront(orangeView)
// orangeView.superview?.bringSubviewToFront(orangeView)
let button = UIButton(type: .system)
button.setTitle("hello world", for: .normal)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
// 将button居中
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// 改变tintColor的颜色
// 会发现button内文字的颜色也变成了红色
view.tintColor = .red
}
}
4.7 如何使用 viewWithTag() 找到一个UIView的子视图?
如果你想快速的从一个复杂层级的视图内获取一个视图引用,可以使用 viewWithTag(_ tag: Int)
方法,这个方法会搜索所有的子视图,以及子子视图,直到找到匹配的tag number.这个方法返回一个 UIView?
类型,因此使用时要注意解包:
if let foundView = view.viewWithTag(0xDEADBEEF) {
// 将找到的视图从父视图中移除
foundView.removeFromSuperview()
}
tags 例如0xDEADBEEF
是很常见的coders。这个方法只是偶尔使用的一种捷径,不要在开发中依赖这种方法。
4.8 如何给UIView添加一个阴影?
iOS可以动态的给任何UIView添加阴影,这些阴影会自动的适配item的形状,甚至能够沿着UILabel内文字的曲线。
-
shadowColor
: 设置阴影的颜色,需要是一个CGColor
类型 -
shadowOpacity
: 阴影的透明度,0表示不可见,1表示最强 -
shadowOffset
:阴影的偏移,给一种3D偏移效果 -
shadowRadius
: 设置阴影的宽度
例如:
yourView.layer.shadowColor = UIColor.black.cgColor
yourView.layer.shadowOpactiy = 1
yourView.layer.shadowOffset = CGSize.zero
yourView.layer.shadowRadius = 10
动态的产生阴影是很消耗内存的,因为iOS必须按照视图形状绘制。如果可以的话,可以将shadowPath
设置 为一个具体值,这样iOS不需要动态计算透明度。例如,下面创建一个等于view frame的阴影:
yourView.layer.shadowPath = UIBezierPath(rect: yourView.bounds).cgPath
另外可以告诉ios缓存渲染的阴影,这样避免重复绘制:
yourView.layer.shouldRasterize = true
4.9 如何使用 convert() 将一个UIView里面的CGPoint转换到另一个视图中?
每一个视图都有自己的坐标系统,这意味着如果我点击一个按钮,询问iOS我点在哪里了,它将告诉我按钮相对于左上角的位置。但如果您想将一个视图中的位置转换为一个位置,那么这很容易做到。
例如,下面代码创建2个视图,创建一个虚拟的点击,然后将其从第一个视图坐标空间转换到第二个视图中:
let view1 = UIView(frame: CGRect(x: 50, y: 50, width: 128, height: 128))
let view2 = UIView(frame: CGRect(x: 200, y: 200, width: 128, height: 128))
let tap = CGPoint(x: 10, y: 10)
// convertedTap 将变为 (-140.0,-140.0)
let convertedTap = view1.convert(tap, to: view2)
4.10 如何使用CGAffineTransform对UIView进行缩放,伸展,移动和旋转?
每一个 UIView
都有一个 transform
属性,可以用来操控视图的尺寸,位置和旋转(使用affine transform)。这个属性是可动画的,这意味着可以改变某个值,使一个视图平滑的变大变小,旋转。
// double view size
imageView.transform = CGAffineTransform(scaleX: 2, y: 2)
// 移动到左边256位置
imageView.transform = CGAffineTransform(translateX: -256, y: -256)
// 旋转180度
imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
// 恢复成原样
imageView.transform = CGAffineTransform.identity
动画部分待学习