Back to Blog Home
← all posts

Integrating inaccessible Swift code with NativeScript

May 26, 2022 — by Jason Cassidy

With NativeScript we have full access to native platform APIs in Java, Kotlin, Objective-C and Swift. All platform languages bring their own unique constructs to the table. With Swift, you can work with it all as long as the construct you're wanting to use exposes it's types to Objective-C.

So how do we integrate third-party Swift code into our NativeScript apps if the Swift construct is not exposed to Objective-C?

An Example Pod that we want to integrate

Here is the example swift pod that we will use, https://github.com/SwiftKickMobile/SwiftMessages, it allows you to show nifty message cards in a variety of ways.

It is also not usable from objective-c.

Setup our Project and add the Pod

Let's create a sample project:

ns create SwiftSample --template @NativeScript/template-hello-world-ts
cd SwiftSample
ns run ios

And we get the lovely tap button screen.

Add the Pod

Now we add a reference to the pod in our app:

  • Add a file in App_Resources/iOS/PodFile
  • Add the text:
    pod 'SwiftMessages'
    

Where are the Types?

At this point we can generate typings so we get strongly typed access to what is in the Pod.

ns typings ios

This will generate a file under apps/demo/typings/ios/<architecture>.

And we do have a objc!SwiftMessages.d.ts file, but there is a problem.

If I look at the documentation for SwiftMessages a simple use case is SwiftMessages.show(view: myView) but where is that in our Types file there is no SwiftMessages class?

The issue here is that the SwiftMessages class is not exposed to Objective-c, and therefore not accessible from NativeScript. More information is available here.

Wrap it!

So what is the solution? By the magic of NativeScript we can:

  • Drop our own swift file into our project
  • Make that accessible to Objective-C
  • From there call methods on the classes in the Pod.

First we add a new Swift file to our project at App_Resources/iOS/src/NSCSwiftMessages.swift.

And lets add some code (copied from samples on SwiftMessages github) so we can call it from NativeScript.

NOTE: You can use any class prefix here, however we recommend using the NSC prefix to ensure your class doesn't collide with NS prefixed classes which are prolific throughout iOS code due to the NeXTStep underpinnings of the entire iOS platform. You can think of NSC as short for "NativeScript" or "NativeScript Compiler" if that makes it easier to remember. This also helps streamline naming conventions of any platform code you expose this way just for NativeScript purposes.

import Foundation
import SwiftMessages

@objcMembers
@objc(NSCSwiftMessages)
public class NSCSwiftMessages: NSObject {

  // We can assign a callback to this from TypeScript.
  @objc public var onDoneCallBack: ((String)-> Void)? = nil;

  public func showMessage(title: String, body: String) {

    // Instantiate a message view from the provided card view layout. SwiftMessages searches for nib
    // files in the main bundle first, so you can easily copy them into your project and make changes.
    let view = MessageView.viewFromNib(layout: .cardView)

    // Theme message elements with the warning style.
    view.configureTheme(.success)
    view.button?.isHidden=true;
    // Add a drop shadow.
    view.configureDropShadow()
    view.button?.isHidden=true;
    // Set message title, body, and icon. Here, we're overriding the default warning
    // image with an emoji character.
    let iconText = ["🤔", "😳", "🙄", "😶"].randomElement()!
    view.configureContent(title: title, body: body, iconText: iconText)

    // Increase the external margin around the card. In general, the effect of this setting
    // depends on how the given layout is constrained to the layout margins.
    view.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)

    // Reduce the corner radius (applicable to layouts featuring rounded corners).
    (view.backgroundView as? CornerRoundingView)?.cornerRadius = 10

    var config = SwiftMessages.Config()

    // Specify one or more event listeners to respond to show and hide events.
    config.eventListeners.append() { event in
        if case .didHide = event {
            // If the callback is setup call it.
            self.onDoneCallBack?("Message Alert Hidden");
            
        }
    }

    // Show the message.
    SwiftMessages.show(config: config, view: view)
  }
}

The important element here is that we have exposed our swift to Objective-C by the addition of the annotations @objcMembers & @objc.

Now we can run the typings generation again and there will be a new file objc!nsswiftsupport.d

In this new file we will see the typings for our new class:

declare class NSCSwiftMessages extends NSObject {

	static alloc(): NSCSwiftMessages; // inherited from NSObject

	static new(): NSCSwiftMessages; // inherited from NSObject

	onDoneCallBack: (p1: string) => void;

	showMessageWithTitleBody(title: string, body: string): void;
}

We copy this file to the root of the project (or anywhere that makes sense), and add a refrerence to it in the ./references.d.ts file.

/// <reference path="./node_modules/@nativescript/types/index.d.ts" />
/// <reference path="./objc!nsswiftsupport.d.ts" />

Call our new code

Now we can call our code, so in app/main-view-model.ts change the tap method to:

onTap() {
  // Create an instance of the Swift class we created.
  const message = NSCSwiftMessages.new();
  // Assign a callback to get notified when the message closes
  message.onDoneCallBack = (message: string) => {this.message = message;};
  // Show the actual message
  message.showMessageWithTitleBody("This is the Title", "Hello There!");
}

Now when we run the app and tap the button our nice new message shows!

And when it is hidden we get a callback that changes the label.

The sample project is available on GitHub.