Objective-Cで書かれているプロジェクトをSwiftに書き換えて、Tinder風UIを実装してみました。
作ったものの説明
上記のアニメーションの通りですが、
- カードを左右にスワイプして、中心から一定値を超えるとカードがなくなる
- 下にあるボタンをタップすると、スワイプしたときと同じ処理が走る
- 中心より左にスワイプするとカードに☓マークが表示され、右にスワイプするとチェックマークが表示される
といった内容になっています。
基本的にはStoryboardは使用せず、全てswiftファイルで作られています。
ObjCからSwiftへ書き換えた結果
行数、ファイル数の変化
Objective-C | Swift | |
---|---|---|
行数 | 706 | 424 |
ファイル数 | 11 | 7 |
行数は約40%も削減する結果となり、
ObjCで必要なheaderファイルがなくなったことにより、ファイル数も約37%減りました。
カードViewの作成
import Foundation
import UIKit
let ACTION_MARGIN = 120 //画面中央からどれだけ離れたらカードが自動的に画面から消えるかを決める。
let SCALE_STRENGTH = 4 //カードがシュリンクするスピードを決める。
let SCALE_MAX = 0.93 //カードがどれだけ縮小するかを決める。
let ROTATION_MAX = 1 //カードの回転する大きさを決める。
let ROTATION_STRENGTH = 320
let ROTATION_ANGLE = M_PI/8
// 後に出てくるDraggableViewBackground.swiftで使用する。カードが左右にスワイプされたときのアクションのためのプロトコル。
protocol DraggableViewDelegate {
func cardSwipedLeft(card: UIView)
func cardSwipedRight(card: UIView)
}
class DraggableView: UIView {
var delegate: DraggableViewDelegate?
var information: UILabel = UILabel()
var overlayView: OverlayView?
var panGestureRecognizer: UIPanGestureRecognizer?
var originalPoint: CGPoint = CGPoint()
var xFromCenter: CGFloat = CGFloat()
var yFromCenter: CGFloat = CGFloat()
required init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
information = UILabel(frame:CGRectMake(0, 50, self.frame.size.width, 100))
information.text = "no info given"
information.textAlignment = NSTextAlignment.Center
information.textColor = UIColor.blackColor()
self.backgroundColor = UIColor.blackColor()
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("beingDragged:"))
self.addGestureRecognizer(panGestureRecognizer!)
self.addSubview(information)
overlayView = OverlayView(frame: CGRectMake(self.frame.size.width/2-100, 0, 100, 100))
overlayView!.alpha = 0
self.addSubview(overlayView!)
}
func setupView() {
self.layer.cornerRadius = 4
self.layer.shadowRadius = 3
self.layer.shadowOpacity = 0.2
self.layer.shadowOffset = CGSizeMake(1, 1)
}
func beingDragged(gestureRecognizer: UIPanGestureRecognizer) {
var xFromCenter = gestureRecognizer.translationInView(self).x
var yFromCenter = gestureRecognizer.translationInView(self).y
switch (gestureRecognizer.state) {
case UIGestureRecognizerState.Began:
self.originalPoint = self.center;
break;
case UIGestureRecognizerState.Changed:
var rotationStrength: CGFloat = min(xFromCenter / CGFloat(ROTATION_STRENGTH), CGFloat(ROTATION_MAX))
var rotationAngel: CGFloat = CGFloat(ROTATION_ANGLE) * rotationStrength
var scale: CGFloat = max(1 - CGFloat(fabsf(Float(rotationStrength))) / CGFloat(SCALE_STRENGTH), CGFloat(SCALE_MAX))
self.center = CGPointMake(self.originalPoint.x + xFromCenter, self.originalPoint.y + yFromCenter)
var transform: CGAffineTransform = CGAffineTransformMakeRotation(rotationAngel)
var scaleTransform: CGAffineTransform = CGAffineTransformScale(transform, scale, scale)
self.transform = scaleTransform
self.updateOverlay(xFromCenter)
break
case UIGestureRecognizerState.Ended:
self.afterSwipeAction(xFromCenter)
break
case UIGestureRecognizerState.Possible:
break
case UIGestureRecognizerState.Cancelled:
break
case UIGestureRecognizerState.Failed:
break
}
}
func updateOverlay(distance: CGFloat) {
if distance > 0 {
overlayView!.setMode(GGOverlayViewMode.Right)
} else {
overlayView!.setMode(GGOverlayViewMode.Left)
}
overlayView!.alpha = min(CGFloat(fabsf(Float(distance))/100), 0.4)
}
func afterSwipeAction(xFromCenter: CGFloat) {
if xFromCenter > CGFloat(ACTION_MARGIN) {
self.rightAction()
} else if xFromCenter < CGFloat(-ACTION_MARGIN) {
self.leftAction()
} else {
UIView.animateWithDuration(0.3, animations: {
self.center = self.originalPoint
self.transform = CGAffineTransformMakeRotation(0)
self.overlayView!.alpha = 0
})
}
}
func rightAction() {
var finishPoint: CGPoint = CGPointMake(500, 2*yFromCenter + self.originalPoint.y)
UIView.animateWithDuration(0.3, animations: {
self.center = finishPoint
}, completion: { (value: Bool) in
self.removeFromSuperview()
})
delegate?.cardSwipedRight(self)
NSLog("YES")
}
func leftAction() {
var finishPoint: CGPoint = CGPointMake(-500, 2*yFromCenter + self.originalPoint.y)
UIView.animateWithDuration(0.3, animations: {
self.center = finishPoint
}, completion: { (value: Bool) in
self.removeFromSuperview()
})
delegate?.cardSwipedLeft(self)
NSLog("NO")
}
func rightClickAction() {
var finishPoint: CGPoint = CGPointMake(600, self.center.y)
UIView.animateWithDuration(0.3, animations: {
self.center = finishPoint
self.transform = CGAffineTransformMakeRotation(1)
}, completion: { (value: Bool) in
self.removeFromSuperview()
})
delegate?.cardSwipedRight(self)
NSLog("YES")
}
func leftClickAction() {
var finishPoint: CGPoint = CGPointMake(-600, self.center.y)
UIView.animateWithDuration(0.3, animations: {
self.center = finishPoint
self.transform = CGAffineTransformMakeRotation(-1)
}, completion: { (value: Bool) in
self.removeFromSuperview()
})
delegate?.cardSwipedRight(self)
NSLog("NO")
}
}
メイン画面のView生成
import Foundation
import UIKit
class DraggableViewBackground: UIView, DraggableViewDelegate{
var cardsLoadedIndex:Int = Int()
var loadedCards: NSMutableArray = NSMutableArray()
var menuButton: UIButton = UIButton()
var messageButton: UIButton = UIButton()
var xButton: UIButton = UIButton()
var checkButton: UIButton = UIButton()
let MAX_BUFFER_SIZE: Int = 2
let CARD_HEIGHT: CGFloat = 260
let CARD_WIDTH: CGFloat = 260
var exampleCardLabels: NSArray = NSArray()
var allCards: NSMutableArray = NSMutableArray()
required init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
super.layoutSubviews()
self.setupView()
exampleCardLabels = ["hoge1", "hoge2", "hoge3", "hoge4", "hoge5"]
loadedCards = []
cardsLoadedIndex = 0
self.loadCards()
}
func setupView() {
self.backgroundColor = UIColor(red: 0.92, green: 0.93, blue: 0.95, alpha: 1.0);
menuButton = UIButton(frame:CGRectMake(17,34,22,15))
messageButton = UIButton(frame:CGRectMake(284,34,18,18))
xButton = UIButton(frame:CGRectMake(60,485,59,59))
checkButton = UIButton(frame:CGRectMake(200,485,59,59));
let menuButtonImage = UIImage(named: "menuButton")
let messageButtonImage = UIImage(named: "messageButton")
let xButtonImage = UIImage(named: "xButton")
let checkButtonImage = UIImage(named: "checkButton")
menuButton.setImage(menuButtonImage, forState: UIControlState.Normal)
messageButton.setImage(messageButtonImage, forState: UIControlState.Normal)
xButton.setImage(xButtonImage, forState: UIControlState.Normal)
checkButton.setImage(checkButtonImage, forState: UIControlState.Normal)
xButton.addTarget(self, action: "swipeLeft", forControlEvents: .TouchUpInside)
checkButton.addTarget(self, action: "swipeRight", forControlEvents: .TouchUpInside)
self.addSubview(menuButton)
self.addSubview(messageButton)
self.addSubview(xButton)
self.addSubview(checkButton)
}
func createDraggableViewWithDataAtIndex(index: Int) -> DraggableView {
var draggableView: DraggableView = DraggableView(frame:CGRectMake(30, 100, CARD_WIDTH, CARD_HEIGHT))
draggableView.information.text = exampleCardLabels.objectAtIndex(index) as String
draggableView.backgroundColor = UIColor.whiteColor()
draggableView.delegate = self
return draggableView
}
func loadCards() {
if (exampleCardLabels.count > 0) {
var numLoadedCardsCap = ((exampleCardLabels.count > MAX_BUFFER_SIZE) ? MAX_BUFFER_SIZE : exampleCardLabels.count )
for i in 0..<exampleCardLabels.count {
var newCard: DraggableView = self.createDraggableViewWithDataAtIndex(i)
allCards.addObject(newCard)
if (i < numLoadedCardsCap) {
loadedCards.addObject(newCard)
}
}
for i in 0..<loadedCards.count {
if (i > 0) {
self.insertSubview(loadedCards.objectAtIndex(i) as UIView, belowSubview: loadedCards.objectAtIndex(i-1) as UIView)
} else {
self.addSubview(loadedCards.objectAtIndex(i) as UIView)
}
cardsLoadedIndex++
}
}
}
func cardSwipedLeft(card: UIView) {
loadedCards.removeObjectAtIndex(0)
if ( cardsLoadedIndex < allCards.count ) {
loadedCards.addObject(allCards.objectAtIndex(cardsLoadedIndex))
cardsLoadedIndex++
self.insertSubview(loadedCards.objectAtIndex(MAX_BUFFER_SIZE-1) as UIView, belowSubview: loadedCards.objectAtIndex(MAX_BUFFER_SIZE-2) as UIView)
}
}
func cardSwipedRight(card: UIView) {
loadedCards.removeObjectAtIndex(0)
if ( cardsLoadedIndex < allCards.count ) {
loadedCards.addObject(allCards.objectAtIndex(cardsLoadedIndex))
cardsLoadedIndex++
self.insertSubview(loadedCards.objectAtIndex(MAX_BUFFER_SIZE-1) as UIView, belowSubview: loadedCards.objectAtIndex(MAX_BUFFER_SIZE-2) as UIView)
}
}
func swipeRight() {
var dragView: DraggableView = loadedCards.firstObject as DraggableView
dragView.overlayView!.mode = GGOverlayViewMode.Right
UIView.animateWithDuration(0.2, animations: {
dragView.overlayView!.alpha = 1
})
dragView.rightClickAction()
}
func swipeLeft() {
var dragView: DraggableView = loadedCards.firstObject as DraggableView
dragView.overlayView!.mode = GGOverlayViewMode.Left
UIView.animateWithDuration(0.2, animations: {
dragView.overlayView!.alpha = 1
})
dragView.leftClickAction()
}
}
スワイプしたときにカード上にxマークやチェックマークが出るようにする
import Foundation
import UIKit
enum GGOverlayViewMode: Int {
case Left
case Right
}
class OverlayView: UIView{
var mode: GGOverlayViewMode?
var imageView: UIImageView = UIImageView()
required init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.whiteColor()
imageView = UIImageView(image:UIImage(named: "noButton"))
self.addSubview(imageView)
}
func setMode(mode: GGOverlayViewMode) {
println("load setMode")
println(mode)
if mode == GGOverlayViewMode.Left {
imageView.image = UIImage(named: "noButton")
} else {
imageView.image = UIImage(named: "yesButton")
}
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = CGRectMake(50, 50, 100, 100)
}
}
最後にViewControllerでDraggableViewBackgroundを呼び出す
ViewControllerの viewDidLoad
に下記を追加
var draggableBackground = DraggableViewBackground(frame: self.view.frame)
self.view.addSubview(draggableBackground)
まとめ
ObjCからSwiftへ完全書き換えをするのにはかなり労力を要しましたが、
全てがキュッとスリムになり、なかなかやり甲斐のある内容でした。
まだまだリファクタできる部分があると思うので、何かあればよろしくですm(_ _)m
諸事情でbeta5で作っていたのですが、beta7で動くように書き換えてくれる人大歓迎です(笑
Githubはこちらです。
過去に書いた記事
他にもSwift関連で色々書いてみたので、興味有る方は是非。 -> 過去記事一覧
- SwiftのNotificationでHello, Worldして、アクションを3つ作ってみた(Xcode6 beta4)
- mBaaSを使ってみよう!超簡単にSwiftでTwitterライクなポスト機能を作る(Xcode6 beta5, Parse.com)
- SwiftでTo Doリストを作ってみた(Xcode6 beta4, Tabbed Application, UITableView, UITextField)
- Swift & Parseを使ったユーザー登録、ログイン、ログアウト(Swift, Xcode6 beta5,Parse)
- 下にスワイプししたらTableViewがリロードされてParseからデータ更新するのをSwiftで書いた(Swift, Xcode6 beta5, Parse, UITableView)
- SwiftでFacebook SDKを使用したログイン機能(Xcode6 beta6)