美文网首页
iOS Swift 界面UI相关

iOS Swift 界面UI相关

作者: A_rcher34 | 来源:发表于2020-06-28 15:27 被阅读0次

    控制器相关

    导航栏

    • 导航栏跟随右滑手势返回
    // 第一个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 值为 LightDark

    状态栏

    • 隐藏状态栏
    1. 全局设置
      在target的info中加入View controller-based status bar appearance值为NO


      再将General->Deployment Info中的Hide status bar 勾选
    2. 在视图控制器中单独设置
      这种方法适合于只隐藏部分页面的状态栏。我们在需要隐藏 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 就能显示出多少中图形。

    1. 画曲线
    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/

    相关文章

      网友评论

          本文标题:iOS Swift 界面UI相关

          本文链接:https://www.haomeiwen.com/subject/aapgohtx.html