swift/swift 공부

Custom View Modifier 만들기

isak(이삭) 2023. 9. 18. 16:26

정신 없이 프로젝트가 끝났고 마무리를 하면서, 부족했던 부분이나 코드 재사용성에 대해 고민을 하게 되었다.

특히나 view의 modifier를 통해서 형태를 동일하게 유지해야하는 부분이나, 시간이 급해서 따로 View 파일로 만들지 못하고 코드를 두세번 사용한 부분들을 수정하지 못한게 너무 아쉬웠다.

 

그래서 이렇게 ViewModifier을 중복적으로 사용한 부분을 리팩토링할 방법에 대해 생각해 보았고, 그 결과 Custom View Modifier를 만들어 적용해보기로 하였다.

View Modifier를 중복적으로 사용한 예시


위의 코드는 우리 프로젝트의 일부이기 때문에 예시는 다른 예시 코드를 통해 만들어볼 예정이다.

Custom ViewModifier를 만들어 적용했을 때의 결과 화면

조건 - 한집배달과, 세이브배달 두 버튼이 존재함

1. 버튼의 눌림과 상관 없이 버튼의 background에는 cornerRadius가 5이고 line 두께가 1인 둥근 사각형 모양
2. 버튼이 눌리지 않은 상태일 때, foregroundColor는 .black, 폰트 크기는 18, font weight는 .regular
3. 버튼이 눌린 상태일 때, foregroundColor는 .blue, 폰트 크기는 18, font weight는 .bold

 

1번에 해당하는 custom ViewModifier와 2,3번에 해당하는 custom ViewModifier를 따로 만들어 적용할 예정.

 

순서대로 1번 & 2번

ViewModifier 프로토콜을 채택한 구조체를 만들고 함수를 통해 뷰에 적용할 수 있게 만드는 방법인데,

여기서 "content"에 해당하는 부분이 View에서 형태를 변경하고자 하는 부분에 대한 것이다.

단순히 Text, Image가 될 수 있고, 혹은 Stack으로 감싼 전체가 될 수 있는 것이다.

 

이렇게 만든 modifier를 어떻게 적용하는가 ?!

형태를 바꾸어야하는 부분에 modifier를 호출해주고 위에서 만든 구조체를 호출해주면 되는데, modifer를 부르고 구조체를 호출하는게 조금 불편하다.

 

직접 view modifier를 호출하려면 어떻게 해야할까 ?

뷰에 extension을 통해 정의해주면 다른 modifier와 동일하게 점 접근 연산자를 통해 사용이 가능하다. 또한 extension을 통해 만들어준 함수의 이름이 곧 점 접근 연산자를 통해 호출할 수 있는 modifier의 이름이 된다 !

 

여기서 드는 의문점 !

extension을 파일 분리를 해주고 싶어 다른 파일로 옮겨주었더니 View를 찾을 수 없다는 에러 메세지를 얻게 되었다. View를 확장하는 것이기 때문에 그런가 ? 라는 생각이 들지만 평소에도 extension을 다른 파일로 관리하기도 했어서 왜 안되는지 궁금하다!

 

[ 작성하면서 해결함..ㅎㅎ.. ]

정말 사소한 부분을 인지하지 못한 결과로 의문점을 가지게 되었던 것이다 !

xcode에서 new file을 생성할 때, 뷰를 그릴 것이 아니기 때문에 Swift File을 생성하였고, 그 파일은 자동으로 Foundation framework를 import한다. 여기서 Foundation Framework는 주로 데이터 처리와 시스템 작업을 수행하기 위해 있는 것이기 때문에 View를 찾을 수 없던 것이었다. View는 결국 UI관련이고, 애플에서 주는 UI관련 framework는 SwiftUI와 UIKit, AppKit이기 때문에 그래서 import Foundation이 아니라 import SwiftUI를 해야한다. 그러면 파일 분리가 가능하다.

 

 

<최종 코드>

// Text 관련
struct DeliveryTextStyleModifier: ViewModifier {
    let isSelected: Bool
    
    func body(content: Content) -> some View {
        content
            .foregroundColor(isSelected ? .blue : .black)
            .font(.system(size: 18, weight: isSelected ? .bold : .regular))
    }
}

//Button background 관련
struct DeliveryButtonBackgroundModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding(10)
            .background(
                RoundedRectangle(cornerRadius: 5)
                    .stroke(lineWidth: 1)
            )
    }
}


// import SwiftUI로 바꾼 new file로 파일 분리하여 사용 가능
extension View {
    func borderedCaption(condition: Binding<Bool>) -> some View {
        modifier(BorderCaption(condition: condition))
    }
    
    func deliveryTextStyleModifier(isSelected: Binding<Bool>) -> some View {
        modifier(DeliveryTextStyleModifier(isSelected: isSelected))
    }
    
    func deliveryButtonBackgroundModifier() -> some View {
        modifier(DeliveryButtonBackgroundModifier())
    }
}


struct ContentView: View {
    @State private var deliverOpt = deliveryChoice.onlyOne

    var body: some View {
        VStack {
            ForEach(deliveryChoice.allCases, id: \.self) { deliver in
                Button {
                    deliverOpt = deliver
                } label: {
                    VStack(alignment: .leading ) {
                        HStack {
                            Image(systemName: deliverOpt == deliver ? "o.circle.fill" : "o.circle")
                            VStack(alignment: .leading) {
                                Text(deliver.rawValue)
                            }
                            Spacer()
                            switch deliver {
                            case .onlyOne :
                                Text("배달비 : \(deliver.fee)원")
                            case .severalHome :
                                VStack {
                                    ZStack {
                                        Text("배달비 : \(deliveryChoice.onlyOne.fee)원")
                                        Rectangle()
                                            .frame(width: 73, height: 2)
                                            .foregroundColor(.red)
                                            .padding(.leading, 60)
                                    }
                                    Text("\(deliver.fee)원")
                                        .padding(.leading, 60)
                                }
                            }
                        }
                    }
                    .frame(height: 40)
                    // custom view modifier 적용
                    .deliveryButtonBackgroundModifier()
                    .deliveryTextStyleModifier(isSelected: deliverOpt == deliver)
                }
            }
        }
        .padding()
    }
}

Github : https://github.com/isakatty/TIL/tree/main/ButtonAndViewModifier/Meotstagram