20200817のiOSに関する記事は16件です。

[Multiple commands produce '/Users/ユーザ名/...省略.../Info.plist']が発生した場合

問題

Info.plistを誤って削除してしまい、手動で追加し直した。
その後、エミュレータでアプリを実行すると下記のようなエラーが出るようになった。

Multiple commands produce '/Users/ユーザ名/Library/Developer/Xcode/DerivedData/Runner-hogehoge/Build/Products/Debug-iphonesimulator/Runner.app/Info.plist':
1) Target 'Runner' (project 'Runner') has copy command from ‘/Users/…省略…/Info.plist'
2) Target 'Runner' (project 'Runner') has process command with output '/Users/…省略…/Info.plist'

対応

Xcodeで下記の場所まで移動。

Runner → Build Phases → Copy Bundle Resources

Copy Bundle Resourcesに記載されているInfo.plistを削除。

以上

軽ーく調べてみたけど、手動で追加したときにファイルが重複しちゃってるみたいな感じっぽい
詳しいこと知ってる人いたら教えてください。

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

UIFeedbackGenerator を使おう

概要

UIFeedbackGenerator は UI にフィードバックを追加するための、フィードバックジェネレーターの抽象クラスで、フィードバックの種類別に下記の3つの具象クラスが存在し、それぞれ iOS10 以降で使用することが可能です。

基本的には、UIFeedbackGenerator を自身でインスタンス化することは禁止されていて、上記のサブラクスをインスタンス化してフィードバックをトリガーします。また、システムの設定やアプリケーションの状態、バッテリー残量など特定の要因によって Haptics が再生されないことがあります。下記に例を載せておきます。

  • デバイスが Taptic Engine を搭載していない場合
  • アプリがバックグラウンド状態の場合
  • システムの Haptics 設定が無効な場合

Feedback の種類

UIImpactFeedbackGenerator

最も基本的なフィードバックで、ユーザがボタンをタップしたりした時などに使用されます。強度別にスタイルが用意されており、iOS13 からはトリガーのタイミングで強度(itensity)を指定できるようになりました。詳しくはこちらをご覧ください。

    public enum FeedbackStyle : Int {

        case light

        case medium

        case heavy

        @available(iOS 13.0, *)
        case soft

        @available(iOS 13.0, *)
        case rigid
    }

UISelectionFeedbackGenerator

スライダーでの値の変更など、連続したフィードバックをしたい時に使用します。

UINotificationFeedbackGenerator

イベントの結果などによって、成功・警告・失敗など種類別にフィードバックを行いたい時に使用します。詳しくはこちらを参照してください。

    public enum FeedbackType : Int {


        case success

        case warning

        case error
    }

実際に使ってみる

3種類のフィードバックタイプをそれぞれ個別にインスタンス化して使用するのもいいですが、今回は全てのフィードバックを試してみたかったので下記のようなクラスを作成しました。

import UIKit

enum FeedbackGeneratorType {
    case impact(style: UIImpactFeedbackGenerator.FeedbackStyle)
    case notification
    case selection
    @available(iOS 13.0, *)
    case impactWithIntensity(intensity: CGFloat)
}

final class FeedbackGenerator {
    private var feedbackGenerator: UIFeedbackGenerator?
    private let type: FeedbackGeneratorType

    init(type: FeedbackGeneratorType) {
        self.type = type
    }

    // Please call this method a few seconds before triggering the feedback.
    func prepare() {
        switch type {
        case .impact(let style):
            feedbackGenerator = UIImpactFeedbackGenerator(style: style)
        case .notification:
            feedbackGenerator = UINotificationFeedbackGenerator()
        case .selection:
            feedbackGenerator = UISelectionFeedbackGenerator()
        case .impactWithIntensity:
            feedbackGenerator = UIImpactFeedbackGenerator()
        }
        feedbackGenerator?.prepare()
    }

    func releaseFeedbackEngine() {
        feedbackGenerator = nil
    }

    // MARK: - Excute Haptics methods.
    func excuteImpactFeedback(intensity: CGFloat? = nil) {
        let optionalIntensity = intensity
        guard let impactFeedbackGenerator = feedbackGenerator as? UIImpactFeedbackGenerator else { return }
        if case .impactWithIntensity(let intensity) = type, #available(iOS 13.0, *) {
            if let specificIntensity = optionalIntensity {
                impactFeedbackGenerator.impactOccurred(intensity: specificIntensity)
            } else {
                impactFeedbackGenerator.impactOccurred(intensity: intensity)
            }
        } else {
            impactFeedbackGenerator.impactOccurred()
        }
        releaseFeedbackEngine()
    }

    func excuteNotificationFeedback(notificationType: UINotificationFeedbackGenerator.FeedbackType) {
        guard let notificationFeedbackGenerator = feedbackGenerator as? UINotificationFeedbackGenerator else { return }
        notificationFeedbackGenerator.notificationOccurred(notificationType)
        releaseFeedbackEngine()
    }

    func excuteSelectionFeedback() {
        guard let selectionFeedbackGenerator = feedbackGenerator as? UISelectionFeedbackGenerator else { return }
        selectionFeedbackGenerator.selectionChanged()
        feedbackGenerator?.prepare()
    }
}

注目すべき点は2つあり、prepare() の呼び出しと、feedbackGenerator インスタンスの解放タイミングです。prepare() は Taptic Engine と呼ばれるフィードバックを再生するための振動モーターを準備中にするために呼び出します。これにより、フィードバックを再生する際にレイテンシをなくすことができます。2つ目の feedbackGenerator のインスタンス解放タイミングは、UIFeedbackGenerator がインスタンス化されると Taptic Engine が待機状態になり電力を消費するため、なるべくフィードバック毎にインスタンス を解放することが重要になってきます。また、UISelectionFeedbackGenerator に関しては連続してフィードバックを再生するのでインスタンス は解放せず、再生直後に prepare() を呼び出し、Taptic Engine を準備中にしていますので、任意のタイミングで feedbackGenerator を解放する必要があります。

使い方

UIButton の TouchDown イベントで prepare() して buttonTouchUpInside イベントでフィードバックを再生するサンプルです。

class ViewController: UIViewController {
    let feedbackGenerator1 = FeedbackGenerator(type: .impactWithIntensity(intensity: 10000))

    @IBAction func buttonTouchDown(_ sender: Any) {
        feedbackGenerator1.prepare()
    }

    @IBAction func buttonTouchUpInside(_ sender: Any) {
        feedbackGenerator1.excuteImpactFeedback()
    }

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

ask me! appのログイン画面の実装

1 この記事の内容

ask me! appでのログイン画面の実装方法について記載します。画面レイアウトは下記のとおりです。

quitta①+.jpg

2 ソースコード概要

ソースコードの概要は下記の通りです。

login.dart

  @override
  Widget build(BuildContext context) {
    return
      new GestureDetector(
      onTap:()
      {
        _focusNodePwd.unfocus();
      },
      child:

      new Scaffold(
      appBar: new AppBar(
      centerTitle: true,

        title: new Text('ask me! app',style: TextStyle(fontFamily: 'impact')),
        backgroundColor:Color.fromARGB(255,171 ,232 ,255 ),

      ),

      body:
      KeyboardActions(
      config:_buildConfig(context),
    child:

          _loading?
        bodyProgress:
        new Column(
            children: <Widget>[

              new Padding(
                padding:  EdgeInsets.fromLTRB(24.0*app_size.width/WIDTH,24.0*app_size.height/HEIGHT,24.0*app_size.width/WIDTH,24.0*app_size.height/HEIGHT),
              ),

              new Center(
                child:
                new Text(
                  "H E L L O",
                  style: new TextStyle(fontSize: 56.0*app_size.width*app_size.height/WIDTH/HEIGHT,
                      color: Color.fromARGB(255,171 ,232 ,255 ),
                      fontWeight: FontWeight.w300,
                      fontFamily: 'impact'),
                ),
              ),


              new Center(
                child:
                new Image.asset(
                  'icon/askme.png',
                  width: 230.0*app_size.width/WIDTH,
                  height: 230.0*app_size.width/WIDTH,
                ),
              ),


              new Padding(
                padding:  EdgeInsets.fromLTRB(20.0*app_size.width/WIDTH,10.0*app_size.height/HEIGHT,20.0*app_size.width/WIDTH,10.0*app_size.height/HEIGHT),
              ),


              new Container(
                  width: 300.0*app_size.width/WIDTH,
                  child:new EnsureVisibleWhenFocused(
                    focusNode: _focusNodePwd,
                    child: new TextFormField(
                      controller: passwdcontroller,
                      decoration: const InputDecoration(
                        border: const OutlineInputBorder(),
                        hintText: 'PASSWORD',
                        labelText: 'PASSWORD',
                        hintStyle: TextStyle(fontSize: 20.0,
                            color: const Color(0xFF000000),
                            fontWeight: FontWeight.w200,
                            fontFamily: 'impact'),
                        labelStyle:  TextStyle(fontSize: 20.0,
                            color: const Color(0xFF000000),
                            fontWeight: FontWeight.w200,
                            fontFamily: 'impact'),

                      ),
                      style: new TextStyle(fontSize: 20.0,
                          color: const Color(0xFF000000),
                          fontWeight: FontWeight.w200,
                          fontFamily: 'Roboto'),
                      focusNode: _focusNodePwd,
                      keyboardType: TextInputType.number,
                    ),
                  ),
              ),

              new Padding(
                padding:  EdgeInsets.fromLTRB(20.0*app_size.width/WIDTH,20.0*app_size.height/HEIGHT,20.0*app_size.width/WIDTH,20.0*app_size.height/HEIGHT),
              ),


              new Row(
                  children: <Widget>[

                    new Padding(
                      padding:  EdgeInsets.fromLTRB(app_size.width*0.1,40.0*app_size.height/HEIGHT,0.1*app_size.width,40.0*app_size.height/HEIGHT),
                    ),


                  new Container(

                    width:app_size.width*3/5,
                    child:
                    FlatButton(
                      onPressed: () {

                        buttonPressed();
                      },
                      color: Color.fromARGB(255,171 ,232 ,255 ),
                      child: Text(
                        "    OK    ",
                        style: new TextStyle(fontSize: 50.0,
                            fontWeight: FontWeight.w200,
                            fontFamily: 'impact'
                            ,color: Colors.white
                        ),
                      ),
                      padding: EdgeInsets.symmetric(vertical: 0*app_size.height, horizontal: 0.05*app_size.width),
                     shape:  StadiumBorder()
                    ),
                  ),

                    new Padding(
                      padding:  EdgeInsets.fromLTRB(app_size.width*0.1,40.0*app_size.height/HEIGHT,0.1*app_size.width,40.0*app_size.height/HEIGHT),
                    ),

                  ]
              ),
            ]

        ),
      ),

        bottomNavigationBar: new BottomNavigationBar(
          items: [

            new BottomNavigationBarItem(
              icon: const Icon(Icons.arrow_back),
              title: new Text('back',style: TextStyle(fontFamily: 'impact'),),
            ),


          ],
          onTap:(int index){
            Navigator.of(context).pop();
          },
        ),
      ),
      );
  }

工夫点について個別に説明します。

3 端末のサイズ取得

端末の種類によってレイアウトが変わってしまうのを防ぐために、Container等のwidthには予め取得されたデバイスのサイズを利用します。

サイズ取得に失敗する場合もあるので、その時は再度get_size()を呼び出すようにしています。

ソースコードは下記の通りです。

login.dart
  void get_size(){
    app_size=MediaQuery.of(context).size;
    setState(() {
      width = app_size.width;
      height = app_size.height;
    });

    if(width==0 || height==0) get_size();
  }

  @override
  void initState()  {
    super.initState();
    get_size();
  }

参考サイト:https://qiita.com/najeira/items/c98c5fec9c71104f8263

4 パスワード入力欄 のフォーカス外し

パスワードを入力するために、TextFormFieldをタップするとキーパッドが出ますが、
TextFormField外の箇所をタップするとキーパッドを閉じるようにしました。

実装方法としては、下記の通りです。

login.dart
      new GestureDetector(
      onTap:()
      {
        _focusNodePwd.unfocus();
      },

つまりOnTapイベントでパスワード入力欄に紐づけられたFocusNodeをUnFocusします。

5 TextFormFieldのヘルパーウィジェット

キーバッドが表示されたときに、TextFormFieldが隠れてしまう場合があります。
それを防ぐために下記サイトのヘルパーウィジェットを利用しました。

URL:https://www.didierboelens.com/2018/04/hint-4-ensure-a-textfield-or-textformfield-is-visible-in-the-viewport-when-has-the-focus/

6 キーパッドのDoneボタン

キーパッドでの入力完了後にはDoneボタンを押すことでキーパッドを閉じるようにしました。

Doneボタンの表示には下記のライブラリを使用しました。

Keyboard Actions:https://pub.dev/documentation/keyboard_actions/latest/

7 OKボタン押下時の動作

OKボタン押下時には、入力されたパスワードと予め設定された正解パスワードが合致するかどうか判定しますが、
その際の待ち時間は下記画面のようにCircularProgressIndicatorを使用します。

quitta_②.jpg

ソースコードは下記のとおりです。

login.dart
  var bodyProgress = new Container(
    child: new Stack(
      children: <Widget>[
        //body,
        new Container(
          alignment: AlignmentDirectional.center,
          decoration: new BoxDecoration(
            color: Colors.white70,
          ),
          child: new Container(
            decoration: new BoxDecoration(
                color: Colors.blue[200],
                borderRadius: new BorderRadius.circular(10.0)
            ),
            width: 300.0,
            height: 200.0,
            alignment: AlignmentDirectional.center,
            child: new Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                new Center(
                  child: new SizedBox(
                    height: 50.0,
                    width: 50.0,
                    child: new CircularProgressIndicator(
                      value: null,
                      strokeWidth: 7.0,
                    ),
                  ),
                ),
                new Container(
                  margin: const EdgeInsets.only(top: 25.0),
                  child: new Center(
                    child: new Text(
                      "please wait...",
                      style: new TextStyle(
                          color: Colors.white
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  );



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

【Flutter, Dart】ミュータブルとイミュータブル、そしてfreezed

はじめに

本記事は The Mutability Tax をベースにしています。
意訳・抜粋しまくったので翻訳記事と呼ぶには忍びないですが、記述の足らない箇所があれば元の記事を参照してください。
筆者の David Morgan 氏はGoogleのソフトウェアエンジニアです。

元記事の公開は2019年7月15日です。
本文中に登場するコードは Dart で記述されています。

3点要点

  1. Mutableはダメです
  2. Immutableも正しく扱わないとデメリット大きいです
  3. コード生成(freezed)でImmutableの欠点を補いましょう

Mutableの代償とImmutableの代償

The Mutability Tax では、それぞれの設計によって生じるコードメンテナンスコストのことを Tax(税金) と形容しています。

本記事では 代償 と表現します。

Mutable(値が変更可能)

システム間でMutableなデータを渡す疎結合モジュールを持つシステムは、Mutability-Taxという名のメンテナンスコストを支払います。

? Mutableの代償①:予期せぬ値の変更

上司 ? : おい新人。世界の都市圏人工TOP5のランキングと、その中で最大の都市を表示するコードを書いてみろ。ランキングは以下の通りやで。

1位:Tokyo
2位:Jakarta
3位:Delhi
4位:Manila
5位:Seoul

新人 ? : はい!わかりました!

上司 ? : こういう感じで出力されればOKやで。この中で最大の都市は東京やからな。

// Output
アルファベット順に表示: 
Delhi, Jakarta, Manila, Seoul, Tokyo
この中で最も大きいのは Tokyo です。

新人 ? : [ Dart List sort print ? ]

新人 ? : なるほどな、これコピペして DisplayAlphabetically() の中に書いとけばええか!

void display(List<String> strings) {
  (strings..sort()).forEach(print);
}

新人 ? : あとは上司に指定されたリストを使って表示すればいけるやろ!

var biggestCitiesRanking = ['Tokyo', 'Jakarta', 'Delhi', 'Manila', 'Seoul'];
print('アルファベット順に表示: ');
DisplayAlphabetically().display(biggestCitiesRanking);
print('この中で最も大きいのは ${biggestCitiesRanking.first} です。');

biggestCitiesRankingfirst を指定しているのですから、最大の都市である Tokyo が出力されて欲しいです。

// Output
アルファベット順に表示: 
Delhi, Jakarta, Manila, Seoul, Tokyo
この中で最も大きいのは Delhi です。

新人 ? : なんでや!なんでTokyoじゃなくてDelhiなんや!

どうやら DisplayAlphabetically().display() を実行することで biggestCitiesRanking が勝手に書き換えられているようです。

例えば上のような実装がされていると、本来は 表示するだけ の関数を実装したつもりが、 勝手にリストの中身を変更 してしまい 、結果間違った値を表示したというわけです。

これは DisplayAlphabetically().display を書いた人間が悪いという話ではありません。このような値の変更を行わないことをプログラマーに徹底させることは非常にナンセンスです。

そしてこの問題への最も最適な取り組み方は、 biggestCitiesRanking変更不可能にする(Immutable) にするということです。

上司 ? : おいドアホ!ちゃんとImmutable使わんかい!

Immutable(値が変更不可能)

オブジェクト指向言語では、Immutabilityを人力で管理しようとすることでバグ、パフォーマンス上のリスクを発生させ、Immutability-Taxという名のメンテナンスコストを支払います。

? Immutableの代償①:巨大なクラス

Mutableなオブジェクトの受け渡しでは、オブジェクトが変更されたあとのことをアレコレ考える必要があります。
特にViewの実装や並列処理を行うようなコードにおいては、Mutableなオブジェクトの状態を推論することはとても難しく、莫大なメンテナンスコストと重大なバグを発生させる原因となります。
さきほどの例におけるオブジェクトの受け渡しはたった2個のモジュールの間に発生していましたが、モジュールが大量にある状態ではどうでしょうか。
モジュールの組み合わせの数は nCr で計算されるように爆発的に増加し、組み合わせの数だけコストが増えていきます。

残念ながら、ほとんどのオブジェクト指向の言語は Mutableの代償(Mutability-Tax) を考慮して設計されていません。そのためにプログラマーはImmutableに関するルールやアンチパターンを勉強し、ライブラリを入れて構築しなければなりません。
それらは人為的な対策なので、いつかは落とし穴にハマってしまいます。

落とし穴をもう少し深掘ってみます。


上司 ? : おい新人、顧客管理システムを想定したカスタマーデータのクラスを作ってみろ。

新人 ? : わかりました!

上司 ? : 名前をString、年齢をintで定義するところまではええな。

新人 ? : はい!

上司 ? : 次に positional parameters (位置指定パラメーター)named parameters (名前指定パラメーター) のどちらを採用するか、という議論では、のちにパラメーターが増えたときの可読性をあげるために後者を選択するで。

新人 ? : なるほど!それは賢いですね!

class Customer {
  final String name;
  final int age;
  Customer({this.name, this.age});
}

上司 ? : 次に新しいカスタマー情報を更新する場合を考えるで。年齢を+1するコードを書いてみろ。

var customer = Customer(name: 'John Smith', age: 34);
var updatedCustomer = Customer(
    name: customer.name, age: customer.age + 1);

新人 ? : こんな感じですかね?

上司 ? : そうや。例えばマーケチームから、顧客が何回サイトを訪れたかを記録したいと言われたときを考えてみろ。

新人 ? : わかりました!じゃあ visits パラメーターを追加すればいいですね!仮に12回訪問している場合こんな感じですかね…

var customer = Customer(
    name: 'John Smith', age: 34, visits: 12);

上司 ? : じゃあさっき書いた、年齢を+1するコードを書いてみろ。

新人 ? : コピペでぽいっと

var customer = Customer(
    name: 'John Smith', age: 34, visits: 12);

var updatedCustomer = Customer(
    name: customer.name, age: customer.age + 1);

上司 ? : おいこら、そんなことしたら visits がリセットされるやないか!

新人 ? : うわ〜

上記のコードでは visits の値がリセットされてしまいます。
ここでの対応としては、 positional parameters を採用するか、 @required アノテーションで全てのパラメーターを必須とすることが考えられます。
しかし前述の通り位置指定パラメーターは可読性を落としますし、@required を全てに付与するということは、例えばカスタマーが訪れたときに visits に1を加算する処理においても全てのパラメーターを指定する必要があります。

上司 ? : これをメソッドで書くとこんな感じになるやろ?

class Customer {
  final String name;
  final int age;
  Customer({this.name, this.age});
  Customer copyWith({String name, String age}) =>
      Customer(name: name ?? this.name, age: age ?? this.age);
}

新人 ? : 完璧っすね!

var updatedCustomer = customer.copyWithage customer.visit + 1;

上司 ? : 甘いねん。Dartは null に弱いねん。このメソッドやと visitnull が渡されたことを認識できひんねん。

// nullは認識されないため、このコードは動きません
var customerWithoutVisits = customer.copyWith(visits: null);

上司 ? : Nullを認識させるためにはな、それぞれのフィールドに with メソッドを使ったら一応イケるねん。

class Customer{
  final String name;
  final int age;
  final int visits;
  Customer({this.name, this.age, this.visits});
  Customer withName(String name)
      => Customer(name: name, age: age, visits: visits);
  Customer withAge(int age)
      => Customer(name: name, age: age, visits: visits);
  Customer withVisits(int visits)
      => Customer(name: name, age: age, visits: visits);
}

新人 ? : フィールドがリセットされることも無いし、Nullにも対応したし、今度こそ完璧じゃないですか!

上司 ? : お前な、実際のサービス考えてみい。Customer が持つフィールド何個あると思てんねん。余裕で10個以上あるわ。

新人 ? : 確かに…。その分だけ with を書いて、その分だけ引数を渡すことになりますね。

上司 ? : そんなデカいボイラープレートで運用してたらお前絶対ミスるやろ。何よりクソ重なるわ。

新人 ? : はい…

? Immutableの代償②:ネストされた型への対応

上司 ? : コレクションと、ネストされた型が使用される場合を考えてみろ。

Mutable(変更可能)なコレクションをImmutable(変更不可能)なオブジェクトに組み込む場合には、それらを安全にコピーする必要があります。
またImmutableな型を使用する利点は高速であることですが、遅くなってしまうことが考えられます。
そして、先ほどのように with メソッドをしてしまうとネストされたフィールドの扱いが面倒になってきます。

class ShoppingBasket {
  final Customer customer;
  final List<Item> items;
  final List<Offer> offers;
  ShoppingBasket(
      this.customer,
      Iterable<Item> items,
      Iterable<Offer> offers)
    // Copy defensively to ensure immutability.
    : this.items = List.unmodifiable(items),
      this.offers = List.unmodifiable(offers);
  // TODO: add "with" method per field.
}

上司 ? : 例えば上のECサイトのカート(ShoppingBasket)に商品を追加する場合、 with を使ってこう書けるよな。

var updatedBasket = basket
    .withCustomer(basket.customer.withName(updatedName))
    .withItems([...basket.items, newItem, newItem2]);

新人 ? : でもこんなのいちいち with でコピーしてたら遅くなりませんか…?

上司 ? : …その通りや。Immutableは高速であることが利点やのに、遅くしてどないすんねんっちゅう話や。

上司 ? : 追加に関しては add で書くこともできるけどな、まあこれも同じような話や。

var updatedBasket = basket
    .withCustomerName(updatedName)
    .addItem(newItem)
    .addItem(item2);

新人 ? : なんかまたボイラープレートが大きくなりそうですね。

上司 ? : せやな。その上毎回フルコピーが実行されるから遅いしな。

結局何が実現されていれば良いのか

ここまで問答を繰り返してきましたが、理想としては以下の4点が実現されると良さそうです。
? 1. Immutableなデータが提供され
? 2. 更新が簡単かつ高速で
? 3. Nullを扱うことができ
? 4. フィールドが追加されても既存のコードに影響を与えない

上司 ? : 結論としては、ボイラープレートをコード生成するライブラリを使えっちゅう話や

新人 ? : 例えばどんなのがあるんですか?

上司 ? : Dartの独自のライブラリやと Built Value やな。

Built Valueの特徴
1. Immutableな型 (コレクションは built_collection
2. EnumClass(enumsのような機能)
3. JSONのシリアル化

上司 ? : ボイラープレートの管理は本来コストがかかるしバグを生みやすいよな。でもコード生成の場合はそのコストがかかるのは最初だけやねん。

新人 ? : なるほど…Mutableの代償みたいに税金みたいに段々重くなるコストじゃなくて入場料を払えばOKって感じですね!

上司 ? : お前もわかってきたな。 built_value を使ったらボイラープレートは生成してくれるからな、人間が書くのはこれだけでええねん。

abstract class Customer implements Built<Customer, CustomerBuilder> {
  String get name;
  int get age;
  @nullable
  int get visits;
factory Customer(void Function(CustomerBuilder) updates) = 
    _$Customer;
  Customer._();
}

新人 ? : 基本的なプロパティとアノテーションだけでいいのはありがたいですね!これならエンバグしなさそうです!

上司 ? : これはJavaでいうところの AutoValue.Builder にあたるな。ほんでその開発チームがコード生成ライブラリの必要性についてわかりやすくまとめたスライド AutoValue: what, why and how? を公開してくれてるわ。

AutoValue: what, why and how? の要約
多くのオブジェクト指向言語では、標準の言語仕様に従って 人力で Immutabilityを実現しようとすると、メンテナンスコスト、バグ、パフォーマンスの問題が発生します。つまりMutableの代償を避けるために、今度はImmutableの代償を払う事態になっています。Immutableオブジェクトのボイラープレートを生成し、管理する ライブラリを利用する ことこそが、Mutabilityへの正しい向き合い方です。

結論 freezed がおすすめです

上司 ? : さっきは built_value を例に上げたけど、最近は state_notifier とセットで freezed がよう使われてんねん。


元の記事 の公開後、 built_value より更に優秀な freezed というライブラリが発表されました。

ここでは簡単に freezed によって解決される、 built_value が劣っている点を記しておきます。
1. 大量のボイラープレートが必要となります。
2. built_valueDart1 用に作られており、順次v2に対応しているようですが、所々未対応の部分があるようです。
3. コード生成がかなり遅いです。

※ コード生成については freezed もそれなりに遅いですが、その問題に対してはmonoさんが素晴らしいTipsをtweetしておられました。これには作者であるRemi氏も同意していました。


新人 ? : なるほど、 freezed 使ってみます!

上司 ? : もう人力でやろうとすんなよ!


P.S.
誤りがありましたらコメントにてご指摘いただけますと幸いです。

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

【Flutter, Dart】ミュータブルの代償ととイミュータブルの代償、そしてfreezed

はじめに

本記事は The Mutability Tax をベースにしています。
意訳・抜粋しまくったので翻訳記事と呼ぶには忍びないですが、記述の足らない箇所があれば元の記事を参照してください。
筆者の David Morgan 氏はGoogleのソフトウェアエンジニアです。

元記事の公開は2019年7月15日です。
本文中に登場するコードは Dart で記述されています。

The Mutability Tax では、それぞれの設計によって生じるコードメンテナンスコストのことを Tax(税金) と形容しています。
本記事では 代償 と表現します。

3点要点

  1. Mutableはバグを生みやすく、遅いです
  2. Immutableも正しく扱わないとコードが肥大化してバグを生みやすく、遅くなります
  3. コード生成(freezed)でImmutableの欠点を補いましょう

Mutable(値が変更可能)

システム間でMutableなデータを渡す疎結合モジュールを持つシステムは、Mutability-Taxという名のメンテナンスコストを支払います。

? Mutableの代償①:予期せぬ値の変更

上司 ? : おい新人。世界の都市圏人工TOP5のランキングと、その中で最大の都市を表示するコードを書いてみろ。ランキングは以下の通りやで。

1位:Tokyo
2位:Jakarta
3位:Delhi
4位:Manila
5位:Seoul

新人 ? : はい!わかりました!

上司 ? : こういう感じで出力されればOKやで。この中で最大の都市は東京やからな。

// Output
アルファベット順に表示: 
Delhi, Jakarta, Manila, Seoul, Tokyo
この中で最も大きいのは Tokyo です。

新人 ? : [ Dart List sort print ? ]

新人 ? : なるほどな、これコピペして DisplayAlphabetically() の中に書いとけばええか!

void display(List<String> strings) {
  (strings..sort()).forEach(print);
}

新人 ? : あとは上司に指定されたリストを使って表示すればいけるやろ!

var biggestCitiesRanking = ['Tokyo', 'Jakarta', 'Delhi', 'Manila', 'Seoul'];
print('アルファベット順に表示: ');
DisplayAlphabetically().display(biggestCitiesRanking);
print('この中で最も大きいのは ${biggestCitiesRanking.first} です。');

biggestCitiesRankingfirst を指定しているのですから、最大の都市である Tokyo が出力されて欲しいです。

// Output
アルファベット順に表示: 
Delhi, Jakarta, Manila, Seoul, Tokyo
この中で最も大きいのは Delhi です。

新人 ? : なんでや!なんでTokyoじゃなくてDelhiなんや!

どうやら DisplayAlphabetically().display() を実行することで biggestCitiesRanking が勝手に書き換えられているようです。

例えば上のような実装がされていると、本来は 表示するだけ の関数を実装したつもりが、 勝手にリストの中身を変更 してしまい 、結果間違った値を表示したというわけです。

これは DisplayAlphabetically().display を書いた人間が悪いという話ではありません。このような値の変更を行わないことをプログラマーに徹底させることは非常にナンセンスです。

そしてこの問題への最も最適な取り組み方は、 biggestCitiesRanking変更不可能にする(Immutable) にするということです。

上司 ? : おいドアホ!ちゃんとImmutable使わんかい!

Immutable(値が変更不可能)

オブジェクト指向言語では、Immutabilityを人力で管理しようとすることでバグ、パフォーマンス上のリスクを発生させ、Immutability-Taxという名のメンテナンスコストを支払います。

? Immutableの代償①:巨大なクラス

Mutableなオブジェクトの受け渡しでは、オブジェクトが変更されたあとのことをアレコレ考える必要があります。
特にViewの実装や並列処理を行うようなコードにおいては、Mutableなオブジェクトの状態を推論することはとても難しく、莫大なメンテナンスコストと重大なバグを発生させる原因となります。
さきほどの例におけるオブジェクトの受け渡しはたった2個のモジュールの間に発生していましたが、モジュールが大量にある状態ではどうでしょうか。
モジュールの組み合わせの数は nCr で計算されるように爆発的に増加し、組み合わせの数だけコストが増えていきます。

残念ながら、ほとんどのオブジェクト指向の言語は Mutableの代償(Mutability-Tax) を考慮して設計されていません。そのためにプログラマーはImmutableに関するルールやアンチパターンを勉強し、ライブラリを入れて構築しなければなりません。
それらは人為的な対策なので、いつかは落とし穴にハマってしまいます。

落とし穴をもう少し深掘ってみます。


上司 ? : おい新人、顧客管理システムを想定したカスタマーデータのクラスを作ってみろ。

新人 ? : わかりました!

上司 ? : 名前をString、年齢をintで定義するところまではええな。

新人 ? : はい!

上司 ? : 次に positional parameters (位置指定パラメーター)named parameters (名前指定パラメーター) のどちらを採用するか、という議論では、のちにパラメーターが増えたときの可読性をあげるために後者を選択するで。

新人 ? : なるほど!それは賢いですね!

class Customer {
  final String name;
  final int age;
  Customer({this.name, this.age});
}

上司 ? : 次に新しいカスタマー情報を更新する場合を考えるで。年齢を+1するコードを書いてみろ。

var customer = Customer(name: 'John Smith', age: 34);
var updatedCustomer = Customer(
    name: customer.name, age: customer.age + 1);

新人 ? : こんな感じですかね?

上司 ? : そうや。例えばマーケチームから、顧客が何回サイトを訪れたかを記録したいと言われたときを考えてみろ。

新人 ? : わかりました!じゃあ visits パラメーターを追加すればいいですね!仮に12回訪問している場合こんな感じですかね…

var customer = Customer(
    name: 'John Smith', age: 34, visits: 12);

上司 ? : じゃあさっき書いた、年齢を+1するコードを書いてみろ。

新人 ? : コピペでぽいっと

var customer = Customer(
    name: 'John Smith', age: 34, visits: 12);

var updatedCustomer = Customer(
    name: customer.name, age: customer.age + 1);

上司 ? : おいこら、そんなことしたら visits がリセットされるやないか!

新人 ? : うわ〜

上記のコードでは visits の値がリセットされてしまいます。
ここでの対応としては、 positional parameters を採用するか、 @required アノテーションで全てのパラメーターを必須とすることが考えられます。
しかし前述の通り位置指定パラメーターは可読性を落としますし、@required を全てに付与するということは、例えばカスタマーが訪れたときに visits に1を加算する処理においても全てのパラメーターを指定する必要があります。

上司 ? : これをメソッドで書くとこんな感じになるやろ?

class Customer {
  final String name;
  final int age;
  Customer({this.name, this.age});
  Customer copyWith({String name, String age}) =>
      Customer(name: name ?? this.name, age: age ?? this.age);
}

新人 ? : 完璧っすね!

var updatedCustomer = customer.copyWithage customer.visit + 1;

上司 ? : 甘いねん。Dartは null に弱いねん。このメソッドやと visitnull が渡されたことを認識できひんねん。

// nullは認識されないため、このコードは動きません
var customerWithoutVisits = customer.copyWith(visits: null);

上司 ? : Nullを認識させるためにはな、それぞれのフィールドに with メソッドを使ったら一応イケるねん。

class Customer{
  final String name;
  final int age;
  final int visits;
  Customer({this.name, this.age, this.visits});
  Customer withName(String name)
      => Customer(name: name, age: age, visits: visits);
  Customer withAge(int age)
      => Customer(name: name, age: age, visits: visits);
  Customer withVisits(int visits)
      => Customer(name: name, age: age, visits: visits);
}

新人 ? : フィールドがリセットされることも無いし、Nullにも対応したし、今度こそ完璧じゃないですか!

上司 ? : お前な、実際のサービス考えてみい。Customer が持つフィールド何個あると思てんねん。余裕で10個以上あるわ。

新人 ? : 確かに…。その分だけ with を書いて、その分だけ引数を渡すことになりますね。

上司 ? : そんなデカいボイラープレートで運用してたらお前絶対ミスるやろ。何よりクソ重なるわ。

新人 ? : はい…

? Immutableの代償②:ネストされた型への対応

上司 ? : コレクションと、ネストされた型が使用される場合を考えてみろ。

Mutable(変更可能)なコレクションをImmutable(変更不可能)なオブジェクトに組み込む場合には、それらを安全にコピーする必要があります。
またImmutableな型を使用する利点は高速であることですが、遅くなってしまうことが考えられます。
そして、先ほどのように with メソッドをしてしまうとネストされたフィールドの扱いが面倒になってきます。

class ShoppingBasket {
  final Customer customer;
  final List<Item> items;
  final List<Offer> offers;
  ShoppingBasket(
      this.customer,
      Iterable<Item> items,
      Iterable<Offer> offers)
    // Copy defensively to ensure immutability.
    : this.items = List.unmodifiable(items),
      this.offers = List.unmodifiable(offers);
  // TODO: add "with" method per field.
}

上司 ? : 例えば上のECサイトのカート(ShoppingBasket)に商品を追加する場合、 with を使ってこう書けるよな。

var updatedBasket = basket
    .withCustomer(basket.customer.withName(updatedName))
    .withItems([...basket.items, newItem, newItem2]);

新人 ? : でもこんなのいちいち with でコピーしてたら遅くなりませんか…?

上司 ? : …その通りや。Immutableは高速であることが利点やのに、遅くしてどないすんねんっちゅう話や。

上司 ? : 追加に関しては add で書くこともできるけどな、まあこれも同じような話や。

var updatedBasket = basket
    .withCustomerName(updatedName)
    .addItem(newItem)
    .addItem(item2);

新人 ? : なんかまたボイラープレートが大きくなりそうですね。

上司 ? : せやな。その上毎回フルコピーが実行されるから遅いしな。

結局何が実現されていれば良いのか

ここまで問答を繰り返してきましたが、理想としては以下の4点が実現されると良さそうです。
? 1. Immutableなデータが提供され
? 2. 更新が簡単かつ高速で
? 3. Nullを扱うことができ
? 4. フィールドが追加されても既存のコードに影響を与えない

上司 ? : 結論としては、ボイラープレートをコード生成するライブラリを使えっちゅう話や

新人 ? : 例えばどんなのがあるんですか?

上司 ? : Dartの独自のライブラリやと Built Value やな。

Built Valueの特徴
1. Immutableな型 (コレクションは built_collection
2. EnumClass(enumsのような機能)
3. JSONのシリアル化

上司 ? : ボイラープレートの管理は本来コストがかかるしバグを生みやすいよな。でもコード生成の場合はそのコストがかかるのは最初だけやねん。

新人 ? : なるほど…Mutableの代償みたいに税金みたいに段々重くなるコストじゃなくて入場料を払えばOKって感じですね!

上司 ? : お前もわかってきたな。 built_value を使ったらボイラープレートは生成してくれるからな、人間が書くのはこれだけでええねん。

abstract class Customer implements Built<Customer, CustomerBuilder> {
  String get name;
  int get age;
  @nullable
  int get visits;
factory Customer(void Function(CustomerBuilder) updates) = 
    _$Customer;
  Customer._();
}

新人 ? : 基本的なプロパティとアノテーションだけでいいのはありがたいですね!これならエンバグしなさそうです!

上司 ? : これはJavaでいうところの AutoValue.Builder にあたるな。ほんでその開発チームがコード生成ライブラリの必要性についてわかりやすくまとめたスライド AutoValue: what, why and how? を公開してくれてるわ。

AutoValue: what, why and how? の要約
多くのオブジェクト指向言語では、標準の言語仕様に従って 人力で Immutabilityを実現しようとすると、メンテナンスコスト、バグ、パフォーマンスの問題が発生します。つまりMutableの代償を避けるために、今度はImmutableの代償を払う事態になっています。Immutableオブジェクトのボイラープレートを生成し、管理する ライブラリを利用する ことこそが、Mutabilityへの正しい向き合い方です。

結論 freezed がおすすめです

上司 ? : さっきは built_value を例に上げたけど、最近は state_notifier とセットで freezed がよう使われてんねん。


元の記事 の公開後、 built_value より更に優秀な freezed というライブラリが発表されました。

ここでは簡単に freezed によって解決される、 built_value が劣っている点を記しておきます。
1. 大量のボイラープレートが必要となります。
2. built_valueDart1 用に作られており、順次v2に対応しているようですが、所々未対応の部分があるようです。
3. コード生成がかなり遅いです。

※ コード生成については freezed もそれなりに遅いですが、その問題に対してはmonoさんが素晴らしいTipsをtweetしておられました。これには作者であるRemi氏も同意していました。


新人 ? : なるほど、 freezed 使ってみます!

上司 ? : もう人力でやろうとすんなよ!


P.S.
誤りがありましたらコメントにてご指摘いただけますと幸いです。

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

【iOSアプリ内課金のテスト】サブスクリプション自動更新時間の進み方

サブスクリプションの課金テスト時に、毎回調べてしまうのでメモ&Tipsです。

定期購読更新の時間は早回しで進む

Sandbox環境下では、サブスクリプション更新期間の時間は早回しで進みます。

表でまとめると以下になります。

更新期間の比較

本番 Sandbox
7日 3分
1ヶ月 5分
2ヶ月 10分
3ヶ月 15分
6ヶ月 30分
1年 60分

自動更新は6回まで

  • Sandbox環境では6回しか自動更新されない

例えば、1ヶ月更新のプランだと、半年で自動更新が終了します。

テスト環境での時間を計算すると

5分(1ヶ月)×6回更新=30分

で自動更新が切れるので

プラン購入から30分後に、ユーザが更新継続しなかったパターンをテストする事が出来ます。

なお、本番環境では設定アプリから「購読の停止」がいつでも行えますが

Sandbox環境ではその設定が無く、手動で停止する事が出来ません。(自動更新切れを待つしかない)

無料トライアルを設定している場合

定期購読プランに「最初の1週間は無料!」みたいなお試しオファーを設定している場合は、その分を加味して計算します。

例)1週間の初回無料トライアル付き、1ヶ月プランのケース

3分+(5分×6回更新)=33分

33分後に自動更新が終了します。


参考:
自動購読課金について【iOS編】 | サイバーエージェント 公式エンジニアブログ

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

iOS Keyboard Extensionでキーボードを介さない操作を検知する

iOSの純正キーボードは超絶多機能です。ちょっと考えただけでもこんな機能がついています。

  • 入力中の文字を薄い青色(ダークモードでは黄色)でハイライトする。
  • 点滅するカーソルをドラッグすることによるカーソルの移動を検知し、変換候補を変更する。
  • 点滅するカーソルをドラッグすることによるカーソルの移動を検知し、入力中の範囲から外に出られないようにする。
  • 入力範囲外をタップした場合入力中の文字を確定する。
  • 選択されているテキストを取得し、再変換する。
  • ペースト操作が行われた場合検知し、入力中の文字を確定する。 これは高度な機能です。実際私が日本語対応のkeyboard extensionを漁った限り、純正キーボードと同じ挙動を再現できているものはほとんどありませんでした。 iOS keyboard extension functions.png そこでこれらの機能をできる限り実現すべく、いろいろ努力した結果をまとめます。

入力中の文字のハイライト

いきなり残念なお知らせですが、これは今は断念するのが正解です。
この機能の実現にはsetMarkedText()というメソッドが利用できます。が、これを実現すると他の無数の機能が死にます。
詳しくはこちらを参照してください:UITextDocumentProxyのsetMarkedTextを(まだ)使ってはいけない。 - Qiita

それ以外

  • カーソル移動検知(→カーソル移動制限など)
  • ペースト検知
  • 範囲外タップ検知
  • 選択検知
  • 選択解除検知
  • カット検知

などがどうにかできました。
大体、documentContextBeforeInputdocumentContextAfterInput、それにselectedTextをゴリ押しで取得していくとどうにかなりました。UIInputViewControllertextWillChangetextDidChangeは、引数のtextInputは使えませんが、少なくとも私の環境では一応呼ばれるのでこれを利用します。
documentContextBeforeInputは日本語入力ではカーソルの左側の文字列(改行の後まで)、documentContextAfterInputは右側の文字列(改行の前まで)を取得します。選択部分が存在する場合には両端がそれぞれカーソルとみなされる挙動のようです。したがってテキスト全体はdocumentContextBeforeInput+selectedText+documentContextAfterInputで得られます。

改行周りの悪夢

改行を考慮すると、取得される文字列が悪夢のように複雑になります。考慮しないのが一番ですが、一応こういう挙動をします。

//left/center/rightとして得られる情報は以下の通り
|はカーソル位置。二つある場合は選択範囲
 ---------------------
    abc|def           ->abc/nil/def
 ---------------------
    abc|def|ghi       ->abc/def/ghi
 ---------------------
    abc|              ->abc/nil/nil
 ---------------------
    abc|              ->abc/nil/nil

 ---------------------
    abc|              ->abc/nil/empty
    def
 ---------------------
    abc
    |def              ->\n /nil/def
 ---------------------
    a|bc
    d|ef              ->a/bc \n d/ef
 ---------------------   

実装

まず、ViewController側でなんらかの変化が起こる前に現在の状態を登録、起こった後に変化後の状態を登録します。

KeyboardViewController.swift
class KeyboardViewController: UIInputViewController {
    override func textWillChange(_ textInput: UITextInput?) {
        // The app is about to change the document's contents. Perform any preparation here.
        super.textWillChange(textInput)

        let left = self.textDocumentProxy.documentContextBeforeInput ?? ""
        let center = self.textDocumentProxy.selectedText ?? ""
        let right = self.textDocumentProxy.documentContextAfterInput ?? ""

       registerSomethingWillChange(left: left, center: center, right: right)
    }

    override func textDidChange(_ textInput: UITextInput?) {
        // The app has just changed the document's contents, the document context has been updated.
        super.textDidChange(textInput)

        let left = self.textDocumentProxy.documentContextBeforeInput ?? ""
        let center = self.textDocumentProxy.selectedText ?? ""
        let right = self.textDocumentProxy.documentContextAfterInput ?? ""


       registerSomethingDidChange(left: left, center: center, right: right)
    }
}

で、適当なところに次の二つの関数を書いておきます。まず変化前の状況は保存します。

registerSomethingWillChange.swift
func registerSomethingWillChange(left:String, center:String, right:String){
    self.tempTextData = (left:left, center:center, right:right)
}

変化が起こった場合、変化前と変化後の状態を比較することで状況を判断します。このロジックは愚直に実装しました。私の知る限りこういう諸々の動作を検知するための機構はUIKitでは提供されていません。

registerSomethingDidChange.swift
func registerSomethingDidChange(left:String, center:String, right:String){
    //leftは変化後のtextDocumentProxy.documentContextBeforeInput
    //centerは変化後のtextDocumentProxy.selectedText
    //rightは変化後のtextDocumentProxy.documentContextAfterInput
    let b_left = self.tempTextData.left      //変化前のleft
    let b_center = self.tempTextData.center  //変化前のcenter
    let b_right = self.tempTextData.right    //変化前のafter

    let isWholeTextChanged = !((left+center+right) == (b_left + b_center + b_right)) //全体が変化しているか?
    let wasSelected = !(b_center == "") //選択されていたか?
    let isSelected = !(center == "")    //選択されているか?

    //全体としてテキストが変化せず、選択範囲が存在している場合→新たに選択した、または選択範囲を変更した
    if !isWholeTextChanged && isSelected{
        //なんらかの操作をする。例えば再変換したい場合はcenterの値を用いて変換候補を表示する。
        return
    }

    //全体としてテキストが変化せず、選択範囲が無くなっている場合→選択を解除した
    if !isWholeTextChanged && wasSelected && !isSelected{
        //なんらかの操作をする。例えば再変換の候補の表示を消す。
        return
    }

    //全体としてテキストが変化せず、選択範囲は前後ともになく、左側(右側でも良い)の文字列が変わっていた場合→カーソルを移動した
    if !isWholeTextChanged && !wasSelected && !isSelected && b_left != left{
        //カーソルの移動を処理する。例えば移動範囲が入力中の範囲を超えていた場合はadjustTextPositionなどを用いてカーソルを補正する。
        return
    }
    //それ以外の状況で全体のテキストに変化がなければ、検出の必要はおそらくない。
    if !isWholeTextChanged{
        //なんらかの操作
        return
    }

    //全体としてテキストが変化しており、左は改行コードになっており、かつ前のwholeText(=left+center+right)と後の選択範囲が一致する場合→行全体が選択された
    if isWholeTextChanged && left == "\n" && b_left + b_center + b_right == center{
        //行全体の選択を検知する。
        return
    }

    //全体としてテキストが変化しており、前の左は改行コードで、かつ前のcenterと後のwholeTextが一致する場合→行全体の選択が解除された
    if isWholeTextChanged && b_left == "\n" && b_center == left + center + right{
        //行全体の選択解除を検知する。
        return
    }

    //全体としてテキストが変化しており、左右の文字列を合わせたものが不変である場合→ユーザが選択部分をカットした。
    if isWholeTextChanged && b_left + b_right == left + right{
        //カットを検知する。
        return
    }

    //全体としてテキストが変化しており、右側の文字列が不変であった場合→ペーストが疑われる。
    if isWholeTextChanged && b_right == right{
        //もしクリップボードに文字列がコピーされており、かつ、前の左側文字列にその文字列を加えた文字列が後の左側の文字列に一致した場合→確実にペーストである。
        if let pastedText = UIPasteboard.general.string, pastedText == left.suffix(pastedText.count){
            //なんらかの操作
            return
        }
    }

    //上記のどれにも引っかからず、なおかつテキスト全体が変更された場合→範囲外タップ。
    if isWholeTextChanged{
        //範囲外タップを検出し、例えば確定する。
        return
    }
}

まとめ

お読みいただいた通りで、Keyboard Extension周りはかなり気合が求められます。頑張りましょう。

余談

こんな記事を書いていたら、iOSの純正キーボードでもちょっと怪しい挙動を発見しました。
入力中にカーソルを真ん中あたりまで移動し、その上で入力範囲外をタップするとカーソルの後の部分が全て消えて確定扱いになります。一方入力中にカーソルを真ん中あたりまで移動し、さらに文字を入力、または消去する操作を行ってから入力範囲外をタップすると単に確定扱いになります。
bug.gif
あまり自然な挙動とは思えないので、バグの可能性が高いと思います。きっとApple純正キーボードの開発者も相当苦労しているんでしょうね。

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

ダークモード対応の罠

去年iOS 13の新機能で出たダークモードを自社アプリに対応したのですが、思わぬ挙動でバグが出てしまいそのTips共有です

traitCollectionDidChangeの罠

ライト⇄ダークの色切り替え時に特定の処理を行いたい場合は
func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
を使って切り替え時の検知ができます

単純にライト/ダーク時に専用の配色を入れる(例 CGColorで色を入れる必要がある)場合は問題ないですが、それ以外の何か別の設定を入れる(例 ダークモードの時だけ〇〇する)みたいなケースだと問題がおきます

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

    super.traitCollectionDidChange(previousTraitCollection)

    if UITraitCollection.current.userInterfaceStyle == .dark {
        // ダーク時だけしたい処理
    }

}

コントロールセンターのライト→ダークに切り替え時に traitCollectionDidChange
が呼ばれるのですが、 traitCollectionDidChangeがバックグラウンドに入った瞬間に2回呼ばれます(ライト⇄ダーク切り替えなしに)

しかも、ただ2回呼ばれるのではなくバッググラウンドに入った時に、今端末がライト/ダークどっちの設定が入っているかを見れる UITraitCollection.current.userInterfaceStyleがライトとダークそれぞれの状態で来ます

本来ダークモードの設定してる時だけしたい処理がバックグラウンド時にライトとダークそれぞれ来ることになり、端末設定がライトモードの時でもダークモードの時にしたい処理をしてしまう期待外れなことが起きてしまいます?

この挙動がよくわからず・・・
ちなみに他のライフサイクルでの検知できるメソッドだとどうか?に関しては同様の問題が起きてました・・

  • viewWillLayoutSubviews
  • viewDidLayoutSubviews 

など

回避策

個人的にあんまり納得いく修正方法ではありませんが、バッググラウンド時は特定の処理までしないようにしました
バックグラウンド上で設定を変えてもフォアグラウンドに戻った時呼ばれるので問題ないです

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

    super.traitCollectionDidChange(previousTraitCollection)

    if UIApplication.shared.applicationState == .background {
        return
    }

    if UITraitCollection.current.userInterfaceStyle == .dark {
        // ダークの時のみしたい処理
    }

}

なぜ今回の問題になった挙動をするかいまいちわかってないのでうーんっていう気持ちで修正できました()

おまけ traitCollectionDidChangeで色を入れるのは大丈夫なの?

大丈夫です
ライトモード時はライト、ダークモード時はダークの色を入れてくれます(最終的に)
試しにアセットカタログのファイルで以下の色を用意して
スクリーンショット 2020-08-17 14.39.24.png

  • UITraitCollection.current.userInterfaceStyle

の要素をログで追って確認し、ライトモードでバッググラウンド時に入ってみて検証してみました

dark

Optional([1.0, 1.0, 1.0, 1.0])

light

Optional([0.0, 0.0, 0.0, 1.0])

2回呼ばれますが、
ライトモードだとダーク→ライトの順番に呼ばれるので最終的にライトの色が入ります
同様にダークモードでも ライト→ダークと呼ばれダークの色が入るので大丈夫です

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

APIデータを、JSON形式で取得。【JSON解析】

 JSONとは?

JavaScript Object Notation ?

  • データフォーマット (データ形式) の一つ。
  • キーのペアで構成。(=辞書型)
  • データの記述量が少ないので、読み込みが速い。
  • JavaScriptとの親和性が高い。
  • JavaScriptに限らず、データのやりとりに広く使われる。

picture_pc_5726e4146e6ea68b6e70cc6e7b9361de.png

キーは常に文字列ですが、
値にはString, Int, Bool, 配列, nullなども使えます。

 データフォーマット (データ形式)

現在の主流のデータ形式は、XML, JSON, CSVの3つ。
JSONが一番軽く、使い勝手がよく、多く使われる。

データフォーマットとは?XML、JSON、CSVの違いや特徴についてわかりやすく解説するよ

 マークアップ言語

タグで囲む(マークをつける)ことで構造を表現する言語。

<Title>This is Title</Title>

HTMLXMLは、どちらもマークアップ言語。(Markup Language)
違いは、上記サイト参照。

 JSON と API

JSONは、
APIを介して送信されるデータを構造化するための、最も一般的なデータ形式。

4043253-friday-halloween-jason-movie_113258.png

 APIとは?

Application Programming Interface?

  • ソフトウェア同士を繋ぐのがAPI

  • インターフェイスとは「接点」。
    「何か」と「何か」を繋ぐものです。

 APIで、できること。

認証機能、チャット機能など、色々な機能を共有できます。

APIには、「どうやって使えば良いの?」が示されています。
(ルール・説明書・決まり事)

 例えるなら...

オランダ・清と、日本とを繋ぐのが、長崎の出島です。
外部デバイスとパソコンを繋ぐのが、USBです。
ソフトウェア同士を繋ぐのがAPIです。

?「APIを公開する」=
外部とやりとりする窓口(=API)を作り、
外部アプリとコミュニケーションや連携ができる状態にする。

=「長崎に出島を設置する」
=「PCにUSBを挿し込む」

 API 例

  • オンライン決済 Stripe
  • 顔認識AI Microsoft Face API
  • Google Maps Google Maps JavaScript API
  • 商品の在庫管理や注文レポートの取得などを行う Amazon MWS API

個人でも使える!おすすめAPI一覧


 Webリクエストをしてみる。?

SWAPI(The Star Wars API)

https://swapi.dev/api/people/1のデータを
JSON形式で取得してみたいと思います。

unnamed.jpg

 URLSessionを使って、HTTP通信する。

『HTTP』とは?

Webリクエストを作成して、JSONデータを取得します。

  1. urlに取得したいapiのリクエストURLを設定する
  2. URLSessionを使ってtaskを作る
  3. JSONデータを取得
  4. task.resume()でタスクを開始する

 コード

  • Servicesフォルダ > PersonApi.swift
PersonApi.swift
class PersonApi {

    func getRandomPersonUrlSession() {
        guard let url = URL(string: PERSON_URL) else { return }

        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            print("Data = \(data)")
            print("Response = \(response)")
        }
        task.resume()
    }

}

 Guard

guard文は、
条件を満たさない場合の処理を記述する構文です。

  • return メソッド内の処理を、終了
  • break 繰り返し処理を、終了
  • continue 処理をスキップ
  • throw 例外を投げる

【Swift入門】guardの使い方をマスターしよう!

 URLSessionとは?

関連するネットワーク上のデータ転送処理群(=URLSessionTask)をまとめるクラス。
1つのSessionで繰り返しURLSessionTaskの作成が可能。

 URLSessionTaskとは?

URLで特定されるリソースを実際に取得し、アプリにデータを返却したり、
リモートサーバからファイルのダウンロードやアップロードを行う。

【Swift】URLSessionまとめ

  • Utilityフォルダ > Constants.swiftファイルに、 リンクをまとめた。
Constants.swift
let URL_BASE = "https://swapi.dev/api/"
let PERSON_URL = URL_BASE + "people/1/"
  • Controllerフォルダ > SelectPersonVC.swift=ViewController
SelectPersonVC.swift
import UIKit

class SelectPersonVC: UIViewController {

    var personApi = PersonApi() // クラスをインスタンス化

    override func viewDidLoad() {
        super.viewDidLoad()
        personApi.getRandomPersonUrlSession() // メソッド呼び出し
    }

}

 結果

dataresponseが、無事printされた。

URLSessionを使って、指定URLに対してWebリクエストできた。

サーバから送られてきたJSON文字列を、クライアントで使用するには、
解析が必要らしいので、後ほど行う。

console.
Data = Optional(637 bytes) 

Response = Optional(<NSHTTPURLResponse: 0x600000f223c0> 

{ URL: https://swapi.dev/api/people/1/ } 
{ Status Code: 200, Headers {   // <----------- "Status Code"?
    "Content-Length" =     (
        0
    );
    "Content-Type" =     (
        "application/json"
    );
    Date =     (
        "Mon, 17 Aug 2020 02:15:29 GMT"
    );

// 以下省略

 ちなみに

Status Code?
400~は、自分側のエラー。
500~は、サーバー側のエラー。

スクリーンショット 2020-08-17 17.04.35.png

 JSON解析 ?

【Swift入門】SwiftでJSONを扱ってみよう!

サーバから送られてきたJSON文字列を、クライアントで使用するには、
解析が必要。

  • パース(parse)とも言います。
  • 最初に、import Foundationの宣言が必要。
  • JSONSerialization.jsonObjectを使用する。

 JSONSerialization.jsonObject?

JSONSerializationは、
Apple標準フレームワークのFoundationに含まれている、便利なライブラリ。

let json = JSONSerialization.jsonObject(with: data, options[])

Serializationとは...ざっくりですが、

オブジェクトの状態をStreamの状態に変換すること。
(1バイトずつ読み書きできる、データ構造)

この機能を使うと、簡単にインスタンスを外部記憶装置などに保存し、
インスタンスの情報を永続化することができる。

 throw

一般式.swift
func メソッド名(引数) throws -> 戻り値 {
    // エラーを投げる可能性のある処理
}

jsonObjectOption+クリックすると、Declaration(宣言)が表示されるのですが、

// jsonObjectメソッドの、Declarationが表示されます。

class func jsonObject(
  with data: Data, options opt: JSONSerialization.ReadingOptions = []
) throws -> Any
// Anyは、クラス・構造体・列挙型、「すべての型のインスタンス」を指します。

throwsとあるので、jsonObjectメソッドは、エラーを投げる可能性があります。

そこで、エラーを受け取る必要があります。

 do-catchとtryで、エラーを受け取る

一般式.swift
do {     // エラーを投げる可能性のある処理、do{}。
    try      // メソッド呼び出し 
} catch {
         // エラーが発生した場合の処理、catch{}
}
 do {
     let json = try JSONSerialization.jsonObject(with: data, options: [])
} catch {
     debugPrint(error.localizedDescription)
     return
}
// localizedDescriptionは、エラーの概要を表示する。
  • localizedDescription エラーの概要
  • localizedFailureReason エラーが発生した理由
  • localizedRecoverySuggestion 復旧方法(NextAction)
  • localizedRecoveryOptions AlertViewに表示するボタンの名前   

 localizedとは?

Localizationとは、アプリを各国の言語に合わせること

localizedDescriptionとかを使うと、
フレームワーク側でローカライズしてくれるので、他言語対応の手間が省ける。

 キャストとは?

変数の型を、別の型に変換すること。

guard let jsonAny = json as? [String: Any] else { return }

【Swift入門 文法編】型キャスト(as, as!, as?)をマスターしよう

JSON解析、おわり。


先ほどのtaskに色々追記していきます。

let task = URLSession.shared.dataTask(with: url) { 
(data, responce, error) in
       // ここに色々追記します。
}

 error

errorがnilじゃなければ、エラーメッセージを表示して、
returnで処理を終了。

guard error == nil else {
    debugPrint(error.debugDescription)
    return
}
// Guardは前述

 dataの、アンラップ

dataパラメータは、リクエストに失敗するとnilとなるらしいので、オプショナル型

JSONSerialization.jsonObjectで使用したいので、アンラップします。

guard let data = data else { return }

 全体のコード

personApi.swift
func getRandomPersonUrlSession() {

        guard let url = URL(string: PERSON_URL) else { return }

        let task = URLSession.shared.dataTask(with: url) { (data, responce, error) in

            guard error == nil else {
                debugPrint(error.debugDescription)
                return
            }


            guard let data = data else { return }

            do {
                let json = try JSONSerialization.jsonObject(with: data, options: [])
                guard let jsonAny = json as? [String: Any] else { return }
    --------->  print(json)    
            } catch {
                debugPrint(error.localizedDescription)
                return
            }


        }

        task.resume()

    }

print(json)の実行結果は以下の通り。

SWAPI(The Star Wars API)https://swapi.dev/api/people/1のデータを、
JSON形式で取得できた。

"Luke Skywalker"の誕生日とか、目の色とか、身長、髪の色とか。

実行結果.
{
    "birth_year" = 19BBY;
    created = "2014-12-09T13:50:51.644000Z";
    edited = "2014-12-20T21:17:56.891000Z";
    "eye_color" = blue;
    films =     (
        "http://swapi.dev/api/films/1/",
        "http://swapi.dev/api/films/2/",
        "http://swapi.dev/api/films/3/",
        "http://swapi.dev/api/films/6/"
    );
    gender = male;
    "hair_color" = blond;
    height = 172;
    homeworld = "http://swapi.dev/api/planets/1/";
    mass = 77;
    name = "Luke Skywalker";
    "skin_color" = fair;
    species =     (
    );
    starships =     (
        "http://swapi.dev/api/starships/12/",
        "http://swapi.dev/api/starships/22/"
    );
    url = "http://swapi.dev/api/people/1/";
    vehicles =     (
        "http://swapi.dev/api/vehicles/14/",
        "http://swapi.dev/api/vehicles/30/"
    );
}

 構造体を作る。?

JSON解析の続きです。
さて、Webリクエストにより取得したJSONデータですが、このままでは使えません。

JSONのメンバの値は様々な型を持つため、
パースするにはどのメンバがどの型を持つかを、一つ一つ指定しなければいけません。

「名前: 値」をメンバと呼びます。

JSONを、構造体に変換し、表示項目の宣言をします。

Struct[構造体]とは?
ひとことで簡単に言えば、継承のできないクラス。

PersonModel.swift
struct Person {         // "Person"構造体
    let name : String
    let height : String
    let mass : String
    let hair : String
    let birthYear : String
    let gender : String
    let homeWorldUrl : String
    let filmUrls : String
    let vehicleUrls : [String]  <------- // 配列
    let starshipUrls : [String]  <------- // 配列
}

 private内で、こんなことします。?

let とあるキーの値 = jsonAny["キー"]
personApi.swift
private func parsePersonManual(json: [String: Any]) {
        let name = json["name"] as? String ?? ""
        let height = json["height"] as? String ?? ""
        let mass = json["mass"] as? String ?? ""
        // 以下、省略。 
    }

 private

アクセス修飾子の一つです。

  • 別ファイルからのアクセスはNG。
  • クラス単位のスコープではない。

 『??』とは

「nilガード」です。
bがnilだったら、aに空文字列を代入してくれるという構文です。

let a = b ?? ""

 初期化

同じくprivateメソッド内にて、初期化します。

returnするので、-> Personもお忘れなく。

let person = Person(name: name, height: height, mass: mass, //以下、省略。)
return person

// return person = Person(name: name, ...) でもOK

 privateの全体コード

自動補完もしてくれないから、とても面倒。

JSONのための外部ライブラリ『SwiftyJSON』ってのが
よく使われるらしいので、後日学びます。

private func parsePersonManual(json: [String: Any]) -> Person {
        let name = json["name"] as? String ?? ""
        let height = json["height"] as? String ?? ""
        let mass = json["mass"] as? String ?? ""
        let hair = json["hair"] as? String ?? ""
        let birthYear = json["birthYear"] as? String ?? ""
        let gender = json["gender"] as? String ?? ""
        let homeWorldUrl = json["homeWorldUrl"] as? String ?? ""
        let filmUrls = json["filmUrls"] as? String ?? ""
        let vehicleUrls = json["vehicleUrls"] as? [String] ?? [String]()
        let starshipUrls = json["starshipUrls"] as? [String] ?? [String]()

        let person = Person(name: name, height: height, mass: mass, hair: hair, birthYear: birthYear, gender: gender, homeWorldUrl: homeWorldUrl, filmUrls: filmUrls, vehicleUrls: vehicleUrls, starshipUrls: starshipUrls)
        return person
    }

 個別のJSONデータを、print?

privateメソッドを呼び出して、インスタンス作って、
個別のJSONデータを、print。 

do {
                let json = try JSONSerialization.jsonObject(with: data, options: [])
                guard let jsonAny = json as? [String: Any] else { return }

        ------> let person = self.parsePersonManual(json: jsonAny) 
                print(person.name)
                print(person.height)
} catch {
                debugPrint(error.localizedDescription)
                return
}

JSONデータのnameheightを取得。
Luke Skywalker、意外と背が小さい。

実行結果.
Luke Skywalker
172  

おしまい


JSONでAPIデータ取得するの大変だなあ。

Main.storyboardとの連携や、SwiftyJSONについては、
のちに追記予定。

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

APIデータを、JSON形式で取得。【URLSession】

 JSONとは?

JavaScript Object Notation ?

  • データフォーマット (データ形式) の一つ。
  • キーのペアで構成。(=辞書型)
  • データの記述量が少ないので、読み込みが速い。
  • JavaScriptとの親和性が高い。
  • JavaScriptに限らず、データのやりとりに広く使われる。

picture_pc_5726e4146e6ea68b6e70cc6e7b9361de.png

キーは常に文字列ですが、
値にはString, Int, Bool, 配列, nullなども使えます。

 データフォーマット (データ形式)

現在の主流のデータ形式は、XML, JSON, CSVの3つ。
JSONが一番軽く、使い勝手がよく、多く使われる。

データフォーマットとは?XML、JSON、CSVの違いや特徴についてわかりやすく解説するよ

 マークアップ言語

タグで囲む(マークをつける)ことで構造を表現する言語。

<Title>This is Title</Title>

HTMLXMLは、どちらもマークアップ言語。(Markup Language)
違いは、上記サイト参照。

 JSON と API

JSONは、
APIを介して送信されるデータを構造化するための、最も一般的なデータ形式。

4043253-friday-halloween-jason-movie_113258.png

 APIとは?

Application Programming Interface?

  • ソフトウェア同士を繋ぐのがAPI

  • インターフェイスとは「接点」。
    「何か」と「何か」を繋ぐものです。

 APIで、できること。

認証機能、チャット機能など、色々な機能を共有できます。

APIには、「どうやって使えば良いの?」が示されています。
(ルール・説明書・決まり事)

 例えるなら...

オランダ・清と、日本とを繋ぐのが、長崎の出島です。
外部デバイスとパソコンを繋ぐのが、USBです。
ソフトウェア同士を繋ぐのがAPIです。

?「APIを公開する」=
外部とやりとりする窓口(=API)を作り、
外部アプリとコミュニケーションや連携ができる状態にする。

=「長崎に出島を設置する」
=「PCにUSBを挿し込む」

 API 例

  • オンライン決済 Stripe
  • 顔認識AI Microsoft Face API
  • Google Maps Google Maps JavaScript API
  • 商品の在庫管理や注文レポートの取得などを行う Amazon MWS API

個人でも使える!おすすめAPI一覧


 Webリクエストをしてみる。?

SWAPI(The Star Wars API)

https://swapi.dev/api/people/1のデータを
JSON形式で取得してみたいと思います。

unnamed.jpg

 URLSessionを使って、HTTP通信する。

『HTTP』とは?

Webリクエストを作成して、JSONデータを取得します。

  1. urlに取得したいapiのリクエストURLを設定する
  2. URLSessionを使ってtaskを作る
  3. JSONデータを取得
  4. task.resume()でタスクを開始する

 コード

  • Servicesフォルダ > PersonApi.swift
PersonApi.swift
class PersonApi {

    func getRandomPersonUrlSession() {
        guard let url = URL(string: PERSON_URL) else { return }

        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            print("Data = \(data)")
            print("Response = \(response)")
        }
        task.resume()
    }

}

 Guard

guard文は、
条件を満たさない場合の処理を記述する構文です。

  • return メソッド内の処理を、終了
  • break 繰り返し処理を、終了
  • continue 処理をスキップ
  • throw 例外を投げる

【Swift入門】guardの使い方をマスターしよう!

 URLSessionとは?

関連するネットワーク上のデータ転送処理群(=URLSessionTask)をまとめるクラス。
1つのSessionで繰り返しURLSessionTaskの作成が可能。

 URLSessionTaskとは?

URLで特定されるリソースを実際に取得し、アプリにデータを返却したり、
リモートサーバからファイルのダウンロードやアップロードを行う。

【Swift】URLSessionまとめ

  • Utilityフォルダ > Constants.swiftファイルに、 リンクをまとめた。
Constants.swift
let URL_BASE = "https://swapi.dev/api/"
let PERSON_URL = URL_BASE + "people/1/"
  • Controllerフォルダ > SelectPersonVC.swift=ViewController
SelectPersonVC.swift
import UIKit

class SelectPersonVC: UIViewController {

    var personApi = PersonApi() // クラスをインスタンス化

    override func viewDidLoad() {
        super.viewDidLoad()
        personApi.getRandomPersonUrlSession() // メソッド呼び出し
    }

}

 結果

DataResponseが、無事printされた。
JSON形式でAPIデータを取得できた。

console.
Data = Optional(637 bytes) 

Response = Optional(<NSHTTPURLResponse: 0x600000f223c0> 

{ URL: https://swapi.dev/api/people/1/ } 
{ Status Code: 200, Headers {   // <----------- "Status Code"
    "Content-Length" =     (
        0
    );
    "Content-Type" =     (
        "application/json"
    );
    Date =     (
        "Mon, 17 Aug 2020 02:15:29 GMT"
    );

// 以下省略

 ちなみに

Status Code
400~は、自分側のエラー。
500~は、サーバー側のエラー。

スクリーンショット 2020-08-17 17.04.35.png

 JSON解析

サーバから送られてきたJSON文字列を
クライアントで使用するには、解析が必要。

  • パース(parse)とも言います。
  • 最初に、import Foundationの宣言が必要。
  • JSONSerialization.jsonObjectを使用する。

先ほどのtaskに色々追記していきます。

let task = URLSession.shared.dataTask(with: url) { 
(data, responce, error) in
       // ここに色々追記します。
}

 error

errorがnilじゃなければ、エラーメッセージを表示して、
returnで処理を終了。

guard error == nil else {
    debugPrint(error.debugDescription)
    return
}
// Guardは前述

 Sample

追記予定。

おしまい。

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

Google Maps もダークモード対応しようね

スタイルを適用する

isDarkMode のようなパラメータがあるわけではなく、自分で用意したスタイルを適用する形になります。
背景は黒、路線は青、文字は白などを個々に指定する必要があります。

指定方法としては JSON ファイルで指定する方法String で直接指定する方法 がありますが本記事では前者について紹介します。
実装方法は大きく変わりませんが、可読性や Android との共通化の観点から JSON 形式で指定することをお勧めします。

JSON で指定する

style.json などの JSON ファイルをローカルに用意し、Map の mapStyle に適用します。

// Map のインスタンス
let mapView = GMSMapView()

// ローカルファイルの URL を取得
if let styleURL = Bundle.main.url(forResource: "style", withExtension: "json") {
    // スタイルを適用
    mapView.mapStyle = try GMSMapStyle(contentsOfFileURL: styleURL)
}

パラメータ

下記 3 つの要素を 1 セットとして、スタイル指定を配列で羅列していく形になります。

  • featureType - 地理的な要素 (国境、施設、道 etc.)
  • elementType - 地図上の要素 (線、文字)
  • stylers - スタイル (色、明度、表示 / 非表示 etc.)

例えば 一般道の名前を白くする という指定は以下のようになります。

style.json
[
  {
    "featureType": "road.local",
    "elementType": "labels",
    "stylers": [
      {
        "color": "#FFFFFF"
      }
    ]
  }
]

各パラメータの詳細はこちらから。
https://developers.google.com/maps/documentation/ios-sdk/style-reference

テンプレートあります

パラメータを 1 から全て指定する事は根気がいる作業です。
実際の現場でデザイナーさんに細かく指定してもらう事や確認してもらう事は困難でしょう。

というわけで、スタイルを生成するサイトが公式であります。
https://mapstyle.withgoogle.com

Dark, Night, Aubergine からデザイナーさんと相談して選ぶ or カスタマイズすれば、簡単にダークモード対応が完了します。

備考

Android エンジニアさんは こちら をどうぞ。
iOS と同様に対応できるので、本記事をご参考いただければ幸いです。

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

iosでプッシュ通知の証明書を.p12形式でexportしようとしたら.p12形式が選択できない問題について

備忘録

この記事を参考にしました

まとめると
キーチェーンアクセスのログインからではなく
分類の自分の証明書から選んで右クリックで.p12を使用できる

スクリーンショット 2020-08-17 12.33.22.png

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

【Windows編】FlutterとCodemagicを使ってAndroid/iOSアプリを生成するまでの手順

0. はじめに

勉強不足なもので、スマホアプリの開発は最近全然やっていませんでした:rolling_eyes:
私の自宅にはWindows1台、Mac1台あるのですが、性能的にも新しさ的にもWindowsの方が上ですので、開発は主にWindowsで行っております。
でも、iOSアプリも作りたいなーと思い(スマホはiPhoneなので)、最近流行っているFlutterを使ってみることに。
せっかくなのでアウトプットとして、環境構築手順やらCodemagicについて当記事にまとめました。

1. Flutterとは

  • Googleによって開発されたクロスプラットフォームの開発技術。
  • つまりはReact NativeXamarinのライバル。
  • Android Studio or XCode or VSCodeFlutter SDKをぶち込んで開発する。
  • Dart言語を使って実装する。

2. Codemagicとは

  • Flutter専用のCI / CDサービス。
  • ソースをPushすることで、自動でビルドやテストを行ってくれる。
  • iOSのビルドはMacでしかできないが、Codemagicが代わりにビルドしてくれるらしい。
  • GitHub or GitLab or Bitbucketを連携して利用する。

3. 環境構築

今回はAndroid Studioを使って環境構築していきます。
※Android Studioの環境構築手順については割愛します。私はVer4.0.1を日本語化したものを利用しました。

3-1. Flutterのダウンロード

以下の公式サイトにアクセスし、最新VerのFlutterをダウンロード+解凍します。

1.PNG

3-2. パスを通す

解凍したFlutterを任意のフォルダに配置し、flutter\binまでのパスを通します。

C:\Users\xxx\flutter\bin

3-3. 環境診断コマンドの実行

コマンドプロンプトでflutter doctorを実行することで、Flutterを作成するための環境が整っているかチェックをしてくれるようです。
ここで一度コマンドを実行してみましょう。

PS C:\Users\xxx> flutter doctor
Doctor summary (to see all details, run flutter doctor -v):

[√] Flutter (Channel stable, v1.17.5, on Microsoft Windows [Version 10.0.18363.959], locale ja-JP)

[!] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
    X Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses
[!] Android Studio (version 3.2)
    X Flutter plugin not installed; this adds Flutter specific functionality.
    X Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code, 64-bit edition (version 1.47.3)
    X Flutter extension not installed; install from
      https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter
[!] Connected device
    ! No devices available

! Doctor found issues in 4 categories.

3-4. issueの解消

! (issue)が出たようなので解消してきます。
※「! No devices available」については、実機 or エミュレータを起動していないため表示されているだけです。現状は無視でOKです。

3-4-1. [!] Android toolchain...

X Android licenses not accepted.

Androidのライセンス確認ができないとのことなので、エラーにもある通り、以下のコマンドを実行すればOKです。

flutter doctor --android-licenses

3-4-2. [!] Android Studio...

X Flutter plugin not installed; this adds Flutter specific functionality.
X Dart plugin not installed; this adds Dart specific functionality.

FlutterとDartのプラグインがないとのことなので、以下の手順でインストールすればOKです。

① Android Studioを起動。
② [構成] > [プラグイン]を選択。
③ Flutterをインストール。
 ※FlutterをインストールすればDartも一緒に入ってきますので、一気に2つ「X」を解消できます:innocent:

1.PNG

3-4-3. [!] VS Code...

X Flutter extension not installed; install from xxx

VSCodeにFlutterがないとのことです。今回はAndroid Studioを使うので関係ないですが、「X」なしの方がかっこいいので一応解消しておきましょう。以下の手順でインストールすればOKです。

① VSCodeを起動し、[拡張機能]を選択。
② Flutterをインストール。

3.PNG

3-5. 環境診断コマンドの再実行

ここでもう一度flutter doctorを実行してみましょう。

PS C:\Users\ikuya> flutter doctor
Doctor summary (to see all details, run flutter doctor -v):

[√] Flutter (Channel stable, v1.17.5, on Microsoft Windows [Version 10.0.18363.959], locale ja-JP)

[√] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[√] Android Studio (version 4.0)
[√] VS Code, 64-bit edition (version 1.47.3)
[!] Connected device
    ! No devices available

! Doctor found issues in 1 category.

やった!解消されました:clap:これで環境はOKです:relaxed:

4. プロジェクト作成

環境が整った所で、Android StudioにてFlutterプロジェクトを作成していきます。
※作成したプロジェクトはGitHubに登録してください。(すみません、手順は割愛で・・・)

①「新規 Flutter プロジェクトの開始」を選択します。
1.PNG

② 「Flutterアプリケーション」を選択します。
2.PNG

③ プロジェクト名等を入力し、「次へ」を押下します。
3.PNG

④ パッケージ名を入力し、「完了」を押下します。
4.PNG

⑤ 完了!
キャプチャ.PNG

これでFlutterの空プロジェクトを作成できました:blush:

5. Codemagicの設定

続いてCodemagicの設定を行っていきます。

上記にアクセスし、以下の手順で進めます。
(黒背景に黄色の蛍光ペンは非常に見づらかった・・・:disappointed_relieved:)

① 「Sine up」を押下します。
1.PNG

② 「Join using GitHub」を押下します。
2.PNG

③ GitHub上のFlutterプロジェクトの「Set up build」を押下します。
3.PNG

④ 「Start your first build」を押下します。
4.PNG

⑤ 「Start new build」を押下します。
5.PNG

⑥ ビルド中の画面になります。完了するまで待ちましょう。(3分くらい?:ramen:)
6.PNG

⑦ 完了!
7.PNG

なんとこれだけでアプリケーションファイルが出来上がりました:astonished:すごい!!

6. 動作確認

apkファイルをダウンロードしてエミュレータにインストールしてみた所、うまく動きました:blush:

1.PNG

7. おわりに

環境構築にしろCodemagicの設定にしろ、結構簡単に行えました!
環境によってはflutter doctorで他にもissueが出るかもしれませんので、メッセージでググってみてください:innocent:
次回はDartに踏み込んでアプリを開発していきたいと思います。

↑な感じでmicroCMSとも組み合わせたいなー。
何よりAndroidの実機がほしい:sob::sob::sob:

8. 参考

今回は以下の記事を参考にさせて頂きました。ありがとうございます!

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

【Windows編】FlutterとCodemagicを使ってAndroid/iOSアプリを生成するまで

0. はじめに

勉強不足なもので、スマホアプリの開発は最近全然やっていませんでした:rolling_eyes:
私の自宅にはWindows1台、Mac1台あるのですが、性能的にも新しさ的にもWindowsの方が上ですので、開発は主にWindowsで行っております。
でも、iOSアプリも作りたいなーと思い(スマホはiPhoneなので)、最近流行っているFlutterを使ってみることに。
せっかくなのでアウトプットとして、環境構築手順やらCodemagicについて当記事にまとめました。

1. Flutterとは

  • Googleによって開発されたクロスプラットフォームの開発技術。
  • つまりはReact NativeXamarinのライバル。
  • Android Studio or XCode or VSCodeFlutter SDKをぶち込んで開発する。
  • Dart言語を使って実装する。

2. Codemagicとは

  • Flutter専用のCI / CDサービス。
  • ソースをPushすることで、自動でビルドやテストを行ってくれる。
  • iOSのビルドは基本Macでしかできないが、なんとCodemagicが代わりにビルドしてくれるらしい。
  • GitHub or GitLab or Bitbucketを連携して利用する。

3. 環境構築

今回はAndroid Studioを使って環境構築していきます。
※Android Studioの環境構築手順については割愛します。私はVer4.0.1を日本語化したものを利用しました。

3-1. Flutterのダウンロード

以下の公式サイトにアクセスし、最新VerのFlutterをダウンロード+解凍します。

1.PNG

3-2. パスを通す

解凍したFlutterを任意のフォルダに配置し、flutter\binまでのパスを通します。

C:\Users\xxx\flutter\bin

3-3. 環境診断コマンドの実行

コマンドプロンプトでflutter doctorを実行することで、Flutterを作成するための環境が整っているかチェックをしてくれるようです。
ここで一度コマンドを実行してみましょう。

PS C:\Users\xxx> flutter doctor
Doctor summary (to see all details, run flutter doctor -v):

[√] Flutter (Channel stable, v1.17.5, on Microsoft Windows [Version 10.0.18363.959], locale ja-JP)

[!] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
    X Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses
[!] Android Studio (version 3.2)
    X Flutter plugin not installed; this adds Flutter specific functionality.
    X Dart plugin not installed; this adds Dart specific functionality.
[!] VS Code, 64-bit edition (version 1.47.3)
    X Flutter extension not installed; install from
      https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter
[!] Connected device
    ! No devices available

! Doctor found issues in 4 categories.

3-4. issueの解消

! (issue)が出たようなので解消してきます。
※「! No devices available」については、実機 or エミュレータを起動していないため表示されているだけです。現状は無視でOKです。

3-4-1. [!] Android toolchain...

X Android licenses not accepted.

Androidのライセンス確認ができないとのことなので、エラーにもある通り、以下のコマンドを実行すればOKです。

flutter doctor --android-licenses

3-4-2. [!] Android Studio...

X Flutter plugin not installed; this adds Flutter specific functionality.
X Dart plugin not installed; this adds Dart specific functionality.

FlutterとDartのプラグインがないとのことなので、以下の手順でインストールすればOKです。

① Android Studioを起動。
② [構成] > [プラグイン]を選択。
③ Flutterをインストール。
 ※FlutterをインストールすればDartも一緒に入ってきますので、一気に2つ「X」を解消できます:innocent:

1.PNG

3-4-3. [!] VS Code...

X Flutter extension not installed; install from xxx

VSCodeにFlutterがないとのことです。今回はAndroid Studioを使うので関係ないですが、「X」なしの方がかっこいいので一応解消しておきましょう。以下の手順でインストールすればOKです。

① VSCodeを起動し、[拡張機能]を選択。
② Flutterをインストール。

3.PNG

3-5. 環境診断コマンドの再実行

ここでもう一度flutter doctorを実行してみましょう。

PS C:\Users\ikuya> flutter doctor
Doctor summary (to see all details, run flutter doctor -v):

[√] Flutter (Channel stable, v1.17.5, on Microsoft Windows [Version 10.0.18363.959], locale ja-JP)

[√] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[√] Android Studio (version 4.0)
[√] VS Code, 64-bit edition (version 1.47.3)
[!] Connected device
    ! No devices available

! Doctor found issues in 1 category.

やった!解消されました:clap:これで環境はOKです:relaxed:

4. プロジェクト作成

環境が整った所で、Android StudioにてFlutterプロジェクトを作成していきます。
※作成したプロジェクトはGitHubに登録してください。(すみません、手順は割愛で・・・)

①「新規 Flutter プロジェクトの開始」を選択します。
1.PNG

② 「Flutterアプリケーション」を選択します。
2.PNG

③ プロジェクト名等を入力し、「次へ」を押下します。
3.PNG

④ パッケージ名を入力し、「完了」を押下します。
4.PNG

⑤ 完了!
キャプチャ.PNG

これでFlutterの空プロジェクトを作成できました:blush:

5. Codemagicの設定

続いてCodemagicの設定を行っていきます。

上記にアクセスし、以下の手順で進めます。
(黒背景に黄色の蛍光ペンは非常に見づらかった・・・:disappointed_relieved:)

① 「Sine up」を押下します。
1.PNG

② 「Join using GitHub」を押下します。
2.PNG

③ GitHub上のFlutterプロジェクトの「Set up build」を押下します。
3.PNG

④ 「Start your first build」を押下します。
4.PNG

⑤ 「Start new build」を押下します。
5.PNG

⑥ ビルド中の画面になります。完了するまで待ちましょう。(3分くらい?:ramen:)
6.PNG

⑦ 完了!
7.PNG

なんとこれだけでアプリケーションファイルが出来上がりました:astonished:すごい!!

6. 動作確認

apkファイルをダウンロードしてエミュレータにインストールしてみた所、うまく動きました:blush:

1.PNG

7. おわりに

環境構築にしろCodemagicの設定にしろ、かなり簡単に行えましたね!
環境によってはflutter doctorで他にもissueが出るかもしれませんので、メッセージでググってみてください:innocent:
次回はDartに踏み込んでアプリを開発していきたいと思います。

↑な感じでmicroCMSとも組み合わせたいなー。
何よりAndroidの実機がほしい:sob::sob::sob:

8. 参考

今回は以下の記事を参考にさせて頂きました。ありがとうございます!

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

SwiftのFunctionを学んだ。【returnとか、paramとか】

 Functionの初歩。

func(関数)を学んだので、簡単におさらい。

 Functionを何故使うのか。

amazonのような買い物アプリにて、
ユーザーの買い物カゴの合計金額を計算する箇所が5つあるとき、

  • 5箇所で同じコードを書くのは無駄。
  • コード量が増えて、プログラム自体が複雑で読みにくくなる。

処理が1回しかないものでも、functionには利点あり。

  • functionにしてマトめると、コード量がかなり多いとき読みやすい。
  • function名を適切につけて、判別しやすい。

 パラメータと引数の違い

意思疎通にはそれほど困らないけど・・・
「引数 == パラメータ」ではない

  • パラメータ (仮引数)は、関数に受け渡されるものの宣言
  • 引数は、関数に渡した実際の
// Funtion with parameters

func declare(name: String) {
    print(name)
}

declare(name: "Shimura") // 呼び出し
declare(name: "Ken") // 呼び出し

「志村」と「けん」が引数で、nameがパラメータ
パラメータと引数の違い

因みに、「ひきすう」と読みます。

 関数3パターン、おさらい。

 with no parameters?

func declareName() {
    print("MyName")
}
declareName() // 呼び出し

 with parameters?

func declare(name: String) {
    print(name)
}
declare(name: "Shimura") // 呼び出し
declare(name: "Ken") // 呼び出し

 with a return value?

func 一日の秒数() -> Int {
    return 24 * 60 * 60
}

let 秒数 = 一日の秒数() // 呼び出し & 代入 (=インスタンス化)
print("一日は\(秒数)秒!")  // 一日は86400秒!

 with parameters and a return value?

func createFullName(firstName: String, lastName: String) -> String {
    return firstName + " " + lastName
}

//let fullName =  createFullName(firstName: String, lastName: String)
let fullName = createFullName(firstName: "Suzuki", lastName: "Ichiro") // 呼び出し & 代入 (=インスタンス化)
print(fullName) // Suzuki Ichiro

 returnを使った関数

一方的にただ呼び出す関数も便利ですが、(上記2つのコード??)

関数の中でいろいろな処理をさせて、その「結果」を貰いたいときがあります。
要するに、呼び出しに対する「返事」が欲しいときです。

 with a return value?

swift6_04_01.jpg

 with parameters and a return value?

swift6_04_07.jpg

戻り値を持つ関数
書き方は、こんな感じ。

func 関数の名前() -> 戻り値の型 {
    // 実行する処理
    return 戻り値
}

具体例。

func 一日の秒数() -> Int {
    return 24 * 60 * 60
}

でも、上記コードだけでは実行されない。
「関数の呼び出し」を、変数or定数に代入(=インスタンス化

let seconds = 一日の秒数() // インスタンス化
print("一日は\(seconds)秒!")  // 一日は86400秒!

  • \()」の中に変数を入れると、その内容が埋め込まれます。

  • Swiftでは『戻り値』のデータ型を指定する 必要があります。
    -> データ型』で指定。これがないとエラー。

  • {}内でreturnが実行されると 関数内の処理は終了なので、
    関数{}の中の、一番最後に書く。

func 一日の秒数() -> Int {   // 今回はInt型。(Integer: 整数)
    return 24 * 60 * 60
    print("Hello World")         // エラー。 Code after 'return' will never be executed
}

let seconds= 一日の秒数() 
print("一日は\(seconds)秒!")  // 『\()』を使って、変数secondsを埋め込み。

おしまい。

 参考サイト

[Swift初心者向け] function(メソッド)の使い方

Swiftの関数、引数、戻り値の基本的な書き方と使い方

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

[2020年版]tabBarControllerの設定方法

tabBarControllerとは?

image.png
tabBarControllerとは、よくiPhoneで見かける液晶下部にあるボタンのことを指します。
このBarの働きは複数画面と接続し、UITabBar(以下、tabBar)に並べられたボタンで画面の切り替えを行う部品であります。

tabBarの実装方法

Xcode右上のプラスボタン(+)をクリックしてtabBarControllerを選択
すると以下画像のように3つの画面が出てくるのでそのまま配置。
image.png

tabBarのテキスト変更方法

以下の画像のようにテキストを変更したいtabBarをフォーカスして、右サイドバーのTitleのテキストを編集。
image.png
すると、左の液晶のtabBar左側のボタンを継承して右上の液晶と連携していることが分かる。(今回は'テキスト編集'と編集)
image.png

tabBarアイコン変更方法

アイコンも同様に変更したいtabBarをフォーカスして、右サイドバーで編集。なお、アイコンの場合はImageを変更すればアイコンを変更できる。
image.png
ちなみに、よく見かけるアイコンはAppleがすでに用意してくれています。
image.png

実装確認

ここまで編集をおこないシュミレーター を起動するとアイコンとテキストの両方の編集が成功しているとおもいます。
これだけの設定で画面遷移を実装できる点、Xcodeは素晴らしいですよね!しかも割と直感的に操作ができる!

最後に

今回はtabBarControllerに関してアウトプットを行いました。
久々の更新となりましたが今後も継続していきますので応援宜しくお願いします!

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