【番外】AWS Lambda / API Gateway / DynamoDB を使ったサーバレスなネットワークロック機構

2016年4月14日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

あいも変わらず Evernote 用の iOSアプリを作っているのだが、複数端末からの更新競合を防ぐ為にロックが欲しくなった。Evernote の APIには欲しい機能が見つからず、苦肉の作としてネットワーク越しにロックを管理する仕組みを試してみた。今回 iOSの話ではないが番外編として記録を残す。

ネットワークロックサービス


便宜上勝手に名付けた。排他制御を持たないリソースへのアクセス競合を防ぐため、ネットワーク上にロックサービスを用意する。既存のリソースを更新する場合、最初にネットワークロックの取得を試みて、その後読み出し→更新→ロック解放と処理を行う。ロック取得に失敗した場合は後でリトライする。もしロック取得したプログラムが落ちてしまう場合は解放されないロックが残ってしまう。この問題を最小限にする目的でロックにタイムアウトの機能を持たせる。時間が経過したロックは無視して、再取得できるようにする。

アーキテクチャ


AWSの LambdaとAPI Gateway、そして DynamoDB を使ったサーバレスアーキテクチャで組み立てる。

サーバ(EC2)を立てる必要がなく楽ちん。
Lambda Function は JavaScript (node.js)でコーディングする。


ロック取得に対する排他制御


ロックを保管管理するストレージ(DB)が排他的な操作に対応しているかどうかが鍵になる。今回の用途を考えると RDSのような大げさなものは使いたくない。またLambdaから容易に操作できるものということで、 DynamoDBを採用する。最小構成で使えば安価に運用できるのも魅力的。この DynamoDBについて排他制御を調べいるとまったく同じことを考えている人が居た。参考になる。
DynamoDBをロックマネージャーとして使う

DynamoDBではアイテム(レコード)作成時の前提条件を定義することができて、これが成り立たないと更新処理を失敗させることができる。この前提条件に、これから操作を行いたいリソースのIDの非存在を使えば、排他制御が可能になる。

アイテム作成実行(情報:リソースID)
前提条件:リソースIDの非存在
もし前提条件が
満たされる場合:
リソースIDを主キーとするアイテムの作成成功
満たされない場合(既にリソースIDが存在する場合):
アイテムの作成失敗(エラー)

さらにアイテムにタイムアウト時刻を入れて、前提条件に時刻も盛り込めばタイムアウトが働くようになる。最終的にはこんな感じ。

アイテム作成実行(情報:リソースID、タイムアウト時刻)
前提条件:リソースIDの非存在 OR タイムアウト時刻 < 現在時刻
もし前提条件が
満たされる場合:
リソースIDを主キーとするアイテムの作成成功
満たされない場合(既にリソースIDが存在する場合):
アイテムの作成失敗(エラー)

なお expectedは Deprecated扱いの様で ConditionExpression が推奨らしい。

設定


設定の流れは次の通り。※細かいところは省く。

1. DynamoDB テーブル作成




2. Lambda Function 作成


先ほどの排他制御をJavaScriptで実装する。1つの関数でロック取得とロック解放に対応する。



ロック取得のところだけ抜粋するとこんな感じ。
dynamo.putItem({
                "TableName" : tableName,
                "Item" : {
                    "hash" : {"S" : hash},
                    "expirationTime" : {"N" : expirationTime.toString()},
                    // "created" : {"S" : now.toString()}
                },
                "ConditionExpression" : "expirationTime < :nowTime OR attribute_not_exists(#hash)",
                "ExpressionAttributeNames" : {
                    "#hash"  : "hash",
                },
                "ExpressionAttributeValues": {
                    ":nowTime": {"N": nowTime.toString()},
                },
            }, function(err, data) {
                  :

ConditionExpression の箇所がキモ。DynamoDBは予約語が多くて、それを回避するために #の別名を定義したり、プレースホルダとして :を定義したりと、慣れていないとハマる。他にも ”N"は数値だが、文字列で数値を渡さないと行けないとか...。

3. API Gateway作成


作成したLambda FunctionをAPI経由で呼び出せるようにする。GETでロック取得、DELETEでロック解放としてみた。

API Keyを有効にし、URLのクエリパラメータ "hash"を受け取るようにする。

hashパラメータの内容をJSON形式に変換して Lambdaへ渡す(Body Mapping Templatesで定義)。GET、DELETE共に同じ設定を行う。Lambda Functionではプログラム内で HTTP メソッドを元に処理を分岐させている。
設定が終わったらデプロイする。
デプロイ先の環境を複数作成できる。デフォルトでは prod(恐らくproductionの略)が選べる。

今回 API 認証を有効にしたので忘れずに API Keyを作成して、上記で作った API(と環境 prod)に紐付けておく。

API Gateway でデプロイするとAPIを利用するためのエンドポイント(URL)が環境ごとに用意される。このURLとAPI Key(文字列)を使ってアプリからこの APIを呼び出す。

- - - -
しばらく iOSアプリからこのネットワークロックを使ってみることにする。

おわり。



【アプリ】IconTimer 1.0 をリリースしました

2015年12月23日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加


新アプリ IconTimer をリリースしました。決めた時間をカウントダウンしてアラームを鳴らすだけのシンプルなタイマーアプリです。見た目にも音にもこだわりました。


特徴的なのは時間の選び方。
タップしたボタンの数字を足した合計が計測時間(分)になります。


以下は紹介動画。




時間・アイコン・色を自由にカスタマイズ。




以下は開発メモをつらつらと書きます。

ユーザインターフェイスめも


既存のタイマーアプリを使っていて毎回不満に感じていたのは、時間を選ぶ時に頭の中で「ひっかかり」があるところ。うまく説明できないのだけれど、あえて言えば時間を選ぶ時に注意を払わせるのが苦痛だった(どんだけものぐさと言われそうですが。。)
アプリを使うのがちっとも楽しくない。


例えば電卓式の時間を直接入力するタイプの場合、目的の数字を押してスタートするだけ....なんだけど打ち間違えたらクリアしないといけない。だから無意識のうちに間違えないように注意を払う。それがひっかかる。一方あらかじめ時間がリストで選べるタイプの場合、たくさんの選択肢から目的の時間を注意を払って探さなければならない。これもひっかかる。

そんな気を遣わずにポン・ポン・ポンと選ぶだけで時間が選べる UIはできないのか?それが開発の出発点だった。注意を払わ無くて良い UIって、きっと失敗しても直すのが苦にならないものだろう。試行錯誤がしやすいUI はどんなものか。





最初イメージとしてあったのは、少ないボタンを配置してそこから時間を選んでスタートするデザイン。どうせよく使う時間は決まっているだろうから、あらかじめ好きな時間を割り当てておけばボタンは少なくていいだろう。数が少ないのでひと目で把握できて判断もしやすそうだ。ただ問題なのはボタンが少ないと時間のパターンが物足りないこと。さらに使う前に時間を決めて設定するのはいちいち面倒。もしパターンを多くしようとボタンを増やせば選択方式と同じ罠に陥る。


そんなことを思いつつプロトタイプをひたすら弄ってた。ふとボタンを排他的に扱うではなく「足し算」にしたらどうかと思いつく。タップした時にONだった他のボタンをOFFにするのではなくそのまま残す。その残ったONボタンの数字の合計がタイマーの時間とする。逆にもう一度押すとOFFになり、その分タイマーの時間が減る。つまりタップのON・OFFがちょうど足し算・引き算の意味を持つことになる。これなら間違っても何度でもやり直せる。そして何よりもこの方法だとボタンの数以上の時間パターンを作り出すことができるのに気がついた。

でも、タップ数は他の方式に比べると増えてしまう。これって最初の問題に逆戻りしないだろうか?ところがプロトタイプで試してみると意外にもこれが楽しい。妙な話だけど、3分のタイマーを作るのに1分のボタンをわざわざ3回押したくなる。むしろタップが増えて非効率になってるし。。でもついつい目的の時間を作るのに今あるボタンだけで何を足し引きすれば良いのかと考えてしまう。そうか、タップのON・OFF行為がちょっとしたパズルを解く感覚なんだと気がついた。さらにアイコンを表示して音を鳴らすとこの傾向が顕著になった。意味もなくついついボタンを弄ってしまう。これで行くことに決めた。


開発情報

ツール:Xcode7 / swift 2.0
期間:1ヶ月
要員:1人

アイディアを元にプロトタイプから作り始めた。それを iPhoneに入れて暇があれば弄って改良を加える漸進的なアプローチ。プログラムもデザインも一人でやるのでお気楽。

アイコン




アイコンはいつも使っている icons8 のカラーアイコンを使用。自分の開発ではもはや定番。デザインが良くて種類も豊富だし、SVGもサポートしているなど使い勝手がいい。100pxまでのPNGはフリー。$149 でPNGのすべてのサイズと1年間の更新権。$249ならSVG / PDF / EPS / AI 形式のほかフォント/SVGの生成、カラーリング等々さまざまなオプションが付く。


効果音


500を超える効果音集 $35。aifとwavの2種類で提供される。bepp系から始まり、chime、click、dig、pad、pop、zap などなど。各系統でさらに多くのバリエーションが用意されている。これだけあると効果音を選ぶのも大変だが、それだけに GUIに合う音が鳴らせるとかなり気分がいい。

ライブラリ

Crashlytics, AdMob のみ。その他 UI / 機能に関しては自前。UIは色々なパターンを試作して捨てたものも多数。その分作るのは時間がかかったが楽しい作業でもあった。


多言語対応(アプリ)

開発当初から意識していた。アプリの性格上、文字での説明は要らないと割りきって文字の埋め込みをやめて、その分 UI を誰が見ても理解できて迷わないものにすることを心がけた。これによって翻訳の手間がほぼゼロとなった。一人開発でこれは嬉しい。対応言語はXcodeで選択可能なものをかたっぱしからローカライズした。なお唯一文字があるクレジットは英語表記のみとしてどの言語も統一した。

多言語対応(AppStore)

多言語といえばアプリとは別に AppStore向けの説明がある。こればっかりは翻訳をやらないわけには行かないので各言語用に文面を作った。アプローチは、
 (1) 無駄無く簡潔で短い最低限の文面を日本語で作成
 (2) それをGoogle翻訳の助けを借りて英語へ翻訳
 (3) Google Sheetを使って英語から他の言語へ一気に翻訳

多言語展開の元を英語にしたのは日本語起点よりも精度が良さそうだから。

Google Sheet(Google Driveで提供されるオンラインのスプレッドシートアプリ)にはセルの文字列を Google翻訳サービスへ渡す関数が用意されていて、これを使うと大量の文字列を一気に翻訳することができる。こんな感じ。

あらかじめ言語コード(jaとかen)の列を用意しておき、それを関数へ渡して翻訳をかける。

日本語を含む23言語の翻訳がこの方法であっという間にできた。Googleすごい。


Apple審査

12/04 Ver1.0を申請

12/12 リジェクト:1.0 メタデータでの却下
AppStore用のアプリ名に長い説明を入れたのがまずかったらしい。説明はやめて IconTimer だけにして再申請。バイナリは更新せず。

12/18 リジェクト:1.0 バイナリでの却下
設定画面に分を増減させる+−のボタンがあるのだが、それを押している時にクラッシュするのでダメというもの。自分の手持ちの iPhoneやiPadやシミュレータを総動員して試すも再現しない。再現しない上に、メイン機能ならともかく設定の一部の機能のクラッシュだけで落とすな=3、となんだか頭に来て「再現しない」「もっと詳しい情報を出して」的なコメントを送る。

が、その後繰り返しボタンを押し続けるとクラッシュが再現。簡単には起きないが、確かに使っていると誰でもいつかはクラッシュに遭遇しそう。クラッシュログはこんな感じ。

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x00000000155d3f40
Triggered by Thread:  0

Oh...わからん。


その後、Xcodeに繋いだiPhoneで問題を再現させるも AppDelegateで停止して上記例外が出てるだけ。調査を重ねるもさっぱりわからない。仕方なくボタンを連打しつつ(それが唯一の条件なので)、一つ一つコードから行を取り除いてクラッシュに関係のある箇所を絞り込んでいった。泣きが入りそうになったころ、ボタンを押した時に鳴らす音の処理で落ちていることが判明。音を鳴らすために Soundというクラスを作り、AVAudioPlayerのインスタンスをクラス変数で管理していたのだが、どうもそこがクラッシュの原因ぽい。こんな感じ。
class Sound: NSObject {

    static var player:AVAudioPlayer?
    static func play(...) {
        
        if let sound = NSDataAsset(name: name) {
            do {
                 :
                try player = AVAudioPlayer(data: sound.data, fileTypeHint: fileTypeHint)

※元ネタはネット上のブログから拾ってきた。いま手元に情報が無いので略

ボタンが続けて押された場合、前の発音が終わらない内にこの play()メソッドが呼び出され、新しい AVAudioPlayerのインスタンスが作られる。この時、直前の発音で使っていた AVAudioPlayer は音が鳴っている最中に参照が切れて廃棄対象になってしまうケースがあるようだ。そして音が鳴り終わった時にこの廃棄対象のインスタンスで後処理を行おうとしてクラッシュ(たぶん)。そこで次の音を鳴らす時には直前の発音を明示的に停止する処理を入れた。
class Sound: NSObject {

    static var player:AVAudioPlayer?
    static func play(...) {
        
        if let _ = player {
            player?.stop()
        }
        if let sound = NSDataAsset(name: name) {
            do {
                 :
                try player = AVAudioPlayer(data: sound.data, fileTypeHint: fileTypeHint)

if文いらないかも。ともかくこれでクラッシュがピタッと無くなった。速攻でバイナリをアップロードして再申請。頭に来て送ったコメントを思い出すとなんだかバツが悪かったので、クラッシュの指摘の(最大限の)お礼と、ついでに急いで審査お願いと付け足しておく。その後なぜか expedited review はできないよ、という連絡メールが来る(expedited reviewは依頼していない)。

12/22 審査通過
日本時間で 0時ごろレビュー開始、6時すぎに通過の連絡。これは嬉しかった。


(おわり)






Asset Catalog にサウンドファイルをおく

2015年11月17日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Asset Catalogには画像以外のデータも置ける。サウンドファイル(.aif)を置いてみた。


取り出すには NSDataAsset を使う。
let sound = NSDataAsset(name: name)
// use sound.data

取り出したサウンドファイルを鳴らす。
import UIKit
import AVFoundation

class Sound: NSObject {

    static var player:AVAudioPlayer?
    static func play(name:String) {
        if let sound = NSDataAsset(name: name) {
            do {
                try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
                try! AVAudioSession.sharedInstance().setActive(true)
                try player = AVAudioPlayer(data: sound.data, fileTypeHint: "aif")
                player?.volume = 0.3
                player?.play()
            } catch {
                print("error")
            }
        }
    }
}


上記は下記サイトのコードをクラスメソッドに置き換えただけ。サウンドファイルをアセットに置くのもここで知った。
Accessing audio (and other) files stored in the Xcode asset catalog with Swift

[Autolayout] サンプルケース - 親ビューの下半分に子ビューを配置 (2) アニメーション

2015年6月12日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

前回のやつにアニメーションを加えてみる。子ビューが下からシュッと出てくるやつ。

これは制約を使えば簡単。子ビューの上のラインを画面下端から、最終位置(この場合親ビューのCenterY)まで引き上げてやれば良い。
まず子ビューの初期位置を最終下端で作る。
let v1c = NSLayoutConstraint(item: tableView, attribute: .Top, relatedBy: .Equal,
     toItem: parentView, attribute: .CenterY, multiplier: 1.0, constant: parentView.frame.size.height)

一旦 parentView.layoutIfNeeded()で初期描画させた後、アニメーションのコードを走らせる。
v1c.constant = 0.0
        UIView.animateWithDuration(0.5) {
            parentView.layoutIfNeeded()
        }

こうすると子ビューの上の制約の値(constraint)が下端(parentView.frame.size.height)から 0.0へアニメーションしながら変化する。

なおこのままだと制約のワーニングが出る。
2015-06-11 08:10:18.938 LKMenu_Example[89282:6908924] Unable to simultaneously satisfy constraints.
 Probably at least one of the constraints in the following list is one you don't want.
 Try this: (1) look at each constraint and try to figure out which you don't expect; 
(2) find the code that added the unwanted constraint or constraints and fix it.
 (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer
 to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "",
    "",
    ""
)
  :

これは子ビューの上の制約(v1c)と下の制約(v2c)の値が初期状態では等しくなる(逆転する?)ため。こんな時は優先順位(priority)をつけてやればいい。
let v2c = NSLayoutConstraint(item: tableView, attribute: .Bottom, relatedBy: .Equal,
      toItem: parentView, attribute: .Bottom, multiplier: 1.0, constant: 0.0)
        v2c.priority = 750   // 追加
        parentView.addConstraint(v2c)
親ビューに制約を追加する前につけるのを忘れずに。これでワーニングが消える。priorityは大きい方が優先順位が高い。IBでプロパティ欄を見ると 1000(Required), 750(High), 250(Low)となっている。
デフォルト値は 1000(最大値も 1000)。


最終的なコード。
parentView.addSubview(tableView)

        tableView.setTranslatesAutoresizingMaskIntoConstraints(false)
        
        let h1c = NSLayoutConstraint(item: tableView, attribute: .Left, relatedBy: .Equal,
            toItem: parentView, attribute: .Left, multiplier: 1.0, constant: 0.0)
        parentView.addConstraint(h1c)
        let h2c = NSLayoutConstraint(item: tableView, attribute: .Right, relatedBy: .Equal,
            toItem: parentView, attribute: .Right, multiplier: 1.0, constant: 0.0)
        parentView.addConstraint(h2c)
        
        let v1c = NSLayoutConstraint(item: tableView, attribute: .Top, relatedBy: .Equal,
            toItem: parentView, attribute: .CenterY, multiplier: 1.0, constant: parentView.frame.size.height)
        parentView.addConstraint(v1c)
        let v2c = NSLayoutConstraint(item: tableView, attribute: .Bottom, relatedBy: .Equal,
            toItem: parentView, attribute: .Bottom, multiplier: 1.0, constant: 0.0)
        v2c.priority = 750
        parentView.addConstraint(v2c)
        parentView.layoutIfNeeded()
        
        v1c.constant = 0.0
        UIView.animateWithDuration(0.5) {
            parentView.layoutIfNeeded()
        }

Autolayoutはコツがわかってくると楽しい。


(おまけ)animateWithDuration:usingSpringWithDamping: を使えばアニメーションにバネ効果(バウンス効果っぽい)をつけられる。
UIView.animateWithDuration(0.5, delay: 0.0, usingSpringWithDamping: 0.7,
            initialSpringVelocity: 0.0, options: UIViewAnimationOptions.CurveEaseInOut,
            animations: { () -> Void in
            parentView.layoutIfNeeded()
        }) { (Bool) -> Void in
        }

[Autolayout] サンプルケース - 親ビューの下半分に子ビューを配置

2015年6月11日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

メニュー作成中。Autolayoutでこんな感じのビュー配置をしたい。
ランドスケープで。

親ビューが parentView、子ビューが tableViewとするとこんな感じ。
        parentView.addSubview(tableView)

        tableView.setTranslatesAutoresizingMaskIntoConstraints(false)
        
        let h1c = NSLayoutConstraint(item: tableView, attribute: .Left, relatedBy: .Equal, toItem: parentView, attribute: .Left, multiplier: 1.0, constant: 0.0)
        parentView.addConstraint(h1c)
        let h2c = NSLayoutConstraint(item: tableView, attribute: .Right, relatedBy: .Equal, toItem: parentView, attribute: .Right, multiplier: 1.0, constant: 0.0)
        parentView.addConstraint(h2c)
        
        let v1c = NSLayoutConstraint(item: tableView, attribute: .Top, relatedBy: .Equal, toItem: parentView, attribute: .CenterY, multiplier: 1.0, constant: 0.0)
        parentView.addConstraint(v1c)
        let v2c = NSLayoutConstraint(item: tableView, attribute: .Bottom, relatedBy: .Equal, toItem: parentView, attribute: .Bottom, multiplier: 1.0, constant: 0.0)
        parentView.addConstraint(v2c)

        parentView.layoutIfNeeded()

ポイントは v1c のところで、親のCenterYに 子ビューのTOPを紐付けているところ。ここの constantを変更すれば高さを調整することもできる。


[CocoaPods] use_frameworks! で作ったライブラリのバンドル

2015年6月1日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

CocoaPodsで自作ライブラリを作っているケースで、swift対応のためにフレームワーク化(use_framework!)するとバンドルはその中に入ってしまう。

Example.app
 ├── LKPostingQueueManager.framework
 │   ├── Info.plist
 │   ├── LKPostingQueueManager
 │   └── Resources.bundle ←ここにくる(以前はもっと上の階層)


プログラムでこれを参照するにはメインバンドル配下では見つからない。
let path = NSBundle.mainBundle().pathForResource("Resource", ofType: "bundle")! → ここで nilでクラッシュ
let bundle = NSBundle(path: path)

最初にフレームワークのバンドル(クラスが含まれるバンドル)を取って、そこから引き出すのがポイント。
let frameworkBundle = NSBundle(forClass: LKPostingQueueManager.self)
let path = frameworkBundle.pathForResource("Resources", ofType: "bundle")!
let bundle = NSBundle(path: path)


関連

Resource Bundle の作り方と CocoaPodsでの配布
※上記はフレームワーク化する前の話。フレークワーク化するとこんな面倒は入らず設定だけで済む。
s.resource_bundles = { 
    'Resources' => ['Pod/Assets/*']
  }
みたいな。

【アプリ】StackOne 1.6.3 アイコンが変わりました

2015年5月15日金曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

StackOneの最新バージョン 1.6.3をリリースしました。このバージョンからアイコンが変わっています。


象がいなくなり重なりの模様だけのシンプルなものに変えました。

これは某マネー系アプリからクレームがあったためです(英語)。確かにそれとは似ていて先方からは、自分たちが先に出したしメジャーだからそっちを変えてほしい、みたいな文面が来た。あと法的な対処も匂わせたりもしていました。これが1番目の理由です。

2番目の理由は、その件とは無関係に最近の私のエバーノート系アプリはどれも象が入っていないものになっているので、それに合わせたというのもあります。以前から漠然と変えようと思ってたところにクレームが来たのがきっかけになったとも言えます。

アイコンは別のデザインも色々と考えたのですが、しっくりくるものがなかなかできず。最終的には以前の重なりの部分だけを残すシンプルなものに落ち着きました。
象が好きだったという方もいらっしゃったようでその方には誠に申し訳ないです。改良は続けていく予定なのでユーザの方々には今後ともどうぞよろしくお願いいたします。

では。


人気の投稿(過去 30日間)