混用Swift和objc - 使用Cocoa设计模式

Cocoa 中已有的设计模式很有用,但这些模式很多依赖于 objc 的类。由于 objc 与 Swift 有着互操作性,所以也可以在 Swift 中使用这些通用的模式。

Delegation

Swift 使用代理的步骤:

  1. 检查myDelegate不为nil
  2. 检查myDelegate实现了方法window:willUseFullScreenContentSize:
  3. 如果 1 和 2 都为 ture,就调用方法,并把结果赋给fullScreenSize
  4. 打印返回值
1
2
3
4
5
6
7
8
9
class MyDelegate: NSObject, NSWindowDelegate {
func window(_ window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize) -> NSSize {
return proposedSize
}
}
myWindow.delegate = MyDelegate()
if let fullScreenSize = myWindow.delegate?.window(myWindow, willUseFullScreenContentSize: mySize) {
print(NSStringFromSize(fullScreenSize))
}

Lazy Initialization

在 objc 中的延迟初始化:

1
2
3
4
5
6
7
8
9
@property NSXMLDocument *XML;
- (NSXMLDocument *)XML {
if (_XML == nil) {
_XML = [[NSXMLDocument alloc] initWithContentsOfURL:[[Bundle mainBundle] URLForResource:@"/path/to/resource" withExtension:@"xml"] options:0 error:nil];
}
return _XML;
}

Swift 中对于存储型变量的延迟初始化可以用lazy修饰:

1
lazy var XML: XMLDocument = try! XMLDocument(contentsOf: Bundle.main.url(forResource: "document", withExtension: "xml")!, options: 0)

由于在访问一个完全初始化的实例时,延迟属性才会计算,它可能在它的初始化表达式中访问常量或变量属性:

1
2
var pattern: String
lazy var regex: NSRegularExpression = try! NSRegularExpression(pattern: self.pattern, options: [])

对于需要初始化之外的其他额外工作的值,可以将一个返回初始化值的闭包赋值给这个变量。

1
2
3
4
5
6
lazy var currencyFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencySymbol = "¤"
return formatter
}()

如果一个延迟属性还未初始化,并且同时被多个线程访问,那么不能保证它只被初始化一次。

Error Handling

在 objc 中,访问通过传递 NSError 指针值来获取错误信息。Swift 会将其自动转化为原生的错误处理方式。

1
2
- (BOOL)removeItemAtURL:(NSURL *)URL
error:(NSError **)error;
1
func removeItem(at: URL) throws {}

Catching and Handling an Error

objc 中这么处理错误:

1
2
3
4
5
6
7
8
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *fromURL = [NSURL fileURLWithPath:@"/path/to/old"];
NSURL *toURL = [NSURL fileURLWithPath:@"/path/to/new"];
NSError *error = nil;
BOOL success = [fileManager moveItemAtURL:fromURL toURL:toURL error:&error];
if (!success) {
NSLog(@"Error: %@", error.domain);
}

Swift 中等价的代码:

1
2
3
4
5
6
7
8
let fileManager = FileManager.default
let fromURL = URL(fileURLWithPath: "/path/to/old")
let toURL = URL(fileURLWithPath: "/path/to/new")
do {
try fileManager.moveItem(at: fromURL, to: toURL)
} catch let error as NSError {
print("Error: \(error.domain)")
}

可以用catch子句来匹配具体的错误:

1
2
3
4
5
6
7
do {
try fileManager.moveItem(at: fromURL, to: toURL)
} catch CocoaError.fileNoSuchFile {
print("Error: no such file exists")
} catch CocoaError.fileReadUnsupportedScheme {
print("Error: unsupported scheme (should be 'file://')")
}

Converting Errors to Optional Values

Swift 中,用try?将抛出方法变为返回一个可选值,然后检查值是否为nil

1
2
3
4
5
6
7
8
9
10
11
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *tmpURL = [fileManager URLForDirectory:NSCachesDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:YES
error:nil];
if (tmpURL != nil) {
// ...
}
1
2
3
4
let fileManager = FileManager.default
if let tmpURL = try? fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
// ...
}

Throwing an Error

发生错误时,objc 与 Swift 的做法:

1
2
3
4
5
6
// an error occurred
if (errorPtr) {
*errorPtr = [NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorCannotOpenFile
userInfo:nil];
}
1
2
// an error occurred
throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, userInfo: nil)

如果 objc 调用了一个抛出错误的 Swift 方法,这个错误会自动变为 objc 方法中的错误指针参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SerializedDocument: NSDocument {
static let ErrorDomain = "com.example.error.serialized-document"
var representedObject: [String: Any] = [:]
override func read(from fileWrapper: FileWrapper, ofType typeName: String) throws {
guard let data = fileWrapper.regularFileContents else {
throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, userInfo: nil)
}
if case let JSON as [String: Any] = try JSONSerialization.jsonObject(with: data, options: []) {
self.representedObject = JSON
} else {
throw NSError(domain: SerializedDocument.ErrorDomain, code: -1, userInfo: nil)
}
}
}

如果这个方法抛出了错误,Swift 调用的话,错误会转移到调用方作用域;objc 调用的话,错误会变成指针参数。

objc 中如果不提供错误指针,错误就会被忽略,而 Swift 中调用抛出方法需要有显示的错误处理。

如果 objc 方法发生错误,Swift 无法处理,会触发一个运行时错误。所以 objc 中的错误必须在 objc 中处理。

Key-Value Observing

Swift 要使用 KVO,类必须继承 NSObject。步骤如下:

  1. 对于要观察的变量,使用dynamic修饰:
1
2
3
4
5
6
class MyObjectToObserve: NSObject {
dynamic var myDate = NSDate()
func updateDate() {
myDate = NSDate()
}
}
  1. 创建一个全局上下文变量:
1
private var myContext = 0
  1. 添加一个观察者,重写observeValue(for:of:change:context:)方法,并且在deinit中移除观察者:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyObserver: NSObject {
var objectToObserve = MyObjectToObserve()
override init() {
super.init()
objectToObserve.addObserver(self, forKeyPath: #keyPath(MyObjectToObserve.myDate), options: .new, context: &myContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &myContext {
if let newValue = change?[.newKey] {
print("Date changed: \(newValue)")
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
deinit {
objectToObserve.removeObserver(self, forKeyPath: #keyPath(MyObjectToObserve.myDate), context: &myContext)
}
}

Undo

在 app 的事件响应链中,UIResponder的子类有一个只读属性undoManagerNSUndoManager维护着 app 的撤销栈。

NSUndoManager提供两种方式来注册撤销操作:一种“简单撤销”,调用一个带有一个对象参数的选择子;另一个是“基于调用的撤销”,用一个NSInvocation对象来接收任意的参数。

有一个Task模型,ToDoListController用它来展示任务列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Task {
var text: String
var completed: Bool = false
init(text: String) {
self.text = text
}
}
class ToDoListController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
@IBOutlet var tableView: NSTableView!
var tasks: [Task] = []
// ...
}

对于接收多个参数的方法,可以用NSInvocation来创建一个撤销操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@IBOutlet var remainingLabel: NSTextView!
func mark(task: Task, asCompleted completed: Bool) {
if let target = undoManager?.prepare(withInvocationTarget: self) as? ToDoListController {
target.mark(task: task, asCompleted: !completed)
undoManager?.setActionName(NSLocalizedString("todo.task.mark", comment: "Mark As Completed"))
}
task.completed = completed
tableView.reloadData()
let numberRemaining = tasks.filter{ $0.completed }.count
remainingLabel.string = String(format: NSLocalizedString("todo.task.remaining", comment: "Tasks Remaining: %d"), numberRemaining)
}

prepare(withInvocationTarget:)方法对于特定的target返回一个代理。通过转换ToDoListController,返回值可以直接进行对应的调用mark(task:asCompleted:)

Singleton

在 objc 中创建单例:

1
2
3
4
5
6
7
8
9
+ (instancetype)sharedInstance {
static id _sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}

在 Swift 中,可以简单的用一个 static 类型的变量,它保证仅进行一次的延迟初始化,即使在多个线程中同时访问。

1
2
3
class Singleton {
static let sharedInstance = Singleton()
}

如果想要执行除了初始化的额外步骤,可以将闭包的执行结果复制给全局常量:

1
2
3
4
5
6
7
class Singleton {
static let sharedInstance: Singleton = {
let instance = Singleton()
// setup code
return instance
}()
}

Introspection

Swift 中用is操作符检查对象类型,as?用来向下转换类型。

1
2
3
4
5
6
if object is UIButton {
// object is of type UIButton
} else {
// object is not of type UIButton
}
1
2
3
4
5
if let button = object as? UIButton {
// object is successfully cast to type UIButton and bound to button
} else {
// object could not be cast to type UIButton
}

检查是否遵循或转换为某个协议的方法与类型的检查和转换相同:

1
2
3
4
5
if let dataSource = object as? UITableViewDataSource {
// object conforms to UITableViewDataSource and is bound to dataSource
} else {
// object not conform to UITableViewDataSource
}

Serializing

objc 中,可以用 Foundation 框架中的类NSJSONSerialiationNSPropertyListSerialization从 JSON 或列表型的序列化值——通常是NSDictionary<NSString *, id>中初始化对象。在 Swift 中相同,但它需要额外的类型转换。

如转换Venue结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Foundation
import CoreLocation
struct Venue {
enum Category: String {
case entertainment
case food
case nightlife
case shopping
}
var name: String
var coordinates: CLLocationCoordinate2D
var category: Category
}

收到的 JSON 消息可能是这样:

1
2
3
4
5
6
7
8
{
"name": "Caffè Macs",
"coordinates": {
"lat": 37.330576,
"lng": -122.029739
},
"category": "food"
}

待填坑…

Localization

待填坑…

Autorelease Pool

objc 中,autorelease pool 块用@autoreleasepool标记。Swift 中,可以使用autoreleasepool(_:)函数,在一个 autorelease pool 块中执行一个闭包。

1
2
3
4
5
import Foundation
autoreleasepool {
// code that creates autoreleased objects.
}

API Availability

待填坑…

Processing Command-Line Arguments

通过访问CommandLine.arguments,可以得到启动时命令行参数列表。

1
$ /path/to/app --argumentName value
1
2
3
4
5
6
for argument in CommandLine.arguments {
print(argument)
}
// prints "/path/to/app"
// prints "--argumentName"
// prints "value"

CommandLine.arguments的第一个元素是可执行文件的路径,在启动时定义的命令行参数从CommandLine.arguments[1]开始。

参考