paper-onboardingでおしゃれウォークスルー
paper-onboardingというライブラリを使っておしゃれなウォークスルーを実現してみました。
paper-onboardingのインストール
target 'MyApp' do use_frameworks! pod 'paper-onboarding' end
2017/10/16現在はSwift4に対応していないので、Swift3.2で動かしました。
paper-onboardingの使い方
まずは利用するアイコンと写真をセットします。
今回は下のサイトのフリーアイコンと写真を利用しました。
実装は下の通りです。
PaperOnboardingのdataSourceでタイトルやアイコンなどを指定する事で画面をデザインします。
import UIKit import paper_onboarding class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let onboarding = PaperOnboarding() onboarding.dataSource = self onboarding.translatesAutoresizingMaskIntoConstraints = false view.addSubview(onboarding) // add constraints for attribute: NSLayoutAttribute in [.left, .right, .top, .bottom] { let constraint = NSLayoutConstraint(item: onboarding, attribute: attribute, relatedBy: .equal, toItem: view, attribute: attribute, multiplier: 1, constant: 0) view.addConstraint(constraint) } } } extension ViewController: PaperOnboardingDataSource { func onboardingItemsCount() -> Int { return 3 } func onboardingItemAtIndex(_ index: Int) -> OnboardingItemInfo { return ( imageName: UIImage(named: "image\(index+1)")!, title: "タイトル\(index)", description: "説明\(index)", iconName: UIImage(named: "icon\(index+1)")!, color: [UIColor.blue, UIColor.darkGray, UIColor.brown][index], titleColor: UIColor.white, descriptionColor: UIColor.lightGray, titleFont: UIFont.systemFont(ofSize: 20), descriptionFont: UIFont.systemFont(ofSize: 14) ) } }
実行の様子は下の通りです。
PaperOnboardingのdelegateを使うとページ移動のタイミングなどを取得することができます。
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let onboarding = PaperOnboarding() onboarding.dataSource = self onboarding.delegate = self onboarding.translatesAutoresizingMaskIntoConstraints = false view.addSubview(onboarding) // add constraints for attribute: NSLayoutAttribute in [.left, .right, .top, .bottom] { let constraint = NSLayoutConstraint(item: onboarding, attribute: attribute, relatedBy: .equal, toItem: view, attribute: attribute, multiplier: 1, constant: 0) view.addConstraint(constraint) } } } extension ViewController: PaperOnboardingDelegate { func onboardingWillTransitonToIndex(_ index: Int) {} func onboardingDidTransitonToIndex(_ index: Int) {} func onboardingConfigurationItem(_ item: OnboardingContentViewItem, index: Int) {} var enableTapsOnPageControl: Bool { return true } }
RubyでGoogleAnalyticsのデータを取得する
GoogleAnalyticsのデータの取り方を調べてみました。
取得準備
まずはGoogleCloudPlatformにアクセスしてプロジェクトを作成します。
続けて「IAM と管理」の「サービス アカウント」からサービスアカウントを作成します。
この時、jsonファイルがダウンロードされるので、保管しておきます。
次はGoogleAnalyticsの設定のユーザー管理で、先程作成したサービスアカウントのメールアドレスを登録します。
続けてGoogleAnalyticsのAPIを有効にします。
取得処理の実装
ライブラリはgoogle-api-clientを利用します。
source "https://rubygems.org" gem 'google-api-client'
取得処理は下の通りです。
先程ダウンロードしたjsonを使って認証してAPIを叩いています。
require 'google/apis/analytics_v3' client = Google::Apis::AnalyticsV3::AnalyticsService.new client.authorization = Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: File.open('./AnalyticsTest.json'), # 先程ダウンロードしたjsonファイル scope: ['https://www.googleapis.com/auth/analytics.readonly'] ) data = client.get_ga_data( "ga:#{12345}", # ビューIDを入れる (Date.today - 7).strftime, Date.today.strftime, "ga:pageviews", { dimensions: 'ga:date', sort: "-ga:date" } ) data.rows.each do |row| p row end
ビューIDは、Analyticsのビュー設定のものを入れます。
実行したところ、無事にPVが取得できました。
iOSのサイレントプッシュを試してみる
iOSのサイレントプッシュというものを試してみました。
サイレントプッシュとは
その名の通り、ユーザーに通知メッセージが出ないプッシュ通知の事です。
送信時にバックグラウンドで処理を走らせる事ができるので、バックグラウンドでのデータの更新などに向いています。
大まかな流れは通常のpush通知と同じです。
サイレントプッシュ送信準備
まずは下記事の手順に従ってpush通知の実装をします。
続けてFIRMessaging.messaging().subscribeメソッドでトピックの登録をします。
トピックはプッシュ通知の送信者指定に関する機能で、通知を実際に送る際に利用します。
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { FIRInstanceID.instanceID().setAPNSToken(deviceToken, type: .unknown) FIRMessaging.messaging().subscribe(toTopic: "/topics/all") } }
サイレントプッシュを受け取り時の処理
続けてサイレントプッシュを受け取った時の処理を追加します。
AppDelegateにdidReceiveRemoteNotificationを実装します。
サイレントプッシュを受け取った時は、下のメソッドが呼ばれるのでこの中でデータ更新などの処理を記述します。
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { print("メッセージを受け取った") completionHandler(.noData) } }
サイレントプッシュを送る
最後にFirebaseに下のようなリクエストを送れば完了です。
curl --header "Authorization: key=API_KEY" --header Content-Type:"application/json" https://fcm.googleapis.com/fcm/send -d "{\"to\": \"/topics/all\",\"content_available\":true}"
API_KEYはFirebaseの設定のものを利用します。
コンソールからAPIを叩いたら、アプリ側のログも出力されました。
\[weak self]が必要な時を調べてみる
循環参照対策で使う[weak self]ですが、常に付ける必要があるのか、それとも付けなくて良い時があるのかを調べてみました。
UIAlertControllerを使った検証
最初に、下のようなUIAlertControllerで[weak self]が必要なのかを調べてみました。
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { [weak self] _ in self?.dismiss(animated: true, completion: nil) })) present(alert, animated: true, completion: nil)
確認の為、下のようなUIViewControllerを作成しました。
import UIKit class MyViewController: UIViewController { deinit { print("deinit") } override func viewDidAppear(_ animated: Bool) { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { _ in self.dismiss(animated: true, completion: nil) })) present(alert, animated: true, completion: nil) } }
アプリを起動すると、下のようにUIAlertControllerが表示されます。
この状態で循環参照が発生するか調べてみました。
確認した所、無事にdeinitメソッドが呼ばれたので循環参照は発生していませんでした。
次はUIAlertControllerをローカル変数ではなくインスタンス変数にしました。
class MyViewController: UIViewController { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .alert) deinit { print("deinit") } override func viewDidAppear(_ animated: Bool) { alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { _ in self.dismiss(animated: true, completion: nil) })) present(alert, animated: true, completion: nil) } }
今回はdeinitが呼ばれなかったので、循環参照が発生していたようです。
内部で起こっている事を予想する
内部のメモリの動きを想像してみました。
UIAlertControllerがローカル変数だった時
- UIViewControllerを表示する(UIViewControllerの参照カウントが1になる)
- UIAlertControllerインスタンスをローカル変数として作成、それをpresentする(UIViewControllerとUIAlertControllerの参照カウントがそれぞれ+1されて2と1になる)
- OKボタンを押す事で、UIAlertControllerが消える(dismissによってUIAlertControllerへの参照が消えてUIAlertControllerの参照カウントは-1されて0になる)
- UIAlertControllerの参照カウントが0なので、メモリ解放される(それによってUIViewControllerへの参照も-1されて循環参照が起こらなくなる)
UIAlertControllerがインスタンス変数だった時
- UIViewControllerを表示する(UIViewControllerの参照カウントが1になる)
- UIAlertControllerをインスタンス変数として作成、それをpresentする(インスタンス変数にしたこととpresentした事でUIAlertControllerの参照カウントは2になる)
- OKボタンを押す事で、UIAlertControllerが消える(dismissによってUIAlertControllerへの参照が消えてUIAlertControllerの参照カウントは-1されて1になる)
- UIAlertControllerの参照カウントが1なので、メモリ解放されない(それによってUIViewControllerへの参照も+1が残り続けて参照カウントが0にならない)
独自クラスで検証する
次はUIAlertControllerではなく、独自クラスで検証してみます。
まずは下のようにブロックを実行するViewControllerを作ってみます。
class MyViewController: UIViewController { deinit { print("deinit") } override func viewDidAppear(_ animated: Bool) { let block = { self.dismiss(animated: true, completion: nil) } block() } }
このようにブロックをローカル変数として持つと、deinitが呼ばれました。
しかしブロックをインスタンス変数として持つとdeinitは呼ばれませんでした。
class MyViewController: UIViewController { var block: (() -> ())? deinit { print("deinit") } override func viewDidAppear(_ animated: Bool) { block = { self.dismiss(animated: true, completion: nil) } block?() } }
この時、[weak self]を使えばdeinitが呼ばれるようになります。
class MyViewController: UIViewController { var block: (() -> ())? deinit { print("deinit") } override func viewDidAppear(_ animated: Bool) { block = { [weak self] in self?.dismiss(animated: true, completion: nil) } block?() } }
それと下のようにブロック中でselfを使わない場合は[weak self]しなくてもdeinitが呼ばれます。
class MyViewController: UIViewController { var block: (() -> ())? deinit { print("deinit") } override func viewDidAppear(_ animated: Bool) { block = {} self.dismiss(animated: true, completion: nil) } }
まとめ
- UIAlertControllerでは[weak self]を使わなくて良い時はある
- 「UIAlertControllerをインスタンス変数で持つ」など、present以外で参照カウントが+1されるされる時は[weak self]が必要
【iOS】FIrebaseでpush通知
Firebaseを使ったpush通知を試してみました。
APNs証明書の準備
まずはプッシュ通知用の証明書を作成します。
キーチェーンを開き、「認証局に証明書を要求」を選択します。
次の画面で、メールアドレスと名前を入力して証明書要求を作成します。
CertificateSigningRequest.certSigningRequest
というファイルが保存されるので、それを使って証明書を作成します。
まずはMemberCenterのCertificates, Identifiers & Profilesにアクセスして下さい。
そこで、プッシュ通知を使いたいアプリIDの「Push Notifications」にチェックを入れ、先程作ったファイルをアップロードします。
そのまま手順に従っていけば証明書を作成する事ができます。
作成したら、それをダブルクリックでキーチェーンに登録、登録した証明書を右クリックしてp12に書き出しをします。
作成が終わったら、Firebaseのそのプロジェクトの設定に証明書をアップロードします。
これでFirebase上の設定は整いました。
プッシュ用のトークンを受け取る
続けてコード上にプッシュトークンを受け取る処理を追加します。
まずはCocoaPodsでFirebase/CoreとFirebase/Messagingをインストールします。
target 'MyApp' do use_frameworks! pod 'Firebase/Core' pod 'Firebase/Messaging' end
次にCapabilitiesでPush通知を有効にします。
MemberCenterからProvisioningProfileも作成します。
次はAppDelegateなどに、Firebaseの初期化とプッシュ通知の許可を求める処理を追加します。
import Firebase @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { FIRApp.configure() application.registerUserNotificationSettings(UIUserNotificationSettings(types: [.badge, .sound, .alert], categories: nil)) application.registerForRemoteNotifications() return true } }
次はInfo.plistにFirebaseAppDelegateProxyEnabledというキーを追加してNOをセットします。
AppDelegateに通知トークンをサーバーに送る処理を追加します。
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { FIRInstanceID.instanceID().setAPNSToken(deviceToken, type: .unknown) }
以上で対応は完了です。
FirebaseのNotificationからプッシュ通知を送ればデバイスに届くようになりました。
【Swift】isとisMemberOfClassとisKindOfClass
isとisMemberOfClassとisKindOfClassですが、挙動の差がわかりにくかったのでまとめてみました。
isKindOfClass
下のようなケースでは当然trueになります。
それとisKindOfClass/isMemberOfClassはNSObjectProtocolのメソッドなので、NSObjectなどを継承しているクラスでのみ使えます。
class MyClass: NSObject {} let myClass = MyClass() print(myClass.isKind(of: MyClass.self)) // → true
isKindOfClassは判定対象が引数のサブクラスの場合もtrueになります。
class MyClass: NSObject {} class MySubClass: MyClass {} let myClass = MySubClass() print(myClass.isKind(of: MyClass.self)) // → true
isMemberOfClass
isMemberOfClassはisKindOfClassと違って、サブクラスとの判定はfalseになります。
class MyClass: NSObject {} class MySubClass: MyClass {} print(MyClass().isKind(of: MyClass.self)) // → true print(MySubClass().isKind(of: MyClass.self)) // → false
is
isはisKindOfClass同様、サブクラスとの判定もtrueになります。
isKindOfClassと違い、NSObjectProtocolに準拠していないクラスでも使えます。
class MyClass {} class MySubClass: MyClass {} print(MyClass() is MyClass) // → true print(MySubClass() is MyClass) // → true
isMemberOfClass相当の判定方法
NSObjectProtocolに準拠してないクラスでisMemberOfClass同様の判定をしたい場合は、下の方法が使えそうです。
String(describing: type(of: myClass))と"(MyClass.self)"はクラス名を返すので、サブクラスとの比較がfalseになります。
class MyClass {} class MySubClass: MyClass {} let myClass = MyClass() let mySubClass = MySubClass() print(String(describing: type(of: myClass)) == "\(MyClass.self)") print(String(describing: type(of: mySubClass)) == "\(MyClass.self)")
文字列での判定なので、下のように別フレームワークと同じクラス名を使ってる場合は注意が必要です。
class UIView {} print(UIView() is UIKit.UIView) // → false print(String(describing: type(of: UIView())) == "\(UIKit.UIView.self)") → true
【Swift】複数のプロトコルに準拠した変数の定義
下で定義された2つのプロトコルに準拠する型の書き方です。
protocol MyProtocol1 {} protocol MyProtocol2 {}
複数のプロトコルへの準拠を表したい時は、下のように&を使います。
protocol MyProtocol1 {} protocol MyProtocol2 {} class MyClass: MyProtocol1, MyProtocol2 {} let value: (MyProtocol1 & MyProtocol2) = MyClass()
下のように、引数・返り値に使う事もできます。
protocol MyProtocol1 {} protocol MyProtocol2 {} class MyClass: MyProtocol1, MyProtocol2 {} class TestClass { var value1: (MyProtocol1 & MyProtocol2)? var value2: (MyProtocol1 & MyProtocol2) { return MyClass() } func myMethod(value: (MyProtocol1 & MyProtocol2)) -> (MyProtocol1 & MyProtocol2) { return MyClass() } }
上のように何回も使う場合、typealiasで再定義するとすっきりします。
protocol MyProtocol1 {} protocol MyProtocol2 {} class MyClass: MyProtocol1, MyProtocol2 {} typealias MyProtocols = MyProtocol1 & MyProtocol2 let value: MyProtocols = MyClass()