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

本『リーダブルコード』の感想

原題は

The Art of Readable Code: Simple and Practical Techniques for Writing Better Code

である。

邦題では「リーダブルコード」であるが、原題では「The Art of Readable Codes」なので本来は Art の意味もタイトルに含まれている。

その言葉の意味を考えると、これは内容的には、コーディングに関しての Art, 芸術に関する本である。

基本的なところだと、たとえば命名や改行の仕方で、そのコードの読みやすさというは変わる。(命名が適切であり、役割を簡潔に示せているか? 改行された位置により処理が一つのブロックであることを示せているか?)

残されたコードが読みやすく、理解しやすく、あとで読む人(それはすっかりコーディングした当時のことを忘れた自分かもしれない)がストレスなく開発できるかどうか?

そういったコードを通した情報伝達の精度をいかによくするか、という芸術だ。

逆に、この情報伝達がうまくできていないと、コードというダンジョンを、頼りない松明片手に探索する考古学をしなければならない。 (そして運が悪ければダンジョンから出れずに、壁に「yyyy/MM/dd 私は解析に失敗した。ここに眠る ◯◯」とコメントする羽目になる)


本は 200 ページくらいで読みやすい。

「こうしたらコードが読みやすくなる(Readable)」という tips は、自分でコーディングしたり、ほかの人のコードを読んだりして無意識に獲得していくものも多いが (本を読んで、これはやっているなぁ、、という部分もある)、文書にして明文化したことにより、それらが共通知識として残るので価値がある。

個人的には範囲を示すときに start/stop は包括的で、begin/end は終端が排他的なものに使うというのが、なるほどなあ、と思った。

たとえば

2020/01/01 の1日を範囲としたい場合

  • start 2020/01/01 00:00:00
  • stop 2020/01/01 23:59:59

ではなく

  • begin 2020/01/01 00:00:00
  • end 2020/01/02 00:00:00

とできるし、後者の方が自然である。

前者だと厳密に1日の終わりを示すことは難しい。なぜなら 59 秒は1日の終わりでなく、まだ1秒あるからだ。(さらに 999... とミリ秒単位の指定を続けると精度はあげることはできるが、、)