iOS自定义导航条

调整导航条的按钮的位置

在自定义导航条左侧返回按钮,返回按钮明显会有点偏右,应该如何调整的,由于导航栏的NavigationItem是个比较特殊的View,设置Frame是行不通的,在苹果提供的UIButtonBarItem中有个叫做UIBarButtonSystemItemFixedSpace的控件,利用它,我们就可以轻松调整返回按钮的位置,具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let leftBtn = UIButton(type: .custom)
leftBtn.frame = CGRect(x: 0, y: 0, width: 25, height: 25)
leftBtn.setBackgroundImage(UIImage(named: "back"), for: .normal)
leftBtn.addTarget(self, action: #selector(leftBarBtnClicked(btn:)), for: .touchUpInside);
let leftBarBtn = UIBarButtonItem(customView: leftBtn)
//创建UIBarButtonSystemItemFixedSpace
let spaceItem = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
//将宽度设置为负值
spaceItem.width = -15;
//将两个BarButtonItem都返回给NavigationItem
navigationItem.leftBarButtonItems = [spaceItem,leftBarBtn];
```
调整完可以棉线看到返回按钮已经紧靠着屏幕边缘,`这个方法同样适用于调整导航栏右侧的按钮`
#### 让滑动返回手势生效
适用自定义的按钮去替换系统默认的返回按钮,会出现滑动返回手势失效的情况。可以通过重新添加导航栏的`interactivePopGestureRecognizer`的`delegate`即可。
首先为ViewController添加`UIGestureRecognizerDelegate`协议
```swift
class FirstViewController: UIViewController,UIGestureRecognizerDelegate {}

然后设置代理

1
navigationController?.interactivePopGestureRecognizer?.delegate = self;

全屏滑动返回

(1)系统自带的手势是UIScreenEdgePanGestureRecognizer类型对象,看名字就知道这个是屏幕边缘滑动手势。所以系统自带的滑动效果,自然只能实现侧边滑动。
(2)我们自己给导航控制器添加UIGestureRecognizerDelegate协议,添加一个全屏的滑动手势。然后用新添加的滑动手势,来调用系统实现的滑动返回功能(handleNavigationTransition 方法),这样就实现了全屏滑动功能。
(3)注意:我们还要禁止系统自带滑动手势,同时只有非根控制器才有滑动返回功能,根控制器没有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import UIKit
class CustomNavigationController: UINavigationController,UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let target = interactivePopGestureRecognizer?.delegate
//创建全屏滑动手势,调用系统自带滑动手势的target的action方法
let pan = UIPanGestureRecognizer(target: target, action: Selector("handleNavigationTransition:"))
//设置手势的代理
pan.delegate = self;
//给导航控制器的View添加全屏滑动手势
view.addGestureRecognizer(pan)
//禁止使用系统自带的滑动手势
interactivePopGestureRecognizer?.isEnabled = false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if childViewControllers.count == 0 {
return false
}
return true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

这种方法的原理其实很简单,其实就是自定义一个全屏滑动手势,并将滑动事件设置为系统滑动事件,然后禁用系统滑动手势即可。handleNavigationTransition就是系统滑动的方法,虽然系统并未提供接口,但是可以通过runtime找到这个方法,因此直接调用即可。

整体滑动返回

虽然实现了全屏滑动返回,但是滑动时的切换依然是系统自带的动画,如果遇到前一个界面的NavigationBar为透明或前后两个Bar颜色不一样,这种渐变式的动画看起来就会不太友好,尤其当前后两个界面其中一个界面的NavigationBar为透明或隐藏时,其效果更是惨不忍睹。
整体滑动返回等于将两个NavigationBar独立开来,因此可以相对完美的解决导航栏滑动切换中的种种Bug。
实现这个效果有三种基本思路

>

  • 使用UINavigationController自带的setNavigationBarHidden: animated:方法来实现,每次push或pop时,在当前控制器的viewWillDisappear:中设置隐藏,在要跳转的控制器的viewWillAppear:中设置导航栏显示。
  • 在每次Push前对当前页面进行截图并保存到数组,Pop时取数组最后一个元素显示,滑动结束后调用系统Pop方法并删除最后一张截图。
  • 使用iOS 7之后开放的,UIViewControllerAnimatedTransitioning协议,来实现自定义导航栏转场动画及交互。

三种方法中,方法一十分繁琐,且会有很多莫名其妙的BUG,直接pass。
在iOS的交互中,push一般通过按钮的点击事件或View的tap事件触发,而pop则可能通过事件触发,也可能通过右滑手势触发。因此,我们将这个我们要实现的动画效果分为交互效果和无交互效果两种。分别实现这两种效果,可以较为完美的解决Push和Pop的动画问题。

实现交互动画效果

方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import UIKit
let ScreenWidth = UIScreen.main.bounds.size.width
let ScreenHeight = UIScreen.main.bounds.size.height
let kDefaultAlpha : CGFloat = 0.6 //默认的将要变透明的遮罩的初始透明度(全黑)
let kTargetTranslateScale : CGFloat = 0.75 //当拖动的距离,占了屏幕的总宽度的3/4时,就让imageView完全显示,遮盖完全消失
class CustomNavigationController: UINavigationController,UIGestureRecognizerDelegate {
var screenshotImageView : UIImageView!
var coverView : UIView!
var screenshotImgs : Array<UIImage>!
var panGestureRec : UIScreenEdgePanGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
//1、创建Pan手势识别器,并绑定监听方法
panGestureRec = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(panGestureRecognizer(pan:)))
panGestureRec.edges = UIRectEdge.left
//为导航控制器的view添加Pan手势识别器
view.addGestureRecognizer(panGestureRec)
//2、创建截图的ImageView
screenshotImageView = UIImageView()
//app的frame是包括了状态栏高度的frame
screenshotImageView.frame = CGRect(x: 0, y: 0, width: ScreenWidth, height: ScreenHeight)
//3、创建截图上面的黑色半透明遮罩
coverView = UIView()
//遮罩的frame就是截图的frame
coverView.frame = screenshotImageView.frame
//遮罩为黑色
coverView.backgroundColor = UIColor.black
//4、存放所有的截图数组初始化
screenshotImgs = []
}
//MARK:响应手势的方法
func panGestureRecognizer(pan : UIScreenEdgePanGestureRecognizer) {
//如果当前显示的控制器已经是根控制器了,不做任何切换动画,直接返回
if self.visibleViewController == self.viewControllers[0] {
return
}
//判断pan手势的各个阶段
switch panGestureRec.state {
case .began://开始拖拽阶段
dragBegin()
case .ended://结束拖拽阶段
dragEnd()
default://正在拖拽阶段
dragging(pan: pan)
}
}
//MARK:开始拖拽,添加图片和遮罩
func dragBegin() {
//重点:每次开始pan手势时,都要添加截图imageView和遮罩cover到window中
view.window?.insertSubview(screenshotImageView, at: 0)
view.window?.insertSubview(coverView, aboveSubview: screenshotImageView)
//并且,让imageView显示截图数组中的最后(最新)一张截图
screenshotImageView.image = screenshotImgs.last
}
//MARK:正在拖动,动画效果的精髓,进行位移和透明度的变化
func dragging(pan : UIScreenEdgePanGestureRecognizer) {
//得到手指拖动的位移
let offsetX = pan.translation(in: view).x
//让整个view都平移
//挪动整个导航view
if offsetX > 0 {
view.transform = CGAffineTransform(translationX: offsetX, y: 0)
}
//计算目前手指拖动位移占屏幕总的宽高的比例,当这个比例达到3/4时,就让imageview完全显示,遮盖完全消失
let currentTranslateScaleX = offsetX / self.view.frame.width
if offsetX < ScreenWidth {
screenshotImageView.transform = CGAffineTransform(translationX: (offsetX - ScreenWidth) * 0.6, y: 0)
}
// 让遮盖透明度改变,直到减为0,让遮罩完全透明,默认的比例-(当前平衡比例/目标平衡比例)*默认的比例
let alpha = kDefaultAlpha - (currentTranslateScaleX / kTargetTranslateScale) * kDefaultAlpha
coverView.alpha = alpha
}
//MARK:结束拖动,判断结束时拖动的距离做响应的处理,并将图片和遮罩从父控件上移除
func dragEnd() {
//取出挪动的距离
let translateX = view.transform.tx
//取出宽度
let width = view.frame.size.width
if translateX <= 40 {// 如果手指移动的距离还不到屏幕的一半,往左边挪 (弹回)
UIView.animate(withDuration: 0.3, animations: {
//重要~~让被右移的view弹回归位,只要清空transform即可办到
self.view.transform = CGAffineTransform.identity
//让imageview大小恢复默认的
self.screenshotImageView.transform = CGAffineTransform(translationX: -ScreenWidth, y: 0)
//让遮盖的透明度恢复默认的alpha
self.coverView.alpha = kDefaultAlpha
}, completion: { (finished) in
//重要,动画完成之后,每次都要记得 移除两个view,下次开始拖动时,再添加进来
self.screenshotImageView.removeFromSuperview()
self.coverView.removeFromSuperview()
})
}else{// 如果手指移动的距离还超过了屏幕的一半,往右边挪
UIView.animate(withDuration: 0.3, animations: {
// 让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform
self.view.transform = CGAffineTransform(translationX: width, y: 0)
//让imageView位移还原
self.screenshotImageView.transform = CGAffineTransform(translationX: 0, y: 0)
//让遮盖alpha变为0,变得完全透明
self.coverView.alpha = 0
}, completion: { (finished) in
// 重要~~让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform,不然下次再次开始drag时会出问题,因为view的transform没有归零
self.view.transform = CGAffineTransform.identity
// 移除两个view,下次开始拖动时,再加回来
self.screenshotImageView.removeFromSuperview()
self.coverView.removeFromSuperview()
// 执行正常的Pop操作:移除栈顶控制器,让真正的前一个控制器成为导航控制器的栈顶控制器
self.popViewController(animated: false)
})
}
}
//MARK:实现截图保存功能,并在push前截图
func screenShot() {
//将要被截图的view,即窗口的根控制器的view
let beyondVC = self.view.window?.rootViewController;
//背景图片 总的大小
let size = beyondVC?.view.frame.size
//开启上下文,使用参数之后,截出来的是原图(YES 0.0 质量高)
UIGraphicsBeginImageContextWithOptions(size!, true, 0.0)
//要裁剪的矩形范围
let rect = CGRect(x: 0, y: 0, width: ScreenWidth, height: ScreenHeight)
////注:iOS7以后renderInContext:由drawViewHierarchyInRect:afterScreenUpdates:替代
beyondVC?.view.drawHierarchy(in: rect, afterScreenUpdates: false)
//从上下文中,取出UIImage
let snapshot = UIGraphicsGetImageFromCurrentImageContext()
//添加截取好的图片到图片数组
if let _snapshot = snapshot {
screenshotImgs.append(_snapshot)
}
//千万记得,结束上下文(移除栈顶的基于当前位图的图形上下文)
UIGraphicsEndImageContext()
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
//有在导航控制器里面有子控制器的时候才需要截图
if viewControllers.count >= 1 {
//调用自定义方法,使用上下文截图
screenShot()
viewController.navigationItem.leftBarButtonItems = UIBarButtonItem.leftBarbuttonItem(self, action: #selector(leftBarBtnClicked(btn:)), normalIcon: "back", hightlightIcon: "back")
}
//截图完毕之后,才调用父类的push方法
super.pushViewController(viewController, animated: true)
}
//重写常用的Pop方法
/*
由于可能调用的是导航栏的popViewController(animated: Bool) -> UIViewController?方法、popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]?方法 或func popToRootViewController(animated: Bool) -> [UIViewController]?来返回,这种情况下,删除的可能就不是一张截图,因此我们需要分别重写这些Pop方法,去确定我们要删除多少张图片
*/
override func popViewController(animated: Bool) -> UIViewController? {
screenshotImgs.removeLast()
return super.popViewController(animated: animated)
}
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
for vc in viewControllers {
if viewController == vc {
break
}
screenshotImgs.removeLast()
}
return super.popToViewController(viewController, animated: animated)
}
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
screenshotImgs.removeAll()
return super.popToRootViewController(animated: animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK:返回方法
func leftBarBtnClicked(btn:UIButton) {
self.popViewController(animated: true)
}
}

实现非交互动画效果

实现原理

注:FromVC代表即将消失的视图控制器,ToVC表示将要展示的视图控制器

要实现的效果:
Push的时候,FromVC往左移动,ToVC从屏幕右侧出现跟随FromVC左移直至FromVC消失,此时ToVC刚好完整显示在屏幕上。
Pop的时候,FromVC向右移动,ToVC从屏幕边缘出现跟随FromVC向右移动直至FromVC消失,此时ToVC刚好完整显示在屏幕上

实现的时候,我们依然需要将Push和Pop分开讨论
先说Pop
1.和交互式动画一样,每次Push时对屏幕截屏并保存,Pop的再次截屏但不保存
2.把Pop时截取的图片作为FromVC展示,把Push到这个界面时截取的图片作为ToVC展示
3.并对两张图片做位移动画,动画结束后移除两张图片

然后是Push
1.Push时先对当前屏幕截屏。
2.将截取的图片保存方便Pop回来时使用,并把这张图片作为这次Push的FromVC保存。
3.获取当前导航栏控制器对象,调整其Transform属性中的位移参数作为ToVC展示
4.对截图和导航栏做位移,动画结束后直接移除截屏图片

为什么要对导航栏作位移?

首先,在Push结束之前,我们是无法知道ToVC具体是什么样子,系统的截屏方法对于未加载出来的View是无能为力的,而UIView的 snapshotViewAfterScreenUpdates:方法又无法带着导航栏一起映射到一个新的View上,因此视觉效果很差。
正好在Pop的时候,为了达到想要的动画效果,用来展示的两张图片都需要放到导航栏的View上,因此在Push的时候我们就直接将导航栏的View做一个放射变换,当然,这也就意味着,当我们Push的时候,截屏就不能再放到导航栏上,而是应该放到它的“更上一层“ – UITabbarController的View上。

话不多说,附上代码如下

AnimationController

创建动画控制器。更准确的说,需要实现的细节都在UIViewControllerAnimatedTransitioning中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
import UIKit
class AnimationController: NSObject,UIViewControllerAnimatedTransitioning {
var navigationOperation : UINavigationControllerOperation!
var navigationController : UINavigationController?{
didSet{
let beyondVC = navigationController!.view.window?.rootViewController
//判断该导航栏是否有TabBarController
if navigationController!.tabBarController == beyondVC {
isTabbarExist = true
}else {
isTabbarExist = false
}
}
}
//导航栏Pop时删除了多少张截图(调用PopToViewController时,计算要删除的截图的数量)
var removeCount : NSInteger = 0
var screenShotArray : Array<UIImage> = []
//所属的导航栏有没有TabBarController
var isTabbarExist = false
class func animationController(operation : UINavigationControllerOperation) -> AnimationController{
let ac = AnimationController()
ac.navigationOperation = operation;
return ac
}
class func animationController(operation : UINavigationControllerOperation, navigationController : UINavigationController) -> AnimationController {
let ac = AnimationController()
ac.navigationController = navigationController
ac.navigationOperation = operation
return ac
}
//MARK:UIViewControllerAnimatedTransitioning
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let screenImgView = UIImageView(frame: CGRect(x: 0, y: 0, width: ScreenWidth, height: ScreenHeight))
let screenImg = self.screenShot()
screenImgView.image = screenImg
//取出fromViewController,fromView和toViewController,toView
let fromVC = transitionContext.viewController(forKey: .from)
let toVC = transitionContext.viewController(forKey: .to)
let toView = transitionContext.view(forKey: .to)
var fromViewEndFrame = transitionContext.finalFrame(for: fromVC!)
fromViewEndFrame.origin.x = ScreenWidth
var fromViewStartFrame = fromViewEndFrame
let toViewEndFrame = transitionContext.finalFrame(for: toVC!)
let toViewStartFrame = toViewEndFrame
let containerView = transitionContext.containerView
if navigationOperation == UINavigationControllerOperation.push {
screenShotArray.append(screenImg!)
//这句非常重要,没有这句,就无法正常push和Pop出对应的界面
containerView.addSubview(toView!)
toView?.frame = toViewStartFrame
//将截图添加到导航栏的view所属的window上
navigationController?.view.window?.insertSubview(screenImgView, at: 0)
navigationController?.view.transform = CGAffineTransform(translationX: ScreenWidth, y: 0)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
self.navigationController?.view.transform = CGAffineTransform(translationX: 0, y: 0)
screenImgView.center = CGPoint(x: -ScreenWidth / 2.0, y: ScreenHeight / 2.0)
}, completion: { (finished) in
screenImgView.removeFromSuperview()
transitionContext.completeTransition(true)
})
}
if navigationOperation == UINavigationControllerOperation.pop {
fromViewStartFrame.origin.x = 0
containerView.addSubview(toView!)
let lastVCImgView = UIImageView(frame: CGRect(x: -ScreenWidth, y: 0, width: ScreenWidth, height: ScreenHeight))
//若removeCount大于0,则说明pop了不止一个控制器
if removeCount > 0 {
for i in 0 ..< removeCount {
if i == removeCount - 1 {
//当删除到要跳转页面的截图时,不要删除,并将该截图作为ToVC的截图显示
lastVCImgView.image = screenShotArray.last
removeCount = 0
break
}else{
screenShotArray.removeLast()
}
}
}else{
lastVCImgView.image = screenShotArray.last
}
screenImgView.layer.shadowColor = UIColor.black.cgColor
screenImgView.layer.shadowOffset = CGSize(width: -0.8, height: 0)
screenImgView.layer.shadowOpacity = 0.6
navigationController?.view.window?.addSubview(lastVCImgView)
navigationController?.view.addSubview(screenImgView)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
screenImgView.center = CGPoint(x: ScreenWidth * 3 / 2.0, y: ScreenHeight / 2.0)
lastVCImgView.center = CGPoint(x: ScreenWidth / 2.0, y: ScreenHeight / 2.0)
}, completion: { (finished) in
lastVCImgView.removeFromSuperview()
screenImgView.removeFromSuperview()
self.screenShotArray.removeLast()
transitionContext.completeTransition(true)
})
}
}
func removeLastScreenShot() {//调用此方法删除数组最后一张截图 (调用pop手势或一次pop多个控制器时使用)
screenShotArray.removeLast()
}
func removeAllScreenShot() {// 移除全部屏幕截图
screenShotArray.removeAll()
}
func removeLastScreenShot(withNumber number : NSInteger) {//从截屏数组尾部移除指定数量的截图
for _ in 0 ..< number {
screenShotArray.removeLast()
}
}
func screenShot() -> UIImage? {
//将要被截图的view,即窗口的根控制器的view
let beyondVC = self.navigationController?.view.window?.rootViewController;
//背景图片 总的大小
let size = beyondVC?.view.frame.size
//开启上下文,使用参数之后,截出来的是原图(YES 0.0 质量高)
UIGraphicsBeginImageContextWithOptions(size!, true, 0.0)
//要裁剪的矩形范围
let rect = CGRect(x: 0, y: 0, width: ScreenWidth, height: ScreenHeight)
////注:iOS7以后renderInContext:由drawViewHierarchyInRect:afterScreenUpdates:替代
if isTabbarExist {
beyondVC?.view.drawHierarchy(in: rect, afterScreenUpdates: false)
}else {
navigationController?.view.drawHierarchy(in: rect, afterScreenUpdates: false)
}
//从上下文中,取出UIImage
let snapshot = UIGraphicsGetImageFromCurrentImageContext()
//千万记得,结束上下文(移除栈顶的基于当前位图的图形上下文)
UIGraphicsEndImageContext()
return snapshot;
}
}
```
##### CustomNavigationController
CustomNavigationController中添加`UINavigationControllerDelegate`协议,实现其代理方法,对push以及pop 和手势滑动事件进行修改
```swift
import UIKit
let ScreenWidth = UIScreen.main.bounds.size.width
let ScreenHeight = UIScreen.main.bounds.size.height
let kDefaultAlpha : CGFloat = 0.6 //默认的将要变透明的遮罩的初始透明度(全黑)
let kTargetTranslateScale : CGFloat = 0.75 //当拖动的距离,占了屏幕的总宽度的3/4时,就让imageView完全显示,遮盖完全消失
func colorFromRGB(rgbValue : Int) -> UIColor {
return UIColor(red: CGFloat(((rgbValue & 0xFF0000) >> 16))/255.0, green: CGFloat(((rgbValue & 0x0FF00) >> 16))/255.0, blue: CGFloat(((rgbValue & 0x0000FF) >> 16))/255.0, alpha: 1.0)
}
class CustomNavigationController: UINavigationController,UIGestureRecognizerDelegate,UINavigationControllerDelegate {
var screenshotImageView : UIImageView!
var coverView : UIView!
var screenshotImgs : Array<UIImage>!
var panGestureRec : UIScreenEdgePanGestureRecognizer!
var nextVCScreenShotImg : UIImage!
var animationController : AnimationController!
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
navigationBar.tintColor = colorFromRGB(rgbValue: 0x6F7179)
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: -0.8, height: 0)
view.layer.shadowOpacity = 0.6
animationController = AnimationController()
//1、创建Pan手势识别器,并绑定监听方法
panGestureRec = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(panGestureRecognizer(pan:)))
panGestureRec.edges = UIRectEdge.left
//为导航控制器的view添加Pan手势识别器
view.addGestureRecognizer(panGestureRec)
//2、创建截图的ImageView
screenshotImageView = UIImageView()
//app的frame是包括了状态栏高度的frame
screenshotImageView.frame = CGRect(x: 0, y: 0, width: ScreenWidth, height: ScreenHeight)
//3、创建截图上面的黑色半透明遮罩
coverView = UIView()
//遮罩的frame就是截图的frame
coverView.frame = screenshotImageView.frame
//遮罩为黑色
coverView.backgroundColor = UIColor.black
//4、存放所有的截图数组初始化
screenshotImgs = []
}
//MARK:实现`UINavigationControllerDelegate`的代理方法
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animationController.navigationOperation = operation
animationController.navigationController = self
return animationController
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
//只有在导航控制器里面有子控制器的时候才需要截图
if viewControllers.count >= 1 {
//调用自定义方法,使用上下文截图
screenShot()
viewController.navigationItem.leftBarButtonItems = UIBarButtonItem.leftBarbuttonItem(self, action: #selector(leftBarBtnClicked(btn:)), normalIcon: "back", hightlightIcon: "back")
viewController.hidesBottomBarWhenPushed = true
}
//截图完毕之后,才调用父类的push方法
super.pushViewController(viewController, animated: true)
}
//重写常用的Pop方法
/*
由于可能调用的是导航栏的popViewController(animated: Bool) -> UIViewController?方法、popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]?方法 或func popToRootViewController(animated: Bool) -> [UIViewController]?来返回,这种情况下,删除的可能就不是一张截图,因此我们需要分别重写这些Pop方法,去确定我们要删除多少张图片
*/
override func popViewController(animated: Bool) -> UIViewController? {
let index = viewControllers.count
if screenshotImgs.count >= index - 1 {
screenshotImgs.removeLast()
}
return super.popViewController(animated: animated)
}
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
var removeCount = 0
for vc in viewControllers {
if viewController == vc {
break
}
screenshotImgs.removeLast()
removeCount += 1
}
animationController.removeCount = removeCount
return super.popToViewController(viewController, animated: animated)
}
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
animationController.removeCount = screenshotImgs.count
screenshotImgs.removeAll()
return super.popToRootViewController(animated: animated)
}
//MARK:实现截图保存功能,并在push前截图
func screenShot() {
//将要被截图的view,即窗口的根控制器的view
let beyondVC = self.view.window?.rootViewController;
//背景图片 总的大小
let size = beyondVC?.view.frame.size
//开启上下文,使用参数之后,截出来的是原图(YES 0.0 质量高)
UIGraphicsBeginImageContextWithOptions(size!, true, 0.0)
//要裁剪的矩形范围
let rect = CGRect(x: 0, y: 0, width: ScreenWidth, height: ScreenHeight)
////注:iOS7以后renderInContext:由drawViewHierarchyInRect:afterScreenUpdates:替代
if tabBarController == beyondVC {
beyondVC?.view.drawHierarchy(in: rect, afterScreenUpdates: false)
}else{
view.drawHierarchy(in: rect, afterScreenUpdates: false)
}
//从上下文中,取出UIImage
let snapshot = UIGraphicsGetImageFromCurrentImageContext()
//添加截取好的图片到图片数组
if let _snapshot = snapshot {
screenshotImgs.append(_snapshot)
}
//千万记得,结束上下文(移除栈顶的基于当前位图的图形上下文)
UIGraphicsEndImageContext()
}
//MARK:响应手势的方法
func panGestureRecognizer(pan : UIScreenEdgePanGestureRecognizer) {
//如果当前显示的控制器已经是根控制器了,不做任何切换动画,直接返回
if self.visibleViewController == self.viewControllers[0] {
return
}
//判断pan手势的各个阶段
switch panGestureRec.state {
case .began://开始拖拽阶段
dragBegin()
case .ended,.cancelled,.failed://结束拖拽阶段
dragEnd()
default://正在拖拽阶段
dragging(pan: pan)
}
}
//MARK:开始拖拽,添加图片和遮罩
func dragBegin() {
//重点:每次开始pan手势时,都要添加截图imageView和遮罩cover到window中
view.window?.insertSubview(screenshotImageView, at: 0)
view.window?.insertSubview(coverView, aboveSubview: screenshotImageView)
//并且,让imageView显示截图数组中的最后(最新)一张截图
screenshotImageView.image = screenshotImgs.last
}
//MARK:正在拖动,动画效果的精髓,进行位移和透明度的变化
func dragging(pan : UIScreenEdgePanGestureRecognizer) {
//得到手指拖动的位移
let offsetX = pan.translation(in: view).x
//让整个view都平移
//挪动整个导航view
if offsetX > 0 {
view.transform = CGAffineTransform(translationX: offsetX, y: 0)
}
//计算目前手指拖动位移占屏幕总的宽高的比例,当这个比例达到3/4时,就让imageview完全显示,遮盖完全消失
let currentTranslateScaleX = offsetX / self.view.frame.width
if offsetX < ScreenWidth {
screenshotImageView.transform = CGAffineTransform(translationX: (offsetX - ScreenWidth) * 0.6, y: 0)
}
// 让遮盖透明度改变,直到减为0,让遮罩完全透明,默认的比例-(当前平衡比例/目标平衡比例)*默认的比例
let alpha = kDefaultAlpha - (currentTranslateScaleX / kTargetTranslateScale) * kDefaultAlpha
coverView.alpha = alpha
}
//MARK:结束拖动,判断结束时拖动的距离做响应的处理,并将图片和遮罩从父控件上移除
func dragEnd() {
//取出挪动的距离
let translateX = view.transform.tx
//取出宽度
let width = view.frame.size.width
if translateX <= 40 {// 如果手指移动的距离还不到屏幕的一半,往左边挪 (弹回)
UIView.animate(withDuration: 0.3, animations: {
//重要~~让被右移的view弹回归位,只要清空transform即可办到
self.view.transform = CGAffineTransform.identity
//让imageview大小恢复默认的
self.screenshotImageView.transform = CGAffineTransform(translationX: -ScreenWidth, y: 0)
//让遮盖的透明度恢复默认的alpha
self.coverView.alpha = kDefaultAlpha
}, completion: { (finished) in
//重要,动画完成之后,每次都要记得 移除两个view,下次开始拖动时,再添加进来
self.screenshotImageView.removeFromSuperview()
self.coverView.removeFromSuperview()
})
}else{// 如果手指移动的距离还超过了屏幕的一半,往右边挪
UIView.animate(withDuration: 0.3, animations: {
// 让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform
self.view.transform = CGAffineTransform(translationX: width, y: 0)
//让imageView位移还原
self.screenshotImageView.transform = CGAffineTransform(translationX: 0, y: 0)
//让遮盖alpha变为0,变得完全透明
self.coverView.alpha = 0
}, completion: { (finished) in
// 重要~~让被右移的view完全挪到屏幕的最右边,结束之后,还要记得清空view的transform,不然下次再次开始drag时会出问题,因为view的transform没有归零
self.view.transform = CGAffineTransform.identity
// 移除两个view,下次开始拖动时,再加回来
self.screenshotImageView.removeFromSuperview()
self.coverView.removeFromSuperview()
// 执行正常的Pop操作:移除栈顶控制器,让真正的前一个控制器成为导航控制器的栈顶控制器
self.popViewController(animated: false)
self.animationController.removeLastScreenShot()
})
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK:返回方法
func leftBarBtnClicked(btn:UIButton) {
self.popViewController(animated: true)
}
}

参考博客
【iOS】让我们一次性解决导航栏的所有问题