UIResponder.keyboardDidShowNotification が他のアプリに切替した時にも通知される問題

iOS アプリでキーボードが表示されたイベントを知りたい時に UIResponder.keyboardDidShowNotification を通知を受ける。

コードは以下のたとえば下のようになる。

import UIKit
import os

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let textField = UITextField(frame: CGRect(x: 0, y: 0, width: 150, height: 34))
        textField.center = self.view.center
        textField.borderStyle = .roundedRect
        self.view.addSubview(textField)
        
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(keyboardDidShow),
                                               name: UIResponder.keyboardDidShowNotification,
                                               object: nil)
        
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(keyboardWillHide),
                                               name: UIResponder.keyboardWillHideNotification,
                                               object: nil)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
    
    @objc private func keyboardDidShow(notification: NSNotification) {
        // キーボードが表示になった時の通知
    }
    
    @objc private func keyboardWillHide(notification: NSNotification) {
        // キーボードが非表示になった時の通知
}


ただ、これだと「キーボードを開いている他のアプリ」に切替した時も、キーボード表示の通知がきてしまう。

例えば、以下のようにキーボードを開いている状態の Safari を開いた時などだ。

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



このような問題があるので、どうやって自分のアプリ/他のアプリのキーボード通知を見分けるのか調べると、以下のドキュメントがあった。

https://developer.apple.com/documentation/uikit/uiresponder/1621603-keyboardislocaluserinfokey

keyboardIsLocalUserInfoKey

The key for an NSNumber object containing a Boolean that identifies whether the keyboard belongs to the current app. With multitasking on iPad, all visible apps are notified when the keyboard appears and disappears. The value of this key is true for the app that caused the keyboard to appear and false for any other apps.

つまり UIResponder.keyboardIsLocalUserInfoKey の値が true なら自分のアプリ、false なら他のアプリ、ということになる。

実際に下のようなコードで試すと、期待する結果が得られた。

@objc private func keyboardDidShow(notification: NSNotification) {
        guard let isLocalUserInfoKey = notification.userInfo?[UIResponder.keyboardIsLocalUserInfoKey] as? NSNumber else { return }
        
        print("keyboardDidShow ", separator: "", terminator: "")

        if isLocalUserInfoKey.boolValue {
            print("-> My app event")
        }
        else {
            print("-> Other app event")
        }
    }

Python | POI データを取得して、地点の天気を得る

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

今回作成したプロジェクトは GitHub に置いた。



概要

OpenWeatherMap API を使うと、呼び出し頻度の制限はあるが、無料で天気情報を取得できる。

https://openweathermap.org/

この取得する天気情報を、意味のある集まりにしたい。たとえば、全国の公共施設の天気、駅の天気などだ。

こういった地点の情報(POI)は、OSM から node として無料で取得できる。そして OSM のデータは Overpass という APIプログラマブルに取得できる。

https://wiki.openstreetmap.org/wiki/JA:Overpass_API

上記から今回は、以下の流れを試す。

  • ステップ1
    • Overpass API で、POI データを取得。
  • ステップ2
    • OpenWeatherMap API で、POI の天気を取得する。

Overpass API

Overpass API は、サーバにクエリをリクエストして、マッチした OSM データをレスポンスを受け取る使い方になる。

たとえば、世界の大聖堂(カテドラル)を抽出したい場合のクエリは以下。

[out:json];
node["building"="cathedral"];
out body;

日本全国の公共施設を抽出したい場合は以下。

[out:json];
area["name"~"日本"];
node(area)["amenity"="townhall"];
out body;

このクエリを簡単に試すには、以下のサイトを使うと良い。

https://overpass-turbo.eu/

Python から Overpass API を使用するには、以下の overpy ライブラリを使う。

https://pypi.org/project/overpy/

OpenWeatherMap

Overpass API で得た POI の緯度経度から、OpenWeatherMap API で得るには下の格好になる。

https://api.openweathermap.org/data/2.5/weather?appid=APP_KEY&lat=35.658316&lon=139.741423

API キーが必要になるため、事前に取得する。

ここで気をつけたいのが、無料アカウントでは、1分に60回までリクエストが制限されているので、リクエストごとに1秒のウェイトを挟むことで、制限を避ける。

実行例

上記を踏まえて作成した Python スクリプト(overpass_weather)を試してみる。

詳しくは README.md を参照することだが、流れを書くと

Git clone してきて

$ git clone https://github.com/daisuke-t-jp/overpass_weather

必要なパッケージをインストールする

$ pip3 install overpy
$ pip3 install attrdict

overpass_weather を import して、こんなスクリプトを実行すると

#!/usr/bin/python
# coding: UTF-8

import sys
import logging
import enum

sys.path.append('../')
import overpass_weahter

def test_dump_weather(weather):
    osm = weather[overpass_weahter.KEY_OSM]
    openweathermap_weather = weather[overpass_weahter.KEY_OPENWEATHERMAP_CURRENT_WEATHER_DATA]
    
    logging.debug('osm_id[{0}] name[{1}] lat[{2}] lon[{3}] temp[{4}] pressure[{5}] humidity[{6}]'.format(
        osm[overpass_weahter.KEY_ID],
        osm[overpass_weahter.KEY_NAME],
        osm[overpass_weahter.KEY_LAT],
        osm[overpass_weahter.KEY_LON],
        openweathermap_weather['main']['temp'],
        openweathermap_weather['main']['pressure'],
        openweathermap_weather['main']['humidity']))
    
    return

def test_overpass_api():    
    try:
        weathers = overpass_weahter.weathers_with_overpass_api("""
                                            [out:json];
                                            node["building"="cathedral"];
                                            out body;
                                            """,
                                            OPENWEATHERMAP_API_KEY,
                                            0.1)
        for weather in weathers:
            test_dump_weather(weather)
        
    except Exception as exp: 
        logging.error('exception[{0}]'.format(exp))

    return

こんな風に POI と関連する天気を得られた。

2020-03-09 23:56:06,709 DEBUG test.py:27 - test_dump_weather() : osm_id[100090862] name[Dom St. Blasien] lat[47.7600646] lon[8.1300061] temp[280.04] pressure[1017] humidity[71]
2020-03-09 23:56:06,709 DEBUG test.py:27 - test_dump_weather() : osm_id[474375860] name[Собор Успения Пресвятой Богородицы] lat[50.9799235] lon[39.3167911] temp[286.99] pressure[1019] humidity[66]
2020-03-09 23:56:06,709 DEBUG test.py:27 - test_dump_weather() : osm_id[592838468] name[N/A] lat[4.8092301] lon[-74.3537103] temp[289.15] pressure[1030] humidity[67]
...

さらに、Overpass API はレスポンスに時間がかかる、また負荷によりエラーが発生しやすいため、事前に Overpass API のレスポンスを JSON ファイルにしたものでも実行できるようにした。

def test_overpass_file():
    weathers = overpass_weahter.weathers_with_overpass_file('overpass_building_cathedral.json', OPENWEATHERMAP_API_KEY, 0.1)

テスト目的ならば、こちらがやりやすい。 また、この方法だとたまに Overpass API でローカルの JSON 更新して対応していくことで、安定して更新された POI から天気取得できそうだ。

SwiftUI の EdgeInsets で zero プロパティを使用する

iOS 13 SDK の時点では

UIKit の UIEdgeInsets には zero プロパティがあるが

https://developer.apple.com/documentation/uikit/uiedgeinsets/1624518-zero

SwiftUI の EdgeInsets には zero プロパティが無い
https://developer.apple.com/documentation/swiftui/edgeinsets


なので SwiftUI の EdgeInsets で zero を使用したい場合、以下のように extension で拡張すると良さそうだ。

import SwiftUI

extension EdgeInsets {
    static let zero = EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
}

SwiftUI List の高さを、項目の数と高さに合わせてちょうどフィットさせる

サンプルは GitHub にある。




List の高さが項目に対して、ちょうどフィットされていないと以下の課題が発生する

  • List の高さが項目表示に足りない -> List 内スクロールが発生する
  • List の高さが項目表示より大きい -> List が大きすぎて、見た目が悪い

これを解決したいため、SwiftUI の List の高さを、項目に応じて、ちょうどフィットする高さに調整する方法を調べた。


サンプルの動作は、上のようになる。

コードを抜粋すると以下だ。

struct RowView: View {
    var name: String
    
    var body: some View {
        HStack {
            Spacer().frame(width: 10)
            
            Image(systemName: name).foregroundColor(.white)
            
            Text(name).foregroundColor(.white)
            
            Spacer()
        }.listRowBackground(Color.blue)
    }
}

struct ContentView: View {
    let rows = [
        "sun.min",
        "sun.min.fill",
        "sun.max",
        "sun.max.fill",
        "sunrise",
        "sunrise.fill",
        "sunset",
        "sunset.fill",
        "sun.dust",
        "sun.dust.fill",
        "sun.haze",
        "sun.haze.fill",
        "moon",
        "moon.fill",
        "moon.circle",
        "moon.circle.fill",
    ]
    static let rowHeight: CGFloat = 50
    static let rowMargin: CGFloat = 0.5 // I don't want to use fixed value. But i don't know right way.
    
    var body: some View {
        ScrollView(.vertical, showsIndicators: true) {
            VStack {
                Text("Weather Symbols").font(.largeTitle)

                Spacer().frame(height: 10)
                
                List {
                    ForEach(0..<rows.count) { (i) in
                        RowView(name: self.rows[i]).frame(height: ContentView.rowHeight)
                    }
                    .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))  // Important!
                }
                .frame(height: CGFloat(rows.count) * (ContentView.rowHeight + ContentView.rowMargin))
                
                Spacer()
            }
        }
        .padding()
    }
}


重要なのは以下だ。

まず、リスト内の項目の Insets(マージン)を無しにする。

List {
    ForEach(0..<rows.count) { (i) in
        // ...
    }.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}


そして List 自体の高さを明示的に設定する。
この時、重要なのが項目に対して 0.5 をプラスしている箇所。
これが無いと List の高さが足りないためか、 List 内のスクロールが発生してしまう。

ここは 0.5 と固定で設定しているが、本来は SwiftUI から取得した値を使いたい。
しかし、それに当たるものを見つけることができなかった。
おそらく List の項目間にある Divider (仕切り線)の高さに相当するものだと思うだが・・・。

List {
}.frame(height: 項目の数 * (項目の高さ + 0.5))


まあ、とりあえず、こんな感じで List の高さを項目の数・高さに対して、ピッタリ設定することはできた。

blurhash とは

blurhash という興味深い OSS を発見したのでメモ。

  • 「画像」から「ブラー画像」を作るための「ハッシュ値」を作成できる
  • ハッシュ値」から、ブラー画像を作成できる

f:id:daisuke-t-jp:20200222232438p:plain

イメージはこんな感じ。

で、これは何に使うかと言えば、たとえば「アプリがサーバから画像をダウンロードして表示」という場面だ。

具体的には↓

  1. サーバ側に画像を登録
    • このタイミングでサーバは、ブラー画像のハッシュ値も作成しておく
  2. アプリ側からサーバ側に画像要求リクエス
    • サーバ側は、アプリ側に「画像」と「ブラー画像のハッシュ値」を返す
  3. アプリ側は画像ダウンロードする
    • 画像ダウンロード中は「ブラー画像のハッシュ値」からブラー画像を作成し、表示しておく

これで、画像ダウンロード中は、一律に「Loading..」みたいな表示にならず、その画像を想起させるブラーの画像を表示しておけるってわけだ。


実際の動作は以下のサイトで確認できる

映画『たそがれ清兵衛』の感想

たそがれ清兵衛

たそがれ清兵衛

  • 発売日: 2013/11/26
  • メディア: Prime Video

なにか映画を流しながら PC 作業しようかなあ、と思い、チラチラみながらやってたのだが、だんだんとこれはちゃんと見たほうがよいんじゃないか?と思って、PC を閉じて、映画に集中した。

そのくらい良い映画だった。

まず最初の暗い葬式のような場面が、しんとしていて、悲しさがでていて、はじまりからこの映画の完成度を物語っている。

つぎに、宮沢りえが登場して、清兵衛との恋の予感がある。観ていて感情移入できて、最初の暗い葬式と比べてなごやかなシーンで、人間の感情の描き方の質の高さと幅広さがある映画だなと感じられる。

最初のふたつのシーンで出ているが、この映画は、未来の愛の希望と、過去と現実が示す哀しさを描いてる。

そのコントラストが淡く、非常に人間的で共感できる。

全体的にこの映画は淡々と進んでいき、コカ・コーラのような一瞬で頂点にいく爽快感みたいなものはないのだけど、じわじわとゆっくり惹きつけられる面白さになっている。それが、古き良き日本映画という風格を醸し出している。

最後のシーン。 愛する宮沢りえが待っているかもしれない自宅に帰る清兵衛。

ここはおなじ山田洋二監督の幸せの黄色いハンカチを彷彿とさせる切なさがある。

愛する人が待っているかどうかだ。

OSM データから POI データを抽出する

OSM データの読みだしには Overpass API というのがあり、それを介してデータを取得することができる。

手取り早くこれを試すには GUI で Overpass API を試せるサイトがあるので、これを使ってみる↓

https://overpass-turbo.eu/


この左側に以下のクエリを書く。

この例では "public_transport"="station" として、駅データを抽出しているが、条件を変更することで、いろいろなデータを抽出できるはずだ。 OSM の参考リンク

[out:json];
area["name"~"日本"];
node(area)["public_transport"="station"];
out body;


で「実行」ボタンを押すと、以下のように駅一覧が地図上にマークされる。

f:id:daisuke-t-jp:20200217224329p:plain

さらにこの結果を JSON で欲しければ、

メニューを「エクスポート」→「クエリ」と進んでいき

「OverpassQL (コンパクト) へ変換」をクリックする。

すると API にクエリがくっついた形のリンクがあるので、これを使用する。 たとえば、今回の場合だと

https://overpass-api.de/api/interpreter?data=%5Bout%3Ajson%5D%3Barea%5B%22name%22%7E%22%E6%97%A5%E6%9C%AC%22%5D%3Bnode%5B%22public%5Ftransport%22%3D%22station%22%5D%28area%29%3Bout%3B%0A

になり、駅一覧の JSON データが得られる。


プログラムやバッチ的にやりたい時は

たとえば Python の Overpass API ラッパーが用意されていたりするので、これを使うと良さそうだ。

https://github.com/mvexel/overpass-api-python-wrapper