diff --git a/DrawingApp/DrawingApp.xcodeproj/project.pbxproj b/DrawingApp/DrawingApp.xcodeproj/project.pbxproj index ea05771d..9e0166e9 100644 --- a/DrawingApp/DrawingApp.xcodeproj/project.pbxproj +++ b/DrawingApp/DrawingApp.xcodeproj/project.pbxproj @@ -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 = ""; }; 1B4A55A927CD99EC009CDC7B /* RandomizeValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomizeValue.swift; sourceTree = ""; }; 1B4A55AB27CD9F96009CDC7B /* RectanglePropertyCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectanglePropertyCreator.swift; sourceTree = ""; }; 1B54997027D09BE30086EC5F /* Plane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plane.swift; sourceTree = ""; }; @@ -185,6 +187,7 @@ 1B54997A27D0B4C00086EC5F /* Rectangle.swift */, 1BD3EA2827E1DC4600ADB359 /* ColoredRectangle.swift */, 1BD3EA2A27E1DC4F00ADB359 /* ImageRectangle.swift */, + 1B1D7E0527E3510400683FA9 /* FactoryMainScreenRectangle.swift */, ); path = Rectangle; sourceTree = ""; @@ -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", diff --git a/DrawingApp/DrawingApp/Models/Plane.swift b/DrawingApp/DrawingApp/Models/Plane.swift index 780b3154..56505a68 100644 --- a/DrawingApp/DrawingApp/Models/Plane.swift +++ b/DrawingApp/DrawingApp/Models/Plane.swift @@ -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.. RectangleProperty? { + getRectangleProperty(at: index) + } + // MARK: - Plane no using Interface func getRectangleCount() -> Int { diff --git a/DrawingApp/DrawingApp/Views/Base.lproj/Main.storyboard b/DrawingApp/DrawingApp/Views/Base.lproj/Main.storyboard index c07f427a..4cb729df 100644 --- a/DrawingApp/DrawingApp/Views/Base.lproj/Main.storyboard +++ b/DrawingApp/DrawingApp/Views/Base.lproj/Main.storyboard @@ -195,16 +195,8 @@ - - - - - - - - diff --git a/DrawingApp/DrawingApp/Views/MainScreenViewController.swift b/DrawingApp/DrawingApp/Views/MainScreenViewController.swift index 4fc8b2b7..68f09397 100644 --- a/DrawingApp/DrawingApp/Views/MainScreenViewController.swift +++ b/DrawingApp/DrawingApp/Views/MainScreenViewController.swift @@ -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? 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, 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, 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) + } } } diff --git a/DrawingApp/DrawingApp/Views/Rectangle/FactoryMainScreenRectangle.swift b/DrawingApp/DrawingApp/Views/Rectangle/FactoryMainScreenRectangle.swift new file mode 100644 index 00000000..423aceb3 --- /dev/null +++ b/DrawingApp/DrawingApp/Views/Rectangle/FactoryMainScreenRectangle.swift @@ -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 + } + } +} diff --git a/DrawingApp/DrawingApp/Views/Rectangle/Rectangle.swift b/DrawingApp/DrawingApp/Views/Rectangle/Rectangle.swift index b621f48d..311aa5da 100644 --- a/DrawingApp/DrawingApp/Views/Rectangle/Rectangle.swift +++ b/DrawingApp/DrawingApp/Views/Rectangle/Rectangle.swift @@ -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) + } } diff --git a/README.md b/README.md index c82a2356..902c3c6d 100644 --- a/README.md +++ b/README.md @@ -360,3 +360,17 @@ var notificationInfo = [ ### 결과 화면 Step4_Result4 + +--- + +## Step 5 - Touch And Drag + +### 목표 + +* UIGestureRecognizer를 정리하고 실제 구현합니다. +* UIGestureRecognizer 클래스가 어떻게 구체 클래스로 타입을 구체화시키는지 확인해본다. + +### 구현 전략 + +* 각 이벤트에 대해 처리되는 기능을 타입으로 표현할 수 있도록 뷰를 확장해본다. +* 뷰에서 제스쳐 이벤트 발생 -> 뷰 컨트롤러 델리게이트 콜백 -> 모델 변화 -> 모델에서 뷰 컨트롤러에 뷰 변화 요청 -> 뷰 컨트롤러에서 뷰 변화 하는 MVC의 흐름을 따라 개발을 진행한다.