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

[Feature] 화이트보드에 그림 추가 기능 구현(2차) #87

Merged
merged 11 commits into from
Nov 18, 2024
5 changes: 4 additions & 1 deletion Domain/Domain/Sources/Entity/DrawingObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import Foundation

public class DrawingObject: WhiteboardObject {
public private(set) var points: [CGPoint]
public let lineWidth: CGFloat

public init(
id: UUID,
position: CGPoint,
size: CGSize,
points: [CGPoint]
points: [CGPoint],
lineWidth: CGFloat
) {
self.points = points
self.lineWidth = lineWidth
super.init(
id: id,
position: position,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@
import Foundation

public protocol DrawObjectUseCaseInterface {
/// 그림을 그리기 시작한 점
var origin: CGPoint? { get }

/// 선을 나타내는 점들의 배열
var points: [CGPoint] { get }

/// 그리기를 시작하는 메서드
/// 선의 굵기를 설정합니다.
/// - Parameter width: 선의 너비
func setLineWidth(width: CGFloat)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 코드 컨벤션 정할 때 함수명에 get, set 지양하기로 하지 않았었나용 ? 🥺
만약 lineWidth = 5 처럼 선 굵기에 대한 초기값이 정해져있는데 업데이트를 위해 set 함수가 필요한 것이라면 update 혹은 configure, apply 라는 메서드명은 어떠신지요 ??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 그렇네요..!
말씀해주신 것처럼 초기값이 정해져 있으니 update라는 네이밍이 괜찮아보입니다. 수정하겠습니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

func lineWidth(to width: Int)

func configureLineWidth(to width: Int)

조이의 코멘트를 보고 추천 남깁니다~


/// 그리기를 시작합니다.
/// - Parameter point: 시작 지점 CGPoint
func startDrawing(at point: CGPoint)

/// 그림 그리는 중에 새로운 점을 추가하는 메서드
/// 그림 그리는 중에 새로운 점을 추가합니다.
/// - Parameter point: 추가할 점의 CGPoint
func addPoint(point: CGPoint)

/// 그림 그리기를 종료하는 메서드
/// 그림 그리기를 종료합니다.
/// - Returns: 완성된 그림 객체 (옵셔널)
func finishDrawing() -> DrawingObject?
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ public protocol ManageWhiteboardObjectUseCaseInterface {
/// 화이트보드 객체를 추가하는 메서드
/// - Parameter whiteboardObject: 추가할 화이트보드 객체
/// - Returns: 추가 성공 여부
@discardableResult
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#86 (comment)

해당 코멘트 영향으로 @discardableResult를 붙인건가용 ??
혹시 테스트 코드 외에 실제 UseCase 함수를 사용할 때에도 결과 값을 사용하지 않는 부분이 있을지 궁금합니다. !

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선은 실패 처리를 위해 Bool 타입을 반환하도록 설계했습니다. 다만 아직 실패했을 때에 대한 정책이 정해진 것이 없어 반환값을 사용하는 곳이 없습니다. (테스트 코드 제외) 따라서 일단 discardableResult를 붙여놓았습니다!

func addObject(whiteboardObject: WhiteboardObject) -> Bool

/// 화이트보드 객체를 수정하는 메서드
/// - Parameter whiteboardObject: 수정할 화이트보드 객체
/// - Returns: 추가 성공 여부
@discardableResult
func updateObject(whiteboardObject: WhiteboardObject) -> Bool

/// 화이트보드를 제거하는 메서드
/// - Returns: 추가 성공 여부
@discardableResult
func removeObject(whiteboardObject: WhiteboardObject) -> Bool
}
20 changes: 12 additions & 8 deletions Domain/Domain/Sources/UseCase/DrawObjectUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ import Foundation

public final class DrawObjectUseCase: DrawObjectUseCaseInterface {
public private(set) var points: [CGPoint]
public private(set) var origin: CGPoint?
public private(set) var lineWidth: CGFloat

public init() {
points = []
lineWidth = 5
}

public func setLineWidth(width: CGFloat) {
lineWidth = width
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정까지 미리 만들어 주셨군요!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 요 부분은 수정을 염두에 두고 한 것은 아닙니다.!.!
추후 선 그리기 너비 설정이 가능하지 않을까? 싶어 추가해놨습니다!
아마 수정을 한다 하더라도 이미 그린 그림의 너비를 수정은 안하지,,않을까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 그렇네요..??ㅋㅋㅋ


public func startDrawing(at point: CGPoint) {
origin = point
points = [point]
}

Expand All @@ -32,29 +36,29 @@ public final class DrawObjectUseCase: DrawObjectUseCaseInterface {
}

guard
let origin,
let minX = xPoints.min(),
let maxX = xPoints.max(),
let minY = yPoints.min(),
let maxY = yPoints.max()
else { return nil }

let size = CGSize(width: maxX - minX, height: maxY - minY)
let padding = lineWidth / 2
let origin = CGPoint(x: minX - padding, y: minY - padding)
let size = CGSize(width: maxX - minX + padding * 2, height: maxY - minY + padding * 2)
let adjustedPoints = points.map {
CGPoint(x: $0.x - minX, y: $0.y - minY)
CGPoint(x: $0.x - minX + padding, y: $0.y - minY + padding)
}

let drawingObject = DrawingObject(
id: UUID(),
position: origin,
size: size,
points: adjustedPoints)
points: adjustedPoints,
lineWidth: lineWidth)

return drawingObject
}

private func reset() {
origin = nil
points.removeAll()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ public final class ManageWhiteboardToolUseCase: ManageWhiteboardToolUseCaseInter
}

public func selectTool(tool: WhiteboardTool) {
currentToolSubject.send(tool)
let previousTool = currentToolSubject.value
if previousTool == tool {
currentToolSubject.send(nil)
} else {
currentToolSubject.send(tool)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

send(nil)을 통해 nil값에 따라 처리가 필요한 거 겠죠!?

+

삼항 연산자는 어떻게 생각하시나요??

Suggested change
let previousTool = currentToolSubject.value
if previousTool == tool {
currentToolSubject.send(nil)
} else {
currentToolSubject.send(tool)
}
let selectTool = currentToolSubject.value == tool ? nil: tool
currentToolSubject.send(selectTool)

이런 느낌으로!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

깔끔해서 좋은 것 같습니다! 동~

}

public func finishUsingTool() {
Expand Down
53 changes: 21 additions & 32 deletions Domain/DomainTests/DrawObjectUseCaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,80 +8,69 @@ import Domain
import XCTest

final class DrawObjectUseCaseTests: XCTestCase {
let lineWidth: CGFloat = 1
var useCase: DrawObjectUseCaseInterface!

override func setUpWithError() throws {
useCase = DrawObjectUseCase()
useCase.setLineWidth(width: lineWidth)
}

override func tearDownWithError() throws {
useCase = nil
}

// startDrawing시, 시작 좌표를 올바르게 설정하는지 확인
func testStartDrawingSetsOrigin() {
let startPoint = CGPoint(x: 100, y: 100)
useCase.startDrawing(at: startPoint)

XCTAssertEqual(startPoint, useCase.origin)
}


// 그림을 그릴 때, points 배열에 점들을 올바르게 추가하는지 확인
func testAddPointToArray() {
// 준비
let startPoint = CGPoint(x: 100, y: 100)
let point1 = CGPoint(x: 115, y: 115)
let point2 = CGPoint(x: 15, y: 30)

// 실행
useCase.startDrawing(at: startPoint)
useCase.addPoint(point: point1)
useCase.addPoint(point: point2)

// 검증
XCTAssertEqual(useCase.points, [startPoint, point1, point2])
}

// finishDrawing이 올바르게 DrawingObject를 생성하는지 확인
func testFinishDrawingCreatesAndSendsDrawingObject() {
// 준비
useCase.startDrawing(at: CGPoint(x: 10, y: 10))
useCase.addPoint(point: CGPoint(x: 11, y: 11))
useCase.addPoint(point: CGPoint(x: 12, y: 12))
useCase.addPoint(point: CGPoint(x: 13, y: 13))
let padding = lineWidth / 2
let expectedAdjustedPoints = [
CGPoint(x: 0 + padding, y: 0 + padding),
CGPoint(x: 1 + padding, y: 1 + padding),
CGPoint(x: 2 + padding, y: 2 + padding),
CGPoint(x: 3 + padding, y: 3 + padding)]

// 실행
let drawingObject = useCase.finishDrawing()

// 검증
XCTAssertNotNil(drawingObject)
XCTAssertEqual(drawingObject?.position, CGPoint(x: 10, y: 10))
XCTAssertEqual(drawingObject?.size, CGSize(width: 3, height: 3))

let expectedAdjustedPoints = [
CGPoint(x: 0, y: 0),
CGPoint(x: 1, y: 1),
CGPoint(x: 2, y: 2),
CGPoint(x: 3, y: 3)]
XCTAssertEqual(drawingObject?.position, CGPoint(x: 10 - padding, y: 10 - padding))
XCTAssertEqual(drawingObject?.size, CGSize(width: 3 + padding * 2, height: 3 + padding * 2))
XCTAssertEqual(drawingObject?.points, expectedAdjustedPoints)
}

// 그림 그린 후 useCase 내부 상태 초기화가 되는지 확인
func testResetAfterFinishDrawing() {
// 준비
useCase.startDrawing(at: CGPoint(x: 10, y: 10))
useCase.addPoint(point: CGPoint(x: 110, y: 110))
useCase.addPoint(point: CGPoint(x: 120, y: 120))

// 실행
_ = useCase.finishDrawing()

XCTAssertNil(useCase.origin)
// 검증
XCTAssertEqual(useCase.points.count, 0)
}
}

final class MockWhiteboardObjectRepository: WhiteboardObjectRepositoryInterface {
private var continuation: AsyncStream<WhiteboardObject>.Continuation?

func send(whiteboardObject: WhiteboardObject) {
continuation?.yield(whiteboardObject)
}

func whiteboardObjectAsyncStream() -> AsyncStream<WhiteboardObject> {
return AsyncStream { continuation in
self.continuation = continuation
}
}
}
8 changes: 8 additions & 0 deletions Presentation/Presentation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
0080E8D12CE4A00F0095B958 /* WhiteboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0080E8D02CE4A0060095B958 /* WhiteboardViewModel.swift */; };
0080E8D32CE4A0840095B958 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0080E8D22CE4A0820095B958 /* ViewModel.swift */; };
00CCC2672CE60BCD005FA747 /* AirplainFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8525DC72CE201D50089DA5E /* AirplainFont.swift */; };
00D2DD982CE8CD300089F0BA /* DrawingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D2DD972CE8CD300089F0BA /* DrawingView.swift */; };
00D2DD9A2CE8DCEA0089F0BA /* DrawingObjectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D2DD992CE8DCEA0089F0BA /* DrawingObjectView.swift */; };
5B2BC78E2CE4F66F00893B9E /* WhiteboardListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2BC78A2CE4F66F00893B9E /* WhiteboardListViewController.swift */; };
5B2BC78F2CE4F66F00893B9E /* WhiteboardListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2BC78C2CE4F66F00893B9E /* WhiteboardListViewModel.swift */; };
5B7C6EBA2CDB6C5C0024704A /* Domain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B7C6EB92CDB6C5C0024704A /* Domain.framework */; };
Expand Down Expand Up @@ -51,6 +53,8 @@
0080E8C92CE2FF6E0095B958 /* WhiteboardObjectViewFactoryable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardObjectViewFactoryable.swift; sourceTree = "<group>"; };
0080E8D02CE4A0060095B958 /* WhiteboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardViewModel.swift; sourceTree = "<group>"; };
0080E8D22CE4A0820095B958 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
00D2DD972CE8CD300089F0BA /* DrawingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawingView.swift; sourceTree = "<group>"; };
00D2DD992CE8DCEA0089F0BA /* DrawingObjectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawingObjectView.swift; sourceTree = "<group>"; };
5B2BC78A2CE4F66F00893B9E /* WhiteboardListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardListViewController.swift; sourceTree = "<group>"; };
5B2BC78C2CE4F66F00893B9E /* WhiteboardListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiteboardListViewModel.swift; sourceTree = "<group>"; };
5B7C6E502CDB6A380024704A /* Presentation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Presentation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -88,6 +92,7 @@
0080E8C52CE2F04F0095B958 /* WhiteboardObjectView.swift */,
6F199EF02CE31A05005DC40F /* WhiteboardToolBar.swift */,
6F21477C2CE4EFCF00B55E2C /* TextObjectView.swift */,
00D2DD992CE8DCEA0089F0BA /* DrawingObjectView.swift */,
);
path = ObjectView;
sourceTree = "<group>";
Expand Down Expand Up @@ -135,6 +140,7 @@
isa = PBXGroup;
children = (
6F199EF22CE3203A005DC40F /* WhiteboardViewController.swift */,
00D2DD972CE8CD300089F0BA /* DrawingView.swift */,
004645F22CE60A1B00C76EDB /* ObjectView */,
);
path = View;
Expand Down Expand Up @@ -383,12 +389,14 @@
A8E97ABF2CE4E94D00B28063 /* SelectProfileIconViewController.swift in Sources */,
A8E979C52CE493E900B28063 /* ProfileViewController.swift in Sources */,
A85260252CE3447E0089DA5E /* UIColor+.swift in Sources */,
00D2DD982CE8CD300089F0BA /* DrawingView.swift in Sources */,
A8E979CA2CE49A1800B28063 /* ProfileViewModel.swift in Sources */,
0080E8C62CE2F0510095B958 /* WhiteboardObjectView.swift in Sources */,
A8525DCB2CE203D50089DA5E /* UIView+.swift in Sources */,
6F199EF32CE3203A005DC40F /* WhiteboardViewController.swift in Sources */,
6F199EF12CE31A05005DC40F /* WhiteboardToolBar.swift in Sources */,
6F21477D2CE4EFCF00B55E2C /* TextObjectView.swift in Sources */,
00D2DD9A2CE8DCEA0089F0BA /* DrawingObjectView.swift in Sources */,
0080E8D12CE4A00F0095B958 /* WhiteboardViewModel.swift in Sources */,
0080E8D32CE4A0840095B958 /* ViewModel.swift in Sources */,
5B2BC78E2CE4F66F00893B9E /* WhiteboardListViewController.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import Domain
import Foundation

protocol WhiteboardObjectViewFactoryable {
func create(with whiteboardObject: WhiteboardObject) -> WhiteboardObjectView
func create(with whiteboardObject: WhiteboardObject) -> WhiteboardObjectView?
}

struct WhiteboardObjectViewFactory: WhiteboardObjectViewFactoryable {
func create(with whiteboardObject: WhiteboardObject) -> WhiteboardObjectView {
func create(with whiteboardObject: WhiteboardObject) -> WhiteboardObjectView? {
switch whiteboardObject {
case let textObject as TextObject:
return TextObjectView(textObject: textObject)
// TODO: 오브젝트 별 case 추가 예정
case let drawingObject as DrawingObject:
return DrawingObjectView(drawingObject: drawingObject)
default:
break
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ struct DrawingRenderer: DrawingRendererInterface {

let renderer = UIGraphicsImageRenderer(size: drawingObject.size)
let image = renderer.image { context in
context.cgContext.setLineWidth(5)
context.cgContext.setLineCap(.round)
context.cgContext.setLineWidth(drawingObject.lineWidth)
context.cgContext.setStrokeColor(UIColor.black.cgColor)
context.cgContext.move(to: startPoint)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// DrawingView.swift
// Presentation
//
// Created by 이동현 on 11/16/24.
//

import UIKit

protocol DrawingViewDelegate: AnyObject {
func drawingView(_ sender: DrawingView, at point: CGPoint)
func drawingViewDidStartDrawing(_ sender: DrawingView, at point: CGPoint)
func drawingViewDidEndDrawing(_ sender: DrawingView)
}

final class DrawingView: UIView {
private var drawingLayer = CAShapeLayer()
private var drawingPath = UIBezierPath()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 프로퍼티들이 재할당되는 부분이 보이지 않는데 var로 선언된 이유가 궁금합니다.
추후에 재할당될 가능성이 있는 걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아닙니다! let으로 바꾸겠습니다

private var previousPoint: CGPoint?
weak var delegate: DrawingViewDelegate?

init() {
super.init(frame: .zero)
configureAttributes()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
configureAttributes()
}

func reset() {
drawingLayer.path = nil
drawingPath.removeAllPoints()
previousPoint = nil
}

private func configureAttributes() {
backgroundColor = .clear
drawingLayer.strokeColor = UIColor.black.cgColor
drawingLayer.lineWidth = 5
drawingLayer.lineCap = .round
layer.addSublayer(drawingLayer)

let drawingGesture = UIPanGestureRecognizer(target: self, action: #selector(handleDrawingGesture(sender:)))
drawingGesture.minimumNumberOfTouches = 1
drawingGesture.maximumNumberOfTouches = 1
self.addGestureRecognizer(drawingGesture)
}

@objc private func handleDrawingGesture(sender: UIPanGestureRecognizer) {
let point = sender.location(in: self)

switch sender.state {
case .began:
previousPoint = point
delegate?.drawingViewDidStartDrawing(self, at: point)
drawLine(to: point)
case .changed:
delegate?.drawingView(self, at: point)
drawLine(to: point)
case .ended:
delegate?.drawingViewDidEndDrawing(self)
previousPoint = nil
default:
break
}
}
Comment on lines +51 to +68
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프린트 때 PanGesture를 사용해 봤었는데, 이런 드래그 이벤트같은 것들은 UIKit에서는 거의 PanGesture로 처리가 가능하군요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 그렇습니다!!!


private func drawLine(to point: CGPoint) {
guard let previousPoint else { return }

drawingPath.move(to: previousPoint)
drawingPath.addLine(to: point)
drawingLayer.path = drawingPath.cgPath
self.previousPoint = point
}
}
Loading