作者二亮子用写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事件同时响应两个文件
File'sOwner 就是文件的所有者, 这个file就是指该Xib文件,文件的所有者就是处理这个文件所涉及的业务逻辑与交互的对象。
我们可以通过此File'sOwner 来设置他的文件所有者
这样不仅可以在toolBar.swift文件中拖UIbuttonClick事件 也可以在ViewController中去拖拽UIbuttonClick事件 这样 点击Button 两个文件下的事件都是响应
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为NormalToolBar
和EditToolBar
然后在控制器中通过父类去初始化
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!)
}
运行后就可以看到两个视图了 如下
2.4 创建bundle的两种方式(一种可以包含IB,一种不能包含IB)
- Bundle 就是一个有着固定结构的目录, 所以可以新建一个文件夹,把需要封装的资源文件复制到该目录下,然后直接给该文件夹加
.bundle
后缀名就可以了 然后如果需要查看bundle的资源,就点击右键显示包内容。如果我们想加载该bundle的资源的话就可以这样添加
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了
如图:
之所以选择macOS,是因为iOS不支持以target形式创建的Bundle,为了非让刚刚创建的的Bundle能在iOS上顺利工作,需要将Banner这个Target下的Build Setting里的的SupportedPlatment修改成iOS,
然后想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页面已经是自定义效果了
2.6 深入学习:Embed Segue
我们先来看看下图中的这种UI结构
大家应该第一眼就能看出来这种结构数据父子结构,代码大致如下
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连在一起,如下图
删除该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方法即可
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 cache
或Cocoa 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
)
outlets
与actions
就是前面提到的建立@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
才会发生回调,只有用.new
在outlet
阶段是没有回调发生的,只有初始化之后再重新赋值时,用.new
才会发生回调
- 4 调用
awakeFromNib()
方法
对nib中的一些对象调用awakeFromNib
方法,这些对象包括IB
创建的控件,例如UIVIew
的子类等,但是不包括FIle's Owner
, First 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,如下图
假设这个
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")
}
然后将 Object
和 ViewControllerManager
进行关联 如图
然后右键点击 ViewControllerManager
,再弹出的菜单中找到刚添加的法法,然后连线到控制器的按钮
此时运行App , 点击按钮 会看到这样的输出
handle something in VC
handle Something in manager
- 2 用Object 重构 “神VC”的思路
掌握了Object
的简单使用之后,在进一步来讲上面的例子,可以只将Main.storyBoard
中的ViewController
中Button
事件处理放在ViewControllerManager
中。下面来让ViewControllerManager
做更多的事情,现在可以将IB的属性也拖到 ViewControllerManager
中,这样VIewController
就不用关心该UIButton
相关的逻辑了。 这是一个意义很大的事情, 对一个类来说,属性和方法几乎是类的全部,利用Object
可以将VC的属性和方法都放在manager
中管理, 就很方便的解决了神VC的问题了
通常用一个类去承担VC的时候,我们都需要给这个类的实例传参数,而且这个实例往往设置成VC的属性, 方便任何地方使用。 同样的 我们可以把IB中的ViewControllerManager
当成一个属性拖到ViewController
中
连线后 我们就可以随时使用
ViewControllerManager
了,可以给他传递参数了,
在一些复杂的情况下,manager
知道自己服务的VC是谁, 此时可以给ViewControllerManager
一个属性指向ViewController
,但是为了防止循环引用要使用weak
修饰,如下
-
3 如何用好Object
IB中的Object
意义很大,作用也很大,掌握了Object
的用法之后,可以很灵活地运用它
在这里可以提出几个思路,希望能起一个抛砖引玉的作用,能够对大家有所启发,从而把Object
用的更好,更妙- ① 通常一个神VC会成为很多对象的
Delegate
,需要处理很多回调,此事可以用Object
替VC去实现这些Delegate
方法,例如,可以创建一个TableVIewObject.swift
专门实现TableVIew
的Delegate
和DataSource
方法 - ② 可以将一些通用的需求或交互模块化在对应的
Object
里,将这些需求或交互与VC解耦,也就是说,建立各个继承于Object
类的一些子类,每个子类实现特定的需求或交互,这些类作为基本单元存在, 当要实现一个VC时,根据需求在IB中添加不同的Object
控件,这些不同的Object
控件共同完成了该VC中的大部分功能,可以把IB中的Object
和它对应的NSObject
子类想象成一个零散的基础的积木块,把VC想象成用这些积木块搭建起来的城堡,城堡的风格不同(VC的作用不同)使用积木块的数量和种类也不同,这样就使代码的复用率很高,从而大大减少VC的代码,用IB优雅的解决了“神VC”的问题
- ① 通常一个神VC会成为很多对象的
2.9 用 External Object
重构“神VC”
External Object
是与Object
类似的东西,它的功能更加强大,但是只能用于Xib
xib中有一个External Object
更“厉害”,它可以将 Xib源文件、Xib的Files's Owner
源文件和NSObject
类的源文件三者建立关系。
新建一个项目IBExternalObjectDemo
,然后创建一个SegmentView.swift
、SegmentView.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 Object
的class
为 ViewControllerManager
,然后再Show The Attributes inspector
中将Identifier
标签值也设置为 ViewControllerManager
,如图
然后将两个按钮往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 Object
的Indentifier
标签中的值,Value是Manager
,然后生成另一个字段optionDict
,这个key:UINibExternalObjects
是固定的,只有这一个,Value
是paramDic
,接下来就是实例化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 Object
和Object
重构神VC时,一定要清楚每个类的职责是什么,切记矫枉过正,把所有的逻辑都放在Object
和External Object
中,是VC变得无足轻重,所以一定要拿捏好重构“神VC”的角度,毕竟上下文的环境大多都在改VC中。
下图展示了Object
中各个对象之间的关系
下图展示了External Object
中各个对象之间的关系
以上就是本人详读全书之后的总结,此书中还有很多细小的知识点很是值得我们学习的,有想对IB进一步了解学习的强烈推荐阅读此书🙂