Swift-Objective C interoperability and best practices

Initial setup

Prerequisite: Have Objective-C project

  1. Add a new Swift file to the project
  2. A dialogue box will appear on which you select "Create Bridging Header"
  3. In project's general settings:
    • Go to Build Settings
    • In Packaging section, switch "Defines Module" from "No" to "Yes"
      • This is necessary for importing Swift code into Objective-C within the same framework.

Multiple targets in a project

If your project contains multiple targets, you will have multiple bridging headers. To avoid issues that can arise by having multiple bridging headers, you can:

  1. Delete all bridging headers except one, and rename the one that remained to {Product_Module_Name}-Bridging-Header.h.
  2. In Build Settings, under Objective-C Bridging Header put the path to the one bridging header that remained for every target.

After these steps, all targets will use the same bridging header.

To avoid multiple names for Objective-C generated interface header name, you should use the same logic as with bridging headers.

In Build Settings, under Objective-C Generated Interface Header Name, you should use common name for every target, for example {Product_Module_Name}-Swift.h.

General project configuration

After adding your first Swift file to the project, some additional flags are going to come up in General project configuration. Some flags are important to you, and they are:

You want to configure both of them so you can use them in Objective-C and Swift codebase.

Flags example:

$(inherited)
"-D"
"COCOAPODS"
"-D"
"APPSTORE"

Macros example:

 $(inherited)
 COCOAPODS=1
 APPSTORE=1

Usage example:

#if DEBUG
...
#endif
#ifdef DEBUG
...
#endif

Using Swift code in Objective-C

Make Swift visible to Objective-C

In order to expose Swift class or methods to Objective-C, you can use different attributes:

This will only work after you build your project.

It is possible to customise the way Swift method or class is represented in Objective-C. If you want to do that, you should use @objc(name) instead just @objc. For example,

@objc(sharedManager)
static let shared = MyManager()

In this example, by using @objc(sharedManager), our singleton will be used in Swift files like MyManager.shared, while in Objective-C it will be used [MyManager sharedManager]. It is also possible to rename whole methods and parameters so it can match Objective-C naming style.

@objc(processNotificationPayload:notificationType:)
static func processNotification(with payload: String, notificationType: NotificationType) {
  ...
}

Finally, exposing Swift won't always work and here are possible reasons why:

Here are some useful information that can help in further development:

Only exposing Swift methods and classes to Objective-C is not enough to use it in Objective-C codebase. You should also import exposed code into Objective-C.

Importing Swift into Objective-C

To access Swift code in Objective-C codebase, you have to use XCode - generated header file. Header name form is {Product_Module_Name}-Swift.h.

Important: Never include {Product_Module_Name}-Swift.h in any .h file since this can cause cyclic reference.

If you only want to use Swift code in .m file, you should import {Product_Module_Name}-Swift.h in that .m file.

If you need to use Swift code in .h file, you should use something called Forward declaration, which is explained below.

Forward declaration

1

Swift enums and Objective-C

Header Files - .h

IMPORTANT: Swift enums can't be used in Objective-C header files.

But if your project must have Swift enums in header files, there are two possible solutions:

  1. Write Swift enum in Objective-C and use newly written enum in Swift files. By doing this, you will have codebase that is easy to maintain.

  2. Have enum written both in Objective-C and Swift.

Although having duplicated code is inherently a bad thing, and it will be more challenging to maintain, using this solution your code will become more flexible and here are the perks that come with it: * You will avoid 'dirtying up' your Swift codebase with Objective-C enums. And by doing this, somewhere in the future when your project won't use Objective-C anymore, you won't have to go through Swift files and refactor them. * If you want to share your code with others, you won't force Swift developers to have support for Objective-C (bridging headers), and their project can remain pure Swift.

Furthermore, here is an example of how to, in some measure, prevent discrepancies between Objective-C and Swift enums if you are using the second solution:

enum EnumExample {
  case numberOne
  case numberTwo

  init?(enumExampleNumbers: EnumExampleNumbers) {
    switch enumExampleNumbers {
    case .one: self = .numberOne
    case .two: self = .numberTwo
    }
  }
}
typedef NS_CLOSED_ENUM(NSInteger, EnumExampleNumbers) {
    EnumExampleNumbersOne,
    EnumExampleNumbersTwo
};

Implementation files - .m

Using Objective-C code in Swift

Make Objective-C visible to Swift

To use Objective-C code in Swift, it is only necessary to import Objective-C header file in bridging header file to expose it to Swift. If you are working on a framework, you will need to import Objective-C in umbrella header since bridging headers are not available for frameworks.

As your application progresses with migration from Objective-C to Swift, number of imports in bridging header file will probably grow, so it is good practice to start separating import statements into logical sections and use markdowns from the start.

// MARK: - Models -
#import "User.h"
#import "Group.h"

// MARK: - Managers -
#import "AnalyticsManager.h"
#import "AlertManager.h"

...

To customise how Objective-C is represented in Swift, you can use NS_SWIFT_NAME macro.


+ (void)processNotificationPayload:(NSString *)payload notificationType:(NSString *)notificationType
NS_SWIFT_NAME(processNotification(payload:notificationType:));

VIPER

Whether you already have Base VIPER in your project written in Objective-C or not, you should add Swift version of Base VIPER which are going to be used for every new module written in Swift.

Interoperability between Objective-C VIPER and Swift VIPER

Having both versions of VIPER in your project will cause an issue with Wireframe naming since both versions of VIPER base wireframes have the same name.

The solution is to rename Objective-C BaseWireframe into BaseWireframeOld. After that, you should search your project for every time Objective-C BaseWireframe was used and rename it accordingly.

Since there are differences in BaseWireframe implementations and the way each of them navigates to the next screen, you should add @objc annotation into Swift BaseWireframe as it is shown in the code below to achieve easy navigation from Objective-C module to Swift module.

extension BaseWireframe {

    @objc var viewController: UIViewController {
        defer { temporaryStoredViewController = nil }
        return _viewController
    }

    @objc var navigationController: UINavigationController? {
        return viewController.navigationController
    }
}
    ...
    SwiftWireframe *wireframe = [SwiftWireframe new];
    [self.navigationController pushViewController:wireframe.viewController animated:YES];

Polyglot

Initial setup

    language: swift
    ...

After you've changed polyglot.yml file, you should fetch translations with polyglot pull.

Tailoring language files for Objective-C needs

All language files that were created by Polyglot for the Objective-C should be replaced with Swift files.

This will cause an issue since structs are not visible in Objective-C, so Language struct in Language.swift won't be visible in Objective-C codebase. To fix this, you should create LanguageObjC.swift file with class that should represent Language struct from Language.swift file. An example of how to do that is shown below.

public struct Language {

    public let name: String
    public let localName: String
    public let locale: String
    public let languageCode: String

    public static let croatian = Language(name: "Croatian", localName: "Hrvatski", locale: "hr_HR", languageCode: "hr_hr")

    public static let all = [
        Language.croatian
    ]

}
@objcMembers
class LanguageObjC: NSObject {

    public let name: String
    public let localeName: String
    public let locale: String
    public let languageCode: String

    init(language: Language) {
        self.name = language.name
        self.localeName = language.localName
        self.locale = language.locale
        self.languageCode = language.languageCode
    }

    public static let croatian = LanguageObjC(language: Language.croatian)

    public static let all = Language.all.map { LanguageObjC(language: $0) }
}

Important: If a new language is added to the project, LanguageObjC.swift file should be updated.

Finally, change all instances where you used polyglot translations in .swift files from _(@"...") to Strings.{...}.localized.

Language Manager

Important: To localize your app, you should use this library: https://github.com/infinum/iOS-SwiftI18n

However, there is always a possibility to write your language manager which supports both Swift and Objective-C, and if you do decide to do that, it should look something like this:

var LanguageManagerLocaleKey: String = "LanguageManagerLocaleKey"

@objcMembers
class LanguageManager: NSObject {

    @objc(sharedManager)
    static let shared = LanguageManager()

    var locale: String {
        get {
            let locale = UserDefaults.standard.object(forKey: LanguageManagerLocaleKey)

            guard let localeTmp = locale else {
                return "hr_hr"
            }

            return localeTmp as! String
        }
        set (newValue) {
            UserDefaults.standard.set(newValue, forKey: LanguageManagerLocaleKey)
        }
    }

    private override init() {}
}

Setting up language in Objective-C

To set up your macro which is going to be used throughout Objective-C codebase, you need to expose your LanguageManager to Objective-C.

#ifndef _
#define _(s) NSLocalizedStringFromTable(s, [LanguageManager sharedManager].locale, s)
#endif

#ifndef __
#define __(s,...) [NSString stringWithFormat:NSLocalizedStringFromTable(s, [LanguageManager sharedManager].locale, s), ##__VA_ARGS__]
#endif
[LanguageManager sharedManager].locale = [LanguageObjC croatian].languageCode;

Usage

Usage remains the same:

Colors, Images, Constants, and much more...

It is always important to have a single source of truth in your codebase. For that reason, initialising the same resource on two sides (Swift and Objective-C) is error-prone, since it is easy to forget to update both. Keeping that in mind, the developer can write an Objective-C - visible Swift class to expose Swift structures. An example is shown below: 

extension UIColor {

    struct MyProject { }
}

// MARK: - Button -

extension UIColor.MyProject {

    static var button: UIColor {
        return UIColor(named: "MyProject/Color/Primary")!
    }

    static var secondaryButton: UIColor {
        return UIColor(named: "MyProject/Color/Secondary")!
    }
}
@objcMembers
class UIColorMyProject: NSObject {
    private override init() {}

    static var button: UIColor { return UIColor.MyProject.button }
    static var secondaryButton: UIColor { return UIColor.MyProject.secondaryButton }
}

Testing

To test Objective-C in Swift unit test class and vice versa, you should do the same thing you do when you want to use some Swift code in Objective-C and Objective-C in Swift:

It is only important to add Objective-C class which you want to test to bridging header file.

Add @objc or @objcMembers in front of the class or function you want to test.