在之前的Demo中讲解过NSNotification的用法,NSNotification是使用NSOperationQueue实现的,所以使用NSNotification不可避免地会陷入内存问题,比如下面这个情况:在storyboard中准备两个场景。在第一个场景中显示一个label,旁边有一个按钮我们可以点击这个按钮modal segue到另外一个场景中,在其中放置一个textField输入新的name,用来修改第一个页面中的label显示,这是一个非常常见的功能。场景的布局如下:
创建两个控制器:ViewController和ModalViewController分别关联第一个和第二个场景。可以看见第二个场景是放在导航控制器中的,在它的右上角放一个“完成”按钮,用来返回。
第一个场景的编辑按钮点击下去之后触发modal segue到第二个场景,这个segue取名为EditSegue。
关联控制器和代码,在ModalViewController中设置一个nameToEdit属性作为模型:
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var nameLabel: UILabel!
var nameToEdit = ""
因为场景二的职责就是编辑,我需要在从场景一到场景二的时候自动选中textField,键盘滑出,所以需要的做法是在ModalViewController中的viewDidLoad方法中加入一句:
nameTextField.becomeFirstResponder()
如果有多个textField,选择合适的(一般都是最上面的)textField成为第一响应者,现在你过渡到场景二的时候看到的界面如下:
现在增加点击return关闭键盘的事件,要用到UITextField的delegate,首先遵循delegate协议:
class ModalViewController: UIViewController,UITextFieldDelegate
其次设置textField的delegate,这里有个细节,不要把设置delegate的操作写到ViewDidLoad方法中一遍加载,这是因为只有在点击Return按钮的时候才需要调用delegate方法,这样可以实现这样一个功能:如果textField中的内容没有修改的话点击Return是不能返回的。要实现这样的细节可以在属性观察器中设置textField的delegate方法:
@IBOutlet weak var nameTextField: UITextField!{
didSet{ nameTextField.delegate = self}
}
如果场景中有多个textField的话,每一个都做这样的设置。然后在delegate方法中关闭键盘:
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
注意该delegate中resignFirstResponder和viewDidLoad中的becomeFirstResponder不见得是对应的,因为所有的textField都会在点击Return时调用这个方法,所以这里关闭的是传入的textField的第一响应者身份。
现在需要添加修改模型的方法了,因为textField的作用是修改模型,所以只有在模型变化时才更新UI,所以在nameToEdit中设置属性观察者:
var nameToEdit:String?{ didSet{ updateUI() } }
updateUI方法如下:
func updateUI(){
nameTextField?.text = nameToEdit
nameLabel?.text = nameToEdit
}
千万注意updateUI()方法中一定要向可选型赋值!因为在Navigation内部的缘故,为segue做prepare的时候IBOutlet可能还没有加载完成,nameTextField和nameLabel属性是nil的。当然模型的值nameToEdit的值即为第一个场景中的name属性,所以在场景二第一次加载的时候就应该显示从第一个场景中传入的值,因此在ViewDidLoad方法中加入也加载这个方法:
override func viewDidLoad() {
super.viewDidLoad()
nameTextField.becomeFirstResponder()
updateUI()
}
在第一个场景中向第二个场景传入值:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let vc = segue.destinationViewController as? ModalViewController{
vc.nameToEdit = name.text
}
}
如果你这样写的话你会发现场景二中的nameToEdit为nil,可能你已经明白了问题所在,因为ModalViewController是包裹在NavigationController中的,所以segue的destinationViewController应该是NavigationController才对,有一个很好的办法解决这个问题:扩展UIViewController,方法如下:
extension UIViewController{
var contentViewController:UIViewController{
if let navcon = self as? UINavigationController{
return navcon.visibleViewController
} else{
return self
}
}
}
扩展一个属性,如果当前控制器是一个导航控制器则返回其展示的第一个控制器,如果不是则返回自己,现在需要修改prepareForSegue方法了,把原本的:
if let vc = segue.destinationViewController as? ModalViewController
改为:
if let vc = segue.destinationViewController.contentViewController as? ModalViewController
现在当你切换到场景二的时候label和textField都有默认值,就是场景一中的name。现在的问题是当你操作textField的时候是不会改变模型nameToEdit的实际值的,在updateUI方法中设置了label和textField的同步,可以看到现在没有调用updateUI方法,证明没有修改模型的值:
这时候NSNotification就派上用场了,我们在修改textField的时候应该是实时同步修改模型的:
func observeTextField(){
let center = NSNotificationCenter.defaultCenter()
let queue = NSOperationQueue.mainQueue()
center.addObserverForName(UITextFieldTextDidChangeNotification, object: nameTextField, queue: queue) { notification in
if let name = self.nameToEdit{
self.nameToEdit = self.nameTextField.text
}
}
}
这个方法要在合适的时机来调用,通常都放在viewDidAppear方法中,别忘了首先实现父类的方法:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
observeTextField()
}
再次运行看看,OK!已经是一个完美的MVC模式了:
或许你觉得NSNotification还有什么可以学的么?它是如此简单!请考虑下面的情况:如果场景二被移除了怎么办?因为observer是在其他线程中的,它会继续监听这个textField,而textField会被移除了,而闭包是一直存在于内存中的,它无法自己去删除自己。做法是return一小段cookie,做法如下:
var ntfObserver:NSObjectProtocol?
这里NSObjectProtocol类型的意思是在以前它被当做一个“NSObject”对象来对待。
修改observeTextField()方法,在调用单例方法addObserverForName的时候“记录”下它,得到它返回的cookie:
let ntfObserver = center.addObserverForName(UITextFieldTextDidChangeNotification, object: nameTextField, queue: queue) { notification in
if let name = self.nameToEdit{
self.nameToEdit = self.nameTextField.text
}
}
一旦完成了观察,就将其cookie从NSNotificationCneter中删除,在另一个生命周期方法中执行:
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
if let observer = ntfObserver{
NSNotificationCenter.defaultCenter().removeObserver(observer)
}
}
注意先使用可选绑定,这两个生命周期方法非常适合做这样的工作。因为在MVC移除之后,我们不希望在observer的闭包中继续持有这个对象,让它们彻底消失。
最后一个任务就是返回了,在导航栏上增加一个完成按钮,然后关联控制器:
@IBAction func done(sender: UIBarButtonItem) {
presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
由于这里是测试我们在prepare方法中传入的是String,这是一个值类型的所以拷贝了并不是原来的值,如果你传入的是一个类的实例的话,在返回时是可以看到类已经被修改了。