- 投稿日:2020-02-11T22:18:17+09:00
SAP CP Mobile Services (Cloud Foundry) のPush通知APIを叩いてみた
はじめに
この記事は「SAP S/4HANAで購買発注が登録されるたびiPhoneに「アイーン」とPush通知が来たらぜったいどきどきしちゃう」シリーズの2つ目の記事です。
前回までの実装
前回の記事では、「SAP S/4HANAで伝票登録されたら「あい〜〜ん」Push通知が来たらぜったいどきどきしちゃう」という奇特な業務ユーザからの要望をかなえるべく、SAP Cloud Platform (Cloud Foundry) Mobile Servicesから「アイーン」というPush通知を受け取れるiOSアプリを構築してみました。
このアプリをS/4HANAのプロセスと統合するためには、なにはともあれAPIを活用してこの「アイーン」通知機能を呼び出すことが必要になります。
そこで今回は、Mobile ServicesのPush APIを活用してiPhoneアプリに「アイーン」通知を送ってみようと思います。
今回の目標
今回の記事のゴールは、PostmanからMobile ServicesのAPIを叩いてPush通知リクエストを送り、「アイーン」通知をiPhoneアプリで受け取ることです。
前提条件
開発実行環境
- フロントエンド実行環境: iPhone11 pro iOS 13.3
- フロントエンド開発環境: macOS Mojave 10.14.6 / Xcode 11.3 ※ Apple Developer Account(有料)を取得していること。
- バックエンド開発実行環境: SAP S/4HANA 1809
- クラウド開発実行環境: SAP Cloud Platform (Cloud Foundry trial account) ※ 無料
- Mobile Services
- Integration Service
事前手順
前回の記事と同様の手順でPush Notification関連設定を済ませたiOSアプリケーションをMobile Servicesを活用して構築済みであること。
構築手順
それでは構築を始めていきましょう。
前回の記事ですでにだいたい作っているので、実は手順は(答えが分かってさえいれば)大したことありません。権限設定
前回の記事での権限設定(Role Collection
MobileServiceNotification
の割当)をしたと思うのですが、年末に仕様変更があったのかこの権限が出てこなくなっていたので(うーん)、自分でRole Collectionを作って割り当てることにしましょう。SAP Cloud Platform Cockpitの"Security" > "Role Collection" > "New Role Collection"ボタンより、
名前はなんでもいいのですがMobile_Notification
というRole Collectionを作成しました。作成したRolle Collectionの名前をクリックします。
"Add Role"ボタンを押してRoleを紐づけていきます。
場合によっては様々なApplication Identifierが選択可能だと思われますが、Mobile Servicesのアプリケーションインスタンスを作成したときに命名したインスタンスの名称を含んでいるApplication Identifier配下のRoleを紐づけていきます。
(今回はとりあえず疎通できるようにしたかったのでApplication Identifier配下のRoleを全部紐づけてしまいましたが、本当はきちんと選択する必要がありますね。)
最後に、"Security" > "Trust Configuration" > "SAP ID Service" にて、
対象のユーザのメールアドレスを入力して"Assign Role Collection"ボタンを押下し、Role Collectionを紐づけます。API仕様の確認
Mobile ServicesのPush通知関連APIの種類
SAPのAPIのリファレンスといえばもちろんSAP API Business Hubです。
まずこちらを見るのが王道であると思われるので検索してみると、確かにRemote Push Notification APIと呼ばれるAPIのリファレンスが存在しています。ところがCloud Foundry環境のMobile Servicesのヘルプドキュメントを見てみると、似て非なるPush API情報(Native Push Notification for Backend)が記載されています。
迷わしいですが、SAP API Business Hubの情報がNeo環境のサービス向けなのかCloud Foundry環境のサービス向けなのか判然としなかったので、Cloud Foundryを前提としたHelpドキュメントに載っているNative Push Notification for Backendの方を参照することにしました(その結果疎通しました)。
APIをぜんぶ動確する元気はなかったので、この辺りのしゃっきりした情報をお持ちの方はぜひ教えてください。。
APIエンドポイント
上記のドキュメントを参照し、下記の比較的シンプルなエンドポイント(通知先のユーザもデバイスも指定しない)を用いることにします。
https://<mobile services host>/restnotification/application/<applicationId>
今回は一人遊びで実装しているので気にしないことにしますが、エンドポイントやパラメータによって通知先のユーザやデバイスを個別に指定することもできるようです。興味のある方はぜひHelp ドキュメントをご確認ください。
APIホスト
Mobile ServicesのAPIのホストが分からなくてずっと泣いていたのですが、結論としては下記のように作ることができるようです。
https://<subaccount name>-<space name>-<mobile app instance name>.cfapps.eu10.hana.ondemand.com
今回でいえばおよそ下記のような感じです。
https://s0099999999trial-dev-mobileaiiiiin.cfapps.eu10.hana.ondemand.com
APIリクエストbody
Native Push Notification for Backendは、その名の通りさまざまなNative通知サービスの仕様に対応できるよう、かなり柔軟に色々なパラメータをbodyに設定することができます。
今回はiOSの通知なので、APNs(Apple Push Notification Service)と呼ばれる通知の仕様に合わせたパラメータを設定してあげると、iOSっぽい通知の情報を設定することができます。
SAPのドキュメントとAppleのドキュメントなどを参考にしてbodyの中身を決めていきました(具体的な設定値は次のセクション参照)。
APIの動作確認
それでは上記の情報を元に、下記の設定内容でPostmanからAPIを叩いてみましょう!
設定内容
Method: POST
Request URL:
https://s0099999999trial-dev-mobileaiiiiin.cfapps.eu10.hana.ondemand.com/restnotification/application/mobileaiiiiin
Authorization:
設定値 TYPE Basic Username SAP Cloud Platformユーザのログインメールアドレス Password SAP Cloud Platformユーザのログインパスワード Headers:
設定値 Content-Type
application/json
Body:
{ "alert": "{\"title\": \"あい〜〜〜〜〜ん\",\"body\" : \"ドリフ大爆笑DVDの〇〇〇〇伝票(999999999999)が登録されました!\"}", "badge": 1, "sound": "sumikko_aiiiin.aiff" }※ 通知のSoundをカスタムしたい場合、XCode側での設定が必要ですが、今回の記事では手順の紹介を割愛します。
叩いてみた
次回は・・・
というわけで、Mobile ServicesのPush通知APIをじょうずに使うことができました!
次回以降の記事にて、このAPIをSAP Cloud Platform Integration ServiceのIntegration Flowから呼び出せるようにしていきたいと思います。
- 投稿日:2020-02-11T21:57:05+09:00
SAP S/4HANAで購買発注が登録されるたびiPhoneに「アイーン」とPush通知が来たらぜったいどきどきしちゃう(まとめ)
README
この記事は「SAP S/4HANAで購買発注が登録されるたびiPhoneに「アイーン」とPush通知が来たらぜったいどきどきしちゃう」シリーズのまとめ記事です。このシリーズでは、その名の通りSAP S/4HANAでの伝票登録をトリガーにiPhoneにPush通知を送信するための諸々の構築方法について説明しています。
複数に分割して書いた記事へのリンクをこの記事にまとめておくことにします。
INDEX
実装方法をまとめた記事は下記の4つです。
(もしもやってみたい人がいれば)一応上から順番にやっていけばだいたい実装できるようになっていると思います。
とりあえず「アイーン」とpush通知してくれるiOSアプリをSAP CP Mobile Service (Cloud Foundry) を活用して実装してみた
SAP S/4HANAでの伝票登録をIDocで拾ってSAP CP Integration Serviceをトリガーしてみた ※ アップデート予定あり
いったい何を実装しているのか
上記の手順で最終的に実現できる構築物の処理の流れはざっくりこんな感じです。
- SAP S/4HANAで購買発注伝票が登録されると、IDocと呼ばれる機能がそれを拾って、Remote Function CallでSAP CP Integration Service内のIntegration Flowをトリガーする。
- IDocからのリクエストbodyのなかに購買発注伝票番号が含まれているので、Integration Flow内でこれをうまく編集し、さらにSAP CP Mobile ServiceのPush通知APIを叩く。
- APIが叩かれると、iPhoneアプリに「あい〜〜ん! 購買発注伝票(9999999999)が登録されました!」と通知が来る。
……と文章で書いてみましたが、たぶん下記の構成図のほうがわかりやすいかなと思います。
どうしてこんなことになったのか
SAP S/4HANAからの通知がスマホに来たらぜったいどきどきしちゃうなと思ったからです。
現に、お店でランチを食べているときに誰かがS/4HANA社内環境で伝票登録をしたらしく、わたしのスマホが大音量で「アイーン」と鳴ったときにはかなりどきどきしました(この記事では説明を省略しているのですが、そのときはこのアプリの通知音を「アイーン」音声にカスタムしていました)。
ぜひ甘酸っぱくもほろ苦いSAPライフのためにこのアプリケーションをお役立てください。
関連情報?
この実装をもとに、chillSAP主催のライトニングトークイベント「SAPなんでもLT祭 ~若手編~」でデモを行いました。
その時のスライドはslideshareにアップロードしているので、よろしければ合わせてご覧ください。おしまい
- 投稿日:2020-02-11T18:32:09+09:00
Fastlaneのframeitでunexpected token at {....っていうエラーが出る件
Fastlaneを最新版にしてframeitでApp Store用のスクショを作ろうとしたら変なエラーがでた。
なんか文字列がjsonにパースできないらしい。吐き出されてた文字列をコピーしてJson validatorにかけると以下の場所がおかしいことがわかる。
Fastlaneのほうではちょうど1週間前にissueが作られていた。frameit is broken for me in release 2.141.0 #15972
回避方法(2/11時点)
fastlaneを2.138.0に戻す。
(順番に各バージョンに戻して動作確認したところ2.139.0からこのエラーが起きる。)Gemfileを以下の通り修正
source "https://rubygems.org" gem "fastlane", "2.138.0" # version指定を追加で、以下のコマンドで指定したバージョンにfastlaneを巻き戻す。
sudo bundle installしたらエラーが出なくなった。
- 投稿日:2020-02-11T17:02:12+09:00
Swiftの記号あれこれ
背景
Swiftの公式ドキュメント、ヘルプガイド、ブログのコードサンプルなどによく出てくる実装のなかで、どんな意味があるのかすぐに判断できない記述がありました。
メソッドの引数が顔文字化していたり(例えば、max(:_:_)
とか)、変数定義の型の最後にに?
や!
が付いていたりと、あれ?これはどういう意味だったかなというときに自分自身がすぐに思い出すとき用の記事があるといいなぁと思い記事にしました。対象とする読者
Swift初心者
環境
私がこの記事を書いている際に利用しているのは次の環境です。
Xcode 11.3.1
Swift 5.0Swiftの記号あれこれ
関数の引数に出現する_ (アンダースコア)
Swiftでは関数の引数にラベルをあらかじめ付けておき、利用者にそれを明確に指定させることができるが、名前をわざわざ指定しなくても明らかにわかる場合は、その仕組みだとコードが読みにくくなってしまいます。
なので、関数呼び出しするときの引数が、名前なしで渡せますよということを明示するもの。// 引数に名前付きで呼び出してもらう関数の書き方 func add(x: Int, y: Int) -> Int { return x + y } var r1 = add(x: 3, y: 2) // 5: OK var r2 = add(3, 2) // NG(引数に名前がないのでコンパイルエラー) // 引数に名前なしで呼び出してもらう関数の書き方 func addByNoLabel(_ x: Int, _ y: Int) -> Int { return x + y } var r3 = add(4, 7) // 11: 引数に名前なしでOK関数のシグネチャーで、
func(_:)
とかfunc(_:_:)
とかの表現を見かけたときは、_:
のところに、名前なしで引数を入れれば利用できるんだな、ぐらいで見ておけばよいです。型のうしろに?(はてな)
?
通称:はてなマーク
正式名:クエスチョンマーク値がオプショナル型であることを示す場合に利用します。オプショナル型は、指定した型の値もしくは値そのものがない
nil
のいずれかを表す型です。
Optional<Wrapped>
(WrappedにはInt,Stringなど実際の型が入る)が入りますが、コードが冗長となるのを避けるためのシュガーシンタックスとして用意されています。// 正式な構文だと var age: Optional<Int> // 簡潔に書くと var age: Int? = 21 // 利用時には、アンラップが必要(強制アンラップした場合) var nextAge :Int = age! + 1 // 22: オプショナルの値に!を付けて強制アンラップ変数等の後ろに
?
が出現した場合は、この変数は空っぽの状態(nil)もあり得るということと、Optionalを解除(Wrap包み紙から出してあげるイメージ)しないと、元の型としては利用できないという認識を持っておけばよいです。型のうしろに!(びっくり)
!
通称:びっくりマーク
正式名:エクスクラメーションマークこちらもオプショナル型ですが、
?
とは異なり!
で定義した値にアクセスする際には自動的に強制アンラップして元の型にしてくれます。なので「暗黙的にアンラップされたOptional<Wrapped>
型」と呼ばれています。
型としては、Optional<Wrapped>
型だけど、利用するときには通常のWrapped
型として利用可能。
注意点としては、値にアクセスしたときにnil
だった場合は、実行時エラーとなってしまいます。var age: Int! = 21 //アクセス時に自動的に強制アンラップしてくれる var nextAge: Int = age + 1 // 22範囲演算子(終了を含まない)
a..<b
aから始まり、終了bを含まない範囲。var range 1..<4 // CountableRange(1..<4) for value in range { print(value) } 実行結果 1 2 3範囲演算子(終了を含む)
a...b
aから始まり、終了bを含む範囲。var range 1...4 // CountableClosedRange(1...4) for value in range { print(value) } 実行結果 1 2 3 4さいごに
慣れない言語でのコーディングは不安です。
でも、知ってしまえば、慣れてしまえばそれほど難しくないことは多々あります。
私も今回のこの記事を自分で整理してアウトプットすることで、頭の中を整理できました。
私のようなSwift初心者の方に、この記事が少しでも役立てば幸いです。
- 投稿日:2020-02-11T14:06:56+09:00
Flutter プラットフォーム固有機能を利用するためのSystemChannels APIについて
はじめに
Flutterからプラットフォーム固有の機能を利用するためのAPIのSystemChannelsについて解説します。
SystemChannelsとは
まず最初に、基本的にこのAPIを使うことは推奨しません。Flutter Framework内部ではこのAPIを多用していますが、以下のように中間レイヤのAPIであるため今後のバージョンアップで変わる可能性が高いです。
SystemChannelsのソースコードを見ると、中身はMethodChannel, EventChannel等を利用していることが分かると思います。MethodChannelやEventChannel等についてはこちらにまとめていますので参考にしてください。
SystemChannelsの種類
用途毎にいくつかの種類が用意されています。一覧を以下に示します。
種類 機能 上位のFlutter Service lifecycle ライフサイクル widget navigation ナビゲーション widget, system_chrome system 不明…。ここで利用されているが、実質何もしていない widget accessibility アクセシビリティ (テキスト読み上げなど) PlatformViews, Semantics platform システム設定 (画面回転, 終了など) SemanticsService,RouteNotificationMessages etc. platform_views プラットフォーム固有のビュー操作 AndroidView, UiKitView skia グラフィックス - keyEvent キー入力 RawKeyEvent textInput テキスト入力 TextInput, AndroidView, UiKitView サンプルコード
lifecycle
ライフサイクルリスナーSystemChannels.lifecycle.setMessageHandler((message){ print('<SystemChannels.lifecycle> $message'); /* AppLifecycleState.paused AppLifecycleState.inactive AppLifecycleState.resumed AppLifecycleState.suspending AppLifecycleState.detached */ return Future<String>.value(); });navigation
ナビゲーション操作リスナーSystemChannels.navigation.setMethodCallHandler((call) { print('<SystemChannels.navigation> ${call.method} (${call.arguments})'); /* popRoute pushRoute */ return Future<dynamic>.value(); });system
実質何にも使えず、使われておらず、無視して良さそうです。
とりあえずコールバックだけ設定SystemChannels.system.setMessageHandler((message) { print('<SystemChannels.system> $message'); return Future<dynamic>.value(); });accessibility
テキスト読み上げサンプルSemanticsService.announce('Hello world', TextDirection.ltr)のFlutter Framework内部実装が以下です。
Dartfinal AnnounceSemanticsEvent event = AnnounceSemanticsEvent('Hello world', TextDirection.ltr); SystemChannels.accessibility.send(event.toMap());platform
アプリ終了SystemNavigator.pop()のFlutter Framework内部実装が以下です。
アプリ終了SystemChannels.platform.invokeMethod('SystemNavigator.pop');platform_views
flutter_webで利用しているので参考にしてください。
こんな感じでビューを作成していくfinal Map<String, dynamic> args = <String, dynamic>{ 'id': 1, 'viewType': 'Create WebView', }; SystemChannels.platform_views.invokeMethod('create', args);skia
Skiaキャッシュサイズ設定。他に機能がなく、今はこれしか出来ない…const maxBytes = 4 * 1024 * 1024; SystemChannels.skia.invokeMethod('setResourceCacheMaxBytes', maxBytes);keyEvent
キー入力リスナーSystemChannels.keyEvent.setMessageHandler((message) { print('<SystemChannels.keyEvent> $message'); return Future<dynamic>.value(); });textInput
キーボードの表示ON/OFFSystemChannels.textInput.invokeMethod('TextInput.show'); SystemChannels.textInput.invokeMethod('TextInput.hide');