- 投稿日: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-08T23:00:46+09:00
アクセス許可が降りていない時、設定画面に飛ぶ処理
let url = URL(string:UIApplication.openSettingsURLString)! //UIApplication.shared.openURL(url as! URL) //非推奨のため、下記のものを使う UIApplication.shared.open(url, options: [:], completionHandler: nil) //openURlが非推奨のためこちらを使用
- 投稿日: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: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-08T19:32:59+09:00
Swiftで画像を丸くする方法(かんたん!)
- 投稿日:2020-08-08T18:46:50+09:00
Swift ?? 演算子について
Swift ??演算子について
??とは、値が存在しない場合のデフォルト値を指定する演算子
こんな感じに使うらしい
let a: Int = 5 var b: Int? let c: Int = b ?? a print("\(c)") //5具体的にどういうことかというと、
let a: Int = 5 var b: Int? let d: Int = b != nil ? b! : a言語化して伝えると
①定数a = 5と定義
②nilかIntが入っているbを定義
③dは、bがnilじゃなかったら、bをセット。ただし、bにnilが入っていたらエラーを出す。
bがnilだったら、aをセットもう一度見てみましょう、
let c: Int = b ?? abがnilじゃなかったらbをセット
nilだったら、aをセット皆さんの理解が深まれば、幸いです。?
- 投稿日:2020-08-08T16:06:05+09:00
UICollectionViewで画像を横スクロールする方法
Swift初学者の筆者がCollectionViewの使い方の備忘録として執筆しました。
超参考になるCollectionViewの使い方を参考に本記事を執筆しましたので、参考サイトにLGTMお願いします。完成品
CollectionViewを使い、画面を横スクロールすることで画像(表示内容)を動かすアプリケーションです。storybordの設定
Main.storybordで行う設定
CollectionViewの設定
- AutoLayoutをする
- ViewController.swiftとIBoutlet接続する
- ViewController.swiftと紐付けて「dataSource」と「delegate」を編集できるよにする。
(CollectionViewにマウスを合わせて、"control"を押しながらViewControllerの黄色い丸印までドラッグ・アンド・ドロップ)- 属性インスペクタの[Scroll Direction]を[Horizontal]に設定
([Scroll Direction]でスワイプの方向を指定することができる)
CollectionViewCellの設定
- xibと紐づいているファイルをclassに指定する。
- [identifler]を「cell」にします。
xibで行う設定
ImageViewを追加してください。
ImageViewの設定
- AutoLayoutをする
- CollectionViewCell.swiftとIBoutlet接続する
コード
ViewController.swiftimport UIKit class ViewController: UIViewController { @IBOutlet var horizontalCollectionView: UICollectionView! var viewWidth: CGFloat! var viewHeight: CGFloat! var cellWitdh: CGFloat! var cellHeight: CGFloat! var cellOffset: CGFloat! var navHeight: CGFloat! var photoArray = ["baseball","soccer"] override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. viewWidth = view.frame.width viewHeight = view.frame.height //ナビゲーションバーの高さ navHeight = self.navigationController?.navigationBar.frame.size.height //xib読み込み let nib = UINib(nibName: "CollectionViewCell", bundle: .main) horizontalCollectionView.register(nib, forCellWithReuseIdentifier: "cell") //空の背景画像を設定 self.navigationController!.navigationBar.setBackgroundImage(UIImage(), for: .default) //ナビゲーションバーの影画像を空に設定 self.navigationController!.navigationBar.shadowImage = UIImage() //アイテムの色を指定 self.navigationController?.navigationBar.tintColor = .white //タイトル色の指定 self.navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white] } } //ViewControllerのプロトコル定義 extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { //collectionViewの要素の数を返す func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return photoArray.count } //collectionViewのセルを返す(セルの内容を決める) func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell cell.backgroundImageView.image = UIImage(named: photoArray[indexPath.row]) return cell } //セル間の間隔を指定 private func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimunLineSpacingForSectionAt section: Int) -> CGFloat { return 24 } //セルのサイズ(CGSize) func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { cellWitdh = viewWidth-75 cellHeight = viewHeight-300 cellOffset = viewWidth-cellWitdh return CGSize(width: cellWitdh, height: cellHeight) } //余白の調整(UIImageを拡大、縮小している) func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { //top:ナビゲーションバーの高さ分上に移動 return UIEdgeInsets(top: -navHeight,left: cellOffset/2,bottom: 0,right: cellOffset/2) } }CollectionViewCell.swiftimport UIKit class CollectionViewCell: UICollectionViewCell { @IBOutlet var backgroundImageView: UIImageView! override func awakeFromNib() { super.awakeFromNib() // Initialization code } }(プログラムは、https://qiita.com/misakiagata/items/acf869a5c7fc4cebaaad から引用しました。)
参考サイト
- 投稿日: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-08T15:06:52+09:00
[Swift] UIBezierPathで簡単なアイコンサンプルを作成する
はじめに
アプリ開発をしていて簡易的にアイコンを設定したいと思ったときに、
単純なものならUIBezierPathを使用してサックっと作成してしまうのも良いかもと思いサンプルを作成しました。対象バージョン
- Xcode ver.11.6
- iOS ver 13.5.1
完成サンプル
UIBezierPathView.swiftimport UIKit class UIBezierPathView: UIView { private var type: Int = 0 init(frame: CGRect, type: Int) { super.init(frame: frame) self.type = type } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { backgroundColor = UIColor(red: 88/255, green: 149/255, blue: 223/255, alpha: 1) super.layoutSubviews() layer.cornerRadius = frame.width / 2 layer.masksToBounds = true } override func draw(_ rect: CGRect) { switch type { case 1: // プラス let verticalPath = UIBezierPath(); verticalPath.move(to: CGPoint(x: rect.midX, y: 15)); verticalPath.addLine(to: CGPoint(x: rect.midX, y: rect.maxY - 15)); verticalPath.close() UIColor.white.setStroke() verticalPath.lineWidth = 4 verticalPath.stroke(); let horizontalPath = UIBezierPath(); horizontalPath.move(to: CGPoint(x: 15, y: rect.midY)); horizontalPath.addLine(to: CGPoint(x: rect.maxX - 15, y: rect.midY)); horizontalPath.close() UIColor.white.setStroke() horizontalPath.lineWidth = 4 horizontalPath.stroke(); case 2: // バツ let rightPath = UIBezierPath(); rightPath.move(to: CGPoint(x: 18, y: 18)); rightPath.addLine(to: CGPoint(x: rect.maxX - 18, y: rect.maxY - 18)); rightPath.close() UIColor.white.setStroke() rightPath.lineWidth = 4 rightPath.stroke(); let leftPath = UIBezierPath(); leftPath.move(to: CGPoint(x: rect.maxX - 18, y: 18)); leftPath.addLine(to: CGPoint(x: 18, y: rect.maxY - 18)); leftPath.close() UIColor.white.setStroke() leftPath.lineWidth = 4 leftPath.stroke(); case 3: // 四角 let squarePath = UIBezierPath(rect: CGRect(x: 15, y: 15, width: 18, height: 18)) UIColor.white.setStroke() squarePath.lineWidth = 4.0 squarePath.stroke() case 4: // 三角 let trianglePath = UIBezierPath(); trianglePath.move(to: CGPoint(x: rect.maxX - 15, y: rect.midY)); trianglePath.addLine(to: CGPoint(x: 20, y: 15)); trianglePath.addLine(to: CGPoint(x: 20, y: rect.maxY - 15)); trianglePath.close() UIColor.white.setStroke() trianglePath.lineWidth = 4.0 trianglePath.stroke(); case 5: // 三本線 let lineOnePath = UIBezierPath(); lineOnePath.move(to: CGPoint(x: 15, y: 18)); lineOnePath.addLine(to: CGPoint(x: rect.maxX - 15, y: 18)); lineOnePath.close() UIColor.white.setStroke() lineOnePath.lineWidth = 4 lineOnePath.stroke(); let lineTwoPath = UIBezierPath(); lineTwoPath.move(to: CGPoint(x: 15, y: 25)); lineTwoPath.addLine(to: CGPoint(x: rect.maxX - 15, y: 25)); lineTwoPath.close() UIColor.white.setStroke() lineTwoPath.lineWidth = 4 lineTwoPath.stroke(); let lineThreePath = UIBezierPath(); lineThreePath.move(to: CGPoint(x: 15, y: rect.maxY - 18)); lineThreePath.addLine(to: CGPoint(x: rect.maxX - 15, y: rect.maxY - 18)); lineThreePath.close() UIColor.white.setStroke() lineThreePath.lineWidth = 4 lineThreePath.stroke(); default: // 円 let circlePath = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY), radius: 10, startAngle: 0, endAngle: CGFloat(Double.pi) * 2, clockwise: true) UIColor.white.setStroke() circlePath.lineWidth = 4.0 circlePath.stroke() } } }UIBezierPathViewController.swiftimport UIKit class UIBezierPathViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .white let circleView = UIBezierPathView(frame: CGRect(x: 0, y: 50, width: 50, height: 50), type: 0) circleView.center.x = view.center.x view.addSubview(circleView) let plusView = UIBezierPathView(frame: CGRect(x: 0, y: 150, width: 50, height: 50), type: 1) plusView.center.x = view.center.x view.addSubview(plusView) let closeView = UIBezierPathView(frame: CGRect(x: 0, y: 250, width: 50, height: 50), type: 2) closeView.center.x = view.center.x view.addSubview(closeView) let squareView = UIBezierPathView(frame: CGRect(x: 0, y: 350, width: 50, height: 50), type: 3) squareView.center.x = view.center.x view.addSubview(squareView) let triangleView = UIBezierPathView(frame: CGRect(x: 0, y: 450, width: 50, height: 50), type: 4) triangleView.center.x = view.center.x view.addSubview(triangleView) let threeLineView = UIBezierPathView(frame: CGRect(x: 0, y: 550, width: 50, height: 50), type: 5) threeLineView.center.x = view.center.x view.addSubview(threeLineView) } }まとめ
シンプルなものなら簡単に描画できるので、忘れたときに参考にしてもらえればと思います。
- 投稿日: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-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-08T09:53:05+09:00
アプリ道場サロンの課題チャレンジPart1
iOSアプリ開発をテーマとしたオンラインサロンに入っています。
https://community.camp-fire.jp/projects/view/281055その中で課題があって、やってみたい人は自由に課題にチャレンジする事ができます。
やった課題は、サロン主からフィードバックがもらえます。課題
最初の課題は数字を足し算するアプリです。
https://www.youtube.com/watch?v=ICsj-14nhBc&list=PLQ5rERkGSxF-fsdBNQu70r5r0OPwSVygO&index=2&t=0sこの課題で調べものをしたのは下記です。
・キーボードの閉じ方
https://qiita.com/jumpyoshim/items/4b8b5f2297910d7f3d1b・ストーリーボードで並んでいるViewを配列として扱うやり方(StackView)
https://qiita.com/yucovin/items/ff58fcbd60ca81de77cb結果