- 投稿日:2020-08-17T23:53:02+09:00
[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を削除。
以上
軽ーく調べてみたけど、手動で追加したときにファイルが重複しちゃってるみたいな感じっぽい
詳しいこと知ってる人いたら教えてください。
- 投稿日:2020-08-17T23:52:39+09:00
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() } }
- 投稿日:2020-08-17T21:27:12+09:00
ask me! appのログイン画面の実装
1 この記事の内容
ask me! appでのログイン画面の実装方法について記載します。画面レイアウトは下記のとおりです。
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.dartvoid 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.dartnew GestureDetector( onTap:() { _focusNodePwd.unfocus(); },つまりOnTapイベントでパスワード入力欄に紐づけられたFocusNodeをUnFocusします。
5 TextFormFieldのヘルパーウィジェット
キーバッドが表示されたときに、TextFormFieldが隠れてしまう場合があります。
それを防ぐために下記サイトのヘルパーウィジェットを利用しました。6 キーパッドのDoneボタン
キーパッドでの入力完了後にはDoneボタンを押すことでキーパッドを閉じるようにしました。
Doneボタンの表示には下記のライブラリを使用しました。
Keyboard Actions:https://pub.dev/documentation/keyboard_actions/latest/
7 OKボタン押下時の動作
OKボタン押下時には、入力されたパスワードと予め設定された正解パスワードが合致するかどうか判定しますが、
その際の待ち時間は下記画面のようにCircularProgressIndicatorを使用します。ソースコードは下記のとおりです。
login.dartvar 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 ), ), ), ), ], ), ), ), ], ), );
- 投稿日:2020-08-17T20:56:41+09:00
【Flutter, Dart】ミュータブルとイミュータブル、そしてfreezed
はじめに
本記事は The Mutability Tax をベースにしています。
意訳・抜粋しまくったので翻訳記事と呼ぶには忍びないですが、記述の足らない箇所があれば元の記事を参照してください。
筆者の David Morgan 氏はGoogleのソフトウェアエンジニアです。
元記事の公開は2019年7月15日です。
本文中に登場するコードはDart
で記述されています。3点要点
- Mutableはダメです
- Immutableも正しく扱わないとデメリット大きいです
- コード生成(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} です。');
biggestCitiesRanking
のfirst
を指定しているのですから、最大の都市である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.copyWith(age: customer.visit + 1);上司 ? : 甘いねん。Dartは
null
に弱いねん。このメソッドやとvisit
にnull
が渡されたことを認識できひんねん。// 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_value
はDart1
用に作られており、順次v2に対応しているようですが、所々未対応の部分があるようです。
3. コード生成がかなり遅いです。※ コード生成については
freezed
もそれなりに遅いですが、その問題に対してはmonoさんが素晴らしいTipsをtweetしておられました。これには作者であるRemi氏も同意していました。Clever idea!
— Remi Rousselet (@remi_rousselet) July 18, 2020
Honestly, I wonder if this sort of optimization could be built right into package:build
It's probably analyzing files that don't need to be analyzed
新人 ? : なるほど、
freezed
使ってみます!上司 ? : もう人力でやろうとすんなよ!
P.S.
誤りがありましたらコメントにてご指摘いただけますと幸いです。
- 投稿日:2020-08-17T20:56:41+09:00
【Flutter, Dart】ミュータブルの代償ととイミュータブルの代償、そしてfreezed
はじめに
本記事は The Mutability Tax をベースにしています。
意訳・抜粋しまくったので翻訳記事と呼ぶには忍びないですが、記述の足らない箇所があれば元の記事を参照してください。
筆者の David Morgan 氏はGoogleのソフトウェアエンジニアです。
元記事の公開は2019年7月15日です。
本文中に登場するコードはDart
で記述されています。The Mutability Tax では、それぞれの設計によって生じるコードメンテナンスコストのことを Tax(税金) と形容しています。
本記事では 代償 と表現します。3点要点
- Mutableはバグを生みやすく、遅いです
- Immutableも正しく扱わないとコードが肥大化してバグを生みやすく、遅くなります
- コード生成(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} です。');
biggestCitiesRanking
のfirst
を指定しているのですから、最大の都市である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.copyWith(age: customer.visit + 1);上司 ? : 甘いねん。Dartは
null
に弱いねん。このメソッドやとvisit
にnull
が渡されたことを認識できひんねん。// 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_value
はDart1
用に作られており、順次v2に対応しているようですが、所々未対応の部分があるようです。
3. コード生成がかなり遅いです。※ コード生成については
freezed
もそれなりに遅いですが、その問題に対してはmonoさんが素晴らしいTipsをtweetしておられました。これには作者であるRemi氏も同意していました。Clever idea!
— Remi Rousselet (@remi_rousselet) July 18, 2020
Honestly, I wonder if this sort of optimization could be built right into package:build
It's probably analyzing files that don't need to be analyzed
新人 ? : なるほど、
freezed
使ってみます!上司 ? : もう人力でやろうとすんなよ!
P.S.
誤りがありましたらコメントにてご指摘いただけますと幸いです。
- 投稿日:2020-08-17T17:39:58+09:00
【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分後に自動更新が終了します。
- 投稿日:2020-08-17T17:05:31+09:00
iOS Keyboard Extensionでキーボードを介さない操作を検知する
iOSの純正キーボードは超絶多機能です。ちょっと考えただけでもこんな機能がついています。
- 入力中の文字を薄い青色(ダークモードでは黄色)でハイライトする。
- 点滅するカーソルをドラッグすることによるカーソルの移動を検知し、変換候補を変更する。
- 点滅するカーソルをドラッグすることによるカーソルの移動を検知し、入力中の範囲から外に出られないようにする。
- 入力範囲外をタップした場合入力中の文字を確定する。
- 選択されているテキストを取得し、再変換する。
- ペースト操作が行われた場合検知し、入力中の文字を確定する。 これは高度な機能です。実際私が日本語対応のkeyboard extensionを漁った限り、純正キーボードと同じ挙動を再現できているものはほとんどありませんでした。
そこでこれらの機能をできる限り実現すべく、いろいろ努力した結果をまとめます。
入力中の文字のハイライト
いきなり残念なお知らせですが、これは今は断念するのが正解です。
この機能の実現にはsetMarkedText()
というメソッドが利用できます。が、これを実現すると他の無数の機能が死にます。
詳しくはこちらを参照してください:UITextDocumentProxyのsetMarkedTextを(まだ)使ってはいけない。 - Qiitaそれ以外
- カーソル移動検知(→カーソル移動制限など)
- ペースト検知
- 範囲外タップ検知
- 選択検知
- 選択解除検知
- カット検知
などがどうにかできました。
大体、documentContextBeforeInput
とdocumentContextAfterInput
、それにselectedText
をゴリ押しで取得していくとどうにかなりました。UIInputViewController
のtextWillChange
とtextDidChange
は、引数の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.swiftclass 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.swiftfunc registerSomethingWillChange(left:String, center:String, right:String){ self.tempTextData = (left:left, center:center, right:right) }変化が起こった場合、変化前と変化後の状態を比較することで状況を判断します。このロジックは愚直に実装しました。私の知る限りこういう諸々の動作を検知するための機構はUIKitでは提供されていません。
registerSomethingDidChange.swiftfunc 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の純正キーボードでもちょっと怪しい挙動を発見しました。
入力中にカーソルを真ん中あたりまで移動し、その上で入力範囲外をタップするとカーソルの後の部分が全て消えて確定扱いになります。一方入力中にカーソルを真ん中あたりまで移動し、さらに文字を入力、または消去する操作を行ってから入力範囲外をタップすると単に確定扱いになります。
あまり自然な挙動とは思えないので、バグの可能性が高いと思います。きっとApple純正キーボードの開発者も相当苦労しているんでしょうね。
- 投稿日:2020-08-17T14:56:20+09:00
ダークモード対応の罠
去年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
で色を入れるのは大丈夫なの?大丈夫です
ライトモード時はライト、ダークモード時はダークの色を入れてくれます(最終的に)
試しにアセットカタログのファイルで以下の色を用意して
- 色
UITraitCollection.current.userInterfaceStyle
の要素をログで追って確認し、ライトモードでバッググラウンド時に入ってみて検証してみました
dark Optional([1.0, 1.0, 1.0, 1.0]) light Optional([0.0, 0.0, 0.0, 1.0])2回呼ばれますが、
ライトモードだとダーク→ライト
の順番に呼ばれるので最終的にライトの色が入ります
同様にダークモードでもライト→ダーク
と呼ばれダークの色が入るので大丈夫です
- 投稿日:2020-08-17T13:15:07+09:00
APIデータを、JSON形式で取得。【JSON解析】
JSONとは?
JavaScript Object Notation ?
データフォーマット (データ形式)
の一つ。- キーと値のペアで構成。(=辞書型)
- データの記述量が少ないので、読み込みが速い。
- JavaScriptとの親和性が高い。
- JavaScriptに限らず、データのやりとりに広く使われる。
キーは常に文字列ですが、
値にはString
,Int
,Bool
,配列
,null
なども使えます。データフォーマット (データ形式)
現在の主流のデータ形式は、
XML
,JSON
,CSV
の3つ。
JSONが一番軽く、使い勝手がよく、多く使われる。データフォーマットとは?XML、JSON、CSVの違いや特徴についてわかりやすく解説するよ
マークアップ言語
タグで囲む(マークをつける)ことで構造を表現する言語。
<Title>This is Title</Title>HTMLとXMLは、どちらもマークアップ言語。(Markup Language)
違いは、上記サイト参照。JSON と API
JSON
は、
APIを介して送信されるデータを構造化するための、最も一般的なデータ形式。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
Webリクエストをしてみる。?
https://swapi.dev/api/people/1
のデータを
JSON形式で取得してみたいと思います。URLSessionを使って、HTTP通信する。
Webリクエストを作成して、JSONデータを取得します。
url
に取得したいapiのリクエストURLを設定するURLSession
を使ってtask
を作る- JSONデータを取得
task.resume()
でタスクを開始するコード
Services
フォルダ >PersonApi.swift
PersonApi.swiftclass 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 例外を投げる
URLSessionとは?
関連するネットワーク上のデータ転送処理群(=URLSessionTask)をまとめるクラス。
1つのSessionで繰り返しURLSessionTaskの作成が可能。URLSessionTaskとは?
URLで特定されるリソースを実際に取得し、アプリにデータを返却したり、
リモートサーバからファイルのダウンロードやアップロードを行う。
Utility
フォルダ >Constants.swift
ファイルに、 リンクをまとめた。Constants.swiftlet URL_BASE = "https://swapi.dev/api/" let PERSON_URL = URL_BASE + "people/1/"
Controller
フォルダ >SelectPersonVC.swift
=ViewControllerSelectPersonVC.swiftimport UIKit class SelectPersonVC: UIViewController { var personApi = PersonApi() // クラスをインスタンス化 override func viewDidLoad() { super.viewDidLoad() personApi.getRandomPersonUrlSession() // メソッド呼び出し } }結果
data
とresponse
が、無事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~は、サーバー側のエラー。JSON解析 ?
サーバから送られてきたJSON文字列を、クライアントで使用するには、
解析
が必要。
- パース(parse)とも言います。
- 最初に、
import Foundation
の宣言が必要。JSONSerialization.jsonObject
を使用する。JSONSerialization.jsonObject?
JSONSerializationは、
Apple標準フレームワークのFoundationに含まれている、便利なライブラリ。let json = JSONSerialization.jsonObject(with: data, options[])Serializationとは...ざっくりですが、
オブジェクトの状態をStreamの状態に変換すること。
(1バイトずつ読み書きできる、データ構造)この機能を使うと、簡単にインスタンスを外部記憶装置などに保存し、
インスタンスの情報を永続化することができる。throw
一般式.swiftfunc メソッド名(引数) throws -> 戻り値 { // エラーを投げる可能性のある処理 }
jsonObject
をOption+クリックすると、Declaration(宣言)が表示されるのですが、// jsonObjectメソッドの、Declarationが表示されます。 class func jsonObject( with data: Data, options opt: JSONSerialization.ReadingOptions = [] ) throws -> Any // Anyは、クラス・構造体・列挙型、「すべての型のインスタンス」を指します。
throws
とあるので、jsonObjectメソッドは、エラーを投げる可能性があります。そこで、エラーを受け取る必要があります。
do-catchとtryで、エラーを受け取る
一般式.swiftdo { // エラーを投げる可能性のある処理、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.swiftfunc 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.swiftstruct 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.swiftprivate 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, ...) でもOKprivateの全体コード
自動補完もしてくれないから、とても面倒。
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データを、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データの
name
とheight
を取得。
Luke Skywalker、意外と背が小さい。実行結果.Luke Skywalker 172おしまい
JSONでAPIデータ取得するの大変だなあ。
Main.storyboardとの連携や、SwiftyJSONについては、
のちに追記予定。
- 投稿日:2020-08-17T13:15:07+09:00
APIデータを、JSON形式で取得。【URLSession】
JSONとは?
JavaScript Object Notation ?
データフォーマット (データ形式)
の一つ。- キーと値のペアで構成。(=辞書型)
- データの記述量が少ないので、読み込みが速い。
- JavaScriptとの親和性が高い。
- JavaScriptに限らず、データのやりとりに広く使われる。
キーは常に文字列ですが、
値にはString
,Int
,Bool
,配列
,null
なども使えます。データフォーマット (データ形式)
現在の主流のデータ形式は、
XML
,JSON
,CSV
の3つ。
JSONが一番軽く、使い勝手がよく、多く使われる。データフォーマットとは?XML、JSON、CSVの違いや特徴についてわかりやすく解説するよ
マークアップ言語
タグで囲む(マークをつける)ことで構造を表現する言語。
<Title>This is Title</Title>HTMLとXMLは、どちらもマークアップ言語。(Markup Language)
違いは、上記サイト参照。JSON と API
JSON
は、
APIを介して送信されるデータを構造化するための、最も一般的なデータ形式。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
Webリクエストをしてみる。?
https://swapi.dev/api/people/1
のデータを
JSON形式で取得してみたいと思います。URLSessionを使って、HTTP通信する。
Webリクエストを作成して、JSONデータを取得します。
url
に取得したいapiのリクエストURLを設定するURLSession
を使ってtask
を作る- JSONデータを取得
task.resume()
でタスクを開始するコード
Services
フォルダ >PersonApi.swift
PersonApi.swiftclass 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 例外を投げる
URLSessionとは?
関連するネットワーク上のデータ転送処理群(=URLSessionTask)をまとめるクラス。
1つのSessionで繰り返しURLSessionTaskの作成が可能。URLSessionTaskとは?
URLで特定されるリソースを実際に取得し、アプリにデータを返却したり、
リモートサーバからファイルのダウンロードやアップロードを行う。
Utility
フォルダ >Constants.swift
ファイルに、 リンクをまとめた。Constants.swiftlet URL_BASE = "https://swapi.dev/api/" let PERSON_URL = URL_BASE + "people/1/"
Controller
フォルダ >SelectPersonVC.swift
=ViewControllerSelectPersonVC.swiftimport UIKit class SelectPersonVC: UIViewController { var personApi = PersonApi() // クラスをインスタンス化 override func viewDidLoad() { super.viewDidLoad() personApi.getRandomPersonUrlSession() // メソッド呼び出し } }結果
Data
とResponse
が、無事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~は、サーバー側のエラー。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
追記予定。
おしまい。
- 投稿日:2020-08-17T13:10:56+09:00
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.comDark, Night, Aubergine からデザイナーさんと相談して選ぶ or カスタマイズすれば、簡単にダークモード対応が完了します。
備考
Android エンジニアさんは こちら をどうぞ。
iOS と同様に対応できるので、本記事をご参考いただければ幸いです。
- 投稿日:2020-08-17T12:33:56+09:00
iosでプッシュ通知の証明書を.p12形式でexportしようとしたら.p12形式が選択できない問題について
備忘録
まとめると
キーチェーンアクセスのログインからではなく
分類の自分の証明書から選んで右クリックで.p12を使用できる
- 投稿日:2020-08-17T09:38:30+09:00
【Windows編】FlutterとCodemagicを使ってAndroid/iOSアプリを生成するまでの手順
0. はじめに
勉強不足なもので、スマホアプリの開発は最近全然やっていませんでした
私の自宅にはWindows1台、Mac1台あるのですが、性能的にも新しさ的にもWindowsの方が上ですので、開発は主にWindowsで行っております。
でも、iOSアプリも作りたいなーと思い(スマホはiPhoneなので)、最近流行っているFlutterを使ってみることに。
せっかくなのでアウトプットとして、環境構築手順やらCodemagicについて当記事にまとめました。1. Flutterとは
- Googleによって開発されたクロスプラットフォームの開発技術。
- つまりはReact NativeやXamarinのライバル。
- Android Studio or XCode or VSCodeにFlutter 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をダウンロード+解凍します。
3-2. パスを通す
解凍したFlutterを任意のフォルダに配置し、flutter\binまでのパスを通します。
C:\Users\xxx\flutter\bin3-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-licenses3-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」を解消できます
3-4-3. [!] VS Code...
X Flutter extension not installed; install from xxx
VSCodeにFlutterがないとのことです。今回はAndroid Studioを使うので関係ないですが、「X」なしの方がかっこいいので一応解消しておきましょう。以下の手順でインストールすればOKです。
① VSCodeを起動し、[拡張機能]を選択。
② Flutterをインストール。
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.やった!解消されました
これで環境はOKです
4. プロジェクト作成
環境が整った所で、Android StudioにてFlutterプロジェクトを作成していきます。
※作成したプロジェクトはGitHubに登録してください。(すみません、手順は割愛で・・・)①「新規 Flutter プロジェクトの開始」を選択します。
これでFlutterの空プロジェクトを作成できました
5. Codemagicの設定
続いてCodemagicの設定を行っていきます。
上記にアクセスし、以下の手順で進めます。
(黒背景に黄色の蛍光ペンは非常に見づらかった・・・)
③ GitHub上のFlutterプロジェクトの「Set up build」を押下します。
④ 「Start your first build」を押下します。
⑥ ビルド中の画面になります。完了するまで待ちましょう。(3分くらい?
)
なんとこれだけでアプリケーションファイルが出来上がりました
すごい!!
6. 動作確認
apkファイルをダウンロードしてエミュレータにインストールしてみた所、うまく動きました
7. おわりに
環境構築にしろCodemagicの設定にしろ、結構簡単に行えました!
環境によってはflutter doctor
で他にもissueが出るかもしれませんので、メッセージでググってみてください
次回はDartに踏み込んでアプリを開発していきたいと思います。↑な感じでmicroCMSとも組み合わせたいなー。
何よりAndroidの実機がほしい8. 参考
今回は以下の記事を参考にさせて頂きました。ありがとうございます!
- 投稿日:2020-08-17T09:38:30+09:00
【Windows編】FlutterとCodemagicを使ってAndroid/iOSアプリを生成するまで
0. はじめに
勉強不足なもので、スマホアプリの開発は最近全然やっていませんでした
私の自宅にはWindows1台、Mac1台あるのですが、性能的にも新しさ的にもWindowsの方が上ですので、開発は主にWindowsで行っております。
でも、iOSアプリも作りたいなーと思い(スマホはiPhoneなので)、最近流行っているFlutterを使ってみることに。
せっかくなのでアウトプットとして、環境構築手順やらCodemagicについて当記事にまとめました。1. Flutterとは
- Googleによって開発されたクロスプラットフォームの開発技術。
- つまりはReact NativeやXamarinのライバル。
- Android Studio or XCode or VSCodeにFlutter 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をダウンロード+解凍します。
3-2. パスを通す
解凍したFlutterを任意のフォルダに配置し、flutter\binまでのパスを通します。
C:\Users\xxx\flutter\bin3-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-licenses3-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」を解消できます
3-4-3. [!] VS Code...
X Flutter extension not installed; install from xxx
VSCodeにFlutterがないとのことです。今回はAndroid Studioを使うので関係ないですが、「X」なしの方がかっこいいので一応解消しておきましょう。以下の手順でインストールすればOKです。
① VSCodeを起動し、[拡張機能]を選択。
② Flutterをインストール。
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.やった!解消されました
これで環境はOKです
4. プロジェクト作成
環境が整った所で、Android StudioにてFlutterプロジェクトを作成していきます。
※作成したプロジェクトはGitHubに登録してください。(すみません、手順は割愛で・・・)①「新規 Flutter プロジェクトの開始」を選択します。
これでFlutterの空プロジェクトを作成できました
5. Codemagicの設定
続いてCodemagicの設定を行っていきます。
上記にアクセスし、以下の手順で進めます。
(黒背景に黄色の蛍光ペンは非常に見づらかった・・・)
③ GitHub上のFlutterプロジェクトの「Set up build」を押下します。
④ 「Start your first build」を押下します。
⑥ ビルド中の画面になります。完了するまで待ちましょう。(3分くらい?
)
なんとこれだけでアプリケーションファイルが出来上がりました
すごい!!
6. 動作確認
apkファイルをダウンロードしてエミュレータにインストールしてみた所、うまく動きました
7. おわりに
環境構築にしろCodemagicの設定にしろ、かなり簡単に行えましたね!
環境によってはflutter doctor
で他にもissueが出るかもしれませんので、メッセージでググってみてください
次回はDartに踏み込んでアプリを開発していきたいと思います。↑な感じでmicroCMSとも組み合わせたいなー。
何よりAndroidの実機がほしい8. 参考
今回は以下の記事を参考にさせて頂きました。ありがとうございます!
- 投稿日:2020-08-17T09:33:00+09:00
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 Ichiroreturnを使った関数
一方的にただ呼び出す関数も便利ですが、(上記2つのコード??)
関数の中でいろいろな処理をさせて、その「結果」を貰いたいときがあります。
要するに、呼び出しに対する「返事」が欲しいときです。with a return value?
with parameters and a return value?
戻り値を持つ関数。
書き方は、こんな感じ。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を埋め込み。おしまい。
参考サイト
- 投稿日:2020-08-17T09:18:42+09:00
[2020年版]tabBarControllerの設定方法
tabBarControllerとは?
tabBarControllerとは、よくiPhoneで見かける液晶下部にあるボタンのことを指します。
このBarの働きは複数画面と接続し、UITabBar(以下、tabBar)に並べられたボタンで画面の切り替えを行う部品であります。tabBarの実装方法
Xcode右上のプラスボタン(+)をクリックして
tabBarControllerを選択
すると以下画像のように3つの画面が出てくるのでそのまま配置。
tabBarのテキスト変更方法
以下の画像のようにテキストを変更したいtabBarをフォーカスして、右サイドバーのTitleのテキストを編集。
すると、左の液晶のtabBar左側のボタンを継承して右上の液晶と連携していることが分かる。(今回は'テキスト編集'と編集)
tabBarアイコン変更方法
アイコンも同様に変更したいtabBarをフォーカスして、右サイドバーで編集。なお、アイコンの場合はImageを変更すればアイコンを変更できる。
ちなみに、よく見かけるアイコンはAppleがすでに用意してくれています。
実装確認
ここまで編集をおこないシュミレーター を起動するとアイコンとテキストの両方の編集が成功しているとおもいます。
これだけの設定で画面遷移を実装できる点、Xcodeは素晴らしいですよね!しかも割と直感的に操作ができる!最後に
今回はtabBarControllerに関してアウトプットを行いました。
久々の更新となりましたが今後も継続していきますので応援宜しくお願いします!