しめ鯖日記

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

paper-onboardingでおしゃれウォークスルー

paper-onboardingというライブラリを使っておしゃれなウォークスルーを実現してみました。

github.com

paper-onboardingのインストール

target 'MyApp' do
  use_frameworks!

  pod 'paper-onboarding'
end

2017/10/16現在はSwift4に対応していないので、Swift3.2で動かしました。

f:id:llcc:20171016125935p:plain

paper-onboardingの使い方

まずは利用するアイコンと写真をセットします。
今回は下のサイトのフリーアイコンと写真を利用しました。

icooon-mono.com

www.pakutaso.com

f:id:llcc:20171016131202p:plain

実装は下の通りです。
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)
        )
    }
}

実行の様子は下の通りです。

f:id:llcc:20171016131829g:plain

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にアクセスしてプロジェクトを作成します。

Google Cloud Platform

f:id:llcc:20171015150926p:plain

続けて「IAM と管理」の「サービス アカウント」からサービスアカウントを作成します。
この時、jsonファイルがダウンロードされるので、保管しておきます。

f:id:llcc:20171015152633p:plain

次はGoogleAnalyticsの設定のユーザー管理で、先程作成したサービスアカウントのメールアドレスを登録します。

f:id:llcc:20171015153101p:plain

続けてGoogleAnalyticsのAPIを有効にします。

f:id:llcc:20171015153853p:plain

取得処理の実装

ライブラリは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のビュー設定のものを入れます。

f:id:llcc:20171015154436p:plain

実行したところ、無事にPVが取得できました。

f:id:llcc:20171015154949p:plain

iOSのサイレントプッシュを試してみる

iOSのサイレントプッシュというものを試してみました。

サイレントプッシュとは

その名の通り、ユーザーに通知メッセージが出ないプッシュ通知の事です。
送信時にバックグラウンドで処理を走らせる事ができるので、バックグラウンドでのデータの更新などに向いています。
大まかな流れは通常のpush通知と同じです。

サイレントプッシュ送信準備

まずは下記事の手順に従ってpush通知の実装をします。

llcc.hatenablog.com

続けて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の設定のものを利用します。

f:id:llcc:20171014143119p:plain

コンソールからAPIを叩いたら、アプリ側のログも出力されました。

f:id:llcc:20171014143635p:plain

\[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が表示されます。
この状態で循環参照が発生するか調べてみました。

f:id:llcc:20171013131339p:plain

確認した所、無事にdeinitメソッドが呼ばれたので循環参照は発生していませんでした。

f:id:llcc:20171013131439p:plain

次は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が呼ばれなかったので、循環参照が発生していたようです。

f:id:llcc:20171013131626p:plain

内部で起こっている事を予想する

内部のメモリの動きを想像してみました。

UIAlertControllerがローカル変数だった時

  1. UIViewControllerを表示する(UIViewControllerの参照カウントが1になる)
  2. UIAlertControllerインスタンスをローカル変数として作成、それをpresentする(UIViewControllerとUIAlertControllerの参照カウントがそれぞれ+1されて2と1になる)
  3. OKボタンを押す事で、UIAlertControllerが消える(dismissによってUIAlertControllerへの参照が消えてUIAlertControllerの参照カウントは-1されて0になる)
  4. UIAlertControllerの参照カウントが0なので、メモリ解放される(それによってUIViewControllerへの参照も-1されて循環参照が起こらなくなる)

UIAlertControllerがインスタンス変数だった時

  1. UIViewControllerを表示する(UIViewControllerの参照カウントが1になる)
  2. UIAlertControllerをインスタンス変数として作成、それをpresentする(インスタンス変数にしたこととpresentした事でUIAlertControllerの参照カウントは2になる)
  3. OKボタンを押す事で、UIAlertControllerが消える(dismissによってUIAlertControllerへの参照が消えてUIAlertControllerの参照カウントは-1されて1になる)
  4. 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が呼ばれました。

f:id:llcc:20171013132815p:plain

しかしブロックをインスタンス変数として持つと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?()
    }
}

f:id:llcc:20171013133032p:plain

それと下のようにブロック中で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証明書の準備

まずはプッシュ通知用の証明書を作成します。
キーチェーンを開き、「認証局に証明書を要求」を選択します。

f:id:llcc:20171012142142p:plain

次の画面で、メールアドレスと名前を入力して証明書要求を作成します。

f:id:llcc:20171012142307p:plain

CertificateSigningRequest.certSigningRequestというファイルが保存されるので、それを使って証明書を作成します。
まずはMemberCenterのCertificates, Identifiers & Profilesにアクセスして下さい。
そこで、プッシュ通知を使いたいアプリIDの「Push Notifications」にチェックを入れ、先程作ったファイルをアップロードします。

f:id:llcc:20171012142527p:plain

そのまま手順に従っていけば証明書を作成する事ができます。

作成したら、それをダブルクリックでキーチェーンに登録、登録した証明書を右クリックしてp12に書き出しをします。

f:id:llcc:20171012143124p:plain

作成が終わったら、Firebaseのそのプロジェクトの設定に証明書をアップロードします。
これでFirebase上の設定は整いました。

f:id:llcc:20171012143232p:plain

プッシュ用のトークンを受け取る

続けてコード上にプッシュトークンを受け取る処理を追加します。
まずはCocoaPodsでFirebase/CoreとFirebase/Messagingをインストールします。

target 'MyApp' do
  use_frameworks!

  pod 'Firebase/Core'
  pod 'Firebase/Messaging'
end

次にCapabilitiesでPush通知を有効にします。

f:id:llcc:20171012144537p:plain

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をセットします。

f:id:llcc:20171012150137p:plain

AppDelegateに通知トークンをサーバーに送る処理を追加します。

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    FIRInstanceID.instanceID().setAPNSToken(deviceToken, type: .unknown)
}

以上で対応は完了です。
FirebaseのNotificationからプッシュ通知を送ればデバイスに届くようになりました。

f:id:llcc:20171012145327p:plain

f:id:llcc:20171012145425p:plain

【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()