てくてくテック

気ままに開発のメモを書いていこうと思います。主にSwiftかと。

Reporterを使ってiTunes ConnectのSales and Trendsを自動で取得する - 設計編

はじめに

Reporterを使ってiTunes ConnectのSales and Trendsを自動で取得するReporterを使ってみてAppleが提供するレポートファイルのダウンロードするところまでお話をしました。今回は、ダウンロードするだけでなく数値データを取得してどのように自動化するかという全体の設計についてまとめようと思います。

ゴール

今回作成する自動化機構の要件は下記の通りです。rubyなのは少し触ったことがあるからです。

  • デイリーのインストール数(アップデート数などは除く)を取得
  • 同一vendorに限定
  • iPhone, iPad両方の値を混合
  • 取得した値はGoogle Spreadsheetに出力
  • データ収集するアプリとその出力先は別途Google Spreadsheetで管理
  • デイリーでGoogle SpreadsheetからSlackにインストール数と合計インストール数を通知
  • ruby(2.3.1, rbenvにより管理)で実装

まとめると下図のようになります。

f:id:k-s-9190118:20161102110156p:plain

実装指針

Google SpreadsheetとRubyの連携

こちらはgoogel_driveというgemを使うと非常に簡単にできました。詳細は次回以降に書きます。

daily report

iTunes Connectから取得

これはjavaを実行することによってファイルをダウンロードします。この時、Appleアカウント IDやパスワードを記述したReporter.propertiesという設定ファイルが必要になるのですが、これらをファイルにベタ書きしておいておきたくないので、環境変数あたりに入れておいて実行時に一時的に作成&終わったら削除という実装にしようと思います。

インストール数の取得

reportファイルはtsv形式で出力されます。 データ項目は、 Provider, Provider Country, SKU, Title ...etcなど多岐にわたりますが、今回はSKUとProduct Type Identifier, Unitsを利用します。なお、SKUはiTunes Connectにアプリを作成するときに入力したものでBundle Identifierと同一にすることが多いので同一のものとしています。Product Type Identifierは製品タイプIDにあるように初回インストールなのかアップデートなのかの判断に利用できます(もちろんいわゆる製品タイプという区別もあります)。そしてUnitsがインストール数のことになります。

reportファイルの削除

めんどくさいことにapiでjsonを返してくれるとかいう設計じゃないreportファイルを毎日ダウンロードすることになるので、そのままだと不要なファイルがたまり続けることになります。そのため、プログラム実行後にはファイルを削除することにします。

Slackに通知する

SpreadsheetなのでGASを利用します。詳細はGASからIncoming Webhooksでslackに通知を投げるを参照してください。

終わりに

今回は設計や指針まで書きました。次回以降にそれぞれ細かい実装の話をしようと思います。

Swift3でのclosure

はじめに

Swift3にした時にclousureの部分の書き方が変わっていたなーというメモです

変更点

非同期の時は@escapingをつける

Swift2まではデフォルトが@escapingの状態で@noescapeにする際に明示的に@noescapeにするという文法だったのに、Swift3から逆転しました。 @esapingにするとどうなのか?という話はSwift 3 の @escaping とは何かがわかりやすいので、こちらを参照していただけたら良いと思います。

第一引数の前に _をつける

どうもclosureの第一引数の前に_をつけないといけないみたいです。
これはメソッドの第一引数にラベルを書くという文法の変更に近いお話です。この文法では従来のように第一引数にラベルをつけない場合は_をつけることになってます。Establish consistent label behavior across all parameters including first labelsの話です。Swift2では***With???みたいに第一引数のラベルのようなものをWithの後ろにくっつけるような書き方になっていたのを***(???)という感じにすっきりと書けるようになりました。 例えばSwift2では

func fileWithName(name: String){

}

こうだったのに対しSwift3ではこうなります。

func file(name: Strint){

}

//Swift2式で書く場合
func fileWithName(_ name: String){

}

メソッドの文法で第一引数にラベルをつけない場合は_をつけるのはわかったのですが、ではなぜclosureの第一引数では_をつけなければならないのでしょうか?
How do you document the parameters of a function's closure parameter in Swift 3?のstack over flowの回答のなかに

As @Arnaud pointed out, you can use _ to prevent having to use the parameter label when calling the closure:

In fact, this is the only valid approach in Swift 3 because parameter labels are no longer part of the type system (see SE-0111).

とありどうもRemove type system significance of function argument labelsが原因のようです。ここはまだちゃんと解読できていないのでそのうち読んだら追記します。

NavigationbarをHiddenじゃなくて透明にする

はじめに

NavigationbarをHiddenにして消すのではなく、タイトルやボタンは表示するがNavigationbarはあくまで透明としたい時があります(下図参照)。単純にBar TintをclearColorに変更するとなぜか黒くなります。今回は、下図のようにNavigationbarを透明にする方法について書きます。

https://gyazo.com/df36cbd4bb0c7186599a0fe51500e495

コード

透明にする時

self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()

戻す時

self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
self.navigationController?.navigationBar.shadowImage = nil

UIViewControllerのExtensionに入れておくといいかもですね。

Reporterを使ってiTunes ConnectのSales and Trendsを自動で取得する

はじめに

iOSアプリのインストール数はiTunes ConnectのWebページに行ってデータを見るかエクスポートすれば確認することができます。 
しかし、いちいちWebページにアクセスして確認するのは面倒なので、プログラムで自動化しちゃおうというお話です。今回はとりあえずツールを使ってみるところまで書きます。

Autoingestion toolからReporterに

2016年9月末にAppleからAutoingesion tool使えなくなるよ。代わりにReporte使ってねってメールが来てました。今はまだ、iTunes ConnectのSales and Trends関係の自動化を調べるとAutoingesion Toolの方が出るかもしれません(こことか)が、基本はそんなに変わっていないかと

とりあえず使ってみる

Reporteでは大まかにSales and TrendsとPayments and Financial Reportsの二つが取得できるみたいですが,今回はSales and Trendsについてお話します。 なお、ドキュメントはAbout Sales and Trends Commandにあります。

概要

Reporterを使う流れは大まかに下記のようになっています。

  1. download ReporterでReporterをダウンロードする
  2. zipを解凍する.(Reporter.jarとReporter.propertiesがある)
  3. Reporter.propertiesにアカウント情報を書く(UserIdとPassword)
  4. javaを実行してvendor numberを取得する
  5. javaを実行してreportをダウンロードする
  6. ダウンロードしたファイルを解析する(今回はやらない)

次節以降で4と5について説明します。

vendor numberを取得する

reportを取得するjavaプログラムを実行するらしにvendor numberが必要になるので、それを取得します。ドキュメントにあるように下記のコマンドを実行すればわかります。

$ java -jar Reporter.jar p=Reporter.properties m=Robot.XML Sales.getVendors

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Vendors>
    <Vendor>80012345</Vendor>
    <Vendor>80067890</Vendor>
</Vendors>

reportをダウンロードする

これもドキュメント通りです。

$ java -jar Reporter.jar p=Reporter.properties m=Robot.XML Sales.getReport 80012345, Sales, Summary, Daily, 20150201

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Output>
    <Message>Successfully downloaded S_D_80012345_20150201.txt.gz.</Message>
</Output>

これでS_D.txt.gzというファイルがダウンロードされます。解凍するとS_D.txtというテキストファイルが生成されますが、中身はタブ\t区切りのtsvファイルです。またこれはWebページからエクスポートしたのと同じです。
正直jsonとかで返して欲しいですね...

Swift3 jsonをローカルから読み込む

はじめに

apiと実際につなぐ前にダミーデータで実装するときなどに
jsonファイルをローカルに保存しておいてそこから読み込む ということをしたくなった時のお話です

利用するライブラリ

SwiftyJsonを使ってます。 2016年10月26日の時点では本家でもSwift3対応されているようですが 私がSwift3対応をした9月末時点では対応していなかったため こちらを利用しました。

コード

func loadJson(_ fileName : String) -> JSON? {
  let path = Bundle.main.path(forResource: fileName, ofType: "json")
  do{
    //https://www.hackingwithswift.com/example-code/strings/how-to-load-a-string-from-a-file-in-your-bundle
    let jsonStr = try String(contentsOfFile: path!)
    let json =  JSON.parse(jsonStr)
    return json
  } catch{
    return nil
  }
}

やっていることは、ファイルを文字列として読み込んで
その文字列をjsonにパースしているだけです。

Swift3(というかXcode8)にしたらPush通知が動かなかった

はじめに

Swift3にあげたら(というより多分Xcode8にしたら)Push通知が動かなかった時のお話です。

状況

Xcode7で作っていた時はPush通知は動いていたのに、 Xcode8にあげたらPush通知が届かなくなりました。 調べてみるとDevice Tokenが取得できていないようでした。

原因

CapabilitiesのPush NotificationsがONになっていなかった。 f:id:k-s-9190118:20161020105425p:plain

正直昔はどうだったか覚えてないのですが,hogehoge.xcodeproj/project.pbxprojのhistoryを見た感じだとそういう設定はしていなかったので、 Xcode7まではその設定がなくても動いたのかなーという感じです。

Alamofire(非同期通信)をsemaphoreで同期的に処理できなかった

はじめに

Alamofireでapi通信する際に、同期的に処理したくなったためSemaphoreを使ってやってみたけどうまくいかなかった時のお話です。

Semaphoreとは

Semaphoreは排他制御の仕組みのことで、リソースへのアクセス可能な数のことです。Semaphoreが0の状態では他の者はリソースにアクセスできないのが"排他的"ということかなと勝手に解釈してます。
この機能を用いることで、非同期処理が終わるのを待って同期的に実行することが可能になります。 詳細はリンク先を参照してください。

wa3.i-3-i.info

Swift3でのSemaphore

iOSでSemaphoreを使う場合はGCDのDispatchSemaphoreを使います。GCDについてはSwift3のGCD(dispatch_async) を参考にしてください。
Swift3でSemaphoreを使うサンプルは、下記のようになります。

let semaphore:DispatchSemaphore = DispatchSemaphore(value: 0)
Alamofire.request(url, method: .get, parameters: parameters, headers: headers)
   .validate()
   .responseJSON { response in
      semaphore.signal()
}
semaphore.wait()

しかし、このサンプルではアプリが止まってしまいます。なぜなら

.responseJSONのブロック内と、その外側が同じスレッドで処理されるためらしいです。 つまり、dispatch_semaphore_waitで待機してしまうと、.responseJSONのブロック内も処理が止まってしまい、いつまで経ってもロック解除ができずにデッドロック・・・という事みたいです。

Alamofireでデータ受信が終わるまで待機する方法 より

解決策

Alamofireでデータ受信が終わるまで待機する方法 のように実装します。(あまりすっきりしませんが)

var keepAlive = true

//ロックが解除されるまで待つ
let runLoop = RunLoop.current
Alamofire.request(url, method: .get, parameters: parameters, headers: headers)
   .validate()
   .responseJSON { response in
      semaphore.signal()
}
while keepAlive &&
   runLoop.run(mode: RunLoopMode.defaultRunLoopMode, before: NSDate(timeIntervalSinceNow: 0.1) as Date) {
   // 0.1秒毎の処理なので、処理が止まらない
}

ちなみにAlamofireの部分をDIspatchQue.globalとかで囲ってみたりしてみましたがダメでした。