サンプルGif
事前準備など
RxSwiftを使うので追加をしておく
今回はVer6.6.0を使用しました
cocoapods
pod 'RxSwift', '6.6.0'
pod 'RxCocoa', '6.6.0'
CGPointについてのExの作成
extension CGPoint {
// 二点間の距離を返す
func length(from: CGPoint) -> CGFloat {
return sqrt(pow(self.x - from.x, 2.0) + pow(self.y - from.y, 2.0))
}
// 二点を結んだ線分の角度(ラジアン)を返す
func radian(from: CGPoint) -> CGFloat {
return atan2(self.y - from.y, self.x - from.x)
}
// 二点を結んだ線分の角度(度)を返す
func degree(from: CGPoint) -> CGFloat {
return atan2(self.y - from.y, self.x - from.x) * 180 / CGFloat.pi
}
}
本体コード
- TouchMoveImageManagerView.swift
import Foundation
import UIKit
struct TouchMoveImageViewConstData {
static let BORDER_COLOR : CGColor = UIColor.blue.cgColor
static let BORDER_WIDTH : CGFloat = 2
static let SCALE_MAX : CGFloat = 10
static let SCALE_MIN : CGFloat = 0.5
}
protocol TouchMoveImageManagerViewDelegate{
func longPress(_ gesture : UILongPressGestureRecognizer,selectView: TouchMoveImageView?)
}
class TouchMoveImageManagerView : UIView{
var delegate: TouchMoveImageManagerViewDelegate?
// 追加をしているView
var m_ListViews : NSMutableArray = []
// 選択しているView
var m_SelectView : TouchMoveImageView?
// タッチしているView
var m_TouchView : TouchMoveImageView?
// 回転用
var m_fViewAngle : CGFloat = 0
var m_fTouchAngle : CGFloat = 0
// 拡縮用
var m_fSelectViewScale : CGFloat = 0
var m_fTouchDistance : CGFloat = 0
// 移動 or 拡縮回転
var isTouchMove = false
override init(frame: CGRect) {
super.init(frame: frame)
initSetting()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSetting()
}
deinit {
}
func initSetting(){
self.isMultipleTouchEnabled = true
let longPressGesture = UILongPressGestureRecognizer(
target: self,
action: #selector(longPress(_:))
)
self.addGestureRecognizer(longPressGesture)
}
// UIImageから作成する場合
public func addTouchMoveView(image :UIImage){
let view = TouchMoveImageView(frame: CGRect(origin: CGPoint.zero, size: image.size))
view.center = self.center
view.image = image
addTouchMoveView(view)
}
// Viewから生成する場合
public func addTouchMoveView(_ view :TouchMoveImageView){
m_ListViews.add(view)
self.addSubview(view)
}
// 全削除
public func removeAllTouchMoveView(){
for vi in m_ListViews{
(vi as! TouchMoveImageView).removeFromSuperview()
}
m_ListViews.removeAllObjects()
m_TouchView = nil
}
// index指定での削除
func removeTouchMoveView(index :Int){
if(index < m_ListViews.count){
let vi = m_ListViews.object(at: index)
removeTouchMoveView(vi as! TouchMoveImageView)
}
}
// view指定での削除時
func removeTouchMoveView(_ view :TouchMoveImageView){
view.removeFromSuperview()
m_ListViews.remove(view)
}
// 長押しの動作
@objc func longPress(_ sender: UILongPressGestureRecognizer) {
self.delegate?.longPress(sender, selectView: m_SelectView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if(event?.allTouches?.count == 1){
isTouchMove = true
m_TouchView = nil
let touch = touches.first!
let location = touch.location(in: self)
var touchView : TouchMoveImageView?
for vi in m_ListViews{
if(CGRectContainsPoint((vi as! UIView).frame, location)){
touchView = vi as? TouchMoveImageView
break
}
}
if(touchView != nil){
if let vi = m_SelectView{
vi.isSelectedBehavor.accept(false)
m_SelectView = nil
}
m_TouchView = touchView
m_SelectView = touchView
m_SelectView?.isSelectedBehavor.accept(true)
}else{
if let vi = m_SelectView{
vi.isSelectedBehavor.accept(false)
m_SelectView = nil
}
}
}else if(event?.allTouches?.count == 2){
isTouchMove = false
let ary : NSMutableArray = []
for touch in event!.allTouches! {
ary.add(touch.location(in: self))
}
let touch01 = ary[0] as! CGPoint
let touch02 = ary[1] as! CGPoint
// 距離
m_fTouchDistance = touch01.length(from: touch02)
// 角度
m_fTouchAngle = touch01.degree(from: touch02)
if let vi = m_SelectView{
m_fViewAngle = vi.m_fAngleBehavor.value
m_fSelectViewScale = vi.m_fScaleBehavor.value
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if(m_TouchView != nil){
m_TouchView = nil
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if(isTouchMove){
let touch = event?.allTouches?.first
if(m_TouchView != nil && touch != nil){
m_TouchView!.center = touch!.location(in: self)
}
}else if(!isTouchMove && event?.allTouches?.count == 2){
let alltouch = event?.allTouches
let ary : NSMutableArray = []
for touch in alltouch! {
ary.add(touch.location(in: self))
}
let touch01 = ary[0] as! CGPoint
let touch02 = ary[1] as! CGPoint
let dist = touch01.length(from: touch02)
let ang = touch01.degree(from: touch02)
if let vi = m_SelectView {
// 角度
vi.m_fAngleBehavor.accept(m_fViewAngle + ang - m_fTouchAngle)
let scalelng = dist - m_fTouchDistance
let addscale = scalelng / vi.m_SizeDefult.width
// スケール
vi.m_fScaleBehavor.accept(m_fSelectViewScale + addscale)
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
}
}
- TouchMoveImageView.swift
import UIKit
import RxSwift
import RxRelay
class TouchMoveImageView : UIImageView{
private let disposeBag = DisposeBag()
// 位置サイズ
public var m_SizeDefult:CGSize!
// 回転
var m_fAngleBehavor: BehaviorRelay<CGFloat>!
// スケール
var m_fScaleBehavor : BehaviorRelay<CGFloat>!
var m_fScaleMin : CGFloat!
var m_fScaleMax : CGFloat!
// 選択されているかどうか
var isSelectedBehavor : BehaviorRelay<Bool>!
// 選択時のborder
var m_colorBorder : CGColor!
var m_fBorderWidth : CGFloat!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
m_SizeDefult = frame.size
initSetting()
setRxContents()
}
init(frame : CGRect,
borderColor : CGColor = TouchMoveImageViewConstData.BORDER_COLOR,
borderWidth : CGFloat = TouchMoveImageViewConstData.BORDER_WIDTH,
scaleMax : CGFloat = TouchMoveImageViewConstData.SCALE_MAX,
scaleMin : CGFloat = TouchMoveImageViewConstData.SCALE_MIN) {
super.init(frame: frame)
m_SizeDefult = frame.size
m_colorBorder = borderColor
m_fBorderWidth = borderWidth
m_fScaleMax = scaleMax
m_fScaleMin = scaleMin
setRxContents()
}
func initSetting(){
m_colorBorder = TouchMoveImageViewConstData.BORDER_COLOR
m_fBorderWidth = TouchMoveImageViewConstData.BORDER_WIDTH
m_fScaleMax = TouchMoveImageViewConstData.SCALE_MAX
m_fScaleMin = TouchMoveImageViewConstData.SCALE_MIN
}
func setRxContents(){
m_fAngleBehavor = BehaviorRelay(value: 1.0)
m_fScaleBehavor = BehaviorRelay(value: 1)
isSelectedBehavor = BehaviorRelay(value: false)
// 角度
m_fAngleBehavor.asObservable()
.subscribe (onNext: { ang in
self.changeAngleScale(angle: ang, scale: self.m_fScaleBehavor.value)
}).disposed(by:disposeBag)
// 拡縮
m_fScaleBehavor.asObservable()
.map({ scale in
min(max(scale, self.m_fScaleMin), self.m_fScaleMax)
})
.subscribe (onNext: { scale in
self.changeAngleScale(angle: self.m_fAngleBehavor.value, scale: scale)
}).disposed(by:disposeBag)
// 選択時の枠
isSelectedBehavor.asObservable()
.subscribe(onNext: { b in
self.layer.borderColor = self.m_colorBorder
self.layer.borderWidth = b ? self.m_fBorderWidth : 0
}).disposed(by:disposeBag)
}
func changeAngleScale(angle : CGFloat,scale : CGFloat){
let t1 = CGAffineTransform(scaleX: scale, y: scale)
let a1 = angle * CGFloat.pi / 180
let t2 = CGAffineTransform(rotationAngle: a1)
let t3 = CGAffineTransformConcat(t1, t2)
self.transform = t3
}
}
操作などについて
- タップをすることでImageViewの選択
- 選択されると枠が表示される
- 1本指でImageを選択して、移動することができる。この時指の位置を基準にセンタリングされて移動をする
- 拡縮と回転をする場合は2本指を使用する
- 拡縮は2本指の距離が離れた場合、それに応じて拡縮を行う
- TouchMoveImageViewの
m_fScaleMin
とm_fScaleMax
が拡縮の最小値と最大値
- TouchMoveImageViewの
- 回転は2本指を回転させることで回転ができる
- Imageがない場所をタップすることで選択が解除される
ざっくりした使い方
StoryboardなどでUiViewを置いてCustomClassをTouchMoveImageManagerViewだけでも使用できる
class ViewController: UIViewController,TouchMoveImageManagerViewDelegate {
@IBOutlet weak var m_touchMoveView: TouchMoveImageManagerView!
@IBOutlet weak var m_btnAdd: UIButton!
@IBOutlet weak var m_btnRemove: UIButton!
@IBOutlet weak var m_btnAllRemove: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
m_touchMoveView.delegate = self
}
@IBAction func onClick(sender : UIButton){
switch(sender.tag){
case m_btnAdd.tag:
m_touchMoveView.addTouchMoveView(image: UIImage(named: "***")!)
break
case m_btnRemove.tag:
m_touchMoveView.removeTouchMoveView(index: 0)
break
case m_btnAllRemove.tag:
m_touchMoveView.removeAllTouchMoveView()
break
default:
break
}
}
func longPress(_ gesture: UILongPressGestureRecognizer, selectView: TouchMoveImageView?) {
if gesture.state == .began {
if(selectView != nil){
let alert: UIAlertController = UIAlertController(title: "削除しますか?", message: "", preferredStyle: UIAlertController.Style.alert)
// 確定ボタンの処理
let confirmAction: UIAlertAction = UIAlertAction(title: "削除", style: UIAlertAction.Style.default, handler:{ [self]
(action: UIAlertAction!) -> Void in
if let vi = selectView{
m_touchMoveView.removeTouchMoveView(vi)
}
})
// キャンセルボタンの処理
let cancelAction: UIAlertAction = UIAlertAction(title: "キャンセル", style: UIAlertAction.Style.cancel, handler:{
(action: UIAlertAction!) -> Void in
})
alert.addAction(confirmAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
}
}
}
sampleのGifのコードとしてはこちら
- AddImageを押した場合にスタンプを追加
- Remeveを押した場合にスタンプの削除
- AllRemeveを押した場合に追加をしたスタンプの全削除
- 選択したViewを長押しをした場合に削除ダイアログの表示
というような仕様で作成しました。
Future
現状するかどうかは不明
- Githubなどで公開、Cocoadpodsなどの対応
- Scale値について上限と下限を過ぎた場合にそのままの数値になっているので、対応をする
- 2点で触って回転をした際に、たまに画像が反転してしまうバグの修正
- 外枠のカラー指定をできるようにする
- センター以外の場所にもAddできるような仕様にする