UINavigationBar下方有一个1像素高的横线,这个iOS的UI风格是保持了很多年的,但是随着app的迭代,需求是变化,为了配合UI的风格,现在有非常多的app的导航栏也是动态的隐现了,有时导航栏和下方的UI要保持一个整体效果,那么原生UINavigationBar下方的横线就比较的突兀了,对于追求极致用户体验的开发和产品来说,这是不可接受的,所以今天就来研究一下横线的隐藏方式的。
对于一般的UINavigationBar,目前网上可以查到的解决方案有几种,
方式一:
self.navigationController?.navigationBar.setBackgroundImage(UIImage.init(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage.init()
复制代码
方式二:
extension UINavigationBar {
public func setLineViewHidden(hidden: Bool) {
if let shadowImg = seekLineImageView(view: self) {
shadowImg.isHidden = hidden
}
}
private func seekLineImageView(view: UIView) -> UIImageView? {
if view.isKind(of: UIImageView.classForCoder()) && view.bounds.size.height <= 1.0 {
return view as? UIImageView
}
for subView in view.subviews {
return seekLineImageView(view: subView)
}
return nil
}
}
复制代码
上面方式一,在iOS11上实验发现,其效果是navigationbar导航栏呈透明状态,底部横线没有出现,如果添加上self.navigationController?.navigationBar.isTranslucent = false
,设置为不透明,那么导航栏为白色不透明,同时底部横线也没有出现。
上面方案一这在iOS11之前是没有问题的,但是升级iOS11之后,系统增加了一些新特性,首先是self.navigationController?.navigationBar.prefersLargeTitles = true
支持大标题模式,其次是self.navigationItem.searchController
集成了searchController属性,在iOS11上进行方式一的设置,如果没有设置大标题模式也可以正常显示,但是添加了prefersLargeTitles
特性后,该页面导航栏就变成黑色的了,往上滑动回到一般导航模式又变回正常,可见iOS11上有特殊情况的处理。
方式二在没有采取self.navigationItem.searchController
的情况下,也是可以达到效果的,相对于方式一的好处是导航栏的半透明特效还保留着。但是添加了self.navigationItem.searchController
特效之后,发现在该页面上上面两个方法都失效了。经过图层定位发现,1像素的底部线挪位置了,不再navigationbar上了,而是在_UINavigationControllerPaletteClippingView
这个私有图层上了,所以这种情况下,我们采取特殊处理,我的想法和方式二类似,采取遍历的方式,代码如下:
extension UINavigationController {
@available(iOS 11.0, *)
public func setPaletteClippingViewLineHidden(hidden: Bool) -> Bool {
var result = false
for subView in self.view.subviews {
if let cls = NSClassFromString("_UINavigationControllerPaletteClippingView"),subView.isKind(of: cls) {
if let shadowImg = seekLineImageView(view: subView) {
shadowImg.isHidden = hidden
result = true
}
}
}
return result
}
private func seekLineImageView(view: UIView) -> UIImageView? {
if view.isKind(of: UIImageView.classForCoder()) && view.bounds.size.height <= 1.0 {
return view as? UIImageView
}
for subView in view.subviews {
return seekLineImageView(view: subView)
}
return nil
}
}
复制代码
原以为这样就可以解决问题了,实验结果发现,并没有解决该页面下的问题,那么问题出在哪里呢,跟踪发现,在UIViewController的viewDidLoad和viewWillAppear方法中,_UINavigationControllerPaletteClippingView
视图的subViews是空的,最后再viewDidAppear方法内才找到子视图,就是说上面的方法需要放到viewDidAppear方法内才有效,其他方法中无效。
总结一下:
1、在iOS11之前,采用方式一和二都是没问题的;
2、在iOS11之后,设置prefersLargeTitles
特性时,方式一添加isTranslucent = false
会导致navigationbar变成黑色,没有添加isTranslucent = false
则是透明导航栏;
3、iOS11之后设置self.navigationItem.searchController
后,底部线位置转移到_UINavigationControllerPaletteClippingView
上,此时需要特殊处理(见上文介绍)。
最后,在网上发现对于隐藏_UINavigationControllerPaletteClippingView
底部线,有另外一种比较hack的方式,见代码如下:
@available(iOS 11.0, *)
private func configLineView() {
let unManaged = Unmanaged<ViewController>.passRetained(self).toOpaque()
let unsafeRawPointe = UnsafeMutableRawPointer(unManaged)
var context = CFRunLoopObserverContext(version: 0, info: unsafeRawPointe , retain: nil, release: nil, copyDescription: nil)
self.observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.beforeWaiting.rawValue, true, 0, { (observer, activity, info) in
if info == nil {
return
}
let wkSelf = Unmanaged<ViewController>.fromOpaque(info!).takeUnretainedValue()
if (wkSelf.navigationController?.setPaletteClippingViewLineHidden(hidden: true))! {
} else {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), wkSelf.observerRef, CFRunLoopMode.commonModes)
}
}, &context)
CFRunLoopAddObserver(CFRunLoopGetMain(), self.observerRef, CFRunLoopMode.commonModes)
}
复制代码
实验发现,上面的方法也是可以解决问题的,其利用了RunLoop的知识,相当于在MainRunLoop中添加一个监听事件,在beforeWaiting事件时触发该block回调,内部再提取该wkSelf,再对其进行处理,相当于一种延迟处理的方式吧,但是在swift中使用指针的方式目前还是比较复杂,所以不是很推荐该方式。