20200802のiOSに関する記事は9件です。

[初学者向け]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の導入方法についてまとめました。
参考にして下さい!

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

【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

ボタンを押したらスキャン用カメラが起動し、画像から検出したテキストデータを画面に表示する簡単なアプリです。
※カメラ機能はシュミレーターでは使えないので、実機ビルドできる環境でお試しください

画面を作る

UIButtonUITextViewだけのシンプルな画面です。
適当なAutoLayoutをつけて、UIButtonはIBOutlet、UITextViewはIBActionで繋げておきます。

コードを書く

まずインポートを記述します。

ViewController
import Vision
import VisionKit

VNRecognizeTextRequestをセットアップするメソッドを実装し、viewDidLoad()で呼び出します。

  • VNRecognizeTextRequestで画像から検出したテキスト情報を受け取り、文字列として連結
  • recognitionLevelで「文字認識のレベル」を.accurateに設定
    • .fast.accurateの2つの選択肢がある
      • .fastは動画などのリアルタイム読み込みに向いており、速い代わりに文字認識の精度は低め
      • .accurateは非同期での読み込みに向いており、若干時間はかかるが、筆記体なども正しく認識できるほど精度が高い
ViewController
    override 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に検出したテキスト情報を表示する
ViewController
extension 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.plistPrivacy - Camera Usage DescriptionをKeyとして追加します。
Valueには文字認識のためにカメラを使用しますなど適当な文言を入れます。

実機ビルドに成功したら、文字認識で遊ぶ準備完了です:tada:

おまけ

下記メソッドを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

遊んでみる

ティッシュ

まずは目についたティッシュをパシャリ :camera_with_flash: (勝手に文字領域を認識してくれて賢い)
まずはお手並み拝見です。
IMG_0886.PNG

↓↓解析結果

:100: :white_flower:

ANKERケーブルの箱の裏

続いてANKERケーブルの箱の裏をパシャリ :camera_with_flash:
文量が多めですが、どうでしょうか?
IMG_0888.PNG

↓↓解析結果

TextViewが小さくて全部お見せできないのが残念ですが、ちゃんと最後まで認識していました!
箱に書いてある文字が小さいので、半角スペースを読み取れていない箇所が稀にあるものの、ほぼ :100: と言える精度です。

Googleサイト

最後はGoogleサイトをパシャリ :camera_with_flash:
これまでのような印刷物ではなく、また日本語も含まれていますね。結果はいかに!
IMG_0892.PNG

↓↓解析結果

日本語部分はサポートされていないので当然だめでしたが、英語部分は問題なく認識できました!

おわりに

簡単なコードだけで文字認識が実現できたのは驚きでした。
日本語がサポートされれば活用の幅がぐっと広がると思うので、今後の拡張に期待したいと思います:grin:

参考記事

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

Ctrl + ドラッグが出来ない、Mac初心者のあなたへ。

ViewController.swiftと、紐付けができない事件

Qiita初投稿です㊗︎。
内容は陳腐です。

結論

Ctrlキーを押さず、commandキーを押していました。以上。

MacにはCtrlないのかと思ってた。
WindowsのCtrl = MacのCommand って、よく聞くし。


Could not insert new outlet connection"

上記エラーの方は、このサイトを参考に。

上記エラーが出たわけでもなく、ただ何も反応しないだけの方は、
キーボードの位置を今一度確認してくださいまし。

割と大きな文字で『control』って書かれたキーが、左隅に潜んでいます。


まとめ

それでもMacは、美しい。

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

Ctrl + ドラッグが出来ない、Mac初心者のあなたへ。

ViewController.swiftと、紐付けができない事件

Qiita初投稿です㊗︎。
内容は陳腐です。

結論

Ctrlキーを押さず、commandキーを押していました。以上。

MacにはCtrlないのかと思ってた。
WindowsのCtrl = MacのCommand って、よく聞くし。


Could not insert new outlet connection"

上記エラーの方は、このサイトを参考に。

上記エラーが出たわけでもなく、ただ何も反応しないだけの方は、
キーボードの位置を今一度確認してくださいまし。

割と大きな文字で『control』って書かれたキーが、左隅に潜んでいます。


まとめ

それでもMacは、美しい。

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

【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を書く上で非常に重要な、オプショナル型の基本について解説しました。オプショナル型についてはまだまだ大切な使い方があるので、また今度オプショナル型に関した記事を書こうと思います。オプショナル型はすごく大切な知識なので、ぜひ身に着けてください。

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

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()
    }
}

ValueURLQueryValueCompatibleである場合は簡単に生成できるようにします。

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
}

これは主にわかりやすさのためにつけます。

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

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;

@end
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

継承先のSwiftファイル

以下のように、継承するクラスをSwiftで書いたとします。

SampleChildViewController.swift
import 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はハマると解決に時間がかかる仕様が多いと思います。そんな人の助けになれると幸いです。

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

爆速でFlutterアプリをtestFlightに投げる手順

想定読者

  • 毎週何個もアプリをデプロイする忙しいPoC屋
  • 単に今時のデプロイ環境を学びたい人

方針

  • アプリのコードを書き始める前に、まずfastlane環境の構築からtestFlightアップロードまでをやる
  • デプロイが先!コードがあと!

前提/検証環境

  • Mac OSX Catarlina
  • いい感じのバージョンのXcodeインストール済
  • Homebrewインストール済み
  • fastlane match用のリポジトリが作成済

マシンで1度だけやればOKな手順

  1. Homebrewのパッケージをインストール
  2. direnv環境の構築
  3. fastlane用に.envrcファイルの作成

アプリ毎にやる手順

  1. flutterプロジェクト作成
  2. fastlane initでfastlane/AppFileを生成
  3. fastlane matchでプロビジョニングプロファイルの生成
  4. 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)"' >> ~/.zshrc 

3. 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からインストールするのか?

  • fastlanecocoapods自体のバージョンは、外部の環境に依存しているので、Gemfile/Gemfile.lock等でバージョンを固定することは、他の端末でcheckout&buildをするときにトラブルの原因になりやすい。
    • 同様にaws-clifirebase-cliもHomebrew(Brewfile)のほうが良いと考えている
  • トラブった場合、brew upgradeで解決するほうが筋が良さそう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

爆速でFlutterアプリをTestFlightに投げる環境のつくりかた

想定読者

  • 毎週何個もアプリをデプロイする忙しいPoC屋
  • 単に今時のデプロイ環境を学びたい人

方針

  • アプリのコードを書き始める前に、まずfastlane環境の構築からTestFlightアップロードまでをやる
  • デプロイが先!コードがあと!

前提/検証環境

  • Mac OSX Catarlina
  • いい感じのバージョンのXcodeインストール済
  • Homebrewインストール済み
  • fastlane match用のリポジトリが作成済

マシンで1度だけやればOKな手順

  1. Homebrewのパッケージをインストール
  2. direnv環境の構築
  3. fastlane用に.envrcファイルの作成

アプリ毎にやる手順

  1. flutterプロジェクト作成
  2. fastlane initでfastlane/AppFileを生成
  3. fastlane matchでプロビジョニングプロファイルの生成
  4. 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)"' >> ~/.zshrc 

3. 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からインストールするのか?

  • fastlanecocoapods自体のバージョンは、外部の環境に依存しているので、Gemfile/Gemfile.lock等でバージョンを固定することは、他の端末でcheckout&buildをするときにトラブルの原因になりやすい。
    • 同様にaws-clifirebase-cliもHomebrew(Brewfile)のほうが良いと考えている
  • トラブった場合、brew upgradeで解決するほうが筋が良さそう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む