20200317のSwiftに関する記事は9件です。

エンジニア向けオープンプロジェクトトレース

https://www.reiwarss.com/OpenProject

Top tags
python
swift
javascript
go
C
C++
C#
Ruby
TypeScript
PHP

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spotify SDK for iOSで簡単なアプリを作ってみた

はじめに

こんにちは
渋谷でiOSエンジニアとして働いているChikatoです!

自作アプリを作る過程でSpotify SDKを使ってみたくて、公式のチュートリアルサンプルプロジェクトを参考にしましたが詰まってしまったところも多く、また日本語の資料も少ない(あっても少し古い)のでまとめてみたいと思います!

また、自分が詰まってしまったポイントも合わせて記載致します。
これからSpotify SDK iOSを使われる方のご参考になれば幸いです!

サンプルアプリとして、簡単に以下のアプリを作ってみました
このアプリでは、Spotifyで再生している曲の取得と、スキップができます。

ezgif.com-video-to-gif.gif
コード:https://github.com/Take111/SportifyTutorial

開発環境

Xcode Ver.11.3.1
Swift 5.1.3
Spotify iOS SDK Ver.1.2.2

セットアップ

SDKのダウンロードやXcode上で設定が必要になるので、チュートリアルに従って進めてください

SceneDelegate

今回のSceneDelegateでは以下のことを行います。
-AccessTokenの取得
-端末にあるSpotifyとの接続
ただ、後述する認証を行う前だとAccesTokenは取得できません。

SceneDelegate.swift
    private let clientID = ""
    private let redirectURL = URL(string: "SpotifyTutorial://spotify/callback")!

    lazy var appRemote: SPTAppRemote = {
        let configuration = SPTConfiguration(clientID: clientID, redirectURL: redirectURL)
        let appRemote = SPTAppRemote(configuration: configuration, logLevel: .debug)
        appRemote.connectionParameters.accessToken = self.accessToken
        appRemote.delegate = self
        return appRemote
    }()

    var accessToken = UserDefaults.standard.string(forKey: kAccessTokenKey) {
        didSet {
            let defaults = UserDefaults.standard
            defaults.set(accessToken, forKey: SceneDelegate.kAccessTokenKey)
            print("SceneDelegate: accessToken: \(accessToken)")
        }
    }

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        print("SceneDelegate: openURLContexts is called")
        guard let url = URLContexts.first?.url else {
            return
        }
        // authoricationParameters()これがAccessTokenかエラーを返す
        let parameters = appRemote.authorizationParameters(from: url)

        if let access_token = parameters?[SPTAppRemoteAccessTokenKey] {
            appRemote.connectionParameters.accessToken = access_token
            self.accessToken = access_token
        }
        else if let errorDescription = parameters?[SPTAppRemoteErrorDescriptionKey] {
            print("SceneDelegate: errorDescription has error: \(errorDescription)")
        }
    }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    // 端末にあるSpotifyと接続
    func sceneDidBecomeActive(_ scene: UIScene) {
        print("SceneDelegate: sceneDidBecomeActive")
        appRemote.connect()
    }

詰まりポイント

・途中でSpotifyのDashBoardでRedirect URlを設定しますが、ここで設定したURlとXcodeで設定するURlを間違えると認証に失敗して以下のようなエラーで返ってくるので注意してください
エラー:
AppRemote: Failed to authenticate with the Spotify app.
AppRemote: Failed to establish a sesssion with error: Error Domain=com.spotify.app-remote.wamp-client Code=-1001 "wamp.error.authorization_failed" UserInfo={details={
message = "No token provided.";

スクリーンショット 2020-03-15 18.16.35.png

Xcodeで設定するところは、Projectファイル→Targetsのアプリ名→info→URL Typesにあります。
スクリーンショット 2020-03-15 18.24.03.png

ViewController

ViewControllerでは主に以下を行っています。
-Spotifyへの認証

アプリを起動したときは以下の画面になっています。
Spotifyとの接続の状態を示すために右上に色のついたボタンを付けました。(赤: 未接続、青: 接続中、緑: 接続済)
今回はそのボタンを押すことでSpotifyへの認証ができるようにしました。
IMG_6280.PNG

ViewController.swift
   // trackIdentifierは実際の曲のIDでも良い(Spotify URI) ""の場合は前回再生した曲を取得する
   private let trackIdentifier = ""

   // ボタンを押した時のアクション
   @objc func auth() {
        if appRemote?.isConnected == false  {
            print("ViewController: appRemote.is not Connected")
            // authorizeAndPlayURIでSpotifyが端末にインストールされているか認証ができるかを試す
            // asRadioで取得後にその曲を再生するかどうかを決める
            if appRemote?.authorizeAndPlayURI(trackIdentifier, asRadio: true) == false {
                // Spotifyがインストールされていないときは、インストール画面を出す
                showAppStroreInstall()
            }
            else {
                print("ViewController: appRemote: \(appRemote?.isConnected)")
            }
        }
        else {
            print("ViewController: appRemote?.isConnected == false: && appRemote?.playerAPI != nil")
        }
    }

上を実行することで、Spotifyへと遷移します。

これに同意することで認証が完了して、アプリに戻ってきます。

戻ってきた際に、前述したSceneDelegateのメソッドが呼ばれて、AccessTokenが取得されます。

SceneDelegate.swift
   func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {}

IMG_6282.PNG
これでアプリ上でSpotifyの情報が取得できるようになりました!
ここまでできれば、あとはサンプルプロジェクトを見ながら適宜作っていけるかと思いますが、今回作成したアプリで使用しているものメソッド等を以下にまとめます。

再生中の曲を取得
ViewController.swift
  func fetchPlayerState() {
     appRemote?.playerAPI?.getPlayerState({ [weak self] (result, error) in
        guard let self = self else { return }
        if let error = error {
                print("ViewController: fetchPlayerState is failure: error: \(error)")
        }
        else {
            guard let state = result as? SPTAppRemotePlayerState else {
                print("ViewController: fetchPlayerState: state is nil")
                return
            }
            print("ViewController: fetchPlayerState: state: \(state)")
            DispatchQueue.main.async {
                self.updateUserInfo(with: state) 
            }
        }
      })
  }

  func fetchImage(track: SPTAppRemoteTrack, handler: @escaping (UIImage) -> Void) {
     appRemote?.imageAPI?.fetchImage(forItem: track, with: imageSize, callback: { (result, error) in
        if let error = error {
             print("ViewController: fetchImage is failure: error: \(error)")
        }
        else {
             guard let image = result as? UIImage else { return }
             handler(image)
        }
     })
  }

  func updateUserInfo(with item: SPTAppRemotePlayerState) {
     songNameLabel.text = item.track.name
     artistNameLabel.text = item.track.artist.name
     fetchImage(track: item.track) { [weak self] (image) in
       guard let self = self else { return }
            self.artworkImage.image = image
     }

     isPause = item.isPaused
     view.setNeedsLayout()
  }

曲の操作
ViewController.swift
  func changeSongStatus() {
       if isPause {
           appRemote?.playerAPI?.pause(defaultCallBack) // 再生
       }
       else {
           appRemote?.playerAPI?.resume(defaultCallBack) // 一時停止 
       }
   }

   @objc func didTappedNextButton() {
       appRemote?.playerAPI?.skip(toNext: defaultCallBack) // 次の曲に進む
   }

   @objc func didTappedReturnButton() {
       appRemote?.playerAPI?.skip(toPrevious: defaultCallBack) // 一つ前に戻る
   }

   @objc func didTappedPlayButton() {
       isPause = !isPause
       changeSongStatus()
   }

Subscribeで曲変更を受け取る

Spotify iOS SDKではSubsribeが用意されており、状態の変更を簡単に受け取ることができます。

ViewController.swift
    // これでSubsribeを走らせることができる
    func subscribePlayerState() {
        appRemote?.playerAPI?.subscribe(toPlayerState: defaultCallBack)
    }
    // 解除はこれ
    func unSubsribePlayerState() {
        appRemote?.playerAPI?.unsubscribe(toPlayerState: defaultCallBack)
    }

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // SPTAppRemotePlayerStateDelegateで曲(SPTAppRemotePlayerState)の変更を取得できる
  extension ViewController: SPTAppRemotePlayerStateDelegate {
    func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
        updateUserInfo(with: playerState)
    }
  }

以上をアプリに使用しています。
他の細かい実装についてはGithubにコードを載せていますので、そちらをご覧ください。

調査が必要なもの

・アプリを操作しない状態が続くと認証が切れる。その度に認証の為にSpotifyに遷移する動きをする。
認証が切れた時に自動的に認証をするような動きがないか。
・認証中に以下のエラーが発生して、認証に失敗する
AppRemote: Failed connection attempt with error: Error Domain=com.spotify.app-remote.transport Code=-2000 "Stream error." UserInfo={NSLocalizedDescription=Stream error.,
Spotifyを再起動させると再認証すると直ることがある。

最後に

自分は認証のところでかなり詰まってしまいましたが、それ以外はすんなりと進むことができました。
これを組み合わせて色々アプリが作れそうですね!
また、何か間違っている箇所等ございましたら、何なりとお申し付けください。修正させていただきます。

ご覧いただきありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オンラインSwiftコード生成ツールをつくった

概要

ブラウザで利用できるSwiftコード生成ツールを作成しました。
https://shtnkgm.github.io/SwiftCodeGenerator/

↓こんなの

SwiftでTDDを進めるにあたり、Mockとなるインスタンスの生成コードを書くのが大変だっため、Webツールを作成しました。
SourceryやPure Swiftでやる方法も検討しましたが、Vue.jsでコードも書きたかったため、Web技術で作成しました。

できること

以下のような型定義を入力フォームにコピペすると、

struct Book {
    let price: Int
    let title: String
}

以下のようなコードを出力します。

メンバーワイズイニシャライザ

extension Book {
    init(
        price: Int,
        title: String
    ) {
        self.price = price
        self.title = title
    }
}

ファクトリメソッド(適当な値でインスタンスを生成するmakeメソッド)

extension Book {
    static func make(
        price: Int = 0,
        title: String = ""
    ) -> Book {
        return Book(
            price: price,
            title: title
        )
    }
}

Codableに準拠するための実装

extension Book: Codable {
    enum CodingKeys: String, CodingKey {
        case price
        case title
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        price = try container.decode(Int.self, forKey: .price)
        title = try container.decode(String.self, forKey: .title)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(price, forKey: .price)
        try container.encode(title, forKey: .title)
    }
}

Equatableに準拠するための実装

extension Book: Equatable {
    static func == (lhs: Book, rhs: Book) -> Bool {
        return
            lhs.price == rhs.price &&
            lhs.title == rhs.title
    }
}

構成

コード生成処理はクライアントサイドで行うため、かなりライトな構成です。

試してみてください

β版で不具合や足りない点もあるかと思いますが、活用いただけたら幸いです。

Swift Code Generator
https://shtnkgm.github.io/SwiftCodeGenerator/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

まだStoryboardの多言語対応で消耗してるの?ローカライズの最強ベストプラクティス対応法

こんにちは。もぐめっとです。

Qiita初投稿です。

先日、大先輩のfmtonakai大師匠からすごい目からウロコのstoryboardのローカライズ対応について教えてもらいました。
普段は自分のブログに書いているのですが、あまりにも目からウロコ過ぎたのでQiitaで共有しておきます。

storyboardにローカライズのキーを指定できるようにする

UILabel+Extension.swift
extension UILabel {
    @IBInspectable
    private var localizedKey: String? {
        get { fatalError("only set this value") }
        set {
            if let newValue = newValue {
                text = newValue.localized()
            }
        }
    }
}
String+Extension.swift
extension String {
    func localized() -> String? {
        return NSLocalizedString(self, comment: "")
    }
}

こう書いておくだけで、Android Studioのようにstoryboardからキーを設定してローカライズすることができるようになります。

image.png

すごい!画期的!!

ローカライズのキーtypoを防ぐ その1

このままだと、typoとかしたときにローカライズのキーがそのまま表示されてしまうのでローカライズ漏れに気づけるように改良してみました。

String+Extension.swift
extension String {
    private static let localizedEmptyKey = "##not exists##"
    func localized() -> String {
        let string = NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: String.localizedEmptyKey, comment: "")
        if string == String.localizedEmptyKey {
            fatalError("not exists localized key")
        }
        return string
    }
}

キーが無かった場合には強制的にアプリを落としてしまうことによってキーの漏れに気づけるようになりました。

ローカライズのキーtypoを防ぐ その2

その1の対応だと、画面を表示したときでないとtypoに気づけません。

そこで、ビルド時にキーのチェックをしてtypoに気づけるようにさらに改良してみました。

Build Phasesに下記スクリプトを追加するだけ!

RunScript.sh
#!/bin/bash

for file in `\find . -name \*.storyboard`; do
    IFS=$'\n'
    for xmlKey in `\grep 'keyPath="localizedKey"' ${file}`; do
        localizedKey=`echo $xmlKey | sed -e 's/.* keyPath="localizedKey" value="\([0-9a-zA-Z_-]*\)".*/\1/g'`
        for localizedStringFile in `\find ${SRCROOT} -name Localizable.strings`; do
            grep "\"${localizedKey}\" =" $localizedStringFile > /dev/null 2>&1
            if [ $? != 0 ]; then
                echo "not exists key '${localizedKey}' in ${localizedStringFile}"
                exit 1
            fi
        done
    done
done

storyboardで設定されているlocalizedKeyをひっぱてきてLocalizable.stringsのファイルと突き合わせて存在しなければエラーを吐き出します。

まとめ

Android Studioに比べるとxcodeで至らぬところというのはまだまだありますが、今回の対応でLocalizable.stringsに文言を集約することができるようになり、よりローカライズがしやすくなってとってもウロコな方法でした。

storyboardでのローカライズはこの方法でやって、コードでの動的なローカライズについてはR.swiftやswiftgenなどで対応していって適材適所に使ってローカライズしていけるといいと思います。

今回の検証コードはこちらにおいてあります。

Special Thanks fmtonakai

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NumberConvertibleの紹介

この記事はhttps://academy.realm.io/jp/posts/richard-fox-casting-swift-2/を元にしています。
Swiftもバージョンアップされて当時と状況も変わってきたので、いつくかアップデートしてます。

Swiftは厳格な静的型付け言語です。

そのため

let num1: Int = 1 
let num2: Double = 2.0
let num3: CGFloat = num1 + num2

のような計算は行えません。

しかし、ビット演算や金額処理などを除いてモバイルアプリの数値計算にここまで厳格な数値への型付けは必要でしょうか?

そのため今回は演算子のオーバーロードとプロトコルを活用して、Swiftの型安全性を少し犠牲に簡易的な演算手法を紹介します。

最初に思いつくのが、全ての型同士の演算に対して演算子を定義してしまうと言う手法です。しかしこれでは必要な演算子の数は膨大になり、コンパイラが型を決定するのにかかる時間も長くなってしまいます。

そのためprotocolを使って一般化し、様々な以下のように型同士で演算ができるようにします。

実装

まずは以下のようなprotocolを定義します。

protocol Dividable {
    static func / (lhs: Self, rhs: Self) -> Self
}
protocol NumberConvertible: Numeric, Dividable {
    init (_ value: Int)
    init (_ value: Float)
    init (_ value: Double)
    init (_ value: CGFloat)
}

Numericには除算が定義されてないため、Dividableを定義するととで、除算に対応しています。

そして、各数値型をNumberConvertibleに対応させます。

extension Double  : NumberConvertible {}
extension Float   : NumberConvertible {}
extension Int     : NumberConvertible {}
extension CGFloat : NumberConvertible{
    init(_ value: CGFloat){
        self = value
    }
}

CGFloatにはCGFloatからのイニシャライザは提供されていないので、その部分だけ定義します。

変換用のメソッドとしてconvert()を定義します。

extension NumberConvertible {
    func convert<T: NumberConvertible>() -> T {
        switch self {
        case let x as CGFloat: return T(x)
        case let x as Float: return T(x)
        case let x as Double: return T(x)
        case let x as Int: return T(x)
        default:
            fatalError("NumberConvertible convert cast failed!")
        }
    }
}

最後に各演算子を定義してあげます。

func + <T: NumberConvertible, U: NumberConvertible, V: NumberConvertible>(rhs: T, lhs: U) -> V {
    let v: V = lhs.convert()
    let w: V = rhs.convert()
    return v + w
}
func - <T: NumberConvertible, U: NumberConvertible, V: NumberConvertible>(rhs: T, lhs: U) -> V {
    let v: V = lhs.convert()
    let w: V = rhs.convert()
    return v - w
}
func * <T: NumberConvertible, U: NumberConvertible, V: NumberConvertible>(rhs: T, lhs: U) -> V {
    let v: V = lhs.convert()
    let w: V = rhs.convert()
    return v * w
}
func / <T: NumberConvertible, U: NumberConvertible, V: NumberConvertible>(rhs: T, lhs: U) -> V {
    let v: V = lhs.convert()
    let w: V = rhs.convert()
    return v / w
}

代入演算子も

func += <T: NumberConvertible, U: NumberConvertible>(rhs: inout T, lhs: U)  {
    rhs = rhs.convert() + lhs.convert()
}
func -= <T: NumberConvertible, U: NumberConvertible>(rhs: inout T, lhs: U)  {
    rhs = rhs.convert() - lhs.convert()
}
func *= <T: NumberConvertible, U: NumberConvertible>(rhs: inout T, lhs: U)  {
    rhs = rhs.convert() * lhs.convert()
}
func /= <T: NumberConvertible, U: NumberConvertible>(rhs: inout T, lhs: U)  {
    rhs = rhs.convert() / lhs.convert()
}

最後にOptional型に対して ?? を定義し

簡易変換用に独自演算子 ~~ を定義します。

func ?? <T: NumberConvertible, U: NumberConvertible, V:NumberConvertible>(lhs: T?, rhs: U) -> V {
    let v: V? = lhs?.convert()
    return v == nil ? rhs.convert() : v!
}

prefix operator ~~
prefix func ~~<T: NumberConvertible, U: NumberConvertible>(lhs: T) -> U {
    return lhs.convert()
}

以上で完成です。

使い方

let num1: Int = 2
let num2: Double = 3.14
let num3: CGFloat = num1 + num2
let view = UIView()
let width: Int = 320
let height: Double = 100.5

view.frame.size.width = ~~width
view.frame.size.height = ~~height

速度の調整などをしたものは
https://github.com/ObuchiYuki/NumberConvertible/blob/master/NumberConvertible.swiftにあります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

`array.sorted(by: key)` 的なことがしたかったんだ

概要

キーを指定するだけでサクッとソートしてくれるようなextensionを作ってみた

実装

Sequence+KeySort.swift
extension Sequence {
    public func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
        sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
    }

    public func sorted<T: Comparable>(by keyPath: KeyPath<Element, T?>) -> [Element] {
        sorted {
            guard let l = $0[keyPath: keyPath],
                let r = $1[keyPath: keyPath] else { return false }
            return l < r
        }
    }
}

Usage

struct Person: CustomStringConvertible {
    var id: Int
    var name: String

    var description: String { name }
}

let people: [Person] = [
    .init(id: 3, name: "Bob"),
    .init(id: 1, name: "Emma"),
    .init(id: 4, name: "Amelia"),
    .init(id: 2, name: "George"),
]

print(people.sorted(by: \.id))   // -> ["Emma", "George", "Bob", "Amelia"]
print(people.sorted(by: \.name)) // -> ["Amelia", "Bob", "Emma", "George"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vaporのboot.swiftって何書くの

Application boot

DocsのApplicationの説明にboot.swiftは「アプリケーションにアクセスする最適な場所」と書いてありますが、「your code here」の部分には何を書くのでしょう?

import Vapor

public func boot(_ app: Application) throws {
    // your code here
}

考察

まったくわかりませんので、Web検索してみました。

検索結果1.「未処理のオーダーをループしてすべて処理する」

オーダーを処理するシステムで、起動時に溜まっているオーダーを処理してから始めるのに、ここで処理するらしい。

app.make(_:)

※「_:」は外部名なしで1つのパラメーターを取るということらしい。

検索結果2.「リピートのタイマーを起動する」

常に起動しているタイマーをここで起動している。

app.eventLoop.scheduleTask

検索結果3.「外部のAPIへPOSTする」

外部へ何かを通知している。

app.client()

注意

Vaporはアプリケーションへの静的アクセスをサポートしない。

結論

bootは、アプリケーションを起動するたびに実行するコードを書くところということで、サーバーの状態を確認するコードを追加するのが良さそう。
あまり思い浮かびませんが、以下のような事を実装するかなと思います。

  • サーバーのバージョン確認
    • 新しいバージョンがリリースされているかを確認して通知する
  • ライセンス確認
    • サーバーがライセンス下で動作している場合にライセンスの状態を確認する
  • サーバーの状態確認
    • 前回終了時の内容
    • DBなどの外部データへの接続可否

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Background URLSession

この記事は

iOSで利用可能なバックグラウンド処理の1つである「URLSessionのバックグラウンドモード」 についてのまとめと挙動の検証です。

機能概要

Background URL Session

  • 特徴
    • URLSessionの実行モードの1つ
    • 通信を開始した後バックグラウンドに移行しても、継続して通信処理を実行させ続けることができる
    • アプリがフォアグラウンドのままでも処理を実行することはできる
  • 実行タイミング
    • タスクの実行命令を出した直後
  • 実行可能時間
    • 環境によって変わる(明記されている公式ドキュメントを見つけることができなかった)
      • UIApplication.shared.backgroundTimeRemaining で取得できる
      • 検証環境(iPhone11Pro iOS13)では30secだった
  • 所感
    • Backgorund Task Completion を使ってもある程度同じようなことは実現できるので、正直あまり使い所が思い浮かばなかった
    • 後述する Discretionary Background URL Session の方が使い所がありそう

Discretionary Background URL Session

  • 特徴
    • Background URL Sessionの実行オプションの1つ
      • URLSessionConfigurationの isDiscretionary をtrueに指定することで、このモードにすることができる
    • 通信の開始タイミングを遅延させることができるので、即時で必要でない処理を先延ばしできる
    • 実行リクエストはフォアグラウンドで行うが、タスクの実行自体はアプリとは別のバックグラウンドプロセスで行われる
    • アプリが停止状態の時に処理が完了しても、システムがバックグラウンドでアプリを再開または起動してくれる
      • この挙動を実現するには sessionSendsLaunchEventsがtrueになっている必要がある
      • ※ ユーザーによって明示的にアプリがkillされていた場合は実行されない
  • 実行タイミング
    • 正確に指定することはできず、ある程度の実行開始条件を事前に与えておくことしかできない
    • 与えられた条件を考慮して、システムが最適なタイミングを判断して処理を実行する
  • 実行可能時間
    • Background URL Sessionと同じ
  • 所感
    • アプリがフォアグラウンド状態の時に、システムリソースを消費させてまで行いたくない処理を実行するのに適している
    • 確認した限りでは簡単にデバッグする方法は特に提供されていなさそうだった
      • 「スケジューリングしたあと実行されるのを待つ」しかなさそうなので、テストが辛そう

サンプルコード

https://github.com/chocoyama/BackgroundSamples/blob/master/BackgroundSample/Views/URLSessionView.swift
https://github.com/chocoyama/BackgroundSamples/search?q=handleEventsForBackgroundURLSession&unscoped_q=handleEventsForBackgroundURLSession

URLSessionDownloadTask を利用しています

Background URL Session の場合

バックグラウンドのセッションを作成・実行する。この時、クロージャではなくdelegateで設定を行う。

let config = URLSessionConfiguration.background(withIdentifier: UUID().uuidString)
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

session.downloadTask(with: URLRequest(url: url)).resume()

ダウンロード処理が完了すると、下記のDelegateメソッドが呼び出される。
ダウンロードされたデータは、引数に受け渡される location のURLに配置されたファイルから参照することができる。
このデータはメソッドの終了と共に利用できなくなるので、メソッド外でも利用したい場合は別のファイルに退避させるなどの対応が必要になる。

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    let jsonString = try! String(contentsOf: location)
    NotificationHelper.postLocalNotification(with: Message(body: jsonString))
}

Discretionary Background URL Session の場合

実行が遅延されるため、実行時にはすでにアプリが停止されている可能性がある。
そのため、フォアグラウンド時に生成したセッションがすでに失われている可能性があり、セッションを再生成しないとリクエスト時に設定していたdelegate処理を実行することができない。
※ 処理自体はバックグラウンドの別プロセスで実行されているので、正しくセッションの復帰を行えば、設定したdelegate処理を実行させることができる。

以下の対応を行うことで、通信完了時などに呼び出されるdelegate処理をバックグラウンド時でも実行させられる。

  1. URLSessionの復帰
  2. システムへの復帰処理完了通知

1.URLSessionの復帰

UIApplicationDelegateには、 handleEventsForBackgroundURLSession というメソッドが定義されている。
このメソッドはバックグラウンドで通信処理が完了した後、システムがアプリを起動して呼び出すもの。
引数としてセッションIDが受け渡されるので、これを用いて再度URLSessionを起動してセッションの再生成を行うことができる。

※ URLSessionの再生成処理は必ずしもこのメソッド内で行う必要はない。
別の起動ロジックの中で生成時と同一のSessionIDでURLSessionを起動している箇所があれば、そちらで再生成を担保することも可能。

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    self.backgroundCompletionHandler = completionHandler

    // 必要に応じてここで再生成する
    // let config = URLSessionConfiguration.background(withIdentifier: "some unique identifier")
    // let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}

また、ここで受け渡される completionHandler は、実行後にURLSessionのdelegateメソッド(didFinishDownloadingToLocation など)が呼び出されることになる。
そのため、URLSessionの再生成処理が終わった段階で呼び出す必要がある。
別のクラスなどでURLSessionの再生成を行っている場合は、一旦プロパティなどに保持しておくことで後からこの完了ハンドラを呼び出せるようする。
その後、再生成が完了したタイミングで呼び出すことで、想定した動作にすることができる。

2. システムへの復帰処理完了通知

特定のURLSessionに関する全てのイベントが実行されたあとは、 NSURLSessionDelegateurlSessionDidFinishEvents が呼び出される。
このタイミングではURLSessionの再生成処理が終わっているので、保持しておいた handleEventsForBackgroundURLSession の完了ハンドラを実行する。
完了ハンドラが呼び出されたあとは didFinishDownloadingToLocation が呼び出されるので、通常のBackgroundURLSessionと同一の処理を実行すれば良い。

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
        appDelegate.backgroundCompletionHandler?()
        appDelegate.backgroundCompletionHandler = nil
    }
}

検証

準備

検証環境
iPhone11Pro iOS13.3.1 (実機)
iPhone11ProMax iOS13.3.1 (Simulator)

ローカルに簡易的なAPIサーバーをたてて重たいAPIをシミュレートしながら検証を行った。

// Express
router.get('/', function(req, res, next) {
  setTimeout(() => {
    res.send(JSON.stringify({'name': 'SampleName'}));
  }, 10000);
});

結果

↓のサンプルコードで実行した結果です
https://github.com/chocoyama/BackgroundSamples/blob/master/BackgroundSample/Views/URLSessionView.swift

フォアグラウンドモードで通信開始

  • 実行直後にバックグラウンドに移行した場合、バックグラウンド状態では処理が停止された
  • ただし、すぐにフォアグラウンドに復帰させると、中断されていた処理が再開する挙動になった

バックグラウンドモードで通信開始

  • 実行直後にバックグラウンドに移行しても、バックグラウンド状態で処理が継続された

バックグラウンドモード & isDiscretionary=trueで通信開始

  • 実行直後にバックグラウンドに移行した時、処理が遅延されたことが確認できた(すぐに実行されなかった)
  • バックグラウンドに移行後、フォアグラウンドに復帰しても処理は実行されなかった
  • スケジューリングをしたあと、しばらく待つと通信処理が実行されたことが確認できた(実機検証)

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エンジニアの貴方必見

https://www.reiwarss.com/OpenProject

Top tags
python
swift
javascript
go
C
C++
C#
Ruby
TypeScript
PHP

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む