(iOS) Slate+ Expand Samples with Riiiver

This is a sample app with which you can learn how to integrate the Riiiver SDK into your companion app for Slate+.
With this app you'll find:

- Expansion of UX with Slate+ by utilizing the Riiiver SDK.
- How the Riiiver functions, piece(s) and iiidea(s), work.
- User Interface provided by our SDK, e.g. login screen, iiidea list screen.


How does this sample demonstrate?
  • (1) Scan BLE devices nearby.
  • (2) Connect to Slate+ evaluation board(EVB).

    • Discover the service and characteristics provided as Riiiver_Demo by QCLI.
    • If it doesn't find the service and characteristics, it doesn't move to forward pages.
  • (3) Login to Riiiver.

  • (4) Complete iiidea settings.
  • (5) Assign the Riiiver functions that'll be triggered by pushing the button on the EVB.
    • Push the button and find the result that appears on the display of the EVB.
Screenshots

app_journey

Out of scope for this Sample App
  • This app is not a template. Integrate our SDK to your app as you like, e.g. you can choose a suitable architecture for your app.
  • This app does not implement data storage that keeps the app state even after killing and relaunching the app.
  • This app does not implement a complete paring of BLE connection. The Riiiver functions only work when the connection is alive between Slate+ and your mobile phone.
Prepare for development using Riiiver SDK
Preparing for development
What this section describes:
  • Sign up as a developer
  • [resource file] erconfiguration.json.

To develop with Riiiver SDK, sign up as a developer and register information about your service and company. Please visit our developer site and sign up by following the instructions.

Riiiver Developer Site

The required information is as follows:

  • Your company information.
  • Your application information.
  • Your device information.

Riiiver SDK reads vendor and app information by referring to the erconfiguration.json file. The VendorId and AppId should be the IDs that Riiiver has given you when you registered your vendor and app information.

The prepared erconfiguration.json file must be located in the root hierarchy of the bundle. In this project, the file is placed in the directory Infra/RiiiverSDK/SupportingFiles, but Xcode's Run Script copies the file to the root of the bundle when the build is executed.

Install the Riiiver SDK
What this section describes:
  • How to import the RiiiverSDK.framework into your project

In this sample project, we use CocoaPods to install Riiiver SDK. We have provided a Podfile in the root directory of the project. Please install Riiiver SDK by yourself with the below command.

$ pod install

After the installation is completed, you will find the RIiiver SDK and external libraries used in this project under the Pods/ directory, and a {project_name}.xcworkspace file in the root directory. When building the application, please open and build the .xcworkspace file.

Note
The `Pods/` directory and Xcode workspace are not subject to git management.
Using Riiiver SDK features
Initialize the Riiiver SDK
What this section describes:
  • [method] initialize(deviceIdList:)
  • [method] addListener(listener:)
  • [protocol] ERSdkListener

Be sure to execute the initialize(deviceIdList:) method before using any of the methods provided in the Riiiver SDK.

In this sample app, we have prepared the initialization method in the class RiiiverSDKClient for calling methods of Riiiver SDK, and that method is called every time before executing a method of Riiiver SDK. The implementation is designed to return immediately if it has already been initialized, so it does not matter how many times it is called. The argument is passed the ID of the device supported by the application as an Array of String. If you have not registered your device information, please visit our developer site and register it first.

Also, since the ERSdkListener protocol is provided to delegate processing to the app side when an event occurs in the Riiiver SDK, we use the addListener(listener:) method to set the delegate after initialization is complete. This method is also implemented to do nothing if the delegate instance is the same, so it does not matter how many times it is called.

/* qcc5100.sample/Infra/RiiiverSDK/RiiiverSDKClient.swift */private func initialize() async throws -> Void {
    let error = await RiiiverSdkManager.initialize(deviceIdList: RiiiverConstants.deviceIds)
    if let error {
        print("## RiiiverSdkClient: initialized failed due to \(error.localizedDescription)")
        throw error
    }
    RiiiverSdkManager.addListener(delegate: self) // ← add delegate
    return
}

The ERSdkListener protocol provides the following delegate methods, so please implement the necessary processing.

Methods of ERSdkListener
  • startApplet(applet:error:)

    Called when the execution of iiidea is started.

  • endApplet(applet:blockName:output:error:)

    Called when the execution of iiidea has finished. blockName is the type of Piece (trigger, service, or action) that terminated. In case of abnormal termination, it will tell which Piece has been reached. Also, error contains error information.

  • onLogin(applets:)

    Called when login to Riiiver is completed.

  • onLogout()

    Called when users log out of Riiiver.

Note
- `Applet` is the name of iiidea when developing Riiiver. Please read "iiidea" instead of "applet" as it is used in the code and documentation.
- `Block` is the name of piece when developing Riiiver. Please read "piece" instead of "block" as it is used in the code and documentation.
Use the authentication features(login and out)
What this section describes:
  • [method] getLoginUrl()
  • [method] verifyCodeOf(uri:)
  • [method] logout()

An authentication page is provided to login to Riiiver. Please open the URL in the return value of the getLoginUrl() method. When a user completes the login operation on the authentication page, a redirect will occur. The redirect URL is registered as a part of your application information via our developer site. If you have not registered, please visit our developer site and register the redirect URL. Please set it in info.plist so that the app can be launched via a custom scheme.
When the app is launched via a custom scheme, a short-lived authorization code is given as a query parameter in the URL. The URL retrieved by the application(_:open:options:) delegate method in the AppDelegate or the onOpenURL(perform:) method if you are using SwiftUI, is passed to the verifyCodeOf(uri:) method to complete the login to Riiiver.

Note
- It is recommended to use `SFSafariViewController` to open the authentication page, as it will cause a redirect and pass values to the app with a custom scheme. If you use the `UIApplication.shared.open(url:)` method to open in an external browser, the user may have changed their default browser from Safari.

If you want to log out of Riiiver, execute the logout() method at any time.

Display the iiidea list and the settings screen
What this section describes:
  • [class] ERUserWebPagesView
  • [protocol] ERUserWebPagesViewDelegate

We provide a web page that aggregates a iiidea store for users to search for and obtain iiidea, and a list view for users to set or delete already obtained iiidea. The ERUserWebPagesView class, which extends the WKWebView class, is provided as a class for displaying these web pages.

loadPage(type:) method can be executed to display the page, in the viewDidLoad() method when using UIViewController in your project, or in the makeUIView(context:) method when using SwiftUI's UIViewRepresentable in your project.

Some iiidea require the use of device's permissions or OAuth authentication for third-party services at runtime, so when a user requests those permissions on a web page, the ERUserWebPagesViewDelegate protocol is provided to delegate the handling of those events to the app side. The following delegate methods are provided for this delegate, so please implement the necessary processing.

Methods of ERUserWebPagesViewDelegate.
  • didReceivePermissionRequest(permissionType:)

    Called when the request button for a device's permission on a web page is tapped. The type of permission being requested is passed as an argument, so that you can present the user with the permission's usage authorization dialog.

  • didReceiveOpenUrlRequest(url:)

    Called when a new URL needs to be opened, such as for OAuth authentication. Open the URL passed as argument.

  • didReceiveCloseRequest()

    Called when the close button on the web page is tapped.

  • onError(error:)

    Called when an error occurs on a web page.

Once the necessary settings for iiidea have been completed, iiidea can be activated. iiidea is activated and is ready to run.

Pre-install some iiidea(s)
What this section describes:
  • [method] forcedDownloadApplet(appletIds:)

If you want to make pre-installed some features when the user installs your application, you can use the forcedDownloadApplet(appletIds:) method to make the iiidea of the ID specified in the argument already obtained.

Get the available iiideas list
What this section describes:
  • [method] getMyApplets(deviceIdList:)
  • [class] ERApplet

This sample app is implemented so that each of the two physical buttons on the Slate+ evaluation board can execute a different iiidea when pressed. For this purpose, the app provides a UI that allows the user to freely select the iiidea to be executed. At this time, the method getMyApplets(deviceIdList:) is used to retrieve the list of iiidea already obtained by the user. By passing a device ID as an argument in the form of an Array of String, a list of available iiideas on that device can be obtained. The return value is an Array of ERApplet.

The ERApplet class provides properties containing iiidea metadata, as well as methods related to activation deactivation and iiidea execution. When you want to find out if a iiidea is ready to run (activated state), you can call getActivateStatus() method.

Implement the Piece(s) and execute iiidea
What this section describes:
  • [protocol] ERBlockExecutor
  • [protocol] ERBlockDelegate

iiidea is a small application that combines 3 Pieces. We call Trigger Piece for the first one, Service Piece for the second one, and Action Piece for the third one, respectively.

All Pieces implemented in the app or SDK must inherit the ERBlockExecutor protocol. The following methods are defined in this protocol, so please implement the necessary processing.

Methods of ERBlockExecutor
  • enable(applet:)

    Called when an applet using this Piece is activated by the user. Return true if the piece can be activated, or false if it cannot be activated.

    Although there is no example implementation in this sample app, the geofence trigger implemented in the SDK, for example, checks whether permission to use location information has been obtained and returns false if permission has not been obtained.

  • execute(applet:blockName:preferenceJson:inputJson:handler:)

    Called when an applet that uses this Piece is executed and in the order of execution of this Piece. In other words, this method is called when the previous Piece is completed.
    When the processing of the Piece is completed, the completed(outputJson:error:) method of the ERBlockDelegate passed as the handler is executed. In case of abnormal termination, call the completed(outputJson:error:) method with the argument error.
    Note that this method is called when it turns activated in the case of the first Piece of iiidea, the trigger Piece. It is necessary to make sure that the execution of iiidea starts when the desired event actually occurs. Regarding the QCC5100TriggerPieceCore, which fires when a button on the Slate+ EVB is pressed by the user, the QCC5100TriggerPieceCore instance holds the iiidea id and a handler for it as a key-value pair in the dictionary type. And it implements the didTrigger(appletId:input:) method to notify events. At the timing when the peripheral(_:didUpdateValueFor:error:) method of CBPeripheralDelegate is called (i.e., when the app is notified of the button press event on the EVB), the didTrigger( appletId:input:) method is called to start iiidea execution.

  • disable(applet:)

    Called when an applet that uses this Piece is deactivated. Implement a process such as releasing resources that needed to be held while waiting for execution.

    In QCC5100TriggerPieceCore, the ERBlockDelegate corresponding to iiideaId is discarded. Also, the geofence trigger implemented in the SDK stops monitoring location information.

Run iiidea from outside the app using Firebase
What this section describes:.
  • [method] setNotificationToken(_:)
  • [method] executeTaskForRemoteNotification(_:)
Preparation

The Firebase Cloud Messaging API is used to achieve this. The following files are required to use the function.

  • GoogleService-info.plist

  • APNs authentication key

For information on how to prepare the file, please refer to the following document

Registering a key

Registering an App Identifier

Generating a provisioning profile

Enable Push Notifications

Enable Background Modes (iOS only)

The prepared Google-Service-info.plist file must be placed in the root hierarchy of the bundle. In this project, the file is placed in the directory Infra/Firebase/GoogleService, but Xcode's Run Script copies the file to the root of the bundle at build execution.

FirebaseMessaging Setup

In order to use the FirebaseMessaging APIs, some implementation is required in the AppDelegate.
In the application(_:didFinishLauchingWithOptions:) method of the AppDelegate, you must perform these actions: (1) the FCM token issue request, (2) FCM delegate configuration, (3) push notification permission request, and (4) APNs device token issue request in the application(_:didFinishLauchingWithOptions:) method of AppDelegate.

// (1) Request to issue FCM token and set delegates
FirebaseApp.configure()     
// (2) Set messaging delegate
Messaging.messaging().delegate = self
        
// Register for remote notifications.
// (3) This shows a permission dialog on first run, to show the dialog at
// a more appropriate time move this registration accordingly.
if #available(iOS 10.0, *) {
    // For iOS 10 display notification (sent via APNS)
    UNUserNotificationCenter.current().delegate = self

    let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
    UNUserNotificationCenter.current().requestAuthorization(
          options: authOptions,
          completionHandler: { _, _ in }
    )
} else {
    let settings: UIUserNotificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
    application.registerUserNotificationSettings(settings)
}

// (4) Request to issue APNs device token
application.registerForRemoteNotifications()

Implement the application(_:didRegisterForRemoteNotificationsWithDeviceToken:) method, which will be called when the APNs device token is issued. The FCM token is issued when the device token for the APNs is registered with the Firebase Messaging SDK.

func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    debugPrint("[APNs]デバイストークン受信 [\(deviceToken.map{ String(format: "%.2hhx", $0) }.joined())]")
    Messaging.messaging().apnsToken = deviceToken
}

Inherit the MessagingDelegate protocol and implement the messaging(_:didReceipRegistrationToken:) method, which will be called when the FCM token is issued, and set the FCM token to the Riiiver SDK. Execute the setNotificationToken(_:) method.

extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        if let fcmToken {
            print("fcmToken = \(String(describing: fcmToken))")
            self.fcmToken = fcmToken
            Task(priority: .medium) {
                do {
                    try await RiiiverSDKClient.sharedInstance.setNotificationToken(fcmToken)
                } catch (let error) {
                    print("## MessagingDelegate setNotificationToken failed due to \(error.localizedDescription)")
                }
            }
        } else {
            debugPrint("fcm token is nil.")
            return
        }
    }
}

Implement the application(_:didReceiveRemoteNotification:fetchCompletionHandler:) method, which is called when a data message is received using Firebase (cf. message type). The received push notification is passed to the Riiiver SDK to delegate subsequent processing. Use the ExecuteTaskForRemoteNotification(_:) method.

For Riiiver-based data messages, the userInfo contains the classification information of the push notification with the key "notification_id". If your project already uses Firebase, you can branch the process depending on the presence or absence of this key. In other words, determine if this key is included and execute the executeTaskForRemoteNotification(_:) method only if it is included.