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

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进一步了解学习的强烈推荐阅读此书🙂

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342