pod install と update の違い。あと deintegrate

pod install / update をいままで何となく使っていたので、違いを調べる。

https://guides.cocoapods.org/terminal/commands.html

コマンドのマニュアル↑をみると

  • install
    • Podfile.lock に記載されているバージョンで、依存関係をインストールする
  • update
    • 古い依存関係を更新し、あたらしい Podifle.lock を作成する

となる。

使いわけとしては Podfile.lock がプロジェクトにあり、そこに書かれているライブラリのバージョンにて開発する場合は pod install をする。 もし、ライブラリのバージョンを更新したい場合は pod update を使用する。という感じになる。

実際の使い分けのケースとしては、複数メンバで開発している場合だと、開発時のライブラリバージョンを統一したい時は、リポジトリに Podfile.lock を追加し、各自 pod install でライブラリをインストールする流れにすると、バージョン差異の問題が起きない、ということができる。


あと pod deintegrate というコマンドもあるが、これを実行すると CocoaPods の統合を解除することができる。

具体的には

  • Xcode のプロジェクト(xcodeproj)から CocoaPods の記載を削除する
  • Podfile, Podfile.lock は残る

という感じなので、リポジトリに push したタイミングで自動ビルド(pod install から始まる)をしたい時は deintegrate された状態で、バージョン管理していくといいかもしれない。

SwiftUI で GoogleAdMob バナーを表示する

SwiftUI で GoogleAds のバナー広告を表示するサンプルを作った。




方法としてはまず GADBannerView を表示する UIViewController を用意する。

class GADBannerViewController: UIViewController {
    
    let bannerView = GADBannerView(adSize: kGADAdSizeBanner)
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // This app using sample id.
        // https://developers.google.com/admob/ios/test-ads#sample_ad_units
        bannerView.adUnitID = "ca-app-pub-3940256099942544/2934735716"
        
        bannerView.delegate = self
        bannerView.translatesAutoresizingMaskIntoConstraints = false
        bannerView.rootViewController = self
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        bannerView.load(GADRequest())
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        bannerView.removeFromSuperview()
    }
    
}



// MARK: - GADBannerViewDelegate
extension GADBannerViewController: GADBannerViewDelegate {
    func adViewDidReceiveAd(_ bannerView: GADBannerView) {
        
        guard let viewController = bannerView.rootViewController else {
            return
        }
        
        guard bannerView.superview == nil else {
            return
        }
        
        
        viewController.view.addSubview(bannerView)
        viewController.view.bringSubviewToFront(bannerView)
        
        viewController.view.addConstraints([
            NSLayoutConstraint(item: bannerView,
                               attribute: .bottom,
                               relatedBy: .equal,
                               toItem: viewController.view.safeAreaLayoutGuide,
                               attribute: .bottom,
                               multiplier: 1,
                               constant: 0),
            NSLayoutConstraint(item: bannerView,
                               attribute: .centerX,
                               relatedBy: .equal,
                               toItem: viewController.view,
                               attribute: .centerX,
                               multiplier: 1,
                               constant: 0)
        ])
        
    }
}


次に UIViewController はそのままだと SwiftUI で使用できないので SwiftUI 用に UIViewControllerRepresentable でビューコントローラーを wrap する。

struct GADBannerViewControllerRepresentable: UIViewControllerRepresentable {
    
    typealias UIViewControllerType = GADBannerViewController
    static let adsBannerSize: CGSize = CGSize(width: 320, height: 50)
    
    
    func makeUIViewController(context: Context) -> UIViewControllerType {
        return .init()
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
    }
    
}


最後に UIViewControllerRepresentable を SwiftUI の View に配置すれば、広告が表示される。

struct ContentView: View {
    var body: some View {
        
        NavigationView() {
            VStack {
                Spacer()
                
                Text("SwiftUI GoogleAdsSample")
                
                Spacer().frame(height: 30)
                
                NavigationLink(destination: ContentView2()) {
                    Text("Link to Next View.")
                }
                
                Spacer()
                
                GADBannerViewControllerRepresentable().frame(width: GADBannerViewControllerRepresentable.adsBannerSize.width,
                                                             height:GADBannerViewControllerRepresentable.adsBannerSize.height,
                                                             alignment: .center)
            }
            .navigationBarHidden(false)
            .padding()
        }

    }
}

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 から天気取得できそうだ。