Chapters

Hide chapters

Expert Swift

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

12. Objective-C Interoperability
Written by Shai Mishali

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You love Swift. Otherwise, you probably wouldn’t be reading this book! Heck, you might’ve even started your iOS journey with Swift without touching its much more battle-tested ancestor — Objective-C.

Objective-C is a Smalltalk-inspired superset on top of the C programming language. Like it or not, Objective-C is still a heavily used language in legacy codebases and apps that have been in production for many years. Put that together with the fact that most third-party SDKs are still provided in Objective-C for compatibility reasons, and it could turn out to be quite useful to know at least some key portions of it.

In your own apps, you’ll often have a sizable Objective-C codebase that just doesn’t feel at home inside your Swift code or want to use some of your shiny new Swift code in your Objective-C code.

Luckily, Apple provides relatively thorough interoperability — hence, the ability for Objective-C code to “see” Swift code and vice versa. But there’s only so much Apple can do for you automatically, which is where this chapter kicks in!

What you’ll learn

As a relatively new father, tracking what my child does takes up a huge part of my day. Like other things that need optimizing, there’s an app for that!

In this chapter, you’ll work on an app called BabyTrack, which lets you track what your baby does: eat, drink, sleep, etc.

The version of the app you’ll start with uses its own Objective-C based framework — BabyKit. You’ll spend this entire chapter creating a wholesome experience for consumers of both the Objective-C and Swift portions of your codebase in a way that feels as if it were designed for either.

Getting started

Open the starter project in the projects/starter folder and then BabyTrack.xcodeproj.

let navigation = UINavigationController(
  rootViewController: UIViewController()
)

Bridging and umbrella headers

Bridging and umbrella headers are two headers that do the same thing, in essence: They notify their consumers which portions are exposed to their use, in the header’s context.

Umbrella header

You can think of an umbrella header as the master header of a framework. In the context of a framework, it tells its consumers which framework portions are publicly available without the headers needing to be manually imported one by one.

#import <BabyKit/Feed.h>
#import <BabyKit/FeedItem.h>
/<module-includes>:1:1: Umbrella header for module 'BabyKit' does not include header 'Feed.h'
#import <BabyKit/BabyKit.h>

Bridging header

A bridging header belongs to the scope of an app instead of a framework. As its name suggests, it bridges Objective-C files into Swift by exposing any headers imported into it to your Swift files.

Making the app launch

To expose ViewController.h, start by right-clicking Boilerplate in your project navigator and selecting New File…. Then, select Header File and name it BabyKit-Bridging.h. Make sure you select the BabyTrack target.

#import "ViewController.h"

Enriching FeedItem

Open FeedCell.m and find configureWithFeedItem:. You’ll notice that there is absolutely nothing in this configuration method that modifies the title or icon on the left. The only “right” piece is the subtitle — the date itself.

Exposing Swift to Objective-C

Although you needed a fancy bridging header to bridge Objective-C code into Swift, going the other way is quite simple. Simply annotate Swift methods, classes and properties with @objc, and they’ll be available to your Objective-C code.

extension FeedItem
@objc extension FeedItem

@interface FeedItem (SWIFT_EXTENSION(BabyTrack))
@property (nonatomic, readonly, copy) NSURL * _Nullable attachmentURL;
@end
public struct FeedItemKind : Equatable, RawRepresentable {
    public init(_ rawValue: UInt32)

    public init(rawValue: UInt32)

    public var rawValue: UInt32
}

What can and can’t be directly bridged

Bridging happens automatically in many cases. For example, when you create a Swift class that inherits from an Objective-C class or write Swift code that extends an Objective-C object, that Swift code is automatically exposed to your Objective-C code as long as you annotate it with @objc. Exceptions to this are Swift-only features, such as:

@interface BatchArchiver<T: id<NSCoding>> : NSObject
+ (NSArray <NSData *> *) archive:(NSArray<T> *) objects;
+ (NSArray <T> *) unarchiveFromData:(NSArray<NSData *> *) data;
@end
open class BatchArchiver<T>: NSObject where T: NSCoding {
  open class func archive(_ object: [T]) -> [Data]
  open class func unarchive(fromData data: [Data]) -> [T]
}

Extending without extending

To still support accessing the Swift-based extensions on FeedItemKind, you can simply wrap the properties in methods. Go back to FeedItem+Ext.swift and add the following static methods to the FeedItem extension:

static func color(for kind: FeedItemKind) -> UIColor {
  kind.color
}

static func emoji(for kind: FeedItemKind) -> String {
  kind.emoji
}

static func title(for kind: FeedItemKind) -> String {
  kind.title
}
#import "BabyTrack-Swift.h"
self.lblKindEmoji.text = [FeedItem emojiFor:feedItem.kind];
self.lblKindEmoji.backgroundColor = [FeedItem colorFor:feedItem.kind];
self.lblKindTitle.text = [FeedItem titleFor:feedItem.kind];
[NSURL new]
feedItem.attachmentURL

Setting explicit names for @objc members

Although a method called emoji(for:) makes sense for Swift, Objective-C consumers would expect a method simply called emojiForKind:. The automatic bridging doesn’t really get this correctly, but no worries!

@objc(colorForKind:)
static func color(for kind: FeedItemKind) -> UIColor {
  kind.color
}

@objc(emojiForKind:)
static func emoji(for kind: FeedItemKind) -> String {
  kind.emoji
}

@objc(titleForKind:)
static func title(for kind: FeedItemKind) -> String {
  kind.title
}

Improving Objective-C enums

As you noticed before, your FeedItemKind enum is bridged as a non-finite standard C enum, which is not optimal when working in a Swift codebase where you’re used to working with a finite set of strongly typed cases.

typedef enum {
typedef NS_CLOSED_ENUM(NSInteger, FeedItemKind) {
@frozen public enum FeedItemKind: Int {
    case bottle = 0
    case food = 1
    case sleep = 2
    case diaper = 3
    case moment = 4
    case awake = 5
}
UIButton *button = self.actionButtons[kind];
[button setTitle:[FeedItem emojiForKind:kind]
        forState:UIControlStateNormal];
[button setBackgroundColor:[FeedItem colorForKind:kind]];

Objective-C and … SwiftUI ?!

You heard it. In this section, you’re going to pretend your Objective-C app doesn’t exist. You’ve just been handed this Objective-C framework, and you want to build a brand new SwiftUI app that uses it.

Improving nullability

Nullability in Objective-C is the parallel of using Optional in Swift. Generally speaking, these nullability constraints are automatically bridged in a decent way but not good enough for a demanding developer, such as yourself.

print(feedItem.date)
open class FeedItem: NSObject {
  public init!(kind: FeedItemKind)
  public init!(kind: FeedItemKind, date: Date!)  
  public init!(kind: FeedItemKind,
               date: Date!,
               attachmentId: UUID!)

  open var kind: FeedItemKind
  open var date: Date!
  open var attachmentId: UUID!
}

public func FeedItemKindDescription(_: FeedItemKind) -> String!
@property (nonatomic, strong) NSUUID * attachmentId;
@property (nonatomic, strong) NSUUID * _Nullable attachmentId;
- (FeedItem *) initWithKind: (FeedItemKind) kind
                       date: (NSDate * _Nullable) date
               attachmentId: (NSUUID * _Nullable) attachmentId;
- (FeedItem * _Nonnull) initWithKind: (FeedItemKind) kind;
NS_ASSUME_NONNULL_BEGIN
NS_ASSUME_NONNULL_END
open class FeedItem: NSObject {
    public init(kind: FeedItemKind)
    public init(kind: FeedItemKind, date: Date)
    public init(kind: FeedItemKind,
                date: Date?,
                attachmentId: UUID?)
    
    open var kind: FeedItemKind
    open var date: Date
    open var attachmentId: UUID?
}

Setting up SwiftUI

With Feed optimized, it’s time for you to start working on the SwiftUI part of your app.

private func startNewApp() {
  self.window?.rootViewController = UIHostingController(
    rootView: FeedView()
  )
  self.window?.makeKeyAndVisible()
}

let isBabySleeping: Bool
let onKindTapped: (FeedItemKind) -> Void
let kinds: [FeedItemKind] = [.bottle, .food, .sleep,
                             .diaper, .moment]

// 1
HStack(spacing: 16) {
  // 2
  ForEach(kinds, id: \.self) { kind in
    // 3
    let type = kind == .sleep && isBabySleeping ? .awake : kind

    Button(type.emoji) {
      // 4
      onKindTapped(type)
    }
    .frame(minWidth: 52, maxWidth: .infinity,
           minHeight: 52, idealHeight: 52)
    .background(Color(kind.color))
    .cornerRadius(4)
  }
}
.padding([.leading, .trailing])

Improving FeedItemKind’s naming

There is already some room for improvement here from the Swift perspective. Notice the type FeedItemKind.

struct FeedItem { ... }

extension FeedItem { 
  enum Kind {
    // cases
  }
}
} NS_SWIFT_NAME(FeedItem.Kind);
NavigationView {
  VStack {
    AddFeedItemBar(isBabySleeping: false) { kind in

    }
    .padding([.top, .bottom], 8)

    List(1..<5) { i in
      FeedCell(feedItem: .init(kind: .food))
    }
  }
  .navigationTitle("BabyTrack")
}
.navigationViewStyle(StackNavigationViewStyle())

Understanding the problem with BabyKit.Feed

Although your UIKit-based Objective-C code uses a regular UITableView with an associated delegate and imperatively reloads the table and reads items from a Feed object, SwiftUI is quite different.

Refining the Feed object

Open Feed.h. Above the @interface line, add the following line:

NS_REFINED_FOR_SWIFT

let feed = Feed()
let feed = __Feed()
import Foundation
import Combine
import SwiftUI

// 1
public class Feed: ObservableObject {
  // 2
  @Published public var items: [FeedItem] = []
  // 3
  private let legacyFeed = __Feed()

  public init() {
    // 4
    items = legacyFeed.loadItems()
  }

  // 5
  public var isBabySleeping: Bool {
    legacyFeed.babySleeping
  }

  public func addItem(of kind: FeedItem.Kind) {
    items.insert(legacyFeed.addItem(of: kind), at: 0)
  }

  public func addMoment(with attachmentId: UUID) {
    items.insert(
      legacyFeed.add(FeedItem(kind: .moment,
                              date: nil,
                              attachmentId: attachmentId)),
                     at: 0
    )
  }

  public func storeImage(_ image: UIImage) -> UUID? {
    legacyFeed.store(image)
  }
}
@property (nonatomic, readonly) BOOL babySleeping;
@property (nonatomic, readonly) BOOL babySleeping NS_SWIFT_NAME(isBabySleeping);
- (NSUUID * _Nullable) storeImage:(UIImage *) image;
- (NSUUID * _Nullable) storeImage:(UIImage *) image NS_SWIFT_NAME(storeImage(_:));

Improving property mirroring with @dynamicMemberLookup

Right now, isBabySleeping simply mirrors legacyFeed.isBabySleeping. This is fine for a single item, but it can become quite tedious and full of boilerplate as you add more and more properties to your Objective-C Feed.

@dynamicMemberLookup
public class Feed: ObservableObject {
public subscript<T>(
  dynamicMember keyPath: KeyPath<__Feed, T>) -> T {
  legacyFeed[keyPath: keyPath]
}

Finalizing FeedView

You have almost everything ready to go to finalize FeedView. Head over to FeedView.swift.

let feed = __Feed()
@StateObject var feed = Feed()
List(feed.items, id: \.date) { item in
  FeedCell(feedItem: item)
}

AddFeedItemBar(isBabySleeping: false) { kind in
AddFeedItemBar(isBabySleeping: feed.isBabySleeping) { kind in

Reacting to tapping the action bar

Inside the AddFeedItemBar closure, add the following code:

// 1
print("Selected \(FeedItemKindDescription(kind))")

// 2
if kind == .moment {
  // ???
  return
}

// 3
feed.addItem(of: kind)
Selected Awake
Selected Bottle
Selected Diaper
Selected Food
Selected Sleeping
NSString * FeedItemKindDescription(FeedItemKind);
NSString * FeedItemKindDescription(FeedItemKind)
NS_SWIFT_NAME(getter:FeedItemKind.description(self:));

Letting the user pick a moment photo

When the user wants to add a new moment, the Objective-C version of the app modally presents a PHPickerViewController to let the user pick a photo.

// 1
result.itemProvider
  .loadObject(ofClass: UIImage.self) { [weak self] obj, err in
  // 2
  defer { self?.parent.isPresented = false }

  // 3
  guard let image = obj as? UIImage,
        let parent = self?.parent else { return }

  // 4
  if let err = err {
    print("Error in picked image: \(err)")
    return
  }

  guard let attachmentId = parent.feed.storeImage(image) else {
    print("Failed storing, no UUID")
    return
  }

  // 5
  DispatchQueue.main.async {
    parent.feed.addMoment(with: attachmentId)
  }
}
let feed: Feed
@State private var isPickingMoment = false
isPickingMoment = true
.sheet(isPresented: $isPickingMoment) {
  AddMomentView(feed: feed,
                isPresented: $isPickingMoment)
}
NS_SWIFT_UNAVAILABLE("Use `AddMomentView` instead")

Key points

  • Objective-C is a powerful language, with relatively comprehensive bridging to Swift. When that automatic bridging isn’t enough, you can get as granular as you want with your Swift-specific customizations.
  • A bridging header exposes Objective-C headers inside your app, while an umbrella header exposes all Objective-C headers for a given framework.
  • Because nullability is a given in Objective-C’s dynamic nature, you can use _Nullable or _Nonnull to define the appropriate nullability or use NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END to make an entire definition block non-nullable.
  • You can use the @objc or @objc(name) annotation to expose Swift types to Objective-C.
  • You can use NS_CLOSED_ENUM to bridge Objective-C enums into entirely Swifty enums.
  • Although full-blown generics aren’t fully supported, lightweight generics pack quite a powerful punch for most common needs.
  • If you want to have Swift-specific names for properties, objects or even global functions as getters, use NS_SWIFT_NAME.
  • If you want to hide an Objective-C implementation so you can wrap it in an improved Swift interface, use NS_REFINED_FOR_SWIFT. This allows you to leverage Swift-specific features that are otherwise inaccessible to your Objective-C based code.
  • If you want to make a method or property entirely unavailable to Swift, use NS_SWIFT_UNAVAILABLE.
  • Don’t give up on Objective-C — it’s here to stay.

Where to go from here?

Congratulations on completing the BabyTrack app! You’ve modernized an old-school Objective-C SDK and app, while wrapping most SDK components with modern Swift-centric APIs and keeping the same interface for your Objective-C consumers, creating a wholesome experience that feels as if you designed the code for either language.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now