mac で使用する Python のバージョンを Python 3 にする

mac のシステムでデフォルトインストールされているのは Python 2系。(Big Sur 時点)
これを Python 3系に変える。

brew インストール

% /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

pyenv インストール

% brew install pyenv

インストール可能な Python のバージョン一覧を確認。

% pyenv install --list

使いたい Python のバージョンをインストールする(例は 3.8.6)

% pyenv install 3.8.6

pyenv で管理されているバージョン一覧を表示する。
(先ほどインストールしたバージョンがあるはず)

% pyenv versions
* system
  3.8.6

システム全体で使用する Python のバージョンを指定する。

% pyenv global 3.8.6

~/.zshrc に以下の内容記載する(ファイルがなければ作成)
Catalina 以降は bash から zsh にシェルが変わっている。

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv 1>/dev/null 2>&1; then
  eval "$(pyenv init -)"
fi

ターミナルを再起動して、Pyhton のバージョンが切り替わっていれば OK

% python --version
Python 3.8.6

システムで使用している Python のパスが pyenv になっているかも確認できる。

% which python

/Users/<ユーザ名>/.pyenv/shims/python

既存の SwiftUI アプリのライフサイクルを iOS 14 の SwiftUI App に変更してみる

Xcode 11 を使って iOS 13 向けに作成されたSwiftUI のアプリだと AppDelegate と SceneDelegate のライフサイクルでアプリが作成されている。

Xcode 12 以降だと、プロジェクト作成時に SwiftUI のアプリのライフサイクルが選べるようになっている。

f:id:daisuke-t-jp:20201022013103p:plain:w500

選べるのは、以下だ。

既存の IUIKit App Delegate で作成した SwiftUI のアプリを iOS 14 以降の SwiftUI App のライフサイクルに変更する手順を以下にメモする。(iOS 13 は動作対象外になることに注意)

Info.plist

  • Info.plist の UISceneConfigurations の部分を削除する。

こうなっていたのが

 <key>UIApplicationSceneManifest</key>
    <dict>
        <key>UIApplicationSupportsMultipleScenes</key>
        <false/>
        <key>UISceneConfigurations</key>
        <dict>
            <key>UIWindowSceneSessionRoleApplication</key>
            <array>
                <dict>
                    <key>UISceneConfigurationName</key>
                    <string>Default Configuration</string>
                    <key>UISceneDelegateClassName</key>
                    <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
                </dict>
            </array>
        </dict>
    </dict>

削除すると、こうなる。

 <key>UIApplicationSceneManifest</key>
    <dict>
        <key>UIApplicationSupportsMultipleScenes</key>
        <false/>
    </dict>

アプリのメインエントリを App プロトコルに変更

App から派生した struct を定義したファイルを作る。

WindowGroup に最初に始まる View を設定する。

import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView() // 最初に表示される View
        }
    }
}

既存の AppDelegate.swift, SceneDelegate.swift はプロジェクトから外して、ビルドされないようにする。

特に AppDelegate の @UIApplicationMain@main が両方存在すると、メインエントリが重複してしまいエラーになることに注意。

この状態でビルド→実行で、アプリ起動して View が表示されるところまで確認できる。

以下、Info.plist の UIApplicationSupportsMultipleScenes についての注意点。

既存のライフサイクルのアプリを、あたらしいアプリに置き換える際に UIApplicationSupportsMultipleScenes がもともと false の場合、インストール済みで起動しているアプリをあたらしいライフサイクルのアプリで上書きして実行すると、最初の View が表示されず黒い画面のままになる現象を確認した。(一度前のアプリを停止させてれば、画面は表示される)

UIApplicationSupportsMultipleScenes がもともと false で、ライフサイクル変更と共に true にして上書きインストールすると、上記の現象は起きなかった。

この時 UIApplicationSupportsMultipleScenes を変えるとアプリの動作にも影響があるため、変更する場合は気を付けたい。

UIApplicationSupportsMultipleScenes

既存の AppDelegate で実行している処理を新しいライフサイクルでも実現

AppDelegatedidFinishLaunchingWithOptions で初期化処理をしていたとする。たとえば Firebase とか。

class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Fireabase を初期化する
        FirebaseApp.configure() 

        return true
    }
}

これが新しいライフサイクルでも呼ばれるようにするには UIApplicationDelegateAdaptor というプロパティラッパーを使用して、従来の UIKit の UIApplicationDelegate の機能も利用できるようにする。

import SwiftUI

@main
struct TestApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView() // 最初に表示される View
        }
    }
}



class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Fireabase を初期化する
        FirebaseApp.configure() 

        return true
    }
}

MIT License、Apache License 2.0 についてのメモ

OSS でよくみるMIT, Apache License 2.0 の意味がよくわからなかったので調べたことをメモ。

日本語でオープンソースライセンスについて書かれた資料は、 IPA情報処理推進機構)が提供している物が分かりやすい。

OSSライセンス関連情報:IPA 独立行政法人 情報処理推進機構

この中で、各ライセンスの比較は以下の資料にまとまっている。(AGPLv3, EUPL, BSD License, Apache License 2.0, MIT License, OpenSSL License などについて記載あり)

OSSライセンスの比較、利用動向および係争に関する調査:IPA 独立行政法人 情報処理推進機構

資料の中で「ライセンシ」「ライセンサ」という単語が登場するが、意味は以下である。

  • ライセンシ ... ライセンスをうける側。OSS を使用する側。
  • ライセンサ ... ライセンスをあたえる側。OSS を提供する側。

MIT, Apache License 2.0 をみてみると

MIT License

IPA の資料から抜粋。

・ライセンシは、OSS を配布する際、ライセンス本⽂および著作権を含めなければならない。

Apache License 2.0

IPA の資料から抜粋。

・ライセンシは、OSS を配布する際、ライセンス本⽂を提供しなければならない。

・ ライセンシは、OSSソースコード形式で配布する際、著作権・特許・商標・帰属63についての告知を添付しなければならない。

・ ライセンシは、OSS に改変を加えて配布する際、改変を加えた事実を分かりやすく告知しなければならない。

・ ライセンシは、オリジナル OSS の NOTICE ファイルに帰属告知が含まれている場合、配布する OSS に同告知を含めなければならない。

・ ライセンサは、配布する OSS に⾃⾝の特許が含まれる場合、ライセンシに対して当該特許を無償でライセンス付与しなければならない。

・ ライセンシが誰かを特許侵害で訴えた場合、ライセンサがライセンシに与えていた特許ライセンスは失効することになる。

・ ライセンサは、配布する OSS に関して、いかなる保証も提供しない。

・ ライセンサは、配布した OSS が引き起こす損害に対して、⼀切の責任を持たない。

MIT はとてもシンプル。ライセンス文章も短い。

Apache License 2.0 はライセンシが改変した時の記載があったり、特許の扱いが決められたり、ランセンサとライセンシの権利を細かく保証している印象がある。

Firebase iOS SDK を使っていると SwiftUI の Preview が機能しない問題(Firebase iOS SDK v6.33.0)

Firebase iOS SDK を使っている SwiftUI のプロジェクトで Preview が表示されなくなった。(Xcode 12.0.1)

とりあえず configure()コメントアウトすれば、Preview は表示される。

// FirebaseApp.configure()

調べたら、この問題について issue が上がっていた。(2020/10/4 現在、修正リリースは出ていないが issue 自体は close されている)

issue の内容を読むと、どうやらこれは v6.33.0 で起きる問題で、その前のバージョン(v6.30.0 とか)では発生しないらしい。なので、前のバージョンを指定してライブラリ追加しておくと、とりあえず回避できる。CocoaPods ならこんな感じ。

pod 'Firebase/Analytics', '6.30.0'

今後は、、
Firebase iOS SDK v6.34.0 のマイルストーンにこの issue はあるので、次のバージョンでは直っている。。。のかな。と。

https://github.com/firebase/firebase-ios-sdk/milestone/70?closed=1

Nominatim(OSM) API を試してみた(iOS)

iOS で Nominatim API の動作モックを作って試してみた。

動作イメージはこんな感じ。

Nominatim は OSM(Open Street Map)のデータからジオコーディングした結果を取得できる API で、たとえば地名から住所(緯度経度)検索ができる。また、緯度経度から地名を取得する逆ジオコーディングの機能も API にある。


以下、2020/9/23 現在の API の感想。(OSM のデータは更新されるため、最新のデータでは状況が変わっているかもしれないので、注意)

良い点

試してみた感じはやはり OSM はデータが膨大なので、結果の候補が多く取得できる。

たとえば「中央区」と検索した時、日本各地の中央区の候補がちゃんと取得できる。

https://nominatim.openstreetmap.org/search?q=%E4%B8%AD%E5%A4%AE%E5%8C%BA&format=json

[
{
"place_id": 235480386,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
"osm_type": "relation",
"osm_id": 1758897,
"boundingbox": [
"35.6403529",
"35.6966147",
"139.758555",
"139.7931533"
],
"lat": "35.666255",
"lon": "139.775565",
"display_name": "中央区, 東京都, 日本",
"class": "boundary",
"type": "administrative",
"importance": 0.6646376901336712,
"icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_boundary_administrative.p.20.png"
},
{
"place_id": 233767601,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
"osm_type": "relation",
"osm_id": 358727,
"boundingbox": [
"34.662424",
"34.6953569",
"135.496732",
"135.5355055"
],
"lat": "34.679846",
"lon": "135.510316",
"display_name": "中央区, 大阪府, 日本",
"class": "boundary",
"type": "administrative",
"importance": 0.615352652067619,
"icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_boundary_administrative.p.20.png"
},
★以下略...

]

気を付けたい点

OSM はデータがローカライズはされていたり、されていなかったりが割と多い印象なので

たとえば「Anchorage」では結果が得られるが

https://nominatim.openstreetmap.org/search?q=Anchorage&format=json

[
{
"place_id": 236481643,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright",
"osm_type": "relation",
"osm_id": 8867140,
"boundingbox": [
"60.733788",
"61.483938",
"-150.420615",
"-148.460007"
],
"lat": "61.2163129",
"lon": "-149.8948523",
"display_name": "Anchorage, アラスカ州, アメリカ合衆国",
"class": "boundary",
"type": "administrative",
"importance": 0.6801377964834151,
"icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_boundary_administrative.p.20.png"
},

★以下略...

]

「アンカレッジ」では結果が得られない

https://nominatim.openstreetmap.org/search?q=%E3%82%A2%E3%83%B3%E3%82%AB%E3%83%AC%E3%83%83%E3%82%B8&format=json

[]

ということには、気を付けたい。

プロジェクトを 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") {
    // 広告を非表示にする
}