재치 프로젝트에서는 많은 태그 디자인들이 존재한다.
현재 Tag 디자인 시스템은 Tag라는 부모 클래스가 존재하며, 자식 클래스를 색상으로 분리해 구현했다.
extension ZatchComponent{
enum TagColor{
case purple
case yellow
}
enum TagConfiguration{
case height20
case height25
case height29
case height31
}
}
class Tag: PaddingLabel{
var isDisabled = false{
didSet{
isDisabled ? setDisabledState() : setNormalState()
}
}
var isSelected = false{
didSet{
isSelected ? setSelectState() : setNormalState()
}
}
private let configuration: TagType
private let colorType: TagColor
init(color: TagColor, configuration: TagType){
self.colorType = color
self.configuration = configuration
super.init(frame: .zero)
...
}
private func style(){
self.layer.cornerRadius = configuration.height / 2
self.clipsToBounds = true
self.setTypoStyleWithSingleLine(typoStyle: configuration.font)
}
private func setNormalState(){
self.tag = ViewTag.normal
self.textColor = colorType.textColor
self.backgroundColor = colorType.backgroundColor
}
private func setDisabledState(){
self.tag = ViewTag.deselect
self.textColor = colorType.disabledTextColor
self.backgroundColor = colorType.disabledBackgroundColor
}
private func setSelectState(){
self.tag = ViewTag.select
self.textColor = colorType.selectedTextColor
self.backgroundColor = colorType.selectedBackgroundColor
}
}
class PurlpleTag: Tag{
init(configuration: ZatchComponent.Tag.TagType){
super.init(color: .purple, configuration: configuration)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class YellowTag: Tag{
init(configuration: ZatchComponent.Tag.TagType){
super.init(color: .yellow, configuration: configuration)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
위의 설계를 통해 Tag 디자인 시스템을 사용할 때는 아래와 같이 선언해 줬었다.
let tagLabel = ZatchComponent.YellowTag(configuration: .height31)
이렇게 설계해 놓은 Tag에 대해서 다시 설계해야겠다고 생각한 이유는 다음과 같다
새롭게 설계할 때는 어떻게 클래스를 나눌지, 프로토콜을 사용할지 아니면 클래스 상속을 사용할지 고민을 시작했다.
먼저 프로젝트에서 사용되는 모든 태그들을 모아서 케이스를 나누어 보았다.
일단 stroke와 filled 스타일은 무조건 클래스를 구분해야겠다고 생각했다.
stroke는 색상 지정 프로퍼티가 strokeColor가 되어야 하고, filled는 backgroundColor가 되어야 한다고 생각했기 때문이다.
이제 새롭게 설계한 Tag와 하위 클래스에 대해서 설명해 보도록 하겠다.
먼저 구조를 살펴보면
Tag
┣ FilledTag + Tagable
┣ StrokeTag + Tagable
Tag에서 하위 클래스의 인스턴스를 생성해 주는 정적 메서드도 추가하였다.
static func filled(color: ZatchComponent.TagColor, configuration: ZatchComponent.TagConfiguration) -> FilledTag{
return FilledTag(color: color, configuration: configuration)
}
static func stroke(color: ZatchComponent.TagColor, configuration: ZatchComponent.TagConfiguration) -> StrokeTag{
return StrokeTag(color: color, configuration: configuration)
}
여기서 그냥 FilledTag(color: , configuration)을 사용하지, 왜 정적 메서드를 선언해 인스턴스를 생성하고 반환하는 거지라고 의문을 가질 수도 있을 것이다.
프로젝트를 한 2주(1주일 수도)만 안 해도 Tag 하위로 어떤 클래스가 있는지 기억을 못 할 것이다. (실제 경험담…ㅋㅋ)
Tag라는 디자인 시스템이 있다는 것은 알더라도 borderLine 속성을 가진 Tag 하위 클래스 이름이 StrokeFilled인 것을 모를 가능성이 높다.
이를 위해서 정적 메서드를 활용하는 것이다.
일단 Tag에 접근하면 정적 메서드 리스트를 생성해 줄 것이니 그 안에서 원하는 타입을 고르면 되는 것이다.
사실 아래 코드와 같은 케이스는 정적 메서드 구현하는 것을 망설이지 않을 텐데, 위의 경우는 조금 고민을 하긴 했었다.
static func generateMakeSufficientSpaceMessage() -> ToastMessageView{
return ToastMessageView(image: Image.toastSufficientSpace, message: "스크롤을 올리면 이모지를 남길 수 있어요!")
}
정적 메서드의 매개변수와 생성자의 매개변수가 완전히 일치하기 때문..
하지만 난 나의 기억력을 믿지 않기 때문에.. 메서드를 추가하기로 결정했다
Tag 클래스는 Stroke, Filled 케이스 모두가 공통으로 사용하는 프로퍼티/메서드만 넣었다.
(응집도를 높여야..)
공통으로 사용되는 부분들이 꽤 있기 때문에 프로토콜을 통해 역할 확장만 시키는 것보다는 직접 상속시켜 주는 것이 낫다고 판단했다.
extension ZatchComponent{
class Tag: PaddingLabel{
private let configuration: ZatchComponent.TagConfiguration
let color: ZatchComponent.TagColor
init(color: ZatchComponent.TagColor, configuration: ZatchComponent.TagConfiguration){
self.color = color
self.configuration = configuration
super.init(padding: configuration.padding)
initialize()
}
static func filled(color: ZatchComponent.TagColor, configuration: ZatchComponent.TagConfiguration) -> FilledTag{
return FilledTag(color: color, configuration: configuration)
}
static func stroke(color: ZatchComponent.TagColor, configuration: ZatchComponent.TagConfiguration) -> StrokeTag{
return StrokeTag(color: color, configuration: configuration)
}
func initialize(){
style()
layout()
}
private func style(){
layer.cornerRadius = configuration.height / 2
clipsToBounds = true
setTypoStyleWithSingleLine(typoStyle: configuration.font)
}
private func layout(){
self.snp.makeConstraints{
$0.height.equalTo(configuration.height)
}
}
func setTitle(_ title: String){
text = title
}
func setCategoryTitle(categoryId: Int){
let category = ServiceType.Zatch.getCategoryFromCategories(at: categoryId)
text = category.title
}
}
}
다음은 Tagable 프로토콜이다.
사실 네이밍이 마음에 들지 않는다. 하지만 아직 프로토콜 네이밍을 어떻게 지어야 할지 감도 안 오고 도저히 모르겠어서.. 일단은 이렇게
isSelected 프로퍼티를 통해 select 상태를 표현할 것이다.
여담이지만 프로토콜은.. private이 불가능한 게 너무 아쉽다..
protocol Tagable{
var isSelected: Bool { get set }
func setSelectState()
func setUnselectState()
}
FilledTag의 경우
2가지를 모두 표현해야 하므로 Tagable을 통해 isSelected 프로퍼티 설정하는 것과 더불어 isDisabled 프로퍼티와 setDisabledState() 메서드도 추가하였다.
// FilledTag.swift
extension ZatchComponent{
class FilledTag: ZatchComponent.Tag, Tagable{
var isDisabled = false{
didSet{
isDisabled ? setDisabledState() : setUnselectState()
}
}
var isSelected = false{
didSet{
isSelected ? setSelectState() : setUnselectState()
}
}
override func initialize() {
super.initialize()
setUnselectState()
}
func setSelectState(){
tag = ViewTag.select
textColor = color.selectedTextColor
backgroundColor = color.selectedBackgroundColor
}
func setUnselectState(){
tag = ViewTag.normal
textColor = color.textColor
backgroundColor = color.backgroundColor
}
private func setDisabledState(){
tag = ViewTag.deselect
textColor = color.disabledTextColor
backgroundColor = color.disabledBackgroundColor
}
}
}
// StrokeTag.swift
extension ZatchComponent{
class StrokeTag: ZatchComponent.Tag, Tagable{
var isSelected = true{
didSet{
isSelected ? setSelectState() : setUnselectState()
}
}
override func initialize() {
super.initialize()
setSelectState()
}
func setSelectState() {
layer.borderWidth = 1.5
layer.borderColor = color.selectedColor.cgColor
textColor = color.selectedColor
}
func setUnselectState() {
layer.borderWidth = 1
layer.borderColor = color.unselectedColor.cgColor
textColor = color.unselectedColor
}
}
}
여기서 이번 리팩토링을 하면서 가장 큰 수확이라고 생각한 fileprivate 접근제어 활용 방안을 소개하겠다.
아까 언급했다시피 TagColor는 2가지 케이스가 존재한다.
enum TagColor{
case purple
case yellow
}
이 열거형은 Filled 타입, Stroke 타입 구분 없이 동일하다.
하지만 TagColor extension으로 TagColorStyle 구조체를 선언하고, 이 안에 선언하는 프로퍼티 종류는 달라야 했다.
앞에서 언급했다 싶이 stroke 타입은 색상을 지정할 때 filled 케이스와 다른 네이밍의 프로퍼티를 사용해야 한다고 판단했기 때문이다.
처음에는 where 절을 통해 사용되는 클래스에 따라 TagColorStyle를 다르게 구현할 생각이었다.
하지만 아쉽게도 TagColor는 protocol이 아닌 enum형이기 때문에 이 방법은 불가능했다.
그래서 다른 방법은 뭐가 있지 고민해 보았는데fileprivate라는 접근제어가 생각이 났다.
해당 파일 내에서만 사용이 가능한 것이니까 같은 파일 안에 넣어준다면 가능하지 않을까? 싶었던 것이다.
결과는 성공이었다.
// FilledTag.swift
fileprivate extension ZatchComponent.TagColor{
struct TagColorStyle{
let textColor: UIColor
let backgroundColor: UIColor
let selectedTextColor: UIColor = .white
let selectedBackgroundColor: UIColor
let disabledTextColor: UIColor
let disabledBackgroundColor: UIColor
}
private var colorInfo: TagColorStyle{
switch self{
case .purple:
return TagColorStyle(textColor: .zatchPurple,
backgroundColor: .purple40,
selectedBackgroundColor: .zatchPurple,
disabledTextColor: .black20,
disabledBackgroundColor: .black10)
case .yellow:
return TagColorStyle(textColor: .zatchDeepYellow,
backgroundColor: .yellow40,
selectedBackgroundColor: .zatchDeepYellow,
disabledTextColor: .black20,
disabledBackgroundColor: .black10)
}
}
var textColor: UIColor{
colorInfo.textColor
}
var backgroundColor: UIColor{
colorInfo.backgroundColor
}
var selectedTextColor: UIColor{
colorInfo.selectedTextColor
}
var selectedBackgroundColor: UIColor{
colorInfo.selectedBackgroundColor
}
var disabledTextColor: UIColor{
colorInfo.disabledTextColor
}
var disabledBackgroundColor: UIColor{
colorInfo.disabledBackgroundColor
}
}
// StrokeTag.swift
fileprivate extension ZatchComponent.TagColor{
struct TagColorStyle {
let backgroundColor: UIColor = .white
let unselectStateColor: UIColor = .black45
let selectedStateColor: UIColor
}
private var colorInfo: TagColorStyle{
switch self{
case .purple:
return TagColorStyle(selectedStateColor: .zatchPurple)
case .yellow:
return TagColorStyle(selectedStateColor: .zatchYellow)
}
}
var backgroundColor: UIColor{
colorInfo.backgroundColor
}
var unselectedColor: UIColor{
colorInfo.unselectStateColor
}
var selectedColor: UIColor{
colorInfo.selectedStateColor
}
}
새롭게 설계한 Tag는 아래와 같이 선언해 사용하고 있다.
Tag를 접근하기만 하면 filled와 stroke라는 정적 메서드가 등장하기 때문에 오랜만에 프로젝트를 진행하더라도 어떤 하위 클래스가 있는지 찾아볼 필요 없이 접근할 수 있을 것이다!
let filledTag = ZatchComponent.Tag.filled(color: .yellow, configuration: .height31)
let strokeTag = ZatchComponent.Tag.stroke(color: .yellow, configuration: .height31)
이번 리팩토링 진행하면서 얻은 것은
Animation 활용해 TabBar 구현하기 (0) | 2023.04.27 |
---|---|
JSON 파일 생성 및 파싱해 프로젝트에서 사용하기 (0) | 2023.04.27 |
Protocol과 상속 관계를 활용해 HeaderView 구성하기 (0) | 2023.02.05 |
UITextField에 값 할당한 경우 이벤트 감지시키기 (0) | 2023.01.08 |
UITableView 상단 space 제거 (0) | 2023.01.03 |