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

SwiftUI の UIView を画像にして保存するサンプル

SwiftUI で UIView の内容を画像(UIImage)にして、アルバムに保存するサンプルを作った。

SwiftUI は現在の View を直接 UIView として扱うことは難しいので UIApplication 経由で UIView を取得する感じになる。

extension UIView {
    
    func image() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: self.bounds)
        
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
    
}

extension UIApplication {
    
    static func image() -> UIImage? {
        guard let rootViewController = shared.windows[0].rootViewController else {
            return nil
        }
        
        guard let view = rootViewController.view else {
            return nil
        }
        
        return view.image()
    }
    
}