- 投稿日:2020-09-09T23:12:42+09:00
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 valueXib で View を生成する方法
Xib で View を生成する方法は、大きく分けて
View Class
とFile's Owner
を指定する2通りの方法があります。View Class
View Class
は xib上の View に対してクラスを指定することで直接的に xib と参照を作ることができます。この方法は UINib を使用して View をコードから初期化する場合は、全てのオブジェクトが使用できる状態になっていることが担保されますが、Storyboard
・xib
などで直接 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.
つまり、基本的に
View Class
を指定した View を使用する場合には、コードでインスタンスを生成して使用する必要があります。例えば、UITableViewCell
などは再利用性がありコードから生成するのが適切なので、 Xcode のテンプレートなどでも用意されていると考えられます。File's Owner
File's Owner
は名前の通り Xib 上の View の保持先を指定することで、@IBOutlet
などでクラスに View の参照を付けることができます。つまり、クラスと Xib で作成された View のインスタンスは別なので、nib-loading
を適切なタイミングで呼ぶことで View の全てのオブジェクトが使用可能になっていることが担保されます。これによって、コードから生成される時は、init(frame: CGRect)
で、Storyboard
・Xib
から生成される時はinit?(coder aDecoder: NSCoder)
から View を生成することができるようになります。参考
- 投稿日:2020-09-09T21:21:22+09:00
[アプリ開発]DKImageViewControllerで写真共有アプリ作ってみた
作った経緯
初めて作ったファッションアプリが途中でできなかったので、なんかCollectionView使ってやりたいなーって思いましてインスタみたいな写真共有アプリ作ろうと決心して開発しました!
参考にしたアプリはtabiori
さんですとりあえず作ってみた!
こんな感じでやろうと思ったら、、、ア〜〜〜〜〜〜!!!!
ViewControlller多くなりすぎて訳わからんくなった。。
初心者の僕はパンクしました
っていう感じで何回もプロジェクト作り直しました。インスタの写真一枚だけならスクールの教材で出来るんですが、何せConllectionView使いたかったんでタイムラインの画像を複数表示出来るようにさせたかったんですよね
問題
このアプリ開発するのめっちゃ時間かかりましたこれにスクールの大半の時間消費しましたねw
まずFirebaseですね
ライブラリの画像を選択→CollectionViewで表示→ボタン選択→Firebaseに保存したかったんですがうまくいかず諦めて、
日付とタイトル、画像を別の画面でやろうとするとすごくめんどくさくなったり、、、
いろいろうまくいきませんでした。
でライブラリから写真を複数選択しようとすると標準?のライブラリだとできないしDKImagePickerControllerの極意
http://cocoadocs.org/docsets/DKImagePickerController/3.8.1/ライブラリから選択した画像を画面遷移先にデータを送るとかファイル形式違うものに渡しかたとか、、
どうやってやんの〜〜〜って感じですCollectionViewとTableViewの組み合わせ極意
https://qiita.com/Erica_pon/items/f1c6a06e399723f32549こちらを参考にタイムライン画面でcolleCtionViewとTableView使ってモデル作ってみたけど画像表示できず開発休止しました、、、
結果
リリースはできてません!!
まだ未完成です
諦めませんねー
よかったらみてください!今回作ったリポジトリ
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
- 投稿日:2020-09-09T20:51:08+09:00
【入門】iOS アプリ開発 #8【スペシャルターゲット・得点表示】
はじめに
今回はスペシャルターゲット(くだもの類)、パックマンがそれを食べた時やゴーストを噛み付いた時の得点表示を作成していく。
イメージ図は下記の通り。スペシャルターゲット(サクランボ表示)と、それをパックマンが食べると得点が表示される。
仕様書
まずはスペシャルターゲットの仕様の確認。
表示されている間にパックマンが食べると、得点が2秒だけ表示される(仕様にはないが実機確認)。
続いてパックマンがゴーストを噛み付いた時の得点表示の仕様は以下の通り。
こちらの得点表示は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クラスを継承して作成する。
得点表示は 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行程度。
必要な役者はそろったので、次回はこれら役者のシーケンス動作を組み合わせて、ゲームとしてプレイできるものを作成していく。
- 投稿日:2020-09-09T20:41:36+09:00
CryptoKit を使って ECDSA を用いた署名を実装する
ECDSAでJWTによる電子署名を作成することがあったのでメモ
ECDSAとは
楕円曲線暗号(だえんきょくせんあんごう、Elliptic Curve Cryptography、ECC)とは、 楕円曲線 上の 離散対数問題 (EC-DLP) の困難性を安全性の根拠とする 暗号。
- 暗号化と復号とで異なる2つの鍵を使用し、暗号化の鍵を公開できるようにした公開鍵暗号
- RSA暗号 と比べて、短いデータ長で処理速度も早いが同レベル安全性が実現できるのがメリット
CryptoKit を使ってやること
CryptoKitを使って以下の手順により署名を作成していきます。
- 鍵の生成
- 電子署名の作成
全体のコードはこちらに上げてあります。
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.ECDSASignature は derRepresentation と rawRepresentation の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+sSecurity Frameworkの SecKeyCreateSignature を使う場合、こちらは ASN.1 DERフォーマット で返されるので上記のような変換が必要です。
CryptoKitを使えばsignature.rawRepresentation
だけで取得できるのですごく便利です。まとめ
CryptoKitを使うと鍵の生成や署名が1行くらいでできてしまうのでとても簡単に使えて驚きました。
ただし、CryptoKitで生成した鍵は自分で保存する必要があるのでそれを考えると生成はSecurity Frameworkの SecKeyGeneratePair をつかってもいい気がしました。
CryptoKitで生成した鍵をキーチェーンに保存する方法はAppleがSampleコードを提供しているのでこれを参考に。
Storing CryptoKit Keys in the Keychain
- 投稿日:2020-09-09T19:22:48+09:00
EXC_BAD_ACCESSの対処法【All Exceptionの設定は不要らしい。】
EXC_BAD_ACCESSとは?
通常、エラーメッセージを持たないため、デバッグするのが最も難しい種類のクラッシュ。
でも、Swiftでは非常に稀。要点
All Exception
を使わずとも、EXC_BAD_ACCESSのエラー箇所が出るらしい。
※ Xcode ver 11.7下部にエラー表示。僕の場合は、不適切な
self
が原因でした。
ご覧の通り、Navigator AreaではAll Exception
の設定はしていません。デバッグ方法
どのバージョンから上記の仕様になったか知りませんが、
少なくともXcode 9.4.1ではAll Exceptionの設定が必要だったらしい。↓【Xcode】All Exceptionの設定方法(デバッグ)
バージョンによって、
Exception Breakpoint
とか、Add Exception BreakPoint
とか。
表示は異なれど、やることは同じ。
EXC_BAD_ACCESS対処時には、これらのサイトを参考にしました。
Xcodeでデバッグ実行中にクラッシュした時に捗るブレークポイント設定
おしまい。
- 投稿日:2020-09-09T19:22:48+09:00
EXC_BAD_ACCESSの対処法
EXC_BAD_ACCESSとは?
通常、エラーメッセージを持たないため、デバッグするのが最も難しい種類のクラッシュ。
でも、Swiftでは非常に稀。要点
All Exception
を使わずとも、EXC_BAD_ACCESSのエラー箇所が出るらしい。
※ Xcode ver 11.7下部にエラー表示。僕の場合は、不適切な
self
が原因でした。
ご覧の通り、Navigator AreaではAll Exception
の設定はしていません。デバッグ方法
どのバージョンから上記の仕様になったか知りませんが、
少なくともXcode 9.4.1ではAll Exceptionの設定が必要だったらしい。↓【Xcode】All Exceptionの設定方法(デバッグ)
バージョンによって、
Exception Breakpoint
とか、Add Exception BreakPoint
とか。
表示は異なれど、やることは同じ。
EXC_BAD_ACCESS対処時には、これらのサイトを参考にしました。
Xcodeでデバッグ実行中にクラッシュした時に捗るブレークポイント設定
おしまい。
- 投稿日:2020-09-09T18:32:54+09:00
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) //92.コンピューテッドプロパティの特徴
上記の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の匿名ログインのコード2Auth.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を使いました。型の入れ子とCodingKeysstruct 構造体 { 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)」を読んでいこうと思います。
- 投稿日:2020-09-09T15:41:47+09:00
【IOU】 Object Detection の性能指標【I/U】
ユニオン交差 Intersection Over Union (IOU) が一般的なようです。
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を使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-09-09T11:47:07+09:00
[Swift]ColorをRGBで指定する
はじめに
SwiftでcolorをRGBで指定する方法を説明します。
Swiftでのcolor指定
まず通常の指定方法は
ViewController.swiftsampleView.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.swiftimport 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.swiftsampleView.backgroundColor = UIColor.rgb(red: 121, green: 162, blue: 255)
- 投稿日:2020-09-09T11:16:03+09:00
セグメンテーションモデル(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 } } }マスク画像がとれると、背景だけぼかしたり、お馴染みのセグメンテーション画像が
できます。
Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-09-09T09:52:25+09:00
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を使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-09-09T08:05:57+09:00
GoogleスプレッドシートとFastlaneでApp Store説明文を自動更新する方法
大変なことはなるだけ機械に任せたい。
当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。
これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。
たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!
というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。
1. 用意するもの
- App Store説明文を入力したGoogleスプレッドシート
- fastlane v2.158.0
- fastlaneは事前にインストールしておくこと
- google-drive-ruby v3.0.5
- Google Cloud Platformのサービスアカウントキー(JSONファイル)
- Google Drive APIを有効
- Google Spreadsheet APIを有効
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 3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する
Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。
そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。
Google Cloud Platform にアクセスしてプロジェクトを作りましょう。
APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。
名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。
サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。
完了ボタンをクリックします。
サービスアカウントの欄に新たな項目が追加されていますね。クリックします。
鍵を追加をクリックして、新しい鍵の作成をクリックします。
このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。
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はシート名を指定
- COLUMNSは左から各項目別のFastlaneのテキストファイル名を指定(各テキストファイル名はdeliverを参考に)
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 end6. 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_sheets8. さいごに
これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分ラクになりました。
ぜひ、同じくお困りの方はお試しください。
こんな記事もどうぞ。
- 投稿日:2020-09-09T08:05:57+09:00
【iOS】FastlaneとGoogleスプレッドシートでApp Store説明文の更新を自動化してラクする手順を書いておく。
大変なことはなるだけ機械に任せたい。
当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。
これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。
たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!
というわけで、うまく行ったので、FastlaneとGoogleスプレッドシートを使ってApp Store説明文の更新を自動化する手順を書いておきます。
1. 用意するもの
- App Store説明文を入力したGoogleスプレッドシート
- fastlane v2.158.0
- fastlaneは事前にインストールしておくこと
- google-drive-ruby v3.0.5
- Google Cloud Platformのサービスアカウントキー(JSONファイル)
- Google Drive APIを有効
- Google Spreadsheet APIを有効
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 3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する
Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。
そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。
Google Cloud Platform にアクセスしてプロジェクトを作りましょう。
APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。
名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。
サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。
完了ボタンをクリックします。
サービスアカウントの欄に新たな項目が追加されていますね。クリックします。
鍵を追加をクリックして、新しい鍵の作成をクリックします。
このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。
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はシート名を指定
- COLUMNSは左から各項目別のFastlaneのテキストファイル名を指定(各テキストファイル名はdeliverを参考に)
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 end6. 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_sheets8. さいごに
これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。
ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。
こんな記事もどうぞ。
- 投稿日:2020-09-09T08:05:57+09:00
【iOS】GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化してラクする手順を書いておく。
大変なことはなるだけ機械に任せたい。
当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。
これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。
たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!
というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。
1. 用意するもの
- App Store説明文を入力したGoogleスプレッドシート
- fastlane v2.158.0
- fastlaneは事前にインストールしておくこと
- google-drive-ruby v3.0.5
- Google Cloud Platformのサービスアカウントキー(JSONファイル)
- Google Drive APIを有効
- Google Spreadsheet APIを有効
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 3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する
Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。
そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。
Google Cloud Platform にアクセスしてプロジェクトを作りましょう。
APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。
名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。
サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。
完了ボタンをクリックします。
サービスアカウントの欄に新たな項目が追加されていますね。クリックします。
鍵を追加をクリックして、新しい鍵の作成をクリックします。
このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。
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はシート名を指定
- COLUMNSは左から各項目別のFastlaneのテキストファイル名を指定(各テキストファイル名はdeliverを参考に)
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 end6. 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_sheets8. さいごに
これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。
ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。
こんな記事もどうぞ。
- 投稿日:2020-09-09T08:05:57+09:00
【iOS】GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化してラクする手順を書いておく
大変なことはなるだけ機械に任せたい。
当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。
これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。
たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!
というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。
1. 用意するもの
- App Store説明文を入力したGoogleスプレッドシート
- fastlane v2.158.0
- fastlaneは事前にインストールしておくこと
- google-drive-ruby v3.0.5
- Google Cloud Platformのサービスアカウントキー(JSONファイル)
- Google Drive APIを有効
- Google Spreadsheet APIを有効
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 3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する
Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。
そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。
Google Cloud Platform にアクセスしてプロジェクトを作りましょう。
APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。
名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。
サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。
完了ボタンをクリックします。
サービスアカウントの欄に新たな項目が追加されていますね。クリックします。
鍵を追加をクリックして、新しい鍵の作成をクリックします。
このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。
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はシート名を指定
- COLUMNSは左から各項目別のFastlaneのテキストファイル名を指定(各テキストファイル名はdeliverを参考に)
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 end6. 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_sheets8. さいごに
これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。
ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。
こんな記事もどうぞ。
- 投稿日:2020-09-09T08:05:57+09:00
GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化してラクする手順を書いておく
大変なことはなるだけ機械に任せたい。
当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。
これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。
たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!
というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。
1. 用意するもの
- App Store説明文を入力したGoogleスプレッドシート
- fastlane v2.158.0
- fastlaneは事前にインストールしておくこと
- google-drive-ruby v3.0.5
- Google Cloud Platformのサービスアカウントキー(JSONファイル)
- Google Drive APIを有効
- Google Spreadsheet APIを有効
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 3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する
Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。
そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。
Google Cloud Platform にアクセスしてプロジェクトを作りましょう。
APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。
名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。
サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。
完了ボタンをクリックします。
サービスアカウントの欄に新たな項目が追加されていますね。クリックします。
鍵を追加をクリックして、新しい鍵の作成をクリックします。
このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。
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はシート名を指定
- COLUMNSは左から各項目別のFastlaneのテキストファイル名を指定(各テキストファイル名はdeliverを参考に)
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 end6. 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_sheets8. さいごに
これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分キラクになりました。
ぜひ、同じくお困りの方はお試しください。思ったよりもラク出来るかと思います。
こんな記事もどうぞ。
- 投稿日:2020-09-09T08:05:57+09:00
GoogleスプレッドシートとFastlaneでApp Store説明文の更新を自動化する手順を書いておく
大変なことはなるだけ機械に任せたい。
当たり前ですが、iOSアプリを更新するにはアップデートの内容文なども更新する必要があります。
これまで自分は、運営さんがGoogleスプレッドシートに入力してくれたアプリ文言を、一個一個、App Store Connectにコピー&ペーストしていました。
たまになら問題ないですが、多くなってくるとちと大変です。自動化してラクしたい。ラクしたい。ラクできた!
というわけで、うまく行ったので、GoogleスプレッドシートとFastlaneを使ってApp Store説明文の更新を自動化する手順を書いておきます。
1. 用意するもの
- App Store説明文を入力したGoogleスプレッドシート
- fastlane v2.158.0
- fastlaneは事前にインストールしておくこと
- google-drive-ruby v3.0.5
- Google Cloud Platformのサービスアカウントキー(JSONファイル)
- Google Drive APIを有効
- Google Spreadsheet APIを有効
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 3. Google Cloud Platformで「Google Drive API」と「Google Spreadsheet API」を使えるように設定する
Googleスプレッドシートから文言を取得するには「Google Drive API」と「Google SpreadSheet API」を叩く必要があります。
そして、これらのAPIをCIから叩くには、GCPでサービスアカウントを作成する必要があります。
Google Cloud Platform にアクセスしてプロジェクトを作りましょう。
APIとサービス>認証情報に移動して「認証情報を作成」をクリックし、サービスアカウントをクリックします。
名前、ID、サービスアカウントの説明を記入して、作成ボタンをクリックします。
サービスアカウントの権限を決めます。今回、Googleスプレッドシートを読み込たいので閲覧者に(適時、適切な権限を選んでください)して続行ボタンをクリックします。
完了ボタンをクリックします。
サービスアカウントの欄に新たな項目が追加されていますね。クリックします。
鍵を追加をクリックして、新しい鍵の作成をクリックします。
このようなダイアログが出るのでJSONを指定して作成をクリックします。
保存したJSONファイルは、config.jsonにリネームしてください。あとは、APIサービス > APIライブラリから「Google Drive API」と「Google Spreadsheet API」にを検索し、それぞれのAPIを有効にすれば設定は完了です。
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はシート名を指定
- COLUMNSは左から各項目別のFastlaneのテキストファイル名を指定(各テキストファイル名はdeliverを参考に)
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 end6. 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_sheets8. さいごに
これまでは、コピー&ペーストする度に間違いがないかハラハラしてましたが、自動更新してからは間違うこともなく随分ラクになりました。
ぜひ、同じくお困りの方はお試しください。
こんな記事もどうぞ。