Guides and Sample Code

Developer

Using Swift with Cocoa and Objective-C (Swift 4)

iBooks
On This Page

Adopting Cocoa Design Patterns

One aid in writing well-designed, resilient apps is to use Cocoa’s established design patterns. Many of these patterns rely on classes defined in Objective-C. Because of Swift’s interoperability with Objective-C, you can take advantage of these common patterns in your Swift code. In many cases, you can use Swift language features to extend or simplify existing Cocoa patterns, making them more powerful and easier to use.

Delegation

In both Swift and Objective-C, delegation is often expressed with a protocol that defines the interaction and a conforming delegate property. Just as in Objective-C, before you send a message that a delegate may not respond to, you ask the delegate whether it responds to the selector. In Swift, you can use optional chaining to invoke an optional protocol method on a possibly nil object and unwrap the possible result using if–let syntax. The code listing below illustrates the following process:

  1. Check that myDelegate is not nil.

  2. Check that myDelegate implements the method window:willUseFullScreenContentSize:.

  3. If 1 and 2 hold true, invoke the method and assign the result of the method to the value named fullScreenSize.

  4. Print the return value of the method.

  1. class MyDelegate: NSObject, NSWindowDelegate {
  2. func window(_ window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize) -> NSSize {
  3. return proposedSize
  4. }
  5. }
  6. myWindow.delegate = MyDelegate()
  7. if let fullScreenSize = myWindow.delegate?.window(myWindow, willUseFullScreenContentSize: mySize) {
  8. print(NSStringFromSize(fullScreenSize))
  9. }

Lazy Initialization

A lazy property is a property whose underlying value is only initialized when the property is first accessed. Lazy properties are useful when the initial value for a property either requires complex or computationally expensive setup, or cannot be determined until after an instance’s initialization is complete.

In Objective-C, a property may override its synthesized getter method such that the underlying instance variable is conditionally initialized if its value is nil:

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

In Swift, a stored property with an initial value can be declared with the lazy modifier to have the expression calculating the initial value only evaluated when the property is first accessed:

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

Because a lazy property is only computed when accessed for a fully-initialized instance it may access constant or variable properties in its default value initialization expression:

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

For values that require additional setup beyond initialization, you can assign the default value of the property to a self-evaluating closure that returns a fully-initialized value:

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

For more information, see Lazy Stored Properties in The Swift Programming Language (Swift 4).

Error Handling

In Cocoa, methods that produce errors take an NSError pointer parameter as their last parameter, which populates its argument with an NSError object if an error occurs. Swift automatically translates Objective-C methods that produce errors into methods that throw an error according to Swift’s native error handling functionality.

For example, consider the following Objective-C method from NSFileManager:

  1. - (BOOL)removeItemAtURL:(NSURL *)URL
  2. error:(NSError **)error;

In Swift, it’s imported like this:

  1. func removeItem(at: URL) throws

Notice that the removeItem(at:) method is imported by Swift with a Void return type, no error parameter, and a throws declaration.

If the last non-block parameter of an Objective-C method is of type NSError **, Swift replaces it with the throws keyword, to indicate that the method can throw an error. If the Objective-C method’s error parameter is also its first parameter, Swift attempts to simplify the method name further, by removing the “WithError” or “AndReturnError” suffix, if present, from the first part of the selector. If another method is declared with the resulting selector, the method name is not changed.

If an error producing Objective-C method returns a BOOL value to indicate the success or failure of a method call, Swift changes the return type of the function to Void. Similarly, if an error producing Objective-C method returns a nil value to indicate the failure of a method call, Swift changes the return type of the function to a nonoptional type.

Otherwise, if no convention can be inferred, the method is left intact.

Catching and Handling an Error

In Objective-C, error handling is opt-in, meaning that errors produced by calling a method are ignored unless an error pointer is provided. In Swift, calling a method that throws requires explicit error handling.

Here’s an example of how to handle an error when calling a method in Objective-C:

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

And here’s the equivalent code in Swift:

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

Additionally, you can use catch clauses to match on particular error codes as a convenient way to differentiate possible failure conditions:

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

Converting Errors to Optional Values

In Objective-C, you pass NULL for the error parameter when you only care whether there was an error, not what specific error occurred. In Swift, you write try? to change a throwing expression into one that returns an optional value, and then check whether the value is nil.

For example, the NSFileManager instance method URL(for:in:appropriateForURL:create:) returns a URL in the specified search path and domain, or produces an error if an appropriate URL does not exist and cannot be created. In Objective-C, the success or failure of the method can be determined by whether an NSURL object is returned.

  1. NSFileManager *fileManager = [NSFileManager defaultManager];
  2. NSURL *tmpURL = [fileManager URLForDirectory:NSCachesDirectory
  3. inDomain:NSUserDomainMask
  4. appropriateForURL:nil
  5. create:YES
  6. error:nil];
  7. if (tmpURL != nil) {
  8. // ...
  9. }

You can do the same in Swift as follows:

  1. let fileManager = FileManager.default
  2. if let tmpURL = try? fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
  3. // ...
  4. }

Throwing an Error

If an error occurs in an Objective-C method, that error is used to populate the error pointer argument of that method:

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

If an error occurs in a Swift method, the error is thrown, and automatically propagated to the caller:

  1. // an error occurred
  2. throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, userInfo: nil)

If Objective-C code calls a Swift method that throws an error, the error is automatically propagated to the error pointer argument of the bridged Objective-C method.

For example, consider the read(from:ofType:) method in NSDocument. In Objective-C, this method’s last parameter is of type NSError **. When overriding this method in a Swift subclass of NSDocument, the method replaces its error parameter and throws instead.

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

If the method is unable to create an object with the regular file contents of the document, it throws an NSError object. If the method is called from Swift code, the error is propagated to its calling scope. If the method is called from Objective-C code, the error instead populates the error pointer argument.

In Objective-C, error handling is opt-in, meaning that errors produced by calling a method are ignored unless you provide an error pointer. In Swift, calling a method that throws requires explicit error handling.

Catching and Handling Custom Errors

Objective-C frameworks can use custom error domains and enumerations to group related categories of errors.

The example below shows a customized error type defined using the NS_ERROR_ENUM macro in Objective-C:

  1. static NSString *const MyErrorDomain = @"com.example.MyErrorDomain";
  2. typedef NS_ERROR_ENUM(MyErrorDomain, MyError) {
  3. specificError1 = 0,
  4. specificError2 = 1
  5. };

This example shows how to generate errors using that custom error type in Swift:

  1. func customThrow() throws {
  2. throw NSError(
  3. domain: MyErrorDomain,
  4. code: MyError.specificError2.rawValue,
  5. userInfo: [
  6. NSLocalizedDescriptionKey: "A customized error from MyErrorDomain."
  7. ]
  8. )
  9. }
  10. do {
  11. try customThrow()
  12. } catch MyError.specificError1 {
  13. print("Caught specific error #1")
  14. } catch let error as MyError where error.code == .specificError2 {
  15. print("Caught specific error #2, ", error.localizedDescription)
  16. // Prints "Caught specific error #2. A customized error from MyErrorDomain."
  17. } let error {
  18. fatalError("Some other error: \(error)")
  19. }

Key-Value Observing

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects. You can use key-value observing with a Swift class, as long as the class inherits from the NSObject class. You can use these two steps to implement key-value observing in Swift.

  1. Add the dynamic modifier and @objc attribute to any property you want to observe. For more information on dynamic, see Requiring Dynamic Dispatch.

    1. class MyObjectToObserve: NSObject {
    2. @objc dynamic var myDate = NSDate()
    3. func updateDate() {
    4. myDate = NSDate()
    5. }
    6. }
  2. Create an observer for the key path and call the observe(_:options:changeHandler) method. For more information on key paths, see Keys and Key Paths.

    1. class MyObserver: NSObject {
    2. @objc var objectToObserve: MyObjectToObserve
    3. var observation: NSKeyValueObservation?
    4. init(object: MyObjectToObserve) {
    5. objectToObserve = object
    6. super.init()
    7. observation = observe(\.objectToObserve.myDate) { object, change in
    8. print("Observed a change to \(object.objectToObserve).myDate, updated to: \(object.objectToObserve.myDate)")
    9. }
    10. }
    11. }
    12. let observed = MyObjectToObserve()
    13. let observer = MyObserver(object: observed)
    14. observed.updateDate()

Undo

In Cocoa, you register operations with NSUndoManager to allow users to reverse that operation’s effect. You can take advantage of Cocoa’s undo architecture in Swift just as you would in Objective-C.

Objects in an app’s responder chain—that is, subclasses of NSResponder on macOS and UIResponder on iOS—have a read-only undoManager property that returns an optional NSUndoManager value, which manages the undo stack for the app. Whenever an action is taken by the user, such as editing the text in a control or deleting an item at a selected row, an undo operation can be registered with the undo manager to allow the user to reverse the effect of that operation. An undo operation records the steps necessary to counteract its corresponding operation, such as setting the text of a control back to its original value or adding a deleted item back into a table.

NSUndoManager supports two ways to register undo operations: a “simple undo”, which performs a selector with a single object argument, and an “invocation-based undo”, which uses an NSInvocation object that takes any number and any type of arguments.

For example, consider a simple Task model, which is used by a ToDoListController to display a list of tasks to complete:

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

For properties in Swift, you can create an undo operation in the willSet observer using self as the target, the corresponding Objective-C setter as the selector, and the current value of the property as the object:

  1. @IBOutlet var notesLabel: NSTextView!
  2. @objc dynamic var notes: String! {
  3. willSet {
  4. let selector = #selector(setter:notes)
  5. undoManager?.registerUndo(withTarget: self, selector: selector, object: self.title)
  6. undoManager?.setActionName(NSLocalizedString("todo.notes.update", comment: "Update Notes"))
  7. }
  8. didSet {
  9. notesLabel.string = notes
  10. }
  11. }

For methods that take more than one argument, you can create an undo operation using an NSInvocation, which invokes the method with arguments that effectively revert the app to its previous state:

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

The prepare(withInvocationTarget:) method returns a proxy to the specified target. By casting to ToDoListController, this return value can make the corresponding call to mark(task:asCompleted:) directly.

For more information, see Undo Architecture.

Target-Action

Target-action is a common Cocoa design pattern in which one object sends a message to another object when a specific event occurs. The target-action model is fundamentally similar in Swift and Objective-C. In Swift, you use the Selector type to refer to Objective-C selectors. For an example of using target-action in Swift code, see Selectors.

Singleton

Singletons provide a globally accessible, shared instance of an object. You can create your own singletons as a way to provide a unified access point to a resource or service that’s shared across an app, such as an audio channel to play sound effects or a network manager to make HTTP requests.

In Objective-C, you can ensure that only one instance of a singleton object is created by wrapping its initialization in a call the dispatch_once function, which executes a block once and only once for the lifetime of an app:

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

In Swift, you can simply use a static type property, which is guaranteed to be lazily initialized only once, even when accessed across multiple threads simultaneously:

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

If you need to perform additional setup beyond initialization, you can assign the result of the invocation of a closure to the global constant:

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

For more information, see Type Properties in The Swift Programming Language (Swift 4).

Introspection

In Objective-C, you use the isKindOfClass: method to check whether an object is of a certain class type, and the conformsToProtocol: method to check whether an object conforms to a specified protocol. In Swift, you accomplish this task by using the is operator to check for a type, or the as? operator to downcast to that type.

You can check whether an instance is of a certain subclass type by using the is operator. The is operator returns true if the instance is of that subclass type, and false if it is not.

  1. if object is UIButton {
  2. // object is of type UIButton
  3. } else {
  4. // object is not of type UIButton
  5. }

You can also try and downcast to the subclass type by using the as? operator. The as? operator returns an optional value that can be bound to a constant using an if-let statement.

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

For more information, see Type Casting in The Swift Programming Language (Swift 4).

Checking for and casting to a protocol follows exactly the same syntax as checking for and casting to a class. Here is an example of using the as? operator to check for protocol conformance:

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

Note that after this cast, the dataSource constant is of type UITableViewDataSource, so you can only call methods and access properties defined on the UITableViewDataSource protocol. You must cast it back to another type to perform other operations.

For more information, see Protocols in The Swift Programming Language (Swift 4).

Serialization

Serialization allows you to encode and decode objects in your app to and from architecture-independent representations, such as JSON or property lists. These representations can then be written to a file, or transmitted to another process locally or over a network.

In Objective-C, you can use the Foundation framework classes NSJSONSerialization and NSPropertyListSerialization to initialize objects from a decoded JSON or property list serialization value—usually an object of type NSDictionary<NSString *, id>.

In Swift, the standard library defines a standardized approach to data encoding and decoding. You adopt this approach by making your types conform to the Encodable or Decodable protocols, or by conforming to Codable as shorthand for conforming to both protocols. You can use the Foundation framework classes JSONEncoder and PropertyListEncoder to convert instances to JSON or property list data. Similarly, you can use the JSONDecoder and PropertyListDecoder classes to decode and initialize instances from JSON or property list data.

For example, an app that communicates with a web server would receive JSON representations of grocery products, such as:

  1. {
  2. "name": "Banana",
  3. "points": 200,
  4. "description": "A banana grown in Ecuador.",
  5. "varieties": [
  6. "yellow",
  7. "green",
  8. "brown"
  9. ]
  10. }

Here’s how to write a Swift type that represents a grocery product and can be used with any serialization format that provides encoders and decoders:

  1. struct GroceryProduct: Codable {
  2. let name: String
  3. let points: Int
  4. let description: String
  5. let varieties: [String]
  6. }

You can create a GroceryProduct from a JSON representation by creating a JSONDecoder instance and passing it the GroceryProduct.self type along with the JSON data:

  1. let json = """
  2. {
  3. "name": "Banana",
  4. "points": 200,
  5. "description": "A banana grown in Ecuador.",
  6. "varieties": [
  7. "yellow",
  8. "green",
  9. "brown"
  10. ]
  11. }
  12. """.data(using: .utf8)!
  13. let decoder = JSONDecoder()
  14. let banana = try decoder.decode(GroceryProduct.self, from: json)
  15. print("\(banana.name) (\(banana.points) points): \(banana.description)")
  16. // Prints "Banana (200 points): A banana grown in Ecuador."

For information about encoding and decoding more complex custom types, see Encoding and Decoding Custom Types. For more information about encoding and decoding JSON, see Using JSON with Custom Types.

Localization

In Objective-C, you typically use the NSLocalizedString family of macros to localize strings. These include NSLocalizedString, NSLocalizedStringFromTable, NSLocalizedStringFromTableInBundle, and NSLocalizedStringWithDefaultValue. In Swift, the functionality of these macros is made available through a single function: NSLocalizedString(_:tableName:bundle:value:comment:).

Rather than defining separate functions that correspond to each Objective-C macro, the Swift NSLocalizedString(_:tableName:bundle:value:) function specifies default values for the tableName, bundle, and value arguments, so that they may be overridden as necessary.

For example, the most common form of a localized string in an app may only need a localization key and a comment:

  1. let format = NSLocalizedString("Hello, %@!", comment: "Hello, {given name}!")
  2. let name = "Mei"
  3. let greeting = String(format: format, arguments: [name as CVarArg])
  4. print(greeting)
  5. // Prints "Hello, Mei!"

Or, an app may require more complex usage in order to use localization resources from a separate bundle:

  1. if let path = Bundle.main.path(forResource: "Localization", ofType: "strings", inDirectory: nil, forLocalization: "ja"),
  2. let bundle = Bundle(path: path) {
  3. let translation = NSLocalizedString("Hello", bundle: bundle, comment: "")
  4. print(translation)
  5. }
  6. // Prints "こんにちは"

For more information, see Internationalization and Localization Guide.

Autorelease Pools

Autorelease pool blocks allow objects to relinquish ownership without being deallocated immediately. Typically, you don’t need to create your own autorelease pool blocks, but there are some situations in which either you must—such as when spawning a secondary thread—or it is beneficial to do so—such as when writing a loop that creates many temporary objects.

In Objective-C, autorelease pool blocks are marked using @autoreleasepool. In Swift, you can use the autoreleasepool(_:) function to execute a closure within an autorelease pool block.

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

For more information, see Advanced Memory Management Programming Guide.

API Availability

Some classes and methods are not available to all versions of all platforms that your app targets. To ensure that your app can accommodate any differences in functionality, you check the availability those APIs.

In Objective-C, you use the respondsToSelector: and instancesRespondToSelector: methods to check for the availability of a class or instance method. Without a check, the method call throws an NSInvalidArgumentException “unrecognized selector sent to instance” exception. For example, the requestWhenInUseAuthorization method is only available to instances of CLLocationManager starting in iOS 8.0 and macOS 10.10:

  1. if ([CLLocationManager instancesRespondToSelector:@selector(requestWhenInUseAuthorization)]) {
  2. // Method is available for use.
  3. } else {
  4. // Method is not available.
  5. }

In Swift, attempting to call a method that is not supported on all targeted platform versions causes a compile-time error.

Here’s the previous example, in Swift:

  1. let locationManager = CLLocationManager()
  2. locationManager.requestWhenInUseAuthorization()
  3. // error: only available on iOS 8.0 or newer

If the app targets a version of iOS prior to 8.0 or macOS prior to 10.10, requestWhenInUseAuthorization() is unavailable, so the compiler reports an error.

Swift code can use the availability of APIs as a condition at run-time. Availability checks can be used in place of a condition in a control flow statement, such as an if, guard, or while statement.

Taking the previous example, you can check availability in an if statement to call requestWhenInUseAuthorization() only if the method is available at runtime:

  1. let locationManager = CLLocationManager()
  2. if #available(iOS 8.0, macOS 10.10, *) {
  3. locationManager.requestWhenInUseAuthorization()
  4. }

Alternatively, you can check availability in a guard statement, which exits out of scope unless the current target satisfies the specified requirements. This approach simplifies the logic of handling different platform capabilities.

  1. let locationManager = CLLocationManager()
  2. guard #available(iOS 8.0, macOS 10.10, *) else { return }
  3. locationManager.requestWhenInUseAuthorization()

Each platform argument consists of one of platform names listed below, followed by corresponding version number. The last argument is an asterisk (*), which is used to handle potential future platforms.

Platform Names:

  • iOS

  • iOSApplicationExtension

  • macOS

  • macOSApplicationExtension

  • watchOS

  • watchOSApplicationExtension

  • tvOS

  • tvOSApplicationExtension

All of the Cocoa APIs provide availability information, so you can be confident the code you write works as expected on any of the platforms your app targets.

You can denote the availability of your own APIs by annotating declarations with the @available attribute. The @available attribute uses the same syntax as the #available runtime check, with the platform version requirements provided as comma-delimited arguments.

For example:

  1. @available(iOS 8.0, macOS 10.10, *)
  2. func useShinyNewFeature() {
  3. // ...
  4. }

Processing Command-Line Arguments

On macOS, you typically open an app by clicking its icon in the Dock or Launchpad, or by double-clicking its icon from the Finder. However, you can also open an app programmatically and pass command-line arguments from Terminal.

You can get a list of any command-line arguments that are specified at launch by accessing the CommandLine.arguments type property.

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

The first element in CommandLine.arguments is a path to the executable. Any command-line arguments that are specified at launch begin at CommandLine.arguments[1].