SwiftUI キーボードを表示時に特定の View を非表示にする

たとえばこんなコードで ScrollView 内に TextField を置いて、さらにスクロール位置によらないで常に画面下部にある View が表示されるような構成を ZStack で表現すると

import SwiftUI

struct ContentView: View {
    @State private var text: String = ""
    let bottomBoxHeight: CGFloat = 150
    
    var body: some View {
        ZStack {
            ScrollView(.vertical, showsIndicators: true) {
                VStack {
                    ForEach(0..<20) { (i) in
                        TextField("Textfield\(i)", text: $text)
                            .padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 33))
                            .overlay(
                                RoundedRectangle(cornerRadius: 30)
                                    .stroke(Color(.systemGray3), lineWidth: 1)
                            )
                        
                        Spacer()
                            .frame(height: 10)
                    }
                    
                    Spacer()
                        .frame(height: bottomBoxHeight)
                }.padding()
            }
            
            VStack {
                Spacer()
                
                Group {
                    Text("Bottom box")
                        .font(.headline)
                        .foregroundColor(.white)
                }
                .frame(minWidth: 0, idealWidth: .infinity, maxWidth: .infinity,
                       minHeight: 0, idealHeight: bottomBoxHeight, maxHeight: bottomBoxHeight,
                       alignment: .center)
                .background(Color(.red))
            }
        }
    }
}

こういうふうに TextField のキーボードを開いた時に、キーボード上に View が重なってしまい、TextField が見えなくなってしまう。(入力できない)

キーボードを表示した時に特定の View を非表示にできれば解決しそう、と考えて調べてみると

KeyboardObserving というライブラリがあり、これを使って対応できた。

https://github.com/nickffox/KeyboardObserving

使い方は、まずアプリのエントリで最初の View を表示する部分のコードで .environmentObject(Keyboard()) を追加する。
(これをしないと、このあと追加するコードでクラッシュする)

import SwiftUI

import KeyboardObserving

@main
struct SwiftUIHideOnKeyboardSampleAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(Keyboard())    // これを追加
        }
    }
}

あとは問題が起きている View に以下を追加する。

ScrollView(.vertical, showsIndicators: true) {
    KeyboardObservingView { // KeyboardObservingView にキーボード表示を監視したい View を追加する
        VStack {
            // .... 中略 ....
        }.padding()
    }
}

// .... 中略 ....

VStack {
    // .... 中略 ....
}
.hideOnKeyboard()    // キーボードを表示した時に非表示にしたい View に .hideOnKeyboard() を追加する

これでキーボード表示時に、TextField が隠されることは無くなった。

今回の記事の完全なコードは GitHub にある。

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

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 のバージョンをインストールする(例は 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