request-quote
iOS

Using Swift Modules When React Native Is Not Enough

By Alex K. February 17th, 2016

Have you ever faced the problem when you lack React Native resources to implement certain functionality? Say you want to send iOS Local Notifications and process an event when a user clicks on Local Notification.

React Native provides some API to send Local Notifications but it lacks API for their processing. There two ways to solve this issue:

1.      Write a handler tool in a native language

2.      Write API in a native language for javascript

By default, a basic React Native project is written in Objective-C. At first, we will move the project to Swift. Then we will write API for the handler.

So let’s see how to do it step by step. 

 

Migrating React Native project from Objective-C to Swift

 

The following tools are used in this article:

1.         Xcode 7.2

2.         Nodejs 5.6

3.         react-native-cli 0.1.10

4.         react-native 0.19

Install react-native-cli:

$ npm install -g react-native-cli

451

 

Create a React Native project:

$ react-native init SwiftReactNative

455

 

Open file ios/SwiftReactNative.xcodeproj in Xcode and start the project.

456

 

Several minutes later our application starts.

457

 

Stop the app and migrate the project to Swift.

There are 3 files written in Objective-C which we need to replace.

1.         AppDelegate.h

2.         AppDelegate.m

3.         main.m

458

 

Create a new swift file and name it AppDelegate.swift.

459

 

Once the file added Xcode will offer us to create Objective-C with bridging header. Click Create bridging header (API React Native is written in Objective-C, that’s why there will be 3 languages in the project - Objective-C, Swift, JS).

460

 

2 new files should appear in the project.

1.         AppDelegate.swift

2.         SwiftReactNative-Bridging-Header.h

Add this code to SwiftReactNative-Bridging-Header.h:

#import "RCTBridgeModule.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
#import "RCTRootView.h"
#import "RCTUtils.h"
#import "RCTConvert.h"

 

Then edit file AppDelegate.swift. It should include the same content as AppDelegate.m does but only in Swift.

AppDelegate.swift contains the following:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  var bridge: RCTBridge!
  
  func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
    
    /**
     * Loading JavaScript code - uncomment the one you want.
     *
     * OPTION 1
     * Load from development server. Start the server from the repository root:
     *
     * $ npm start
     *
     * To run on device, change `localhost` to the IP address of your computer
     * (you can get this by typing `ifconfig` into the terminal and selecting the
     * `inet` value under `en0:`) and make sure your computer and iOS device are
     * on the same Wi-Fi network.
     */
    
    var jsCodeLocation = NSURL(string: "http://localhost:8081/index.ios.bundle?platform=ios&dev=true")
    
    
    /**
    * OPTION 2
    * Load from pre-bundled file on disk. The static bundle is automatically
    * generated by "Bundle React Native code and images" build step.
    */
    
    // jsCodeLocation = NSBundle.mainBundle().URLForResource("main", withExtension: "jsbundle")
    
    let rootView = RCTRootView(bundleURL:jsCodeLocation, moduleName: "SwiftReactNative", initialProperties: nil, launchOptions:launchOptions)
    
    self.bridge = rootView.bridge
    
    self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
    let rootViewController = UIViewController()
    
    rootViewController.view = rootView
    
    self.window!.rootViewController = rootViewController;
    self.window!.makeKeyAndVisible()
    
    return true
  }
}

 

Delete the following files:

1.       AppDelegate.h

2.      AppDelegate.m

3.      main.m

In project settings (General -> Deployment Target) select version 9.0.

461

 

Clean the project (Product->Clean) and then start it on emulator (Product-> Run).

462

 

The application has started! The migration to Swift has been completed.

 

Class for Data Conversion

 

We will need class that will convert data from LocalNotification to data valid for JS conversion.

To achieve this it is necessary to create file NotificationToDictionaryTransformer.swift and the class with the same title:

 

class NotificationToDictionaryTransformer {
  var notification: UILocalNotification
  
  init(notification: UILocalNotification) {
    self.notification = notification
  }
  
  func transform() -> [String: AnyObject] {
    var data = [String: AnyObject]()
    data["hasAction"] = notification.hasAction
    
    if let alertBody = notification.alertBody {
      data["alertBody"] = alertBody
    }
    
    if let fireDate = notification.fireDate {
      data["fireDate"] = fireDate.timeIntervalSince1970
    }
    
    if let userInfo = notification.userInfo {
      data["userInfo"] = userInfo
    }
    
    if let alertAction = notification.alertAction {
      data["alertAction"] = alertAction
    }
    
    if let alertTitle = notification.alertTitle {
      data["alertTitle"] = alertTitle
    }
    
    if let alertTitle = notification.alertTitle {
      data["alertTitle"] = alertTitle
    }
    
    return data
  }
}

 

API to process LocalNotifications

 

Create file LocalNotificator.swift.

Create class LocalNotificator:

 

import UIKit

@objc(LocalNotificator)
class LocalNotificator: NSObject {
}

 

Add some logic from module PushNotificationIOS (React Native module) to our module. The following methods are required: requestPermissions, checkPermissions and scheduleLocalNotification.

@objc func requestPermissions() -> Void {
    if (RCTRunningInAppExtension()) {
      return;
    }
    
    let app: UIApplication = RCTSharedApplication()
    let types: UIUserNotificationType = [.Badge, .Alert, .Sound]
    
    let category = UIMutableUserNotificationCategory()
    category.identifier = "SwiftReactNativeCategory"
    
    let settings = UIUserNotificationSettings( forTypes: types, categories: [category] )
    
    app.registerUserNotificationSettings(settings)
    app.registerForRemoteNotifications()
  }
  
  @objc func checkPermissions(callback: RCTResponseSenderBlock) -> Void {
    let defaultPermissions = ["alert": false, "badge": false, "sound": false]
    
    if (RCTRunningInAppExtension()) {
      callback([defaultPermissions]);
      return;
    }
    
    var types: UIUserNotificationType;
    if (UIApplication.instancesRespondToSelector(Selector("currentUserNotificationSettings"))) {
      if let settings = RCTSharedApplication().currentUserNotificationSettings() {
        types = settings.types
        var permissions = [String: Bool]()
        permissions["alert"] = types.contains(.Alert)
        permissions["badge"] = types.contains(.Badge)
        permissions["sound"] = types.contains(.Sound)
        
        callback([permissions]);
        return;
      }
    }
    
    callback([defaultPermissions]);
  }

@objc func scheduleLocalNotification(notificationData: [String: AnyObject], callback: RCTResponseSenderBlock) -> Void {
    let notification = createLocalNotification(notificationData)
    RCTSharedApplication().scheduleLocalNotification(notification)
    callback([NotificationToDictionaryTransformer(notification: notification).transform()]);
    return;
  }

 

Create the method to generate notifications. This method will get the following values: alertBody, alertAction, alertTitle, hasAction, fireDate, userInfo. To identify notifications we will also add a unique UUID parameter to each notification.

 

private func createLocalNotification(notificationData: [String: AnyObject]) -> UILocalNotification {
    let notification = UILocalNotification()
    notification.soundName = UILocalNotificationDefaultSoundName
    notification.alertBody = notificationData["alertBody"] as? String
    notification.alertAction = notificationData["alertAction"] as? String
    notification.alertTitle = notificationData["alertTitle"] as? String
    
    if let hasAction = notificationData["hasAction"] {
      notification.hasAction = (hasAction as? Bool)!
    }
    
    notification.category = "schedulerViewItemCategory"
    
    if let fireDate = notificationData["fireDate"] {
      notification.fireDate = RCTConvert.NSDate(fireDate)
    }
    
    let uuid = NSUUID().UUIDString
    
    if let userInfo = notificationData["userInfo"] as? [NSObject : AnyObject]{
      notification.userInfo = userInfo
      notification.userInfo!["UUID"] = uuid
    } else {
      notification.userInfo = ["UUID": uuid]
    }
    
    return notification;
  }

 

Add the method to cancel notifications for UUID.

@objc func cancelLocalNotification(uuid: String) -> Void {
    for notification in RCTSharedApplication().scheduledLocalNotifications! as [UILocalNotification] {
      
      guard (notification.userInfo != nil) else {
        continue
      }
      
      guard notification.userInfo!["UUID"] as! String == uuid else {
        continue
      }
      RCTSharedApplication().cancelLocalNotification(notification)
      break
    }
  }

 

In order to provide API for class LocalNotificator in javascript we need to create file LocalNotificatorBridge.m with the interface described.

Create file LocalNotificatorBridge.m:

#import "RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(LocalNotificator, NSObject)

RCT_EXTERN_METHOD(requestPermissions)
RCT_EXTERN_METHOD(checkPermissions:(RCTResponseSenderBlock)callback)
RCT_EXTERN_METHOD(scheduleLocalNotification:(NSDictionary *)notificationData callback:(RCTResponseSenderBlock)callback)
RCT_EXTERN_METHOD(cancelLocalNotification:(NSString *)uuid)

@end

With a help of these two files we have added new functionality to React Native.

 

Notifications handler

 

Now it’s time to create the notification handler.

We need to add the code to file AppDelegate.swift. This code will send information to js when LocalNotification is received. 

func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
    RCTSharedApplication().cancelLocalNotification(notification)
    self.bridge.eventDispatcher.sendAppEventWithName(String("didReceiveLocalNotification"), body: NotificationToDictionaryTransformer(notification: notification).transform())
  }

 

Ready! Now we can create the app.

 

JS app creation and using recently added API (written with ES2015)

 

Note that the following components are required:

1)      React. AlertIOS – to display information on the monitor

2)      React.NativeAppEventEmitter – to get information about new notifications

3)      NativeModules.LocalNotificator – the module itself

 

Edit file index.ios.js

Add the code to get and display information stored in LocalNotification in area userInfo.message:

componentDidMount() {
    this.didReceiveLocalNotification = (notification) => {
        AlertIOS.alert(notification.userInfo.message);
        this.setState({lastNotification: null});
    };

    NativeAppEventEmitter.addListener('didReceiveLocalNotification', this.didReceiveLocalNotification);
}

 

Add the method to generate notifications:

generateNotification() {
	let date = new Date;
	LocalNotificator.scheduleLocalNotification({
        alertBody: 'The body',
        fireDate: date.getTime() + 1000 * 10,
        alertAction: 'View',
        alertTitle: 'The title',
        userInfo: {
            UUID: this.lastNotification,
            message: 'Created at: ' + date.toString()
        }
    }, (notificationData) => {
        this.setState({lastNotification: notificationData});
    });
}

 

In field fireDate we indicate time to show notifications. In our case it’s 10 sec after function request. In addition, we add a line to userInfo.message. This line saves function launch time.

Add the method to cancel notifications. We use UUID to cancel notifications:

cancelNotification() {
    LocalNotificator.cancelLocalNotification(this.state.lastNotification.userInfo.UUID);
    this.setState({lastNotification: null});
}

 

Add buttons to generate and to cancel notifications:

getCancelNotificationButton() {
	return (
		<TouchableOpacity onPress={() => this.cancelNotification()} style={[styles.button, styles.errorButton]}>
		<Text>Cancel Notification</Text>
		</TouchableOpacity>
	);
}

getGenerateNotificationButton() {
	return (
		<TouchableOpacity onPress={() => this.generateNotification()} style={styles.button}>
		<Text>Generate New Notification</Text>
		</TouchableOpacity>
	);
}

 

To generate notifications we need user’s permission. So it is necessary to add permission request code at the end of the file:

// request permissions
LocalNotificator.requestPermissions();

AppRegistry.registerComponent('SwiftReactNative', () => SwiftReactNative);



You may check the full code of the file here.

 

Checking functionality

 

Start the app in the emulator: Product->Clean and “Product-> Run

After its launch the app should request the permission to send notifications:

463

 

We grant this permission and start testing:

1)      Testing how the app gets notifications:

-          Click on green button

-          Wait for 10 seconds

-          Get a notification

464

 

2)      Testing how the app cancels notifications:

-          Click on green button

-          Click on red button

465

                 -          Wait for 10 seconds

                 -          Make sure that there are no notifications    

 

Though React Native is written in Objective-C it is possible and advised to use Swift modules in applications. 

Application code is available here.                                                                                                                                    

 

 

Alex K.

Alex K.

Senior Ruby on Rails Developer at iKantam

Browse Recent Posts