しめ鯖日記

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

【Swift】mp3をバックグラウンド再生する

バックグラウンド再生を試してみました。

mp3をフォアグラウンドで再生

まずはフォアグラウンドでmp3を再生してみます。
再生したいmp3ファイルをAssets.xcassetsに追加して下さい。

f:id:llcc:20171018131635p:plain

そのあと下のようにmp3再生処理を記述すれば完了です。

import UIKit
import AVFoundation

class ViewController: UIViewController {
    var player: AVAudioPlayer?
    
    override func viewDidLoad() {
        if let sound = NSDataAsset(name: "bgm") {
            player = try? AVAudioPlayer(data: sound.data)
            player?.play() // → これで音が鳴る
        }
    }
}

mp3をバックグラウンドで再生

次はアプリをバックグラウンドにしても再生し続ける方法を試します。

まずはCapabilitiesのBackground ModesをONにして、Audio Airplay and Picture in Pictureにチェックを入れます。

f:id:llcc:20171018132041p:plain

その後、AppDelegateなどに下2行を追加すれば完了です。

try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: .mixWithOthers)
try? AVAudioSession.sharedInstance().setActive(true)
import UIKit
import AVFoundation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: .mixWithOthers)
        try? AVAudioSession.sharedInstance().setActive(true)
        
        return true
    }
}

これでアプリがバックグラウンドに移動してもbgmが再生されます。
シミュレータでは上の処理を追加してもバックグラウンド再生されないので注意が必要です。

【iOS】1つの画像データで2xと3xにも対応する

毎回2xと3xのデータを作るのが大変だったので、1つの画像データ(PDF形式)で全部に対応する方法を調査しました。

今回は下のような画像で検証しました。
利用したツールはSketchです。

f:id:llcc:20171017135428p:plain

まずはこのデータをpdfとして書き出します。

f:id:llcc:20171017135437p:plain

次にAssets.xcassetsでImage Setを作成します。

f:id:llcc:20171017135651p:plain

今作ったImage SetのScaleをSingle Scaleに変更します。

f:id:llcc:20171017135725p:plain

変更したら先程作ったPDFをドラッグ&ドロップで追加します。

f:id:llcc:20171017135755p:plain

登録したPDFは、普通の画像と同じように利用する事ができます。

f:id:llcc:20171017135826p:plain

アプリを起動すると、画像が正常に表示されている事が分かります。

f:id:llcc:20171017135917p:plain

下はPDFの代わりにpngを使った画像です。
PDFでないと画像がぼやける事が分かります。

f:id:llcc:20171017140036p:plain

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