- 投稿日:2020-03-17T22:40:24+09:00
エンジニア向けオープンプロジェクトトレース
https://www.reiwarss.com/OpenProject
Top tags
python
swift
javascript
go
C
C++
C#
Ruby
TypeScript
PHP
- 投稿日:2020-03-17T22:38:14+09:00
Spotify SDK for iOSで簡単なアプリを作ってみた
はじめに
こんにちは
渋谷でiOSエンジニアとして働いているChikatoです!自作アプリを作る過程でSpotify SDKを使ってみたくて、公式のチュートリアルとサンプルプロジェクトを参考にしましたが詰まってしまったところも多く、また日本語の資料も少ない(あっても少し古い)のでまとめてみたいと思います!
また、自分が詰まってしまったポイントも合わせて記載致します。
これからSpotify SDK iOSを使われる方のご参考になれば幸いです!サンプルアプリとして、簡単に以下のアプリを作ってみました
このアプリでは、Spotifyで再生している曲の取得と、スキップができます。
コード: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.swiftprivate 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.";Xcodeで設定するところは、Projectファイル→Targetsのアプリ名→info→URL Typesにあります。
ViewController
ViewControllerでは主に以下を行っています。
-Spotifyへの認証アプリを起動したときは以下の画面になっています。
Spotifyとの接続の状態を示すために右上に色のついたボタンを付けました。(赤: 未接続、青: 接続中、緑: 接続済)
今回はそのボタンを押すことでSpotifyへの認証ができるようにしました。
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.swiftfunc scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {}
これでアプリ上でSpotifyの情報が取得できるようになりました!
ここまでできれば、あとはサンプルプロジェクトを見ながら適宜作っていけるかと思いますが、今回作成したアプリで使用しているものメソッド等を以下にまとめます。再生中の曲を取得
ViewController.swiftfunc 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.swiftfunc 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を再起動させると再認証すると直ることがある。最後に
自分は認証のところでかなり詰まってしまいましたが、それ以外はすんなりと進むことができました。
これを組み合わせて色々アプリが作れそうですね!
また、何か間違っている箇所等ございましたら、何なりとお申し付けください。修正させていただきます。ご覧いただきありがとうございました!
- 投稿日:2020-03-17T21:47:23+09:00
オンライン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 } }構成
コード生成処理はクライアントサイドで行うため、かなりライトな構成です。
- JSフレームワーク: Vue.js
- CSSフレームワーク: UIkit JS
- ホスティング: GitHub Pages
試してみてください
β版で不具合や足りない点もあるかと思いますが、活用いただけたら幸いです。
Swift Code Generator
https://shtnkgm.github.io/SwiftCodeGenerator/
- 投稿日:2020-03-17T18:26:00+09:00
まだStoryboardの多言語対応で消耗してるの?ローカライズの最強ベストプラクティス対応法
こんにちは。もぐめっとです。
Qiita初投稿です。
先日、大先輩のfmtonakai大師匠からすごい目からウロコのstoryboardのローカライズ対応について教えてもらいました。
普段は自分のブログに書いているのですが、あまりにも目からウロコ過ぎたのでQiitaで共有しておきます。storyboardにローカライズのキーを指定できるようにする
UILabel+Extension.swiftextension UILabel { @IBInspectable private var localizedKey: String? { get { fatalError("only set this value") } set { if let newValue = newValue { text = newValue.localized() } } } }String+Extension.swiftextension String { func localized() -> String? { return NSLocalizedString(self, comment: "") } }こう書いておくだけで、Android Studioのようにstoryboardからキーを設定してローカライズすることができるようになります。
すごい!画期的!!
ローカライズのキーtypoを防ぐ その1
このままだと、typoとかしたときにローカライズのキーがそのまま表示されてしまうのでローカライズ漏れに気づけるように改良してみました。
String+Extension.swiftextension 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 donestoryboardで設定されているlocalizedKeyをひっぱてきてLocalizable.stringsのファイルと突き合わせて存在しなければエラーを吐き出します。
まとめ
Android Studioに比べるとxcodeで至らぬところというのはまだまだありますが、今回の対応でLocalizable.stringsに文言を集約することができるようになり、よりローカライズがしやすくなってとってもウロコな方法でした。
storyboardでのローカライズはこの方法でやって、コードでの動的なローカライズについてはR.swiftやswiftgenなどで対応していって適材適所に使ってローカライズしていけるといいと思います。
今回の検証コードはこちらにおいてあります。
Special Thanks fmtonakai
- 投稿日:2020-03-17T16:44:00+09:00
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 + num2let 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にあります。
- 投稿日:2020-03-17T14:32:30+09:00
`array.sorted(by: key)` 的なことがしたかったんだ
概要
キーを指定するだけでサクッとソートしてくれるようなextensionを作ってみた
実装
Sequence+KeySort.swiftextension 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"]
- 投稿日:2020-03-17T12:14:02+09:00
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などの外部データへの接続可否
以上
- 投稿日:2020-03-17T11:54:09+09:00
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処理をバックグラウンド時でも実行させられる。
- URLSessionの復帰
- システムへの復帰処理完了通知
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に関する全てのイベントが実行されたあとは、
NSURLSessionDelegate
のurlSessionDidFinishEvents
が呼び出される。
このタイミングでは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で通信開始
- 実行直後にバックグラウンドに移行した時、処理が遅延されたことが確認できた(すぐに実行されなかった)
- バックグラウンドに移行後、フォアグラウンドに復帰しても処理は実行されなかった
- スケジューリングをしたあと、しばらく待つと通信処理が実行されたことが確認できた(実機検証)
参考
- https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background
- https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1411552-discretionary
- https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1407496-background
- https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1617174-sessionsendslaunchevents
- https://academy.realm.io/jp/posts/gwendolyn-weston-ios-background-networking/
- 投稿日:2020-03-17T05:55:28+09:00
エンジニアの貴方必見
https://www.reiwarss.com/OpenProject
Top tags
python
swift
javascript
go
C
C++
C#
Ruby
TypeScript
PHP