今回作るアプリ
今回はTinderのようなUIをUIKitを用いて実装していきます。
また今回の前提として、UIKitのみを用いて実装していきたいと思います。
そのため、余計なライブラリのインストール手段とかを取っ払っていけます!笑
さらにUIはコードベースで実装し余計な責務も持たせないことで少しでもパフォーマンスの良いコードを書いていきます。
今回実装する完成形UIはこんな感じです!
解説はコード内でコメントアウトしているので実際に動かしてみていただけると理解しやすいかと思います。
ソースコード
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()
}
}
}
}
enum SwipeDirection: Int {
case left = -1
case right = 1
}
最後に
今回はコードベースで実装しているので、コピペして再利用していただきやすいと思います。
ぜひ、必要な際にお使いください。
自分はこういったUIを実装するのが比較的好みなので少しでも反響があれば、これにさまざまな機能を実装してみたいと思います!
最後までご覧いただきありがとうございました。