- 投稿日:2020-08-02T22:57:45+09:00
[初学者向け]cocoapodsの導入方法
cocoapodsとは
複雑な機能をpod(ライブラリ)を使って簡単に機能を実装する手段。
podのインストール方法
①下記コードで
cocoapods
をインストール。ターミナル.$ sudo gem install cocoapods②①のダウンロードを終えたら
pod setup
で設定。ターミナル.$ pod setup③podをダウンロードしたいアプリのディレクトリへ移動。
ターミナル.$ cd アプリ名④
pod init
を実行してアプリ内にpodfile
を作成。ターミナル.$ pod init⑤
podfile
を編集してpod
をインストール(pod install
)する。ターミナル.$ pod install
podfile
の編集例。podfire.pod 'SwiftyJSON' pod 'Alamofire' pod 'SDWebImage'最後に
cocoapodsの導入方法についてまとめました。
参考にして下さい!
- 投稿日:2020-08-02T22:52:30+09:00
【iOS13】Vision.frameworkの文字認識(OCR)で遊んでみる
はじめに
Vision.framework とは
iOS11で登場した、画像解析のApple標準フレームワークです。
画像処理に関する研究分野のことを「コンピュータビジョン(computer vision)」と言いますが、Vision.framework
という名称はそれに由来しています。
同じくiOS11から追加された機械学習フレームワークのCore ML
が内部で使われています。Vision - Apple Developer Documentation
iOS13でのアップデートについて
従来の
Vision.framework
では、文字と認識されていた部分は矩形の画像領域と判定されていましたが、iOS13ではその部分をテキストデータとして取得できるようになったようです。自分はiOS11の頃の
Vision.framework
は触ったことがありませんが、当時の記事を拝見する限り、ハマリポイントも減り、簡単に実装できるようになったのではないかと思います(実際、100行にも満たないコードで実装が完了しました)。WWDC 2019の動画が分かりやすいです。
Text Recognition in Vision Framework - WWDC 2019 - Videos - Apple Developer実装
公式サンプルコードをもとにテストアプリを作ってみます。
サンプルコード / Locating and Displaying Recognized Text on a Documentボタンを押したらスキャン用カメラが起動し、画像から検出したテキストデータを画面に表示する簡単なアプリです。
※カメラ機能はシュミレーターでは使えないので、実機ビルドできる環境でお試しください画面を作る
UIButton
とUITextView
だけのシンプルな画面です。
適当なAutoLayoutをつけて、UIButton
はIBOutlet、UITextView
はIBActionで繋げておきます。
コードを書く
まずインポートを記述します。
ViewControllerimport Vision import VisionKit
VNRecognizeTextRequest
をセットアップするメソッドを実装し、viewDidLoad()
で呼び出します。
VNRecognizeTextRequest
で画像から検出したテキスト情報を受け取り、文字列として連結recognitionLevel
で「文字認識のレベル」を.accurate
に設定
.fast
と.accurate
の2つの選択肢がある
.fast
は動画などのリアルタイム読み込みに向いており、速い代わりに文字認識の精度は低め.accurate
は非同期での読み込みに向いており、若干時間はかかるが、筆記体なども正しく認識できるほど精度が高いViewControlleroverride func viewDidLoad() { super.viewDidLoad() setupVision() } // Setup Vision request as the request can be reused func setupVision() { let textRecognitionRequest = VNRecognizeTextRequest { request, _ in guard let observations = request.results as? [VNRecognizedTextObservation] else { print("The observations are of an unexpected type.") return } // 解析結果の文字列を連結する let maximumCandidates = 1 for observation in observations { guard let candidate = observation.topCandidates(maximumCandidates).first else { continue } self.resultingText += candidate.string + "\n" } } // 文字認識のレベルを設定 textRecognitionRequest.recognitionLevel = .accurate self.requests = [textRecognitionRequest] }ボタンが押されたときにスキャン用のカメラが起動するように実装します。
ViewController@IBAction func cameraButtonTapped(_ sender: UIButton) { let documentCameraViewController = VNDocumentCameraViewController() documentCameraViewController.delegate = self present(documentCameraViewController, animated: true) }
VNDocumentCameraViewController
のデリゲートメソッドdocumentCameraViewController(_:didFinishWith:)
を実装します。
これはカメラでスキャン用画像の保存に成功したときに呼ばれます。
- 非同期でリクエストを実行
- メインスレッドで
textView
に検出したテキスト情報を表示するViewControllerextension ViewController: VNDocumentCameraViewControllerDelegate { // DocumentCamera で画像の保存に成功したときに呼ばれる func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) { controller.dismiss(animated: true) // Dispatch queue to perform Vision requests. let textRecognitionWorkQueue = DispatchQueue(label: "TextRecognitionQueue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem) textRecognitionWorkQueue.async { self.resultingText = "" for pageIndex in 0 ..< scan.pageCount { let image = scan.imageOfPage(at: pageIndex) if let cgImage = image.cgImage { let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:]) do { try requestHandler.perform(self.requests) } catch { print(error) } } } DispatchQueue.main.async(execute: { // textViewに表示する self.textView.text = self.resultingText }) } } }最後に、
info.plist
にPrivacy - Camera Usage Description
をKeyとして追加します。
Valueには文字認識のためにカメラを使用します
など適当な文言を入れます。実機ビルドに成功したら、文字認識で遊ぶ準備完了です
おまけ
下記メソッドを
viewDidLoad()
などで呼び出すことで、文字認識がサポートされている言語が配列で取得できます。
現状日本語はサポートされておらず、英数字だけが対応しているようです。ViewController// 文字認識できる言語の取得 private func getSupportedRecognitionLanguages() { let accurate = try! VNRecognizeTextRequest.supportedRecognitionLanguages(for: .accurate, revision: VNRecognizeTextRequestRevision1) print(accurate) // ["en-US"] }GitHub
ここで書いたコードはGitHubに上げています。
https://github.com/orimomo/VisionFrameworkTest遊んでみる
ティッシュ
まずは目についたティッシュをパシャリ (勝手に文字領域を認識してくれて賢い)
まずはお手並み拝見です。
ANKERケーブルの箱の裏
続いてANKERケーブルの箱の裏をパシャリ
文量が多めですが、どうでしょうか?
↓↓解析結果
TextView
が小さくて全部お見せできないのが残念ですが、ちゃんと最後まで認識していました!
箱に書いてある文字が小さいので、半角スペースを読み取れていない箇所が稀にあるものの、ほぼ と言える精度です。Googleサイト
最後はGoogleサイトをパシャリ
これまでのような印刷物ではなく、また日本語も含まれていますね。結果はいかに!
↓↓解析結果
日本語部分はサポートされていないので当然だめでしたが、英語部分は問題なく認識できました!おわりに
簡単なコードだけで文字認識が実現できたのは驚きでした。
日本語がサポートされれば活用の幅がぐっと広がると思うので、今後の拡張に期待したいと思います参考記事
- 投稿日:2020-08-02T21:51:38+09:00
Ctrl + ドラッグが出来ない、Mac初心者のあなたへ。
ViewController.swiftと、紐付けができない事件
Qiita初投稿です㊗︎。
内容は陳腐です。結論
Ctrlキーを押さず、commandキーを押していました。以上。
MacにはCtrlないのかと思ってた。
WindowsのCtrl = MacのCommand って、よく聞くし。
Could not insert new outlet connection"上記エラーの方は、このサイトを参考に。
上記エラーが出たわけでもなく、ただ何も反応しないだけの方は、
キーボードの位置を今一度確認してくださいまし。割と大きな文字で『control』って書かれたキーが、左隅に潜んでいます。
まとめ
それでもMacは、美しい。
- 投稿日:2020-08-02T21:30:33+09:00
Ctrl + ドラッグが出来ない、Mac初心者のあなたへ。
ViewController.swiftと、紐付けができない事件
Qiita初投稿です㊗︎。
内容は陳腐です。結論
Ctrlキーを押さず、commandキーを押していました。以上。
MacにはCtrlないのかと思ってた。
WindowsのCtrl = MacのCommand って、よく聞くし。
Could not insert new outlet connection"上記エラーの方は、このサイトを参考に。
上記エラーが出たわけでもなく、ただ何も反応しないだけの方は、
キーボードの位置を今一度確認してくださいまし。割と大きな文字で『control』って書かれたキーが、左隅に潜んでいます。
まとめ
それでもMacは、美しい。
- 投稿日:2020-08-02T19:35:38+09:00
【Swift】オプショナル型とnilの基本
はじめに
今回はSwiftを書いていくうえでとても重要な、オプショナル型の基本について解説します。
オプショナル型とnilとは
Swiftでは扱うべき値が存在しないことを表すために、nilという特別な値が存在しています。変数や定数、関数の返り値、式の評価結果や、通常の値だけでなく未設定やエラーなどの状態を表すことがある場合、その値をnilで表現します。
例えば整数の場合、変数や式の型はInt型ですが、通常はInt型の値を持ち、特殊な場合にnilを値として持つことがある変数や式は、Int?型で扱います。これをオプショナルInt型と呼びます。次の例を見てください。var a : Int = 0 var b : Int? = 10 b = nil //代入可能 a = nil //エラーこのように、int型は整数以外の値を保持できませんが、オプショナルint型は整数以外に値nilを保持することができます。
なおオプショナル型は、データ型のイニシャライザの返り値にも使われます。ここではInt型イニシャライザの、文字列を引数とするイニシャライザの例を以下に示します。
let olympic = "2020" var year : Int? = Int(olympic) //2020が返される var city : Int? = Int("Tokyo") //整数として評価できないので、nilが返されるこのように引数の文字列を整数として評価した値を返しますが、整数として評価できなかった場合はnilを返すため、このイニシャライザの返り値を受け取る変数の型はInt?である必要があります。
オプショナル型の値を開示する
オプショナルInt型(Int?型)の値は整数かnilですが、型がInt型ではないので、そのままInt型に代入することも、Int型として式の中で演算することもできません。そのためにオプショナル型からデータを取り出す、すなわちInt?型からInt型を取り出す必要があります。このことを、ここでは開示と呼ぶこととします。開示のためにはオプショナル型に対して「!」という記号を使います。具体的な例を以下に示します。
let year : Int? = Int("2020") let next : Int = year! + 4 //開示指定(!)が必要この例ではyearの後ろに!がないとコンパイル時にエラーとなります。では、オプショナル型の変数の値がnilだった場合はどうなるのでしょうか。
let year : Int? = Int("令和20年") //yearの値はnil let next : Int = year! + 4 //実行時エラーになるこの場合にはInt型の値を取り出すことができないので、コンパイルはできますが実行時にエラーになります。また、開示さえすれば復合代入演算子を使うこともできます。
条件判定
オプショナル型は開示しようとしても、nilが格納されているとエラーが発生してしまいます。そこで、開示前にどんな値が格納されているのか確認する方法として、比較の演算子「==」や「!=」を利用することができます。この際に開示は必要ありません。以下に例を示します。
var nagano : Int? = Int("1998") if nagano != nil { //真(ここでは開示指定は使わない) print("Nagano: \(nagano!)") //ここで開示指定が必要 } if nagano == 2020 { //偽(ここでは開示指定は使わない) print(2020) }変数または定数をnilと比較したとき、nilなのかどうかが調べられます。値と比較すると、nilではなく、かつその値かどうかが調べられます。
終わりに
今回はSwiftを書く上で非常に重要な、オプショナル型の基本について解説しました。オプショナル型についてはまだまだ大切な使い方があるので、また今度オプショナル型に関した記事を書こうと思います。オプショナル型はすごく大切な知識なので、ぜひ身に着けてください。
- 投稿日:2020-08-02T19:06:42+09:00
Custom URL Schemeをstructにマッピングしたい&どこが悪いかすぐ知りたい
URLをstructにマッピングしたい
Custom URL SchemeをURLで受け取った場合、そのままでは使いにくいため、structなどにマッピングしたくなります。そのときの以下の要望を叶える部品が欲しくなったので作ってみました。
- 簡単かつできるだけType Safeにマッピングしたい
- URLに問題がありマッピングに失敗した場合、その原因を知りたい
部品を使ったマッピングと生成の例
myscheme://host/openArticle?title=title-string&url=https://google.com/&mode=0
を以下にマッピングする場合を考えます。struct Article { var title: String var url: URL } enum Mode: Int { case safari = 0 case webView = 1 } struct OpenArticle { var article: Article var mode: Mode }以下のように
URLQueryKeys
を用意しURLComponentsCompatible
に対応させて…extension URLQueryKeys { static let title: URLQueryKey<String> = .init("title") static let url: URLQueryKey<URL> = .init("url") static let mode: URLQueryKey<Mode> = .init("mode") } struct Article: URLComponentsCompatible { var title: String var url: URL init(urlComponents: URLComponents) throws { title = try urlComponents.queryValue(key: .title) url = try urlComponents.queryValue(key: .url) } } enum Mode: Int, Codable, URLQueryValueCompatible { case safari = 0 case webView = 1 } struct OpenArticle: URLComponentsCompatible { var article: Article var mode: Mode init(urlComponents: URLComponents) throws { guard urlComponents.path == "/openArticle" else { throw URLComponentsCompatibleError.incompatible(description: nil) } article = try Article(urlComponents: urlComponents) mode = try urlComponents.queryValue(key: .mode) } }実際に生成するときは以下のように呼び出します。
// urlComponentsは // myscheme://host/openArticle?title=title-string&url=https://google.com/&mode=0 do { try OpenArticle(urlComponents: urlComponents) } catch { print(error) }例えば
url
が存在しないURLだった場合// urlComponentsは // myscheme://host/openArticle?title=title-string&mode=0 do { try OpenArticle(urlComponents: urlComponents) } catch { print(error) // queryItemNotFound(name: "url") }
queryItemNotFound(name: "url")
が投げられてurlが足りないことがすぐにわかります。JSONをDecodableでstructにマッピングするくらい簡単にできると一番嬉しいのですが、これでも十分に役に立つかなと思います。
上記例では一部機能しか使っていませんが、以下の処理に対応しています。
- queryの型として
Int
Double
Float
Bool
String
Codable
Optional
に対応- queryの型を追加可能
URLQueryKey
に(String?) throws -> Value
を渡し独自の変換処理が可能(100以下のInt
など)- queryが存在しない場合エラーを投げるか投げないか選択可能
- queryの文字列のフォーマットが異なる場合エラーを投げる
マッピングのための部品のコード
省略なしのコードはCustom URL Schemeをstructにマッピングする。 · GitHubを参照してください。
URLQueryItemのvalueを特定の型に変換する
URLQueryItemのvalueは
String?
型です。まずString?
を各種型に変換します。// URLQueryValue public enum URLQueryValueCompatibleError: Error { case none // nil case empty // isEmpty case format // 期待した書式ではない } public protocol URLQueryValueCompatible { init(urlQueryValue: String?) throws } extension Int: URLQueryValueCompatible { public init(urlQueryValue: String?) throws { guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none } guard !urlQueryValue.isEmpty else { throw URLQueryValueCompatibleError.empty } guard let int = Int(urlQueryValue) else { throw URLQueryValueCompatibleError.format } self = int } } extension Optional: URLQueryValueCompatible where Wrapped: URLQueryValueCompatible { public init(urlQueryValue: String?) throws { do { self = try Wrapped(urlQueryValue: urlQueryValue) } catch let error as URLQueryValueCompatibleError { switch error { case .none, .empty: self = .none case .format: throw error } } } } extension URLQueryValueCompatible where Self: Codable { public init(urlQueryValue: String?) throws { guard let urlQueryValue = urlQueryValue else { throw URLQueryValueCompatibleError.none } guard !urlQueryValue.isEmpty else { throw URLQueryValueCompatibleError.empty } guard let urlQueryData = urlQueryValue.data(using: .utf8) else { throw URLQueryValueCompatibleError.format } self = try JSONDecoder().decode(Self.self, from: urlQueryData) } }独自の型に
URLQueryValueCompatible
を実装することもできます。URLQueryItemのnameと型を紐付ける
URLQueryItemの
var name: String
と実際に使用したい型を紐付けるURLQueryKeys
を用意します。// URLQueryKey public class URLQueryKeys { public init() {} } public class URLQueryKey<Value>: URLQueryKeys { public typealias Converter = (String?) throws -> Value public var name: String public var converter: Converter public init(_ name: String, converter: @escaping Converter) { self.name = name self.converter = converter super.init() } }
Value
がURLQueryValueCompatible
である場合は簡単に生成できるようにします。extension URLQueryKey where Value: URLQueryValueCompatible { public convenience init(_ name: String) { self.init(name, converter: { source in try Value(urlQueryValue: source) }) } }queryの値を取り出す
おそらく一番面倒なのが
[URLQueryItem]
から値を取り出す部分です。
これをできるだけ簡単に取り出せるようにしましょう。// URLComponents public enum URLComponentsCompatibleError: Error { case notURL case incompatible(description: String?) case queryItemNotFound(name: String) case queryValue(name: String, error: Error) } extension URLComponents { public func queryValue<Value>(key: URLQueryKey<Value>) throws -> Value { if let queryItem = queryItems?.first(where: { $0.name == key.name }) { do { return try key.converter(queryItem.value) } catch { throw URLComponentsCompatibleError.queryValue(name: key.name, error: error) } } else { throw URLComponentsCompatibleError.queryItemNotFound(name: key.name) } } public func queryValue<Value>(ifContainsKey key: URLQueryKey<Value>) throws -> Value? { do { return try queryValue(key: key) } catch URLComponentsCompatibleError.queryItemNotFound(_) { return nil } catch { throw error } } public func queryValue<Value>(ifContainsKey key: URLQueryKey<Value?>) throws -> Value? { do { return try queryValue(key: key) } catch URLComponentsCompatibleError.queryItemNotFound(_) { return nil } catch { throw error } } }QueryItemが必ず必要な
key
版とと、QueryItemがなくてもいいifContainsKey
版があります。
またOptionalが二重にならないようにする対策でifContainsKey
は2種類メソッドがあります。URLComponentsからマッピングできる場合の目印
// URLComponentsCompatible public protocol URLComponentsCompatible { init(urlComponents: URLComponents) throws }これは主にわかりやすさのためにつけます。
- 投稿日:2020-08-02T09:23:37+09:00
Objective-Cクラスを継承したSwiftクラスで、継承元のメンバ変数を扱う方法
はじめに
古くから存在するアプリでは、Objective-CとSwiftが共存していることがよくあります。はじめはObjective-Cで作られ、Swiftが発表された後はSwiftで作っている、というパターンですね。こういったアプリでは、Objective-Cで書かれたクラスをSwiftで継承する、という実装を行う場面が出てくると思います。そのときのメンバ変数の取り扱いで少し手間取ったので、備忘録として残します。
サンプルファイル
以下に、この記事で扱うファイルを記載します。
継承元のObjective-Cファイル
継承元のクラスが書かれたファイルは以下であるとします。
SampleParentViewController.h#import <UIKit/UIKit.h> @protocol SampleDelegate - (void) delegateMethod; @end @interface SampleParentViewController: UIViewController { id<SampleDelegate> sampleDelegate; } - (instancetype)initWithDelegate:(id<SampleDelegate>)delegate; @endSampleParentViewController.m#import "SampleParentViewController.h" @implementation SampleParentViewController - (instancetype)initWithDelegate:(id<SampleDelegate>)delegate { self = [super init]; if (self) { sampleDelegate = delegate; } return self; } - (void)viewDidLoad { [super viewDidLoad]; [sampleDelegate delegateMethod]; } @end継承先のSwiftファイル
以下のように、継承するクラスをSwiftで書いたとします。
SampleChildViewController.swiftimport UIKit class SampleChildViewController: SampleParentViewController { let otherViewController: SampleDelegate = OtherViewController() override func viewDidLoad() { self.sampleDelegate = otherViewController super.viewDidLoad() } }しかし、このSwiftファイルをつくるとコンパイルエラーになります。以下で、このファイルの実装するための変更手順を示していきます。
変更手順
段階を追って、意図通りに動くための手順を示していきます。
プロパティを追加
まずこのsampleDelegateはメンバ変数として定義されているため、クラスの外から参照することはできません。参照できるようにするには、メンバ変数ではなくプロパティに変更する必要があります。そのために、まずはヘッダファイルにプロパティの宣言を追加します。
SampleParentViewController.h#import <UIKit/UIKit.h> @protocol SampleDelegate - (void) delegateMethod; @end @interface SampleParentViewController: UIViewController { id<SampleDelegate> sampleDelegate; } @property id<SampleDelegate> sampleDelegate; - (instancetype)initWithDelegate:(id<SampleDelegate>)delegate; @end一応、これだけでコンパイルエラーは解消します。しかし、実行してみるとotherViewControllerのdelegateMethodは実行されず、sampleDelegateは書き換えられていないことがわかります。これはヘッダファイルで新しくプロパティを宣言し変数を追加しましたが、もともとのsampleDelegateには何の変更も加わっていないためです。
Objective-Cのプロパティ
Objective-Cでは、ヘッダファイルでプロパティを宣言すると、メソッドファイルでは頭に_がついて扱われます。そのため現時点では、もともと存在していたsampleDelegateと新しく追加した_sampleDelegateが存在していることになっています。今回はsampleDelegateをプロパティに置き換えるのが目的なので、メソッドファイルに書かれているsampleDelegateを_sampleDelegateに書き換えていきます。
SampleParentViewController.m#import "SampleParentViewController.h" @implementation SampleParentViewController - (instancetype)initWithDelegate:(id<SampleDelegate>)delegate { self = [super init]; if (self) { _sampleDelegate = delegate; } return self; } - (void)viewDidLoad { [super viewDidLoad]; [_sampleDelegate delegateMethod]; } @endこれで、sampleDelegateが完全にプロパティに置き換わりました。したがって継承先のファイルからsampleDelegateが扱えるようになっています。一応、使わなくなったメンバ変数のsampleDelegateの宣言を消しておきましょう。
SampleParentViewController.h#import <UIKit/UIKit.h> @protocol SampleDelegate - (void) delegateMethod; @end @interface SampleParentViewController: UIViewController @property id<SampleDelegate> sampleDelegate; - (instancetype)initWithDelegate:(id<SampleDelegate>)delegate; @endまとめ
メンバ変数が定義されているObjective-CファイルをSwiftファイルで継承してその変数を扱うために、プロパティで宣言し直してメソッドファイルを書き換える手順を記載しました。Objective-Cはハマると解決に時間がかかる仕様が多いと思います。そんな人の助けになれると幸いです。
- 投稿日:2020-08-02T00:19:46+09:00
爆速でFlutterアプリをtestFlightに投げる手順
想定読者
- 毎週何個もアプリをデプロイする忙しいPoC屋
- 単に今時のデプロイ環境を学びたい人
方針
- アプリのコードを書き始める前に、まずfastlane環境の構築からtestFlightアップロードまでをやる
- デプロイが先!コードがあと!
前提/検証環境
- Mac OSX Catarlina
- いい感じのバージョンの
Xcode
インストール済Homebrew
インストール済み
- 公式ドキュメントを参照
fastlane match
用のリポジトリが作成済
- まだ作ってない人は @kotarella1110 さんの こちらの記事 がおすすめ!
マシンで1度だけやればOKな手順
- Homebrewのパッケージをインストール
- direnv環境の構築
- fastlane用に.envrcファイルの作成
アプリ毎にやる手順
- flutterプロジェクト作成
- fastlane initでfastlane/AppFileを生成
- fastlane matchでプロビジョニングプロファイルの生成
- Xcodeでプロビジョニングプロファイルを指定
マシンで1度だけやればOKな手順
1. Homebrewでパッケージのインストール
- コマンドラインで入れるのは二流っぽいので今時は
Brewfile
を置いてbrew bundle
$ brew tap Homebrew/bundle $ touch Brewfile $ (いい感じのエディタコマンド) Brewfile# Brewfile brew "direnv" brew "fastlane" brew "cocoapods"というBrewfileを置いて
$ brew bundle
2. direnv環境を構築
- パスワードとかデリケートな情報をうっかりお漏らししないように、かつ毎回入力するのはしんどいので
direnv
を使ってそれらをディレクトリ毎に指定できる環境変数に追い出すようにする。- すでにBrewfileでdirenv自体はインストール済みなので設定を実施
# bashの人は.bashrcに読み替えて $ echo 'eval "$(direnv hook $SHELL)"' >> ~/.zshrc3. fastlane用に.envrcファイルの作成
.envrc
にそのフォルダでだけ使える環境変数を閉じ込める- 超セキュアな情報なのでコミットしないように注意
$ touch .envrc $ (いいかんじのエディタコマンド) .envrc# .envrc export FASTLANE_PASSWORD={AppleIDのパスワード} export MATCH_USERNAME={AppleID(メールアドレス)} export PRODUCE_USERNAME={同上} export MATCH_PASSWORD={fastlane matchのパスワード(リポジトリを複合するためのパス)} export MATCH_GIT_URL={fastlane match用ファイルが置いてあるgitのURL(だいたいGitHub)} export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD={後述}注:FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORDについて
このURLの「App用パスワード」から生成したパスワードを入れる。
これがないと、testFlightアップロードする時に二段階認証くらったりする1. アプリ毎にやる手順
1. flutterプロジェクト作成
flutter create
なりAndroid Studio
で新規作成- マシン毎の手順3で作った
.evnrc
をコピーしてきてdirevn allow
。
- 尚、書き換えるたびに実行する必要がある。
2. fastlane initでfastlane/AppFileを生成
$ cd ios $ fastlane init
fastlane initの
実態はfastlane produce- 2回アプリ名を聞かれるが、1度目は
Dev Center
へ登録するアプリ名。- 2度目の質問は
iTunes Connect
への登録名。2度目の名前は世界で唯一である必要があるので注意。3. fastlane matchでプロビジョニングプロファイルの生成
$ fastlane match appstore $ fastlane match development
- TIPS: testFlightに飛ばすだけならappstoreだけやれば良い。実機実行のためのにdevelopmentが必要。
- TIPS:
fastlane match init
は.evnrc
の環境変数に全部書いてあるので不要4. Xcodeでプロビジョニングプロファイルを指定
- プロジェクト設定のSigining Capabilityのタブ(下記画像)
- Automatically manage signingのチェックを外す
debug
にはmatch Development〜
のを指定release
にはmatch AppStore〜
を指定デプロイの時間だ!
$ fastlane beta結論
- 全然爆速じゃない。手間かかりすぎ。
補足:どうしてcocoapods/fastlaneをHomebrewからインストールするのか?
fastlane
やcocoapods
自体のバージョンは、外部の環境に依存しているので、Gemfile/Gemfile.lock等でバージョンを固定することは、他の端末でcheckout&buildをするときにトラブルの原因になりやすい。
- 同様に
aws-cli
やfirebase-cli
もHomebrew(Brewfile)のほうが良いと考えている- トラブった場合、brew upgradeで解決するほうが筋が良さそう
- 投稿日:2020-08-02T00:19:46+09:00
爆速でFlutterアプリをTestFlightに投げる環境のつくりかた
想定読者
- 毎週何個もアプリをデプロイする忙しいPoC屋
- 単に今時のデプロイ環境を学びたい人
方針
- アプリのコードを書き始める前に、まずfastlane環境の構築からTestFlightアップロードまでをやる
- デプロイが先!コードがあと!
前提/検証環境
- Mac OSX Catarlina
- いい感じのバージョンの
Xcode
インストール済Homebrew
インストール済み
- 公式ドキュメントを参照
fastlane match
用のリポジトリが作成済
- まだ作ってない人は @kotarella1110 さんの こちらの記事 がおすすめ!
マシンで1度だけやればOKな手順
- Homebrewのパッケージをインストール
direnv
環境の構築fastlane
用に.envrc
ファイルの作成アプリ毎にやる手順
- flutterプロジェクト作成
fastlane init
でfastlane/AppFileを生成fastlane match
でプロビジョニングプロファイルの生成- Xcodeでプロビジョニングプロファイルを指定
マシンで1度だけやればOKな手順
1. Homebrewでパッケージのインストール
- コマンドラインで入れるのは二流っぽいので今時は
Brewfile
を置いてbrew bundle
$ brew tap Homebrew/bundle $ touch Brewfile $ (いい感じのエディタコマンド) Brewfile# Brewfile brew "direnv" brew "fastlane" brew "cocoapods"というBrewfileを置いて
$ brew bundle
2. direnv環境を構築
- パスワードとかデリケートな情報をうっかりお漏らししないように、かつ毎回入力するのはしんどいので
direnv
を使ってそれらをディレクトリ毎に指定できる環境変数に追い出すようにする。- すでにBrewfileからdirenv自体はインストール済みなので設定を実施
# bashの人は.bashrcに読み替えて $ echo 'eval "$(direnv hook $SHELL)"' >> ~/.zshrc3. fastlane用に.envrcファイルの作成
.envrc
にそのフォルダでだけ使える環境変数を閉じ込める- 超セキュアな情報なのでコミットしないように注意
$ touch .envrc $ (いいかんじのエディタコマンド) .envrc# .envrc export FASTLANE_PASSWORD={AppleIDのパスワード} export MATCH_USERNAME={AppleID(メールアドレス)} export PRODUCE_USERNAME={同上} export MATCH_PASSWORD={fastlane matchのパスワード(リポジトリを複合するためのパス)} export MATCH_GIT_URL={fastlane match用ファイルが置いてあるgitのURL(だいたいGitHub)} export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD={後述}注:FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORDについて
このURLの「App用パスワード」から生成したパスワードを入れる。
これがないと、TestFlightにアップロードする時に二段階認証くらったりする1. アプリ毎にやる手順
1. flutterプロジェクト作成
flutter create
なりAndroid Studio
で新規作成- マシン毎の手順3で作った
.evnrc
をコピーしてきてdirevn allow
。
- 尚、書き換えるたびに実行する必要がある。
$ flutter create {アプリ名} $ cd {アプリ名} $ cp ~/.envrc ./ # .evnrcをどっかから持ってくる $ direnv allow # 忘れずに2. fastlane initでfastlane/AppFileを生成
$ cd ios $ fastlane init
fastlane initの
実態はfastlane produce- 2回アプリ名を聞かれるが、1度目は
Dev Center
へ登録するアプリ名。- 2度目の質問は
iTunes Connect
への登録名。2度目の名前は世界で唯一である必要があるので注意。3. fastlane matchでプロビジョニングプロファイルの生成
$ fastlane match appstore $ fastlane match development
- TIPS: testFlightに飛ばすだけならappstoreだけやれば良い。実機実行のためのにdevelopmentが必要。
- TIPS:
fastlane match init
は.evnrc
の環境変数に全部書いてあるので不要4. Xcodeでプロビジョニングプロファイルを指定
- プロジェクト設定のSigining Capabilityのタブ(下記画像)
- Automatically manage signingのチェックを外す
debug
にはmatch Development〜
のプロビジョニングプロファイルを指定release
にはmatch AppStore〜
のプロビジョニングプロファイルを指定デプロイの時間だ!
$ fastlane beta結論
- 全然爆速じゃない。手間かかりすぎ。
補足:どうしてcocoapods/fastlaneをHomebrewからインストールするのか?
fastlane
やcocoapods
自体のバージョンは、外部の環境に依存しているので、Gemfile/Gemfile.lock等でバージョンを固定することは、他の端末でcheckout&buildをするときにトラブルの原因になりやすい。
- 同様に
aws-cli
やfirebase-cli
もHomebrew(Brewfile)のほうが良いと考えている- トラブった場合、brew upgradeで解決するほうが筋が良さそう