[Swift] Tinder風のUIを実装してみよう🔥


## 今回作るアプリ
今回はTinderのようなUIをUIKitを用いて実装していきます。
また今回の前提として、UIKitのみを用いて実装していきたいと思います。
そのため、余計なライブラリのインストール手段とかを取っ払っていけます！笑

さらにUIはコードベースで実装し**余計な責務も持たせないこと**で少しでもパフォーマンスの良いコードを書いていきます。

今回実装する完成形UIはこんな感じです！
解説はコード内でコメントアウトしているので実際に動かしてみていただけると理解しやすいかと思います。


![Simulator Screen Recording - iPhone 13 Pro - 2022-04-10 at 02.29.23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/552896/1d5787cd-b9a4-a72a-5b69-8ae420e138f8.gif)



## ソースコード

### ViewController
```ViewController.swift
import UIKit

final class ViewController: UIViewController {
    
    // MARK: - Properties
    
    // カードのViewをデッキViewとして画面に貼る
    private let deckView: UIView = {
        let view = UIView()
        view.layer.cornerRadius = 20
        view.backgroundColor = .white
        return view
    }()
    
    // 表示するカードの配列
    private var cardViews = [CardView(imageName: "user1"),
                             CardView(imageName: "user2"),
                             CardView(imageName: "user3"),
                             CardView(imageName: "user4")]
    
    // MARK: - Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
        configureCardViews()
    }
    
    // MARK: - Helper functions
    
    // UIの制約
    private func configureUI() {
        view.addSubview(deckView)
        deckView.translatesAutoresizingMaskIntoConstraints = false
        deckView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
        deckView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 12).isActive = true
        deckView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
        deckView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -12).isActive = true
    }
    
    // deckViewへの貼付
    private func configureCardViews() {
        cardViews.forEach { cardView in
            deckView.addSubview(cardView)
            cardView.translatesAutoresizingMaskIntoConstraints = false
            cardView.topAnchor.constraint(equalTo: deckView.topAnchor).isActive = true
            cardView.leftAnchor.constraint(equalTo: deckView.leftAnchor).isActive = true
            cardView.bottomAnchor.constraint(equalTo: deckView.bottomAnchor).isActive = true
            cardView.rightAnchor.constraint(equalTo: deckView.rightAnchor).isActive = true
        }
    }
}

```

### CardView
カードを表示するUIViewをカスタムしていきます。

```CardView.swift
import UIKit

final class CardView: UIView {
    
    // MARK: - Properties
    
    private let imageView: UIImageView = {
        let iv = UIImageView()
        iv.contentMode = .scaleAspectFill
        iv.clipsToBounds = true
        iv.layer.cornerRadius = 20
        return iv
    }()
    
    // MARK: - Lifecycle
    
    override init(frame: CGRect) {
        super.init(frame: .zero)
        configureUI()
    }
    
    init(imageName: String) {
        self.init()
        imageView.image = UIImage(named: imageName)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Helper functions
    
    // panGestureとimageViewの貼付
    private func configureUI() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture))
        addGestureRecognizer(panGesture)
        
        addSubview(imageView)
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        imageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
        imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        imageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
    }
    
    // panGestureの動きをswitchで分岐
    @objc
    private func handlePanGesture(sender: UIPanGestureRecognizer) {
        switch sender.state {
        case .began:
            superview?.subviews.forEach { $0.layer.removeAllAnimations() }
        case .changed:
            panCard(sender: sender)
        case .ended:
            resetCardPosition(sender: sender)
        default:
            break
        }
    }
    
    // カードを持った時、指についていく滑らかな移動の処理
    private func panCard(sender: UIPanGestureRecognizer) {
        let translation = sender.translation(in: nil)
        let degrees: CGFloat = translation.x / 20
        let angle = degrees * .pi / 180
        let rotationalTransform = CGAffineTransform(rotationAngle: angle)
        transform = rotationalTransform.translatedBy(x: translation.x, y: translation.y)
    }
    
    // スワイプが終わった時のカードが退く動きの処理
    private func resetCardPosition(sender: UIPanGestureRecognizer) {
        let direction: SwipeDirection = sender.translation(in: nil).x > 0 ? .right : .left
        let shouldDismissCard = abs(sender.translation(in: nil).x) > 100
        
        UIView.animate(withDuration: 0.75, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.1, options: .curveEaseOut, animations: { [weak self] in
            guard let me = self else { return }
            if shouldDismissCard {
                let xTranslation = CGFloat(direction.rawValue) * 1000
                let offScreenTransform = me.transform.translatedBy(x: xTranslation, y: 0)
                me.transform = offScreenTransform
            } else {
                me.transform = .identity
            }
        }) { [weak self] _ in
            guard let me = self else { return }
            print("DEBUG: アニメーション終了時の処理")
            if shouldDismissCard {
                me.removeFromSuperview()
            }
        }
    }
}
```

```Swift
enum SwipeDirection: Int {
    case left = -1
    case right = 1
}
```

## 最後に
今回はコードベースで実装しているので、コピペして再利用していただきやすいと思います。
ぜひ、必要な際にお使いください。

自分はこういったUIを実装するのが比較的好みなので少しでも反響があれば、これにさまざまな機能を実装してみたいと思います！

最後までご覧いただきありがとうございました。