20200405のiOSに関する記事は12件です。

【iOS】Realm Databaseは超簡単で高性能なモバイルデータベース

はじめに

Realm Databaseの簡単な解説とサンプルの記事です。
Realm自体、もう登場してから何年か経っているので
それなりに記事も出てきています!
なので特に目新しい事は無いかもしれませんが、
生暖かい目で見てくださいw

Realm Databaseとは

特徴はざっくり言うと次の通り

  • モバイル向けに作られたNoSQLのデータベース  (データの抽出はNSPredicateを利用する)
  • SQLite、CoreDataより高速!
  • 従来のテーブル定義がクラス定義になっている!
  • 1体多のリレーションが持てる
  • バックアップ、リストアが可能

この中でも「従来のテーブル定義がクラス定義になっている」という所が
私の中では一番のメリットだと思います!
このおかげてSwiftでアプリを作ってる方であれば
比較的抵抗無くデータベースを構築できると思います!

言葉だけではイメージが湧かないと思いますので、
さっそくサンプルを見ていきましょう!
ちなみに今回はRealmの導入方法は記載していません。
導入方法を探している方は他の記事をあたってください。すみませんm(_ _)m
CocoaPodsなりCarthageなり好きな方法でプロジェクトに追加してください!
私はCarthageで入れました!

サンプル

クラス定義(テーブル定義)

普通のデータベースでいうところのテーブル定義ですね。
人に見立てたPersonというクラスを定義します!

Person.swift
import RealmSwift
import Foundation

class Person: Object {
    /// 名前
    @objc dynamic var name: String = ""
    /// 年齢
    @objc dynamic var age: Int = 0
    /// ID
    @objc dynamic var id: String = NSUUID().uuidString
    /// 作成日時
    @objc dynamic var createDate: Date = Date()

    /// IDをプライマリキーに設定
    override static func primaryKey() -> String? {
        return "id"
    }
}

これだけでテーブル定義ができます。

プライマリキーの設定

プライマリキーの設定はprimaryKeyをoverrideして
プライマリキーに設定するプロパティ名を返すだけです。
ちなみにIDにはUUIDを設定するようにしています。

作成日時の設定

作成日時は現在日時を初期値で設定しています。

データの追加(レコードの追加)

データの追加もインスタンスを生成して
Realmのインスタンス(標準Realmと言ったらいいのか?)に追加するだけです。

hoge.swift
    /// Personを追加します
    private func addPerson() {
        do {
            let realm = try Realm()
            let newPerson = Person()
            try realm.write {
                realm.add(newPerson)
            }
        } catch {
            // 例外
        }
    }

プロパティも一緒に設定する場合は次の通りvalueに設定します。

hoge.swift
    /// 名前、年齢を設定したPersonを追加します
    private func addPerson(name: String, age: Int) {
        do {
            let realm = try Realm()
            let newPerson = Person(value: ["name": name, "age": age])
            try realm.write {
                realm.add(newPerson)
            }
        } catch {
            // 例外
        }
    }

注意点

データの操作(追加、更新、削除)をする場合は必ずrealm.writeのトランザクション内で行ってください。
じゃないとアプリが落ちます???

データの更新(レコードの更新)

データの更新はRealmからデータを取得して更新するだけです!
サンプルは名前を変更しています。

hoge.swift
    /// Personの名前を更新します
    private func updatePerson(id: String, name: String) {
        do {
            let realm = try Realm()
            let person = realm.objects(Person.self).filter(NSPredicate.init(format: "id == %@", argumentArray: [id])).first
            try realm.write {
                person?.name = name
            }
        } catch {
            // 例外
        }
    }

NSPredicateを利用してidを元に絞り込んだデータを
writeトランザクション内で名前を変更するだけです。
ほとんど普通のクラスのプロパティ操作と変わらない!素晴らしい!めちゃ簡単!

データの削除(レコードの削除)

更新のときと同じでRealmからデータを取得し、削除するだけです!

hoge.swift
    /// Personを削除します
    private func deletePerson(id: String) {
        do {
            let realm = try! Realm()
            let persons = realm.objects(Person.self).filter(NSPredicate.init(format: "id == %@", argumentArray: [id]))
            try realm.write {
                realm.delete(persons)
            }
        } catch {
            // 例外
        }
    }

最後に

いかがだったでしょうか!
今までiOSのデータベースはなんだか敷居が高いって思っていた方も
これを見れば意外と簡単やん!ってなったと思います!
どんどん使っていきましょう!
この記事読んでる方は大丈夫だと思いますが、
UserDefaultsでなんでもかんでもデータ持つのは絶対駄目ですよwww

まだまだ色々書きたい事があるので
今度は今回の内容と下記事項を含め、まとめ記事を書きたいと思います!
・導入方法
・1対1のリレーションシップ
・1対多のリレーションシップ
・バックアップ、リストアの方法
・マイグレーション(プロパティ(カラム)の追加)

間違いなどありましたがご指摘頂ければ幸いです。
最後までご覧いただき、ありがとうございました!

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

【iOS】Realm Database簡単解説&サンプル!超簡単で高性能なモバイルデータベース!みんな使お??

はじめに

Realm Databaseの簡単な解説とサンプルの記事です。
Realm自体、もう登場してから何年か経っているので
それなりに記事も出てきています!
なので特に目新しい事は無いかもしれませんが、
生暖かい目で見てくださいw

Realm Databaseとは

特徴はざっくり言うと次の通り

  • モバイル向けに作られたNoSQLのデータベース  (データの抽出はNSPredicateを利用する)
  • SQLite、CoreDataより高速!
  • 従来のテーブル定義がクラス定義になっている!
  • 1体多のリレーションが持てる
  • バックアップ、リストアが可能

この中でも「従来のテーブル定義がクラス定義になっている」という所が
私の中では一番のメリットだと思います!
このおかげてSwiftでアプリを作ってる方であれば
比較的抵抗無くデータベースを構築できると思います!

言葉だけではイメージが湧かないと思いますので、
さっそくサンプルを見ていきましょう!
ちなみに今回はRealmの導入方法は記載していません。
導入方法を探している方は他の記事をあたってください。すみませんm(_ _)m
CocoaPodsなりCarthageなり好きな方法でプロジェクトに追加してください!
私はCarthageで入れました!

サンプル

クラス定義(テーブル定義)

普通のデータベースでいうところのテーブル定義ですね。
人に見立てたPersonというクラスを定義します!

Person.swift
import RealmSwift
import Foundation

class Person: Object {
    /// 名前
    @objc dynamic var name: String = ""
    /// 年齢
    @objc dynamic var age: Int = 0
    /// ID
    @objc dynamic var id: String = NSUUID().uuidString
    /// 作成日時
    @objc dynamic var createDate: Date = Date()

    /// IDをプライマリキーに設定
    override static func primaryKey() -> String? {
        return "id"
    }
}

これだけでテーブル定義ができます。

プライマリキーの設定

プライマリキーの設定はprimaryKeyをoverrideして
プライマリキーに設定するプロパティ名を返すだけです。
ちなみにIDにはUUIDを設定するようにしています。

作成日時の設定

作成日時は現在日時を初期値で設定しています。

データの追加(レコードの追加)

データの追加もインスタンスを生成して
Realmのインスタンス(標準Realmと言ったらいいのか?)に追加するだけです。

hoge.swift
    /// Personを追加します
    private func addPerson() {
        do {
            let realm = try Realm()
            let newPerson = Person()
            try realm.write {
                realm.add(newPerson)
            }
        } catch {
            // 例外
        }
    }

プロパティも一緒に設定する場合は次の通りvalueに設定します。

hoge.swift
    /// 名前、年齢を設定したPersonを追加します
    private func addPerson(name: String, age: Int) {
        do {
            let realm = try Realm()
            let newPerson = Person(value: ["name": name, "age": age])
            try realm.write {
                realm.add(newPerson)
            }
        } catch {
            // 例外
        }
    }

注意点

データの操作(追加、更新、削除)をする場合は必ずrealm.writeのトランザクション内で行ってください。
じゃないとアプリが落ちます???

データの更新(レコードの更新)

データの更新はRealmからデータを取得して更新するだけです!
サンプルは名前を変更しています。

hoge.swift
    /// Personの名前を更新します
    private func updatePerson(id: String, name: String) {
        do {
            let realm = try Realm()
            let person = realm.objects(Person.self).filter(NSPredicate.init(format: "id == %@", argumentArray: [id])).first
            try realm.write {
                person?.name = name
            }
        } catch {
            // 例外
        }
    }

NSPredicateを利用してidを元に絞り込んだデータを
writeトランザクション内で名前を変更するだけです。
ほとんど普通のクラスのプロパティ操作と変わらない!素晴らしい!めちゃ簡単!

データの削除(レコードの削除)

更新のときと同じでRealmからデータを取得し、削除するだけです!

hoge.swift
    /// Personを削除します
    private func deletePerson(id: String) {
        do {
            let realm = try! Realm()
            let persons = realm.objects(Person.self).filter(NSPredicate.init(format: "id == %@", argumentArray: [id]))
            try realm.write {
                realm.delete(persons)
            }
        } catch {
            // 例外
        }
    }

最後に

いかがだったでしょうか!
今までiOSのデータベースはなんだか敷居が高いって思っていた方も
これを見れば意外と簡単やん!ってなったと思います!
どんどん使っていきましょう!
この記事読んでる方は大丈夫だと思いますが、
UserDefaultsでなんでもかんでもデータ持つのは絶対駄目ですよwww

まだまだ色々書きたい事があるので
今度は今回の内容と下記事項を含め、まとめ記事を書きたいと思います!
・導入方法
・1対1のリレーションシップ
・1対多のリレーションシップ
・バックアップ、リストアの方法
・マイグレーション(プロパティ(カラム)の追加)

間違いなどありましたがご指摘頂ければ幸いです。
最後までご覧いただき、ありがとうございました!

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

flutter run で起きたエラー(No supported devices connected.)

はじめに

作成したアプリを実行するためにターミナルにて以下を実行したところ、
エラーが発生したので原因を解説します。

flutter run

エラー内容

エラー内容としてはこちらになります。

A new version of Flutter is available!
To update to the latest version, run "flutter upgrade".
No supported devices connected.

実際のターミナル画面では以下のように表示されています。
スクリーンショット 2020-04-05 19.51.21.png

エラーの原因

1.結論

結論から申し上げますと、
Flutterの最新バージョンにアップデートが必要ということです。

2.解説

では以下解説していきます。
まずエラー文に何が書いてあるのか翻訳してみましょう。

A new version of Flutter is available!
Flutterの新しいバージョンが利用可能です!

To update to the latest version, run "flutter upgrade".
最新バージョンに更新するには、「flutter upgrade」を実行してください。

No supported devices connected.
サポートされているデバイスが接続されていません。

となります。
翻訳そのままですね。
エラー文2行目のflutter upgradeをターミナルにて実行しましょう。

flutter upgrade

するとアップグレードが始まり、最後に以下が表示されます。
スクリーンショット 2020-04-05 20.10.34.png

確認ができたら、もう一度flutter runを実行すると無事にアプリが起動します。

flutter run

最後に

解説は以上となります。
最後までご覧くださりありがとうございました。

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

Flutter環境構築 - 2(iOSシミュレーター実行編)

はじめに

前回の「Flutter SDKインストール編」に引き続き、
今回は「iOSシミュレーター実行編」です!
Flutter SDKのインストール・Flutterコマンドのセットアップがお済みでない方は、
先に「Flutter SDKインストール編」をご覧ください。

「iOSシミュレーター実行編」でやること

本編では以下の項目を行います。

  • Xcodeのインストール
  • iOSシミュレーターのセットアップ
  • Flutterアプリを作成し実行する

では次項目より実際に作業していきます。

Xcodeをインストールする

1.App StoreよりXcodeをインストール

App StoreにてXcodeを検索し、インストールします。

すでにインストールされている方はアップデートがないかを確認し、
最新バージョンでなければアップデートする方が吉だと思います。
※ Flutter公式サイトより引用 ※
[ 最新の安定バージョンより古いバージョンでも動作する可能性がありますが、Flutter開発にはお勧めできません。古いバージョンのXcodeを使用してビットコードをターゲットにすることはサポートされておらず、機能しない可能性があります。 ]
スクリーンショット 2020-03-30 8.43.19.png

2.最新Ver.のXcodeを使用するようにXcodeコマンドラインツールを構成

ターミナルにて以下を実行します。
パスワードを求められるので入力します。

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch

3.Xcodeライセンスの確認

以下を実行し内容を確認します。
spaceキーで下にスクロールすることができます。
qキーでページを抜けることができます。

sudo xcodebuild -license

ページを抜けた後、以下を入力することでライセンスに同意します。

agree

iOSシミュレーターのセットアップ

1.iOSシミュレーターを実行

ターミナルにて以下を実行します。

open -a Simulator

するとiOSシミュレーターが立ち上がります。
スクリーンショット 2020-04-05 16.51.22.png

Flutterアプリを作成し実行する

シンプルなFlutterアプリを作成し、実行していきます。

1.ディレクトリ作成

今回のサンプルアプリ用にディレクトリを作成します。
各々好きな名前で構いません。

mkdir 好きな名前

2.作成したディレクトリへ移動

cd 先ほど作成したディレクトリ名

3.Flutterアプリ作成

以下コマンドを入力すると、
先ほど作成したディレクトリの中に
my_appというサンプルのFlutterアプリが作成されます。

flutter create my_app

4.作成したmy_appへ移動

cd my_app

5.iOSシミュレーターでmy_appを実行

iOSシミュレーターを起動した状態で以下を実行します。

flutter run

するとiOSシミュレーター内でmy_appが実行されます。
右下の+ボタンを押すと数字がカウントアップされます。
ターミナルでqキーを入力するとアプリを終了できます。
スクリーンショット 2020-04-05 17.23.05.png
これでシンプルなFlutterアプリの作成・実行ができました!

最後に

以上で「iOSシミュレーター実行編」は終了となります。お疲れさまでした。
実機にてアプリの確認をしたい場合はFlutter公式サイト
「Deploy to iOS devices」欄を参考にしてみてください。
私はiPhoneユーザーではないので。(iPhone欲しい。。。)

最後までご覧くださりありがとうございました。

参考サイト[Flutter公式サイト]

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

Flutter環境構築 - 2(Xcode・iOSシミュレーターセットアップ編)

はじめに

前回の「Flutter SDKインストール編」に引き続き、
今回は「Xcode・iOSシミュレーターセットアップ編」です!
Flutter SDKのインストール・Flutterコマンドのセットアップがお済みでない方は、
先に「Flutter SDKインストール編」をご覧ください。

「iOSシミュレーター実行編」でやること

本編では以下の項目を行います。

  • Xcodeのインストール
  • iOSシミュレーターのセットアップ
  • Flutterアプリを作成し実行する

では次項目より実際に作業していきます。

Xcodeをインストールする

1.App StoreよりXcodeをインストール

App StoreにてXcodeを検索し、インストールします。

すでにインストールされている方はアップデートがないかを確認し、
最新バージョンでなければアップデートする方が吉だと思います。
※ Flutter公式サイトより引用 ※
[ 最新の安定バージョンより古いバージョンでも動作する可能性がありますが、Flutter開発にはお勧めできません。古いバージョンのXcodeを使用してビットコードをターゲットにすることはサポートされておらず、機能しない可能性があります。 ]
スクリーンショット 2020-03-30 8.43.19.png

2.最新Ver.のXcodeを使用するようにXcodeコマンドラインツールを構成

ターミナルにて以下を実行します。
パスワードを求められるので入力します。

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch

3.Xcodeライセンスの確認

以下を実行し内容を確認します。
spaceキーで下にスクロールすることができます。
qキーでページを抜けることができます。

sudo xcodebuild -license

ページを抜けた後、以下を入力することでライセンスに同意します。

agree

iOSシミュレーターのセットアップ

1.iOSシミュレーターを実行

ターミナルにて以下を実行します。

open -a Simulator

するとiOSシミュレーターが立ち上がります。
スクリーンショット 2020-04-05 16.51.22.png

Flutterアプリを作成し実行する

シンプルなFlutterアプリを作成し、実行していきます。

1.ディレクトリ作成

今回のサンプルアプリ用にディレクトリを作成します。
各々好きな名前で構いません。

mkdir 好きな名前

2.作成したディレクトリへ移動

cd 先ほど作成したディレクトリ名

3.Flutterアプリ作成

以下コマンドを入力すると、
先ほど作成したディレクトリの中に
my_appというサンプルのFlutterアプリが作成されます。

flutter create my_app

4.作成したmy_appへ移動

cd my_app

5.iOSシミュレーターでmy_appを実行

iOSシミュレーターを起動した状態で以下を実行します。

flutter run

するとiOSシミュレーター内でmy_appが実行されます。
右下の+ボタンを押すと数字がカウントアップされます。
ターミナルでqキーを入力するとアプリを終了できます。
スクリーンショット 2020-04-05 17.23.05.png
これでシンプルなFlutterアプリの作成・実行ができました!

最後に

以上で「Xcode・iOSシミュレーターセットアップ編」は終了となります。お疲れさまでした。
実機にてアプリの確認をしたい場合はFlutter公式サイト
「Deploy to iOS devices」欄を参考にしてみてください。
私はiPhoneユーザーではないので。(iPhone欲しい。。。)

最後までご覧くださりありがとうございました。

参考サイト[Flutter公式サイト]

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

SwiftUI で開発したアプリをリリースしてみた

先日 SwiftUI ベースの iOS/iPadOS アプリをリリースしたので、そこで気づいたことを共有します。

作ったのは icotile というアプリで、Twitter のリストや友達を管理するものです。Vue.js ベースの Web アプリケーションとしてすでにリリースしていて、API も揃っているので、移植してみました。

? 無料なので、動きなどは実際に試してもらえればと思います。
‎icotile (Twitter List Manager) on the App Store

icotile-iphone-ipadのコピー.png

(関連)8年前に作った HTML5 アプリを最近の技術で作り直した話(5年ぶり2回目) - Qiita

SwiftUI って業務で使えるの?

いきなりですが結論からすると、業務で SwiftUI を使うのは時期尚早だと思います。

  • 実装されていない機能や UI があり、必要ならば UIKit で補う必要がある。
  • まだ細かいバグもあり、フレームワークとして完成度が高くない。
  • ドキュメントも不十分で、試行錯誤に時間が取られる。

もちろん、簡単なカタログアプリ的なものであれば、トライアルで作ってみるのもありかもしれないですが、まだベータバージョンと言っても過言ではないです。SwiftUI のクセを理解して、うまくプロダクトに落とし込むのに試行錯誤が必要です。

また、次のメジャーバージョンで大幅に改善が入ると、これまでのノウハウが使えなく可能性もあります。まあ、それはよくあることですが、業務で行うとなると、そこもリスクとなるでしょう。

ちなみに、業務レベルで採用されたメジャーな例は、探した感じこのアプリぐらいでした。他にもあるのかな?

ICカードリーダーのiOSアプリをSwiftUIで開発しました | Money Forward Engineers' Blog

SwiftUI って初心者向け?

では、どういうケースで利用できるかというと、最初の iOS アプリ開発の入り口として SwiftUI はアリだと思います。UIKit ではじめると、最初、delegate の概念がわからないとか、Storyboard でどう Auto Layout を指定するのかなど、いろいろつまづくポイントはあると思いますが、その点、SwiftUI は素直な記述で実現が可能です。プレビューもできるし、楽しく学べる気がします。

ある程度スキルがついてきて、より複雑な機能を実装したくなったら、そこから UIKit に移行しても良いと思います。SwiftUI から UIKit を呼び出すことも可能なので、シームレスに移行することも(頑張れば)可能です。

どこに UIKit を使ったか?

ビューのほとんどは SwiftUI で記述しましたが、一部、UIKit も使っています。

UIKit を呼び出すのはさほど難しくなく、ラッパーを書くぐらいの感じです。SwiftUI Tutorial でもわかりやすく説明があります。

Interfacing with UIKit — SwiftUI Tutorials | Apple Developer Documentation

今回は、下記の UI を UIViewRepresentable でラップして利用しています。

  • WKWebView: Twitter のログインを WebView で行うため。
  • UIActivityIndicatorView: プログレス表示。これは標準で欲しかった...
  • UITextView: SwiftUI の TextField では複数行の編集ができなかったため。
  • AdMob: 3rd party の UIView も使えます。

サンプルとして、ログインで使ったWkWebView を呼び出すコードは下記のような感じです。

クリックして表示
import SwiftUI
import WebKit

struct WebView : UIViewRepresentable {
  var url: URL
  var frame: CGRect?
  var configuration: WKWebViewConfiguration?
  var navigationDelegate: WKNavigationDelegate? = nil

  func makeUIView(context: Context) -> WKWebView  {
    var webView: WKWebView? = nil
    if let frame = frame {
      if let configuration = configuration {
        webView = WKWebView(frame: frame, configuration: configuration)
      } else {
        webView = WKWebView(frame: frame)
      }
    } else {
      webView = WKWebView()
    }
    if navigationDelegate != nil {
      webView!.navigationDelegate = navigationDelegate!
    }
    webView?.load(URLRequest(url: url))
    return webView!
  }

  func updateUIView(_ uiView: WKWebView, context: Context) {
  }
}

// 呼び出す時(シートを利用)
// 引数に渡している `navigationDelegate` は `WKNavigationDelegate` の実装で、
// この中で認証情報を処理している。
SomeView()
  .sheet(isPresented: $viewModel.isLoginWebPresented) {
    LoginWebView(isPresented: self.$viewModel.isLoginWebPresented)
  }

実は Combine が肝?

SwiftUI はあくまでビューのフレームワークです。データの処理はこれまでと同じ手法が使えますが、同時に発表された Combine と組み合わせることが想定された作りになっています。

Combine は Apple 純正のフレームワークで、非同期処理やイベント処理を宣言的に記述できます。Rx を使ったことがある人にはとっつきやすいと思いますが、これまで避けてきたので理解するのに苦労しました。SwiftUI で UI は簡単に作れたけど、API を呼んでデータを UI に反映させるところにかなり時間がかかりました。

ただ、SwiftUI と Combine は親和性が高いので、おそらく UIKit + RxSwift よりもシンプルで、理解がしやすかったんじゃないかなと思います。今後は UIKit でも Combine を使ってみたいと思いました。

当初は仕様がコロコロ変わって学ぶのと同時に追いつくのが大変でしたが、最近はだいぶ落ち着いたんじゃないかな。これらのページがとても参考になりました。

3rd Party ライブラリ

SwiftUI であっても、普通に Swift 系のライブラリは使用可能です。もちろん、CocoaPods や Carthage、Swift Package Manager も利用可能です。

SwiftUI 向けのライブラリはあまりないと思っていましたが、たまたま見つけた QGrid というライブラリをアイコンを並べるビューに使っています。

他にも、いろんなライブラリがあるみたいです(この記事を書いていて、下記の記事を見つけました)。

SwiftUIおすすめライブラリ!! - Qiita

工夫したことや困ったこと

iPhone と iPad への対応

SwiftUI では、Storyboard は使わず、コードでレイアウトを構築します。はじめから様々なデバイスに対応しやすいように設計されていて、iPad 対応も Storyboard より簡単に感じました。

例えば、Master-Detail レイアウトは、下記のように記述すると、iPhone では Master ビューだけが表示され、iPad の横画面では、Master ビューと Detail ビューを同時に表示してくれます。

struct ContentView: View {
    var body: some View {
        NavigationView {
            MasterView()
            DetailView()
        }.navigationViewStyle(DoubleColumnNavigationViewStyle())
    }
}

なお、XCode の New Project から、Master-Detail App を選択して開発を始めた方が、トラブルは少ないと思います。

isPresented は使わない

Sheet や ActionSheet などは、View にオーバーレイする形で表示します。ここで定義されていますが、メソッドが2つ提供されています。

View Presentation | Apple Developer Documentation

isPresented: はフラグで制御し、item: はその中に表示させたい View を与えるかどうかで制御します。

例えば、同一ビューにて複数の Sheet を表示したい場合には、isPresented だと上手くいかず、他にも不都合なことがありました(忘れちゃったけど)。

なので、使用する場合は item: に統一するようにしました。

  @State private var sheetView: SheetView?
  ...
  var body: some View {
    ...
    AnyView()
      .sheet(item: self.$sheetView) { view in view }

例えば、上記で self.$sheetView に Sheet で表示させたいビューを代入すると、Sheet が表示されます。

Sheet を下スワイプで閉じるのを禁止できない。

iOS13 で追加された Sheet は、.sheet() で呼び出すことができますが、SwiftUI では下記を制御することができません。

  • フルスクリーンで表示できない(UIKit での modalPresentationStyle = .fullScreen ができない)。
  • 下方向にスワイプすることを禁止できない(UIKit での isModalInPresentation = true ができない)。

特に後者は、入力フォームなどで入力内容を明示的に保存させたいようなケースで困ります。今回は、スワイプで閉じたときはキャンセルとみなすようにしましたが、あまりいい体験ではないので、閉じさせないオプションをつけてもらいたいところです。

View エレメントの数に比例してパフォーマンスが悪くなる。

このアプリでは、1つの画面内に多くの画像を並べることがあり、その数が多くなると表示までのタイムラグが大きくなります。かつ、その間、読み込み中の表示をしたりすることが難しいです。

UITableView/UICollectionView のように、コンテンツの数によらずパフォーマンスが一定になるような工夫を入れるのは、ビューをロジックで制御することになり、SwiftUI のメリットが薄れてしまいます。

アイコンボタンの反応が悪い

iOS13 から SF Symbols という公式のアイコンセットが提供されて、このアプリでも利用しています。

このアイコンでボタンを作ったのですが、ただ Image()を使っただけではタップした時の反応が悪かったので、自分で大きさを調整しました。この辺りも、OS 側でよろしくやって欲しいところ。

.navigationBarItems(
  leading: Button(action: {
    ...
  }, label: {
    Image(systemName: "plus")
      .font(.system(size: 22.0))
      .frame(minWidth: 44.0, maxWidth: .infinity,
             minHeight: 44.0, alignment: .center)
  })
)

iPad での ActionSheet

iPhone で ActionSheet を表示するコードを書いて、それを iPad で実行すると、Popover で表示されるのですが、その位置を指定することができず、想定外の場所に表示されました。

下記は、Master ビューの右上のアイコンをタップした時の表示。

iPhone iPad
Simulator Screen Shot - iPhone 11 - 2020-04-05 at 15.41.37.png Simulator Screen Shot - iPad Pro (9.7-inch) - 2020-04-05 at 15.41.32.png

同じコードでここまで対応してくれているのでいいのですが、できれば Popover の場所を指定できるようにして欲しいところ。別のコードを書いて Popover を表示させることもできるのですが、そのとき、Popover の中に ActionSheet を表示することができないので、現在は、このままにしています(一応 Apple の審査は通過しています)。

今後の期待

と、困った点を中心に書いてみましたが、もちろんいい点もあります。

  • UI をコードでシンプルに記述でき、ロジックと明確に分離できる。
  • Combine との組み合わせで、リアクティブなコードを書ける。
  • View のプレビューができる。
  • Dark Mode やアクセシビリティなどへの対応が容易。
  • iPhone/iPad だけでなく、他の Apple プロダクトともコードを共有できる。

まだリリースされて1年も経たないフレームワークなので、いろいろな面で不十分なのは仕方ないと思っていますが、おそらく、7月の WWDC で大きなアップデートがあると思うので、そこでの進化を期待しています。

リンク

ドキュメント

開発した系の記事

その他参考にした記事

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

SwiftUI で開発した iPhone/iPad アプリをリリースしてみた

先日 SwiftUI ベースの iOS/iPadOS アプリをリリースしたので、そこで気づいたことを共有します。

作ったのは icotile というアプリで、Twitter のリストや友達を管理するものです。Vue.js ベースの Web アプリケーションとしてすでにリリースしていて、API も揃っているので、移植してみました。

? 無料なので、動きなどは実際に試してもらえればと思います。
‎icotile (Twitter List Manager) on the App Store

icotile-iphone-ipadのコピー.png

(関連)8年前に作った HTML5 アプリを最近の技術で作り直した話(5年ぶり2回目) - Qiita

SwiftUI って業務で使えるの?

いきなりですが結論からすると、業務で SwiftUI を使うのは時期尚早だと思います。

  • 実装されていない機能や UI があり、必要ならば UIKit で補う必要がある。
  • まだ細かいバグもあり、フレームワークとして完成度が高くない。
  • ドキュメントも不十分で、試行錯誤に時間が取られる。

もちろん、簡単なカタログアプリ的なものであれば、トライアルで作ってみるのもありかもしれないですが、まだベータバージョンと言っても過言ではないです。SwiftUI のクセを理解して、うまくプロダクトに落とし込むのに試行錯誤が必要です。

また、次のメジャーバージョンで大幅に改善が入ると、これまでのノウハウが使えなく可能性もあります。まあ、それはよくあることですが、業務で行うとなると、そこもリスクとなるでしょう。

ちなみに、業務レベルで採用されたメジャーな例は、探した感じこのアプリぐらいでした。他にもあるのかな?

ICカードリーダーのiOSアプリをSwiftUIで開発しました | Money Forward Engineers' Blog

SwiftUI って初心者向け?

では、どういうケースで利用できるかというと、最初の iOS アプリ開発の入り口として SwiftUI はアリだと思います。UIKit ではじめると、最初、delegate の概念がわからないとか、Storyboard でどう Auto Layout を指定するのかなど、いろいろつまづくポイントはあると思いますが、その点、SwiftUI は素直な記述で実現が可能です。プレビューもできるし、楽しく学べる気がします。

ある程度スキルがついてきて、より複雑な機能を実装したくなったら、そこから UIKit に移行しても良いと思います。SwiftUI から UIKit を呼び出すことも可能なので、シームレスに移行することも(頑張れば)可能です。

どこに UIKit を使ったか?

ビューのほとんどは SwiftUI で記述しましたが、一部、UIKit も使っています。

UIKit を呼び出すのはさほど難しくなく、ラッパーを書くぐらいの感じです。SwiftUI Tutorial でもわかりやすく説明があります。

Interfacing with UIKit — SwiftUI Tutorials | Apple Developer Documentation

今回は、下記の UI を UIViewRepresentable でラップして利用しています。

  • WKWebView: Twitter のログインを WebView で行うため。
  • UIActivityIndicatorView: プログレス表示。これは標準で欲しかった...
  • UITextView: SwiftUI の TextField では複数行の編集ができなかったため。
  • AdMob: 3rd party の UIView も使えます。

サンプルとして、ログインで使ったWkWebView を呼び出すコードは下記のような感じです。

クリックして表示
import SwiftUI
import WebKit

struct WebView : UIViewRepresentable {
  var url: URL
  var frame: CGRect?
  var configuration: WKWebViewConfiguration?
  var navigationDelegate: WKNavigationDelegate? = nil

  func makeUIView(context: Context) -> WKWebView  {
    var webView: WKWebView? = nil
    if let frame = frame {
      if let configuration = configuration {
        webView = WKWebView(frame: frame, configuration: configuration)
      } else {
        webView = WKWebView(frame: frame)
      }
    } else {
      webView = WKWebView()
    }
    if navigationDelegate != nil {
      webView!.navigationDelegate = navigationDelegate!
    }
    webView?.load(URLRequest(url: url))
    return webView!
  }

  func updateUIView(_ uiView: WKWebView, context: Context) {
  }
}

// 呼び出す時(シートを利用)
// 引数に渡している `navigationDelegate` は `WKNavigationDelegate` の実装で、
// この中で認証情報を処理している。
SomeView()
  .sheet(isPresented: $viewModel.isLoginWebPresented) {
    LoginWebView(isPresented: self.$viewModel.isLoginWebPresented)
  }

実は Combine が肝?

SwiftUI はあくまでビューのフレームワークです。データの処理はこれまでと同じ手法が使えますが、同時に発表された Combine と組み合わせることが想定された作りになっています。

Combine は Apple 純正のフレームワークで、非同期処理やイベント処理を宣言的に記述できます。Rx を使ったことがある人にはとっつきやすいと思いますが、これまで避けてきたので理解するのに苦労しました。SwiftUI で UI は簡単に作れたけど、API を呼んでデータを UI に反映させるところにかなり時間がかかりました。

ただ、SwiftUI と Combine は親和性が高いので、おそらく UIKit + RxSwift よりもシンプルで、理解がしやすかったんじゃないかなと思います。今後は UIKit でも Combine を使ってみたいと思いました。

当初は仕様がコロコロ変わって学ぶのと同時に追いつくのが大変でしたが、最近はだいぶ落ち着いたんじゃないかな。これらのページがとても参考になりました。

3rd Party ライブラリ

SwiftUI であっても、普通に Swift 系のライブラリは使用可能です。もちろん、CocoaPods や Carthage、Swift Package Manager も利用可能です。

SwiftUI 向けのライブラリはあまりないと思っていましたが、たまたま見つけた QGrid というライブラリをアイコンを並べるビューに使っています。

他にも、いろんなライブラリがあるみたいです(この記事を書いていて、下記の記事を見つけました)。

SwiftUIおすすめライブラリ!! - Qiita

工夫したことや困ったこと

iPhone と iPad への対応

SwiftUI では、Storyboard は使わず、コードでレイアウトを構築します。はじめから様々なデバイスに対応しやすいように設計されていて、iPad 対応も Storyboard より簡単に感じました。

例えば、Master-Detail レイアウトは、下記のように記述すると、iPhone では Master ビューだけが表示され、iPad の横画面では、Master ビューと Detail ビューを同時に表示してくれます。

struct ContentView: View {
    var body: some View {
        NavigationView {
            MasterView()
            DetailView()
        }.navigationViewStyle(DoubleColumnNavigationViewStyle())
    }
}

なお、XCode の New Project から、Master-Detail App を選択して開発を始めた方が、トラブルは少ないと思います。

isPresented は使わない

Sheet や ActionSheet などは、View にオーバーレイする形で表示します。ここで定義されていますが、メソッドが2つ提供されています。

View Presentation | Apple Developer Documentation

isPresented: はフラグで制御し、item: はその中に表示させたい View を与えるかどうかで制御します。

例えば、同一ビューにて複数の Sheet を表示したい場合には、isPresented だと上手くいかず、他にも不都合なことがありました(忘れちゃったけど)。

なので、使用する場合は item: に統一するようにしました。

  @State private var sheetView: SheetView?
  ...
  var body: some View {
    ...
    AnyView()
      .sheet(item: self.$sheetView) { view in view }

例えば、上記で self.$sheetView に Sheet で表示させたいビューを代入すると、Sheet が表示されます。

Sheet を下スワイプで閉じるのを禁止できない。

iOS13 で追加された Sheet は、.sheet() で呼び出すことができますが、SwiftUI では下記を制御することができません。

  • フルスクリーンで表示できない(UIKit での modalPresentationStyle = .fullScreen ができない)。
  • 下方向にスワイプすることを禁止できない(UIKit での isModalInPresentation = true ができない)。

特に後者は、入力フォームなどで入力内容を明示的に保存させたいようなケースで困ります。今回は、スワイプで閉じたときはキャンセルとみなすようにしましたが、あまりいい体験ではないので、閉じさせないオプションをつけてもらいたいところです。

View エレメントの数に比例してパフォーマンスが悪くなる。

このアプリでは、1つの画面内に多くの画像を並べることがあり、その数が多くなると表示までのタイムラグが大きくなります。かつ、その間、読み込み中の表示をしたりすることが難しいです。

UITableView/UICollectionView のように、コンテンツの数によらずパフォーマンスが一定になるような工夫を入れるのは、ビューをロジックで制御することになり、SwiftUI のメリットが薄れてしまいます。

アイコンボタンの反応が悪い

iOS13 から SF Symbols という公式のアイコンセットが提供されて、このアプリでも利用しています。

このアイコンでボタンを作ったのですが、ただ Image()を使っただけではタップした時の反応が悪かったので、自分で大きさを調整しました。この辺りも、OS 側でよろしくやって欲しいところ。

.navigationBarItems(
  leading: Button(action: {
    ...
  }, label: {
    Image(systemName: "plus")
      .font(.system(size: 22.0))
      .frame(minWidth: 44.0, maxWidth: .infinity,
             minHeight: 44.0, alignment: .center)
  })
)

iPad での ActionSheet

iPhone で ActionSheet を表示するコードを書いて、それを iPad で実行すると、Popover で表示されるのですが、その位置を指定することができず、想定外の場所に表示されました。

下記は、Master ビューの右上のアイコンをタップした時の表示。

iPhone iPad
Simulator Screen Shot - iPhone 11 - 2020-04-05 at 15.41.37.png Simulator Screen Shot - iPad Pro (9.7-inch) - 2020-04-05 at 15.41.32.png

同じコードでここまで対応してくれているのでいいのですが、できれば Popover の場所を指定できるようにして欲しいところ。別のコードを書いて Popover を表示させることもできるのですが、そのとき、Popover の中に ActionSheet を表示することができないので、現在は、このままにしています(一応 Apple の審査は通過しています)。

今後の期待

と、困った点を中心に書いてみましたが、もちろんいい点もあります。

  • UI をコードでシンプルに記述でき、ロジックと明確に分離できる。
  • Combine との組み合わせで、リアクティブなコードを書ける。
  • View のプレビューができる。
  • Dark Mode やアクセシビリティなどへの対応が容易。
  • iPhone/iPad だけでなく、他の Apple プロダクトともコードを共有できる。

まだリリースされて1年も経たないフレームワークなので、いろいろな面で不十分なのは仕方ないと思っていますが、おそらく、7月の WWDC で大きなアップデートがあると思うので、そこでの進化を期待しています。

リンク

ドキュメント

開発した系の記事

その他参考にした記事

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

iPhoneのショートカットアプリからAPIを使って複数のSESAMEを同時に解錠する

先日、CANDY HOUSEのSESAME miniを購入しました。我が家のドアには鍵が2つ付いてるので、SESAMEも2つです。解錠するときiPhoneアプリのUnlock All機能を使っていたのですが、

  1. iPhoneのロック解除
  2. アプリの起動
  3. サイドバーの表示
  4. Unlock All

とちょっとステップが多いので、iPhoneのショートカットアプリを使うことにしました。ショートカットをウィジェットに登録しておけばiPhoneのロック解除も不要になるので、

  1. ウィジェットを表示
  2. ショートカット

でいけます。

実はショートカットを使ったSESAMEの解錠はSESAMEの公式ブログでも紹介されている1のですが、残念ながらiPhone 8では利用できませんでした。
そこで、この記事ではショートカットからSesame APIを使って解錠する方法を紹介します。

やり方

SESAMEとWi-Fiアクセスポイントの設定は完了しているものとします。

SESAMEのクラウド連携を有効にする

  1. iPhoneのSESAMEアプリを開く
  2. Status -> Change Settings -> INTEGRATIONのCloudをオンにする(SESAMEの数だけ)

image.png
image.png
image.png

Sesame API Keyを発行する

  1. CANDY HOUSE Dashboardにログインする
  2. API Settings -> SEND CODEをタップしてVerification Codeをメールで送ってもらう
  3. 送られてきた数字6桁をテキストボックスに入力してCONFIRMをタップする
  4. ADDをタップしてAPI Keyを発行する

Note: API Keyは発行時にしか表示されないので、画面を閉じる前にコピーしておきましょう。

image.png
image.png
image.png

ショートカットを作成する

これですね。

image.png

こういうやつを作ります。

image.png

処理をShellっぽく書くとこんな感じです。(実行して試したわけじゃないのであくまでイメージです)

for i in 00000000-0000-0000-0000-000000000001 \
         00000000-0000-0000-0000-000000000002
do
  curl -H "Authorization: YOUR_AUTH_TOKEN" \
       -H "Content-Type: application/json" \
       -X POST -d '{"command":"unlock"}' \
       https://api.candyhouse.co/public/sesame/${i}
done

次の3つのアクションを作ります。

  1. List: SESAMEのデバイスIDのリスト
  2. Repeat with Each: リストの要素に対する繰り返し処理
  3. Get Contents of URL: Sesame APIによる解錠

Sesame APIの仕様についてはこちらを参照してください。

SESAMEのデバイスIDのリスト

  1. Scripting -> Lists -> Listを選ぶ
  2. SESAMEのデバイスIDを入力する(SESAMEの数だけ)

SESAMEのデバイスIDの確認方法はこちらのページ下部を参照してください。

image.png
image.png

リストの要素に対する繰り返し処理

  1. Scripting -> Control Flow -> Repeat with Eachを選ぶ

image.png
image.png

Sesame APIによる解錠

  1. Web -> Web Requests -> Get Contents of URLを選ぶ
  2. Get Contents of URLをドラッグ&ドロップでRepeat with Eachの中に入れる
  3. Repeat Itemの前にhttps://api.candyhouse.co/public/sesame/を追加する(スクリーンショット参照)
  4. Show MoreをタップしてMethod、Headers、Request Bodyを次のとおり変更する
    • Method: POST
    • Headers:
      • Authorization: (発行したAPI Key)
      • Content-Type: application/json
    • Request Body:
      • command: unlock

ここまでできたら右下の:arrow_forward:を押してテストしてみましょう。

image.png
image.png
image.png
image.png

ショートカットの保存

最後に名前を付けて保存します。名前はもちろん例のやつ。せっかくなのでSESAMEっぽいアイコンにしてみます。

image.png

終わり

このとおり、ウィジェットから解錠できるようになりました。それでは皆様、良いセサミライフを。

image.png

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

【Flutter】BLoCパターンとは?

BLoCパターンとは?

BLoCパターンは、Business Logic Componentの略
BLoC PatternはFlutterでのアプリケーション開発時に用いる、状態管理手法の1つです。
ビジネスロジックをコンポーネント単位で管理しやすくするためのパターンです。

BLoCのガイドライン

  1. インプットとアウトプットは、単純なStreamとSinkに限定する。(Inputs and outputs are simple Streams/Sinks only.)
  2. 依存性は、必ず注入可能でプラットフォームに依存しないものとする。(Dependencies must be injectable and platform agnostic.)
  3. プラットフォームごとの条件分岐は、許可しない。(No platform branching allowed.)

メモ的に参考資料をまとめておきます。

参考記事

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

iOS app の習得日記 (1)

こちらの記事を参考に
実習中
(https://qiita.com/gomi_ningen/items/4e0e5bd98f08c4bcf93d)

import UIKit

class ViewController: UIViewController {


    @IBOutlet weak var lable: UILabel!

    @IBAction func buttonOnTouchUpInside(_ sender: Any) {
        let now = Date() // 現在時刻を取得
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "ja_JP") // ロケールを指定
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss" // 時刻表示のフォーマットを指定
        // 指定したフォーマットで現在時刻を取得してラベルに反映させる
        lable.text = dateFormatter.string(from: now)
    }


    }

シミュレーターでトライするも
日付がでない

(https://ediienote.com/Wp_ediieNote/2019/09/21/swiftsignalsgabrt/)

こちらの記事が参考になった。

どうやら 接続を何回もやったようで、
接続先が複数になってた。

一つにもどしたらできた

スクリーンショット 2020-04-05 2.30.04.png

しかし
レイアウトが重なるのはなんでだろう?
xcodeではちゃんと離れているのに、、、

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

iOS app の習得日記

こちらの記事を参考に
実習中
(https://qiita.com/gomi_ningen/items/4e0e5bd98f08c4bcf93d)

import UIKit

class ViewController: UIViewController {


    @IBOutlet weak var lable: UILabel!

    @IBAction func buttonOnTouchUpInside(_ sender: Any) {
        let now = Date() // 現在時刻を取得
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "ja_JP") // ロケールを指定
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss" // 時刻表示のフォーマットを指定
        // 指定したフォーマットで現在時刻を取得してラベルに反映させる
        lable.text = dateFormatter.string(from: now)
    }


    }

シミュレーターでトライするも
日付がでない

(https://ediienote.com/Wp_ediieNote/2019/09/21/swiftsignalsgabrt/)

こちらの記事が参考になった。

どうやら 接続を何回もやったようで、
接続先が複数になってた。

一つにもどしたらできた

スクリーンショット 2020-04-05 2.30.04.png

しかし
レイアウトが重なるのはなんでだろう?
xcodeではちゃんと離れているのに、、、

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

Swift データの保存と呼び出し

iOSのアプリでは、データの保存をしていないと、タスクキルをするたびに、行った操作がリセットされる。そのため、データの保存と呼び出しが必要となる。

データの保存のやり方

sample.swift
//objectsというデータを保存したい場合
UserDefaults.standrad.set(objects, forKey: String)

objectを定義している場所によっては、self.objectsとする。
[forKey: String]の部分は、[forKey: "Store1"]のように自分で名前をつける。
この場合だと、以下のように書く。

sample.swift
//objectsというデータを保存したい場合
UserDefaults.standrad.set(objects, forKey: "Store1")

これは、Store1にobjectsを保存するということである。
新しくobjectsを更新するたびに、同じように保存すれば、Store1も更新される。

同じキーに2回以上保存を行った場合は、古いデータは消え、一番最新のデータのみ保持する。

*UserDefaulsによるデータの保存は保持できる型は決まっている。そのため、保持できない型は型変換などを行って保持する必要がある。

*構造体の保存も可能である。(やり方は別方法)

データの呼び出し

sample.swift
//Store1の中身をString型で保持している場合
let load = UserDefauls.standard.object(forKey: "Store1")
if (load as? String != nil {
self.objects = load as! String
}

データの保存が1つだけというのは基本的に少ないと思うので、配列などを使うと思う。

sample.swift
var objects = [Any]()

配列を用いる場合は、以下のようにするだけである。

sample.swift
//Store1の中身をString型で保持している場合
let load = UserDefauls.standard.object(forKey: "Store1")
if (load as? [String] != nil {
self.objects = load as! [String]
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む