しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

【fastlane】1プロジェクトに複数アプリの入っている場合のメタデータ管理

1つのプロジェクトで複数アプリをリリースしている場合のメタデータ管理方法です。
複数アプリが全く同じメタデータならいいのですが、アイコンやスクリーンショットを別々にする場合少し工夫をする必要があります。

fastlaneの設定方法はこちらをご参照下さい。

www.cl9.info

複数アプリ対応

まずは1アプリに対応します。
プロジェクトのルートで以下コマンドを実行します。

fastlane init

アプリのメタデータが以下のような構成でダウンロードされます。

f:id:llcc:20161225203212p:plain

2つ目以降のアプリは以下のように別フォルダを指定してダウンロードします。

fastlane deliver download_screenshots --app_identifier com.example.other  --screenshots_path fastlane/screenshots_other_app
fastlane deliver download_metadata --app_identifier com.example.other  --metadata_path fastlane/metadata_other_app

これで別アプリのメタデータがフォルダに保存されます。

f:id:llcc:20161225203757p:plain

別アプリへのメタデータのアップロードは以下のようにオプション付ける事で実現できます。

fastlane deliver --app_identifier com.example.other  --screenshots_path fastlane/screenshots_other_app --metadata_path fastlane/metadata_other_app

上コマンドはFastfileに登録しておけば簡単に実行できるようになります。

fastlane update_metadata_other_app 
platform :ios do
  lane :update_metadata_other_app do
    deliver(
      app_identifier: 'com.example',
      screenshots_path: 'fastlane/screenshots_other_app',
      metadata_path: 'fastlane/metadata_other_app',
    )
  end
end

InAppPurchase用ライブラリ、RMStoreを使ってみる

RMStoreというiPhoneのアプリ内課金を楽にしてくれるライブラリを使ってみました。

github.com

インストール

インストールはcocoapodsを使います。

pod 'RMStore'

使い方

商品情報は以下のように取得します。
引数にproductIdを渡せばSKProductの配列が返ってきます。

let productIds = ["com.example.item"]
RMStore.default().requestProducts(productIds, success: { products, invalidIdentifiers in
    self?.products = products as? [SKProduct]
}, failure: { error in
})

購入処理はaddPaymentメソッドを使います。
レシートチェックなどは引数で渡されるSKPaymentTransactionクラスのインスタンスを使います。

RMStore.default().addPayment("com.example.item", success: { transaction in
        if transaction?.transactionState != .purchased {
            return
        }
    }, failure: { transaction, error in
})

リストア処理は以下の通りです。
購入履歴が引数として渡されるので、それを使って購入したかの判定をします。

RMStore.default().restoreTransactions(onSuccess: { [weak self] transactions in
        if let transactions = (transactions as? [SKPaymentTransaction]), transactions.map({ $0.payment.productIdentifier }).contains("com.example.com") {
            print("商品情報がありました")
        } else {
            print("商品情報が見つかりませんでした")
        }
    }, failure: { error in
})

今回は試していないのですが、コンテンツダウンロードも対応しているようです。

【iOS】iTunesConnectのSalesデータ取得プログラムを動かす【新方式に対応】

iTunesConnectの売上やダウンロード数などのデータを取得するプログラムに関する話です。

今まではiTunesConnectのSalesデータをAutoIngestion Toolというものを使って取得できました。
しかしこのツールは非推奨になり、2016年12月で停止してしました。
AppleからはReporterというツールを使うよう通知があったので今回はそれを試してみました。

ダウンロード

ダウンロードは下ページのSetupにあるダウンロードリンクから行います。

Reporter User Guide

ダウンロードすると、Reporter.jarReporter.propertiesの2つのファイルが入っています。
Reporter.jarが実際にデータを取得するプログラムで、Reporter.propertiesはユーザー情報を書くファイルです。

f:id:llcc:20161223191603p:plain

Salesデータの取得方法

まずは認証情報を記入します。
Reporter.propertiesを開くと以下のようになっているので、userIDとpasswordを埋めます。

userID = 
password = 

Mode=Robot.xml

SalesUrl=https://reportingitc-reporter.apple.com/reportservice/sales/v1
FinanceUrl=https://reportingitc-reporter.apple.com/reportservice/finance/v1

入力が終わったらReporter.jarを使ってSalesデータを取得します。

java -jar Reporter.jar p=Reporter.properties Sales.getReport [vendor id], Sales, Summary, Daily, 20161201

vendor idは以下コマンドで取得する事ができます。

java -jar Reporter.jar p=[properties file] Sales.getVendors

コマンドは以下のようなXML返します。

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

xxxxxxx.txt.gzファイルには各アプリのダウンロード数などのデータが入っています。
形式は以下のように1行1アプリ、各要素タブ区切りになります。

Provider Provider Country    SKU Developer   Title   Version Product Type Identifier Units   Developer Proceeds  Begin Date  End Date    Customer Currency   Country Code    Currency of Proceeds    Apple Identifier    Customer Price  Promo Code  Parent Identifier   Subscription    Period  Category    CMB Device  Supported Platforms Proceeds Reason Preserved Pricing   Client
APPLE   US  com.example developername   appname 1.0 0   0   0   12/16/2016  12/16/2016  JPY JP  JPY 123456789   0                   Business        iPhone  iOS          

fastlaneで複数の国のメタデータを一括更新

アプリの申請や証明書周りを自動化するfastlaneを試してみました。

インストール

インストールはgemを利用します。

gem install fastlane

fastlaneの初期化

インストールが終わったらfastlaneの初期化を行います。
プロジェクトのフォルダで以下コマンドを実行します。

fastlane init

そうするとAppleIDを聞かれるので答えます。

f:id:llcc:20161222222647p:plain

今回は既にあるアプリに適用したので、iTunesconnectからメタデータをダウンロードしています。

f:id:llcc:20161222222832p:plain

ダウンロードしたファイルはfastlaneフォルダ以下に入ります。

f:id:llcc:20161222223028p:plain

fastlaneでメタデータをアップロード

fastlaneを使ったメタデータの更新を試してみます。
fastlane/metadata/en-USフォルダのrelease_notes.txtを更新します。

f:id:llcc:20161222225222p:plain

次にプロジェクトのルートフォルダで以下コマンドを打ちます。

fastlane deliver

すると以下のようにアップロードを開始してくれます。

f:id:llcc:20161222225417p:plain

iTunesconnectにアクセスすると先程更新したReleaseNoteが反映されている事が分かります。
ファイルを用意するだけでメタデータの更新ができるので、複数国対応しているアプリのリリースも非常に楽になりそうです。

f:id:llcc:20161222230036p:plain

【Swift】Chartsを使って色々なグラフを描画してみる

Chartsというライブラリを試して色々なグラフを使ってみました。

github.com

インストール

CocoaPodsでインストールしました。

target 'MyApp' do
  use_frameworks!

  pod "Charts"
end

棒グラフ

import UIKit
import Charts

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var rect = view.bounds
        rect.origin.y += 20
        rect.size.height -= 20
        let barChartView = BarChartView(frame: rect)
        let entry = [
            BarChartDataEntry(x: 10, y: 30),
            BarChartDataEntry(x: 20, y: 20),
            BarChartDataEntry(x: 30, y: 40),
            BarChartDataEntry(x: 40, y: 10),
            BarChartDataEntry(x: 50, y: 30)
        ]
        let set = [
            BarChartDataSet(values: entry, label: "Data")
        ]
        barChartView.data = BarChartData(dataSets: set)
        view.addSubview(barChartView)
    }
}

f:id:llcc:20161219231209p:plain

バブルチャート

import UIKit
import Charts

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var rect = view.bounds
        rect.origin.y += 20
        rect.size.height -= 20
        let chartView = BubbleChartView(frame: rect)
        let entries = [
            BubbleChartDataEntry(x: 1, y: 10, size: 30),
            BubbleChartDataEntry(x: 2, y: 60, size: 10),
            BubbleChartDataEntry(x: 3, y: 20, size: 20),
            BubbleChartDataEntry(x: 4, y: 40, size: 30),
            BubbleChartDataEntry(x: 5, y: 30, size: 40),
            BubbleChartDataEntry(x: 6, y: 10, size: 20)
        ]
        let set = BubbleChartDataSet(values: entries)
        chartView.data = BubbleChartData(dataSet: set)
        view.addSubview(chartView)
    }
}

f:id:llcc:20161219231744p:plain

ロウソク足

import UIKit
import Charts

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var rect = view.bounds
        rect.origin.y += 20
        rect.size.height -= 20
        let chartView = CandleStickChartView(frame: rect)
        let entries = [
            CandleChartDataEntry(x: 1, shadowH: 12, shadowL: 10, open: 11, close: 11),
            CandleChartDataEntry(x: 2, shadowH: 20, shadowL: 15, open: 18, close: 17),
            CandleChartDataEntry(x: 3, shadowH: 30, shadowL: 24, open: 26, close: 28),
        ]
        let set = CandleChartDataSet(values: entries, label: "Test")
        chartView.data = CandleChartData(dataSet: set)
        view.addSubview(chartView)
    }
}

f:id:llcc:20161219232244p:plain

横棒グラフ

データは棒グラフ同様、BarChartDataを使います。

import UIKit
import Charts

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var rect = view.bounds
        rect.origin.y += 20
        rect.size.height -= 20
        let chartView = HorizontalBarChartView(frame: rect)
        let entry = [
            BarChartDataEntry(x: 1, y: 30),
            BarChartDataEntry(x: 2, y: 20),
            BarChartDataEntry(x: 3, y: 40),
            BarChartDataEntry(x: 4, y: 10),
            BarChartDataEntry(x: 5, y: 30)
        ]
        let set = BarChartDataSet(values: entry, label: "Data")
        chartView.data = BarChartData(dataSet: set)
        view.addSubview(chartView)
    }
}

f:id:llcc:20161219232913p:plain

折れ線グラフ

import UIKit
import Charts

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var rect = view.bounds
        rect.origin.y += 20
        rect.size.height -= 20
        let chartView = LineChartView(frame: rect)
        let entries = [
            BarChartDataEntry(x: 1, y: 30),
            BarChartDataEntry(x: 2, y: 20),
            BarChartDataEntry(x: 3, y: 40),
            BarChartDataEntry(x: 4, y: 10),
            BarChartDataEntry(x: 5, y: 30)
        ]
        let set = LineChartDataSet(values: entries, label: "Data")
        chartView.data = LineChartData(dataSet: set)
        view.addSubview(chartView)
    }
}

f:id:llcc:20161219233138p:plain

円グラフ

import UIKit
import Charts

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var rect = view.bounds
        rect.origin.y += 20
        rect.size.height -= 20
        let chartView = PieChartView(frame: rect)
        let entries = [
            PieChartDataEntry(value: 10, label: "A"),
            PieChartDataEntry(value: 20, label: "B"),
            PieChartDataEntry(value: 30, label: "C"),
            PieChartDataEntry(value: 40, label: "D"),
            PieChartDataEntry(value: 50, label: "E")
        ]
        let set = PieChartDataSet(values: entries, label: "Data")
        chartView.data = PieChartData(dataSet: set)
        view.addSubview(chartView)
    }
}

f:id:llcc:20161219233402p:plain

レーダーチャート

import UIKit
import Charts

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var rect = view.bounds
        rect.origin.y += 20
        rect.size.height -= 20
        let chartView = RadarChartView(frame: rect)
        let entries = [
            RadarChartDataEntry(value: 40),
            RadarChartDataEntry(value: 30),
            RadarChartDataEntry(value: 20),
            RadarChartDataEntry(value: 40),
        ]
        let set = RadarChartDataSet(values: entries, label: "Data")
        chartView.data = RadarChartData(dataSet: set)
        view.addSubview(chartView)
    }
}

f:id:llcc:20161219233623p:plain

点グラフ

import UIKit
import Charts

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var rect = view.bounds
        rect.origin.y += 20
        rect.size.height -= 20
        let chartView = ScatterChartView(frame: rect)
        let entries = [
            BarChartDataEntry(x: 1, y: 10),
            BarChartDataEntry(x: 2, y: 15),
            BarChartDataEntry(x: 3, y: 9),
            BarChartDataEntry(x: 4, y: 13),
        ]
        
        let set = ScatterChartDataSet(values: entries, label: "Data")
        chartView.data = ScatterChartData(dataSet: set)
        view.addSubview(chartView)
    }
}

f:id:llcc:20161219233942p:plain

まとめ

今回は簡単に一通りのグラフに触れてみました。
どれも同じようなインターフェースな為、一つ実装すれば他のものも同じように作れそうです。

【iOS】指紋認証を使ってみる

iPhone5sから登場した指紋認証ですが、まだちゃんと使った事がなかったので試してみました。

指紋認証可能化どうかはLAContextクラスのcanEvaluatePolicyメソッドを使います。 LAContextを使うためにはLocalAuthenticationをimportする必要があります。

import LocalAuthentication

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let context = LAContext()
        var error: NSError?
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            // 指紋認証可能
        }
    }
}

認証はevaluatePolicyメソッドを使います。
引数には認証画面での説明テキストも入れます。

import LocalAuthentication

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let context = LAContext()
        var error: NSError?
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "パスコードロックに使う") { success, error in
                
            }
        }
    }
}

起動すると以下のようにパスコードを求められる画面になります。

f:id:llcc:20161218114101p:plain

.deviceOwnerAuthenticationWithBiometricsでなく.deviceOwnerAuthenticationを使った場合

シミュレータで試したところ以下のようにパスコードを求められました。

f:id:llcc:20161218114448p:plain

【iOS】Smile-Lockでパスコードロック機能を実現する

Smile-Lockというライブラリを試してみました。
URL見るとリクルートライフスタイルで出しているライブラリなんですね。

github.com

インストール

CocoaPodsでインストールしました。

target 'MyApp' do
  use_frameworks!

  pod 'SmileLock'
end

使い方

下のようにWindowやViewに貼り付ける事で利用します。

import UIKit
import SmileLock

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let kPasswordDigit = 6
        let contentView = PasswordContainerView.createWithDigit(kPasswordDigit)
        if let window = view.window {
            let baseView = UIView(frame: window.bounds)
            baseView.addSubview(contentView)
            contentView.center = baseView.center
            window.addSubview(baseView)
        }
    }
}

実行すると下のような表示になります。

f:id:llcc:20161217214232p:plain

パスコードを入力した時の動作はプロトコルに記述します。
passwordInputComplete:input:は入力が完了した時に呼ばれます。

extension ViewController: PasswordInputCompleteProtocol {
    func passwordInputComplete(_ passwordContainerView: PasswordContainerView, input: String) {
        print(input)
    }
    
    func touchAuthenticationComplete(_ passwordContainerView: PasswordContainerView, success: Bool, error: NSError?) {
        
    }
}

パスコードが違う場合のシェイクはwrongPasswordメソッドを使います。
入力のクリアだけしたい場合はclearInputメソッドを使えば良さそうです。

extension ViewController: PasswordInputCompleteProtocol {
    func passwordInputComplete(_ passwordContainerView: PasswordContainerView, input: String) {
        passwordContainerView.wrongPassword()
    }
    
    func touchAuthenticationComplete(_ passwordContainerView: PasswordContainerView, success: Bool, error: NSError?) {
        
    }
}

それとPasswordContainerViewのプロパティーを触る事で少しだけデザインを変更できます。
ボタンの色を変えるtintColor、ハイライト時の色を変えるhighlightedColor、曇りガラス風デザインに変更できるisVibrancyEffectなどがあります。

f:id:llcc:20161217220112p:plain

タッチIDが使える場合は自動的にViewにタッチボタンが表示されます。

f:id:llcc:20161217220703p:plain

押すとタッチIDを求める表示になります。

f:id:llcc:20161217220830p:plain

指紋認証が完了するとdelegateのtouchAuthenticationCompleteメソッドが呼ばれます。

extension ViewController: PasswordInputCompleteProtocol {
    func passwordInputComplete(_ passwordContainerView: PasswordContainerView, input: String) {
    }
    
    func touchAuthenticationComplete(_ passwordContainerView: PasswordContainerView, success: Bool, error: NSError?) {
        print(success)
    }
}

今回は触れないのですが、PasswordUIValidationというクラスを使ったアプローチもあるようです。

THPinViewControllerとの比較

THPinViewControllerより自由度が高いのが良さそうでした。
THPinViewControllerは良く使っているんですが、「パスコード正解時は自動的にdissmissされる為、確認ページへ遷移させるのが難しい」みたいな問題があってそれが解決するだけでも良さそうです。
それと指紋認証を標準で対応してくれているのもありがたいです。

www.cl9.info