137
95

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swiftでハートボタンを作る(Instagram風)

Posted at

はじめに

Instagramのアプリにあるようなハート型のボタンを作成し、OSSとして公開しましたので、解説として本記事を書きました。

HeartButton

作成したライブラリはこちらです。
https://github.com/darquro/heart-button

タップすると、アニメーションして状態が変わるというシンプルなものです。

UIBezierPathを使ってハート型を描く

今回はUIBezierPathを使って、線を描画し、ハートを描いています。
よくボタンはpngなど画像リソースを使用することが多く、
製品プロダクトではデザイナーによるアプリ全体のテーマに合わせた画像を使うべきだと思いますが、個人で作るアプリなどで、いちいち各解像度ごとの画像用意するのが面倒というケースでは用途あるのではないかと思います。

以下がUIBezierPathを使用して、ハートを描いているコードになります。

extension UIBezierPath {

    /// Create heart path
    ///
    /// - Parameter rect: draw in the rect
    convenience init(heartIn rect: CGRect) {
        self.init()
        
        let bottomCenter = CGPoint(x: rect.width * 0.5, y: rect.height)
        let topCenter = CGPoint(x: rect.width * 0.5, y: rect.height * 0.25)
        let leftSideControl = CGPoint(x: -(rect.width * 0.45), y: (rect.height * 0.45))
        let leftTopControl = CGPoint(x: (rect.width * 0.25), y: -(rect.height * 0.2))
        let rightTopControl = CGPoint(x: rect.width - leftTopControl.x, y: leftTopControl.y)
        let rightSideControl = CGPoint(x: rect.width + (leftSideControl.x * -1), y: leftSideControl.y)
        
        self.move(to: bottomCenter)
        
        // Left Side Curve
        self.addCurve(to: topCenter,
                      controlPoint1: leftSideControl,
                      controlPoint2: leftTopControl)
        
        // Right Side Curve
        self.addCurve(to: bottomCenter,
                      controlPoint1: rightTopControl,
                      controlPoint2: rightSideControl)
        
        self.close()
    }
}

幾つかハート型を描く方法はあるかと思いますが、これが一番シンプルな方法だと思います。

UIBezierPathaddCurve(to:controlPoint1:controlPoint2:)を使用し、
中央下の位置から左側の線、右側の線と描きます。

controlPointをどのように配置するかで、カーブの形が変わってきます。
以下の図がそのイメージになります。

heart.001.png

線を引っ張るようなイメージでcontrolPointを決めていくといいと思います。
ざっくりとイメージが出来たらPlaygroundで描画しながら微調整していきました。

また、OffのときのハートとOnのときのハートはそれぞれ別のCAShapeLayerを作成して、addSubLayerしていきます。

注意
UIColorsetStroke()setFill()UIViewのdrawRectで単一の描画する際には使えますが、複数のCAShapeLayerを使用する場合は、CAShapeLayer側に線の色や塗りつぶし色を指定します。

extension CAShapeLayer {
   
   /// Create heart shape layer
   ///
   /// - Parameters:
   ///   - rect: draw in the rect
   ///   - lineWidth: used to line width
   ///   - lineColor: used to line color
   ///   - fillColor: used to fill color
   convenience init(heartIn rect: CGRect,
                    lineWidth: CGFloat,
                    lineColor: UIColor,
                    fillColor: UIColor) {
       self.init()
       let path = UIBezierPath(heartIn: rect)
       self.path = path.cgPath
       self.lineWidth = lineWidth
       self.strokeColor = lineColor.cgColor
       self.fillColor = fillColor.cgColor
   }
}

UIBezierPathを使用した場合のメリットとして、Storyboard上でのデザイン変更ができるということもあります。

screen_capture_3.png

@IBDesignableのattributeを使用することで、線幅や色などを指定できるようにしました。

ステータスの変更

ハンドラはdelegateパターンではなくクロージャをそのままプロパティに設定する形式にしました。

self.heartButton.stateChanged = { sender, isOn in
    if isOn {
      // selected
    } else {
      // unselected
    }
}

状態をコードから変更するには以下のようになります。

self.heartButton.setOn(true, animated: true)

アニメーション

アニメーションはCASpringAnimationを使用しました。バネのアニメーションの細かい指定が可能です。

extension CASpringAnimation {
    
    /// The Expansion and bouncing animation
    static var expansionAndBouncingAnimation: CASpringAnimation {
        let anim = CASpringAnimation(keyPath: "transform.scale") //変化させるプロパティ(今回はCAShapeLayer#transform.scale)
        anim.fromValue = 0.5         // 開始
        anim.toValue = 1.0           // 終了
        anim.mass = 1.0              // 質量
        anim.initialVelocity = 30.0  // 初速度
        anim.damping = 30.0          // 減衰率
        anim.stiffness = 120.0       // バネの硬さ
        anim.duration = anim.settlingDuration // 設定により、停止するまでの時間を指定
        return anim
    }
}

この初速度、減衰率、バネの硬さの数値も大体で設定して、微調整しました。

上記のCASpringAnimationを使用し、タップジェスチャーから、

  1. 変更後レイヤーを表示
  2. レイヤーの順番変更
  3. アニメーション
  4. 変更前レイヤーを非表示

という順番で行っています。

// MARK: Changes The button display
    
/// Change display to on state
///
/// - Parameter animated: true to animate, otherwise false.
private func changeDisplayToOn(animated: Bool) {
    onLayer?.isHidden = false
    switchLayer()
    if animated {
        let animGroup = CAAnimationGroup()
        animGroup.animations = self.onAnimations.compactMap { $0 }
        layer.add(animGroup, forKey: nil)
    }
    offLayer?.isHidden = true
}
    
/// Change display to off state
///
/// - Parameter animated: true to animate, otherwise false.
private func changeDisplayToOff(animated: Bool) {
    offLayer?.isHidden = false
    switchLayer()
    if animated {
        let animGroup = CAAnimationGroup()
        animGroup.animations = self.offAnimations.compactMap { $0 }
        layer.add(animGroup, forKey: nil)
    }
    onLayer?.isHidden = true
}
    
/// Switch the order of sub layers
private func switchLayer() {
    guard let sublayers = self.layer.sublayers,
        sublayers.count == 2,
        let firstLayer = self.layer.sublayers?.first,
        let secondLayer = self.layer.sublayers?.last
        else {
            return
    }
    self.layer.insertSublayer(secondLayer, at: 0)
    self.layer.insertSublayer(firstLayer, at: 1)
}

以上です。よかったら使ってみてください!

137
95
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
137
95

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?