文字っぽいの。

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

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の値をみることで下がるアニメーションと上がるアニメーションをトグルできるようにしているので、隠す時と表示する時で対応した動きができるようになっている。