4

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

 2 years ago
source link: https://rhetty.github.io/2017/04/11/%E6%B7%B7%E7%94%A8Swift%E5%92%8Cobjc-%E4%BD%BF%E7%94%A8Cocoa%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

Delegation

Swift 使用代理的步骤:

  1. 检查myDelegate不为nil
  2. 检查myDelegate实现了方法window:willUseFullScreenContentSize:
  3. 如果 1 和 2 都为 ture,就调用方法,并把结果赋给fullScreenSize
  4. 打印返回值
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 中的延迟初始化:

@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修饰:

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

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

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

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

lazy var currencyFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencySymbol = "¤"
return formatter

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

Error Handling

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

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

Catching and Handling an Error

objc 中这么处理错误:

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 中等价的代码:

let fileManager = FileManager.default
let fromURL = URL(fileURLWithPath: "/path/to/old")
let toURL = URL(fileURLWithPath: "/path/to/new")
try fileManager.moveItem(at: fromURL, to: toURL)
} catch let error as NSError {
print("Error: \(error.domain)")

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

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

NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *tmpURL = [fileManager URLForDirectory:NSCachesDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:YES
error:nil];
if (tmpURL != nil) {
// ...
let fileManager = FileManager.default
if let tmpURL = try? fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
// ...

Throwing an Error

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

// an error occurred
if (errorPtr) {
*errorPtr = [NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorCannotOpenFile
userInfo:nil];
// an error occurred
throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, userInfo: nil)

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

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修饰:
class MyObjectToObserve: NSObject {
dynamic var myDate = NSDate()
func updateDate() {
myDate = NSDate()
  1. 创建一个全局上下文变量:
private var myContext = 0
  1. 添加一个观察者,重写observeValue(for:of:change:context:)方法,并且在deinit中移除观察者:
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)

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

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

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

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来创建一个撤销操作。

@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 中创建单例:

+ (instancetype)sharedInstance {
static id _sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
return _sharedInstance;

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

class Singleton {
static let sharedInstance = Singleton()

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

class Singleton {
static let sharedInstance: Singleton = {
let instance = Singleton()
// setup code
return instance

Introspection

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

if object is UIButton {
// object is of type UIButton
} else {
// object is not of type UIButton
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

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

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结构:

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 消息可能是这样:

"name": "Caffè Macs",
"coordinates": {
"lat": 37.330576,
"lng": -122.029739
"category": "food"

Localization

Autorelease Pool

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

import Foundation
autoreleasepool {
// code that creates autoreleased objects.

API Availability

Processing Command-Line Arguments

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

$ /path/to/app --argumentName value
for argument in CommandLine.arguments {
print(argument)
// prints "/path/to/app"
// prints "--argumentName"
// prints "value"

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK