20200619のiOSに関する記事は3件です。

ウォークスルー後の通知許可とフォアグラウンド 通知を両立しようとしたら,うまくいかなかった件について

TL;DR

  • よくあるチュートリアルの後に通知許可の処理をしたらアプリ内通知が動かなかった
  • これをするときはappDelegateにフォアグラウンド処理を書いてもダメ
  • 通知許可のviewControllerにフォアグラウンド処理を書こう

何があったのか(前提条件)

  • appDelegateのapplication(_ application: UIApplication, didFinishLaunchingWithOptions)に通知許可の処理を書かない -> アプリ立ち上げでは通知許可は出さない
  • ウォークスルー終了後のメイン画面で通知許可を記述.
  • フォアグラウンド通知のためのfunc userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification:〜以下略)はappDelegateに書いていた.

実行環境

機材 バージョン
Xcode 11.5
iOS 13.5.1
Swift 5.2.4

解決方法

上に書いてあるとおり,appDelegateには通知系の処理を書かず,通知許可を取りたい画面のコードに,

MainViewController.swift
import UIKit
import UserNotifications

class MainViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        //通知許可
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] (granted, _) in
            if granted {
                let center = UNUserNotificationCenter.current()
                center.delegate = self
            }
        }
    //~~~~~~
    (中略)
    //~~~~~~
}

extension MainViewController: UNUserNotificationCenterDelegate {
    //フォアグラウンド通知処理
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                    willPresent notification: UNNotification,
                                    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        //通知とサウンドのみ
        completionHandler([.alert, .sound])
    }
}

と記述する.

最後に

なんでこんな仕様なんだろう・・・
原理や「お前の勘違いや」,「もっとこうすべき」などのご指摘がございましたらコメントにてよろしくお願いいたします.

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

SwiftでiOS開発している時に、Timerを利用したコードを、モダンでいい感じにUnitTestする方法

0. 初めに

画面タップイベントや、APIアクセスといった処理は、(ユーザーが連打するなどの場合を除けば)多くの場合一度きりの処理であることが多く、これらのロジックをUnitTestでテストするのは、そこまで大変ではないケースが多くを占めると思います。

しかしながら、一度きりではない処理を実装しないといけない(画面上にカウントダウンを表示させる場合など)ケースがあるのもまた事実かと思います。

いざTimerを使ったコードを書いてみると、実際に動かす場合はまぁよいにしろ、UnitTestを書くところで、

「どうやってテストしたらいいんだろう? :thinking: まさか待つわけにはいかないしな...」

といった感じで、思った以上にTimer部分のテストを書くのが難しく、Timer部分のUnitTestをスキップしてしまうことがあったり・・・するのではないでしょうか???

この記事では、そんなTimerのUnitTestが難しいなと思っている皆さんに向けて、僕はこんなふうにやったよという感じで、僕なりのテスト方法を提案しようと思います。

大まかに分けて下記のような流れで進めていきます。

  1. ApplicationTimerProviderProtocol を作る
  2. Timer そのものではなく、 ApplicationTimerProviderProtocol に依存するようにする(差し替え可能にする)
  3. UnitTest用の FakeApplcationiTimerProvider を作る
  4. 実際のテストを書いてみる(ここではQuick/Nimbleを利用したコードを載せます)

1. ApplicationTimerProviderProtocol を作る

UnitTestが書きやすいプログラムとは外部から差し替え可能なクラスに依存しているクラスである、と僕は思います。

Timerを使おう〜!と思って下記のようにTimerを使ってしまっても実際の動きとしては良いのですが、Testコードを書こうとすると、TimeInterval分待つしか無くなってしまい、綺麗なUnitTestが書けないことになります。

import Foundation

final class TimerClass {
    var timer: Timer?

    func check() {
        timer = Timer.scheduledTimer(
            withTimeInterval: 1.0,
            repeats: false,
            block: { timer in
                // do something...
        })
    }
}

実際にTimeInterval分待たなくてはならない」問題は、上記のプログラムが Timer 自体に依存しているため発生しています。

そのため、下記のような ApplicationTimerProviderProtocol を作成し、 Timer への依存を外しましょう。

protocol ApplicationTimerProviderProtocol {
    func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer
}

struct ApplicationTimerProvider {
}

extension ApplicationTimerProvider: ApplicationTimerProviderProtocol {
    func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer {
        return Timer.scheduledTimer(withTimeInterval: interval, repeats: repeats, block: block)
    }

}

2. ApplicationTimerProviderProtocol に依存させる

作成した、 ApplicationTimerProviderProtocol を利用することで、UnitTestの時に差し替えを行うことが可能になります。

UnitTestの時に差し替えを行うため、作成した ApplicationTimerProvider を利用するように TimerClass のコードを下記のように修正します、

final class TimerClass {
    let timerProvider: ApplicationTimerProviderProtocol
    var timer: Timer?

    init(timerProvider: ApplicationTimerProviderProtocol) {
        self.timerProvider = timerProvider
    }

    func check() {
        timer = Timer.scheduledTimer(
            withTimeInterval: 1.0,
            repeats: false,
            block: { timer in
                // do something...
        })
    }
}

3. UnitTest用の FakeApplcationiTimerProvider を作る

下記のようにUnitTestの際に利用する FakeApplicationTimerProvider を作成します。

class FakeApplcationiTimerProvider: ApplcationiTimerProviderProtocol {
    final class DummyTimer: Timer {
        override func invalidate() { }
    }

    var blocks = [(Timer) -> Void]()
    var scheduledTimer_callCount = 0

    func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer {
        scheduledTimer_callCount += 1

        blocks.append(block)

        return DummyTimer()
    }

    var fire_callCount = 0
    func fire()  {
        fire_callCount += 1

        guard let first = blocks.first else { return }

        first(DummyTimer())
        _ = blocks.removeFirst()
    }
}

4. 実際のテストを書いてみる

ここまでくれば下記のように、UnitTestを書くことができるようになっていると思います。

class TimerClassSpec: QuickSpec {
    override func spec() {
        var fakeTimerProvider: FakeApplcationiTimerProvider!
        var subject: TimerClass!

        beforeEach {
            fakeTimerProvider = FakeOrderAppliTimerProvider()
            subject = TimerClass(timerProvider: fakeTimerProvider)
        }

        it("check") {
            subject.check()


            fakeTimerProvider.fire()


            // ここまでくればクロージャーが発火しているのでテストをすることができる
        }

    }
}

終わりに

Timerを利用したことは何度かあったのですが、上手なUnitTestの書き方がわからず、悩んでおりました。

僕なりのテスト方法ではありますが皆様のお力になれれば幸いです。

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

WKWebView オブジェクトの破棄後にデバッグ領域に "Could not find specified service" というメッセージが出力される

概要

WKWebView オブジェクトを破棄してから30秒ほど経過したら、以下の2つのエラーメッセージが Xcode のデバッグ領域に出力されます。

Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service
Could not signal service com.apple.WebKit.Networking: 113: Could not find specified service

このメッセージが出力される原因について調査したところ、いろいろな情報を見つけたのですが、Apple Developer Forums のページで Apple Staff が返信しているものが最も信頼できる情報であると判断しました。そのページによると、このメッセージについては特に気にする必要はないとのことです。

確認環境

ビルド環境

  • macOS 10.15.5
  • Xcode 11.5 (11E608c)
  • Deployment Target: iOS 13.0

実行環境

  • iPhone 11 Pro Max (iOS 13.5.1)

Apple Developer Forums での情報

下記リンク先の Apple Developer Forums のページで Apple Staff が返信しています。

com.apple.WebKit errors after deallocating WKWebView |Apple Developer Forums

返信内容の一部を抜粋します。

These are two XPC Services used by the web view to implement core functionality (page rendering and networking, respectively).
I suspect what’s going on here is that the web view has shut down and, 30 seconds later, its XPC Services are shut down.  That does not seem particularly worrying in and of itself.

この返信を読む限りでは、 com.apple.WebKit.WebContentcom.apple.WebKit.Networking についての 113: Could not find specified service は特に気にする必要はなさそうです。

その他の情報

他の情報源の中には、 次のリンク先の記事のように、Xcode プロジェクトの設定や WKWebView の扱いに問題がある可能性に言及しているものもありました。

Xcode - Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service とは?|teratail

このリンク先のページでは、 com.apple.WebKit.WebContentcom.apple.WebKit.Networking についての 113: Could not find specified service の原因として、以下2つを挙げています。

  1. addSubview をする前に loadHTMLString を呼び出していた
  2. Info.plistNSAppTransportSecurity を設定せずに http のURLに接続していた

今回はこれらの問題がない状態でもエラーメッセージが出力されていたので、最終的には上の Apple Developer Forums のページが最も信頼できる情報であると判断しました。けれども、原因として挙げられているこれら2つについても、これはこれで気をつけるべき点かもしれませんので、本記事で紹介しておきます。

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