UISearchBarのプレースホルダーをアニメーションさせる
完成品
検索窓のプレースホルダーを、予め設定したキーワードを表示して、順番にローテーションするUIです。
何が嬉しいのか
検索窓のプレースホルダーは具体的な言葉の方が「どういう言葉を検索すればいいのか分かる」というメリットがあります。
その一方で、具体的すぎると他のキーワードがイメージしづらくなって、ざっくりとした表現のプレースホルダーを入れがち。
「ラーメン店の名前を入力」って書いてるより、「一風堂」って書いてる方が具体的に検索ワードが思いつきやすい。けど、一風堂だけだと流石に違和感があるので「一蘭」とか「天下一品」も表示したいよね、っていうことです。
技術
UIKitの標準的なアニメーションで(view.animate
)は、今回のようなplaceholderの透明度など一部のパラメータをアニメーションできないという問題があります。
他にも角丸を徐々に変更するとかが無理っぽい。
そういうときはCoreAnimation系のAPIを使うことになるのですが、今回使用しているのはCADisplayLink
。
CADisplayLink
はコードの実行を画面のリフレッシュレート(基本60fps)に同期してくれるタイマーみたいなAPIです。
タイマーといえばNSTimer
というものもあって、これでアニメーションを実装しているコードなども見かけるのですが、NSTimer
は60fpsを保証してくれるわけではないらしく、CoreAnimationのAPIであるDisplayLinkの方がアニメーション向けには良いらしい。
前述の通り、あくまでタイマー的な機能でしかないので、アニメーションの管理は自分でやらないといけません。
コード
//
// ViewController.swift
// UILabelAnimation
//
// Created by Ukkun
import UIKit
class ViewController: UIViewController {
let searchBox = UISearchBar()
var placeholders = ["MacBook Air", "MacBook Pro","AirPods","AirPods Pro","iPad Pro",]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(searchBox)
searchBox.frame = view.frame
let displayLink = CADisplayLink(target: self, selector: #selector(handleUpdate))
displayLink.add(to: .main, forMode: .default)
self.placeholders = self.placeholders.shuffled()
}
let keyframes:[Double] = [0.0, 0.4, 3.4, 3.8]
var animationStartDate = Date()
var placeHolderOpcity:Double = 1
var currentPlaceholder = 0
func setPlaceholderTextAndOpacity(opacity:Double, text:String) {
searchBox.searchTextField.attributedPlaceholder = NSAttributedString( string: text, attributes: [NSAttributedString.Key.foregroundColor : UIColor.init(red: 0.576, green: 0.569, blue: 0.561, alpha: CGFloat(opacity))])
}
@objc func handleUpdate() {
let now = Date()
let elapsedTime = now.timeIntervalSince(animationStartDate)
switch elapsedTime {
case 0 ..< keyframes[1]:
//keyframe 0
let animationDuration = keyframes[1] - keyframes[0]
let percentage:Double = elapsedTime / animationDuration
placeHolderOpcity = percentage
setPlaceholderTextAndOpacity(opacity: placeHolderOpcity, text: placeholders[currentPlaceholder])
case keyframes[1] ..< keyframes[2]:
//keyframe 1
placeHolderOpcity = 1
setPlaceholderTextAndOpacity(opacity: placeHolderOpcity, text: placeholders[currentPlaceholder])
case keyframes[2] ..< keyframes[3]:
//keyframe 2
let elapsedTimeInKeyframe = elapsedTime - keyframes[2]
let animationDuration = keyframes[3] - keyframes[2]
let percentage:Double = elapsedTimeInKeyframe / animationDuration
placeHolderOpcity = 1 - percentage
setPlaceholderTextAndOpacity(opacity: placeHolderOpcity, text: placeholders[currentPlaceholder])
default:
animationStartDate = Date()
if currentPlaceholder == placeholders.count - 1 {
currentPlaceholder = 0
} else {
currentPlaceholder += 1
}
setPlaceholderTextAndOpacity(opacity: 0, text: placeholders[currentPlaceholder])
}
}
}
解説
DisplayLinkの設置
let displayLink = CADisplayLink(target: self, selector: #selector(handleUpdate)) // 1フレームごとに、handleUpdate関数を実行する。
displayLink.add(to: .main, forMode: .default)
キーフレームの設定。
各種初期化。
let keyframes:[Double] = [0.0, 0.4, 3.4, 3.8] //キーフレームの設定。アニメーションが発生して、0.0秒後がkeyframe 0で、0.4秒がkeyframe 1という具合。
var animationStartDate = Date() //DisplayLinkのタイマー用。アニメーション開始時の時刻を記録して、どれだけ時間が経ったかカウントする。
var placeHolderOpcity:Double = 1 // placeholderのOpacityをアニメーションするので、初期値を1にしておく。
var currentPlaceholder = 0 // どのPlaceholderを出すかの初期化。
placeholderの透明度とテキストを変更する独自関数。
// placeholderの不透明度を設定するコードが長すぎるので、関数化。不透明度と、プレースホルダーにするテキストを受け取る
func setPlaceholderTextAndOpacity(opacity:Double, text:String) {
searchBox.searchTextField.attributedPlaceholder = NSAttributedString( string: text, attributes: [NSAttributedString.Key.foregroundColor : UIColor.init(red: 0.576, green: 0.569, blue: 0.561, alpha: CGFloat(opacity))])
}
60fpsで実行されるコード部分。
@objc func handleUpdate() {
///この中が1フレーム(1/60秒)ごとに実行される
}
アニメーションの準備。
let now = Date() // 経過時間を測るための、「今何時?」を決める
let elapsedTime = now.timeIntervalSince(animationStartDate) // アニメーション開始から「今」までの経過時間
アニメーションの中身。
switch elapsedTime {
case 0 ..< keyframes[1]: // keyframe 0 から 1 までの間はここを実行
let animationDuration = keyframes[1] - keyframes[0] // アニメーションのかかる時間=Durationを計算
let percentage:Double = elapsedTime / animationDuration // アニメーションが全体のうち何%進んだかを計算。
placeHolderOpcity = percentage // プレースホルダーの不透明度はアニメーションの進行度合いと一致させる。つまり、徐々に不透明になる。
setPlaceholderTextAndOpacity(opacity: placeHolderOpcity, text: placeholders[currentPlaceholder]) // プレースホルダーの透明度を設定。
case keyframes[1] ..< keyframes[2]:
//keyframe 1 から 2まではここを実行
placeHolderOpcity = 1 // プレースホルダーの不透明度はずっと1。つまりずっと見えたまま。
setPlaceholderTextAndOpacity(opacity: placeHolderOpcity, text: placeholders[currentPlaceholder])
case keyframes[2] ..< keyframes[3]:
//keyframe 2から3までの間はここを実行。
let elapsedTimeInKeyframe = elapsedTime - keyframes[2] // Keyframe0と同様にpercentageを計算するために、このキーフレーム内での経過時間を計算する必要がある
let animationDuration = keyframes[3] - keyframes[2] // アニメーションにかかる時間を計算。
let percentage:Double = elapsedTimeInKeyframe / animationDuration // アニメーションの進行度合いを計算
placeHolderOpcity = 1 - percentage // プレースホルダーの不透明度はアニメーションの進行度合いに伴って、0に近く。つまり徐々に透明になる。
setPlaceholderTextAndOpacity(opacity: placeHolderOpcity, text: placeholders[currentPlaceholder])
default: // 最後Keyframeを超えるとここが実行される
animationStartDate = Date() //アニメーションの開始時刻を初期状態にリセット。
if currentPlaceholder == placeholders.count - 1 { // 次のプレースホルダーに。
currentPlaceholder = 0
} else {
currentPlaceholder += 1
}
setPlaceholderTextAndOpacity(opacity: 0, text: placeholders[currentPlaceholder])
}
Discussion