Flutter触り始め

この度、新しい仕事で必要になりそうになったので、Flutterを触り始めた。

iOSAndroidの2つのアプリを1つのソースで開発できるというのは、ReactNativeやらUnityやらいろいろな方法はあるけれど、あんまりいい印象は無い。
いい印象は無いと言うけれど、そもそもそんなに知らない。印象もなにもない。
でも結局はどこかで無理が生じて、ネイティブが一番だねってなる印象だ。あれ、印象があったぞ。

ここでは、自分がFlutterのドキュメントを読み進めていく中で理解したことを、自分なりの言葉で書き記せば、その分頭に入ってくるのではないかなと思ったので、雑に書き記していこうと思う。つまり中身はたいしてない。この記事を読んでいる人がもしいるとしたら、ここで止めておいてFlutterの公式ドキュメントを読みに行くことを強くおすすめする。

なぜなら、Flutterのドキュメント、めちゃくちゃちゃんとしているからである。

flutter.dev

チュートリアルが手厚い。その手の本を読んでいるように、手を動かしながら学べる。
そして、ちゃんと動くものを作ることができる。楽しい。楽しいは強い。

あと第一印象として、とてもとっつきやすい気がする。
これはでかい。昨今のframeworkの第一歩目の険しさたるやいなや。Hello Worldが全然Helloしてこない。
しかし、Flutterはいとも簡単にWorldがHelloしてきた。emulatorで動いたのだ。あれ、これアプリ作れるじゃんってひとまず錯覚したぞ。すごいぞGoogle。ありがとうGoogle

もちろんこれはまだ序盤も序盤で、山を登るにつれてどんどん難しくなっていくのかもしれない。
富士山をちょろっと登っただけで、「あれ?富士山てこんなものなの?登頂なんて余裕じゃん。」って調子こいてたら、ベテラン登山家にボコボコにされるだろう。
でも入山は本当に簡単だった。入山さえしてしまえば、とりあえず登ることはできる。だからとりあえず登ってみよう。途中で崖に落ちるかもしれないし、降りたくなるかもしれないけど。

そう、かの有名な登山家は言った。

「なぜ、山に登るんですか?」
「そこに、山があるからです。」

それと一緒です。

「なぜ、Flutterを触り始めたんですか?」
「仕事があるからです。」

最初に書きましたね。仕事で必要になったので触り始めたのです。
さぁ、登るぞ〜

iOSアプリ開発の証明書周りがよくわかっていないので自分なりに整理してみた

iOSアプリの証明書周り、非常にわかりにくい。

開発時に毎回どうだったっけ?って忘れてしまうので、自分なりに整理してみました。

証明書周りの構成

証明書周りの構成は4つに分類されています。

  1. App ID
  2. 証明書
  3. 端末
  4. プロビジョニングファイル

developmentとdistribution

証明書やプロビジョニングファイルには、開発時に使用するdevelopmentと、リリース時に使用するdistributionの2つの種類があります。

  • development
    • 自分のxcodeで開発をして、実機にインストールする時などはdevelopment
  • distribution
    • アプリをarchiveして配信する場合はdistribution

App ID

App IDはアプリごとに発行するIDです。

xcodeのプロジェクト内のBundle IDと、このApp IDを合わせる必要があります。

このIDに対して、そのアプリ上で使用するサービスを選択します。(プッシュ通知や、Apple Payなど)

証明書

開発時にはdevelopment用の証明書を使用して、実機にインストールしたりします。

配信用にipaファイルを作成する場合は、production用の証明書が必要です。

この証明書を作る時に、キーチェーンを使って秘密鍵やらなにやらを用意する必要があります。

証明書作成後、Appleの管理画面からは証明書自体はDownloadできますが、秘密鍵はDownloadできませんので、必ず無くさないようにする必要があります。

※アプリ配信用の証明書の他にも、Push通知用の証明書など複数種類があります。

端末

Ad Hoc版の際にインストール可能な端末を指定するために、端末を登録する必要があります。

端末の登録には端末ごとに発行されているUDIDが必要です。

端末は最大で100件まで登録が可能。

端末を指定してDisableにすることは可能だが、削除することは基本的にできない。(100件登録してしまうと、新しい端末を登録できなくなってしまう。)

1年に1度更新のタイミングで、端末を整理することができ、そのタイミングのみ端末の削除が可能になる。

プロビジョニングファイル

プロビジョニングファイルは、下記のデータにひも付き、アプリごとに発行します。

  • App ID
  • 証明書
  • 端末

プロビジョニングファイルがいろいろな情報を持っており、アプリをビルドする時にそのプロビジョニングファイルを指定してビルドさせます。

ただ、プロビジョニングファイルだけでビルドできてしまうと、セキュリティ的によろしくないため、プロビジョニングファイルに紐付いている証明書を持っていなければビルドができない、という仕様です。

また、プロビジョニングファイルが持っているApp IDと、ProjectのBundle IDが一致しないとビルドができません。

持っている情報が変わればプロビジョニングファイルも変わる

検証する端末が増えたりした時など、管理画面からプロビジョニングファイルの編集を行います。

この結果、新しいプロビジョニングファイルには新しいデータが書き込まれているため、古いプロビジョニングファイルと交換する必要があります。

よく、管理画面で端末を追加したのにインストールできない、というケースがありますが、これはビルド時のプロビジョニングファイルが変わっていない可能性が高いです。

Ad Hoc

本番の証明書を使用して、限られた端末にのみインストールできるようにするものを、Ad Hoc版といいます。

Test flightやCrashlyticsで検証用のアプリを配信する際には、Ad Hoc版のプロビジョニングファイルを作成して指定する必要があります。

ものすごいざっくりですが、概念をまとめてみました。

【vim】正規表現のマッチする範囲を指定して文字列置換をもっと簡単にする

\zs\zeを使えば、正規表現のマッチする範囲を指定できて、置換がとても楽になる。

例えば、

<div>ABCDEF</div>
<p>123456</p>
<div class="sample">abcdef</div>

という文字列があって、divタグの中身だけ変えたい時。

普通にやると、ちょっとめんどくさい正規表現を書かないと置換できません。

%s/\(<div.*>\).\+\(<\/div>\)/\1REPLACE\2/

だいぶごちゃごちゃしてます。

でもこれを、\zs\zeを使えば、こうなります。

%s/<div.*>\zs.\+\ze<\/div>/REPLACE/

マッチする部分が、\zsから\zeで囲んだ部分だけになるので、()でグループ化して後方参照させる必要がなくなります。

便利すぎる。

参考記事

僕が一番 Vim の正規表現をうまく使えるんだ(\zs \ze 編) - TIM Labs

Vimの正規表現 “\v”, “\zs”, “\ze” | blog.remora.cx

【swift】Viewの一部を角丸にする

swiftで角丸のViewを作るには、layerのcornerRadiusを設定すれば簡単にできますが、これだと4隅すべてが角丸になってしまいます。

上だけを角丸にしたい時などには、一部だけ角丸のパスをUIBezierPathで作成し、CAShapeLayerに設定します。

developer.apple.com

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .grayColor()

        // 上だけ角丸にするViewを用意 
        let view1 = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        view1.backgroundColor = .greenColor()

        view.addSubview(view1)
        view1.center = view.center

        // TopLeft, TopRightを5px丸めたパスを生成
        let maskPath = UIBezierPath(roundedRect: view1.frame, byRoundingCorners: [.TopLeft, .TopRight], cornerRadii: CGSize(width: 5, height: 5))
        
        // CAShapeLayerを生成し、先ほど生成したパスをセットする
        let maskLayer = CAShapeLayer()
        maskLayer.path = maskPath.CGPath

        // 対象のViewのlayer.maskにCAShapeLayerをセットする
        view1.layer.mask = maskLayer
    }

}

f:id:w6500:20160527002452p:plain:w300

【swift】グラデーションを描画する

swiftでグラデーションを描画するには、CAGradientLayerを使います。

developer.apple.com

let startColor = UIColor(white: 0, alpha: 0).CGColor
let endColor = UIColor(white: 0, alpha: 1).CGColor

let layer = CAGradientLayer()
layer.colors = [startColor, endColor]
layer.frame = view.bounds

view.layer.addSublayer(layer)

f:id:w6500:20160525201545p:plain:w300

グラデーションをかけたい色を複数渡してあげるだけでした。簡単。

グラデーションの方向を変えたい場合は、startPointendPointを設定します。

// 横方向
layer.startPoint = CGPoint(x: 1, y: 0.5)
layer.endPoint = CGPoint(x: 0, y: 0.5)

// 斜め
layer.startPoint = CGPoint(x: 1, y: 0)
layer.endPoint = CGPoint(x: 0, y: 1)

f:id:w6500:20160525201601p:plain:w300

f:id:w6500:20160525201618p:plain:w300

【swift】Viewに自分自身の大きさを定義して、動的なViewを正しくレイアウトする

動的に要素を並べるようなViewを作成した時に、AutoLayoutの設定の仕方で悩みました。

class CustomView: UIView {

    init() {
        super.init(frame: .zero)

        backgroundColor = .whiteColor()

        // 正方形のViewを左右に並べる
        let view1 = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        let view2 = UIView(frame: CGRect(x: 110, y: 0, width: 100, height: 100))

        view1.backgroundColor = .blueColor()
        view2.backgroundColor = .greenColor()

        addSubview(view1)
        addSubview(view2)
    }
}

このようにinitializeの中で動的に要素を作成しているViewを作成します。
このViewの中ではAutoLayoutは定義せずにCGRectで位置を設定しています。

CustomViewを表示してみる

このViewをViewControllerでaddSubviewしてあげます。
addしたCustomViewはAutoLayoutで中央揃えになるように設定します。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .grayColor()

        let customView = CustomView()
        view.addSubview(customView)
        customView.translatesAutoresizingMaskIntoConstraints = false

        // customView自体には中央揃えになるようにAutoLayoutを設定
        customView.centerYAnchor.constraintEqualToAnchor(view.layoutMarginsGuide.centerYAnchor).active = true
        customView.centerXAnchor.constraintEqualToAnchor(view.layoutMarginsGuide.centerXAnchor).active = true
    }

}

f:id:w6500:20160524110915p:plain:w300

ビルドしてみるとこんな感じです。
うまく中央揃えになりません。
また、CustomView自体の白背景部分が描画されていないため、CustomViewに大きさが無いことがわかります。

intrinsicContentSizeの設定

UIViewにはintrinsicContentSizeというCGSizeを返すメソッドが定義されており、そのサイズを自分自身のサイズと認識します。

ですので、先ほどのCustomViewにintrinsicContentSizeをoverrideさせます。

class CustomView: UIView {

    init() {
        super.init(frame: .zero)

        backgroundColor = .whiteColor()

        let view1 = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        let view2 = UIView(frame: CGRect(x: 110, y: 0, width: 100, height: 100))

        view1.backgroundColor = .blueColor()
        view2.backgroundColor = .greenColor()

        addSubview(view1)
        addSubview(view2)
    }
    
    // 追加
    override func intrinsicContentSize() -> CGSize {
        return CGSize(width: 210, height: 100)
    }
}

これでCustomView自体は、幅210px、高さ100pxの要素だと認識するようになりました。

この状態でビルドしてみると

f:id:w6500:20160524112001p:plain:w300

正しく中央揃えになり、CustomViewの白背景も描画されています。

今回の例はサイズを全て決め打ちで書いていますが、この辺りをちゃんと計算して算出するようにすれば、動的な要素も正しくレイアウトされるようになります。

【swift】ViewControllerとViewを分離させてViewControllerの記述を減らす

最近は、storyboardもxibも使わない方針で実装しているため、Viewを構築するのも全てコードで定義しています。

特に考えなければ、initializeの時に組み立ててしまうのですが、そうするとViewControllerとは関係のないコードが増えていき、可読性が悪くなります。

class ViewController: UIViewController {

    init() {
        super.init(nibName: nil, bundle: nil)

        // ここで組み立ててしまうとあっという間に行数が増えていく...
        addSubview(UIView())
        addSubview(UIView())
    }

}

その変わりに、View自体を独自クラスにしてあげて、それをViewControllerで読み込むようにすれば、Viewの組み立てを分離できます。
ViewControllerにはloadViewというメソッドが定義されているので、それをoverrideして、独自に定義したViewクラスを設定します。

class ViewController: UIViewController {
    overide func loadView() {
        view = SampleView()
    }
}

class SampleView: UIView {
    init() {
        super.init(frame: .zero)
      
        // Viewの組み立てはここでしてしまう
        addSubview(UIView())
        addSubview(UIView())       
    }
}

developer.apple.com