Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Beck] Step5 - Touch and Drag 1 #109

Open
wants to merge 7 commits into
base: Beck
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions DrawingApp/DrawingApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
1B1D7E0627E3510400683FA9 /* FactoryMainScreenRectangle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B1D7E0527E3510400683FA9 /* FactoryMainScreenRectangle.swift */; };
1B4A55AA27CD99EC009CDC7B /* RandomizeValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B4A55A927CD99EC009CDC7B /* RandomizeValue.swift */; };
1B4A55AC27CD9F96009CDC7B /* RectanglePropertyCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B4A55AB27CD9F96009CDC7B /* RectanglePropertyCreator.swift */; };
1B54997127D09BE30086EC5F /* Plane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B54997027D09BE30086EC5F /* Plane.swift */; };
@@ -61,6 +62,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
1B1D7E0527E3510400683FA9 /* FactoryMainScreenRectangle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactoryMainScreenRectangle.swift; sourceTree = "<group>"; };
1B4A55A927CD99EC009CDC7B /* RandomizeValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomizeValue.swift; sourceTree = "<group>"; };
1B4A55AB27CD9F96009CDC7B /* RectanglePropertyCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectanglePropertyCreator.swift; sourceTree = "<group>"; };
1B54997027D09BE30086EC5F /* Plane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plane.swift; sourceTree = "<group>"; };
@@ -185,6 +187,7 @@
1B54997A27D0B4C00086EC5F /* Rectangle.swift */,
1BD3EA2827E1DC4600ADB359 /* ColoredRectangle.swift */,
1BD3EA2A27E1DC4F00ADB359 /* ImageRectangle.swift */,
1B1D7E0527E3510400683FA9 /* FactoryMainScreenRectangle.swift */,
);
path = Rectangle;
sourceTree = "<group>";
@@ -420,6 +423,7 @@
1BD7287427CCBC7A008DED9E /* SceneDelegate.swift in Sources */,
1B54997427D0A21F0086EC5F /* UIView+Extension.swift in Sources */,
1B4A55AA27CD99EC009CDC7B /* RandomizeValue.swift in Sources */,
1B1D7E0627E3510400683FA9 /* FactoryMainScreenRectangle.swift in Sources */,
1BD3EA2927E1DC4600ADB359 /* ColoredRectangle.swift in Sources */,
1BD728B027CD0E88008DED9E /* SystemLog.swift in Sources */,
1BD728AB27CCFB8D008DED9E /* FactoryRectangleProperty.swift in Sources */,
@@ -611,6 +615,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UIUserInterfaceStyle = Light;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -639,6 +644,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UIUserInterfaceStyle = Automatic;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
11 changes: 11 additions & 0 deletions DrawingApp/DrawingApp/Models/Plane.swift
Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@ import Foundation
protocol MainSceneTapDelegate {
// 델리게이트 패턴의 메소드는 어떤 요소가 언제 누가 선택되었는지 명시하기 위해 네이밍을 변경하였습니다.
func didSelect(at index: Int?)
func getRectangleModel(at index: Int)->RectangleProperty?
func didMoved(rect point: RectOrigin, at index: Int)
}

/// ViewController와 MainScreenViewController 사이를 잇고, 생성된 사각형의 모델들을 저장하는 모델입니다.
@@ -105,6 +107,11 @@ final class Plane: MainSceneTapDelegate {
NotificationCenter.default.post(noti) // Plane.alphaDidChanged
}

func didMoved(rect point: RectOrigin, at index: Int) {
guard (0..<rectangleModels.endIndex) ~= index else { return }
rectangleModels[index].setPoint(point)
}

// MARK: - RectangleViewTapDelegate implementation
func didSelect(at index: Int?) {
guard -1..<rectangleModels.endIndex ~= (index ?? -1) else {
@@ -126,6 +133,10 @@ final class Plane: MainSceneTapDelegate {
NotificationCenter.default.post(noti) // Plane.rectangleViewTouched
}

func getRectangleModel(at index: Int) -> RectangleProperty? {
getRectangleProperty(at: index)
}

// MARK: - Plane no using Interface

func getRectangleCount() -> Int {
8 changes: 0 additions & 8 deletions DrawingApp/DrawingApp/Views/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -195,16 +195,8 @@
<viewLayoutGuide key="safeArea" id="2m4-7r-jvv"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<connections>
<outlet property="tapGesture" destination="yTy-GT-aHK" id="8IT-V5-1gu"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="5vn-HN-Bl0" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<tapGestureRecognizer id="yTy-GT-aHK">
<connections>
<outlet property="delegate" destination="mMi-uD-Q6q" id="dxq-PD-xax"/>
</connections>
</tapGestureRecognizer>
</objects>
<point key="canvasLocation" x="-665" y="853"/>
</scene>
128 changes: 112 additions & 16 deletions DrawingApp/DrawingApp/Views/MainScreenViewController.swift
Original file line number Diff line number Diff line change
@@ -7,14 +7,18 @@

import UIKit

final class MainScreenViewController: UIViewController, UIGestureRecognizerDelegate {
final class MainScreenViewController: UIViewController {

@IBOutlet var tapGesture: UITapGestureRecognizer!
private var rectangleViews = [Rectangle]()
private var selectedIndexes: Set<Int>?

var rectangleDelegate: MainSceneTapDelegate?

private let factoryRectangle = FactoryMainScreenRectangle()

private let selectGesture = UITapGestureRecognizer()
private let doubleTouchesCopyGesture = UITapGestureRecognizer()

override func viewDidLoad() {
super.viewDidLoad()

@@ -85,6 +89,10 @@ final class MainScreenViewController: UIViewController, UIGestureRecognizerDeleg
self.setRectangleStatusChange(userInfo)
}
}

doubleTouchesCopyGesture.delegate = self
doubleTouchesCopyGesture.numberOfTouchesRequired = 2
view.addGestureRecognizer(selectGesture)
}

// MARK: - Methods Process ObserverTask
@@ -96,18 +104,9 @@ final class MainScreenViewController: UIViewController, UIGestureRecognizerDeleg
return
}

var rect: Rectangle!

switch model {
case let model as ImageRectangleProperty:
rect = ImageRectangle(model: model, index: index)
case let model as ColoredRectangleProperty:
rect = ColoredRectangle(model: model, index: index)
default:
LoggerUtil.debugLog(message: "Initialize rectangle failed. \(model.name)")
return
}
guard let rect = factoryRectangle.makeRectangle(from: model, at: index) else { return }

rect.addGestureRecognizer(selectGesture)
rectangleViews.append(rect)
view.addSubview(rect)
}
@@ -148,9 +147,106 @@ final class MainScreenViewController: UIViewController, UIGestureRecognizerDeleg
}
}

// MARK: - UIGestureRecognizerDelegate implementation
@objc func drag(_ sender: UIPanGestureRecognizer) {
guard let rect = sender.view as? Rectangle else { return }

// panGesture가 끝났기 때문에 copiedView를 rectangle이 있던 자리를 차지하도록 하고, 기존 이동하던 rectangle은 지웁니다.
if sender.state == .ended, let copiedView = rect.copiedView {
copiedView.addGestureRecognizer(selectGesture)

let origin = RectOrigin(x: rect.frame.minX, y: rect.frame.minY)
let index = rect.index

UIView.animate(withDuration: 0.5) {
copiedView.frame.origin = rect.frame.origin
rect.removeFromSuperview()
self.rectangleViews[index] = copiedView
}

rectangleDelegate?.didMoved(rect: origin, at: index)

return
}

let translation = sender.translation(in: view)
var positionMoved = CGPoint(x: rect.frame.minX + translation.x, y: rect.frame.minY + translation.y)

if positionMoved.x < 0 {
positionMoved.x = 0
}

if view.frame.width < (positionMoved.x + rect.frame.width) {
positionMoved.x = view.frame.width - rect.frame.width
}

if positionMoved.y < 0 {
positionMoved.y = 0
}

if view.frame.height < (positionMoved.y + rect.frame.height) {
positionMoved.y = view.frame.height - rect.frame.height
}

rect.frame.origin = positionMoved
sender.setTranslation(.zero, in: view)
}
}

// MARK: - UIGestureRecognizerDelegate implementations.

extension MainScreenViewController: UIGestureRecognizerDelegate {

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let rect = touches.first?.view as? Rectangle else {
rectangleDelegate?.didSelect(at: nil)
return
}

// 처음 rectangle을 선택하면 두 손가락으로 선택하는 제스쳐, 팬 제스쳐를 추가하여 임시 뷰 생성 및 이동이 가능하도록 합니다.
if touches.count == 1 && rect.isSelected == false {
rectangleDelegate?.didSelect(at: rect.index)
rect.addGestureRecognizer(doubleTouchesCopyGesture)

let panGesture = UIPanGestureRecognizer(target: self, action: #selector(drag(_:)))
rect.addGestureRecognizer(panGesture)
panGesture.minimumNumberOfTouches = 2
panGesture.delegate = self
}
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
guard
gestureRecognizer == doubleTouchesCopyGesture,
let rect = touch.view as? Rectangle, rect.isSelected, rect.copiedView == nil,
let model = rectangleDelegate?.getRectangleModel(at: rect.index),
let copiedView = factoryRectangle.makeRectangle(from: model, at: rect.index)
else {
return true
}

// 두 손가락으로 선택하는 제스쳐로 뷰를 복사하고 선택 효과는 낼 수 없게 만듭니다. 선택 효과를 내는 제스쳐와 두 손가락 제스쳐가 겹쳐서 오류가 발생하였습니다.
view.insertSubview(copiedView, belowSubview: rect)

rect.setCopiedView(rect: copiedView)
rect.removeGestureRecognizer(selectGesture)
rect.setAlpha(model.alpha/20)
return true
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchedView = touches.first?.view as? Rectangle
rectangleDelegate?.didSelect(at: touchedView?.index)
super.touchesEnded(touches, with: event)

guard let rect = touches.first?.view as? Rectangle, let copiedView = rect.copiedView else { return }

let rectX = rect.frame.maxX
let rectY = rect.frame.maxY

// 만약 임시 뷰가 있음에도 이동이 없었던 경우라면 rectangle이 임시뷰 역할을 하지 않도록 하고, 복사된 뷰는 제거합니다.
if (rectX...rectX+4) ~= copiedView.frame.maxX && (rectY...rectY+4) ~= copiedView.frame.maxY {
copiedView.removeFromSuperview()
rect.setAlpha((rectangleDelegate?.getRectangleModel(at: rect.index)?.alpha ?? 1)/10)
rect.setCopiedView(rect: nil)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// FactoryMainScreenRectangle.swift
// DrawingApp
//
// Created by 백상휘 on 2022/03/17.
//

import Foundation

final class FactoryMainScreenRectangle {
func makeRectangle(from model: RectangleProperty, at index: Int) -> Rectangle? {
switch model {
case let model as ImageRectangleProperty:
return ImageRectangle(model: model, index: index)
case let model as ColoredRectangleProperty:
return ColoredRectangle(model: model, index: index)
default:
LoggerUtil.debugLog(message: "Initialize rectangle failed. \(model.name)")
return nil
}
}
}
10 changes: 10 additions & 0 deletions DrawingApp/DrawingApp/Views/Rectangle/Rectangle.swift
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ protocol EnableSetAlphaRectangle {
class Rectangle: UIView, IndexedRectangle {

var index: Int = 0
// copiedView 변수는 Rectangle이 바로 참조할 수 있도록 하기 위함입니다.
private(set) var copiedView: Rectangle?

var isSelected = false {
didSet {
@@ -36,4 +38,12 @@ class Rectangle: UIView, IndexedRectangle {
override init(frame: CGRect) {
super.init(frame: frame)
}

func setCopiedView(rect: Rectangle?) {
copiedView = rect
}

func setAlpha(_ alpha: Double) {
backgroundColor = backgroundColor?.withAlphaComponent(alpha)
}
}
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -360,3 +360,17 @@ var notificationInfo = [
### 결과 화면

<img src="DrawingApp/IMAGES/Step4_Result4.jpg" alt="Step4_Result4" width="500" />

---

## Step 5 - Touch And Drag

### 목표

* UIGestureRecognizer를 정리하고 실제 구현합니다.
* UIGestureRecognizer 클래스가 어떻게 구체 클래스로 타입을 구체화시키는지 확인해본다.

### 구현 전략

* 각 이벤트에 대해 처리되는 기능을 타입으로 표현할 수 있도록 뷰를 확장해본다.
* 뷰에서 제스쳐 이벤트 발생 -> 뷰 컨트롤러 델리게이트 콜백 -> 모델 변화 -> 모델에서 뷰 컨트롤러에 뷰 변화 요청 -> 뷰 컨트롤러에서 뷰 변화 하는 MVC의 흐름을 따라 개발을 진행한다.