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
Create a React Native project:
$ react-native init SwiftReactNative
Open file ios/SwiftReactNative.xcodeproj in Xcode and start the project.
Several minutes later our application starts.
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
Create a new swift file and name it AppDelegate.swift.
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).
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.
Clean the project (Product->Clean) and then start it on emulator (Product-> Run).
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:
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
2) Testing how the app cancels notifications:
- Click on green button
- Click on red button
- 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.