20200909のiOSに関する記事は17件です。

Xib で View を生成する時に知っておくといいこと

先日、うっかり ViewClass で指定した Xib で作成したカスタムビューのプロパティにアクセスし下記のように Error を出してしまいました。初心者の頃はよく分からずググったものコピペ なりで回避していましたが、そもそも何故これが Error になるのかを今回は紹介したいと思います。

    override func viewDidLoad() {
        super.viewDidLoad()
        hogeView.label.text = "hogehoge"
    }
    // Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

Xib で View を生成する方法

Xib で View を生成する方法は、大きく分けて View ClassFile's Owner を指定する2通りの方法があります。

View Class

image.png

View Class は xib上の View に対してクラスを指定することで直接的に xib と参照を作ることができます。この方法は UINib を使用して View をコードから初期化する場合は、全てのオブジェクトが使用できる状態になっていることが担保されますが、Storyboardxib などで直接 View を生成する場合は基本的には nib-loadingではない(init(decode: NSCoder))ため、全ての View が初期化されていることは担保されません。(これが、View Class で作成したカスタムビューなどを Xib 上で生成した時に、プロパティへの参照が nil でクラッシュする原因と考えられます)

The order in which the nib-loading code calls the awakeFromNib methods of objects is not guaranteed. In OS X, Cocoa tries to call the awakeFromNib method of File’s Owner last but does not guarantee that behavior. If you need to configure the objects in your nib file further at load time, the most appropriate time to do so is after your nib-loading call returns. At that point, all of the objects are created, initialized, and ready for use.

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html#//apple_ref/doc/uid/10000051i-CH4-SW24

つまり、基本的に View Class を指定した View を使用する場合には、コードでインスタンスを生成して使用する必要があります。例えば、UITableViewCell などは再利用性がありコードから生成するのが適切なので、 Xcode のテンプレートなどでも用意されていると考えられます。

File's Owner

image.png

File's Owner は名前の通り Xib 上の View の保持先を指定することで、@IBOutlet などでクラスに View の参照を付けることができます。つまり、クラスと Xib で作成された View のインスタンスは別なので、nib-loading を適切なタイミングで呼ぶことで View の全てのオブジェクトが使用可能になっていることが担保されます。これによって、コードから生成される時は、init(frame: CGRect) で、StoryboardXib から生成される時は init?(coder aDecoder: NSCoder) から View を生成することができるようになります。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[アプリ開発]DKImageViewControllerで写真共有アプリ作ってみた

5568440d5934344ad6f605910ee9f3e1.gif
464b0ad207bdce4da56f3c8461957761.gif

作った経緯

初めて作ったファッションアプリが途中でできなかったので、なんかCollectionView使ってやりたいなーって思いましてインスタみたいな写真共有アプリ作ろうと決心して開発しました!
参考にしたアプリはtabioriさんです

https://tabiori.com/

とりあえず作ってみた!
スクリーンショット 2020-08-23 14.58.10.png
こんな感じでやろうと思ったら、、、ア〜〜〜〜〜〜!!!!
スクリーンショット 2020-09-09 20.34.11.png
ViewControlller多くなりすぎて訳わからんくなった。。
初心者の僕はパンクしました
っていう感じで何回もプロジェクト作り直しました。

インスタの写真一枚だけならスクールの教材で出来るんですが、何せConllectionView使いたかったんでタイムラインの画像を複数表示出来るようにさせたかったんですよね

問題

このアプリ開発するのめっちゃ時間かかりましたこれにスクールの大半の時間消費しましたねw
まずFirebaseですね
ライブラリの画像を選択→CollectionViewで表示→ボタン選択→Firebaseに保存したかったんですがうまくいかず諦めて、
日付とタイトル、画像を別の画面でやろうとするとすごくめんどくさくなったり、、、
いろいろうまくいきませんでした。
でライブラリから写真を複数選択しようとすると標準?のライブラリだとできないし

DKImagePickerControllerの極意
http://cocoadocs.org/docsets/DKImagePickerController/3.8.1/

ライブラリから選択した画像を画面遷移先にデータを送るとかファイル形式違うものに渡しかたとか、、
どうやってやんの〜〜〜って感じです:joy_cat:

CollectionViewとTableViewの組み合わせ極意
https://qiita.com/Erica_pon/items/f1c6a06e399723f32549

こちらを参考にタイムライン画面でcolleCtionViewとTableView使ってモデル作ってみたけど画像表示できず開発休止しました、、、

結果

リリースはできてません!!
picture_pc_fb4f1c662c0d1ac2606e5093ff74ddec.png
まだ未完成です
諦めませんねー
よかったらみてください!

今回作ったリポジトリ
https://github.com/rentamaeda/Travel2

感想

なんでもそうですが作る前の計画練るのって楽しいですよねー
なんか途中から気持ち折れてしまって何回も諦めかけましたよ

次もしollectioView使うならタスク管理アプリ作りたいなーって思いますw

参考にしたサイト等

taiori様HP
https://tabiori.com/

DKImagePickerControllerの極意
http://cocoadocs.org/docsets/DKImagePickerController/3.8.1/

CollectionViewとTableViewの組み合わせ極意
https://qiita.com/Erica_pon/items/f1c6a06e399723f32549

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【入門】iOS アプリ開発 #8【スペシャルターゲット・得点表示】

はじめに

今回はスペシャルターゲット(くだもの類)、パックマンがそれを食べた時やゴーストを噛み付いた時の得点表示を作成していく。

イメージ図は下記の通り。スペシャルターゲット(サクランボ表示)と、それをパックマンが食べると得点が表示される。

Image7.png

仕様書

まずはスペシャルターゲットの仕様の確認。

Image5.png

表示されている間にパックマンが食べると、得点が2秒だけ表示される(仕様にはないが実機確認)。

続いてパックマンがゴーストを噛み付いた時の得点表示の仕様は以下の通り。

Image51.png

こちらの得点表示は1秒間だけ(同じく仕様にはないが実機確認)。

設計方針

スペシャルターゲットは、パックマンがエサを70個、170個食べた時に、

specialTarget.start(kind: .Cherry)

とするとサクランボのスペシャルターゲットが表示され、10秒後に自動的に消えるように設計したい。

表示されている間にパックマンが食べると 100 というように得点表示する。表示の時間は2種類(2秒、1秒)あるため、得点の種類や位置に加えて時間指定も行えるようにする。

ptsManager.start(kind: .pts100, position: specialTarget.position, interval: 2000)  // 2000ms

といった具合にする。
得点表示は、スペシャルターゲットとゴースト4匹を連続で噛みつくとして、最大5つまで同時表示できるように設計しておきたい(実際はゴーストをかみついている間はパックマンの動きが停止するため、実質2つまでとなる)。

クラス構造

スペシャルターゲットと得点表示は、CgActorクラスを継承して作成する。

Image62.png

得点表示は CgScorePtsクラスとして、これを最大5つ表示する管理クラス CgScorePtsManager を作成する。

CgSpecialTarget クラス

スペシャルターゲット(くだもの類)を表示する。

class CgSpecialTarget : CgActor {

    enum EnSpecialTarget: Int {
        case Cherry
        case Strawberry
        case Orange
        case Apple
        case Melon
        case Galaxian
        case Bell
        case Key
        case None

        func getScorePts() -> CgScorePts.EnScorePts {
            switch self {
                case .Cherry:     return .pts100
                case .Strawberry: return .pts300
                case .Orange:     return .pts500
                case .Apple:      return .pts700
                case .Melon:      return .pts1000
                case .Galaxian:   return .pts2000
                case .Bell:       return .pts3000
                case .Key:        return .pts5000
                case .None:       return .pts0
            }
        }

        func getTexture() -> Int {
            switch self {
                case .Cherry:     return 16*3+2
                case .Strawberry: return 16*3+3
                case .Orange:     return 16*3+4
                case .Apple:      return 16*3+5
                case .Melon:      return 16*3+6
                case .Galaxian:   return 16*3+7
                case .Bell:       return 16*3+8
                case .Key:        return 16*3+9
                case .None:       return 16*3+10
            }
        }
    }

    private var kindOfSpecialTarget: EnSpecialTarget = .None
    private var timer_disappearSpecialTarget: CbTimer!

    override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object, deligateActor: deligateActor)
        timer_disappearSpecialTarget = CbTimer(binding: self)
        actor = .SpecialTarget
        sprite_number = actor.getSpriteNumber()
    }

    // ============================================================
    //   Core operation methods for actor
    //  - Sequence: reset()->start()->update() called->stop()
    // ============================================================

    /// Reset special target state.
    override func reset() {
        super.reset()
        timer_disappearSpecialTarget.set(interval: 10000) // 10s
        timer_disappearSpecialTarget.reset()
        position.set(column: 13, row: 15, dx: 4)
    }

    /// Start to draw special target at the specified position.
    override func start() {
        super.start()
        timer_disappearSpecialTarget.start()
        deligateActor.setTile(column: position.column, row: position.row, value: .Fruit)
        sprite.draw(sprite_number, x: position.x, y: position.y, texture: kindOfSpecialTarget.getTexture())
    }

    /// Update handler
    /// - Parameter interval: Interval time(ms) to update
    override func update(interval: Int) {
        if timer_disappearSpecialTarget.isEventFired() {
            stop()
        }
    }

    /// Stop drawing special target.
    override func stop() {
        super.stop()
        timer_disappearSpecialTarget.stop()
        deligateActor.setTile(column: position.column, row: position.row, value: .Road)
        sprite.clear(sprite_number)
    }

    // ============================================================
    //  General methods in this class
    // ============================================================

    func start(kind: EnSpecialTarget) {
        kindOfSpecialTarget = kind
        self.start()
    }
}

コンストラクタで CbTimer を生成して、reset() メソッドで 10秒を設定しておく。
start()メソッドが呼ばれたら、スプライトを表示しタイマーを起動させる。
update()メソッドがフレーム毎に呼ばれる中でタイマーが 0 になったら、内部のstop()メソッドを呼びスプライトを消去する。

簡単なコードとなった。

CgScorePts クラス

得点クラスはスペシャルターゲットとほぼ同様のコードとなる。1000点以上はスプライトを2つ使って表示する。

class CgScorePts : CgActor {

    enum EnScorePts: Int {
        case pts100 = 0
        case pts200
        case pts300
        case pts400
        case pts500
        case pts700
        case pts800
        case pts1000
        case pts1600
        case pts2000
        case pts3000
        case pts5000
        case pts0

        func getScore() -> Int {
            switch self {
                case .pts100  : return 100
                case .pts200  : return 200
                case .pts300  : return 300
                case .pts400  : return 400
                case .pts500  : return 500
                case .pts700  : return 700
                case .pts800  : return 800
                case .pts1000 : return 1000
                case .pts1600 : return 1600
                case .pts2000 : return 2000
                case .pts3000 : return 3000
                case .pts5000 : return 5000
                case .pts0    : return 0
            }
        }

        func get2times() -> EnScorePts {
            switch self {
                case .pts100  : return .pts200
                case .pts200  : return .pts400
                case .pts400  : return .pts800
                case .pts800  : return .pts1600
                default : return self
            }
        }

        func getTextures() -> (Int, Int) {
            switch self {
                case .pts100  : return (16*9   , 0)
                case .pts200  : return (16*8   , 0)
                case .pts300  : return (16*9+1 , 0)
                case .pts400  : return (16*8+1 , 0)
                case .pts500  : return (16*9+2 , 0)
                case .pts700  : return (16*9+3 , 0)
                case .pts800  : return (16*8+2 , 0)
                case .pts1000 : return (16*9+4 , 16*9+5)
                case .pts1600 : return (16*8+3 , 0)
                case .pts2000 : return (16*10+4, 16*10+5)
                case .pts3000 : return (16*11+4, 16*11+5)
                case .pts5000 : return (16*12+4, 16*12+5)
                case .pts0    : return (16*9   , 0)     //
            }
        }
    }

    private var ptsNumber: EnScorePts = .pts0
    private var timer_disappearPts: CbTimer!

    override init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object, deligateActor: deligateActor)
        timer_disappearPts = CbTimer(binding: self)
        actor = .Pts
    }

    // ============================================================
    //   Core operation methods for actor
    //  - Sequence: reset()->start()->update() called->stop()
    // ============================================================

    /// Reset
    override func reset() {
        super.reset()
    }

    /// Start
    override func start() {
        super.start()
        timer_disappearPts.start()

        let textures: (Int,Int) = ptsNumber.getTextures()
        sprite.draw(sprite_number, x: position.x, y: position.y, texture: textures.0)
        if  textures.1 != 0 {
            sprite.draw(sprite_number+1, x: position.x+16, y: position.y, texture: textures.1)
        }
    }

    /// Update handler
    /// - Parameter interval: Interval time(ms) to update
    override func update(interval: Int) {
        if timer_disappearPts.isEventFired() {
            stop()
        }
    }

    /// Stop
    override func stop() {
        super.stop()
        timer_disappearPts.reset()

        sprite.clear(sprite_number)
        if ptsNumber.getTextures().1 != 0 {
            sprite.clear(sprite_number+1)
        }
    }

    // ============================================================
    //  General methods in this class
    // ============================================================

    func start(kind: EnScorePts, position at: CgPosition, interval time: Int) {
        ptsNumber = kind
        timer_disappearPts.set(interval: time)
        self.position.set(at)
        start()
    }

}

CgScorePtsManager クラス

CgScorePts のオブジェクトを5つ管理して表示の制御を行う。

class CgScorePtsManager: CbContainer {

    private let firstSpriteNumber: Int = CgActor.EnActor.Pts.getSpriteNumber()
    private let numberOfActors: Int = 5

    private var actors: [CgScorePts] = []
    private var doings: [CgScorePts] = []

    init(binding object: CgSceneFrame, deligateActor: ActorDeligate) {
        super.init(binding: object)
        for i in 0 ..< numberOfActors {
            let actor: CgScorePts = CgScorePts(binding: object, deligateActor: deligateActor)
            actor.sprite_number = firstSpriteNumber+i*2
            actors.append(actor)
        }
    }

    /// Reset
    func reset() {
        for each in actors {
            each.reset()
        }
        self.enabled = false
    }

    /// Start to draw Pts
    /// - Parameters:
    ///   - kind: Kind of pts
    ///   - position: Position to draw
    ///   - time: Time to disappear
    func start(kind: CgScorePts.EnScorePts, position: CgPosition, interval time: Int) {
        let actor: CgScorePts

        if actors.count == 0 {
            actor = doings.remove(at: 0)
            actor.stop()
        } else {
            actor = actors.remove(at: 0)
        }
        actor.start(kind: kind, position: position, interval: time)
        doings.append(actor)
        self.enabled = true
    }

    /// Update handler
    /// - Parameter interval: Interval time(ms) to update
    override func update(interval: Int) {
        for each in doings {
            if !each.enabled {
                actors.append(each)
                doings.remove(at: 0)
            }
        }

        if doings.count == 0 {
            enabled = false
        }
    }

    /// Stop
    func stop() {
        for each in doings {
            each.stop()
            actors.append(each)
            doings.remove(at: 0)
        }
        self.enabled = false
    }

}

コンストラクタで CgScorePtsオブジェクトを5つ生成して actors配列に格納しておく。
start()メソッドが呼ばれた時に、actors配列から CgScorePtsオブジェクトを取り出して start()し、doings配列へ移動しておく。enabled = true にすると、フレーム毎に update()が呼ばる。
update()メソッド内では、doings配列にある CgScorePtsオブジェクトが stop(enabled= false) していたら、doings 配列から actors へ戻しておく。

start()メソッドが呼ばれた時に5つ全部表示中で actors配列から取り出されなかった場合は、doings から1つ持ってきて使いまわす。

まとめ

スペシャルターゲットと得点表示のクラスを作成した。作成したソースコードは合計300行程度。

必要な役者はそろったので、次回はこれら役者のシーケンス動作を組み合わせて、ゲームとしてプレイできるものを作成していく。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CryptoKit を使って ECDSA を用いた署名を実装する

ECDSAでJWTによる電子署名を作成することがあったのでメモ :pencil:

ECDSAとは

楕円曲線暗号(だえんきょくせんあんごう、Elliptic Curve Cryptography、ECC)とは、 楕円曲線 上の 離散対数問題 (EC-DLP) の困難性を安全性の根拠とする 暗号

  • 暗号化と復号とで異なる2つの鍵を使用し、暗号化の鍵を公開できるようにした公開鍵暗号
  • RSA暗号 と比べて、短いデータ長で処理速度も早いが同レベル安全性が実現できるのがメリット

CryptoKit を使ってやること

CryptoKitを使って以下の手順により署名を作成していきます。

  1. 鍵の生成
  2. 電子署名の作成

全体のコードはこちらに上げてあります。

Key Pairの生成

CryptoKitの P256.Signing.PrivateKey を使えば2行で書けます

let privateKey = P256.Signing.PrivateKey()
let publicKey = privateKey.publicKey

P256.SigningはECDSA(P-256)を使用した署名、検証のためのもの
P256.KeyAgreement という鍵交換に使うためのものも用意されている

署名の作成

JWT Header

Headerに指定できる各パラメータについての説明は Registered Header Parameter Names を参照

{ "alg": "ES256", "typ": "JWT" }

JWT Claims

Claimsに指定できる各パラメータについての説明は Registered Claim Names を参照

{
  "aud": "my-project",
  "iat": 1509650801,
  "exp": 1509654401
}

JWT signature

ECDSA P-256 の署名の例が JSON Web Signature (JWS) に書いてあります
Base64urlエンコードした JWT Header と JWT claims を . でつなげたものを署名し、以下の順序で . でつないだものがJWTとなります

{base64url-encoded header}.{base64url-encoded claim set}.{base64url-encoded signature}

署名にはP256.Signing.PrivateKey が持っている signature(for:) を使います

let input = "{base64url-encoded header}.{base64url-encoded claim set}"
let data = input.data(using: .utf8)!
let signature = try privateKey.signature(for: data)

ここで注意が必要なのが署名のフォーマットです。
ECDSA P-256 SHA-256の署名は、符号なし整数のECポイント(各32オクテットのR, S)で表されます。
signature(for:) が返してくる P256.Signing.ECDSASignaturederRepresentationrawRepresentation の2つをもっていて、それぞれ以下の値が返ってきます。

  • derRepresentation ASN.1 DER でフォーマットされた署名
  • rawRepresentation 署名のrawデータ

ECDSA P-256 SHA-256の署名では rawRepresentation の方を使用します。
先述の通りJWTはbase64urlエンコードした header と claim set と signature をドットでつなぐので

extension Data {
    func base64urlEncodedString() -> String {
        return base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }
}
let raw = signature.rawRepresentation
let signedJWT = "\(input).\(raw.base64urlEncodedString())"

これでJWTの完成です。

ちなみに、ASN.1 DERフォーマットは以下のような構造になっており、この中からrとsを取り出しつなげたものが rawRepresentation に相当します。

0x30|b1|0x02|b2|r|0x02|b3|s

  • b1: 0x02以降に続くバイト列の長さ
  • b2: rのバイト列の長さ
  • b3: sのバイト列の長さ をそれぞれ表す

この仕様に基づいて以下のように derRepresentation から raw data に変換してみると rawRepresentation と同じ値が得られます。

let der = signature.derRepresentation
let sequence = der.removeFirst() // 0x30

let b1 = der.removeFirst()

let tag1 = der.removeFirst() // 0x02
let b2 = der.removeFirst()
var r = der.prefix(Int(b2))
der = der.advanced(by: Int(b2))

let tag2 = der.removeFirst() // 0x02
let b3 = der.removeFirst()
var s = der.prefix(Int(b3))

let octetLength = 32
guard r.count <= octetLength + 1, s.count <= octetLength + 1 else {
    throw SignatureError.invalidLength
}
r = (r.count == octetLength + 1) ? r.dropFirst() : r
s = (s.count == octetLength + 1) ? s.dropFirst() : s

return r+s

Security Frameworkの SecKeyCreateSignature を使う場合、こちらは ASN.1 DERフォーマット で返されるので上記のような変換が必要です。
CryptoKitを使えば signature.rawRepresentation だけで取得できるのですごく便利です。

まとめ

CryptoKitを使うと鍵の生成や署名が1行くらいでできてしまうのでとても簡単に使えて驚きました。
ただし、CryptoKitで生成した鍵は自分で保存する必要があるのでそれを考えると生成はSecurity Frameworkの SecKeyGeneratePair をつかってもいい気がしました。
CryptoKitで生成した鍵をキーチェーンに保存する方法はAppleがSampleコードを提供しているのでこれを参考に。
Storing CryptoKit Keys in the Keychain

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EXC_BAD_ACCESSの対処法【All Exceptionの設定は不要らしい。】

スクリーンショット 2020-09-09 19.12.58.png

 EXC_BAD_ACCESSとは?

通常、エラーメッセージを持たないため、デバッグするのが最も難しい種類のクラッシュ。
でも、Swiftでは非常に稀。

 要点

All Exceptionを使わずとも、EXC_BAD_ACCESSのエラー箇所が出るらしい。
※ Xcode ver 11.7

下部にエラー表示。僕の場合は、不適切なselfが原因でした。
ご覧の通り、Navigator AreaではAll Exceptionの設定はしていません。

スクリーンショット 2020-09-09 18.51.02.png

 デバッグ方法

どのバージョンから上記の仕様になったか知りませんが、
少なくともXcode 9.4.1ではAll Exceptionの設定が必要だったらしい。↓

【Xcode】All Exceptionの設定方法(デバッグ)

バージョンによって、
Exception Breakpointとか、Add Exception BreakPointとか。
表示は異なれど、やることは同じ。


EXC_BAD_ACCESS対処時には、これらのサイトを参考にしました。

Xcodeでデバッグ実行中にクラッシュした時に捗るブレークポイント設定

おしまい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EXC_BAD_ACCESSの対処法

スクリーンショット 2020-09-09 19.12.58.png

 EXC_BAD_ACCESSとは?

通常、エラーメッセージを持たないため、デバッグするのが最も難しい種類のクラッシュ。
でも、Swiftでは非常に稀。

 要点

All Exceptionを使わずとも、EXC_BAD_ACCESSのエラー箇所が出るらしい。
※ Xcode ver 11.7

下部にエラー表示。僕の場合は、不適切なselfが原因でした。
ご覧の通り、Navigator AreaではAll Exceptionの設定はしていません。

スクリーンショット 2020-09-09 18.51.02.png

 デバッグ方法

どのバージョンから上記の仕様になったか知りませんが、
少なくともXcode 9.4.1ではAll Exceptionの設定が必要だったらしい。↓

【Xcode】All Exceptionの設定方法(デバッグ)

バージョンによって、
Exception Breakpointとか、Add Exception BreakPointとか。
表示は異なれど、やることは同じ。


EXC_BAD_ACCESS対処時には、これらのサイトを参考にしました。

Xcodeでデバッグ実行中にクラッシュした時に捗るブレークポイント設定

おしまい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift基礎文法書に付箋を貼ってみた

Swiftの基礎文法書の復習

Swiftの基礎が学べる良書である「絶対に挫折しない iPhoneアプリ開発「超」入門 第8版 【Xcode 11 & iOS 13】 完全対応」を読み返しながら、開発していて注意すべきところ知らなかったところに付箋を貼ってみました。

for文

知らなかったのですが、forの後に続くのは定数らしいです。

以下、コード例です。

for 定数 in 範囲 {
    処理
}

一見、範囲の値の回数だけ値が変わっているように見えるので変数ではないかと思っていたのですがどうやら違うみたいです。

for文で使われている定数nは、for文が一度実行されるたびに寿命が切れて、メモリ上から消去され、そして次の文を実行する際に再度同名の定数nが新たに宣言されて・・・・・

プロックとスコープ

以下のコードで{}で囲われたところをブロックと呼びます。

プロック
var count = 0
for n in 1...5 {
    count += 1
}
print(count) // => 0 countのスコープはプロックの中だけ

知ってはいるんですけど、開発中についつい忘れてしまいます。

Swiftのswitch文にbreakいらない

以下、コード例です。

case文
switch  {
    case 定数A:
        処理A
    case 定数B:
        処理B
    ...
    default:
        処理X
    //breakいらない
}

なぜかついついbreakを書いてしまいます。
ちなみにSwiftでは一つのcase内の処理がされるとswitch文は終了します。

forと配列

以下、コード例です。

配列を使用した繰り返し処理
for 定数 in 配列 {
    処理
}

配列に入った各要素に対して処理を加えた時に使います。配列のキャストなどで使えます。また、配列のキャストではmap使ったりもします。

プロパティの種類

プロパティにもいろいろな種類があります。

1.ストアドプロパティ

構造体を定義する際にプロパティを定義しますが、ここで値を保持するためのプロパティをストアドプロパティと言います。

2.コンピューテッドプロパティ

そして、値を計算するためのプロパティをコンピューテッドプロパティと言います。

3.タイププロパティ

プロパティを分ける際、基準が値の持ち方による分け方呼び出し元による分け方でわかれます。

値の持ち方による分け方
・ストアドプロパティ
or
・コンピューテッドプロパティ

呼び出し元による分け方
・インスタンスプロパティ //インスタンスから呼び出すプロパティ つまり、ストアドorコンピューテッドプロパティのこと
or
タイププロパティ

そして、このタイププロパティというのは構造体から直接呼び出すプロパティのことです。

プロパティの種類 コード例

以下、コード例です。

ストアドプロパティとコンピューテッドプロパティ
struct Square {
    var length = 3     //一辺の長さ
    var area:Int {     //面積
        let result = length * length
        return result
    }
}
let square = Square()
print(square.area)  //9

2.コンピューテッドプロパティの特徴

上記のlengthがストアドプロパティで、areaがコンピューテッドプロパティです。
ストアドプロパティの方は馴染みがありますが、コンピューテッドプロパティは見たことがあるくらいです。
コンピューテッドプロパティには以下の特徴があります。
■固定の値は保持しない //つまり定数は指定できない
■呼び出されたタイミングで処理を行い、処理結果を戻す
■型名の省略が不可能
■文末に{}がある

コンピューテッドプロパティはメソッドと似ていますね、同じ計算式を繰り返す時に使えそうです。

3.タイププロパティコード例

宣言時の式
struct 構造体名 {
    static var タイププロパティ名 = 初期値
}

タイ焼きに背びれがあるかどうかを確認するタイププロパティを以下に記述してみます。

タイ焼きに背びれがあるかどうか
struct Taiyaki {
    static var hasFin = true
}

Taiyaki.hasFin //trueになる

初期値が決まっているものを指定して呼び出す際に安全です。(初期値として何が格納されているのかわかっているため。)

タイプメソッド

タイププロパティと考え方が全く一緒なので、載せておきます。実はメソッドも「インスタンスメソッド」もしくは「タイプメソッド」に分類できます。
インスタンスから呼びだすメソッド => 「インスタンスメソッド」
構造体から呼びだすメソッド => 「タイプメソッド」

宣言(タイ焼きの例と合わせる)
struct Taiyaki {
    static func hasFin() -> bool {
        return true
    }
}
Taiyaki.hasFin() //trueが返る

イニシャライザのself

イニシャライザの引数構造体の{ }内のプロパティ名が同じならselfで区別します。
構造体の{ }内でselfをつけるとそのselfは構造体から作成されたインスタンスを指します。

以下、コード例です。

struct Car {
    var forwardWheel: Int
    var backWheel: Int
    init(forwardWheel: Int, backWheel: Int) {
        self.forwardWheel = forwardWheel   //self.forwardWheelが構造体のプロパティ
        self.backWheel = backWheel         //backWheelはinitの引数
    }
}

つまり、
■selfをつけた変数が「プロパティ」
■selfを付けない変数が「引数」です

デフォルトイニシャライザ

インスタンスの生成時に構造体()のようにコードを書けて、引数が1つもないinit()というイニシャライザが自動的に呼び出されます。このイニシャライザをデフォルトイニシャライザと言います。

構造体に含まれるすぺてのプロパティに初期値が設定されている場合に、デフォルトイニシャライザが自動的に呼び出されます。

イニシャライザを記述しない構造体
class hoge {
    var fuga = "Hi!"
    var piyo = 123
}

ここで重要なのはイニシャライザが必要ないというわけではないということです。

構造体のデフォルトイニシャライザ
class hoge {
    var fuga = "Hi!"
    var piyo = 123
    init() {
    }
}

クロージャ

正直、今までテキトーに理解していたので復習しました。
クロージャは言ってしまえば、名前のない関数みたいなもので文の中に埋め込めます。

Firebaseの匿名ログインのコード
Auth.auth().signInAnonymously() { (authResult, error) in
  // ... 引数"authResult"もしくは"error"に値が入った後に、ここの処理が行われる
}

さらに具体的に実装してみます。

Firebaseの匿名ログインのコード2
Auth.auth().signInAnonymously() { (authResult, error) in
   if error != nil{        //errorに値が入った場合の処理
       print("Auth error: \(error)")
   } else {
       print("success: \(authResult)")
   }
}

Firebaseの匿名ログイン実装時につかったので、公式ドキュメントを参考にしました。
参考:iOS で Firebase 匿名認証を行う

なお、クロージャからプロパティにアクセスする場合selfがつきます。よくエラーメッセージでますよね笑

列挙型

enumとうやつです。理解するのにかなり時間かかりました。これ。
enumとは簡単に言えば、新しい型を独自に作成するようなものです。

Week型の宣言
enum Result {      //Result型
    case true      //値名
    case false
    case other
}

//インスタンス作成時
var result = Result.true  //値の
result = .false           //このように

仮にResultという型を作りたいとして、値がtrueとfalseとotherしか値に取れなくなり、意図しない値が入るのを防げます。私は具体的にはMVCを学習している時にJSONの変換を通して学んだので、別記事にしてまとめたいです。
型の入れ子の書式でCodingKeyを使いました。

型の入れ子とCodingKeys
struct 構造体 {
    private enum CodingKeys:String CodingKey {
        case title
        case url
        ... 
    }
}

2回CodingKeyが記述されてるように見えますが、これはCodingKeysという名でString型のCodingKeyを宣言しているという意味になります。

オプショナル

オプショナルについては別で記事を書きました。
オプショナルへの予想を上回る気遣い

構造体は「値型」、クラスが「参照型」

構造体とclassは同じものに見えて、微妙に違います。
これに関してはかなりわかりやすい記事を見つけたので、載せておきます。
【swift】イラストで分かる!classとstructの違いについて【初心者向け】

プロトコル

MVCの設計を学習する時やtable周りの学習で目にしていると思います。
イメージは構造体で定義したメソッドを渡してあげるものという感じです。

構造体とプロトコルの宣言
//Model
protocol HogeDelegate {
    func fuga(with someData: Data) //メソッド名のみ
}

struct HogeModel {

    weak var delegate: HogeDelegate? //弱参照で記述(弱参照についての説明は省きます)

    func fuga() {
        処理
    }
}

//ViewController
private let hogeModel = HogeModel() //privateの説明も省きます

class piyoViewController: UIViewController, HogeDelegate{  //extensionさせて記述できるとより見やすくなります。

    override func viewDidLoad() {
        super.viewDidLoad()
        hogeModel.delegate = self
        hogeModel.fuga()
    }

    //
    func fuga(with someData: Data) {
        //with someDataに対する処理
    }
}




/*extensionで記述した場合
extension ItemsViewController: HogeDelegate {
    func fuga(with someData: Data) {
        //with someDataに対する処理
    }
}
*/

上記は、fuga()メソッドがviewDidLoad()で呼び出され、Modelのfuga()メソッドが処理されます。その処理結果をHogeDelegateに渡し、ViewControllerに通知(渡す)してあげています。

プロトコルのストアドプロパティ

またまた、ストアドプロパティとコンピューテッドプロパティがでてきました。プロトコルでのストアドプロパティは少し特殊な宣言をします。

プロトコルでのストアドプロパティ
protcol HogeDelegate {
    var ストアドプロパティ:  { get set } //getは読み込み、setは書き込みの意味。読み込みだけさせたいのなら、{ get }にする
}

{ get set }がプロトコルで、でてきたらストアドプロパティだ!と思い出せれば大丈夫ですね。

プロトコルでのコンピューテッドプロパティ
protcol HogeDelegate {
    var コンピューテッドプロパティ:  { get } //型指定必須
}

タプル

var person = (name: "山田太郎", age: 27) //変数名 = (要素名(key):値(value), 要素名:値...)

print(person.name) //変数.要素名で取り出し

あまり使わないかもしれませんが、一応記述します。
配列や辞書のようにタプルは複数の値をまとめて格納することができます。異なる点は、タプルには異なる型の要素を格納できること。また、要素の追加・削除が行えないことです。

これから

特に以下の文法に関するキーワードは別途記事を書いて思考を再度整理していこうと思います。
- classとstruct
- Guard let文
- enum(列挙型)
- varとlet
- publicとかprivateのアクセス修飾子
- final
- 弱参照、強参照
- Any型について

他にもSwiftの理解を深めるために、「[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 (WEB+DB PRESS plus)」を読んでいこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【IOU】 Object Detection の性能指標【I/U】

ユニオン交差 Intersection Over Union (IOU) が一般的なようです。
iou.png

IOU = 正解boxと予測boxの重なりあう領域 / 正解boxと予測boxの重なり合っている部分と重なり合っていない部分の和

つまり、IOUが100%であれば、完璧に正解と予測が一致しているということです。

CreateMLの正解率は、このIOUが50%を越えているもの(50%重なっているもの)を正解とみなして%表示しています。

I/U 50%
87%

というのは50%領域が重なっているケースが87%あったということです。

もう一つの指標、varied I/Uというのは、閾値を50%〜95%まで変化させた時の平均正解率。
これが低いと、重なっている部分はあるものの、重なる領域は狭い、ということになります(もちろん50%閾値の正解率以下になります)。


Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]ColorをRGBで指定する

はじめに

SwiftでcolorをRGBで指定する方法を説明します。

Swiftでのcolor指定

まず通常の指定方法は

ViewController.swift
sampleView.backgroundColor = .blue

sampleView.backgroundColor = UIColor(red: 0.1, green: 0.5, blue: 1.0, alpha: 1.0)

sampleView.backgroundColor = UIColor(red: 30/255, green: 144/255, blue: 255/255, alpha: 1.0)

下に載せている記事が参考になるかと思いますが、SwiftでRGB指定しようとすると全ての値を255で割って分数表記にしないといけないため、若干めんどくさいです。
@shu26 さんのQiita
https://qiita.com/shu26/items/bc0a8a06019b24d799d4

そこで、RGBの値をそのまま利用するためのextensionを作成します。

255で割るためのextensionを作成する

UIColorとかの名前をつけたSwiftファイルに、extensionを作成します。

UIColor.swift
import UIKit

extension UIColor {
    static func rgb(red: CGFloat, green: CGFloat, blue: CGFloat) -> UIColor{
        return self.init(red: red / 255, green: green / 255, blue: blue / 255, alpha: 1)
    }
}

Viewの色を変更してみる

作成したextensionを使ってViewの色を変更してみます。

ViewController.swift
sampleView.backgroundColor = UIColor.rgb(red: 121, green: 162, blue: 255)

rgbsample.png
これでRGBでcolorを指定することができるようになります!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

セグメンテーションモデル(DeepLabV3)の結果をCore ML Helpersでラベルごとに振り分ける。

アップルの公式配布DeepLabV3のCore ML モデルは、(512,512)のML Multi Arrayを出力します。
各ピクセルは0~14のラベル値です。

0,'background'背景
1,'aeroplane'飛行機
2,'bicycle'自転車
3,'bird'鳥
4,'boat'ボート
5,'bottle'ボトル
6,'bus'バス
7,'car'車
8,'cat'猫
9,'chair'椅子
10,'cow'牛
11,'diningtable'テーブル
12,'dog'犬
13,'horse'馬
14,'motorbike'バイク
15,'person'人
16,'pottedplant'鉢植え
17,'sheep'羊
18,'sofa'ソファ
19,'train'電車
20,'tv'テレビ

出力結果をマスク画像として使いたい場合、ピクセルのラベル値を判別する必要があります。
Core ML Helpersでラベル値によって判別してマスク画像にできます。

// Core ML Helpersの toRawBytes関数の中で、対象のラベル値を白くする

for c in 0..<channels {
      for y in 0..<height {
        for x in 0..<width {
          var value = ptr[c*cStride + y*yStride + x*xStride]
       //例えば車を判別したい場合、ラベル値が7なので、7は0(白)にして他は255(黒)にするとマスク画像ができる。
            if value != T(7) {
                value = T(0)
            } else {
                value = T(255)
            }
          let scaled = (value - min) * T(255) / (max - min)
          let pixel = clamp(scaled, min: T(0), max: T(255)).toUInt8
          pixels[(y*width + x)*bytesPerPixel + c] = pixel
        }
      }
    }

IMG_0530.PNG

マスク画像がとれると、背景だけぼかしたり、お馴染みのセグメンテーション画像が
できます。
IMG_0531.PNG

スクリーンショット 2020-09-09 11.13.10.png


Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

360度、デバイスの向きを測定する時の【Tips】

Core MotionのDevice MotionかARKitでデバイスの向きはとれます。

一周は6.28318531ラジアンです。

開始原点が0ラジアン。

デバイスのY軸を中心に回転させた場合、
右回りだとラジアンが加算。
左回りだとラジアンが減算されます。

ただし、半周で+-が逆転します。
右回りの場合、3.14ラジアンまで加算すると、次は-3.14ラジアンになります。

← -3.13 ← -3.14 ← 分水嶺 ← 3.14 ← 3.13

という具合です。

条件式で右回りを判別する場合、加算しているかにくわえて、分水嶺の部分も判別しておく必要があります。

if rotation.y > recentRotation.y || (recentRotation.y > 3 && rotation.y < 0) {
// 現在のラジアンが以前のラジアンより大きい、または、以前のラジアンが3以上で現在のラジアンがマイナス値の場合、右回り
    print("turning right!")
}

分水嶺を越えてしまえば、以降右回りは加算、左回りは減算なのは同じです。


Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoogleスプレッドシートとFastlaneでApp Store説明文を自動更新する方法

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動肩揉み機 血流改善にどうぞ  ぜひ使ってください。  これはとても健康的なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto Shoulder massager Please improve blood flow. Please use it. It's a very healthy app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分ラクになりました。

ぜひ、同じくお困りの方はお試しください。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】FastlaneとGoogleスプレッドシートでApp Store説明文の更新を自動化してラクする手順を書いておく。

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、FastlaneとGoogleスプレッドシートを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動マッスィーン 魁、全自動  ぜひ使ってください。  これはとても便利なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto machine Let's Full Auto Please use it. It's a very useful app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。

ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化してラクする手順を書いておく。

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動マッスィーン 魁、全自動  ぜひ使ってください。  これはとても便利なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto machine Let's Full Auto Please use it. It's a very useful app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。

ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化してラクする手順を書いておく

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動マッスィーン 魁、全自動  ぜひ使ってください。  これはとても便利なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto machine Let's Full Auto Please use it. It's a very useful app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。

ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化してラクする手順を書いておく

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動マッスィーン 魁、全自動  ぜひ使ってください。  これはとても便利なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto machine Let's Full Auto Please use it. It's a very useful app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。

ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化する手順を書いておく

大変なことはなるだけ機械に任せたい。

当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。

これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。

たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!

というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。

1. 用意するもの

2. App Store説明文を入力したGoogleスプレッドシートを作る

今回は、この内容のスプレッドシートからApp Storeの文言を取得する例として書きます。

言語別にシートは分けてください(シート名は、日本語は「ja」、英語は「en-US」とします)

日本語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
全自動肩揉み機 血流改善にどうぞ  ぜひ使ってください。  これはとても健康的なアプリです。 軽微な修正を施しました 全自動、 マッスィーン

英語

アプリ名 サブタイトル プロモーション 詳細 アップデート内容 キーワード
Full Auto Shoulder massager Please improve blood flow. Please use it. It's a very healthy app.  bugfix Full Auto、 Let's

Screen_Shot_2020-09-08_at_18_52_32.png

3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する

Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。

そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。

Google Cloud Platform にアクセスしてプロジェクトを作りましょう。

Screen_Shot_2020-09-04_at_14_28_51.png

APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。

Screen_Shot_2020-09-04_at_14_29_56.png

名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_32_46.png

サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_33_07.png

完了ボタンをクリックします。

Screen_Shot_2020-09-04_at_14_37_24.png

サービスアカウントの欄に新たな項目が追加されていますね。クリックします。

Screen_Shot_2020-09-08_at_19_03_24.png

鍵を追加をクリックして、新しい鍵の作成をクリックします。

Screen_Shot_2020-09-08_at_19_03_48.png

このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。

Screen_Shot_2020-09-08_at_19_04_03.png

あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。

Screen Shot 2020-09-08 at 19.12.03.png

4. 独自Fastlane Actionを作る

Fastfileに書いてしまっても良いのですが、Fastfileを肥大化させないためにも、独自Actionを作って処理を分けます。

以下のコマンドを叩きます。

bundle exec fastlane new action

名前を聞かれるので適当な名前を当てます。 metadata とでもしておきましょうか。

[20:28:11]: Name of your action: metadata

fastlane/action/metadata.rb というファイルが生成されます。

5. 「google-drive-Ruby」を使って、GoogleスプレッドシートからApp Store説明文を読み込む

下準備

Googleスプレッドシートを読み込むために、google-drive-rubyというgemを使います。

Gemfile に以下を追加します。

gem "google_drive"

インストール。

bundle install

fastlane/action/ に先ほどGoogle Cloud Platformで生成したサービスアカウントキー(config.json)を置きます(サービスアカウントキーを置きたくない方はここへジャンプ

コードを書く

fastlane/action/metadata.rb の先頭に、以下を追加します。

require "google_drive"

self.run(params) に以下のコードを書いていきます。
冒頭に書いたスプレッドシートの内容を以下の定数に指定します。

LANGUAGES = ["ja", "en-US"]
COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

サービスアカウントキーファイルのパスとスプレッドシートIDを指定して、スプレッドシートを読み込みます。

session = GoogleDrive::Session.from_config("config.json")
spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

あとは、各言語別のシートの最終行から各カラムごとのテキストを引っ張って、各テキストファイルに保存するだけ。

LANGUAGES.each do |language|
  spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
    File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
  end
end

コード全文

require "google_drive"

module Fastlane
  module Actions
    class MetadataAction < Action
      def self.run(params)
        LANGUAGES = ["ja", "en-US"]
        COLUMNS = ["name", "subtitle", "promotional_text", "description", "release_notes", "keywords"]

        session = GoogleDrive::Session.from_config("config.json")
        spreadsheet = session.spreadsheet_by_key("スプレッドシートのID")

        LANGUAGES.each do |language|
          spreadsheet.worksheet_by_title(language).rows.last.each_with_index do |text, i|
            File.open("#{FastlaneCore::FastlaneFolder.path}metadata/#{language}/#{COLUMNS[i]}.txt", mode = "wb") do |f| f.write(text) end
          end
        end
      end

      def self.description
        "A short description with <= 80 characters of what this action does"
      end

      def self.details
        "You can use this action to do cool things..."
      end

      def self.available_options
        []
      end

      def self.authors
        ["Your GitHub/Twitter Name"]
      end

      def self.is_supported?(platform)
        platform == :ios
      end
    end
  end
end

6. Fastfileで実行

あとは、Fastfile上でmetadataを実行すれば、各テキストファイルにGoogleスプレッドシートから読み込んだ項目を保存します。

その次に、deliver(skip_metadata: false) を実行すればApp Storeの説明文を更新してくれます。

lane :deploy_appstore do
  metadata
  deliver(skip_metadata: false)
end

ちなみに、diffが出たらプルリクエストを作るようにすれば、前のバージョンの違いが一目瞭然となるのでおすすめです。

7. Fastlane Pluginにして公開してみた

ここまで説明して何ですが、Fastlane Pluginにしてみました。

他にも同じ目的のプラグインはありましたが、 これは、サービスアカウントキーファイルをGit管理したくなかったので環境変数で指定できるようにしています。 あと、deliverがmetadata更新に使用していないテキストファイル名をcolumsに指定すれば、そこのカラムは無視する様にもしています。

こんな感じで使えます。

fetch_metadata_from_google_sheets(
  languages: ["ja", "en-US"],
  columns: ["version", "name", "subtitle", "release_notes", "promotional_text", "description", "keywords"],
  spreadsheet_id: ENV["TEST_APP_STORE_METADATA_SPREADSHEET_ID"],
  project_id: ENV["TEST_GCP_PROJECT_ID"],
  service_account_private_key_id: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY_ID"],
  service_account_private_key: ENV["TEST_GCP_SERVICE_ACCOUNT_PRIVATE_KEY"],
  service_account_client_email: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_EMAIL"],
  service_account_client_id: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_ID"],
  service_account_auth_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_URI"],
  service_account_token_uri: ENV["TEST_GCP_SERVICE_ACCOUNT_TOKEN_URI"],
  service_account_auth_provider_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"],
  service_account_client_x509_cert_url: ENV["TEST_GCP_SERVICE_ACCOUNT_CLIENT_X509_CERT_URL"]
)

詳しくは、以下のリボジトリを参考にしてください。

kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets
https://github.com/kurarararara/fastlane-plugin-fetch_metadata_from_google_sheets

8. さいごに

これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分ラクになりました。

ぜひ、同じくお困りの方はお試しください。

こんな記事もどうぞ。

GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む