SwiftUI로 앱을 작성하다 보면, View에 대한 ViewModifier를 수족처럼 사용하게 된다.
이를테면 가장 많이 사용하는 .padding()부터, .foregroundStyle(), .background() 등 View에 사용할 수 있는 많은 Modifier가 존재한다.
이에 더해 Button, NavigationLink 등 Label 아규먼트에 View 객체를 전달하여 사용하는 경우 비슷한 패턴의 코드가 반복될 수 있다.
가령 기계적으로 다음과 같은 뷰를 작성하다 보면, 문득 이런 생각이 들 수 있다.
var coinList: some View {
LazyVStack {
ForEach(filteredList, id: \.id) { item in
if let index = coin.firstIndex(where: { $0.id == item.id }) {
NavigationLink {
CoinDetailView(item: $coin[index])
} label: {
CoinRowView(coin: $coin[index])
.padding(.horizontal, 20)
}
.buttonStyle(PlainButtonStyle())
Divider()
.isHidden(item == coin.last)
.padding(.horizontal, 100)
}
}
}
}
var coinGrid: some View {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(filteredList, id: \.id) { item in
if let index = coin.firstIndex(where: { $0.id == item.id }) {
NavigationLink {
CoinDetailView(item: $coin[index])
} label: {
CoinCellView(coin: $coin[index])
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
NavigationLink를 더 간단하게 쓸 수 없을까?
ViewModifier를 활용해서 NavigationLink 코드를 줄일 수 없을까??
이런 경우에, ViewModifier Protocol을 활용해 효과적으로 코드를 개선할 수 있다.
ViewModifier
뷰 또는 다른 뷰 수정자에 적용하여 새로운 뷰를 생성하는 수정자입니다.
SwiftUI와 그 시작을 함께한 이 프로토콜을 채택하면, 내가 원하는 여러 가지 Modifier를 손쉽게 조합하여 사용할 수 있다.
Reducing view modifier manintenance 아티클을 보면, 이를 활용하는 예시가 잘 설명되어 있다.
과정을 간략히 요약해 보자면,
- 반복적으로 사용되는 modifier들을 ViewModifier protocol을 채택한 Instance에 모은다.
- 편리하고, 직관적으로 사용할 수 있도록 View의 extension에 위에서 구현한 Custom Modifier를 사용하는 메서드를 만든다.
단순이 이 작업만으로 반복적인 패턴을 줄일 수 있다.
Practice!
그럼 이제 위에서 본 코드에 적용해보고 싶어진다.
Idea
나는 NavigationLink를 더욱 간결하게 쓰고 싶다.
NavigationLink {
CoinDetailView(item: $coin[index])
} label: {
CoinCellView(coin: $coin[index])
}
이를테면 다음과 같은 형태로 사용하고 싶다.
PresentaionView // 기존 Label 아규먼트에 넘겨주던 View
.wrapToLink(DestinationView) // CustomModifier로 만들 부분
그렇다면 어떻게 해야 할까?
위 아티클의 내용을 보고 내릴 수 있는 가정은 다음과 같다.
- 기존의 View를 NavigationLink로 반환하는 Wrapper를 만든다.
- View에서 간편하게 사용하기 위해 extension에 해당 Wrapper를 간편하게 사용할 수 있는 메서드를 만든다.
그럼 하나씩 해보자.
Step 1.
NavigationLink에는 여러 가지 생성자가 있다. 그중 현재 사용 중인 형태의 아규먼트 타입을 확인하기 위해 캡처해 봤다.
destination을 Escaping Closure로 받고 있다. 메서드의 Definition을 타고 들어가 확인해 보면, Destination은 View protocol을 받아주는 Generic인 걸 알 수 있다.
struct NavigationLinkWrapper<Destination: View>: ViewModifier {
let destination: Destination
func body(content: Content) -> some View {
NavigationLink {
destination
} label: {
content
}
.buttonStyle(PlainButtonStyle())
.contentShape(Rectangle())
}
}
그래서 destination을 받아줄 Property를 이렇게 작성했다.
추가로 Label View의 UI를 지정한 대로 보여주기 위해 .buttonStyle()을, View 전체를 Hitbox로 잡기 위해 .contentShape() modifier를 사용했다.
Step 2.
기존 View를 NavigationLink로 반환하는 Wrapper를 만들었으니, '편리하고 직관적으로' 사용할 수 있도록 메서드를 만들어야 한다.
extension View {
func wrapToLink<Destination: View>(_ destination: Destination) -> some View {
modifier(NavigationLinkWrapper(destination: destination))
}
}
이렇게 메서드를 만들어주면, 다음과 같이 사용할 수 있다.
var coinList: some View {
LazyVStack {
ForEach(filteredList, id: \.id) { item in
if let index = coin.firstIndex(where: { $0.id == item.id }) {
CoinRowView(coin: $coin[index])
.wrapToLink(CoinDetailView(item: $coin[index]))
Divider()
.isHidden(item == coin.last)
.padding(.horizontal, 100)
}
}
}
}
var coinGrid: some View {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(filteredList, id: \.id) { item in
if let index = coin.firstIndex(where: { $0.id == item.id }) {
CoinCellView(coin: $coin[index])
.wrapToLink(CoinDetailView(item: $coin[index]))
}
}
}
}
이렇게 사용하면, 기존 NavigationLink 생성자를 이용한 방법보다 훨씬 깔끔하게 코드를 작성할 수 있다. 또 Brace 사용을 줄여 조금이나마 덜 헷갈리는 SwiftUI 코드를 작성할 수 있다.
Wrap UP
ViewModifie를 이용해 Custom Modifier를 만들어 코드를 개선해 봤다.
이를 잘 활용하면, 원하는 스타일을 적용해 Custom Component처럼 사용할 수도 있고, 반복적인 패턴을 줄여 가독성을 높일 수도 있을 것 같다.
최근, 오래간만에 SwiftUI를 공부하고 있다.
ViewModifier는 WWDC19에서 SwiftUI와 함께 공개된 프로토콜이지만, 여태 모르고 있었다는 것에 많은 생각에 잠겼다.
왜 그동안 몰랐을까?라고 고민해 봤을 때, 불편한 점을 느끼지 못했거나, 코드를 개선하기 위한 고민이 부족했던 게 그 원인으로 꼽을 수 있을 것 같다. (가장 위험한 적은 '이 정도면 충분해~')
그간 몰랐던 새로운 지식을 얻음과 동시에 코드에 대한 태도를 점검해 볼 수 있는 시간이었다.
앞으로는 코드의 작성보다도 이미 작성한 코드를 더욱 면밀하고 치밀하게 고민해 보는 시간을 늘려 나가야겠다.
'개발 > iOS' 카테고리의 다른 글
[RxSwift] .observe(on:) vs .subscribe(on:) (0) | 2025.02.19 |
---|---|
[UIButton] .addTarget VS .addAction (0) | 2025.02.06 |