はじめに
以下のようなTwitterのツイートボタンライクなUIを実装してみます。
実装イメージ
複数のボタンを重ねて配置し、アニメーションで拡散するようにしてみます。
レイアウト実装
今回は、UIViewのサブクラスを用意して、そのクラス内で必要なボタンを全て配置してみるようにします。
class CircleSpreadButtonView: UIView {}
なので、CircleSpreadButtonViewのinit時に配置するボタンに関する情報を渡してあげます。
今回は以下のstructにまとめて渡すようにしました。
import UIKit
// 円の中心のボタン情報
struct CenterButtonInfo {
let title: String
let backgroundColor: UIColor
let backgroundImage: UIImage?
let type: CircleSpreadButtonType?
init(title: String = "", backgroundColor: UIColor = .white, backgroundImage: UIImage? = nil, type: CircleSpreadButtonType? = nil) {
self.title = title
self.backgroundColor = backgroundColor
self.backgroundImage = backgroundImage
self.type = type
}
}
// 円形に広がるボタン情報
struct SpreadButtonInfo {
let title: String
let backgroundColor: UIColor
let backgroundImage: UIImage?
let type: CircleSpreadButtonType?
let task: () -> ()
init(title: String = "", backgroundColor: UIColor = .white, backgroundImage: UIImage? = nil, type: CircleSpreadButtonType? = nil, task: @escaping () -> ()) {
self.title = title
self.backgroundColor = backgroundColor
self.backgroundImage = backgroundImage
self.type = type
self.task = task
}
}
structの引数にCircleSpreadButtonTypeという型がありますが、これはデフォルトで用意したボタン情報を使用する際に渡すenumですので、独自でボタン情報を設定する場合はnilで問題ないです。
CircleSpreadButtonViewクラスのinit時に作成したstructを渡します。
/// イニシャライズ
///
/// - Parameters:
/// - origin: origin
/// - length: 自身の長さ(絶対正方形にしたいので一辺の長さだけ渡します)
/// - firstSpreadButtonCenterAngle: 一つ目の拡散用ボタンの中心ボタンから見た配置角度(以降は等間隔で配置される)
/// - spreadButtonAngleInterval: 拡散用ボタンの配置間隔の角度
/// - centerButtonInfo: 中心ボタン情報
/// - spreadButtonInfoArray: 拡散用ボタン情報の配列
init(origin: CGPoint, length: CGFloat = 80,
firstSpreadButtonCenterAngle: Double = 100, spreadButtonAngleInterval: Double = 40,
centerButtonInfo: CenterButtonInfo, spreadButtonInfoArray: [SpreadButtonInfo]) {
self.origin = origin
viewSize = CGSize(width: length, height: length)
self.centerButtonInfo = centerButtonInfo
self.spreadButtonInfoArray = spreadButtonInfoArray
self.firstSpreadButtonCenterAngle = firstSpreadButtonCenterAngle
self.spreadButtonAngleInterval = spreadButtonAngleInterval
super.init(frame: CGRect(origin: origin, size: viewSize))
// 自身を円形にする
roundCorner(view: self)
// 拡散用ボタンの生成・配置
setupSpreadButton()
// 中心ボタンの生成・配置
setupCenterButton()
}
// 拡散用ボタンの生成・配置
func setupSpreadButton() {
spreadButtonInfoArray.enumerated().forEach { [weak self] (index, spreadButtonInfo) in
guard let self = self else { return }
let spreadButton = UIButton(frame: CGRect(origin: .zero,
size: CGSize(width: self.viewSize.width * self.spreadButtonSizeParcentage,
height: self.viewSize.height * self.spreadButtonSizeParcentage)))
spreadButton.setBackgroundImage(spreadButtonInfo.type?.backgroundImage ?? spreadButtonInfo.backgroundImage,
for: .normal)
spreadButton.center = self.roundViewCenter(length: self.viewSize.width)
spreadButton.backgroundColor = spreadButtonInfo.type?.backgroundColor ?? spreadButtonInfo.backgroundColor
spreadButton.setTitle(spreadButtonInfo.type?.title ?? spreadButtonInfo.title, for: .normal)
spreadButton.setTitleColor(.white, for: .normal)
spreadButton.setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted)
spreadButton.titleLabel?.lineBreakMode = .byTruncatingTail
spreadButton.tag = index + 1
spreadButton.addTarget(self, action: #selector(spreadButtonAction(sender:)), for: .touchUpInside)
self.addSubview(spreadButton)
self.roundCorner(view: spreadButton)
}
}
// 中心ボタンの生成・配置
func setupCenterButton() {
let centerButton = UIButton(frame: CGRect(origin: .zero, size: viewSize))
centerButton.setBackgroundImage(centerButtonInfo.type?.backgroundImage ?? centerButtonInfo.backgroundImage,
for: .normal)
centerButton.backgroundColor = centerButtonInfo.type?.backgroundColor ?? centerButtonInfo.backgroundColor
centerButton.setTitle(centerButtonInfo.type?.title ?? centerButtonInfo.title, for: .normal)
centerButton.setTitleColor(.white, for: .normal)
centerButton.setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted)
centerButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 24)
centerButton.addTarget(self, action: #selector(centerButtonAction(sender:)), for: .touchUpInside)
addSubview(centerButton)
roundCorner(view: centerButton)
}
※ 実装の細かい部分に関しては、記事の最後にリポジトリのURLを貼ってありますので、そちらを参照ください
アニメーションの実装
用意したボタンを拡散させるアニメーションを実装します。
上で用意した中心ボタンのタップをトリガーにアニメーションさせるようにします。
ちょっと長くなってますが、やっていることは単純で、各ボタンのアニメーション後の座標計算と実行だけです。
@objc func centerButtonAction(sender: UIButton) {
// 連打防止
sender.isEnabled = false
// 拡散用ボタンとアニメーション後のcenterの座標を保持
var buttonPairs = [(button: UIButton, center: CGPoint)]()
// アニメーション後のこのクラスのサイズ
let spreadViewSize = isOpen ? viewSize : CGSize(width: viewSize.width * spreadViewSizeParcentage,
height: viewSize.height * spreadViewSizeParcentage)
// アニメーションさせる前のcenter座標を保持(アニメーション時の位置調節に使用)
let spreadViewCenterBeforeTransform = center
// アニメーション後の中心ボタンのcenter座標
let centerButtonCenter = CGPoint(x: spreadViewSize.width / 2, y: spreadViewSize.height / 2)
// アニメーション後のこのクラスのcenter座標
let spreadViewCenterAfterTransform = self.roundViewCenter(length: spreadViewSize.width)
// アニメーション後の拡散用ボタンのcenter座標を生成
spreadButtonInfoArray.enumerated().forEach { [weak self] (index, _) in
guard let self = self, let spreadButton = self.viewWithTag(index + 1) as? UIButton else {
sender.isEnabled = true
return
}
// 角度ごとの円形状の座標を取得
let degree = firstSpreadButtonCenterAngle + (spreadButtonAngleInterval * Double(index))
let circumferencePoint = self.convert(self.circumferenceCoordinate(degree: degree,
radius: (spreadViewSize.width / 2) - (spreadButton.frame.width * 0.6)), to: self)
// Viewの配置位置に合わせて、アニメーション後の拡散用ボタンのcenter座標を計算
let spreadButtonCenter = CGPoint(x: self.isOpen ? spreadViewCenterAfterTransform.x : spreadViewCenterAfterTransform.x + circumferencePoint.x,
y: self.isOpen ? spreadViewCenterAfterTransform.y : spreadViewCenterAfterTransform.y + circumferencePoint.y)
buttonPairs.append((spreadButton, spreadButtonCenter))
}
// アニメーションの用意
let animation = { [weak self] () -> () in
guard let self = self else { return }
// 本Viewを円形に広げるアニメーション
self.frame.size = spreadViewSize
self.center = spreadViewCenterBeforeTransform
self.roundCorner(view: self)
// 本Viewのアニメーションに合わせて中心ボタンもアニメーションさせる
sender.center = centerButtonCenter
// 生成した拡散用ボタンをアニメーションさせる(中心座標の移動)
buttonPairs.forEach({ (button, center) in
button.center = center
})
}
let animator = self.isOpen ?
UIViewPropertyAnimator(duration: 0.1, curve: .linear, animations: {
animation()
})
:
UIViewPropertyAnimator(duration: 0.7, dampingRatio: 0.5) {
animation()
}
animator.addCompletion { [weak self] (_) in
self?.isOpen.toggle()
sender.isEnabled = true
}
// アニメーションの実行
animator.startAnimation()
}
※ 拡散用ボタンのみ移動させるのではなく、CircleSpreadButtonView自体も拡散に合わせて変形させているのは、親Viewの範囲外まで子Viewが移動するとタップジェスチャーを認識できないためです。
動作確認
実際にCircleSpreadButtonViewをinitしてレイアウトを確認します。
let centerButtonInfo = CenterButtonInfo(title: "Tap", backgroundColor: .black)
let redButtonInfo = SpreadButtonInfo(title: "赤", backgroundColor: .red, backgroundImage: nil, type: nil, task: {
print("赤ボタンを押したよ")
})
let blueButtonInfo = SpreadButtonInfo(title: "青", backgroundColor: .blue, backgroundImage: nil, type: nil, task: {
print("青ボタンを押したよ")
})
let greenButtonInfo = SpreadButtonInfo(title: "緑", backgroundColor: .green, backgroundImage: nil, type: nil, task: {
print("緑ボタンを押したよ")
})
let circleSpreadButtonView = CircleSpreadButtonView(origin: view.center,
length: 80,
centerButtonInfo: centerButtonInfo,
spreadButtonInfoArray: [redButtonInfo, blueButtonInfo, greenButtonInfo])
デフォルトで用意したenumでボタンを生成する場合
let centerButtonInfo = CenterButtonInfo(title: "Tap", backgroundColor: .black)
let cameraButtonInfo = SpreadButtonInfo(type: .camera, task: {
print("カメラ")
})
let libraryButtonInfo = SpreadButtonInfo(type: .library, task: {
print("フォトライブラリ")
})
let writeButtonInfo = SpreadButtonInfo(type: .write, task: {
print("作成")
})
let circleSpreadButtonView = CircleSpreadButtonView(origin: view.center,
length: 80,
centerButtonInfo: centerButtonInfo,
spreadButtonInfoArray: [cameraButtonInfo, libraryButtonInfo, writeButtonInfo])
円形に広がるということで、こんな感じもやってみました。
let centerButtonInfo = CenterButtonInfo(title: "Tap", backgroundColor: .black)
let b1 = SpreadButtonInfo(title: "😆", backgroundColor: .red, task: { print("red") })
let b2 = SpreadButtonInfo(title: "😃", backgroundColor: .blue, task: { print("blue") })
let b3 = SpreadButtonInfo(title: "😌", backgroundColor: .green, task: { print("green") })
let b4 = SpreadButtonInfo(title: "😝", backgroundColor: .cyan, task: { print("cyan") })
let b5 = SpreadButtonInfo(title: "😍", backgroundColor: .purple, task: { print("purple") })
let b6 = SpreadButtonInfo(title: "😀", backgroundColor: .orange, task: { print("orange") })
let b7 = SpreadButtonInfo(title: "🤓", backgroundColor: .brown, task: { print("brown") })
let b8 = SpreadButtonInfo(title: "😁", backgroundColor: .magenta, task: { print("magenta") })
let b9 = SpreadButtonInfo(title: "😊", backgroundColor: .yellow, task: { print("yellow") })
let length: CGFloat = 80
let centerOrigin = CGPoint(x: (view.frame.width / 2) - (length / 2), y: view.frame.height / 2 - (length / 2))
let circleSpreadButtonView = CircleSpreadButtonView(origin: centerOrigin,
length: length,
firstSpreadButtonCenterAngle: 0,
centerButtonInfo: centerButtonInfo,
spreadButtonInfoArray: [b1, b2, b3, b4, b5, b6, b7, b8, b9])