- 投稿日:2020-08-08T23:58:48+09:00
iOS13 で UILabel のスタイルが間違えて表示されている可能性がある
起きたこと
UILabel の
attributedText
を nil (リセット) にしてもラベルのスタイルが変わらないという現象が発生していました。TL;DR
原因
UILabel の attributedText の値が nil(リセット)になった場合、通常(iOS12以前)は Label の attributes もリセットされるが iOS13 ではリセットされないとの報告があり、それが起因して考えられます。
解決策
UILabel の
text
・attributedText
はどちらか一方の値が変わるともう一方の値も変わるという連動性があることから(下記ドキュメント)、今回のようにtext
・attributedText
を共存させて更新することはシステムのバグに限らず望ましくないので、attributedText
のみ明示的に更新処理を行うように修正したところ解決しました。
[https://developer.apple.com/documentation/uikit/uilabel/1620542-attributedtext:title]
[https://developer.apple.com/documentation/uikit/uilabel/1620538-text:title]
※ iOS13のバグかどうかについては現在のところ不明
具体的な発生タイミングと改善
Before
チャットに表示する UI で、コメントがリンクの場合は青く表示させて Clickable にさせるように UILabel を継承したサブラクス(以降: LinkLabel)を作成していました。基本的な仕様としては LinkLabel の
text
プロパティに didSet をセットし、更新されたタイミングでテキストをトリミングしてattributedText
を更新するという感じです。final class LinkLabel: UILabel { override var text: String? { didSet { updateAttributedText() } } func updateAttributedText() { guard let text = text else { return } let mutableAttributedString = NSMutableAttributedString(string: text) // 省略.... // いろいろ Attributes をセットする attributedText = mutableAttributedString } }上記で作成した LinkLabel を Cell に配置して TableView で表示した時に、Cell が再利用されるタイミングで以前の attributes がキャッシュされていて関係ないコメントも青く表示されることがたびたび起こるようになりました。
After
TL;DR
にも記載してある通り、attributedText
だけを明示的に更新することで改善できたので下記のように修正します。final class LinkLabel: UILabel { var displayText: String? func setText(_ text: String?) { displayText = text updateAttributedText() } func updateAttributedText() { guard let text = displayText else { return } let mutableAttributedString = NSMutableAttributedString(string: text) // 省略.... // いろいろ Attributes をセットする attributedText = mutableAttributedString } }使用する Cell 側でもキャッシュをリセットできるように
prepareForReuse()
セットしますfinal class CommentCell: UITableViewCell { let linkLabel = LinkLabel() override func prepareForReuse() { super.prepareForReuse() linkLabel.attributedText = nil } // 省略... }調べたこと
UILabel の attributes がリセットされないという事例・報告がありました。
解決のヒントは下記にありました。
typingAttributes
で attributes が保存されてるからか?と思いましたが、そもそも UITextView のプロパティで UILabel には存在しませんでした。公式ドキュメント
- 投稿日:2020-08-08T22:30:30+09:00
Swift でアプリ内アイテムフィルタリングを実現する話
前書き
こんにちは、リビリンです。日本語も記事書きも初心者です。
この記事はジェネリックタイプと関数型プログラミングのテクニックを使ってフィルタリングを実現する方法を紹介します。読むには、Swift のジェネリックタイプと、ちょっとした関数型プログランミングの概念が必要です(といっても
filter
とsorter
のような高階関数とクロージャが理解していれば大丈夫です)。背景
自作アプリの「デレガイド2」は、アイドルリズムゲーム「アイドルマスター シンデレラガールズ スターライトステージ(デレステ)」の非公式アシスタントアプリで、その中で一番基本的な機能が、アイドルたちのカードを収録した図鑑機能です。
今2000枚以上のカードがアプリ内でローカルに保存されています。カードにはそれぞれのレア度、組1(キュート・クール・パッション)、特技などの属性を持っていて、ユーザーが任意の属性を指定してカードを絞り込む、つまりフィルタリングができます。
例えば、下図のよう、フィルタリングページで「SR+」「SSR+」と「キュート組」を指定すると、「レア度が SR+ あるいは SSR+、かつキュートに所属する」という条件に従うカードが絞り込まれます。
多くの属性の中で、「レア度・組」のように属性の数が一定するものもあって、「特技種類・特技の発動確率・間隔」のような、ゲーム本体の更新につれて増える可能性のあるものもあります。ゲーム更新に対して、自動的に、あるいは手動でも簡単で柔軟に対応できるために、フィルタリング機能をどう書けばいいんですか?
よくないやり方
既に気ついたかもしれないが、デレガイド2ってことは、バージョン1があることです。開発者は自分ではないが、昔のバージョンが同じ機能がありました。4年前のコードを見ると、全ての属性がハードコーディングされていて、
OptionSet
でフィルタリングを実現したようです。OptionSet が知らない方には、とりあえず複数の値を持つ
Enum
と考えても良いです。例えば、特技のフィルタリング用の構造体がこのように書かれています:
struct CGSSSkillTypes: OptionSet { let rawValue: UInt init(rawValue: UInt) { self.rawValue = rawValue } static let comboBonus = CGSSSkillTypes(rawValue: 1 << 0) static let perfectBonus = CGSSSkillTypes(rawValue: 1 << 1) static let overload = CGSSSkillTypes(rawValue: 1 << 2) static let perfectLock = CGSSSkillTypes(rawValue: 1 << 3) // // ...すべての特技 // static let all = CGSSSkillTypes(rawValue: 0b1111_1111_1111_1111_1111) }カード用フィルターと使い方:
struct CGSSCardFilter: CGSSFilter { var attributeTypes: CGSSAttributeTypes // 組 var rarityTypes: CGSSRarityTypes var skillTypes: CGSSSkillTypes // ...いくつあります func filter(_ cards: [CGSSCard]) -> [CGSSCard] { return cards.filter { card in return cardTypes.contains(card.cardType) && rarityTypes.contains(card.rarityType) && attributeTypes.contains(v.attributeType) && // ... } } } let resultCards = cardFilter.filter(allCards)かなり直球的なデザインですね。このやり方にすると、いくつデメリットがあります:
- 実際に使用したそれぞれの構造(Rarity, Skill など)と別として、ある程度同じ意味の持つフィルタリング用の構造体(RarityType, SkillType)を新設して、余剰性(redundancy)が持たされた。
- アプリ内では、絞り込まれるものはカードだけではなく、曲とアイドルも同じ。違う実体に対して、またその配下の属性に、それぞれのコードを書く必要があり、コード量が異常に多い。
- ゲーム本体の更新につれ新しい属性が追加されるたび、相応の
OptionSet
に新しい選択肢を手動で追加しなければならないし、all
も変える必要がある。メンテナンスしにくい。まったくエレガントではない!
今のやり方
基本定義
考えとしては、(概念上の)フィルターを、ハードコードで書き込むではなく、一つ一つの変数として扱うのほうが便利ではないですか。この関数を変数化するデザインが、まさに Swift の
filter
がやっていることと同じです。まず、フィルタータイプの関数クロージャをリネームします:
typealias Filter<T> = (T) -> Boolここから、この
Filter
をめぐって様々な機能を拡張します。フィルタリング用のプロトコルを作ります:
protocol PropertyForFiltering { associatedtype Category let localized: String // id として区別用 var filter: Filter<Category> { get } }意味は「このプロトコルに従うものが
Category
を絞り込むために使われる」です。filter
は、「このプロトコルに従う属性と一致するようにCategory
オブジェクトを絞り込む」という関数クロージャです。使い方としては、例えば、レア度が列挙型で定義されています:
enum Rarity: Int, CaseIterable { // ... case sr case srp case ssr case ssrp }
Rarity
をPropertyForFiltering
に conform する。extension Rarity: PropertyForFiltering { var filter: Filter<Card> { return { card in return card.rarity == self } } }こうすると、一つのケースが、一つの「レア度がそのケースと一致するカードを選ぶ」というフィルターを変数として作れます。使い方は:
ssrCards = allCards.filter(Rarity.ssr.filter) // 以下と相当する ssrCards = allCards.filter { card in return card.rarity == Rarity.ssr }そして、複数のフィルターを取り組んで使いたいので、まず汎用的な方法を書きます:
/// And(conjunction), return a new filter that satisfy two parameter filters. func && <T> (f1: @escaping Filter<T>, f2: @escaping Filter<T>) -> Filter<T> { return { t in return f1(t) && f2(t) } } /// Or(disjunction), return a new filter that satisfy one of parameter filters as least. func || <T> (f1: @escaping Filter<T>, f2: @escaping Filter<T>) -> Filter<T> { return { t in return f1(t) || f2(t) } }ここでは、
Filter
に対して&&
論理AND演算子を再定義して、「二つのFilter
とも満足する」という新しいFilter
を作ることができます。||
OR 演算子も同じ。
こうすると、あらゆるフィルターをいとも簡単に書けます:// カードのレア度が SR あるいは SSR let filter1 = Rarity.sr.filter || Rarity.ssr.filter // レア度が SSR+、組がキュートあるいはクール、特技がスコアボーナス // という条件に絞り込むフィルター let filter2 = Rarity.ssrp.filter && (Chara.Attribute.cute.filter || Chara.Attribute.cool.filter) && scoreBonusSkill.filter let filteredCard = allCard.filter(filter2)整合
ここまで、基盤的なものが準備できました。次に多数の属性を一箇所に管理しよう、と思いましたが:
struct PropertySet<Category> { var properties: [PropertyForFiltering<Category>] = [] } // ❌ Compile Error: Cannot specialize non-generic type 'PropertyForFiltering'どうやら、少なくとも Swift 5.3 まで、
Self
やassociatedtype
付きのProtocol
が変数の型として直接に使えないので、Type Erasure の方法を使います:struct AnyFilterToggle<Category>: PropertyForFiltering { let localized: String let filter: Filter<Category> var isOn = false init<Property: PropertyForFiltering>(property: Property) where Category == Property.Category { self.localized = property.localized self.filter = property.filter } init(localized: String, abbreviation: String? = nil, filter: @escaping Filter<Category>) { self.localized = localized self.filter = filter } }ここの
AnyFilterToggle
が、PropertyForFiltering
に従う具体的な型を抹消して、すべての属性を同じ型として扱うことができるようにしました。AnyObject
や SwiftUI のAnyView
も同じパターンです。
(本来AnyPropertyForFiltering
みたいな名前が適切ですけど、有効ブール値もここに入れたので、スウィッチ(Toggle)のほうに似ています)// SR の Toggle let srToggle = AnyFilterToggle(property: Rarity.sr) // すべてのレア度の Toggle let allRarityToggles = Rarity.allCases.map(AnyFilterToggle.init) // 具体的属性変数がなくても、いろんなフィルターを柔軟に作れる // Vocal能力値が5000以上のフィルター Toggle let customToggle = AnyFilterToggle<Card>(localized: "Vocal is over 5000", filter: { card in card.vocal >= 5000 })ここで、属性たちの関係を振り返ります:
一つの属性を Toggle にし、同じ種類の複数属性の Toggle を Set に格納されます:
struct FilterPropertySet<Category> { var filterToggles: [AnyFilterToggle<Category>] init?(toggles: [AnyFilterToggle<Category>]) { ... } init?<Property: PropertyForFiltering>(properties: [Property]) where Property.Category == Category { // ... filterToggles = properties.map(AnyFilterToggle.init) } // Toggle 全部選ばれている、あるいは全部選ばれていない、という場合は無効と見なす var isAffectable: Bool { ! filterToggles.allSatisfy({$0.isOn}) && ! filterToggles.allSatisfy({!$0.isOn}) } // 有効した Toggle の filter を OR 関係で組み合って、Set レベルの filter を作る var filter: Filter<Category> { guard isAffectable else { return { _ in true } } var result: Filter<Category> = { _ in false } for toggle in filterToggles where toggle.isOn { result = result || toggle.filter } return result } }そして同じロジックで、複数の Set を Requirement に格納されて、filter を論理 AND 関係で組み合って 最終 filter を作ります。
struct FilterRequirement<Category> { var propertySets: [FilterPropertySet<Category>] // テスト:このコードを理解できますか?(上の filter とほぼ同じ意味) var finalFilter: Filter<Category> { propertySets .filter(\.isAffectable) .map(\.filter) .reduce({ _ in true }, &&) } // こういうコード書けるのが Swift の醍醐味ですよ! }これで準備万端。
使い方
let cardFilterRequirement = FilterRequirement<Card>(propertySets: [ FilterPropertySet(properties: Rarity.allCases), FilterPropertySet(properties: allSkillCategory), FilterPropertySet(properties: Card.FetchMethod.allCases), // ... ]) let filteredCards = allCards.filter(cardFilterRequirement.finalFilter)こうやって、複数の属性で
FilterSet
を作成して、そして複数のFilterSet
でFilterRequirement
を作成します。Set の中の Toggle の isON をいじりながら、有効した Toggle から出たすべてのfilter
をFilterSet
とFilterRequirement
のfilter
を通して 一つのfilter
にまとめます。すべてのものが様々な属性変数から作れるので、特技種類みたいに、サーバーからのデータで生成されたものが、新しい特技が追加されても、自動的に変数として
allSkillCategory
に含まれていて、フィルターに変わるので、非常に便利です。同じパターン
実際に、アイテムの属性だけではなく、ユーザーが検索欄で入力した文字列や:
func makeCardSearchFilter(searchText: String) -> Filter<Card> { return { card in card.name.contains(searchText) } } let searchFilter = makeCardSearchFilter(searchText: "卯月") let filteredCard = allCards.filter(searchFilter && requirement.filter)アイテムの並び替えも:
typealias Sorter<T> = (T, T) -> Bool // protocol PropertyForSorting // class AnyPropertyForSorting<Category> // class SorterRequirement<Category> let sortedCard = cards.sorted(cardSorterRequirement.finalSorter)同じデザインで実装しています。
こういったアーキテクチャーで、すべてのカードを絞り込んで、並べ替えて展示します:
このやり方のメリット
新しい属性が追加された時、既存コードを変える必要がない
新しい属性の追加が容易にできる。
列挙型の属性が追加された時、
case
を一行で書き加えるだけで;サーバーから生成された動的属性でしたら、コードを書く必要さえなく、自動的に対応する;たとえ新しい属性セットが入れたい時でも、新しいFilterPropertySet
を書くだけで良い。既存なコードを変える必要が全くない。柔軟性が高い、テストしやすい
変数ではなくでも、上の
textFilter
のようなフィルターを直接書き出せるので、柔軟性が高く、様々なフィルターを自由に組み合わせる。このように具体的なロジックを予め用意して、使う時には宣言的にフィルター作るだけで、コンパイラーに通されたらバグが出る可能性が低く、テストもしやすい。
コード量が少ない
実際に、カード・曲・アイドル、三つの実体に、それぞれの Filter と Sorter を実装しました。以前のやり方より圧倒的コード量が少ない。
おわりに
初投稿でちょうどいいテーマにしようと思って、書けば書くほど思った以上の量になりました。
日本語もバラバラですので、説明が不十分や、質問したいことがあれば気軽にコメントしてください!
Refer:
- Associated Types — The Swift Programming Language (Swift 5.3)
Self
やassociatedtype
付きのProtocol
が変数の型として直接に使えないことについて:型システムの理論からみるSwiftの存在型(Existential Type)- Swift の関数型プログランミングに興味があれば Objc の本めっちゃおすすめ! Functional Swift 日本語Ver Functional Swift 日本語版(Swift 3対応)
組:普通でしたら「キュート・クール・パッション」がカードの属性(Attribute)と呼びますが、本記事では Property という意味の「属性」を区別するため、「組」と呼びます。 ↩
- 投稿日:2020-08-08T21:52:36+09:00
Dart vs Swift
| 作者 : Andrea Bizzotto
| 原文 : medium
| 翻訳 : jamestong
| 校正 : jamestongDartとSwiftは私のお気に入りの2つのプログラミング言語です。私は商用およびオープンソースのコードでこれらを幅広く使用してきました。
この記事では、DartとSwiftを並べて比較してみます。
- 両者の違いを強調する;
- 開発者の参考として、一方の言語から他方の言語に移行する(または両方を使用する)時の注意点を挙げる。
背景:
- Dartは、単一のコードベースから美しいネイティブアプリを構築するためのGoogleのフレームワークであるFlutterに対応している;
- Swiftは、iOS、macOS、tvOS、watchOSにまたがるAppleのSDKをサポートしている。
以下は、両言語の主な機能(
Dart 2.1
とSwift 4.2
の時点で)を比較したものです。各機能の詳細な議論はこの記事の範囲を超えているので、必要に応じて両言語の参考文献を参考してください。目次
- 対照表
- 変数
- 型推論
- 可変型/不可変型変数
- 関数
- 名前付きパラメータと名前なしパラメータ
- オプションとデフォルトのパラメータ
- クロージャ
- タプル
- 制御フロー
- コレクション(arrays, sets, maps)
- Nullability & Optionals
- クラス
- 継承
- プロパティー
- プロトコル / 抽象クラス
- Mixins
- 拡張
- 列挙型
- 構造体
- エラー処理
- ジェネリック
- アクセス制御
- 非同期プログラミング:
Futures
- 非同期プログラミング:
Streams
- メモリ管理
- コンパイルと実行
- その他の機能
- Dartに欠けている私のお気に入りのSwiftの機能
- Swiftに欠けている私のお気に入りのDartの機能
- 結論
対照表
変数
変数宣言の構文は、Dartでは次のように:
String name; int age; double height;Swiftではこのように:
var name: String var age: Int var height: Double変数の初期化はDartではこのように:
var name = 'Andrea'; var age = 34; var height = 1.84;Swiftではこのように:
var name = "Andrea" var age = 34 var height = 1.84上記らの例では、型のアノテーションは必要ありません。これは、どちらの言語も代入の右側の式から型を推測できるからです。
型推論
型推論とは、Dartで次のように書けるということです。
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>そして、
arguments
の型はコンパイラによって自動的に解決されます。Swiftでは、同じように書くことができます:
var arguments = [ "argA": "hello", "argB": 42 ] // [ String : Any ]さらに
Dartのドキュメントを引用します。
解析器は、フィールド、メソッド、ローカル変数、およびほとんどのジェネリック引数の型を推論することができます。解析器が特定の型を推論するのに十分な情報を持っていない場合は、動的型付け
dynamic
を使います。そしてSwiftの場合は:
Swiftは広範囲に型推論を使用しており、コード中の多くの変数や式の型や型の一部を省略することができます。例えば、
var x: Int = 0
のように書くことではなく,var x = 0
のように書けば、 コンパイラは x がInt
型の値を指定していることを正しく推論します。動的型付け
任意の型を持つ変数は、Dartでは
dynamic
キーワード、SwiftではAny
キーワードで宣言されます。動的型付けはJSONなどのデータを読み込むときによく使われます。
可変型/不変型変数
変数は、可変なものと不変なものを宣言することができます。
可変な変数を宣言するには、両言語とも
var
キーワードを使用します。var a = 10; // int (Dart) a = 20; // ok var a = 10 // Int (Swift) a = 20 // ok不変変数の宣言には、Dartでは
final
、Swiftではlet
を使います。final a = 10; a = 20; // 'a': a final variable, can only be set once. let a = 10 a = 20 // Cannot assign to value: 'a' is a 'let' constant注意:Dartのドキュメントでは、
final
とconst
という2つのキーワードが定義されていますが、これらは以下のように動作します。変数を変更するつもりがない場合は、
var
の代わりに、または型に加えて、final
またはconst
を使用してください。final
変数は一度だけ設定することができ、const
変数はコンパイル時の定数です。(const
変数は暗黙のうちにfinal
になります。) finalのトップレベル変数やクラス変数は、初めて使用されたときに初期化されます。さらに
final
はシングルアサインメントを意味します。最終的な変数やフィールドにはinitializer
が必要です。一度値が代入されると、final
変数の値を変更することはできません。TL;DR: Dartで不変変数を定義するにはfinalを使います。
Swiftでは
let
で定数を宣言します。定数の宣言は、定数の値をプログラムに導入します。定数は
let
キーワードを使って宣言され、以下のような形式になります。let constant name: type = expression定数の宣言は、定数名とイニシャライザ式の値との間の不変の結合を定義します。
定数の値が設定された後は変更できません。関数
関数はSwiftとDartの中では一等市民です。
これはオブジェクトと同じように、関数は引数として渡されたり、プロパティとして保存されたり、結果として返されたりすることができることを意味します。
最初の比較として、引数のない関数の宣言方法を見てみましょう。
Dartでは、メソッド名の前にリターンタイプが付きます。
void foo(); int bar();Swift では、サフィックスとして
-> T
記法を使用しています。戻り値がない場合(Void)にはそれを付く必要がありません。func foo() func bar() -> Int名前付きパラメータと名前なしパラメータ
どちらの言語も名前付きと名前なしのパラメータをサポートしています。
Swiftでは、パラメータはデフォルトで名前が付けられています。
func foo(name: String, age: Int, height: Double) foo(name: "Andrea", age: 34, height: 1.84)Dartでは、中括弧({ })で名前付きパラメータを定義します。
void foo({String name, int age, double height}); foo(name: 'Andrea', age: 34, height: 1.84);Swiftでは、外部パラメータとしてアンダースコア(_)を使って名前のないパラメータを定義します。
func foo(_ name: String, _ age: Int, _ height: Double) foo("Andrea", 34, 1.84)Dartでは、中括弧({ })を省略して名前のないパラメータを定義します。
void foo(String name, int age, double height); foo('Andrea', 34, 1.84);オプションとデフォルトのパラメータ
どちらの言語もデフォルトのパラメータをサポートしています。
Swiftでは、パラメータの型の後にパラメータに値を割り当てることで、関数内の任意のパラメータのデフォルト値を定義することができます。デフォルト値が定義されている場合、関数を呼び出す際にそのパラメータを省略することができます。
func foo(name: String, age: Int = 0, height: Double = 0.0) foo(name: "Andrea", age: 34) // name: "Andrea", age: 34, height: 0.0Dartでは、オプションのパラメータは位置指定か名前指定のどちらかを指定することができますが、両方を指定することはできません。
// positional optional parameters void foo(String name, [int age = 0, double height = 0.0]); foo('Andrea', 34); // name: 'Andrea', age: 34, height: 0.0 // named optional parameters void foo({String name, int age = 0, double height = 0.0}); foo(name: 'Andrea', age: 34); // name: 'Andrea', age: 34, height: 0.0クロージャ
一級オブジェクトである関数は、他の関数の引数として渡されたり、変数に代入されたりすることができます。
この文脈では、関数はクロージャ
closure
としても呼ばれています。ここでは、各項目のインデックスと内容を表示するためにクロージャを使用して、項目のリストを反復処理する関数のDartの例を示します。
final list = ['apples', 'bananas', 'oranges']; list.forEach((item) => print('${list.indexOf(item)}: $item'));クロージャは1つの引数(
item
)を取り、そのitem
のインデックスと値を表示するが、リターンしません。矢印表記 (
=>
) を使用していることに注意してください。これは中括弧の中にある単一のreturn
文の代わりに使うことができます。list.forEach((item) { print('${list.indexOf(item)}: $item'); });Swiftでは同じコードは次のようになります。
let list = ["apples", "bananas", "oranges"] list.forEach({print("\(String(describing: list.firstIndex(of: $0))) \($0)")})この場合、クロージャに渡される引数の名前は指定せず、代わりに
$0
を使用して最初の引数を意味します。これは完全にオプションであり、必要に応じて名前付きのパラメータを使用することもできます。list.forEach({ item in print("\(String(describing: list.firstIndex(of: item))) \(item)")})Swiftでは非同期コードの補完ブロックとしてクロージャがよく使われます。
タプル
Swiftドキュメントより:
タプルは複数の値を1つの複合値にまとめます。タプル内の値は任意の型をとることができ、互いに同じ型である必要はありません。
これらは小型の軽量型として使用することができ、複数の戻り値を持つ関数を定義する際に便利です。
Swiftでのタプルの使い方:
let t = ("Andrea", 34, 1.84) print(t.0) // prints "Andrea" print(t.1) // prints 34 print(t.2) // prints 1.84Dartではサードパーティパッケージでタプルを実現出来ます。
const t = const Tuple3<String, int, double>('Andrea', 34, 1.84); print(t.item1); // prints 'Andrea' print(t.item2); // prints 34 print(t.item3); // prints 1.84制御フロー
どちらの言語も様々な制御フロー文を提供しています。
例としては、if 条件文、for ループ、while ループ、switch文などがあります。
これらをここで説明するとかなり長くなるので、公式ドキュメントを参照してください。
コレクション(arrays, sets, maps)
Arrays / Lists
配列はオブジェクトの順序付けされたグループです。
Dartでは
List
で配列Arrays
を作ることができます。var emptyList = <int>[]; // empty list var list = [1, 2, 3]; // list literal list.length; // 3 list[1]; // 2Swiftでの配列は組み込み型です。
var emptyArray = [Int]() // empty array var array = [1, 2, 3] // array literal array.count // 3 array[1] // 2Sets
Swiftのドキュメントより:
Setは、定義された順序を持たないコレクションの中に、同じ型の異なる値を格納します。アイテムの順序が重要ではない場合や、アイテムが一度しか表示されないようにする必要がある場合には、配列の代わりにセットを使うことができます。
DartのSet クラスはこのように定義されています。
var emptyFruits = <String>{}; // empty set literal var fruits = {'apple', 'banana'}; // set literal同様に、Swiftは:
var emptyFruits = Set<String>() var fruits = Set<String>(["apple", "banana"])Maps / Dictionaries
Swiftのドキュメントには、
map/dictionary
に対し良い定義があります。辞書は、同じ型のキーと同じ型の値の間の関連付けを、定義された順序を持たないコレクションに格納します。各値は唯一のキーに関連付けられ、辞書内でその値の識別子として機能します。
Dartは
map
を次のように定義しています。var namesOfIntegers = Map<Int,String>(); // empty map var airports = { 'YYZ': 'Toronto Pearson', 'DUB': 'Dublin' }; // map literalSwiftでは
map
を辞書dictionary
と呼びます。var namesOfIntegers = [Int: String]() // empty dictionary var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"] // dictionary literalNullability & Optionals
Dartでは、どんなオブジェクトでも
null
にすることができます。そして、null
オブジェクトのメソッドや変数にアクセスしようとすると、null
ポインタ例外が発生します。これは、コンピュータプログラムで最も一般的なエラーの原因の一つです(最も一般的ではないにしても)。最初から、Swiftにはオブジェクトが値を持てるか持てないかを宣言するための組み込みの言語機能であるオプショナルがありました。ドキュメントを引用します。
値が存在しないかもしれない状況では、オプショナル
Optional
を使用します。オプショナルは2つの可能性を表します。値があり、その値にアクセスするためにオプションをアンラップすることができる場合と、値が全く存在しない場合です。これとは対照的に、非オプショナル変数を使用することで、常に値を持つことを保証できます。
var x: Int? // optional var y: Int = 1 // non-optional, must be initializedノート:Swiftの変数がオプショナルであると言うことは、Dartの変数がnullになる可能性があると言うこととほぼ同じです。
言語レベルでオプショナルをサポートしていない場合、変数が
null
であるかどうかを実行時にチェックすることしかできません。オプショナルを使うと、コンパイル時に情報をエンコードします。オプショナルをアンラップすることで、値を保持しているかどうかを安全にチェックすることができます。
func showOptional(x: Int?) { // use `guard let` rather than `if let` as best practice if let x = x { // unwrap optional print(x) } else { print("no value") } } showOptional(x: nil) // prints "no value" showOptional(x: 5) // prints "5"そして、変数が値を持たなければならないことがわかっている場合は、非オプショナル
non-optional
を使うことができます。func showNonOptional(x: Int) { print(x) } showNonOptional(x: nil) // [compile error] Nil is not compatible with expected argument type 'Int' showNonOptional(x: 5) // prints "5"上の最初の例は、Dartではこのように実装することができます。
void showOptional(int x) { if (x != null) { print(x); } else { print('no value'); } } showOptional(null) // prints "no value" showOptional(5) // prints "5"そして2個目はこんな感じ:
void showNonOptional(int x) { assert(x != null); print(x); } showNonOptional(null) // [runtime error] Uncaught exception: Assertion failed showNonOptional(5) // prints "5"オプショナルを持つことは、実行時ではなくコンパイル時にエラーをキャッチできることを意味します。そして、エラーを早期にキャッチすることで、より安全でバグの少ないコードを作ることができます。
Dart はオプショナルをサポートしていませんが、アサーション (および名前付きパラメータの
@required
アノテーション) を使用することでなんとか緩和されています。これらのアサーションは
Flutter SDK
で広く使われていますが、結果的には、余計の定型的なコードが生じます。クラス
クラスはオブジェクト指向言語でプログラムを書くための主要な構成要素です。
クラスは Dart と Swiftが両方ともサポートしていますが、いくつかの違いがあります。
構文
Swiftの
initializer
と3つのメンバ変数を持つクラスです。class Person { let name: String let age: Int let height: Double init(name: String, age: Int, height: Double) { self.name = name self.age = age self.height = height } }Dartも同じように:
class Person { Person({this.name, this.age, this.height}); final String name; final int age; final double height; }Dart のコンストラクタで
this.[propertyName]
を使用することに注意してください。これは、コンストラクタが実行される前にインスタンスのメンバ変数を設定するための糖衣構文syntactic sugar
です。ファクトリ コンストラクタ
Dartでは、ファクトリ・コンストラクタを作成することができます。
常にクラスの新しいインスタンスを生成しないコンストラクタを実装する場合は
factory
キーワードを使用します。ファクトリコンストラクタの実用的な使用例としては、JSONからモデルクラスを作成する場合に使われます。
class Person { Person({this.name, this.age, this.height}); final String name; final int age; final double height; factory Person.fromJSON(Map<dynamic, dynamic> json) { String name = json['name']; int age = json['age']; double height = json['height']; return Person(name: name, age: age, height: height); } } var p = Person.fromJSON({ 'name': 'Andrea', 'age': 34, 'height': 1.84, });継承
Swiftはシングル継承モデルを使用しており、どのクラスも1つのスーパークラスしか持っていないことを意味します。Swiftのクラスは複数のインターフェイス(プロトコルとも呼ばれる)を実装することができます。
Dartクラスは
mixin
ベースの継承を持っています。ドキュメントを引用します。すべてのオブジェクトは1つのクラスのインスタンスであり、すべてのクラスは
Object
から派生します。mixin
ベースの継承とは、(Objectを除く)すべてのクラスにはちょうど1つのスーパークラスがありますが、クラス本体は複数のクラス階層で再利用できることを意味します。Swiftでのシングル継承の動作:
class Vehicle { let wheelCount: Int init(wheelCount: Int) { self.wheelCount = wheelCount } } class Bicycle: Vehicle { init() { super.init(wheelCount: 2) } }Dartは:
class Vehicle { Vehicle({this.wheelCount}); final int wheelCount; } class Bicycle extends Vehicle { Bicycle() : super(wheelCount: 2); }プロパティー
これらはDartではインスタンス変数と呼ばれ、Swiftでは単にプロパティと呼ばれます。
Swiftでは、保存されたプロパティと計算されたプロパティに区別があります。
class Circle { init(radius: Double) { self.radius = radius } let radius: Double // stored property var diameter: Double { // read-only computed property return radius * 2.0 } }Dartでは、同じような区別があります:
class Circle { Circle({this.radius}); final double radius; // stored property double get diameter => radius * 2.0; // computed property }計算されたプロパティの
getters
に加えて、setters
を定義することもできます。上の例を使って、
diameter
プロパティをsetter
を含むように書き換えることができます。var diameter: Double { // computed property get { return radius * 2.0 } set { radius = newValue / 2.0 } }Dartでは、このように単独の
setter
を追加することができます。set diameter(double value) => radius = value / 2.0;プロパティオブザーバ
これがSwiftの特徴です。
プロパティオブザーバーはプロパティの値の変化を観測し、それに反応します。プロパティオブザーバーは、プロパティの値が設定されるたびに、新しい値がプロパティの現在の値と同じであっても呼び出されます。
使い方:
var diameter: Double { // read-only computed property willSet(newDiameter) { print("old value: \(diameter), new value: \(newDiameter)") } didSet { print("old value: \(oldValue), new value: \(diameter)") } }プロトコル / 抽象クラス
ここでは、どのように実装されているかを指定せずに、メソッドやプロパティを定義するために使用される構文について説明します。これは他の言語ではインターフェースとして知られています。
Swiftでは、インターフェースをプロトコルと呼ばれています。
protocol Shape { func area() -> Double } class Square: Shape { let side: Double init(side: Double) { self.side = side } func area() -> Double { return side * side } }Dartには、抽象クラスとして知られる同様の構成があります。抽象クラスはインスタンス化できません。しかし、実装を持つメソッドを定義することはできます。
上の例は、Dartではこのように書くことができます。
abstract class Shape { double area(); } class Square extends Shape { Square({this.side}); final double side; double area() => side * side; }Mixins
Dartでは、
Mixin
は通常のクラスであり、複数のクラス階層で再利用することができます。先ほど定義した
Person
クラスをNameExtension mixin
で拡張する方法を紹介します。abstract class NameExtension { String get name; String get uppercaseName => name.toUpperCase(); String get lowercaseName => name.toLowerCase(); } class Person with NameExtension { Person({this.name, this.age, this.height}); final String name; final int age; final double height; } var person = Person(name: 'Andrea', age: 34, height: 1.84); print(person.uppercaseName); // 'ANDREA'拡張
拡張機能はSwift言語の1つの特徴です。ドキュメントを引用します。
拡張機能は既存のクラス、構造体、列挙、またはプロトコルの型に新しい機能を追加します。これには、元のソースコードにアクセスできない型を拡張する機能が含まれます(レトロアクティブモデリング
retroactive modeling
として知られています)。これは、Dart の mixins ではできません。
上の例を借りて、
Person
クラスを次のように拡張します。extension Person { var uppercaseName: String { return name.uppercased() } var lowercaseName: String { return name.lowercased() } } var person = Person(name: "Andrea", age: 34, height: 1.84) print(person.uppercaseName) // "ANDREA"拡張機能にはここで紹介した以上に多くがあり、特にプロトコルやジェネリックと組み合わせて使用される場合には、それ以上に多くのことができます。
拡張機能の最も一般的な使用例の一つは、既存の型にプロトコルに準拠した機能を追加することです。例えば、既存のモデルクラスにシリアライズ機能を追加するために拡張機能を使用することができます。
列挙型
Dartは、列挙型に対しいくつかの基本的なサポートを持っています。
Swiftの列挙型は、関連する型をサポートしているので、非常に強力です。
enum NetworkResponse { case success(body: Data) case failure(error: Error) }これにより、このようなロジックを書くことが可能になります。
switch (response) { case .success(let data): // do something with (non-optional) data case .failure(let error): // do something with (non-optional) error }
data
パラメータとerror
パラメータが相互に排他的であることに注意してください。Dartでは列挙型に追加の値を関連付けることはできませんので、上記のコードは次のように実装できるかもしれません。
class NetworkResponse { NetworkResponse({this.data, this.error}) // assertion to make data and error mutually exclusive : assert(data != null && error == null || data == null && error != null); final Uint8List data; final String error; } var response = NetworkResponse(data: Uint8List(0), error: null); if (response.data != null) { // use data } else { // use error }いくつか注意点があります。
- ここではアサーションを使用して、オプショナルがないという事実を補っています。
- コンパイラはすべてのケースをチェックすることはできません。これはレスポンスを処理するプロセスで
switch
を使わないからです。まとめると、Swiftの列挙型はDartに比べてかなり強力で表現力があります。
Dart Sealed Unions
のようなサードパーティのライブラリは、Swift列挙型と同様の機能を提供し、ギャップを埋めるのに役立ちます。構造体
Swiftでは構造体とクラスを定義することができます。
どちらの構造にも多くの共通点があり、いくつかの違いがあります。
主な違いは:
クラスは参照型であり、構造体は値型である。
ドキュメントを引用します。
値型とは、変数や定数に代入されたとき、または関数に渡されたときに値がコピーされる型のことです。
Swiftでは、構造体と列挙はすべて値型です。これは、作成した構造体と列挙のインスタンス、およびそれらがプロパティとして持つ値型は、コード内で渡されるときに常にコピーされることを意味します。
値型とは異なり、参照型は変数や定数に代入されたときや関数に渡されたときにはコピーされません。コピーではなく、同じ既存のインスタンスへの参照が使用されます。これが何を意味するのかを知るために、次の例を考えてみましょう。
class Person { var name: String var age: Int var height: Double init(name: String, age: Int, height: Double) { self.name = name self.age = age self.height = height } } var a = Person(name: "Andrea", age: 34, height: 1.84) var b = a b.age = 35 print(a.age) // prints 35
Person
を構造体struct
として定義し直すと、こんな感じになります。struct Person { var name: String var age: Int var height: Double init(name: String, age: Int, height: Double) { self.name = name self.age = age self.height = height } } var a = Person(name: "Andrea", age: 34, height: 1.84) var b = a b.age = 35 print(a.age) // prints 34構造体には、ここで取り上げた以外にも多くのことがあります。
構造体は、Swiftでデータやモデルを扱うために効果的に使うことができ、バグの少ないロバストなコードrobust code
につながります。エラー処理
Swiftのドキュメントからの定義:
エラー処理とは、プログラムのエラー状態に応答して回復するプロセスです。
DartもSwiftもエラーを処理するためのテクニックとして
try/catch
を使用していますが、いくつかの違いがあります。Dartでは、任意のメソッドは任意の型の例外を投げることができます。
class BankAccount { BankAccount({this.balance}); double balance; void withdraw(double amount) { if (amount > balance) { throw Exception('Insufficient funds'); } balance -= amount; } }例外は
try/catch
ブロックでキャッチできます。var account = BankAccount(balance: 100); try { account.withdraw(50); // ok account.withdraw(200); // throws } catch (e) { print(e); // prints 'Exception: Insufficient funds' }Swiftでは、メソッドが例外を投げることができるときに明示的に宣言します。これは
throws
キーワードで行われ、全てのエラーはエラープロトコルに準拠しなければなりません。enum AccountError: Error { case insufficientFunds } class BankAccount { var balance: Double init(balance: Double) { self.balance = balance } func withdraw(amount: Double) throws { if amount > balance { throw AccountError.insufficientFunds } balance -= amount } }エラーを処理する際には、
do/catch
ブロックの中でtry
キーワードを使用します。var account = BankAccount(balance: 100) do { try account.withdraw(amount: 50) // ok try account.withdraw(amount: 200) // throws } catch AccountError.insufficientFunds { print("Insufficient Funds") }
throw
できるメソッドを呼び出す際には、try
キーワードが必須であることに注意してください。
また、エラー自体は強く型付けされているので、すべての可能なケースをカバーするために複数のキャッチブロックを持つことができます。try, try?, try!
Swiftでは、エラーを扱うためにあまり冗長ではない方法を提供しています。
do/catch
ブロックを使わずにtry?
を使用し、これにより全ての例外は無視されるようになります。var account = BankAccount(balance: 100) try? account.withdraw(amount: 50) // ok try? account.withdraw(amount: 200) // fails silentlyあるいは、あるメソッドはエラーが投げられないことが確実な場合は、
try!
を使用できます。var account = BankAccount(balance: 100) try! account.withdraw(amount: 50) // ok try! account.withdraw(amount: 200) // crash上の例では、プログラムがクラッシュしてしまいます。したがって、
try!
はプロダクションコードでは推奨されず、テストを書くときに適しています。全体的に、Swiftにおけるエラー処理の明示的な性質はAPI設計において非常に有益です。メソッドがエラーを投げることができるかどうかを簡単に確認できるからです。
同様に、メソッド呼び出しで
try
を使用すると、投げることができるコードに注意が向けられ、エラーケースを考慮することを余儀なくされます。この点(エラー処理)では、Swift は Dart よりも安全で堅牢なものになっています。
ジェネリック
Swiftのドキュメントを引用します。
ジェネリックコードを使うと、定義した要件に従って、どんな型でも動作する柔軟で再利用可能な関数や型を書くことができます。重複を避け、明確で抽象的な方法で意図を表現するコードを書くことができます。
ジェネリックについて両言語ともサポートしています。
ジェネリックの最も一般的な使用例の1つは、配列、集合、マップなどのコレクションです。
そして、これらを使って独自の型を定義することができます。Swiftでジェネリック
Stack
型を定義する方法を以下に示します。struct Stack<Element> { var items = [Element]() mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } }同様に、Dartではこう書きます。
class Stack<Element> { var items = <Element>[] void push(Element item) { items.add(item) } void pop() -> Element { return items.removeLast() } }Swiftのジェネリックでは非常に便利で強力なもので、プロトコルの型制約や関連する型を定義するために使用することができます。
アクセス制御
Swiftのドキュメントを引用します。
アクセス制御は、他のソースファイルやモジュールのコードからコードの一部へのアクセスを制限します。この機能により、コードの実装の詳細を隠したり、そのコードにアクセスして使用できる優先インターフェースを指定したりすることができます。
Swiftには5つのアクセスレベルがあります:
open
、public
、internal
、file-private
、private
です。これらはモジュールやソースファイルを扱う際に使用されます。
モジュールとは、コード配布の単一ユニット、つまりフレームワークやアプリケーションを構築して単一ユニットとして出荷され、Swiftの
import
キーワードで別のモジュールからインポートできるものです。
open
とpublic
アクセスのレベルは、モジュールの外部からコードにアクセスできるます。
private
およびfile-private
のアクセスレベルは、定義されているファイルの外ではコードにアクセスできません。例:
public class SomePublicClass {} internal class SomeInternalClass {} fileprivate class SomeFilePrivateClass {} private class SomePrivateClass {}アクセスレベルはDartの方がシンプルで、
public
とprivate
に限定されています。Javaとは異なり、Dartには
public
、protected
、private
というキーワードはありません。識別子がアンダースコア_
で始まる場合は、それはプライベートです。例:
class HomePage extends StatefulWidget { // public @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { ... } // privateアクセス制御は、DartとSwiftでは異なる目的で設計されました。その結果、アクセスレベルが大きく異なっています。
非同期プログラミング:
Futures
非同期プログラミングはDartが最も得意とする分野です。
以下のようなユースケースを扱う場合、何らかの形の非同期プログラミングが必要になります。
- ウェブからコンテンツをダウンロードする
- バックエンドとの通信
- 長時間稼働する業務を行う
これらのケースでは、実行中のメインスレッドをブロックしない方が良いでしょう。
Dartのドキュメントを引用します。
非同期操作は、操作が終わるのを待っている間に他の作業を完了させることができます。Dartでは、非同期操作の結果を表現するためにFutureオブジェクトを使用します。
Future
を使用するには、async/await
またはFuture API
を使用します。例として、非同期プログラミングをどのように使うかを見てみましょう。
- サーバでユーザを認証する
- アクセストークンを格納してストレージを守る
- ユーザープロファイル情報を取得する
Dartでは、これは
Future
とasync/await
の組み合わせで実現できます。Future<UserProfile> getUserProfile(UserCredentials credentials) async { final accessToken = await networkService.signIn(credentials); await secureStorage.storeToken(accessToken, forUserCredentials: credentials); return await networkService.getProfile(accessToken); }Swiftでは、
async/await
というAPIを提供しておらず、クロージャ(補完ブロック)でしか実現できません。func getUserProfile(credentials: UserCredentials, completion: (_ result: UserProfile) -> Void) { networkService.signIn(credentials) { accessToken in secureStorage.storeToken(accessToken) { networkService.getProfile(accessToken, completion: completion) } } }これは、ネストされた補完(
completion
)ブロックによる「運命のピラミッド(pyramid of doom
)」につながります。そして、このシナリオではエラー処理が非常に難しくなります。Dartでは、上記コードのエラーの処理は、
getUserProfile
メソッドにtry/catch
ブロックを追加するだけで済みます。参考として、将来的にはSwiftに
async/await
を追加するという提案があります。これが実装されるまでは、開発者はGoogleの
Promises
のようなサードパーティ製のライブラリを使うことができます。非同期プログラミング:
Streams
Streams
はDartのコアライブラリの一部として実装されていますが、Swiftでは実装されていません。Dartのドキュメントを引用します。
Stream
は非同期イベントのシーケンスです。
Stream
はリアクティブなアプリケーションの基本であり、状態管理で重要な役割を果たします。例えば、ユーザーが検索フィールドのテキストを更新するたびに、新しい結果のセットが出力されます。
Stream
はSwiftのコアライブラリには含まれていません。RxSwift
のようなサードパーティのライブラリはStream
に対するサポートやその他多くの機能を提供しています。ストリーム(
Stream
)は幅広いトピックなので、ここでは詳しく説明しません。メモリ管理
Dartは高度なガベージコレクション(garbage collection)方式でメモリを管理します。
Swiftは自動参照カウント(ARC)でメモリを管理します。
これは、メモリが使われなくなるとすぐに解放されるので、優れたパフォーマンスを保証します。
しかし、コンパイラから開発者に負担を部分的にシフトしています。
Swiftでは、保持サイクルを回避するためにオブジェクトのライフサイクルと所有権について考慮し、適切なキーワード(
weak
,strong
,unowned
)を正しく使う必要があります。コンパイルと実行
まず最初に、ジャストインタイム
just-in-time
(JIT)コンパイラと事前ahead-of-time
(AOT)コンパイラの重要な違いを説明します。JITコンパイラ
JIT コンパイラはプログラムの実行中に実行され、その場でコンパイルを行います。
JIT コンパイラは、型が事前に固定されていない動的な言語で使用されるのが一般的です。JITプログラムはインタプリタや仮想マシン(VM)を介して実行されます。
AOTコンパイラ
AOT コンパイラはプログラムの作成時に実行前に実行されます。
AOT コンパイラは通常、データの型を知っている静的言語で使用されます。AOTプログラムはネイティブのマシンコードにコンパイルされ、実行時にハードウェアによって直接実行されます。
Wm Leler
氏のこの素晴らしい記事を引用します。AOTコンパイルが開発中に行われる場合、開発サイクル(プログラムに変更を加えてからその変更の結果を見るためにプログラムを実行できるようになるまでの時間)が大幅に遅くなります。しかし、AOT コンパイルは、実行時に解析やコンパイルのために一時停止することなく、より予測可能なプログラムを実行することができます。また、AOTコンパイルされたプログラムは、(すでにコンパイルされているため)より速く実行を開始します。
逆に、JITコンパイルは開発サイクルを大幅に短縮しますが、実行が遅くなったり、不安定になったりします。特に、JIT コンパイラはプログラムの実行を開始すると、コードを実行する前に解析とコンパイルを行わなければならないため、起動時間が遅くなります。研究によると、実行開始までに数秒以上かかると、多くの人がアプリを放棄してしまうという結果が出ています。静的言語として、Swiftは事前にコンパイルされます
DartはAOTとJITの両方でコンパイルできます。これはFlutterと併用した場合に大きなメリットがあります。
再度引用します。
開発時には、特に高速なコンパイラを使ってJITコンパイルを行います。そして、アプリのリリース準備が整ったら、AOTコンパイルを行います。その結果、先進的なツールとコンパイラの力を借りて、Dartは、開発サイクルの超高速化と、実行時間及び起動時間の高速化という両世界のベストを実現することができます。-
Wm Leler
Dartを使うと、両世界の最高のものを手に入れることができます。
SwiftはAOTコンパイルに主な欠点があります。つまり、コンパイル時間はコードベースのサイズに応じて増加します。
中規模のアプリ(10Kから100K行)では、アプリのコンパイルに数分かかることがあります。
Flutter アプリの場合はそうではなく、コードベースのサイズに関係なく、常に一秒以内のホットリロード(
hot-reload
)ができます。その他の機能
以下の機能は、DartとSwiftはかなり似ているので取り上げませんでした。
- 演算子
- 文字列
- Swiftではオプショナルチェイニング
Optional chaining
(Dartでは条件付きメンバーアクセスとして知られています)並行性
- 同時で並行プログラミングはDartの
isolate
で提供されています。- Swiftは
Grand Central Dispatch (GCD)
とディスパッチキュー(dispatch queue
)を使用します。Dartに欠けている私のお気に入りのSwiftの機能
- 構造体
- 関連する型を持つ列挙型
- オプションナル
Swiftに欠けている私のお気に入りのDartの機能
- JITコンパイラ
- Future と
await/async
- Stream と
yield/async*
(RxSwiftはリアクティブアプリケーション用のストリーム(Stream
)のスーパーセットを提供しています)結論
DartとSwiftはどちらも優れた言語であり、現代のモバイルアプリやそれ以上のものを構築するのに適しています。
どちらの言語も独自の長所を持っているので、どちらが優れているということはありません。
モバイルアプリの開発と2つの言語のツールを見ると、私はDartが優位に立っていると感じます。これは、Flutter のステートフルホットリロード
stateful hot-reload
の基盤となっている JIT コンパイラによるものです。ホットリロードはアプリを構築する際に生産性を大きく向上させます。
開発者の時間は、コンピューティングの時間よりも希少なリソースです。
そのため、開発者の時間を最適化することは非常にスマートな動きです。
一方で、Swiftは非常に強力な型システムを持っていると感じています。型の安全性はすべての言語機能に組み込まれており、より自然にロバスト(頑健)なプログラムにつながります。
個人的な好みはさておき、プログラミング言語は単なるツールに過ぎません。そして、その仕事に最も適したツールを選ぶのが開発者としての私たちの仕事です。
いずれにしても、どちらの言語もお互いに最高のアイデアを借りながら進化していくことを期待しています。
- 投稿日:2020-08-08T21:33:00+09:00
実践・新規モバイルアプリ開発の手順(iOS)
久しぶりに新規アプリ開発をしていて、改めて手順を確認しました。
前提
実際の開発では多くの場合、要件定義、設計、API開発、デザインがある程度並行で走るため、仕様がフワフワした状態で着手することが多いです。
その際にどのように立ち回るのがよいか。また、複数人で開発する場合にどのように作業分担するか書いてきます。
なお、設計方針はオーソドックスなMVCで、アプリの構成は一般的なAPI通信をするサービス系アプリと想定します。
設計方針やアプリの構成が異なる場合は読み替えてください。このフローは何案件かで新規開発を行った人であれば頭の中で構築しているのでしょうけど、言語化されてるのはあまり見かけません。
利用方法
- どういうタスクが存在するか確認
- 今の状態でできることは何かを見つける
- 今やってるタスクの意味を理解する
- 今やってるタスクが他のタスクとどう繋がっているか確認する
- 残タスクに対して何の資料や仕様か、何がブロック要因なのか確認する
- 進捗の目処を立てる
iOSアプリの新規開発フロー
既に仕様が全て揃っている状態からスタートする場合はこの様になると思います。
手順は必ずしも順番に行わなければならないわけではありませんが、上のタスクが終わらないと下のタスクができないことがあります。手順の確認
これらは必ずしも順番にやる必要はありません。
(順番にやらなくても良いように手順を構築しています)
仕様が固まったところから順に着手していっていいと思います。1. Codableの作成
必要なドキュメント:
API仕様書私はCodableに関してはAPI仕様書に完全に準拠させて、アプリ側で使い回すためのModelは別途作成しています。
これはCodableとModel(とEntity)を同一化すると、一部が仕様変更になった際に影響範囲が非常に広く重くなるためです。2. Sample JSONを作成し、Codableにdecodeする
必要なドキュメント:
API仕様書確認すること:
Sample JSONがちゃんとdecodeできること。WebAPIがまだ動いていない場合でもこの作業はできます。
WebAPIが既に動いている場合でも、この作業はやることをオススメします。
もしdecode周りでエラーが有った際に、WebAPIに不備があるのか、アプリ側に不備があるのかの判別が簡単にできます。
アプリ作成後は、この部分はテストコードに書き換えても良いと思います。もちろん最初からテストコードでも構いません。3. 画面が使用するModelの作成
必要なドキュメント:
仕様書全般
ER図
ワイヤー など私はCodableとModelは分ける主義です。
Modelの設計は要件定義やインターフェースの相談、デザイン面での相談も必要になります。
よく変更が入るのでコメントをちゃんと残しましょう。4. CodableからModelへの変換
これもテストコード化が可能です。
5. Modelのスタブを作成
5〜7はワンセットです。
6. 画面遷移を設計し、大まかなレイアウトを作成する
必要なドキュメント:
ワイヤー各viewに値を埋め込める状態にしておくあたりまで。
これはワイヤーや未完成のデザインの状態でも着手可能ですが、後で変更になることも想定しておいてください。7. Modelのスタブの内容を画面に表示する
画面やviewが何のModelをどのように持つか検討し、データが与えられたらそれを表示できるようにします。
8. 細かなデザインの反映
必要なドキュメント:
UI仕様書デザイン仕様ができたらそれを反映します。
NoImageやデフォルトテキスト、テキストが長い場合、端末差による画面崩れ、アニメーションなど、ここらへんはちゃんとやると結構重いです。9. API反映
APIが動いたらそれが反映されることを確認します。
10. 異常系、その他のタスク
9まで出来たら半分以上終わったイメージです。
そこから異常系とか、特殊ケースの対応をしたり、ページング処理や細かな調整をしていきます。おわりに
かなりざっくりしていますし、タスクはこれ以外にもたくさんありますが。
骨子はこんな感じで作るとスムーズかと思います。何か改善点があればご指摘ください。
- 投稿日:2020-08-08T21:12:14+09:00
【Swift】バックグラウンド的な処理をタイマーで実装してみた
どうも、ねこきち(@nekokichi1_yos2)です。
Swiftにはバックグラウンド処理を実装する方法はありますが、
・特定の用途に限定
・汎用的な方法もあるが短時間
・長時間の方法はAppleが推奨してない
・処理が安定しない
の理由で扱いが難しいです。しかし、簡単な処理ならば、バックグラウンド処理のメソッドを使用しなくても、バックグラウンドは実現できます。
そこで、デリゲートを使って、バックグラウンドに対応したタイマーを実装します。
解説
実装する機能は、
・タイマー
・バックグラウンドとのやりとり
・デリゲート
の3つ。処理の流れは、下記の通り。
1. ボタン押下
2. タイマー起動
3. バックグラウンドに移行
4. アプリ画面に復帰
5. バックグラウンドでの経過時間を残り時間から引く
6. タイマー終了使用するのは、
- backgroundTimer.swift
- SceneDelegate.swift
のファイルです。タイマーの設定
まずは、タイマーをちょちょいと。
backgroundTimer.swiftimport UIKit class backgroundTimer: UIViewController { @IBOutlet weak var currentTimeLabel: UILabel! @IBOutlet weak var start: UIButton! //タイマー var timer:Timer! //残り時間 var currentTime = 15 override func viewDidLoad() { super.viewDidLoad() currentTimeLabel.text = "15" } @IBAction func start(_ sender: Any) { timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(advancedTime), userInfo: nil, repeats: true) start.isHidden = true } @objc func advancedTime() { //残り時間が1秒以上あるか if currentTime >= 1 { currentTime -= 1 currentTimeLabel.text = "\(currentTime)" } else { timer.invalidate() start.isHidden = false currentTime = 15 currentTimeLabel.text = "\(currentTime)" } } }バックグラウンドの設定
SceneDelegateでは、
・バックグラウンドでの経過時間を取得
・デリゲートによるバックグラウンド関連の処理
を任せています。バックグラウンドでの経過時間を取得
バックグラウンド中の時間は、
アプリ画面へ復帰した時の時刻 - バックグラウンドへ移行時の時刻
で算出してます。UserDefaultで移行時の時刻を一時保持しておき、アプリ画面へ復帰する際に現在時刻を取得し、2つの時刻を引けば、指定した単位で時間を算出(今回は秒)。
SceneDelegate.swiftclass SceneDelegate: UIResponder, UIWindowSceneDelegate { let ud = UserDefaults.standard //アプリ画面に復帰した時 func sceneDidBecomeActive(_ scene: UIScene) { if バックグラウンドに移行した? { let calender = Calendar(identifier: .gregorian) let date1 = ud.value(forKey: "date1") as! Date let date2 = Date() let elapsedTime = calender.dateComponents([.second], from: date1, to: date2).second! /*ここで経過時間をタイマーに渡す*/ } } //アプリ画面から離れる時(ホームボタン押下、スリープ) func sceneWillResignActive(_ scene: UIScene) { ud.set(Date(), forKey: "date1") /*ここでバックグラウンドへの移行を検知*/ /*ここでタイマーを破棄*/ } }デリゲートの設定
デリゲートで実装したいのは、
・バックグラウンドへの移行を検知
・バックグラウンド時にタイマーを破棄
・バックグラウンドの経過時間をタイマーに渡す
の3つです。そして、用意するデリゲートの変数、関数は、下記の通り。
SceneDelegate.swiftprotocol backgroundTimerDelegate: class { //バックグラウンドの経過時間を渡す func setCurrentTimer(_ elapsedTime:Int) //バックグラウンド時にタイマーを破棄 func deleteTimer() //バックグラウンドへの移行を検知 func checkBackground() //バックグラウンド中かどうかを示す var timerIsBackground:Bool { set get } }SceneDelegate.swift
デリゲートを検知する側(SceneDelegate.swift)にデリゲートを作っておき、デリゲートメソッドで復帰後にタイマーを再実行できるように諸々を処理しています。
SceneDelegate.swiftclass SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? //デリゲート weak var delegate: backgroundTimerDelegate? let ud = UserDefaults.standard //アプリ画面に復帰した時 func sceneDidBecomeActive(_ scene: UIScene) { //タイマー起動中にバックグラウンドへ移行した? if delegate?.timerIsBackground == true { let calender = Calendar(identifier: .gregorian) let date1 = ud.value(forKey: "date1") as! Date let date2 = Date() let elapsedTime = calender.dateComponents([.second], from: date1, to: date2).second! //経過時間(elapsedTime)をbackgroundTimer.swiftに渡す delegate?.setCurrentTimer(elapsedTime) } } //アプリ画面から離れる時(ホームボタン押下、スリープ) func sceneWillResignActive(_ scene: UIScene) { ud.set(Date(), forKey: "date1") //タイマー起動中からのバックグラウンドへの移行を検知 delegate?.checkBackground() //タイマーを破棄 delegate?.deleteTimer() } }backgroundTimer.swift
デリゲートの処理を実行する側(backgroundTimer.swift)にデリゲート、デリゲートの変数・関数郡の設定をします。
backgroundTimer.swiftclass backgroundTimer: UIViewController,backgroundTimerDelegate { @IBOutlet weak var currentTimeLabel: UILabel! @IBOutlet weak var start: UIButton! //タイマー起動中にバックグラウンドに移行したか var timerIsBackground = false var timer:Timer! var currentTime = 15 override func viewDidLoad() { super.viewDidLoad() //SceneDelegateを取得 guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let sceneDelegate = windowScene.delegate as? SceneDelegate else { return } //デリゲートを設定 sceneDelegate.delegate = self } func checkBackground() { //バックグラウンドへの移行を確認 if let _ = timer { timerIsBackground = true } } func setCurrentTimer(_ elapsedTime:Int) { //残り時間から引数(バックグラウンドでの経過時間)を引く currentTime -= elapsedTime currentTimeLabel.text = "\(currentTime)" //再びタイマーを起動 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(advancedTime), userInfo: nil, repeats: true) } func deleteTimer() { //起動中のタイマーを破棄 if let _ = timer { timer.invalidate() } } }バックグラウンドへの移行を検知
タイマー起動中にバックグラウンドへの移行した時、のみを検知するために、タイマーが起動中かどうかをif文でチェックしています。
もしチェックしなければ、タイマーを起動してない状態からバックグラウンドへの移行も検知してしまいます。
backgroundTimer.swiftfunc checkBackground() { //バックグラウンドへの移行を確認 if let _ = timer { timerIsBackground = true } }バックグラウンドへの移行時にタイマーを破棄
checkBackground()と同様に、タイマーが起動中かをチェックしてから、タイマーを破棄しています。
残念ながら、Timerクラスには一時停止する機能がないので、タイマー処理を止めるには破棄するしかありません。
backgroundTimer.swiftfunc deleteTimer() { //起動中のタイマーを破棄 if let _ = timer { timer.invalidate() } }バックグラウンドでの経過時間をタイマーに渡す
SceneDelegateで算出した経過時間を引数でタイマーに渡します。
バックグラウンドへ移行時にタイマーが破棄されたので、残り時間のcurrentTimeはカウントダウンされないままだったので、引数で受け取った経過時間を引きます。
また、アプリ画面へ復帰時に実行されるので、タイマー処理を再実行してます。
backgroundTimer.swiftfunc setCurrentTimer(_ elapsedTime:Int) { //残り時間から引数(バックグラウンドでの経過時間)を引く currentTime -= elapsedTime currentTimeLabel.text = "\(currentTime)" //再びタイマーを起動 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(advancedTime), userInfo: nil, repeats: true) }実装結果
ホームボタン押下、スリープ、の順にバックグラウンドへ移行してます。
ソースコード
backgroundTimer.swiftimport UIKit class backgroundTimer: UIViewController,backgroundTimerDelegate { @IBOutlet weak var currentTimeLabel: UILabel! @IBOutlet weak var start: UIButton! //タイマー起動中にバックグラウンドに移行したか var timerIsBackground = false var timer:Timer! var currentTime = 15 override func viewDidLoad() { super.viewDidLoad() //SceneDelegateを取得 guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let sceneDelegate = windowScene.delegate as? SceneDelegate else { return } sceneDelegate.delegate = self currentTimeLabel.text = "15" } @IBAction func start(_ sender: Any) { timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(advancedTime), userInfo: nil, repeats: true) start.isHidden = true } @objc func advancedTime() { if currentTime >= 1 { currentTime -= 1 currentTimeLabel.text = "\(currentTime)" } else { timer.invalidate() start.isHidden = false currentTime = 15 currentTimeLabel.text = "\(currentTime)" } } func checkBackground() { //バックグラウンドへの移行を確認 if let _ = timer { timerIsBackground = true } } func setCurrentTimer(_ elapsedTime:Int) { //残り時間から引数(バックグラウンドでの経過時間)を引く currentTime -= elapsedTime currentTimeLabel.text = "\(currentTime)" //再びタイマーを起動 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(advancedTime), userInfo: nil, repeats: true) } func deleteTimer() { //起動中のタイマーを破棄 if let _ = timer { timer.invalidate() } } }SceneDelegate.swiftimport UIKit //デリゲート用の変数、関数 protocol backgroundTimerDelegate: class { func setCurrentTimer(_ elapsedTime:Int) func deleteTimer() func checkBackground() var timerIsBackground:Bool { set get } } class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? weak var delegate: backgroundTimerDelegate? let ud = UserDefaults.standard func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let _ = (scene as? UIWindowScene) else { return } } //アプリ画面に復帰した時 func sceneDidBecomeActive(_ scene: UIScene) { if delegate?.timerIsBackground == trued { let calender = Calendar(identifier: .gregorian) let date1 = ud.value(forKey: "date1") as! Date let date2 = Date() let elapsedTime = calender.dateComponents([.second], from: date1, to: date2).second! delegate?.setCurrentTimer(elapsedTime) } } //アプリ画面から離れる時(ホームボタン押下、スリープ) func sceneWillResignActive(_ scene: UIScene) { ud.set(Date(), forKey: "date1") delegate?.checkBackground() delegate?.deleteTimer() } }参考
[iOS]バックグラウンドで長時間BLE通信続ける方法
【Swift 4.2】 アラーム時計の作り方 - Qiita
[Swift] iOSのバックグラウンド処理について - Qiita
iOSにおけるバックグラウンド処理の全体感 - Qiita
[iOS][小ネタ] アプリのバックグラウンド実行を禁止する方法 | Developers.IO
- 投稿日:2020-08-08T20:44:31+09:00
【Flutter】Pigeon を使ってネイティブコードをスマートに呼ぶ
はじめに
先日発表された、 Flutter 1.20 のリリース記事 で、 Pigeon というパッケージが紹介されていました。
このパッケージを使うことで、
- ネイティブ側と型安全に通信
- 自動生成よって手書きコード量を削減
することができます。
今回は、このパッケージを使って、swift / Kotlin で実装した単純な add メソッドを Flutter 側 から呼ぶ方法を見ていきます。サンプルプロジェクトはこちらで公開しています。
Pigeon が行うこと
Dart ファイルで定義した、引数や戻り値の情報を元に、Java / Objective-C のインターフェースやプロトコルを自動生成します。
ネイティブ側ではこれらを元にした実装を行うことで、Flutter 側と型安全に通信することができるようになります。インストール
pubspec.yamldev_dependencies: pigeon: ^0.1.4Dart 側
まずは、ネイティブと通信するスキーマを定義した dart ファイルを作ります。
自分の場合はプロジェクトルートにpigeon/
というフォルダを作ってその中に置きました。schema.dartimport 'package:pigeon/pigeon.dart'; // 引数の定義 class AddRequest { int n1; int n2; } // 戻り値の定義 class AddReply { int result; } @HostApi() abstract class Api { AddReply add(AddRequest req); } // 生成されるファイルの出力先などの設定 void configurePigeon(PigeonOptions opts) { opts.dartOut = 'lib/api_generated.dart'; opts.javaOut = 'android/app/src/main/java/io/flutter/plugins/Pigeon.java'; opts.javaOptions.package = "io.flutter.plugins"; opts.objcHeaderOut = 'ios/Runner/Pigeon.h'; opts.objcSourceOut = 'ios/Runner/Pigeon.m'; opts.objcOptions.prefix = 'FLT'; }次に、pigeon コマンドを実行して、必要な Java ファイルなどを生成します。
flutter pub run pigeon --input pigeon/schema.dart
あとは、ネイティブ関数を呼びたい箇所で、Pigeon によって生成された dart ファイルをインポートして使うだけです。
home_page.dartimport './api_generated.dart'; void callNativeAdd () async { final api = Api(); final req = AddRequest() ..n1 = 10 ..n2 = 20; final reply = await api.add(req); print(reply.result); // prints 30 }Kotlin 側
先ほどの pigeon コマンドによって、Api のインターフェースが書かれた Java ファイルが生成されているので、これを実装したクラスを作って、setup メソッドに渡します。
MainActivity.ktclass MainActivity: FlutterActivity() { // 1. 自動生成されたApiインターフェースを実装したクラスを作る private class MyApi: Pigeon.Api { override fun add(arg: Pigeon.AddRequest): Pigeon.AddReply { val reply = Pigeon.AddReply() reply.result = arg.n1 + arg.n2 return reply } } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // 2. setup()を呼ぶ Pigeon.Api.setup(flutterEngine.dartExecutor.binaryMessenger, MyApi()) } }自分の場合は kotlin から Java 側の static なメソッドは呼べないよというエラーが出ましたが、エラーメッセージにしたがって build.gradle に下記を追加することで動きました。
app/build.gradleandroid { ... kotlinOptions { jvmTarget = '1.8' } }Swift 側
Pigeon によって生成された Objective-C ファイルを Swift 側から参照できるようにするため、
ios/Runner
内にあるRunner-Bridging-Header.h
にインポート文を追加します。(参考)ios/Runner/Runner-Bridging-Header.h#import "Pigeon.h"
次に、生成されたプロトコルを実装したクラスを作ります。
専用のファイルを作っても良いですし、AppDelegate.swift
内にベタ書きでも OK です。class MyApi: FLTApi { func add(_ input: FLTAddRequest, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> FLTAddReply? { let reply = FLTAddReply() let result = input.n1!.intValue + input.n2!.intValue reply.result = NSNumber.init(value: result) return reply } }あとは、
AppDelegate
内で Api をインスタンス化して setup に渡すだけです。AppDelegate.swift@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController // Setupを呼ぶ FLTApiSetup(controller.binaryMessenger, MyApi()) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }まとめ
今まで MethodChannel を使って書いていたネイティブとの接続部分を自動生成してくれる。( 厳密にはPigeon では BasicMessageChannel が使われてる)
事前にスキーマを定義するので、ネイティブと型安全に通信できる
生成されるコードは、Java / Objective-Cだが、Kotlin からは Java が、Swift からは Objective-C が呼べるので、普通に使える。
Dart 側からネイティブのコードを意識しなくて良い (自動生成されたAPIを呼ぶだけ)
ネイティブ側から Dart のコードを意識しなくて良い (自動生成されたインターフェースを実装するだけ)
公式の vider_player プラグインで実際に使われていたり、 Flutter 1.20 のリリース記事 で紹介されていたりするので、今後広まっていくかも。
- 投稿日:2020-08-08T20:44:31+09:00
【Flutter】Pigeon を使ってネイティブコードを型安全に呼ぶ
はじめに
先日の、 Flutter 1.20 のリリース記事 で、 Pigeon というパッケージの紹介がありました。
本来 Flutter 側からネイティブコードを呼ぶには、関数名や引数などを文字列ベースで合わせる必要があるなど、少し大変ですが、このパッケージを使うことで、
- ネイティブ側と型安全に通信
- 自動生成よる手書きコード量の削減
が可能になります。
本投稿では、このパッケージを使って、swift / Kotlin で実装した単純な add メソッドを Flutter 側 から呼ぶ方法を見ていきます。サンプルプロジェクトはこちらで公開しています。
Pigeon が行うこと
Dart 側で定義した、引数や戻り値の情報を元に、Java / Objective-C のインターフェースやプロトコルを自動生成します。
ネイティブ側ではこれらを元に実装を行うことで、Flutter 側と型安全に通信することができるようになります。
イメージとしては、 TypeScript で型定義ファイルを作るのに近いかもしれません。インストール
pubspec.yamldev_dependencies: pigeon: ^0.1.4Dart 側
まずは、ネイティブと通信するスキーマを定義した dart ファイルを作ります。
自分の場合はプロジェクトルートにpigeon/
というフォルダを作ってその中に置きました。schema.dartimport 'package:pigeon/pigeon.dart'; // 引数の定義 class AddRequest { int n1; int n2; } // 戻り値の定義 class AddReply { int result; } @HostApi() abstract class Api { AddReply add(AddRequest req); } // 生成されるファイルの出力先などの設定 void configurePigeon(PigeonOptions opts) { opts.dartOut = 'lib/api_generated.dart'; opts.javaOut = 'android/app/src/main/java/io/flutter/plugins/Pigeon.java'; opts.javaOptions.package = "io.flutter.plugins"; opts.objcHeaderOut = 'ios/Runner/Pigeon.h'; opts.objcSourceOut = 'ios/Runner/Pigeon.m'; opts.objcOptions.prefix = 'FLT'; }次に、pigeon コマンドを実行して、必要な Java ファイルなどを生成します。
flutter pub run pigeon --input pigeon/schema.dart
あとは、ネイティブ関数を呼びたい箇所で、Pigeon によって生成された dart ファイルをインポートして使うだけです。
home_page.dartimport './api_generated.dart'; void callNativeAdd () async { final api = Api(); final req = AddRequest() ..n1 = 10 ..n2 = 20; final reply = await api.add(req); print(reply.result); // prints 30 }Kotlin 側
Pigeon によって生成された Java ファイルの中に Api のインターフェースが書かれているので、これを実装したクラスを作って、setup メソッドに渡します。
MainActivity.ktclass MainActivity: FlutterActivity() { // 1. 自動生成されたApiインターフェースを実装したクラスを作る private class MyApi: Pigeon.Api { override fun add(arg: Pigeon.AddRequest): Pigeon.AddReply { val reply = Pigeon.AddReply() reply.result = arg.n1 + arg.n2 return reply } } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // 2. setup()を呼ぶ Pigeon.Api.setup(flutterEngine.dartExecutor.binaryMessenger, MyApi()) } }自分の場合は kotlin から Java 側の static なメソッドは呼べないよというエラーが出ましたが、エラーメッセージにしたがって build.gradle に下記を追加することで動きました。
app/build.gradleandroid { ... kotlinOptions { jvmTarget = '1.8' } }Swift 側
Pigeon によって生成された Objective-C ファイルを Swift 側から参照できるようにするため、
ios/Runner
内にあるRunner-Bridging-Header.h
にインポート文を追加します。(参考)ios/Runner/Runner-Bridging-Header.h#import "Pigeon.h"
生成されたファイルには、 Api のプロトコルが書かれているので、これを実装したクラスを作ります。
専用のファイルを作っても良いですし、AppDelegate.swift
内にベタ書きでも OK です。class MyApi: FLTApi { func add(_ input: FLTAddRequest, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> FLTAddReply? { let reply = FLTAddReply() let result = input.n1!.intValue + input.n2!.intValue reply.result = NSNumber.init(value: result) return reply } }あとは、
AppDelegate
内で Api をインスタンス化して setup に渡すだけです。AppDelegate.swift@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController // Setupを呼ぶ FLTApiSetup(controller.binaryMessenger, MyApi()) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }まとめ
今まで MethodChannel を使って書いていたネイティブとの接続部分を自動生成してくれる。( 厳密にはPigeon では BasicMessageChannel が使われてる)
事前にスキーマを定義するので、ネイティブと型安全に通信できる
生成されるコードは、Java / Objective-Cだが、Kotlin からは Java が、Swift からは Objective-C が呼べるので、普通に使える。
Dart 側からネイティブのコードを意識しなくて良い (自動生成されたAPIを呼ぶだけ)
ネイティブ側から Dart のコードを意識しなくて良い (自動生成されたインターフェースを実装するだけ)
公式の vider_player プラグインで実際に使われていたり、 Flutter 1.20 のリリース記事 で紹介されていたりするので、今後広まっていくかも。
- 投稿日:2020-08-08T19:32:59+09:00
Swiftで画像を丸くする方法(かんたん!)
- 投稿日:2020-08-08T15:29:11+09:00
GitHub奮闘記②【手順まとめ】
㊗︎ 初 push
用語に続いて、この勢いで 手順もまとめます。
GitHubのユーザー登録は済ませて下さい。手順
0.最終的に、こんな感じ。
「GitTestProject」というリポジトリ名。
initial Commit
に加えて、追加したCommitが反映されています。1. gitの初期設定?
Gitに「ユーザ名」「メールアドレス」を登録します。
GitHubと同じものを指定しておくのが良いです。以下のコードを元に、ターミナルで設定します。(1行ずつ)
git config --global user.name "ユーザー名"git config --global user.email メールアドレスターミナル ?
Xcode内にターミナルは無いですが、Macに標準インストールされてます。
Finder -> アプリケーション -> ユーティリティ -> ターミナル
2. プロジェクト開始と同時に、リポジトリを作成?
Create Git repository on (My Mac)にチェック。✅
initial Commit
されます。
Commit
= 変更履歴を、ローカルリポジトリに保存すること。✅により、プロジェクト開始と同時にローカルリポジトリが作成され、
そこにソースコードが保存されました。
initial Commit
では、デフォルトのコードしかCommitされません。viewController.swiftimport UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } }補足
Xcodeメニューバーにて、
Source Control > Create Git Repositories...
からでもinitial Commit
出来ますが、リポジトリを「✅」で作成するデメリットは特に無いので、
チェック入れるのが吉。3. リモートリポジトリを作成する。?
Remoteを右クリック > Create "GitTestProject" Remote..
で、Create画面が出ます。createを押すと、「Pushing...」 ぐるぐるLoadingされます。?
ご自身でGitHubにログインして、
リモートレポジトリが作られているか確認してみましょう。
initial Commit
のみがpushされていると思います。
無事、push完了です?4. コード変更後、Xcodeでプッシュする。?
さて、『いつ、誰が、どこに、どのような変更を行ったか』。
バージョン管理がGitの利点です。実際にコードを変更して、Commitして、pushしてみます。
コードいじる。
ViewController.swift
を修正後、Commitします。
今回は以下のコードを追記してみました。ViewController.swiftfunc Log() { print("Hello") }変更したので、Commitする。
ゲームでいう、「セーブ」
- Mのついたファイルを右クリックして、
Source Control > Commit "ViewController.swift"...
(M= Modify: 修正する)- Xcodeメニューバーからでも、出来ます。
Commit完了!
補足
Commit Message欄には、『どのような変更を行ったか』を記載します。
書かないと、Commitを実行できません。
コード変更したので、Xcodeでプッシュする。
Xcodeメニューバーにて、
Source Control > Push...
GitHubを確認。
無事、pushできています✌️変更箇所も分かりやすい。
これで共同開発チームの皆が、コード変更を見ることができます。?おしまい。
参考サイト
Xcodeでgit機能を使う(プロジェクトの作成からgithubにpushするまで)
XcodeからgitとGitHubを使う方法・基本編関連記事
追記予定
- 投稿日:2020-08-08T15:28:42+09:00
GitHub奮闘記①【用語まとめ】
㊗︎ 初 push
先日まで push が出来なかったのですが、
gitの初期設定をしていなかった事が原因でした。
以下のコードを元に、ターミナルで設定します。(1行ずつ)git config --global user.name "ユーザー名"git config --global user.email メールアドレスターミナル ?
Xcode内にターミナルは無いですが、Macに標準インストールされてます。
Finder -> アプリケーション -> ユーティリティ -> ターミナル
用語
「リポジトリ」 (repository)
- ソースコードを管理する単位。
-「保管場所」をカッコ付けて言った表現。
-「ローカル」と「リモート」 がある。
- ローカルリポジトリで作業を行い、その作業内容をリモートリポジトリへpush。
-
今さら聞けない!GitHubの使い方【超初心者向け】「ソースコード」
人間が理解しやすいプログラミング言語を使って、記述されたもの。
プログラムの"元" (source)なので、ソースプログラムとも言われる。
ソースコードをコンパイルして、プログラムが作られる。
「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典「バージョン管理」
システムの変更を管理すること。
『いつ、誰が、どこに、どのような変更を行ったか』Webデザイン, Webライターとかにも使われる。
ちょっと、ややこしい用語?
Git と GitHub
-
Git
ソースコードのバージョンを管理するためのシステム。-
GitHub
GitHub社が作ったサービス。Gitに、便利な機能を付け加えた。「Git」
リーナス・トーバルズ氏が開発。
Linux?も開発した、すごい人らしい。
「GitHub」
よく見かける生物。
以下、GitHub共同創業者さんのインタビュー引用。コマンドラインで使うにはGitは最高だったんですが、
オンラインでコードをシェアするのが難しかったのです。他の開発者と、コードをシェアしたかったのです。
GitHubは、週末のサイドプロジェクトとしてスタートしました。push と commit
-
commit
変更履歴を、ローカルリポジトリに保存すること。
-push
ローカルリポジトリ内の変更履歴を、GitHubに送ること。
(=リモートリポジトリ にアップロードすること。)commit → ゲームのセーブ push → セーブデータをサーバに保存commit → メールの下書き保存 push → メールの送信おしまい。
関連記事
追記予定
- 投稿日:2020-08-08T13:33:10+09:00
[Swift勉強会] カウントアップアプリを改造して,通知をn秒後に通知を出してみる (n>0)
前回までの内容
カウントアップアプリの作成まではこちら
作:https://qiita.com/appgrape
今回のゴール
- 通知の基本文法がわかる
- UISwitchの基本的な使い方がわかる
- Switchをオンにした時に通知される
- アプリ内で通知が来る
- カウントアップで指定した数秒後に通知が出せる
完成アプリ
完成品がこちらになります
アプリの実装
UISwitchの実装
UIの実装
前回までと同様に
Main.Storyboard
にUIパーツを配置する(今回はUISwitch
)
次に設置したUISwitchをクリックし,画面右の
Attributes Inspector
をクリックする
するとAttributes Inspector
の一番上に,Switchの初期状態をセットするState
があるのでオンからオフに変える
そして
Assistant Editor
を起動してViewController.Swift
にUISwitchを紐付ける.
名前は
onSwitchDidChanged
とした.
画像のようにType
はUISwitch
とするコードの実装
次にコードを書く
ViewController.swift@IBAction func onSwitchDidChanged(_ sender: UISwitch) { //switchを押すとこの中身が動く if sender.isOn { //switchをオンにするとswitch tapped!が出力される print("switch tapped!") } }
通知の実装
通知の実装ではStoryboardをいじることはない
(全部コードで完結する)
通知の許可を取る
通知機能のあるiOSアプリには,まず起動時に通知の許可を取る必要がある
次のコードを追加するAppDelegate.swiftimport UIKit import UserNotifications //<-通知関係を使用する時に必要 //~~(中略)~~ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. //ここから // 通知許可の取得 UNUserNotificationCenter.current().requestAuthorization( options: [.alert, .sound, .badge]){ //許可を取るもの: // .alert -> 通知のポップアップを許可するか, // .sound -> 通知音を許可するか, // .badge -> 通知時にアプリアイコンに通知数の表示を許可するか (granted, _) in if granted{ UNUserNotificationCenter.current().delegate = self } } //ここまで追加 return true } //~~(中略)~~ //コード末尾に追加 extension AppDelegate: UNUserNotificationCenterDelegate{ }このコードを書くと,アプリ起動時に1回だけ通知許可のポップアップが出てくる.
ただこれは許可を取っただけなので,通知自体は実装されていない.
通知のテストをしてみる
前のページで通知の許可を取ったため,次は実際にswitchを押して通知させてみる.
次のコードを追加するViewController.swiftimport UIKit import UserNotifications //<-通知関係を使用する時に必要 //~~(中略)~~ @IBAction func countDounButton(_ sender: Any) { } //ここから @IBAction func onSwitchDidChanged(_ sender: UISwitch) { //switchを押すとこの中身が動く if sender.isOn { //switchをオンにすると通知が来る showNotification() } } func showNotification() { let content = UNMutableNotificationContent() //<-通知のコンテンツを入れる定数contentを初期化 content.title = "countUpAppForBeginners" //<-通知のタイトル content.body = "switchをオンにしました。" //<-通知の詳細文 content.sound = UNNotificationSound.default //<-通知の音はデフォルトのやつ let request = UNNotificationRequest(identifier: "changedSwitch", content: content, trigger: nil) //通知のリクエストを任意の名前(identifier),内容(content),発動条件(trigger)をつけて定数に保存する UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) //通知センターに通知のリクエストを追加する }動かない
実は
let request
のtriggerがnilだと,即座に通知が呼ばれる
しかし通知するアプリが起動中は通知はこない(設定でアプリ内でも通知可能)
つまり通知は機能してるが見えない状態
なので3秒後に通知に設定して確認する
3秒後に通知してみる
ViewController.swift
に次のコードを追加,編集するViewController.swiftfunc showNotification() { let content = UNMutableNotificationContent() //content:通知に表示するものを編集する content.title = "countUpAppForBeginners" content.body = "switchをオンにしました。" content.sound = UNNotificationSound.default let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(3), repeats: false) //3秒後に通知させる let request = UNNotificationRequest(identifier: "changedSwitch", content: content, trigger: trigger)//<-ここをnilからtriggerに変える UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) }注意点としてtrriger=0だとエラーが出る
n秒後に通知してみる(n > 0)
最後にカウントアップで設定した値秒後に通知を実装する
前回のところでtrriger=0だとエラーが出ると述べた.
試していないがおそらく負の値でもエラーが出るのでカウントアップを1未満にならないように設定する.ViewControlelr.swift//~~(前略)~~ //数字を格納する場所 var count = 1 //<- 初期値を0から1に //~~(中略)~~ @IBAction func countDounButton(_ sender: Any) { //-ボタンを押すとラベルの文字をカウントダウン if count > 1 { //countが1未満にならないようにカウントダウンさせる count = count - 1 countLabel.text = String(count) } //カウントにあわせて文字の色を変更 changeTextColor() } //~~(中略)~~ //n秒後に通知させる(n>0) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(count), repeats: false) //countをTimerIntervalに変換する //~~(後略)~~これで完成
おまけ:アプリ内で通知させてみる
AppDelegate
に次のコードを追加するだけAppDelegate.swiftextension AppDelegate: UNUserNotificationCenterDelegate{ func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { // アプリ起動中でも通知させる completionHandler([.alert, .sound]) } }
最後に
今回はUISwitchの使い方と通知の実装をざっくりと説明しました.
質問,訂正があればコメント欄かtwitterにお願いします
- 投稿日:2020-08-08T12:08:15+09:00
[SwiftUI]Buttonのカスタマイズ
SwiftUIでButtonのカスタマイズ方法を調べました。
カスタマイズ方法はUIKitのUIButtonとだいぶ違ったので、試行錯誤に時間がかかりました。どのようなボタンを作るか
- 背景が塗りつぶしで角丸
- シャドーがある
- ボタンを押した時のエフェクトをつける
作成したもの
Playground
- 1つ目:角丸の通常ボタン
- 2つ目:角丸の無効状態のボタン
- 3つ目:角丸が完全な丸のボタン
実装方法
SwiftUIのButtonStyleを使います。
文字の内容・画像とそのpaddingについてはButtonStyleで共通化できませんでした。(paddingが共通化できないのはなんでだろう)CustomButtonStyle.swiftstruct CustomButtonStyle: ButtonStyle { @State var isEnabled: Bool //ボタン有効・無効 var cornerRadius: CGFloat //角丸半径 var color: Color //通常Color var disabledColor: Color //無効Color var textColor: Color //テキストcolor func makeBody(configuration: Configuration) -> some View { let isPressed = configuration.isPressed let foregroundColor = isEnabled ? color : disabledColor return configuration.label .background(RoundedRectangleFill.init(cornerRadius: cornerRadius, fillColor: foregroundColor)) .foregroundColor(textColor) .shadow(color: foregroundColor, radius: 5, x: 0, y: 0) .scaleEffect(x: isPressed ? 0.95 : 1, y: isPressed ? 0.95 : 1, anchor: .center) .animation(.spring(response: 0.2, dampingFraction: 0.9, blendDuration: 0)) } }細かい実装はソースを見てください。
- cornerRadiusに
.infinity
を設定すると、角丸が完全な丸になります。(スクショ3つ目のボタン)- backgroundにRoundRectangleベースの角丸塗りつぶしを設定しています
- foregroundColorでテキスト・画像の色を設定しています
- shadowで影を描画しています
- scaleEffectとanimationでボタンを押した時のアニメーションを設定しています
ソース
こちらに置いてあります。
参考
https://developer.apple.com/documentation/swiftui/buttonstyle
https://developer.apple.com/documentation/swiftui/primitivebuttonstyle
https://medium.com/better-programming/how-to-build-and-customize-buttons-in-swiftui-448f5994022d
https://qiita.com/muukii/items/46125dced6be51ddb736
https://stackoverflow.com/questions/59169436/swiftui-buttonstyle-how-to-check-if-button-is-disabled-or-enabled
https://will.townsend.io/2019/an-intro-to-swiftui-button-styles
- 投稿日:2020-08-08T10:30:25+09:00
[swift]エラーThread 1: Exception: "App ID not found. Add a string value with your app ID for the key FacebookAppID to the Info.plist or call [FBSDKSettings setAppID:]."の対処法
エラー内容
Firebaseを用いてFacebookログインを実装しており、シュミレーター(実機)でログインボタンをクリックし動作確認を行った際に、画面遷移されずアプリケーションがフリーズしてしまうエラーです。
Xcodeを確認したところ
Thread 1: Exception: "App ID not found. Add a string value with your app ID for the key FacebookAppID to the Info.plist or call [FBSDKSettings setAppID:].
というエラーが発生しておりました。環境▼
Swift version 5.2.4
Xcode version 11.6仮説
エラー文を翻訳すると"アプリのIDが見つからないので、info.plistにアプリIDを登録するか、[FBSDKSettings setAppID:]を用いて文字列の値を追加するかをして下さい。"
というようにまとめることができます。
エラー文を元に仮説を立てると...
仮説①info.plistのアプリIDの登録を誤っている。
仮説②ファイル内のいずれかに"FBSDKSettings setAppID:"を用いてString型でアプリIDを宣言する。といったように候補が挙がる。一個ずつ確認するしかないですね!
仮説を元に対応したこと
まず仮説①を検証します。
info.plist/Property List
を確認したところ問題なさそう。
念の為info.plist/Open As/Source Code
も確認。(下記画像)
ん?<key>FacebookAppID</key>
の箇所の頭に空白がありますね。
もしかしてこれが原因かな?仮設検証
訂正して再ビルド、シミュレーターにて動作確認...
無事、画面遷移されFacebookログインが実行されました!今回はエラー文を翻訳すると、ヒントが記されていましたね。
ちなみに私は翻訳機能としてDeepL翻訳というアプリを使っています!
https://www.deepl.com/ja/translatorよかったら参考にしてみて下さい。
- 投稿日:2020-08-08T03:15:17+09:00
[Unity]iosでキーボードから文字入力をする&キャンセル処理
概要
環境
Unity 2019.4.5f1
Xcode 11.6
ios 13.5.1
InputField公式リファレンス
https://docs.unity3d.com/2019.1/Documentation/ScriptReference/UI.InputField.htmlInputField
InputFieldはPCだとキーボードから入力可能だが、スマホへBuildするとスマホのキーボードが出てきて入力が可能になる。
実装は簡単で、Create/UIから選択するだけでいろいろ整えてくれたオブジェクトを出してくれる。
こちらでやるべき部分は下のコード部分。InputFieldManager.csusing UnityEngine.UI; public class InputFieldManager : MonoBehaviour { private InputField inputField; public string resultText; // 入力されたテキストを格納 void Start(){ inputField = this.gameObject.GetComponent<InputField>(); InitInputField (); } // フィールドの初期化 private void InitInputField () { inputField.text = ""; inputText = ""; } // OnValueCangeで呼び出す関数 public void ChangeText(){ // 入力したテキストをstringに格納する resultText = inputField.text; } // OnEndEditで呼び出す関数 public void FinishEditText(){ // 入力が終わった後にどこかに渡すとか // 入力が終わったので初期化 InitInputField (); } }スクリプトを書いたら、適当なオブジェクト(InputFieldやInputPanelなど)にアタッチして、インスペクターからOnValueChangedとOnEndEditのそれぞれにどの関数を呼び出すか指定する。
ちなみに呼び出される順番はOnValueChanged(inputField.textが変わるたびに) → OnEndEdit
キャンセル処理
LINEなどを使ってる時を想像して欲しい。
何かを入力していたが、あれ?と思って一旦中断してキーボードを消し、確認した後で再入力ということがあると思う。CPと挙動が異なる点として、PCはOnEndEditが呼ばれたりinputField.isFocused == falseの場合でも、InputFieldにあるテキストは消えないが、
iosの場合、InputFieldではキーボードを消してしまうとInputFieldにあるテキストも消えてしまう。そこで入力をキャンセルした時にInputFieldにあるテキストを保持したい。
テキストの保持は以下のコードでできる。
注意点として、TouchScreenKeyboard.isSupported=true(Unity Editorは=false)環境のみでTouchScreenKeyboard.Statusは動作する。#if UNITY_IOS
などで囲んでおくといいかも。InputFieldManager.csbool isCancel = false; // OnValueCangeで呼び出す関数 public void ChangeText(){ if (inputField.touchScreenKeyboard.status == TouchScreenKeyboard.Status.Canceled) { // Cancleを押した時 isCancel = true; } else if (inputField.isFocused && !isCancel){ // 他のところをタップした時 inputText = inputField.text; Debug.Log ("inputText: " + inputText); } } // OnEndEditで呼び出す関数 public void FinishEditText(){ if (inputField.touchScreenKeyboard.status == TouchScreenKeyboard.Status.Done) { // 入力完了時何かに渡す // フィールドの初期化 InitInputField (); Debug.Log ("Done"); } else if (inputField.touchScreenKeyboard.status == TouchScreenKeyboard.Status.Canceled || inputField.touchScreenKeyboard.status == TouchScreenKeyboard.Status.LostFocus) { // 入力キャンセル時もしくは inputField.text = inputText; isCancel = false; Debug.Log ("Canseled"); } else { // 他の部分をタップした場合 inputField.text = inputText; isCancel = false; Debug.Log ("Canseled"); } }キーボードとInputField以外の部分をタップすると
inputField.isFocused == false
となり、キーボードが消えてしまう。なぜか、inputField.touchScreenKeyboard.status == TouchScreenKeyboard.Status.LostFocusは呼ばれないようなのでFinishEditTextではelseにもキャンセルされたと認識して同じ処理をしている。
また、inputField.isFocused == false
を利用して他のところがタップされた時はテキストは更新されないようにしている。他にはまった点としては元々はChangeTextでも
resultText = inputField.text
という処理を行っていたが、入力した後にCancelボタンを押すとなぜかinputField.text = ""
という挙動になった。どうやらデフォルトっぽい挙動だが、邪魔だったのでChangeTextでキャンセルが押された場合にはフラグを変更させるだけにさせた。フラグを作った理由としてはCancelを押してキーボードを閉じた場合、
inputField.isFocused == true
となりつつinputField.text = ""
が入ってしまうので、他の部分をタップした時と区別するために行っている。位置調整
下からキーボードが出てくるため何もしなければキーボードで見えなくなる部分が出てくる。
今回はゴリ押しでキーボードの高さを調べて、重なってしまうUIの位置を上にずらすことで対応した。動かすUIは一括りのGameObjectに入れてlocalPositionで変更するのが楽。今回の場合は、InputPanelの上にさらにまとめるGameObjectがあったのでそれを移動させている。
InputFieldのみの移動で大丈夫なら、InputPanelをlocalPosition.y変更すればいい。
Start時に初期位置を保存しておき、
inputField.isFocused
の時一回だけ移動させるようにしてる。
キャンセルか何かでキーボードが消えた時に初期位置を代入してやると元に戻る。private void StartInputText () { if (inputField.isFocused && isOnceInput) { isOnceInput = false; // y軸をいい感じの値にする parentRect.localPosition += new Vector3 (0, 940f, 0); } }最終的なコード
InputFieldManager.csusing UnityEngine; using UnityEngine.UI; public class InputFieldManager : MonoBehaviour { private InputField inputField; public string resultText; // 入力されたテキストを格納 private RectTransform parentRect; private Vector3 defaultParentPos; // 初期位置 private bool isOnceInput = true; // 入力時のfooter・bodyの位置移動フラグ private bool isCancel = false; // cancelボタンが押されたか void Start(){ inputField = this.gameObject.GetComponent<InputField>(); parentRect = this.transform.parent.GetComponent<RectTransform> (); defaultParentPos = parentRect.localPosition; InitInputField (); } void Update(){ StartInputText () } // 入力開始時 private void StartInputText () { if (inputField.isFocused && isOnceInput) { isOnceInput = false; // y軸をいい感じの値にする parentRect.localPosition += new Vector3 (0, 940f, 0); } } // キーボードによって上にずれたUIの位置を戻す public void ResetKeybord () { isOnceInput = true; parentRect.localPosition = defaultParentPos; isCancel = false; } // フィールドの初期化 private void InitInputField () { inputField.text = ""; inputText = ""; ResetKeybord (); } // OnValueCangeで呼び出す関数 public void ChangeText(){ if (inputField.touchScreenKeyboard.status == TouchScreenKeyboard.Status.Canceled) { // Cancleを押した時 isCancel = true; } else if (inputField.isFocused && !isCancel){ // 他のところをタップした時 inputText = inputField.text; Debug.Log ("inputText: " + inputText); } } // OnEndEditで呼び出す関数 public void FinishEditText(){ if (inputField.touchScreenKeyboard.status == TouchScreenKeyboard.Status.Done) { // 入力完了時何かに渡す // フィールドの初期化 InitInputField (); Debug.Log ("Done"); } else if (inputField.touchScreenKeyboard.status == TouchScreenKeyboard.Status.Canceled || inputField.touchScreenKeyboard.status == TouchScreenKeyboard.Status.LostFocus) { // 入力キャンセル時もしくは inputField.text = inputText; ResetKeybord (); Debug.Log ("Canseled"); } else { // 他の部分をタップした場合 inputField.text = inputText; ResetKeybord (); Debug.Log ("Canseled"); } } }