Google AdMob (iOS)バナー広告のサンプルコードで「bottomLayoutGuide」の警告が出る件

https://developers.google.com/admob/ios/banner

2020/04/29 時点で、サンプルコードが以下になっているが

func addBannerViewToView(_ bannerView: GADBannerView) {
    bannerView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(bannerView)
    view.addConstraints(
      [NSLayoutConstraint(item: bannerView,
                          attribute: .bottom,
                          relatedBy: .equal,
                          toItem: bottomLayoutGuide,
                          attribute: .top,
                          multiplier: 1,
                          constant: 0),
       NSLayoutConstraint(item: bannerView,
                          attribute: .centerX,
                          relatedBy: .equal,
                          toItem: view,
                          attribute: .centerX,
                          multiplier: 1,
                          constant: 0)
      ])
   }


bottomLayoutGuide

'bottomLayoutGuide' was deprecated in iOS 11.0: Use view.safeAreaLayoutGuide.bottomAnchor instead of bottomLayoutGuide.topAnchor

の警告が表示されるとおり iOS 11 以降では非推奨なので、以下のように safeAreaLayoutGuide を使用すると警告が出なくなる。


この時、気を付けたいのは bottomLayoutGuide -> view.safeAreaLayoutGuide だとアプリ実行時に crash するため、 同時に attribute.bottom -> .top に変更する必要もある。

      [NSLayoutConstraint(item: bannerView,
                          attribute: .bottom,
                          relatedBy: .equal,
                          toItem: view.safeAreaLayoutGuide, // bottomLayoutGuide
                          attribute: .bottom, // top
                          multiplier: 1,
                          constant: 0),

Swift TextField で入力した数内を金額(通貨)フォーマットにして表示する

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

Swift のテキストフィールドで入力した数値を「金額(円)表記」するサンプルを作ってみた。

ユーロ(€),ドル($) ならば、小数点の考慮も必要だが、日本円ならばこんな感じで。。

Swift URLRequest のタイムアウトを発生させる

URLRequest のリクエストがタイムアウトになった時の動作をみたい時がある。

そういう時は URLRequest の initializer で timeoutinterval をとても小さな値(たとえば 0.0001 秒とか)にすると、タイムアウトを発生させて、動作を試すことができる。

こんな感じだ。

let url = URL(string: "URL")!
let req = URLRequest(url: url,
                     cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
                     timeoutInterval: 0.0001)   // すぐタイムアウト発生する

let task = URLSession.shared.dataTask(with: req) { data, response, error in

    if let err = error as NSError? {
        if err.domain == NSURLErrorDomain,
            err.code == NSURLErrorTimedOut {
            // タイムアウト発生
        }
        return
    }
}

task.resume()

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 の高さを項目の数・高さに対して、ピッタリ設定することはできた。