美文网首页iOS进阶iOS开发技术
《iOS UI开发捷径》之重新认识IB

《iOS UI开发捷径》之重新认识IB

作者: 奥卡姆剃须刀 | 来源:发表于2017-12-03 23:59 被阅读208次
    11511686475_.pic.jpg
    作者二亮子用写IB省出的时间出了这本书,这本书确实是一本好书,本文章根据书中的内容总结了IB开发中自己不熟悉或者是不常用的知识点。

    一 来看一下IB开发的优点以及缺点

    1.1 优点
    • 1.1.1 开发和维护效率高
      IB开发与纯代码开发相比, 效率至少提高两倍 这也就是为什么作者在业余时间能写出这本书的原因😁
    • 1.1.2 减少大量的UI代码和“胶水代码”
      IB 开发与纯代码开发相比,代码量至少减少三分之一
    • 1.1.3 适配变得十分简单
    • 1.1.4 IB也可以做一些非UI的事情
      例如可以用IB中的Object重新组织VC的业务逻辑,减少一下不必要的代码,
    • 1.1.5 利用IB学习控件可以达到事半功倍的效果
    1.2 缺点
    • 1.2.1 IB的执行效率没有纯代码高
      这是一个不争的事实,IB加载UI可以简单理解为两个过程:首先要把xib或sb文件对应的nib或storyboardc文件加载到内存中, 然后利用这些数据去生成UI页面,加以显示。而纯代码只需要一个过程, 这个过程类似于IB加载UI的第二个过程,直接在内存中生成UI页面加以显示

    • 1.2.2 使用IB开发过程中容易出现一些小的问题
      用IB开发确实是会遇见一些小问题,可能这些小问题用代码开发就不会出现,所以如果遇到因为IB开发的问题的话,就把IB的这些坑记录下来,这是一个很好的学习习惯

    • 1.2.3 文件容易冲突

    • 1.2.4 没有代码表达清晰

    • 1.2.5 不利于代码的封装和工程架构的组织

    二 IB开发中的技巧

    2.1 xib是可以不依赖于源文件而单独使用的,纯粹的“死”UI可以只用一个xib文件展示,无需使它与源文件关联
    2.2 理解File's Owner 使用File'sOwner 让xib中的button事件同时响应两个文件
    WechatIMG1.jpeg

    File'sOwner 就是文件的所有者, 这个file就是指该Xib文件,文件的所有者就是处理这个文件所涉及的业务逻辑与交互的对象。
    我们可以通过此File'sOwner 来设置他的文件所有者


    image.png

    这样不仅可以在toolBar.swift文件中拖UIbuttonClick事件 也可以在ViewController中去拖拽UIbuttonClick事件 这样 点击Button 两个文件下的事件都是响应


    image.png
    2.3 封装xib

    可以把loadNibNamed(_:owner:option:)方法封装到源文件的一个类中,源文件派生出几个子类,根据不同情况加载并返回不同的子类。可以使用工厂设计模式

    // 我们创建一个父类
    class ToolBar: UIView {    
        class func  toolBar(type:ToolBarType)-> ToolBar? {    
            if type == .normal {
                return Bundle.main.loadNibNamed("ToolBar", owner: nil, options: nil)?[0] as? ToolBar
            }else if type == .edit {
                return Bundle.main.loadNibNamed("ToolBar", owner: nil, options: nil)?[1] as? ToolBar
            }else {            
                return nil;
            }
        }
        override func awakeFromNib() {
            super.awakeFromNib()
            handleEvent()
        }
        func handleEvent() {
            // 子类重写
        }
    }
    // 实例化两个子类并设置颜色
    class NormalToolBar: ToolBar {
        override func handleEvent() {
            backgroundColor = UIColor.red
        }
    }
    class EditToolBar: ToolBar {
        override func handleEvent() {
            backgroundColor = UIColor.yellow
        }
    }
    

    然后在ToolBar.xib中添加两个VIew 分别更改他们的class为NormalToolBarEditToolBar

    image.png

    然后在控制器中通过父类去初始化

    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let normalToolBar = ToolBar.toolBar(type: ToolBarType.normal)
            normalToolBar?.frame.origin = CGPoint(x: 0, y: 100)
            view.addSubview(normalToolBar!)
            
            let editToolBar = ToolBar.toolBar(type: ToolBarType.edit)
            editToolBar?.frame.origin = CGPoint(x: 0, y: 200);
            view.addSubview(editToolBar!)                
        }
    
    

    运行后就可以看到两个视图了 如下

    image.png
    2.4 创建bundle的两种方式(一种可以包含IB,一种不能包含IB)
    • Bundle 就是一个有着固定结构的目录, 所以可以新建一个文件夹,把需要封装的资源文件复制到该目录下,然后直接给该文件夹加.bundle后缀名就可以了 然后如果需要查看bundle的资源,就点击右键显示包内容。如果我们想加载该bundle的资源的话就可以这样添加
      image.png
            let imageBundle = Bundle.main.path(forResource: "image", ofType: "bundle")
            let imagePath = imageBundle! + "/icon.png"
            let image = UIImage.init(contentsOfFile: imagePath)
    

    用上述的方式创建的bundle可以放几乎所有的资源, 除了IB文件,因为IB文件在工程编译后会被序列化为二进制的nib和storyboardc文件,而修改文件夹后缀名的方式创建的Bundle是静态的其内部的资源不参与项目编译

    • 创建一个基于macOSBundle的target来获得Bundle
      这种方式可以把其中的XIB和SB序列化为二进制的nib和storyboardc文件。创建Bundle的Target的方式是,点击菜单栏中的file->New->Target,在弹出的菜单中选择macOS->Framework&Library->Bundle,就可以创建除一个bundle了
      如图:


      image.png

      之所以选择macOS,是因为iOS不支持以target形式创建的Bundle,为了非让刚刚创建的的Bundle能在iOS上顺利工作,需要将Banner这个Target下的Build Setting里的的SupportedPlatment修改成iOS,

    image.png

    然后想Banner这个Target下添加xib文件,这样就可以在Xcode左上角的scheme选择Banner这个Target编译了,此Banner.Bundle 就可以直接复制到其他工程中使用了

    2.5 自定义的segue

    自定义的segue需要在segue菜单中选择custom选项, 然后再Class标签里指定一个UIStoryboardSegue子类的类名,这个子类必须实现perform方法, 自己完成segue跳转的过程,如果选择了segue,但是并没有在class标签中指定任何 UIStoryboardSegue的子类,那么App运行该segue是会crash,还是以A页面跳转到B页面为例, 来说明一下自定义的segue,要自定义segue,就要继承于UIStoryboardSegue,写一个子类,这个暂且叫做CustomSegue,然后重写perform方法

    class CustomSegue: UIStoryboardSegue {
        override func perform() {
            let svc = source
            let dvc = destination
            dvc.view.frame = svc.view.frame
            dvc.view.alpha = 0.0
            svc.view.addSubview(dvc.view)
            UIView.animate(withDuration: 0.3, animations: {
                dvc.view.alpha = 1.0
            }) { (flag:Bool) in
                svc.navigationController?.pushViewController(dvc, animated: false)
            }
        }
    }
    

    在perform里简单的实现了一个渐显的效果来显示B页面。准备好CustomSegue之后,“拖”一个从A页面的Button到B页面的segue,在弹出的segue菜单中选择一个Custom选项,然后把Class标签设置成CustomSegue,运行App就会发现,从A页码跳转到B页面已经是自定义效果了

    CustomSegue.gif
    2.6 深入学习:Embed Segue

    我们先来看看下图中的这种UI结构

    IMG_341B14EFB937-1.jpeg

    大家应该第一眼就能看出来这种结构数据父子结构,代码大致如下

            let testVC = TestViewController.init()        
            self.view.addSubview(testVC.view)
            self.addChildViewController(testVC)        
    

    而IB中的Embed Segue就是专门解决这种VC嵌套的。在右边栏下面的Show the Object Library中找到Container View,拖 到 View Contwoller的控件显示区域,会看到Container View与另一个 View Contwoller通过Segue连在一起,如下图

    image.png

    删除该Segue箭头指向的 View Contwoller,选中Container View,将segue拖到希望添加的子VC上,在弹出的菜单中选择Embed,注意这里只能选择Embed,当选择了Embed后,你会发现子VC的大小和Container View的大小一样了, 此时改变Container View的大小, 子VC的大小也随之改变, 将Container View调整到合适的尺寸运行App,会发现Container View所在的区域已经变成了子VC,点击子VC的上的按钮,可以正常处理事件,这说明了Embed Segue执行了容器VC的 addChildVIewController,将子VC自动添加到容器VC的ChildVIewController中,整个嵌套过程操作十分简单,Embed Segue的优势不仅体现在不用实例化子VC,不用自己添加到ChildVIewController中,而且可以在IB中调整Container View的frame,给他添加必要的约束,这也是它优势的一个重要体现

    2.7 深入学习:Unwind Segue

    在开发中可能遇到这样的需求,从A页面跳转到B页面,在B页面选择或者填写一些数据后,在回到A页面获取刚刚在B页面选择或填写的数据加以显示;这中需求相信大家都做过无数遍了吧,代理,block,通知等什么方式都可以做到的,现在来学习一下用Unwind Segue的方式
    Unwind Segue 提供了一种从一个页面返回到上一个页面时的回调, 可以利用这个特性,简单优雅的实现页面间的反向传值。这个回调可以由系统自动触发,也可以手动触发,只要在回到的页面里添加一个类似于下边的代码

        @IBAction func handleUnWindSegue(unwindSegue: UIStoryboardSegue) {
            if unwindSegue.identifier == "unwindB" {
                if let svc = unwindSegue.source as? BViewController {
                    print("data fromB : \(svc.textF.text)")
                }
            }
        }
    

    然后再SB中选择要返回到上一个页面的Button,按住control将其拖动到Exit的位置(如下图),在弹出的菜单中选择 handleUnWindSegue方法即可


    image.png
    2.7 IB文件的加载过程(分为5步)

    先看一下两种加载IB的方式

    // 第一种
      let testView = Bundle.main.loadNibNamed("LLTestView", owner: nil, options: nil)?[0] as! UIView
            
    // 第二种
    let testViewNib = UINib.init(nibName: "LLTestView", bundle: Bundle.main)
    let testView = testViewNib.instantiate(withOwner: nil, options: nil)[0] as! UIView                
    

    以上两种方式都包括了这5个过程,下边详细介绍这5个教程

    • 1 将nib加载到内存
      该过程会将nib中的对象和对象所引用的资源加载到内存,例如,在nib中引用了图片, 声音等资源文件,该过程会把这些资源加载到相应的Cocoa Image cacheCocoa sound cache中, 前面说过, 从xib到nib的过程叫做序列化,是将XML格式的plist文件序列化为二进制格式的plist,该过程虽然将nib种的对象加载到了内存,但是没有进行反序列化

    • 2 解雇化 并实例化nib文件里对应的对象
      该过程会将上面加载到内存中的对象进行反序列化,该过程会调用初始化,这里注意,虽然这些对象大多数都是UIVIew类,UIViewController类,或者是它们的子类,但是这些对象通过IB进行初始化,并不会调用init(frame:)或者普通的init方法。UIVIew及其子类会调用Init(coder:)的方法,UIVIewController及其子类会调用Init(nibName:bundle:)的方法,而如果nib中存在Object或者External Object对象,那么会调用这些对象所在类的init方法,经过这一步后,才真正把“数据”变成了“对象”

    • 3 建立 connections(outlets, actions)
      outletsactions 就是前面提到的建立@IBOutlet就与@IBAction的连接。建立Connections的顺序为,先建立outlets连接,然后建立actions连接。建立 outlet连接到过程用到了setValue:forKey:方法,同时建立outlet过程支持KVO,如有有一个属性:

    @IBAction weak var testView : UIView!
    

    那就就可以注册该属性,通过KVO的回调得知outlet建立关系的时刻:

    self.addObserver(self, forKeyPath: "testView", options: .initial, context: nil)
    

    这里注意,因为是初始化阶段,所以options必须有.initial才会发生回调,只有用.newoutlet阶段是没有回调发生的,只有初始化之后再重新赋值时,用.new才会发生回调

    • 4 调用awakeFromNib()方法

    对nib中的一些对象调用awakeFromNib方法,这些对象包括IB创建的控件,例如UIVIew的子类等,但是不包括FIle's OwnerFirst Response,placeholder object

    • 5 将nib中可见的控件显示出来
    2.8 用 Object 重构 “神VC”

    背景: 在开发中, 大家或许遇到过业务和UI都很复杂的页面,这样的页面往往对应了一个代码量庞大,结构臃肿,可维护性差的VC, 这样的VC通常称之为“神VC”, “神VC”一般什么事情都自己做,事无巨细, 如何重构它往往都是我们的一个“心病”,重构思路一般都是用适合的设计模式,将“神VC”的一些工作和职能拆分到其他类,化整为零,使结构更加清晰,
    下面说一下如何利用IB中的Object来重构“神VC”

    • 1 使用 Object
      我们新建一个IBObjectDemo的工程,然后在Main.storyboard中的ViewController 下添加一个Object,注意,要将其 “拖”到IB左边栏或者Scene Dock中才可以添加一个Object,如下图

      image.png

      假设这个ViewController 是一个神VC,为了重构这个神VC,我们新建一个VIewControllerManage.swift类,该类负责处理ViewController.swift中的某一类业务逻辑或交互。
      现在ViewControllerManager.swift中添加如下方法:

    class ViewControllerManager: NSObject {
        @IBAction func handleSomethingForViewController() {        
            print("handle Something in manager")                
        }
    }
    

    Main.storyboard中的Viewcontroller中放一个按钮,然后再文件中添加对应的点击事件

        @IBAction func handleSomething(_ sender: UIButton) {        
            print("handle something in VC")        
        }
    

    然后将 ObjectViewControllerManager进行关联 如图

    image.png

    然后右键点击 ViewControllerManager ,再弹出的菜单中找到刚添加的法法,然后连线到控制器的按钮

    image.png

    此时运行App , 点击按钮 会看到这样的输出

    handle something in VC
    handle Something in manager
    
    • 2 用Object 重构 “神VC”的思路

    掌握了Object的简单使用之后,在进一步来讲上面的例子,可以只将Main.storyBoard中的ViewControllerButton事件处理放在ViewControllerManager中。下面来让ViewControllerManager做更多的事情,现在可以将IB的属性也拖到 ViewControllerManager 中,这样VIewController就不用关心该UIButton相关的逻辑了。 这是一个意义很大的事情, 对一个类来说,属性和方法几乎是类的全部,利用Object可以将VC的属性和方法都放在manager中管理, 就很方便的解决了神VC的问题了

    image.png

    通常用一个类去承担VC的时候,我们都需要给这个类的实例传参数,而且这个实例往往设置成VC的属性, 方便任何地方使用。 同样的 我们可以把IB中的ViewControllerManager 当成一个属性拖到ViewController

    image.png
    连线后 我们就可以随时使用ViewControllerManager了,可以给他传递参数了, image.png

    在一些复杂的情况下,manager知道自己服务的VC是谁, 此时可以给ViewControllerManager一个属性指向ViewController,但是为了防止循环引用要使用weak修饰,如下

    image.png
    • 3 如何用好Object
      IB中的Object意义很大,作用也很大,掌握了Object的用法之后,可以很灵活地运用它
      在这里可以提出几个思路,希望能起一个抛砖引玉的作用,能够对大家有所启发,从而把Object用的更好,更妙
      • ① 通常一个神VC会成为很多对象的Delegate,需要处理很多回调,此事可以用Object替VC去实现这些Delegate方法,例如,可以创建一个TableVIewObject.swift专门实现TableVIewDelegateDataSource方法
      • ② 可以将一些通用的需求或交互模块化在对应的Object里,将这些需求或交互与VC解耦,也就是说,建立各个继承于Object类的一些子类,每个子类实现特定的需求或交互,这些类作为基本单元存在, 当要实现一个VC时,根据需求在IB中添加不同的Object控件,这些不同的Object控件共同完成了该VC中的大部分功能,可以把IB中的Object和它对应的NSObject子类想象成一个零散的基础的积木块,把VC想象成用这些积木块搭建起来的城堡,城堡的风格不同(VC的作用不同)使用积木块的数量和种类也不同,这样就使代码的复用率很高,从而大大减少VC的代码,用IB优雅的解决了“神VC”的问题
    2.9 用 External Object 重构“神VC”

    External Object 是与Object类似的东西,它的功能更加强大,但是只能用于Xib
    xib中有一个External Object更“厉害”,它可以将 Xib源文件、Xib的Files's Owner 源文件和NSObject类的源文件三者建立关系。

    新建一个项目IBExternalObjectDemo,然后创建一个SegmentView.swiftSegmentView.xib 和一个ViewControllerManager.swift 文件,然后再SegmentView.xib中添加两个按钮,往SegmentView.swift文件中连线回调方法

        @IBAction func handleSelect(_ sender: UIButton) {
            print("handle select in SegmentView")
        }
    

    然后再SegmentView.Xib 中选中File's Owner,再Show the Identify inspector 中将class 改为 ViewController,然后再讲两个按钮像控制器中连线回调方法

        @IBAction func handleSegmentChanges(_ sender: UIButton) {
            print("handle select In VC")
        }
    

    重点是是 External Object,向SegmentView中拖入External Object,然后更改External ObjectclassViewControllerManager,然后再Show The Attributes inspector 中将Identifier标签值也设置为 ViewControllerManager,如图

    image.png
    image.png

    然后将两个按钮往ViewControllerManager中脱线回调方法

        @IBAction func managerSegmentView(_ sender: UIButton) {        
            print("handle select In manager")
        }
    

    然后再控制器中初始化SegmentView 并添加再控制器上

        let manager = VIewControllerManager()    
        override func viewDidLoad() {
            super.viewDidLoad()
            let paramDic = ["VIewControllerManager" : manager]
            let optionDict = [UINibExternalObjects : paramDic]
            let segmentVIew = Bundle.main.loadNibNamed("SegmentView", owner: self, options: optionDict)?[0] as! SegmentView
            segmentVIew.center = view.center
            view.addSubview(segmentVIew)        
        }
    

    首先初始化一个VIewControllerManager实例作为VC的Manager属性, 然后生成一个字典,这个字典的key 是 VIewControllerManager,就是segmentVIew.xib中的External ObjectIndentifier标签中的值,Value是Manager,然后生成另一个字段optionDict,这个key:UINibExternalObjects是固定的,只有这一个,ValueparamDic,接下来就是实例化xib,将optionDict传入options这个参数中,而这个参数就是制定External Object,运行代码 点击按钮, 输出一下结果

    handle select In VC
    handle select in SegmentView
    handle select In manager
    

    我们就可以用上述的思路将“神VC”中的功能分在三个模块中完成,重构“神VC”的主要思路就是将该类的代码清晰,合理的分散在其他类中,让每个类仅仅处理自己的职责,各司其职。

    Object 和 External Object总结
    Object可以用于xib 和 sb,而External Object只能用于sb,两者的相似之处是都提供了一种可以将VC中的代码放到其他类中的途径,这里的其他类必须是NSObject的子类,当用Swift开发时要注意到这一点,当用External ObjectObject重构神VC时,一定要清楚每个类的职责是什么,切记矫枉过正,把所有的逻辑都放在ObjectExternal Object中,是VC变得无足轻重,所以一定要拿捏好重构“神VC”的角度,毕竟上下文的环境大多都在改VC中。
    下图展示了Object中各个对象之间的关系

    Object中各个对象之间的关系.png

    下图展示了External Object中各个对象之间的关系

    External Object中各个对象之间的关系.png

    以上就是本人详读全书之后的总结,此书中还有很多细小的知识点很是值得我们学习的,有想对IB进一步了解学习的强烈推荐阅读此书🙂

    相关文章

      网友评论

      本文标题:《iOS UI开发捷径》之重新认识IB

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