LoginSignup
23
16

More than 3 years have passed since last update.

【Swift】 円形に拡散するボタンを実装

Last updated at Posted at 2019-06-29

はじめに

以下のようなTwitterのツイートボタンライクなUIを実装してみます。
tweet_button

実装イメージ

複数のボタンを重ねて配置し、アニメーションで拡散するようにしてみます。
circleButton_image circleButton_image_top

レイアウト実装

今回は、UIViewのサブクラスを用意して、そのクラス内で必要なボタンを全て配置してみるようにします。

CircleSpreadButtonView.swift
class CircleSpreadButtonView: UIView {}

なので、CircleSpreadButtonViewのinit時に配置するボタンに関する情報を渡してあげます。
今回は以下のstructにまとめて渡すようにしました。

CircleSpreadButtonInfo.swift
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を渡します。

CircleSpreadButtonView.swift
/// イニシャライズ
    ///
    /// - 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を貼ってありますので、そちらを参照ください

アニメーションの実装

用意したボタンを拡散させるアニメーションを実装します。
上で用意した中心ボタンのタップをトリガーにアニメーションさせるようにします。
ちょっと長くなってますが、やっていることは単純で、各ボタンのアニメーション後の座標計算と実行だけです。

CircleSpreadButtonView.swift
    @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してレイアウトを確認します。

ViewController.swift
        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])

circleButton_sample_custom

デフォルトで用意したenumでボタンを生成する場合

ViewController.swift
        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])

circleButton_sample_default

円形に広がるということで、こんな感じもやってみました。

ViewController.swift
        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])

circleButton_sample_colorful

ソースコード

今回作ったサンプルのソースは以下のリポジトリにあります。
https://github.com/ddd503/CircleSpreadButton

23
16
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
16