文字っぽいの。

文字を書いています。写真も混ざります。

Package.swiftで利用しているライブラリの依存関係を表示する。

やりたいこと

ライブラリを導入すると、そのライブラリが依存しているライブラリも全部入ってきます。時には「このライブラリはなに経由で入ってきてるんだ?」と調べたいことがあります。

そこで Package.swift のdependencies内に書かれているそれぞれのライブラリが、どんなライブラリに依存しているかを調べます。

やりかた

Package.swiftがあるディレクトリで次のコマンドを実行します。

swift package show-dependencies --format json -o dependencies.json

これによって dependencies.json に依存関係がまとまったJSONファイルが書き出されます。

様子

こんな感じで表示されます。

gist.github.com

便利。

吉祥寺散策

吉祥寺を散策してきたのでその記録。

井の頭公園

ブルースカイコーヒーのねこドーナツとケーニッヒのホットドッグとフランクフルト。

井の頭公園は結構人がいた。あひるさんボートも人気で大行列だった。結構暖かい日だったので、ボート乗るのも気持ちよさそうだったけど流石に混みすぎててやめておいた。

四歩

古道具屋と日用雑貨屋とカフェが一緒になったお店、四歩(しっぽ)へ

売っている雑貨もいい感じだし、店内も落ち着いていて良かった。 いちごのミニパフェといちごのアールグレイティーを注文。いちご祭り。ミニパフェ、ちょうどいい量だし中のアイスも卵感の強いミルクセーキ的な味で美味しかった。甘いものは大量に食べられないので、ミニパフェがあるの助かる。

ダパイダン105

夜はハモニカ横丁へ。すごい久しぶりに来たので、知ってる店がなくなってる。

一軒目はダパイダン105へ。焼き小籠包が有名なお店。

焼き小籠包は「小皿にとって、スープを出してから食べてね」と最初に注意される。それをしてもスープが飛びがちなので、紙エプロンはつけておいたほうが吉。

ザーサイが食べ放題なのも嬉しい。

立ち寿司横丁

締めに寿司。

生さばがあって、脂がのってて美味かった。飲んだあとのあさり汁も美味しい。

星のカービィ スーパーデラックス ドットライトを買った。

めちゃくちゃかわいい。

電源はUSB-Cなので、コンセント経由でもモバイルバッテリー経由でもつながれば光る。でも光ってなくても存在感があってかわいい。

近くで見ると「溝が掘ってあるだけだな」という感じだけど、ちょっとはなれるといい感じにドット感を感じるようになっている。趣味のインテリアとしてちょうどよい。

プレミアムバンダイで買える。

p-bandai.jp

SwiftUIの `.contentTransition(.numericText())` で遊ぶ

iOS 17+で使えるSwiftUI用のAPI.contentTransition(.numericText()) というのがある。

使い方は簡単で

Text("\(value)")
    .contentTransition(.numericText(value: value))

こうやって書けば、Textの中身が変わる時にアニメーションしてくれる。withAnimation {} 経由でStateは変えないといけないことに注意。

試してみる

でっかい乱数を生成して4桁ずつスペースで区切って表示する。

struct ContentView: View {
    @State private var number: Int = 0

    var body: some View {
        VStack {
            Text(format(number: number))
                .font(Font(UIFont.monospacedDigitSystemFont(ofSize: 32, weight: .bold)))
                .contentTransition(.numericText(countsDown: true))
            Button("Random") {
                withAnimation {
                    number = Int.random(in: 1...10000000000000000)
                }
            }
            .buttonStyle(.borderedProminent)
        }
    }

    func format(number: Int) -> String {
        let formatter = NumberFormatter()

        formatter.groupingSeparator = " "
        formatter.groupingSize = 4
        formatter.usesGroupingSeparator = true
        formatter.minimumIntegerDigits = 16

        return formatter.string(from: NSNumber(value: number)) ?? ""
    }
}

実用性がある感じで試す

カウンターを実装すれば早いけど、すぐできちゃうので別の実装をしてみる。

struct CreditCardView: View {
    @State private var cardNumber: String = "4111 1111 1111 1111"
    @State private var show: Bool = false

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 8)
                .fill(Color(.lightGray).shadow(.drop(color: .black.opacity(0.2), radius: 4, x: 0, y: 0)))
                .stroke(Color(.border), style: StrokeStyle(lineWidth: 1))

            HStack {
                HStack(spacing: 16) {
                    Image(systemName: "creditcard")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 20)
                        .foregroundStyle(Color(.border))

                    Text(cardNumber)
                        .font(Font(UIFont.monospacedSystemFont(ofSize: 16, weight: .bold)))
                        .contentTransition(.numericText(countsDown: show))
                }

                Spacer()
                Button(action: {
                    withAnimation {
                        show.toggle()

                        if show {
                            cardNumber = "4111 1111 1111 1111"
                        } else {
                            cardNumber = "**** **** **** 1111"
                        }
                    }
                }, label: {
                    if show {
                        Image(systemName: "eye.fill")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 20)
                            .foregroundStyle(Color.gray)
                    } else {
                        Image(systemName: "eye.slash.fill")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 20)
                            .foregroundStyle(Color.gray)
                    }
                })
            }
            .padding([.leading, .trailing], 16)
        }
        .frame(height: 60)
        .padding([.leading, .trailing], 16)
    }
}

これを実行するとこうなる

いい感じにアニメーションする処理が簡単なコードでかけてめでたい。

.numericText(countsDown: show) でshowの値をみることで下がるアニメーションと上がるアニメーションをトグルできるようにしているので、隠す時と表示する時で対応した動きができるようになっている。

RICOH GR IIIx を買った。

買ったもの

RICOH GR IIIxを買った。

いわゆるコンデジ

撮った写真

プログラムオートやネガフィルムフィルタで撮った写真たち。全部現像せずにJPEG撮って出ししたものを、ブログ用に縮小だけしている。

良い点

  • とにかく小さいのが良い。ショルダーバッグにも入れられるので、気楽に持ち運べる。
  • 撮って出しでいい感じの写真が撮れる。フィルターもいい感じ。
  • 起動が早い。パッとカバンから取り出してすぐに撮れる。
  • ガチ感が薄いので飲食店で撮影してても恥ずかしくない。
  • スマホと通信できるので、撮ってすぐ写真をスマホに転送してSNSに投げられる。
  • 画面をタップしたらそこにフォーカスを合わせるという「こうしたら、こう動いて欲しい」という動きがちゃんとできる。

微妙な点

  • 新品も中古も全然在庫がない。値崩れもしてない。
  • 新品で買う場合は基本入荷待ちになるので2ヶ月ほど待つ。
  • カメラとスマホを接続するための公式iOSアプリの出来が厳しい。
    • 個人開発のアプリがいくつかあるけどちょっと値段が高め。
  • 理解するまで設定画面の使い方・挙動が難しい。

総評

買ってよかった。ちょっとしたお出かけでも常に持ち運んでいる。「1日中観光して写真を撮りまくる」という日でもなければ電池も余裕で持つ。とにかく撮れる画がいい。

SwiftUI Introspect経由でDelegateを設定するとSwiftUIのBindingが死ぬ

結論

わからん。誰か詳しい人にどういう挙動が起きているのか教えてほしい。対応策を教えてもらえるともっと嬉しい。

書いてみたコード

画面にはTextEditorとその文字をクリアするButtonしかない非常にシンプルなもの。

import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect

struct ContentView: View {
    @State var text: String = ""

    @Weak var textView: UITextView? {
        didSet {
            textView?.delegate = delegate
        }
    }

    var delegate = Delegate()

    final class Delegate: NSObject, UITextViewDelegate {
        func textViewDidChange(_ textView: UITextView) {
            print("Delegate:", textView.text)
        }
    }

    var body: some View {
        VStack {
            TextEditor(text: $text)
                .onChange(of: text, {
                    print(text)
                })
                .introspect(.textEditor, on: .iOS(.v16, .v17)){ textView in
                    self.textView = textView
                }
            Button {
                text = ""
            } label: {
                Text("文字クリア")
            }
        }
        .padding()
    }
}

起きること

textView?.delegate = delegateDelegateを渡すとしっかりと func textViewDidChange(_ textView: UITextView) が紐づいてそちらの処理は発火する。一方で、 .onChange(of: text) とButton内の text = "" は発火しなくなる。当然ながらtextView?.delegate = delegateコメントアウトしてやれば、.onChange(of: text)text = ""によるクリアも正しく動作する。

TextEditorの裏側にはUITextViewがおり、そのUITextViewDelegateを奪ってしまうから発火しないのだろうという推測はできるのだが……。

super. 的なコードを書いてどちらも発火させることはできないんだろうか? UITextViewDelegateがつなぎ込めてしまえば実装上は実現できるけれど、なんかね。