超お父さんの日記

日記とか雑談とか、スノーボードとか、サーフィンとか、たまに技術系とか。

【Amplify iOS】DynamoDBのGlobal Secondary Indexを使いたい

AmplifyのDataStoreはとてもすごい。
どうすごいかは公式のブログに書いてある。

aws.amazon.com

そんな便利ですごいDataStoreだけど、データのソートってどうやるの?
例えばゲームスコアのランキング。

DataStoreでデータ持ってきてから、こちらでソートすることもできるけど、
全部のデータ持ってきてソートするとか、ちょっと。
予めソートされたデータを持ってきたい。

DataStoreのデータはDynamoDBと同期されているので、
DynamoDBのGlobal Secondary Index(以下、GSI)を使ってデータ持って来れればイケる気がする。

調べてみると、schema.graphqlでtypeに@keyをつけるとGSIを設定できるみたい。
こちらの記事、とても参考になりました。ありがとうございました。
qiita.com

実際に以下のようなスキーマを定義してみると、、、

type GameScore @model
@key(name: "rankingIndex", fields: ["gameId", "score"], queryField: "rankingQuery")
{
  id: ID!
  userId: String!
  gameId: String!
  score: Int!
}

amplify pushして、DynamoDBを見るとrankingIndexというGSIが作られている!
AppSyncの管理画面でSchemaを見ると rankingQuery という名前のQueryがあり、そのリゾルバーを見るとrankingIndexを使っているっぽい!

と、ここまで順調だったのだけど、DataStoreでGSIのクエリをどう呼び出せばよいのか・・・
ググってもズバリな情報は見つけられず。
こうなったらと、Amplify iOSgithubに質問を投げてみた。

How can I fetch the sorted data using the DynamoDB Global Secondary Index? · Issue #565 · aws-amplify/amplify-ios · GitHub

DataStoreでソートする機能はいつか実装されるようです。
DataStoreに拘らず、他に方法はないのかという質問は返ってこず、
機能リクエストのタグが付けられて終わってしまいました。。
英語が何かダメだったのかな。。?

DataStoreはもう無理そうなので諦めて、APIの機能で rankingQuery を呼び出せないかということで、
Amplify.API.query周りのコードを読み進めてみた。
けどAmplify.APIのquery処理に任意のクエリrankingQueryを呼び出せるような仕組みは無さそうだった。 *1
ただ、少し弄ればできそうだったので、自作することにした。

GraphQLRequestを生成するとこで、いろんなDecoratorクラスが生成され、
生成されたDecoratorクラスたちがGraphQLを呼び出す際のクエリ文字列などを生成していた。

なので、rankingQuery呼び出し用のDecoratorクラスを作り、GraphQLRequestを拡張してrankingメソッドを追加した。
こんな感じ。

public struct RankingIndexDecorator: ModelBasedGraphQLDocumentDecorator {
    
    enum SortDirection: String {
        case asc = "ASC"
        case desc = "DESC"
    }
    
    let gameId: String
    let queryField = "rankingQuery"
    var greaterThan: Double = 0
    var sortDirection: SortDirection = .asc
    
    public func decorate(_ document: SingleDirectiveGraphQLDocument, modelType: Model.Type) -> SingleDirectiveGraphQLDocument {
        
        var inputs = document.inputs
        inputs["gameId"] = GraphQLDocumentInput(type: "String", value: .scalar(gameId))
        inputs["score"] = GraphQLDocumentInput(type: "ModelFloatKeyConditionInput", value: .object(["gt" : greaterThan]))
        inputs["sortDirection"] = GraphQLDocumentInput(type: "ModelSortDirection", value: .scalar(sortDirection.rawValue))
        
        return document.copy(name: queryField, inputs: inputs)
    }
}
extension GraphQLRequest {
    
    public static func ranking<M: Model>(_ modelType: M.Type,
                                         gameId: String,
                                         scoreGreaterThan: Double = 0,
                                         where predicate: QueryPredicate? = nil) -> GraphQLRequest<[M]> {
        var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelType: modelType, operationType: .query)
        documentBuilder.add(decorator: RankingIndexDecorator(gameId: gameId, greaterThan: scoreGreaterThan))
        
        if let predicate = predicate {
            documentBuilder.add(decorator: FilterDecorator(filter: predicate.graphQLFilter))
        }
        
        documentBuilder.add(decorator: PaginationDecorator())
        let document = documentBuilder.build()
        
        return GraphQLRequest<[M]>(document: document.stringValue,
                                   variables: document.variables,
                                   responseType: [M].self,
                                   decodePath: document.name + ".items")
    }
}

で、呼び出すのは

Amplify.API.query(request: .ranking(GameScore.self, gameId: "HogeGame"))

で動かしてみると、、、ソートされてデータが取得できた!!
めでたし。めでたし。

他にちゃんとした方法があるようでしたら、どなたか教えて欲しいです><

*1:podのAmplifyのバージョンは1.0.3

【Amplify iOS】AppSyncの認証モードをIAMにして未認証ユーザーが使えるようにする

前回、未認証ユーザーのIDを取得できるようになって、
アプリをガシガシ作り込んで一通り動くまでになった。

バックエンドを勝手にやってくれるAmplify万歳。
なのだけど、動かせるようになって7日経過した頃に問題が発生。

APIのqueryが全てエラーに。
ステータスコードが401となっていた。
APIキーの期限が切れたようだ。

Amplifyのチュートリアルを何も考えずに進めていると、
知らず知らずのうちに認証モードをAPIキーにしていて、有効期限が7日に設定される。
最大365日に設定できるみたいだけど、この認証モードだとアプリを1年に1回APIキー更新のアップデートが必要になってしまう。
まるで昔のプッシュ通知のように。。。

そこでAppSyncの設定画面をみてみると、認証モードでIAMが設定できることに気付き、設定方法を調べてみる。
公式ドキュメントをみる感じ、IAMのロールにポリシー追加したら行けそうな気配。
セキュリティ - AWS AppSync

amplify add api(あるいはamplify update api )をして認証の質問のとこでIAMを選び、プッシュします。*1
そしてIAMの管理画面でポリシーの追加の際、Resource部分を自分のリソース名に変えて設定。
「1 つ以上のアクションがこのリソースをサポートしていません。」という警告がでますがとりあえず気にせず。
そのポリシーをunauthRoleにアタッチして、アプリを動かしてみたら、、、行けたー。

Amplifyっていろいろ勝手に裏側を設定してくれるので、楽っちゃ楽なのですが、何か起きた時に裏側がどうなってるか知らないと太刀打ちできません。
この記事とても参考になりました、ありがとうございました。
qiita.com

*1:ちなみにAmplify CLIのバージョンは4.22.0

【Amplify iOS】Cognito IDプールの未認証ユーザーのIDを取得する

サーバーレスで簡易的なユーザー登録機能を作りたい。
そういうこと、たまにあると思います。

自分も何年か前にAWS Mobile Hubを使ってiOSアプリを作ったことがあるので、またAWS Mobile Hubかなと思って調べてみると、 ここ数年の間に何やらAmplifyなるものができてるじゃない。

Amplify iOSチュートリアルとかを見つつ、なんとかそれぞれのカテゴリは動く状態を試すことができた。 *1 docs.amplify.aws

ということで、Amplify iOSを使って以下のようなことを実現したい。

  • ユーザー登録せずに使えるアプリ

  • だけどユーザーはニックネームなどを登録できる

つまりアプリ初回起動で個別の変化しないIDを発行して、そのIDでユーザー情報を管理したい。

結論からいうと、すんごくハマったけどできた。
*2 なぜハマったかと言うと、タイトルにある通り、未認証ユーザーのIDの取得方法がわからなかったから。

Amplifyの認証はCognitoのユーザープールなのだけど、一番シンプルなのでもユーザー名やEメールでのユーザー登録が必要だ。
けど、いつかそういったちゃんとしたユーザー登録機能を追加したいってなるかもしれないので、 CognitoのIDプールの未認証ユーザーのIDを使おうと思ったのだけど、 このIDはAmplify iOSだと一体どうやって取得出来るのかっていう。

奇跡的に1つ情報を見つけた。
https://itnext.io/unauthenticated-access-to-aws-for-ios-apps-using-aws-amplify-cb87222eb5d

要は、Amplify Analyticsを未認証ユーザーでもトラッキング出来るような設定にしておいて、 AppDelegateのdidFinishLaunchingのreturnに1行処理を書くだけ。

import AWSMobileClient  

AWSSignInManager.sharedInstance().interceptApplication(application, didFinishLaunchingWithOptions: launchOptions)

AWSSignInManager Class Reference

そうすると、IDプールのIDは

import AWSAuthCore  

AWSIdentityManager.default().identityId

で取得出来た。 Amplifyの存在を知ってから、ここに辿り着くまで何日かかったことやら。。。

追記)
Authentication - Accessing credentials - Amplify DocsにもIDプールのIDの取得方法が書いてありました。
10行目の

let identityId = try identityProvider.getIdentityId().get()

でIDプールのIDの値が取れました。

*1:チュートリアルにあるAmplify toolsというのがエラー吐いて動かせなかったので、そこでちょっとハマった。 自分はxcodeのrun scriptには設定せず、何かAmplifyに変更を加えたらtoolsのシェルを直接実行してます。。 githubでも同じ問題になってる人がいて助かりました。 New iOS project build failure · Issue #345 · aws-amplify/amplify-ios · GitHub

*2:機種変などで端末変わったユーザーはサポートしません。その場合は新規ユーザーになります。

どハマりしたiOSアプリ内課金リジェクトの話

つい先日リリースしたアプリの審査ですんごいハマった。 初めは課金周りだけじゃなく、スクショとか3つくらいの理由でリジェクトされて
課金はsandboxのテストでは何も問題なかったし、課金のリジェクト理由は謎だったし、 あらあらくらいの気持ちで課金の指摘以外を修正して再申請を出した。

そしたらまたリジェクト。
無視した課金部分の理由が残った形だ。

Guideline 2.1 - Performance - App Completeness
We found that your in-app purchase products exhibited one or more bugs when reviewed on iPad running iOS 13.4 on Wi-Fi. Specifically, we were still unable to make any in-app purchases at the time of review.
Next Steps
When validating receipts on your server, your server needs to be able to handle a production-signed app getting its receipts from Apple's test environment. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code "Sandbox receipt used in production," you should validate against the test environment instead.
Resources
You can learn more about testing in-app purchase products in your development sandbox environment in App Store Connect Developer Help. For more information on receipt validation, please see What url should I use to verify my receipt? in the In-App Purchase FAQ. Learn how to generate a receipt validation code in App Store Connect Developer Help.

レシート検証について言われてるけど、そんなこと知ってるし、当然やってる。
sandboxテストでは問題なく課金できるし、いったい何を修正したら良いのかと。

あと厄介なことに課金アイテムまで!マークついて「デベロッパの対応が必要」という謎ステータスになってるし。。
ググってみると課金アイテムを作り直すとかいう記事が出てきた。
(結果的にはこれは不要でした。参照した記事以降に作り直さなくても大丈夫になったのかな?*1setohide.blogspot.com 今回のアプリは消耗型と自動更新サブスクリプションがあって、結構な数の課金アイテムあるから作り直しはまぁまぁ面倒だ。。
藁にもすがる思いで作り直して、入念にsandboxテストをして再申請したりしてみた。
が、無情にもリジェクト。

エラーのログをCrashlyticsに記録するようにして再申請してみたり。
もちろんリジェクトだけど、何か情報が得られるかもしれないと。
しかしそれも無駄だった。何もエラーは記録されなかった。

問題解決センターでレビュワーに箇条書きで質問してみるも
まさかの質問全無視の返事がきた。

Hello,
Thank you for your reply.
During our review, we found that your app displayed an error when attempting to purchase the in-app purchase product(s). While we cannot provide technical assistance or the Apple account associated with the reported issues, we have put together the following resources which discuss common issues seen in apps during review.
To start, please review the App Testing Guide, which gives an overview of how to test your app to minimize the chances of issues occurring during review.
You may also want to review Designing for Real-World Networks, as all apps are reviewed on-device and in an environment that replicates real-world use of your app.
We hope that the above is useful in your revision process, and we look forward to reviewing your revised and resubmitted app.
Best regards,
App Store Review

こんな返事きたら、よしこばりに「クソがっ!」って叫んじゃうよ。

そんなこんなでもう打つ手なしかと思われた時、デベロッパーフォーラムで気になるやりとりを発見。

I learned that Apple introduced some new contact information requirements silently in App Store Connect "Payment / Tax / Banking" section. I added all the required information until everything looked green again. forums.developer.apple.com

むむむっと思って、確認してみると同じ状態だった! f:id:hirokim412:20200405155118p:plain

怪しいことは何でもいいから潰しておこうと思い、
連絡先情報をクライアントに入力してもらいイエローをグリーンにしてもらって、アプリの方は何も変更せず再申請。

そしたら審査通過!!!
大変良い経験になりました。
ありがとうございました。

*1:課金アイテムの「デベロッパの対応が必要」になった場合の作り直しは不要でした。 どれか1つ課金アイテムの情報を適当に更新して保存すると全ての課金アイテムが「審査待ち」ステータスなり、 その状態でアプリを再審査に出して通りました。

iOS13 だとUISliderのTrackImageにセットした画像がバックグラウンド復帰時になくなってしまう?

UISliderの

setMinimumTrackImage(image, for: .normal)
setMaximumTrackImage(image, for: .normal)

で画像を設定し、こんな感じの表示にしてました。

f:id:hirokim412:20200213123816p:plain

これが表示される画面で、ホームボタンを押して一旦バックグラウンドにし、 アプリアイコンタップ等でフォアグラウンドに戻ってくると、、、

f:id:hirokim412:20200213124103p:plain

設定した画像がなくなってる・・・謎っ!!

iOS12のシュミレータだと発生せず、iOS13のシュミレータだと発生しました。

こういうの嫌だな〜と思っていたところ、 ちょうど他の画面でもUISlider使ってて、そちらでは発生せず。

これは、、と思ってコードやらインスペクタやら見比べてまくったところ、 発生する方のUISliderのインスペクタには 'Min Track' と 'Max Track' のカラーがデフォルトじゃないという違いを発見。

f:id:hirokim412:20200213124535p:plain

ここのカラーをデフォルトに戻したら直りましたとさ。

しかしiOS13からこの挙動って、、、バグなの?正しい挙動なの?

iOSのSandboxテスターアカウントのパスワード

久々に課金周りの設定をしていたら、Sandboxテスターアカウントのパスワードで一瞬ハマったのでメモ。

f:id:hirokim412:20200116160915p:plain

表示されてるエラーの内容では謎だったけど、要はもっと複雑にしろって話だった。

ストアカウントだし、パスワードは複雑にして忘れるのが嫌だなと思って

tester001

と単純なものにしてたのを

Test_001

にしたら大丈夫だった。

これでも十分に単純だと思うけど。。。

スノーボードのターンの自分なりのコツ

ターンの調子にバラつきがあって、トゥサイドが良い感じの時はヒールサイドを見失っていたり、その逆だったり。

 

それがここ数シーズン安定してきて、見失う幅が小さくなってきた気がするのだけど、家庭の事情でこれから行ける回数が大幅に減る可能性があって、また見失う前に書き留めておこうと思う。

あ、ターンと言っても色々あるので、ここでは比較的荒れてない締まった圧雪バーンをロング、あるいはミドルの所謂カービングターンをする場合を想定する。

 

ちなみに自分のレベルはスノーボードは18歳から始めて、少なくてもシーズン20日くらいは滑ってるはず。年々その数は増えていて、先シーズンは59日だった。

あとだいぶ前だけどJSBAの検定1級までは取ったことはある。イントラになるつもりも無かったので、その先には進んでいない。

昔は手摺狂会の相内康夫会長とか、一般人グループの地擦馬鹿一代の動画に興奮してグラトリに明け暮れていた。

ある時からターンの奥深さを知ってターンが面白くなってからは、フリーランメインになり、いろんな山のBCにも行くようになった。

ここ数シーズンは知り合いのツテで知った北海道のニセコ界隈を滑っているダウンチルの人達の滑りに割と影響を受けていて、あの気持ち良さそうなライディングに憧れている。

 

 

検定とか受けるとJSBAのスノーボード教程という教科書を必然的に知ることになり、そこにはターンの種類とか内倒だとか角付けだとか専門用語が出てきて、なんともイメージしづらい説明で、あれを読んで頷ける人はもう既にそれが出来てる人に限られる気がする。

あれを読んで上手くなるのは難しいと思う。

 

本や雑誌を読んだり、プロやイントラと滑って教えてもらったりしても、やっぱりそれは滑れてる人目線のアドバイスだったりして、滑走能力が近いレベルじゃないと理解が難しいことが多いと思う。

あと、人によって教え方が違ったりするのがまた厄介で、本質的には同じことを言っているのだけど、表現の方法が人それぞれで、いろんな人に教われば教わるほど沼にハマっていったりもする。

自分ができるようになると、この人とあの人は違うこと言ってるけど、要はこういうことか、などと理解できたりするのだけど。

 

やはりというか、当たり前なのだけど上手くなるためには、上手い人達と滑ったり、教えてもらったりして、それを踏まえて自分で試行錯誤しながら滑りまくるしかないと思う。

 

ということで、以下のリストは自分が今までの経験から自分なりに腹落ちした感覚なので、これを見たからといって他の人がどうなるかは知りません。

 

基本姿勢

これが一番大切。

これが出来てないと板にしっかり力を伝えられない。

自分のビンディングの角度は前24°、後0°(もう少し前に振ってもいいかも)

 

ビンディングつけて板の上に立って、顔の向きは常に板の進行方向。

体というか骨盤?は前向き過ぎず、横向き過ぎず、斜めくらい。

そして上半身を軽く被せるようにする。前の腰をクイっと少し上げる感じにするとしっくりくる。視線を落とすと、自分の顎の下に前膝があるくらいが丁度いい。

 

両足は膝を内側に回し入れて内股のようになる。結構力がいる。ずっとこれで滑ってると腿がパンパンになるけど安定する。漫画「グラップラー刃牙」の愚地独歩がしてた「三戦(サンチン)」と似ている。

 

姿勢を低くして滑る時は、両膝(特に後ろ膝)を曲げて上半身を被せていく。胸と膝が当たるくらいが一番低い姿勢になる。

あと顔はしっかり前を向くこと。低い姿勢になると顔が下を向いて目線だけ前を向いてるような感じになりがちだけど、顎を上げて顔面を進行方向に向ける。

 

姿勢の高さは滑っている中で常に上下するけど、上下動の際でも常に顔が板の幅に収まっている状態、なんというか板に対して垂直に上下するので視界の上下だけが変わって、左右は動かない感じ。

ターンの途中とかに目線だけ下に落として、板の面と垂直になってるか確認するといいと思う。(特にヒールサイドターンの時)

  

トゥサイドターン

ヒールサイドターンの後半で次のトゥサイドターンのピークをどこにするかを決めて、開いた上半身を骨盤ごとそのまま斜め前に持ってくる感じ。後ろの手(というか肘かな)を板の斜め前、ノーズ横くらいの雪面に近づけるイメージでトゥサイドターンに入ると、しっかりとエッジが入って気持ちいいターンができる。

重心は板のセンター、もしくは後ろよりでも面白い。

目線は常に進行方向か、切れ上がるターンの場合は切れ上がりたい場所を見る。

(後ろから滑ってくる人に注意) 

 

ヒールサイドターン

 トゥサイドターンと同じで、トゥサイドターンの後半で次のヒールサイドターンのピークを見定めて、ヒールサイドターンに入る。

いきなり上半身は開かずに、感覚としては後ろの肩を残しつつ、板に対して垂直に姿勢を低くする。

そして前足を内側に入れながら伸ばすイメージで前足の踵を雪面に押し付けてエッジを立てる。

前の手で板の斜め前、ノーズ横くらいの雪面を触りに行くような感じで上半身を被せる。

そこからの重心は後ろ足。お尻を後ろ足のハイバックに近づける感じ。

姿勢が低ければ低いほど後ろ足の腿に負担がかかる。

ヒールサイドターンでは基本姿勢の顎を上げるの忘れがちなので注意。

そしてターンの前半から後半にかけて徐々に上半身を開いていく。

深い切れ上がるターンをするときは、割と早めに上半身は開いてしまってもいいかも。

目線はトゥサイドターンと同じ。

 

ターンのリズム

トゥとヒールで気にしてるとこ書いたけど、ターンのリズムが狂うと動きがバラバラになって全然気持ちよく滑ることはできない。

ターンのリズムはブランコで立ち漕ぎに似ている。

ブランコが地面に一番近くなる時が、スノーボードのターンのピーク。

変なタイミングでブランコの板に力を加えたり、加える力が板に対して垂直にじゃなかったらブランコは上手く加速しない。

スノーボードで上手くリズムをとるためにヒールサイドからトゥサイドのターンの1セットで前の肩(もしくは肘)で∞の八の字を描くような感じが調子いい。

ターン弧のサイズで八の字を描くスピードも変わる。大きいターンなら八の字もゆっくりだし、小さいターンなら早い。

 

 

以上、ターンについて一通り気にしていることを書いたけど、乗っている板によって重心というか軸の位置は変わるけど、そこは上記が出来てる上での微調整の範囲だと思う。

 

最初に書いたけど、これは締まった圧雪バーンを気合い入れて滑る時の感じなので、新雪だったりシャバ雪でバーンが柔らかい時は、トゥサイドもヒールサイドもターンの入りはあまり前足を踏みすぎず、センターくらいから荷重して、下半身でショック吸収するイメージで腰を落とすといい感じ。

 

いつかまた新しいことに気づいたらアップデートします。