しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

Today Extensionでウィジェット作成

Today Extensionを使ったウィジェット作成を試してみました。

プロジェクトを作成したらメニューからTargetの作成を行います。

f:id:llcc:20200113191348p:plain

Today Extensionを選択します。

f:id:llcc:20200113191455p:plain

作成するとToday Extension関連ファイルが作られます。

f:id:llcc:20200113191618p:plain

Today Extensionを起動するとHello worldが表示されます。

f:id:llcc:20200113191726p:plain

Storyboardでテキストを変更する事で文字を変える事ができます。

f:id:llcc:20200113192736p:plain

ラベルはUIViewControllerと紐付ける事で動的な変更もできます。

class TodayViewController: UIViewController, NCWidgetProviding {
    @IBOutlet weak var myLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        myLabel.text = "Hello!"
    }
}

ラベルだけでなくボタンも配置する事ができます。

f:id:llcc:20200113205708p:plain

widgetLargestAvailableDisplayModeをセットする事でウィジェットのサイズを大きくできます。
サイズはwidgetActiveDisplayModeDidChangeでセットする事が可能です。

class TodayViewController: UIViewController, NCWidgetProviding {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        extensionContext?.widgetLargestAvailableDisplayMode = .expanded
    }
    
    func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
        switch activeDisplayMode {
        case .compact:  preferredContentSize = maxSize
        case .expanded: preferredContentSize = CGSize(width: preferredContentSize.width, height: 400)
        }
    }
}

UserDefaultsなどのデータ共有にはApp Groupsが必要になります。
まずはXcode上でApp Groupsを作成します。
この時はアプリのターゲットだけでなくExtension側にも追加が必要です。

f:id:llcc:20200113193701p:plain

あとは作成したグループを使ってデータ作成すればデータ共有できるようになっています。

UserDefaults(suiteName: "group.mygroup")?.set(100, forKey: "Key")
print(UserDefaults(suiteName: "group.mygroup")?.integer(forKey: "Key"))

Realmなどのデータベースのデータ共有もApp Groupsを使います。

var config = Realm.Configuration.defaultConfiguration
let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.mygroup")
config.fileURL = url?.appendingPathComponent("default.realm")
Realm.Configuration.defaultConfiguration = config

データだけでなくクラスも共有できないので、Embedded Frameworkの利用やCompile Sourcesへの追加などが必要です。

f:id:llcc:20200113203354p:plain

CocoaPodsを使う場合、Extension用の処理も必要があるので注意が必要です。

target 'MyApp' do
  use_frameworks!

  pod 'RealmSwift'
end

target 'MyExtension' do
  use_frameworks!

  pod 'RealmSwift'
end

SwiftUIを触ってみる

SwiftUIで簡単なUIを作ってみました。

まずはXcode11で新規プロジェクトを作成します。

f:id:llcc:20191201174101p:plain

テンプレートは「Single View App」を選びました。

f:id:llcc:20191201174134p:plain

プロジェクト構成は以下のようになっています。

f:id:llcc:20191201175809p:plain

Xcode11からはSceneDelegate.swiftというファイルが追加されています。 これはiPadで画面に複数アプリ表示する際に使うクラスで、今までのAppDelegateの役割の一部を担っています。 今回はSwiftUIを中心に見たいので深堀りしないことにします。

まずはアプリを立ち上げてみます。 アプリを起動すると下のようにHello, World!と表示されます。

f:id:llcc:20191201180656p:plain

その下にラベルを1つ追加してみたいと思います。 ContentViewクラスを以下のように修正します。 2つ縦に並べるのはVStackというクラスを使っています。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
            Text("Goodnight, World!")
        }
    }
}

アプリを起動するとラベルが2つ表示されています。

f:id:llcc:20191201180957p:plain

次にラベル間を少し空けてみます。 VStackの初期化時にspacingを渡します。

struct ContentView: View {
    var body: some View {
        VStack(spacing: 200) {
            Text("Hello, World!")
            Text("Goodnight, World!")
        }
    }
}

これで間隔を200pxにする事ができました。

f:id:llcc:20191201181253p:plain

ラベルと色やフォントも変更してみます。

struct ContentView: View {
    var body: some View {
        Text("Hello, World!").font(Font.system(size: 50)).foregroundColor(Color(red: 1, green: 1, blue: 0))
    }
}

少し見づらいですが黄色の大きめの文字に変わりました。

f:id:llcc:20191201182626p:plain

次はボタンの追加とタップ時のアクション追加を行います。

struct ContentView: View {
    var body: some View {
        Button("ボタン") {
            print(10)
        }
    }
}

ボタンをタップするとデバッグエリアに10が表示されます。

f:id:llcc:20191201181647p:plain

次はボタンタップ時にアラートも表示してみます。

struct ContentView: View {
    @State var isPresented = false
    
    var body: some View {
        Button("ボタン") {
            self.isPresented = true
        }.alert(isPresented: $isPresented) {
            Alert(title: Text("Title"), message: Text("Message"))
        }
    }
}

アラートを表示する事ができました。

f:id:llcc:20191201183827p:plain

この.alert(isPresented:)はButtonに付いている必要はなく以下のようにTextに付いていても動きます。

struct ContentView: View {
    @State var isPresented = false
    
    var body: some View {
        VStack {
            Text("Hello, World!").alert(isPresented: $isPresented) {
                Alert(title: Text("Title"), message: Text("Message"))
            }
            Button("ボタン") {
                self.isPresented = true
            }
        }
    }
}

次はボタンを押した時にテキストの内容を変える処理をします。

struct ContentView: View {
    @State var text = "Hello, World"
    
    var body: some View {
        VStack {
            Text(text)
            Button("ボタン") {
                self.text = "Goodnight, World"
            }
        }
    }
}

ボタンを押すとテキストを変更する事ができました。

f:id:llcc:20191201184821p:plain

以下のようにViewModelを別途作る事もできます。

class MyViewModel : ObservableObject, Identifiable {
    @Published
    var text: String = "Hello, World"
}

struct ContentView: View {
    @ObservedObject var viewModel = MyViewModel()
    
    var body: some View {
        VStack {
            Text(viewModel.text)
            Button("ボタン") {
                self.viewModel.text = "Goodnight, World"
            }
        }
    }
}

データをTextFieldと紐付ける事もできます。 TextFieldの引数はBinding<String>なので$viewModel.textとドルを付けてBindingオブジェクトに変えて上げる必要があります。

class MyViewModel : ObservableObject, Identifiable {
    @Published
    var text: String = "Hello, World"
}

struct ContentView: View {
    @ObservedObject var viewModel = MyViewModel()
    
    var body: some View {
        VStack {
            Text(viewModel.text)
            TextField("Placeholder", text: $viewModel.text)
        }
    }
}

f:id:llcc:20191201190104p:plain

画面遷移はNavigationViewを使うと実現できます。

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: DestinationView()) {
                Text("ボタン")
            }
        }
    }
}

struct DestinationView: View {
    var body: some View {
        Text("遷移先")
    }
}

Googleのデータポータル(旧DataStudio)でアプリの新規ユーザーをグラフ化

先日の記事でFirebaseのデータをBigQueryに追加しました。 今度はBigQueryのデータをデータポータルというツールを使ってグラフ化してみようと思います。

www.cl9.info

データポータルとは

データポータルとはGoogleの提供しているデータをビジュアライズ化するツールです。 今回はBigQueryのデータをグラフ化するのですが、BigQuery以外にGoogleアナリティクス・Googleスプレットシート・MySQLのデータなど色々なデータを取り込んで表示する事ができます。

datastudio.google.com

取り込んだデータは下のように様々な形で表示する事ができます。

f:id:llcc:20191201114725p:plain

BigQueryで取り込んだデータを表示する

データポータルのページに移動してから、「空のレポート」を選んでレポート作成します。

f:id:llcc:20191201115052p:plain

レポートを作ったら右下の「新しいデータソースを作成」ボタンからデータソースを作ります。

f:id:llcc:20191201115158p:plain

ボタンを押すとデータ取得元を選ぶ画面に移動します。 今回はBigQueryを選択します。

f:id:llcc:20191201115248p:plain

選択と下のようにデータセット選択ページに飛ぶのでプロジェクトのAnalyticsのデータを選択します。 events_intraday_YYYYMMDDは直近のデータのみなので、今回はevents_YYYYMMDDを選択しました。

f:id:llcc:20191201115907p:plain

フィールドを選ぶ画面が出るんですがここは何も変更せず保存します。

f:id:llcc:20191201171001p:plain

それが終わると画面にイベント数の入ったテーブルが表示されます。 今回は新規ユーザー数を表示したいのでこれは削除します。

f:id:llcc:20191201171031p:plain

削除してから「グラフを追加」で時系列グラフを追加します。

f:id:llcc:20191201171127p:plain

デフォルトだと全イベントの数が入っているので、新規ユーザー数のみに絞ります。 右側の「フィルタを追加」ボタンから、新規登録数を表示するためのフィルターを作成します。

f:id:llcc:20191201171243p:plain

フィルターはEvent Nameがfirst_open(初めてアプリ立ち上げ)のイベントで絞り込みます。

f:id:llcc:20191201171318p:plain

以上で設定は完了です。 2日分しかデータ入れてないのですが、無事に新規ユーザー数を表示する事ができました。

f:id:llcc:20191201171554p:plain

FirebaseのデータをBigQueryに入れて分析する

Firebase単体だと見づらいデータが多いのでBigQueryに入れて分析してみました。
今回はFirebaseとBigQueryを連携して新規ユーザー数を取得するところまでやります。

ドキュメントは下URLになります。

Firebase 向け Google アナリティクスのデータを BigQuery にインポート  |  ソリューション  |  Google Cloud

BigQueryの料金

FirebaseとBigQueryの連携をする場合、まずはFirebaseをBlaze(従量課金)プランに上げる必要があります。
ただ、実際試したところSparkプランでもある程度動かす事ができました。
動かした感じ、無料プランだと制限付きBigQuery(サンドボックス)が適用されるのだと思います。

Firebaseの料金詳細は下URLの通りです。

firebase.google.com

BigQuery側の料金は下のとおりです。
ストレージ容量、クエリの容量などに応じた従量課金で一定まで無料で使う事ができます。

cloud.google.com

FirebaseをBlazeプランに変更

Blazeプランへの変更はFirebaseのプロジェクト画面左下の「アップグレード」ボタンから行う事ができます。

f:id:llcc:20191127144703p:plain

BigQueryとFirebaseの連携

連携はプロジェクト設定の「統合」から行う事ができます。

f:id:llcc:20191127144811p:plain

連携が完了すると下のようにBigQueryにプロジェクト、データセット、テーブルが作られます。
データの反映は半日 ~ 1日くらいかかる事があります。

f:id:llcc:20191127145627p:plain

BigQueryで新規ユーザー数を見る

まずは下のようなSQLでAnalyticsデータセットのテーブル一覧を取得します。
analytics_xxxxxxは自分の環境に置き換えて下さい。

SELECT * FROM analytics_xxxxxx.__TABLES__;

実行すると下のようにテーブル一覧が表示されます。

f:id:llcc:20191127150726p:plain

特定のテーブルのデータは下のようなSQLで取得する事ができます。

SELECT * FROM analytics_xxxxxx.events_20191126;

もし11月26日の新規ユーザー数を取りたい場合は下のようなSQLで取得する事ができます。

SELECT count(*) FROM analytics_xxxxxx.events_20191126 where event_name = "first_open";

複数日のデータをまとめて取りたい場合はテーブル名にワイルドカードを入れる事で実現できます。

SELECT event_date, count(*) FROM `analytics_xxxxxx.events_*` where event_name = "first_open" group by event_date;

データ容量ですがユーザーがほとんどいないプロジェクトでは1日20MB程でした。

f:id:llcc:20191129113832p:plain

ユーザー数も多くてイベントを大量に送っているプロジェクトでは2GB程ありました。

f:id:llcc:20191129113858p:plain

iOSでアプリ内課金したのに反映されないという問い合わせが来た時の対応策

タイトルのような問題が起きたので、その原因と対策について調べてみました。

起こったこと

  • iOSでアプリ内課金したのに反映されない
  • もう一度購入しようとすると、「このアイテムは購入済みです。無料で再入手しますか?」と出て来るが、「OK」を押しても反映されない
  • リストアをしようとしても、購入済みアイテムが見つからないというエラーになる

原因

課金処理途中で離脱するとこのような現象が発生します。 具体的には、購入途中でクレカ情報入力するなどしてアプリ外で購入&アプリのプロセス終了した時に発生します。

システム的には、上のようなフローをたどる事でアプリ上でSKPaymentTransactionが終了せずに残ってしまい、それが悪さをしていました。

再現方法

開発環境では下のようなフローで再現する事ができます。 1. アプリ内アイテムの購入をする 2. パスワード入力画面などで、Xcodeからアプリを終了する 3. そのままアプリ内課金を最後まで実施する

上フローの後にトランザクションの状態を見ると、たしかに1件未終了のトランザクションが残っていました。

print(SKPaymentQueue.default().transactions.count) // → 1

対策

購入画面のviewDidLoadに下処理を行う事で解決しました。 未終了トランザクションが残っていたらトランザクションを終了します。

SKPaymentQueue.default().transactions.forEach {
    if $0.transactionState != .purchasing {
        SKPaymentQueue.default().finishTransaction($0)
    }
}

最初はAppDelegateのdidFinishLaunchingWithOptionsでやろうと思ったのですが、「SKPaymentQueue.default().transactions」が常に0件になってしまったので、購入画面で実施しました。

参考URL

gosyuin-map.seesaa.net

docker runで起動したコンテナを削除する

docker runで生成したコンテナの削除方法です。

まずは下コマンドでコンテナ一覧を表示します。
-aオプションを付けることで起動中でないコンテナも表示する事ができます。

docker ps -a

下のようにコンテナ一覧が出てきます。

f:id:llcc:20181103222502p:plain

最後にコンテナIDを指定してrmコマンドを打てば削除する事ができます。

docker rm CONTAINER_ID

下コマンドでコンテナを一括削除することもできます。

docker rm `docker ps -aq`

MacにDockerをインストールして起動する

今更ですがDockerを触ってみました。

Dockerのインストール

Mac版のインストールはdocker storeから行います。
ページ右上の「Please Login To Download」ボタンからログインしてDockerをインストールします。

Docker Store

DockerでHello World

インストールしたらDockerを起動します。

f:id:llcc:20181030224951p:plain

一度起動すればコマンドライン上でDockerを使う事ができます。

f:id:llcc:20181030225027p:plain

まずはHelloWorldをやってみます。

docker run hello-world

ここでunauthorized: incorrect username or password.というエラーが出る場合、下コマンドでログインする必要があります。
この時、メールアドレスではなくログインIDを使ってログインする必要があります。

docker login

ログインした状態でHello Worldコマンドを打つと下のようにDockerが起動します。

f:id:llcc:20181030225600p:plain

DockerでWEBサーバーを立ち上げる

WEBサーバーはこちらのページのNginxを利用しました。

Docker Store

NginxのDockerは下コマンドで起動します。

docker run --name my-nginx -d -p 80:80 nginx

http://localhostにアクセスするとNginxが起動している事が分かります。

f:id:llcc:20181030233459p:plain

起動中のDockerはdocker psコマンドで確認する事ができます。

f:id:llcc:20181030233550p:plain

docker psで表示されるDockerのコンテナの停止はdocker stopコマンドを使います。
xxxxxにはdocker psで表示されたCONTAINER IDを入れます。

docker stop xxxxx

再びコンテナを起動するにはstartコマンドを使います。

docker start my-nginx

自分のコンテナ名は下コマンドで確認する事ができます。

docker container ls -a

コンテナの削除はrmコマンドを使います。

docker rm my-nginx