はじめに
こんにちは。
最近ChatGPTのiOSアプリをよく使用しているのですが、文章生成アニメーションと生成開始前後に発生する触覚フィードバック(振動)が心地よく個人的に気に入っています。
このUIってどうやって実装されているのかなと思い色々調べて実装してみたのですが、非常に簡単に実装できたので今回こちらを共有したいと思います!
開発環境は以下の通りです。
- Swift 5.10
- Xcode 16.1
実装
今回は以下のような仕様のアプリを実装します。
- ボタンをタップすると、文章がアニメーションで表示される
- ボタンのタップ直後に触覚フィードバック(振動)が発生する
- 文章の表示が完了した直後に触覚フィードバック(振動)が発生する
コード全体
まずはコード全体を以下に示します。
import SwiftUI
import Combine
struct ContentView: View {
let fullText = "こんにちは、これはタイピングアニメーションのテストです。"
@State private var displayedText = ""
@State private var index = 0
@State private var cancellable: AnyCancellable?
var body: some View {
VStack(spacing: 20) {
Text(displayedText)
.font(.title)
.fontWeight(.bold)
Button("スタート") {
startTypingAnimation()
}
.disabled(cancellable != nil) // 途中でボタンを押せないようにする
}
.padding()
}
private func startTypingAnimation() {
displayedText = ""
index = 0
cancellable?.cancel() // 既存の購読を解除
// エフェクト開始時の触覚フィードバック
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
cancellable = Timer.publish(every: 0.05, on: .current, in: .common)
.autoconnect()
.sink { _ in
if index < fullText.count {
let nextIndex = fullText.index(fullText.startIndex, offsetBy: index)
displayedText.append(fullText[nextIndex])
index += 1
} else {
cancellable?.cancel() // タイピングが完了したら購読を解除
cancellable = nil
// エフェクト終了時の触覚フィードバック
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
}
}
}
以下では、このコードの詳細について、文章性アニメーションと触覚フィードバックに分けて説明します。
文章生成アニメーション
こちらのアニメーションですが、一般的にTyping EffectやTypewriter Effectと呼ばれるようです。
まずはじめにボタンをタップしたときの処理は以下のようになります。
Button("スタート") {
startTypingAnimation()
}
ここでは後述するstartTypingAnimation
メソッドを呼び出しています。
では次にstartTypingAnimation
メソッドを実装します。
private func startTypingAnimation() {
displayedText = ""
index = 0
cancellable?.cancel() // 既存の購読を解除
// エフェクト開始時の触覚フィードバック
// ...
cancellable = Timer.publish(every: 0.05, on: .current, in: .common)
.autoconnect()
.sink { _ in
if index < fullText.count {
let nextIndex = fullText.index(fullText.startIndex, offsetBy: index)
displayedText.append(fullText[nextIndex])
index += 1
} else {
cancellable?.cancel() // タイピングが完了したら購読を解除
cancellable = nil
// エフェクト終了時の触覚フィードバック
// ...
}
}
}
ここでは具体的に以下のような処理を行っています。
- 表示する文章と文章のインデックスをリセット、タイマーのサブスクリプションを解除
- Timerを使用して0.05秒ごとに文字を表示
- 文章の表示が完了したらタイマーのサブスクリプションを解除
またタイマー部分はCombineを使用しています。
タイマーの実装方法としては、FoundationのTimer
クラスもあるのですが、公式ドキュメントによると、Combineの方が簡単に実装できるとのことなのでこちらを採用しました。
アニメーションをカスタムしたい場合は、Timer
のインターバルを調整したり、Text
Viewに.animation()
を追加することで実現できると思います。
触覚フィードバック
触覚フィードバックは英語ではHaptic Feedbackと呼ばれるようです。
HIGの触覚フィードバックページを見ると、iOSでは以下の3種類の触覚フィードバックがあり、Swiftでは各々に専用のクラスが用意されています。
触覚フィードバック | クラス |
---|---|
Notification Feedback | UINotificationFeedbackGenerator |
Impact Feedback | UIImpactFeedbackGenerator |
Selection Feedback | UISelectionFeedbackGenerator |
今回は、以下のようにエフェクト開始時と終了時に触覚フィードバックを発生させます。
// エフェクト開始時の触覚フィードバック
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
// エフェクト終了時の触覚フィードバック
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
ここでは開始時には.light
、終了時には.success
を指定しています。1
参考文献
-
当初ChatGPTアプリに似せようとしたのですが、難しかったため適当な値を設定しました。。。おそらく終了時は
.success
に近いのですが、開始時はプリセットのものとは異なる印象をうけたため、.light
を指定しています。触覚フィードバックはカスタムしたい場合は、Core Hapticsを使用すれば可能らしいですが、今回は省略します。 ↩