개요
내가 스위프트에 입문하면서 가장 편하다고 생각했던 부분 중 하나다. 바로 ViewModifier.
물론 안드로이드를 처음 입문했을때는 선언형 UI가 잘 알려지지도 않았을 때라 전부 스토리보드로 xml짜고 드래그 앤 드롭하면서 UI를 만들었었기에 처음 입문했을때는 좀 어색하기는 했다.
근데 2달정도 이걸로 일을 하다보니 1년이상 공부했던 안드로이드보다 생산성적인 측면에서 말도 안되게 성장했다는 것을 느꼈다.
스토리보드 당시(안드로이드에서 이걸 스토리보드라고 하는지는 잘 기억이 안나긴 하지만) xml파일에서 뷰를 먼저 구성해주고,
바인딩 할 거 다 하고, 그 다음 코드를 작성했었다.
근데 SwiftUI를 비롯한 Flutter, RN, Compose등은 파일 하나에서 뷰 생성, 변수(바인딩 등) 관리, 함수 연결 등을 할 수 있다는게 믿을 수가 없었다.
갑자기 옛날 생각이 나서 좀 서론이 길었는데, 하여튼 이 Modifier라는 것을 통해서 Component의 속성을 마음대로 휘어잡을 수 있다.
물론 Modifier 입력 순서에 따라 결과가 조금 달라지긴 하지만, 크게 중요하지는 않다. 당연한거니까.
Flutter, Compose, RN 등 요즘 선두 주자를 달리는 모든 선언형 앱 UI 프레임워크를 전부 사용해봤지만, SwiftUI만큼 간결한 문법은 아니었다. 물론 내가 SwiftUI가 주력이라 익숙해서 그런걸수도 있겠지만, Modifier를 통한 UI의 간단한 변경이 너무 좋았다.
그럼 이제 Modifier가 뭔지 대충 알아보고 Custom Modifier를 만드는 방법을 알아보겠다.
Modifier
직역하면 수정자라고 한다. Constructor, Initializer 같은 생성자와 같은 맥락이라고 보면 된다. 다만 생성자는 객체를 만들어주는 역할이고 이 수정자는 객체의 내부 사항을 수정해주는 역할인거다.
Flutter에서는 Text를 만들 때, 생성자에서 많은 것을 처리해 일괄적으로 만드는 방식이 많다.
const Text(
'FLUTTER',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.red,
),
),
이런식으로 Text의 생성자에 모든 필요한 정보를 입력해서 만든다. 요즘에는 Modifier를 많이 도입하고 있다고 하는데 잘 모르겠다.
하여튼 SwiftUI에서는 텍스트를 만들 때, 생성자가 여러개 있지만 Attribute를 직접적으로 설정할 수는 없다.
Text("SwiftUI")
이렇게 만드는 게 전부다. 텍스트 포맷을 바꾸거나 심지어 이미지까지도 넣을 수는 있지만, Content값만 변경하는거지 Decoration과 관련된 속성들은 생성자에서 처리할 수 없다는 이야기다.
그럼 이 텍스트를 앞서 본 Flutter의 속성과 동일하게 만들기 위해서는 어떻게 해야할까? 이 때 수정자(Modifier)를 사용하면 된다.
Text("SwiftUI")
.font(.system(size: 20, weight: .semibold))
.foregroundStyle(.red)
weight 700 값이 얼마나 되는지 잘 몰라서 세미볼드로 넣어봤다.
여러분이 보기에는 어떤게 가독성이 더 좋아보이는가? 사람마다 다르겠지만, 본인은 후자가 압도적으로 편하다.
플러터도 수정자를 사용해서 빠르게 뷰를 구성할 수 있게 된다면, 나도 플러터..... 생각은 해보도록 하겠다.
이제 이 Modifier를 커스텀하게 만들어서 사용하는 방법 두가지를 알아보도록 하겠다.
특정 뷰에서만 쓸 수 있는 Modifier
이 방법은 사실 ViewModifier는 아니다. ViewModifier라고 하면 어느 뷰에서든 (Text, Image, VStack, ScrollView, etc...) 사용할 수 있는 범용적인 것인데, 이 방법은 특정 뷰(심지어 내가 만든)에서만 사용이 가능하다. 일단 코드를 보도록 하자.
Text("SwiftUI")
.bold()
.foregroundStyle(.yellow)
.strikethrough()
Text("SwiftUI")
.strikethrough()
가령 저 텍스트에 존재하는 모든 수정자를 빈번히 사용하는 상황이 발생했다고 해보자. 100개의 Text중 40개에 적용해야하고 나머지 60개는 아래처럼 strikethrough()만 적용해야한다.
이때 저걸 컴포넌트화 한 뒤, Modifier를 통해 어느것을 적용시킬 지 판단하는 방법이 있다.
struct MyText: View {
private var myEffect: Bool = false
var body: some View {
if myEffect {
Text("SwiftUI")
.bold()
.foregroundStyle(.yellow)
.strikethrough()
} else {
Text("SwiftUI")
.strikethrough()
}
}
public func shouldApplyEffect(_ value: Bool) -> MyText {
var view = self
view.myEffect = value
return view
}
}
개중에 생성자에서 처리하면 되지 왜 함수를 따로 빼서 처리하냐 하는 사람도 있다. 하지만 SwiftUI를 사용하는 이상 Apple의 철학을 따르는게 맞다고 생각하고 본인은 생성자에 많은 인자를 넘기는걸 부담스러워하는 사람이라 이 방법을 선호한다. 다만 앞서 말했듯 이건 특정 뷰, MyText에서만 사용이 가능하다. 따라서 다음과 같은 상황은 컴파일 에러가 발생한다.
MyText()
.padding() // <- 이 수정자는 View를 return한다.
.shouldApplyEffect(true) // View에는 shouldApplyEffect라는 수정자 혹은 함수가 없다.
따라서 위 처럼 사용하기 위해서는 다음과 같이 코드를 써야한다.
MyText()
.shouldApplyEffect(true)
.padding()
혹시나 커스텀 뷰 만들때마다 생성자에서 모든걸 처리하고 있었다면 이렇게 한 번 시도해보는건 어떨까? 한 번 잡숴보시라.
Custom ViewModifier
이번에는 진짜 ViewModifier부분이다. 이 방법대로 수정자를 만들면 어느 뷰에서든 사용할 수 있으니 잘 기억해두자.
가령 여러분이 이미지든 텍스트든 투명도 30퍼센트의 검은색을 오버레이하고 테두리에 노란색 외곽선을 2두께로 칠하며 모서리를 10둥글기로 만들어야 한다면, 코드는 다음과 같을 것이다.
Image(someImage)
.resizable()
.scaledToSomeThing()
.frame(someSize)
.overlay {
ZStack {
Color.black.opacity(0.3).cornerRadius(10)
RoundedRectangle(cornerRadius: 10)
.stroke(.yellow, lineWidth: 2)
}
}
오버레이 부분의 코드를 반복적으로 사용해야한다면 얼마나 끔찍한가. 혹여라도 둥글기가 달라진다거나 색이 달라지면 또 다 찾아가서 바꿔줘야한다. 컴포넌트로 만들어버리면, 나중에 저 효과를 없애라는 요구가 떨어졌을 때, 일반 이미지로 다시 바꿔줘야하니 여간 골치 아픈게 아니다. 또 이미지만이 아니라 텍스트든 레이아웃이든 어디든 적용가능할수도 있으니 이럴 때 수정자를 하나 만들어주면 된다.
먼저 ViewModifier struct를 만들어주자.
struct MyModifier: ViewModifier {
func body(content: Content) -> some View {
content
.overlay {
ZStack {
Color.black.opacity(0.3).cornerRadius(10)
RoundedRectangle(cornerRadius: 10)
.stroke(.yellow, lineWidth: 2)
}
}
}
}
이렇게만 만들어줘도 다음과 같이 사용이 가능하다.
SomeView()
.modifier(MyModifier())
하지만 우리는 우리가 만든 수정자에 이름을 붙여서 다른 수정자처럼 간결하게 사용하고 싶기 때문에, 이것 이상의 작업이 필요하다.
View의 extension에 접근해 다음과 같은 코드를 넣어보자
extension View {
func myEffect() -> some View {
modifier(MyModifier())
}
}
자 이렇게 하면 우리도 우리의 수정자를 만들어냈고 '어느'뷰에서든 이걸 사용할 수 있다.
Text("Super Coooool")
.myEffect()
SomeImage()
.myEffect()
SomeView()
.myEffect()
모두 동일한 효과가 적용이 된다.
확장
지금까지 한 것만으로도 작업량이 대폭 줄어들 수 있지만, 가령 이펙트의 큰 방향은 같은데 외곽선의 두께가 적용해야하는 것마다 약간씩 다르다면? 위 방법만으로는 사용이 불가능하기 때문에 저 지옥의 overlay코드를 일일히 붙여줘야 한다.
하지만 Modifier는 argument도 넘겨줄 수 있다. 다음처럼 작성해보자.
struct MyModifier: ViewModifier {
var lineWidth: CGFloat
func body(content: Content) -> some View {
content
.overlay {
ZStack {
Color.black.opacity(0.3).cornerRadius(10)
RoundedRectangle(cornerRadius: 10)
.stroke(.yellow, lineWidth: lineWidth)
}
}
}
}
extension View {
func myEffect(_ lineWidth: CGFloat) -> some View {
modifier(MyModifier(lineWidth: lineWIdth))
}
}
자 완성이다. 이런식으로 필요에 따라 유동적으로 수정자를 만들어주면 된다.
'iOS' 카테고리의 다른 글
| [Swift] 1인 앱 개발로 사용할 기술 스택 및 서비스 (4) | 2024.10.02 |
|---|---|
| [SwiftUI] onAppear는 화면이 그려진 뒤에 불리는 함수가 아니다? (0) | 2024.09.25 |
| [SwiftUI] GridView 만들기 (1) | 2024.09.10 |
| [SwiftUI] SafeArea height 구하기 (0) | 2024.09.09 |
| [Swift] Firebase Crashlytics 적용하기 (2) | 2024.09.04 |