Today Extensionでウィジェット作成
Today Extensionを使ったウィジェット作成を試してみました。
プロジェクトを作成したらメニューからTargetの作成を行います。
Today Extensionを選択します。
作成するとToday Extension関連ファイルが作られます。
Today Extensionを起動するとHello worldが表示されます。
Storyboardでテキストを変更する事で文字を変える事ができます。
ラベルはUIViewControllerと紐付ける事で動的な変更もできます。
class TodayViewController: UIViewController, NCWidgetProviding { @IBOutlet weak var myLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() myLabel.text = "Hello!" } }
ラベルだけでなくボタンも配置する事ができます。
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側にも追加が必要です。
あとは作成したグループを使ってデータ作成すればデータ共有できるようになっています。
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
への追加などが必要です。
CocoaPodsを使う場合、Extension用の処理も必要があるので注意が必要です。
target 'MyApp' do use_frameworks! pod 'RealmSwift' end target 'MyExtension' do use_frameworks! pod 'RealmSwift' end
SwiftUIを触ってみる
SwiftUIで簡単なUIを作ってみました。
まずはXcode11で新規プロジェクトを作成します。
テンプレートは「Single View App」を選びました。
プロジェクト構成は以下のようになっています。
Xcode11からはSceneDelegate.swiftというファイルが追加されています。 これはiPadで画面に複数アプリ表示する際に使うクラスで、今までのAppDelegateの役割の一部を担っています。 今回はSwiftUIを中心に見たいので深堀りしないことにします。
まずはアプリを立ち上げてみます。 アプリを起動すると下のようにHello, World!と表示されます。
その下にラベルを1つ追加してみたいと思います。 ContentViewクラスを以下のように修正します。 2つ縦に並べるのはVStackというクラスを使っています。
struct ContentView: View { var body: some View { VStack { Text("Hello, World!") Text("Goodnight, World!") } } }
アプリを起動するとラベルが2つ表示されています。
次にラベル間を少し空けてみます。 VStackの初期化時にspacingを渡します。
struct ContentView: View { var body: some View { VStack(spacing: 200) { Text("Hello, World!") Text("Goodnight, World!") } } }
これで間隔を200pxにする事ができました。
ラベルと色やフォントも変更してみます。
struct ContentView: View { var body: some View { Text("Hello, World!").font(Font.system(size: 50)).foregroundColor(Color(red: 1, green: 1, blue: 0)) } }
少し見づらいですが黄色の大きめの文字に変わりました。
次はボタンの追加とタップ時のアクション追加を行います。
struct ContentView: View { var body: some View { Button("ボタン") { print(10) } } }
ボタンをタップするとデバッグエリアに10が表示されます。
次はボタンタップ時にアラートも表示してみます。
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")) } } }
アラートを表示する事ができました。
この.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" } } } }
ボタンを押すとテキストを変更する事ができました。
以下のように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) } } }
画面遷移は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のデータをデータポータルというツールを使ってグラフ化してみようと思います。
データポータルとは
データポータルとはGoogleの提供しているデータをビジュアライズ化するツールです。 今回はBigQueryのデータをグラフ化するのですが、BigQuery以外にGoogleアナリティクス・Googleスプレットシート・MySQLのデータなど色々なデータを取り込んで表示する事ができます。
取り込んだデータは下のように様々な形で表示する事ができます。
BigQueryで取り込んだデータを表示する
データポータルのページに移動してから、「空のレポート」を選んでレポート作成します。
レポートを作ったら右下の「新しいデータソースを作成」ボタンからデータソースを作ります。
ボタンを押すとデータ取得元を選ぶ画面に移動します。 今回はBigQueryを選択します。
選択と下のようにデータセット選択ページに飛ぶのでプロジェクトのAnalyticsのデータを選択します。 events_intraday_YYYYMMDDは直近のデータのみなので、今回はevents_YYYYMMDDを選択しました。
フィールドを選ぶ画面が出るんですがここは何も変更せず保存します。
それが終わると画面にイベント数の入ったテーブルが表示されます。 今回は新規ユーザー数を表示したいのでこれは削除します。
削除してから「グラフを追加」で時系列グラフを追加します。
デフォルトだと全イベントの数が入っているので、新規ユーザー数のみに絞ります。 右側の「フィルタを追加」ボタンから、新規登録数を表示するためのフィルターを作成します。
フィルターはEvent Nameがfirst_open(初めてアプリ立ち上げ)のイベントで絞り込みます。
以上で設定は完了です。 2日分しかデータ入れてないのですが、無事に新規ユーザー数を表示する事ができました。
FirebaseのデータをBigQueryに入れて分析する
Firebase単体だと見づらいデータが多いのでBigQueryに入れて分析してみました。
今回はFirebaseとBigQueryを連携して新規ユーザー数を取得するところまでやります。
ドキュメントは下URLになります。
Firebase 向け Google アナリティクスのデータを BigQuery にインポート | ソリューション | Google Cloud
BigQueryの料金
FirebaseとBigQueryの連携をする場合、まずはFirebaseをBlaze(従量課金)プランに上げる必要があります。
ただ、実際試したところSparkプランでもある程度動かす事ができました。
動かした感じ、無料プランだと制限付きBigQuery(サンドボックス)が適用されるのだと思います。
Firebaseの料金詳細は下URLの通りです。
BigQuery側の料金は下のとおりです。
ストレージ容量、クエリの容量などに応じた従量課金で一定まで無料で使う事ができます。
FirebaseをBlazeプランに変更
Blazeプランへの変更はFirebaseのプロジェクト画面左下の「アップグレード」ボタンから行う事ができます。
BigQueryとFirebaseの連携
連携はプロジェクト設定の「統合」から行う事ができます。
連携が完了すると下のようにBigQueryにプロジェクト、データセット、テーブルが作られます。
データの反映は半日 ~ 1日くらいかかる事があります。
BigQueryで新規ユーザー数を見る
まずは下のようなSQLでAnalyticsデータセットのテーブル一覧を取得します。
analytics_xxxxxx
は自分の環境に置き換えて下さい。
SELECT * FROM analytics_xxxxxx.__TABLES__;
実行すると下のようにテーブル一覧が表示されます。
特定のテーブルのデータは下のような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程でした。
ユーザー数も多くてイベントを大量に送っているプロジェクトでは2GB程ありました。
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
docker runで起動したコンテナを削除する
docker runで生成したコンテナの削除方法です。
まずは下コマンドでコンテナ一覧を表示します。
-aオプションを付けることで起動中でないコンテナも表示する事ができます。
docker ps -a
下のようにコンテナ一覧が出てきます。
最後にコンテナIDを指定してrmコマンドを打てば削除する事ができます。
docker rm CONTAINER_ID
下コマンドでコンテナを一括削除することもできます。
docker rm `docker ps -aq`
MacにDockerをインストールして起動する
今更ですがDockerを触ってみました。
Dockerのインストール
Mac版のインストールはdocker storeから行います。
ページ右上の「Please Login To Download」ボタンからログインしてDockerをインストールします。
DockerでHello World
インストールしたらDockerを起動します。
一度起動すればコマンドライン上でDockerを使う事ができます。
まずはHelloWorldをやってみます。
docker run hello-world
ここでunauthorized: incorrect username or password.
というエラーが出る場合、下コマンドでログインする必要があります。
この時、メールアドレスではなくログインIDを使ってログインする必要があります。
docker login
ログインした状態でHello Worldコマンドを打つと下のようにDockerが起動します。
DockerでWEBサーバーを立ち上げる
WEBサーバーはこちらのページのNginxを利用しました。
NginxのDockerは下コマンドで起動します。
docker run --name my-nginx -d -p 80:80 nginx
http://localhost
にアクセスするとNginxが起動している事が分かります。
起動中のDockerはdocker ps
コマンドで確認する事ができます。
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