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

[Alex] Step4 다형성을 적용한 리팩토링 #105

Open
wants to merge 13 commits into
base: alex
Choose a base branch
from
Open
Show file tree
Hide file tree
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
191 changes: 130 additions & 61 deletions DrawingApp/DrawingApp.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E481265D27CCF8E000A3FFF6"
BuildableName = "DrawingApp.app"
BlueprintName = "DrawingApp"
ReferencedContainer = "container:DrawingApp.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
<TestableReference
skipped = "NO">
Expand Down
108 changes: 60 additions & 48 deletions DrawingApp/DrawingApp/Controller/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class ViewController: UIViewController {

// MARK: - Property for Model
private let plane = Plane()
private var rectangleMap = [Rectangle: RectangleShapable]()
private var shapeMap = [Shape: ShapeViewable]()

// MARK: - View Life Cycle Methods
override func viewDidLoad() {
Expand All @@ -32,16 +32,16 @@ class ViewController: UIViewController {

private func setObservers() {
NotificationCenter.default.addObserver(forName: .RectangleModelDidCreated, object: nil, queue: .main, using: { notification in
guard let rectangle = notification.object as? Rectangle else { return }
self.createRectangleView(ofClass: RectangleView.self, with: rectangle)
guard let shape = notification.object as? Shape else { return }
self.createShapeView(ofClass: ColoredRectangleView.self, with: shape)
})
NotificationCenter.default.addObserver(forName: .RectangleModelDidUpdated, object: nil, queue: .main, using: self.rectangleDataDidChanged)
NotificationCenter.default.addObserver(forName: .RectangleModelDidUpdated, object: nil, queue: .main, using: self.shapeModelDidChanged)

NotificationCenter.default.addObserver(forName: .ImageRectangleModelDidCreated, object: nil, queue: .main, using: { notification in
guard let rectangle = notification.object as? Rectangle else { return }
self.createRectangleView(ofClass: ImageRectangleView.self, with: rectangle)
guard let shape = notification.object as? Shape else { return }
self.createShapeView(ofClass: ImageRectangleView.self, with: shape)
})
NotificationCenter.default.addObserver(forName: .RectangleModelDidUpdated, object: nil, queue: .main, using: self.rectangleDataDidChanged)
NotificationCenter.default.addObserver(forName: .RectangleModelDidUpdated, object: nil, queue: .main, using: self.shapeModelDidChanged)

NotificationCenter.default.addObserver(forName: .PlaneDidSelectItem, object: self.plane, queue: .main, using: self.planeDidSelectItem)
NotificationCenter.default.addObserver(forName: .PlaneDidUnselectItem, object: self.plane, queue: .main, using: self.planeDidUnselectItem)
Expand All @@ -54,12 +54,12 @@ extension ViewController: PlaneViewDelegate {
self.plane.unselectItem()
}

func planeViewDidPressRectangleAddButton() {
let rectangle = RectangleFactory.makeRandomRectangle()
plane.append(item: rectangle)
func planeViewDidPressColoredRectangleAddButton() {
let shape = RectangleFactory.makeRandomRectangle()
plane.append(item: shape)
}

func planeViewDidPressImageAddButton() {
func planeViewDidPressImageRectangleAddButton() {
var configuration = PHPickerConfiguration(photoLibrary: .shared())

configuration.filter = .images
Expand All @@ -76,87 +76,98 @@ extension ViewController: PlaneViewDelegate {
// MARK: - ControlPanelView To ViewController
extension ViewController: ControlPanelViewDelegate {
func controlPanelDidPressColorButton() {
guard let rectangle = self.plane.currentItem else { return }
guard let shape = self.plane.currentItem as? BackgroundAdaptable else { return }

let color = ColorFactory.makeTypeRandomly()
rectangle.setBackgroundColor(color)

shape.setBackgroundColor(color)
}

func controlPanelDidMoveAlphaSlider(_ sender: UISlider) {
let value = sender.value.toFixed(digits: 1)

guard let rectangle = self.plane.currentItem else { return }
guard let shape = self.plane.currentItem as? AlphaAdaptable else { return }
guard let alpha = Alpha(rawValue: value) else { return }

rectangle.setAlpha(alpha)
shape.setAlpha(alpha)
}
}

// MARK: - RectangleView To ViewController
extension ViewController {
@objc private func handleOnTapRectangleView(_ sender: UITapGestureRecognizer) {
@objc private func handleOnTapShapeView(_ sender: UITapGestureRecognizer) {
guard sender.state == .ended else { return }

let point = sender.location(in: self.planeView).convert(using: Point.self)

guard let rectangle = self.plane.findItemBy(point: point) else { return }
guard let shape = self.plane.findItemBy(point: point) else { return }

self.plane.selectItem(id: rectangle.id)
self.plane.selectItem(id: shape.id)
}
}

// MARK: - Rectangle Model To ViewController
extension ViewController {
private func createRectangleView(ofClass Class: RectangleShapable.Type, with rectangle: Rectangle) {
guard let rectangleView = RectangleViewFactory.makeView(ofClass: Class, with: rectangle) else { return }

let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleOnTapRectangleView))
private func createShapeView(ofClass Class: ShapeViewable.Type, with shape: Shape) {
guard let shapeView = ShapeViewFactory.makeView(ofClass: Class, with: shape) else { return }

rectangleView.addGestureRecognizer(tap)
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleOnTapShapeView))

self.planeView.addSubview(rectangleView)
self.rectangleMap.updateValue(rectangleView, forKey: rectangle)
shapeView.addGestureRecognizer(tap)
shapeView.animateScale(CGFloat(1.2), duration: 0.15, delay: 0)

rectangleView.animateScale(CGFloat(1.2), duration: 0.15, delay: 0)
self.planeView.addSubview(shapeView)
self.shapeMap.updateValue(shapeView, forKey: shape)
}

private func rectangleDataDidChanged(_ notification: Notification) {
guard let rectangle = self.plane.currentItem as? Rectangle else { return }
guard let rectangleView = self.rectangleMap[rectangle] else { return }
private func shapeModelDidChanged(_ notification: Notification) {
guard let shape = self.plane.currentItem as? Shape else { return }
guard let shapeView = self.shapeMap[shape] else { return }

if let alpha = notification.userInfo?[Rectangle.NotificationKey.alpha] as? Alpha {
rectangleView.setAlpha(alpha)
}
let color = notification.userInfo?[NotificationKey.updated] as? Color
let alpha = notification.userInfo?[NotificationKey.updated] as? Alpha
let alphaAdaptableShape = shape as? AlphaAdaptableShape
let colorableShapeView = shapeView as? BackgroundViewable

if let color = notification.userInfo?[Rectangle.NotificationKey.color] as? Color {
rectangleView.setBackgroundColor(color: color, alpha: rectangle.alpha)
self.controlPanelView.setColorButtonTitle(title: rectangleView.backgroundColor?.toHexString() ?? "None")
if let _ = alphaAdaptableShape, let alpha = alpha {
shapeView.setAlpha(alpha)
}

if let shape = alphaAdaptableShape, let colorableShapeView = colorableShapeView, let color = color {
colorableShapeView.setBackgroundColor(color: color, alpha: shape.alpha)
self.controlPanelView.setColorButtonTitle(title: shapeView.backgroundColor?.toHexString() ?? "None")
}
}
}

// MARK: - Plane Model to ViewController
extension ViewController {
private func planeDidSelectItem(_ notification: Notification) {
guard let rectangle = notification.userInfo?[Plane.NotificationKey.select] as? Rectangle else { return }
guard let rectangleView = self.rectangleMap[rectangle] else { return }
guard let shape = notification.userInfo?[Plane.NotificationKey.select] as? Shape else { return }
guard let shapeView = self.shapeMap[shape] else { return }

rectangleView.setBorder(width: 2, color: .blue)
shapeView.setBorder(width: 2, color: .blue)

let hexString = UIColor(with: rectangle.backgroundColor).toHexString()
if let backgroundAdaptableShape = shape as? BackgroundAdaptable {
let hexString = Color.toHexString(backgroundAdaptableShape.backgroundColor)
self.controlPanelView.setColorButtonTitle(title: hexString)
}

if let AlphaAdaptableShape = shape as? AlphaAdaptable {
self.controlPanelView.setAlphaSliderValue(value: AlphaAdaptableShape.alpha)
}

self.controlPanelView.setColorButtonControllable(enable: rectangle.isType(of: Rectangle.self))
let isConformed = (shape as? ImageAdaptable) == nil
Comment on lines +151 to +160
Copy link
Author

Choose a reason for hiding this comment

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

프로토콜로 대체하면서 이전에 없던 if 문과 타입케스팅 생겼습니다. 차라리 별도의 함수로 분리하는 것이 더 좋을까요?

Copy link
Contributor

Choose a reason for hiding this comment

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

맞아요. 고민스러운 부분이네요. 추상화를 했기 때문에 적절한 것을 찾지 못해서 어쩔 수 없죠.
그만큼 역할을 잘 나눈거죠. 이벤트 하나에서 역할에 따라 다르게 출력해야 하니까 생기는 현상이니까 괜찮습니다.
그래서 유즈케이스 같은 것을 두고 planeDidSelectItem가 발생하면, 거기서 분기해서 각기 다른 흐름을 만들기도 합니다


self.controlPanelView.setColorButtonControllable(enable: isConformed)
self.controlPanelView.setAlphaSliderControllable(enable: true)
self.controlPanelView.setColorButtonTitle(title: hexString)
self.controlPanelView.setAlphaSliderValue(value: rectangle.alpha)
}

private func planeDidUnselectItem(_ notification: Notification) {
guard let rectangle = notification.userInfo?[Plane.NotificationKey.unselect] as? Rectangle else { return }
guard let rectangleView = self.rectangleMap[rectangle] else { return }
guard let shape = notification.userInfo?[Plane.NotificationKey.unselect] as? Shape else { return }
guard let shapeView = self.shapeMap[shape] else { return }

rectangleView.removeBorder()
shapeView.removeBorder()

self.controlPanelView.reset()
}
Expand All @@ -175,8 +186,9 @@ extension ViewController: PHPickerViewControllerDelegate {
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, error in
guard let url = url, error == nil else { return }

let imageRectangle = RectangleFactory.makeRandomRectangle(with: url)
self.plane.append(item: imageRectangle)
let imageShape = RectangleFactory.makeRandomRectangle(with: url)

self.plane.append(item: imageShape)
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions DrawingApp/DrawingApp/Model/Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@
import Foundation

struct Color {
static let range = UInt8.min...UInt8.max

static let black = Color(red: 0, green: 0, blue: 0)
static let red = Color(red: 255, green: 0, blue: 0)
static let green = Color(red: 0, green: 255, blue: 0)
static let blue = Color(red: 0, green: 0, blue: 255)
static let white = Color(red: 255, green: 255, blue: 255)

static func toHexString(_ color: Self) -> String {
let RGB = Int(color.red * 255) << 16 | Int(color.green * 255) << 8 | Int(color.blue * 255)
return String(format:"#%06x", RGB).uppercased()
}

let red: Double
let green: Double
let blue: Double
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,41 @@
//
// Rectangle.swift
// ColoredRectangle.swift
// DrawingApp
//
// Created by 송태환 on 2022/03/01.
//

import Foundation

protocol RectangleBuildable {
init(x: Double, y: Double, width: Double, height: Double)
}
typealias BackgroundColorControllable = BackgroundAdaptable & AlphaAdaptable

class Rectangle: Shapable, Notifiable, Hashable {
enum NotificationKey {
case alpha
case color
}

class ColoredRectangle: Shape, BackgroundColorControllable, Notifiable {
// MARK: - Properties
private(set) var backgroundColor: Color {
didSet {
self.notifyDidUpdated(key: .color, data: self.backgroundColor)
self.notifyDidUpdated(key: .updated, data: self.backgroundColor)
}
}

private(set) var alpha: Alpha {
didSet {
self.notifyDidUpdated(key: .alpha, data: self.alpha)
self.notifyDidUpdated(key: .updated, data: self.alpha)
}
}

let id: String
let size: Size
let origin: Point

var diagonalPoint: Point {
let maxX = self.origin.x + self.size.width
let maxY = self.origin.y + self.size.height
return Point(x: maxX, y: maxY)
}

Comment on lines -33 to -42
Copy link
Author

Choose a reason for hiding this comment

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

Shape 클래스가 공통 데이터를 가지고 있고 ColoredRectangle 은 backgroundColor 와 alpha, ImageRectangle 은 image 와 alpha 데이터를 소유합니다.
alpha 를 다루지않는 도형이 추가될 것을 염두해 Shape 에 포함시키지 않았습니다.

// MARK: - Initialisers
init(id: String, origin: Point, size: Size, color: Color = .white, alpha: Alpha = .opaque) {
self.id = id
self.origin = origin
self.size = size
self.backgroundColor = color
self.alpha = alpha
super.init(id: id, origin: origin, size: size)
}

init(id: String, x: Double, y: Double, width: Double, height: Double, color: Color = .white, alpha: Alpha = .opaque) {
self.id = id
self.origin = Point(x: x, y: y)
self.size = Size(width: width, height: height)
self.backgroundColor = color
let origin = Point(x: x, y: y)
let size = Size(width: width, height: height)
self.alpha = alpha
self.backgroundColor = color
super.init(id: id, origin: origin, size: size)
}

func convert<T: RectangleBuildable>(using Convertor: T.Type) -> T {
Expand All @@ -79,7 +60,7 @@ class Rectangle: Shapable, Notifiable, Hashable {
}

// MARK: - CustomStringConvertible Protocol
extension Rectangle {
extension ColoredRectangle: CustomStringConvertible {
var description: String {
return """
(\(self.id)), \(self.origin), \(self.size), \(self.backgroundColor), \(self.alpha)
Expand Down
44 changes: 36 additions & 8 deletions DrawingApp/DrawingApp/Model/ImageRectangle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,52 @@

import Foundation

class ImageRectangle: Rectangle {
private(set) var image: URL?
typealias ImageControllable = ImageAdaptable & AlphaAdaptable

class ImageRectangle: Shape, ImageControllable, Notifiable {
private(set) var alpha: Alpha {
didSet {
self.notifyDidUpdated(key: .updated, data: self.alpha)
Copy link
Contributor

Choose a reason for hiding this comment

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

didSet을 사용하는 경우는 이 값이 struct냐 class냐에 따라 동작이 다릅니다.
alpha 자체가 바뀌는 경우와 alpha 속성이 바뀌는 경우를 구분해서 비교해보세요

}
}

private var imageURL: URL?

var imagePath: String? {
return self.imageURL?.path
}
Comment on lines +19 to +23
Copy link
Author

Choose a reason for hiding this comment

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

캡슐화를 유지할 목적으로 getter 역시 메소드처럼 메모리가 동작한다고 해서 외부 객체가 직접 속성에 접근할 수 있게 하는 것보다 getter 를 두었습니다.
결과적으로 속성에 접근하는 것과 크게 다르지 않아 캡슐화가 제대로 이루어진건지 잘 모르겠습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

Plane과 다르게 내부에서 저장을 위해서 생성되는 이런 타입들은 어쩔 수 없이 값을 가져가야 하는 경우가 생깁니다.
이런 타입들에 대해서는 캡슐화를 걱정할 필요가 없습니다. 거의 대부분 만들어진 상태로 값이 바뀔 일이 거의 없죠
그럴 때는 그냥 private(set)을 해도 됩니다.


init(id: String, origin: Point, size: Size, image: URL? = nil) {
self.imageURL = image
self.alpha = .opaque
super.init(id: id, origin: origin, size: size)
self.image = image
}

init(id: String, x: Double, y: Double, width: Double, height: Double, image: URL? = nil) {
super.init(id: id, x: x, y: y, width: width, height: height)
self.image = image
let origin = Point(x: x, y: y)
let size = Size(width: width, height: height)
self.imageURL = image
self.alpha = .opaque
super.init(id: id, origin: origin, size: size)
}

func setImage(with url: URL) {
self.image = url
func setImagePath(with url: URL) {
self.imageURL = url
}

override func notifyDidCreated() {
func setAlpha(_ alpha: Alpha) {
self.alpha = alpha
}

func convert<T: RectangleBuildable>(using Convertor: T.Type) -> T {
return Convertor.init(x: self.origin.x, y: self.origin.y, width: self.size.width, height: self.size.height)
}

func notifyDidCreated() {
NotificationCenter.default.post(name: .ImageRectangleModelDidCreated, object: self)
}

func notifyDidUpdated(key: NotificationKey, data: Any) {
NotificationCenter.default.post(name: .RectangleModelDidUpdated, object: self, userInfo: [key: data])
Comment on lines +51 to +56
Copy link
Contributor

Choose a reason for hiding this comment

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

음.. 이런 하위 타입들은 class여도 immutable 형태나 struct - let 형태로 값 타입으로 사용하는 경우도 많습니다
모든 값을 이렇게 Notification post를 해야 하는 건 아닙니다. 캡슐화가 이루어지는 타입들에서는 필요할 수 있는데, 그렇지 않으면 그냥 값 자체를 매개변수로 전달하거나 리턴값으로 전달하는 경우에 사용하는 경우도 많으니까요

}
}
4 changes: 0 additions & 4 deletions DrawingApp/DrawingApp/Model/Plane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@

import Foundation

protocol Notifiable: AnyObject {
func notifyDidCreated()
}

class Plane {
private var items = [String: Shapable]()
Copy link
Contributor

Choose a reason for hiding this comment

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

String 인걸 보니 Id 값을 이용해서 키를 사용하는 것 같습니다
Shape를 사용한다면 (프로토콜 타입인지 모르겠지만) 타입 자체를 비교하도록 생각해보세요
그래야 Id를 가져오지 않고 타입을 비교하게 됩니다.
카드 게임에서 카드 속성을 가져오지 않고 카드 자체를 비교했던 것처럼요
아래 언급이 있는 것 같은데, Shape가 구체타입이군요. 이 자리를 뭘로 선언해야 할까요?

Copy link
Author

Choose a reason for hiding this comment

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

타입 자체의 비교를 고려한다면 딕셔너리보다는 배열로 바꾸는 것이 적합할 것 같습니다.
원하는 객체를 찾을 땐 동일성 비교로 찾을 수 있고 배열의 타입도 구체타입 대신 프로토콜([Shapable])을 사용할 수 있을 것 같습니다 :)


Expand Down
13 changes: 13 additions & 0 deletions DrawingApp/DrawingApp/Model/Protocols/AlphaAdaptable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// AlphaAdaptable.swift
// DrawingApp
//
// Created by 송태환 on 2022/03/18.
//

import Foundation

protocol AlphaAdaptable {
var alpha: Alpha { get }
func setAlpha(_ alpha: Alpha)
}
Loading