プロジェクトを Xcode 12 にアップグレードする時の対応あれこれ

CocoaPods で導入したライブラリのターゲットが iOS 8.0 になっている警告

f:id:daisuke-t-jp:20200918100755p:plain:w400

The iOS Simulator deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0.99.

Xcode 12 からは iOS 9 以降が対象なのでこの警告が出る。

なので Podfile に以下を追加して、明示的に CocoaPods のライブラリのターゲットを 9.0 以降にする。

post_install do |pi|
    pi.pods_project.targets.each do |t|
        t.build_configurations.each do |config|
            config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
        end
    end
end

そして Pods を更新する。

pod update

CocoaPods で導入したライブラリ内でダブルクォートで include しているエラー

- Double-quoted include "pb.h" in framework header, expected angle-bracketed instead
- Double-quoted include "pb_common.h" in framework header, expected angle-bracketed instead

Firebase SDK 内で発生した。

対応方法は、Pods のプロジェクトの BuildSettings で Quoted Include In Framework Header を NO にすると、とりあえずビルドできる。

f:id:daisuke-t-jp:20200918100804p:plain:w400

Firebase iOS SDK の issue をみると、どうやらこの問題は CocoaPods の ver 1.1.0 を使っている場合は解決されているらしい。

ver 1.1.0 未満の場合はこの解決方法をとる必要があるようだ。

https://github.com/firebase/firebase-ios-sdk/issues/5987

A fix is at CocoaPods/CocoaPods#9905 and targeted for CocoaPods > 1.10. I'll close this bug in favor of CocoaPods/CocoaPods#9902.

Use the workaround described above with CocoaPods versions before 1.10

fastlane でスクリーンショット撮影するまでのセットアップ

fastlane snapshot を試してみたのでメモ。

資料

fastlane snapshot 導入のメリット

導入のコストはそんなに大きくなく、得られるメリットの方が大きいとおもう。

  • UITests の実行状態をスクリーンショットで記録できる。
    • UITests はパスした上での問題(更新により前回とデザインが変わってしまった、など)を確認できる。
  • AppStore 提出用のスクリーンショットを自動で撮影できる。

セットアップ

Homebrew 経由でインストール(gem でも良い)

$ brew install fastlane

インストール確認

$ which fastlane

インストールされている場合はパスが表示されるハズ。

プロジェクトのルートで fastlane を初期化

$ fastlane snapshot init

fastlane フォルダができる。その中に以下のファイルができる。

  • Snapfile (設定ファイル)
  • SnapshotHelper.swift
    • UITests のターゲットに追加する。

UITests に撮影コードを追加

UnitTests に以下の記載を追加する。

class UITests: XCTestCase {
    func testExample() throws {
            let app = XCUIApplication()
            setupSnapshot(app)  // ⭐️ fastlane snapshot をセットアップ
            app.launch()
            snapshot("launch")  // ⭐️ ファイル名を指定して撮影
    }
}

Snapfile 編集

Snapfile はスクリーンショット撮影の設定ファイル。ここに必要な設定を書く。

とりあえず、以下のようにして iPhone 11 で英語の状態で撮影されるようにする。

# Uncomment the lines below you want to change by removing the # in the beginning

# A list of devices you want to take the screenshots from
devices([
#   "iPhone 8",
#   "iPhone 8 Plus",
#   "iPhone SE",
#   "iPhone X",
#   "iPad Pro (12.9-inch)",
#   "iPad Pro (9.7-inch)",
#   "Apple TV 1080p"
    "iPhone 11", // ⭐️ iPhone 11 シミュレーターで撮影
])

languages([
    "en-US",  // ⭐️ 言語は en-US だけ
#   "de-DE",
#   "it-IT",
#   ["pt", "pt_BR"] # Portuguese with Brazilian locale
])

# The name of the scheme which contains the UI Tests
scheme("⭐️UITests を含んでいるスキームの名前")

# Where should the resulting screenshots be stored?
output_directory("./screenshots")

# remove the '#' to clear all previously generated screenshots before creating new ones
clear_previous_screenshots(true)

# Remove the '#' to set the status bar to 9:41 AM, and show full battery and reception.
override_status_bar(true)

# Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments
# launch_arguments(["-favColor red"])

# For more information about all available options run
# fastlane action snapshot

実行

プロジェクトのルート(fastlane フォルダの中ではない)で以下を実行。

$ fastlane snapshot run

プロジェクトのビルドが始まり、成功すると screenshotフォルダができて、ファイルが作られる。

fastlane のコマンドではなく、Xcode から直接ユニットテストを実行するとスクリーンショットは作成されないことに注意。

スクリーンショット撮影のコンディションを Snapfile で指定したい

たとえば「広告が非表示の状態」で撮影したいとする。

この場合の対応方法のひとつとして、まず Snapfile でアプリの起動引数が指定できるので、以下のように設定する。

# Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments
# launch_arguments(["-favColor red"])
launch_arguments(["-hiddenAd"])  // ⭐️ 起動引数設定

そして設定された起動引数でアプリ内で動作を変更すると、広告が消えた状態で撮影できる。

// 起動引数を確認
if ProcessInfo.processInfo.arguments.contains("-hiddenAd") {
    // 広告を非表示にする
}

// CommondLine クラスを使っても起動引数は確認できる
if CommandLine.arguments.contains("-hiddenAd") {
    // 広告を非表示にする
}

UserDefaults と Keychain を使ったカスタムクラスの永続化

https://github.com/daisuke-t-jp/UserDefaultsAndKeychainSample/

サンプルを作った。以下メモ。

Swift の Codable を使えば、プロパティそれぞれをエンコード、デコードのコードを書く必要なく楽だ。

Keychain の場合は

感じ。

iOS 14 以降の Google AdMob 対応(AppTrackingTransparency Framework)

資料

前提

iOS 14 では AppTrackingTransparency Framework が追加され、ユーザのトラッキング収集はこのフレームワークを介して、ユーザに承認を得る必要がある。

もし iOS 13 以前に IDFA を利用して広告を表示していたアプリがあったとして、iOS 14 で AppTrackingTransparency を使ってユーザの承認を得ない場合は、IDFA が無効になった状態で広告が表示されることになる。 (=収益が下がることが予想される)

Google AdMob で必要な変更

Google AdMob のバージョンを更新する

  • Google Mobile Ads SDK は 7.64.0 以降を使用する

ラッキング利用の承諾をリクエス

Info.plist にトラッキング利用の説明文を追加する

NSUserTrackingUsageDescription を Info.plist に追加する。

<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>

ラッキングの承認をリクエストして、その結果 Google AdMob を表示する。

import AppTrackingTransparency
import AdSupport
...
func requestIDFA() {
  ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
    // Tracking authorization completed. Start loading ads here.
    // loadAd()
  })
}

ユーザへのトラッキング利用の許諾アラートに説明文が表示される。 f:id:daisuke-t-jp:20200918092200p:plain:w300

iOS 14 でトラッキング利用をユーザに拒否されたら?

Google AdMob ではトラッキング広告は表示できない。

そういう時は、かわりに Google AdMob は SKAdNetwork を使用して、アプリインストールをアトリビューションすることができる↓

https://developers.google.com/admob/ios/ios14?hl=ja#skadnetwork

Apple の住所(地名)検索 API を比較する(iOS)

テキストから住所(地名)を得たい場合 (たとえば、"嵐山" という入力から "京都府京都市右京区" という結果が欲しい)

その用途に使用できそうな AppleAPI は、以下がある。

これらの違いや使い道を確認したかったので、テキスト入力をしてそれぞれの API の結果を一覧表示するサンプルを作った。


調べて使ってみてわかったこと

CLGeocoder のリクエスト制限

CLGeocoder は短時間に大量リクエストを行うと失敗する。(CLError.Code.network

この動作はドキュメントにも明記されている。

Apps must be conscious of how they use geocoding. Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. (When the maximum rate is exceeded, the geocoder returns an error object with the CLError.Code.network error to the associated completion handler.) Here are some rules of thumb for using this class effectively:

ただし、以下の記載があるので、ユーザアクションに紐づいた頻度程度の API 呼び出し自体は考慮されているようだ。

• Send at most one geocoding request for any one user action.

アプリ側で呼び出すタイミングを気を付ければ、あまりエラーになることは無さそうだ。(逆に言えば、ユーザー入力など関係なく、アプリの動作として大量リクエストを送ったりする用途には使えない)

MKLocalSearch

CLGeocoder とは違い、MKLocalSearch はリクエスト制限でエラーにはならない(連続リクエストをしてエラー発生を観測できなかった)

こちらは、現在地と範囲を指定して地図上にある"近場"の Placemark を検索するのが本来の用途のようだ。(LocalSearch なので)

CLGeocoder とはちがい、住所だけではなく、店舗の名前を知りたいならばこちらを使う。

ただし、MapKit の機能なので、地図表示がないアプリで住所検索をしたいために MKLocalSearch だけ使う、というのはレギュレーションに反していないのか、疑問である。

CLGeocoder では複数の結果が得られない?

f:id:daisuke-t-jp:20200822185109p:plain:w300

iOS に入っている Apple の天気アプリではこのように、テキストに対して住所候補が複数あれば、複数表示される。

このような動作を期待して CLGeocoder を使ってみたが、CLGeocoder はレスポンスは配列であるが、結果は常に単一になっていた。

たとえば "中央区" の場合、iOS の天気アプリのように、日本国内にある中央区を複数候補を返して欲しいが、ひとつの中央区しか CLGeocoder は返さない(現在位置から一番近い "中央区" になる?)

この CLGeocoder のレスポンスが一つしか得られない問題は StackoverFlow でも散見されるので、どうにかなる話では無さそうだ。

「候補が複数欲しい」場合は、AppleAPI は使わず他のサービス(GoogleAPI など)を使う必要があるようだ。

OSS の iOS の Web ブラウザアプリで、UserAgent を設定している箇所を見比べてみる。

気になったので、オープンソースiOS の Web ブラウザアプリで、 WKWebview のカスタムユーザーエージェント(customUserAgent) を設定している部分を見てみる。

Chrome

ユーザーエージェントの例

Mozilla/5.0 (iPhone; CPU iPhone OS 13_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/84.0.4147.71 Mobile/15E148 Safari/604.1

UserAgent関連コードを抜粋(これがすべてではない)

std::string BuildOSCpuInfo() {
  std::string os_cpu;
  // Remove the end of the platform name. For example "iPod touch" becomes
  // "iPod".
  std::string platform =
      base::SysNSStringToUTF8([[UIDevice currentDevice] model]);
  size_t position = platform.find_first_of(" ");
  if (position != std::string::npos)
    platform = platform.substr(0, position);
  base::StringAppendF(&os_cpu, "%s; CPU %s %s like Mac OS X", platform.c_str(),
                      (platform == "iPad") ? "OS" : "iPhone OS",
                      OSVersion().c_str());
  return os_cpu;
}

/* 中略 */

std::string BuildMobileUserAgent(const std::string& mobile_product) {
  std::string user_agent;
  base::StringAppendF(&user_agent,
                      "Mozilla/5.0 (%s) AppleWebKit/605.1.15"
                      " (KHTML, like Gecko) %s Mobile/15E148 Safari/604.1",
                      BuildOSCpuInfo().c_str(), mobile_product.c_str());
  return user_agent;
}

Firefox

ユーザーエージェントの例

Mozilla/5.0 (iPhone; CPU OS 13_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/28.0 Mobile/15E148 Safari/605.1.15

以下、UserAgent関連コードを抜粋(これがすべてではない)

    public static let uaBitSafari = "Safari/605.1.15"
    public static let uaBitMobile = "Mobile/15E148"
    public static let uaBitFx = "FxiOS/\(AppInfo.appVersion)"
    public static let product = "Mozilla/5.0"
    public static let platform = "AppleWebKit/605.1.15"
    public static let platformDetails = "(KHTML, like Gecko)"
 
 /* 中略 */

public static func defaultMobileUserAgent() -> UserAgentBuilder {
        return UserAgentBuilder(product: UserAgent.product, systemInfo: "(\(UIDevice.current.model); CPU OS \(UIDevice.current.systemVersion.replacingOccurrences(of: ".", with: "_")) like Mac OS X)", platform: UserAgent.platform, platformDetails: UserAgent.platformDetails, extensions: "FxiOS/\(AppInfo.appVersion)  \(UserAgent.uaBitMobile) \(UserAgent.uaBitSafari)")
    }

DuckDuckGo

ユーザーエージェントの例

Mozilla/5.0 (iPhone; CPU iPhone OS 12_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.4 Mobile/15E148 DuckDuckGo/7 Safari/605.1.15

UserAgent関連コードを抜粋(これがすべてではない)

 struct UserAgent {
    
    private struct Constants {
        // swiftlint:disable line_length
        static let fallbackWekKitVersion = "605.1.15"
        static let fallbackSafariComponent = "Safari/\(fallbackWekKitVersion)"
        static let fallbackDefaultAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_5 like Mac OS X) AppleWebKit/\(fallbackWekKitVersion) (KHTML, like Gecko) Mobile/15E148"
        static let desktopPrefixComponent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15)"
        static let fallbackVersionComponent = "Version/13.1.1"
        // swiftlint:enable line_length
    }

メモ

  • customUserAgentはそれぞれのアプリで基本的には、コード内でリテラルで指定している
  • AppleWebKit, Safari のバージョンもコード内で固定値で記述されている
    • iOSAPI で取得できる内容だと思ったが、そうではないようだ
  • Chrome は CriOS, Firefox は FxiOS, DuckDuckGoDuckDuckGo がそれぞれ設定される。

Xcode 12.0 beta 2 (12A6163b) に含まれる Framework

Xcode 12.0 beta 2 (12A6163b) の iOS platform の Frameworks の中をみると

$ cd 
/Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks

$ ls
ARKit.framework
AVFoundation.framework
AVKit.framework
Accelerate.framework
Accessibility.framework
Accounts.framework
AdSupport.framework
AddressBook.framework
AddressBookUI.framework
AppClip.framework
AppTrackingTransparency.framework
AssetsLibrary.framework
AudioToolbox.framework
AudioUnit.framework
AuthenticationServices.framework
AutomaticAssessmentConfiguration.framework
BackgroundTasks.framework
BusinessChat.framework
CFNetwork.framework
CallKit.framework
CarPlay.framework
ClassKit.framework
ClockKit.framework
CloudKit.framework
Combine.framework
Contacts.framework
ContactsUI.framework
CoreAudio.framework
CoreAudioKit.framework
CoreAudioTypes.framework
CoreBluetooth.framework
CoreData.framework
CoreFoundation.framework
CoreGraphics.framework
CoreHaptics.framework
CoreImage.framework
CoreLocation.framework
CoreMIDI.framework
CoreML.framework
CoreMedia.framework
CoreMotion.framework
CoreNFC.framework
CoreServices.framework
CoreSpotlight.framework
CoreTelephony.framework
CoreText.framework
CoreVideo.framework
CryptoKit.framework
CryptoTokenKit.framework
DeveloperToolsSupport.framework
DeviceCheck.framework
EventKit.framework
EventKitUI.framework
ExposureNotification.framework
ExternalAccessory.framework
FileProvider.framework
FileProviderUI.framework
Foundation.framework
GLKit.framework
GSS.framework
GameController.framework
GameKit.framework
GameplayKit.framework
HealthKit.framework
HealthKitUI.framework
HomeKit.framework
IOKit.framework
IOSurface.framework
IdentityLookup.framework
IdentityLookupUI.framework
ImageCaptureCore.framework
ImageIO.framework
Intents.framework
IntentsUI.framework
JavaScriptCore.framework
LinkPresentation.framework
LocalAuthentication.framework
MLCompute.framework
MapKit.framework
MediaAccessibility.framework
MediaPlayer.framework
MediaSetup.framework
MediaToolbox.framework
MessageUI.framework
Messages.framework
Metal.framework
MetalKit.framework
MetalPerformanceShaders.framework
MetalPerformanceShadersGraph.framework
MetricKit.framework
MobileCoreServices.framework
ModelIO.framework
MultipeerConnectivity.framework
NaturalLanguage.framework
NearbyInteraction.framework
Network.framework
NetworkExtension.framework
NewsstandKit.framework
NotificationCenter.framework
OSLog.framework
OpenAL.framework
OpenGLES.framework
PDFKit.framework
PassKit.framework
PencilKit.framework
Photos.framework
PhotosUI.framework
PushKit.framework
QuartzCore.framework
QuickLook.framework
QuickLookThumbnailing.framework
RealityKit.framework
ReplayKit.framework
SafariServices.framework
SceneKit.framework
ScreenTime.framework
Security.framework
SensorKit.framework
Social.framework
SoundAnalysis.framework
Speech.framework
SpriteKit.framework
StoreKit.framework
SwiftUI.framework
SystemConfiguration.framework
Twitter.framework
UIKit.framework
UniformTypeIdentifiers.framework
UserNotifications.framework
UserNotificationsUI.framework
VideoSubscriberAccount.framework
VideoToolbox.framework
Vision.framework
VisionKit.framework
WatchConnectivity.framework
WebKit.framework
WidgetKit.framework
_AVKit_SwiftUI.framework
_HomeKit_SwiftUI.framework
_MapKit_SwiftUI.framework
_QuickLook_SwiftUI.framework
_SceneKit_SwiftUI.framework
_SpriteKit_SwiftUI.framework
iAd.framework

という感じだった。

AppClips や ScreenTime など iOS 14 のあたらしい framework は含まれているが、Apple Archive はなかった。

macOS Big Sur の framework はこんな感じ。こちらも AppleArchive はない。

$ cd /Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/iOSSupport/System/Library/Frameworks 

$ ls
ARKit.framework
AVKit.framework
AddressBook.framework
AddressBookUI.framework
AssetsLibrary.framework
AuthenticationServices.framework
BusinessChat.framework
CarPlay.framework
ContactsUI.framework
CoreAudioKit.framework
CoreNFC.framework
EventKitUI.framework
GameController.framework
GameKit.framework
GameplayKit.framework
HealthKit.framework
HealthKitUI.framework
HomeKit.framework
IdentityLookupUI.framework
IntentsUI.framework
JavaScriptCore.framework
LinkPresentation.framework
MapKit.framework
MediaPlayer.framework
MessageUI.framework
Messages.framework
MetalKit.framework
MobileCoreServices.framework
MultipeerConnectivity.framework
NetworkExtensioniOSSupport.framework
NewsstandKit.framework
OpenAL.framework
PDFKit.framework
PassKit.framework
PencilKit.framework
PhotosUI.framework
QuickLook.framework
RealityKit.framework
ReplayKit.framework
SafariServices.framework
SceneKit.framework
ScreenTime.framework
Social.framework
SpriteKit.framework
StoreKit.framework
SwiftUI.framework
Twitter.framework
UIKit.framework
UserNotificationsUI.framework
VisionKit.framework
WatchConnectivity.framework
WebKit.framework
WidgetKit.framework
_AVKit_SwiftUI.framework
_MapKit_SwiftUI.framework
_SpriteKit_SwiftUI.framework
iAd.framework