控制器相关
导航栏
-
导航栏跟随右滑手势返回
// 第一个ViewController
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// 这里一定要使用这个方法 否则会有问题
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
// 第二个ViewController
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
}
-
导航栏设置中间View
self.navigationItem.titleView = customView;
-
present时设置全屏
// Swift
navigationController.modalPresentationStyle = .fullScreen
// OC
navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
TabbarController
push跳转时隐藏tabbar
let nextVC = ALCourseListViewController()
nextVC.hidesBottomBarWhenPushed = true
self.navigationController?.pushViewController(nextVC, animated: true)
// 这样back回来的时候,tabBar会恢复正常显示
self.hidesBottomBarWhenPushed = false
修改tabbar图片和文字的颜色
self.tabBar.tintColor = UIColor.black
修改tabbar整体的背景色
self.tabBar.barTintColor = .white
设置tabbaritem的内容
meNav.tabBarItem.title = ALTool.localizedString("mine")
meNav.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.font: UIFont.systemFont(ofSize: 11)], for: .normal)
meNav.tabBarItem.titlePositionAdjustment = UIOffset.init(horizontal: 0, vertical: -6)
// 使用alwaysOriginal,强制渲染原图。不使用tintColor覆盖。
meNav.tabBarItem.image = ALUIImage(named: "icon_tab_mine_gray")?.withRenderingMode(.alwaysOriginal)
meNav.tabBarItem.selectedImage = ALUIImage(named: "icon_tab_mine_light")?.withRenderingMode(.alwaysOriginal)
meNav.tabBarItem.tag = 2
// 设置tabbar项,并制定默认显示位置
self.setViewControllers([nav0, nav1, meNav], animated: false)
self.selectedIndex = 0
跳转storyboard关联的VC
// xxx 是UIStoryboard的名字,不带后缀;xxxVC 是视图中Identity的StoryboardID
let destinationStoryboard = UIStoryboard(name:"xxx",bundle:nil)
let destinationViewController = destinationStoryboard.instantiateViewController(withIdentifier: "xxxVC") as! XXXViewController
self.navigationController?.pushViewController(destinationViewController, animated: true)
参考文献:
swift 使用多个storyBoard,进行视图跳转
顶部切换tab
黑色模式锁定
在target的info 中添加 User Interface Style
值为 Light
或 Dark
状态栏
-
隐藏状态栏
-
全局设置
在target的info中加入View controller-based status bar appearance
值为NO
再将General->Deployment Info中的Hide status bar
勾选
-
在视图控制器中单独设置
这种方法适合于只隐藏部分页面的状态栏。我们在需要隐藏 statusbar 的 ViewController 中添加如下代码即可。
override var prefersStatusBarHidden: Bool {
return false
}
调节状态栏颜色
在target的info中加入View controller-based status bar appearance
值为YES
在ViewController中,重写以下方法即可
override var preferredStatusBarStyle: UIStatusBarStyle {
return .default // 默认为黑色;lightContent:iOS 7.0以上可用,白色;darkContent:iOS13.0以上可用,黑色;
}
View相关
webView相关
-
UIWebView 替换为 WKWebView
UIWebView:
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error API_DEPRECATED("No longer supported.", ios(2.0, 12.0));
WKWebView:
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
UIAlertController
-
文本对齐方式
let subView1:UIView = alert.view.subviews[0]
let subView2 = subView1.subviews[0];
let subView3 = subView2.subviews[0];
let subView4 = subView3.subviews[0];
let subView5 = subView4.subviews[0];
//取title和message:
let title:UILabel = subView5.subviews[1] as! UILabel
let message:UILabel = subView5.subviews[2] as! UILabel
message.textAlignment = .left // 修改副标题对齐方式
title.textAlignment = .center // 修改主标题对齐方式
参考文献:
https://www.jianshu.com/p/51a7896d8f1c
UITextView
-
设置placeHolder
一个包含placeholder的自定义UITextView
import UIKit
class CustomTextView: UITextView {
lazy var placeHolderLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.backgroundColor = .clear
label.alpha = 0
label.tag = 999
label.lineBreakMode = .byWordWrapping
return label
}()
public var placeholder = ""
public var placeholderColor = UIColor.init(white: 0.8, alpha: 1)
init(frame: CGRect) {
super.init(frame: frame, textContainer: nil)
self.delegate = self
self.addSubview(placeHolderLabel)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
placeHolderLabel.frame = CGRect.init(x: 8, y: 8, width: self.bounds.size.width - 16, height: 0)
placeHolderLabel.font = self.font
placeHolderLabel.textColor = self.placeholderColor
if placeholder.count > 0 {
placeHolderLabel.text = placeholder
placeHolderLabel.sizeToFit()
self.sendSubviewToBack(placeHolderLabel)
}
if self.text.count == 0 && placeholder.count > 0 {
self.viewWithTag(CWTagListManager.CustomTextViewTag)?.alpha = 1
}
super.draw(rect)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
extension CustomTextView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
if placeholder.count == 0 {
return
}
if self.text.count == 0 {
self.viewWithTag(999)?.alpha = 1
} else {
self.viewWithTag(999)?.alpha = 0
}
}
}
UIButton
-
文字换行
button.titleLabel?.numberOfLines = 0
button.titleLabel?.lineBreakMode = .byWordWrapping
-
左对齐
btn.contentHorizontalAlignment = .left
UILabel
-
富文本设置行间距
let label = UILabel(frame:CGRect(x:10, y:20, width:300, height:100))
//设置允许换行
label.numberOfLines = 0
//要显示的文字
let str = "阶段\n测试"
//通过富文本来设置行间距
let paraph = NSMutableParagraphStyle()
//将行间距设置为20
paraph.lineSpacing = 20
//样式属性集合
let attributes = [NSAttributedString.Key.paragraphStyle: paraph]
label.attributedText = NSAttributedString(string: str, attributes: attributes)
self.view.addSubview(label)
参考文献:
UIButton 文字换行的一种方案
UIScrollView
-
滑动到边界后,不允许继续拖动
scrollView.bounces = false
-
顶部有空白解决方法
if #available(iOS 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
} else {
// Fallback on earlier versions
}
UITableView相关
UICollectionView相关
关于viewWithTag的坑
1、superview可以viewWithTag直接访问到subview中对应tag的控件,所以如果要标记一个控件时,同一个superview下的subview,注意不要有存在冲突的相同tag的控件,建议根据view级数来定义,比如superview级的tag用100X,子View用200x,孙view用300x,依次类推。
2、如果父view的tag和子view一样,viewWithTag得到的会是父view,因为viewWithTag得到的是最先设置tag为2000的那个控件(包含父view和子view)。
参考文献:
https://www.jianshu.com/p/5040b6e0f5a0
CAShapeLayer和UIBezierPath
CAShapeLayer 是CALayer 的子类。
let layer = CAShapeLayer()
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = CWCustomColor.colorWithRGB(red: 253, green: 198, blue: 81).cgColor
layer.lineWidth = 5
layer.lineJoin = .round
layer.lineCap = .round
layer.isHidden = true
CAShapeLayer 有一个特别好用的属性,path。我们可以里用 UIBezierPath & CAShapeLayer的 .path 属性,画出我们任意希望显示的图形。并且搭配 CAShapeLayer 的 strokeBegin & strokeEnd 属性。可以做出一些比较炫酷的图形动画效果。
UIBezierPath 专门是用来绘制路径的,常和CAShapeLayer一起配合使用。
/// 使用路径绘制 CAShape
/// 因为用到了 贝塞尔曲线,所以就可以使用核心绘图的一些参数。
/// 比如,线宽、描边、填充等。
func shapeLayerUserPath() {
let shapeLayer = CAShapeLayer()
// 创建路径
let path = UIBezierPath()
path.move(to: CGPoint(x: 10, y: 80))
path.addLine(to: CGPoint(x: 100, y: 80))
path.addLine(to: CGPoint(x: 100, y: 180))
path.addLine(to: CGPoint(x: 10, y: 180))
path.addLine(to: CGPoint(x: 10, y: 80))
// [path closePath]; // 闭合路径
// 设置 CAShapeLayer 的绘制路径
shapeLayer.path = path.cgPath
// 有路径了,就可以设置填充颜色,线的样式,描边颜色等。
shapeLayer.strokeColor = UIColor.purple.cgColor
// CAShapeLayer 如果是闭合路径,那么默认的填充颜色是黑色。
// shapeLayer.fillColor = [UIColor orangeColor].CGColor;
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.lineWidth = 10 // 线宽
shapeLayer.lineJoin = CAShapeLayerLineJoin(rawValue: "round") // 线头样式
shapeLayer.lineCap = CAShapeLayerLineCap(rawValue: "round") // 折现结合处样式
view.layer.addSublayer(shapeLayer)
}
运行效果:
当然,UIBezierPath 能够画出什么图形,那么 CAShapeLayer 就能显示出多少中图形。
- 画曲线
private func addShareLayer() {
let layer = CAShapeLayer()
let path = UIBezierPath()
// 画一条贝塞尔曲线
path.move(to: CGPoint.init(x: 0, y: UIScreen.main.bounds.height * 0.5 + 100))
path.addQuadCurve(to: CGPoint.init(x: UIScreen.main.bounds.width, y: UIScreen.main.bounds.height * 0.5 + 100), controlPoint: CGPoint.init(x: view.center.x, y: view.center.y))
// 设置曲线到 CAShapeLayer 的 path
layer.path = path.cgPath
// 实现线的基本属性
layer.strokeColor = UIColor.purple.cgColor
layer.lineWidth = 5
layer.lineCap = .round
layer.fillColor = UIColor.white.cgColor
self.view.layer.addSublayer(layer)
}
运行效果:
曲线
使用 CAShapeLayer 以动画的方式绘制图形
由于 CAShapeLayer 继承自 CALayer。CALayer 有可以搭配 CAAnimation 使用。所以可以使用 CAShapeLayer & UIBezierPath & CAAnimation 来产生比较酷炫的动画效果。
主要是搭配 CAShapeLayer 的 strokeStart & strokeEnd 来实现比较炫酷的效果。
private func shapeLayerDrawRect() {
let layer = CAShapeLayer()
let path = UIBezierPath.init(rect: CGRect.init(x: 10, y: 200, width: 100, height: 100))
layer.path = path.cgPath
layer.strokeColor = UIColor.orange.cgColor
layer.fillColor = UIColor.white.cgColor
layer.lineWidth = 3
layer.lineCap = .round
self.view.layer.addSublayer(layer)
let anim = CABasicAnimation.init(keyPath: "strokeEnd")
anim.fromValue = 0
anim.toValue = 1
anim.repeatCount = MAXFLOAT
anim.duration = 3
anim.fillMode = .forwards
anim.isRemovedOnCompletion = false
layer.add(anim, forKey: nil)
}
运行效果:
使用场景:
可以利用 CAShapeLayer 的动画特性做一些提示类的动画。由于 CAShapeLayer 不是 UIResponder ,所以,它不能接受事件。
它的作用,就是展示,也仅仅是展示。
可以利用 CAShapeLayer 的路径动画特性,做一些有功能性的动画。例如:选择答案后显示对勾错误。效果和代码可以在CAShapeLayer 初探文章中查看。
参考文献:
Core Graphics 之 路径的填充规则与混合模式
CAShapeLayer 初探
CAShapeLayer和DrawRect
- DrawRect:DrawRect属于CoreGraphic框架,占用CPU,消耗性能大
- CAShapeLayer:CAShapeLayer属于CoreAnimation框架,通过GPU来渲染图形,节省性能。动画渲染直接提交给手机GPU,不消耗内存
override func draw(_ rect: CGRect) {
// 获取当前的图形上下文
let context = UIGraphicsGetCurrentContext()
// 设置线条的属性
// 1.设置线宽
context?.setLineWidth(lineWidth_p_DP)
// 2.设置线条的颜色
context?.setStrokeColor(UIColor.brown.cgColor)
// 3.填充颜色
context?.setFillColor(UIColor.brown.cgColor)
// 开始画线,需要将起点移动到指定的point
context?.move(to: firstPoint_p_DP)
// 添加一根线到另一个点 (两点一线)
context?.addLine(to: secondPoint_p_DP)
context?.addLine(to: thirdPoint_p_DP)
// 闭合路径,连线结束后会把起点和终点连起来
context?.closePath()
// 奇偶规则:从路径覆盖范围内的任意一点做一条射线(确保这条射线的长度要比路径覆盖范围要大) , 如果与该射线相交的边的数量为奇数, 则该点是路径的内部点, 反之该点则是路径的外部点。
// 非零环绕数原则:首先定义一个用于焦点统计的count值,然后从路径覆盖范围内的任意一点做一条射线(确保这条射线的长度要比路径覆盖范围要大). 然后我们对每一条和该射线相交的路径进行统计, 统计规则是这样的: 当路径是从右向左穿过射线的时候, count++, 当路径是从左向右穿过射线的时候, count--. 当我们统计完所有相交的路径后, 如果 count不为0, 则该点是内部点, 该点所在的封闭区域需要填充, 反之该点则是路径的外部点
// 混合模式:混合模式是指在进行绘制时如何使用绘制背景的方式!Quartz2D中使用默认的混合方式,并使用以下公式将背景画和前景画进行结合:result = (alpha * foreground) + (1 - alpha) * background 、alpha表示颜色的不透明值
// 使用CGPathDrawingMode绘制模式绘制当前路径(fill :使用非零环绕路径渲染规则;eoFill:奇偶渲染规则;stroke:沿着路径渲染一条线;fillStroke:先按照非零环绕进行填充然后进行绘制路径;eoFillStroke:先按照奇偶规则填充,然后进行绘制路径)
mainPath?.drawPath(using: .fillStroke)
// 使用CGPathFillRule填充规则(winding 非零环绕规则;evenOdd 奇偶规则)
mainPath?.fillPath(using: .winding)
// 渲染图形到上下文
context?.strokePath()
}
参考文献:
iOS CAShapeLayer 使用
layoutSubviews和drawRect
一、layoutSubviews在以下情况下会被调用:
1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。
3、改变一个UIView的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView引发UIView的重新布局会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
6、直接调用setNeedsLayout 或者 layoutIfNeeded。
二、drawRect在以下情况下会被调用:
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect 掉用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在 控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
以上1,2推荐;而3,4不提倡
参考文献:
https://blog.csdn.net/wangyanchang21/article/details/50774522
离屏渲染
油画算法
图层的绘制,遵循油画算法
。即按层绘制。先绘制距离较远的场景,再绘制较近的场景并覆盖较远的部分。如下图
这样就不会使较远的物体挡住较近的物体。但是有一个局限,就是无法在较近的一层渲染完后,再回去修改较远的图层,因为较远的图层已经被覆盖了。这时候就涉及到了离屏渲染。
离屏渲染
对于上述有前后依赖的图层(如全局剪切,阴影等),油画算法无法满足。这时可以另开辟一个空间,用于临时渲染,渲染完成后再渲染到当前的缓冲区上。这个临时渲染,就是离屏渲染
。
因为离屏渲染,需要开辟新的空间,并且共享同一个上下文,还需要做上下文切换,并且渲染完后还要进行拷贝操作。所以会消耗一定的资源,当离屏渲染过多时,则会导致GPU渲染时间过长而发生卡顿,所以应该避免离屏渲染。
如何避免离屏渲染
若想避免离屏渲染,首先要知道如何检测离屏渲染。在Simulator的Debug中打开Color Off-screen Rendered。
// 1. UIImageView
let imageView = UIImageView(frame: CGRect(x: 50, y: 100, width: 300, height: 200))
self.view.addSubview(imageView)
imageView.image = UIImage.init(named: "test.jpg")
// image + cornerRadius + masksToBounds 不会触发离屏渲染
imageView.layer.cornerRadius = 10
imageView.layer.masksToBounds = true
// 触发离屏渲染
imageView.backgroundColor = UIColor.green
// 添加一个空的UIView不会触发离屏渲染
// imageView.addSubview(UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10)))
// 2. UIButton
let button = UIButton(type: .custom)
button.frame = CGRect(x: 50, y: 300 + 50, width: 300, height: 50)
self.view.addSubview(button)
button.setTitle("Test", for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.layer.cornerRadius = 10
button.layer.masksToBounds = true
// 触发离屏渲染
button.backgroundColor = UIColor.green
// 触发离屏渲染
button.setBackgroundImage(UIImage(named: "test.jpg"), for: .normal)
// 3. UIView
let view = UIView(frame: CGRect(x: 50, y: 400 + 50, width: 300, height: 50))
self.view.addSubview(view)
view.backgroundColor = UIColor.red
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
// label如果被渲染,则会触发渲染,如果text为空不会被渲染
let label = UILabel(frame: CGRect(x: 10, y: 10, width: 1, height: 1))
label.text = "1"
view.addSubview(label)
离屏渲染的区域为黄色
根据上述方法测试,可以得到是否触发离屏渲染的情况:
设置了cornerRadious+masksToBounds的:
- UIImageView设置图片,不会触发;
- UIView设置背景颜色,如果没有subViews,不会触发;
- UILabel设置文字,且设置backgroundColor,会触发;
- UIButton设置文字和背景,会触发;
其他会触发离屏渲染的情况: - 使用了遮罩的layer(layer.mask)
- 需要进行裁剪的layer(layer.masksToBounds / view.clipsToBounds)
- 设置了组透明度为 YES,并且透明度不为 1 的layer (layer.allowsGroupOpacity / layer.opacity)
- 添加了投影的 layer (layer.shadow),但如果设置了shadowPath,则系统已经知道如何绘制阴影了,不会触发离屏渲染
- 采用了光栅化的 layer (layer.shouldRasterize),光栅化也可以优化离屏渲染问题
- 绘制了文字的 layer (UILabel, CATextLayer, CoreText等)
- 使用了毛玻璃/高斯模糊
优化离屏渲染问题
1、避免使用裁切(masksToBounds)方式,如果确保内容不会溢出,则不宜使用masksToBounds;
2、必须使用裁切时,尽量用最外层的view去裁切。因为裁切需要对所有的layer和subviews所有图层进行裁切,越内层的view,离屏渲染所需要的空间越大。
3、提前切好需要的圆角,避免需要的时候再切。
参考文献:
https://blog.bombox.org/2020-07-14/ios-offscreen-render/
网友评论