iosで複数のImageViewを拡縮回転して置けるViewを作成をしてみた

## サンプルGif
![RPReplay_Final1709803076.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165363/c280cbac-b1c8-36cf-566e-ce6333b2312f.gif)


## 事前準備など
RxSwiftを使うので追加をしておく
今回はVer6.6.0を使用しました

cocoapods
```
　pod 'RxSwift', '6.6.0'
　pod 'RxCocoa', '6.6.0'
```

CGPointについてのExの作成
```swift
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
```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
```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`が拡縮の最小値と最大値
- 回転は2本指を回転させることで回転ができる
- Imageがない場所をタップすることで選択が解除される

## ざっくりした使い方
StoryboardなどでUiViewを置いてCustomClassをTouchMoveImageManagerViewだけでも使用できる

```swift
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できるような仕様にする