티스토리 뷰

반응형

 

안녕하세요 :) Zedd입니다.

오늘은 Custom View Modifier만들기 및...이걸로 버전 분기 꼼수..

 

# 목적

이번 글에서는 

1. View Modifier가 어떤것인지

2. Custom View Modifier를 만드는 방법

3. (번외) Custom View Modifier를 사용한 버전 분기 (꼼수)..

를 다룬다. 

 

# View Modifier

SwiftUI에는 ViewModifier라는 프로토콜이 존재한다.

이 modifier를 적용하면 View의 원래 값의 다른 버전을 생성하게 된다. 

 

어렵게 생각할 필요 없이, View Modifier는 그냥 우리가 늘상 쓰는 

struct ContentView: View {
            
    var body: some View {
        Text("Zedd")
            .font(.caption2)
            .padding(10)
            .overlay(
                RoundedRectangle(cornerRadius: 15)
                    .stroke(lineWidth: 1)
            )
            .foregroundColor(Color.blue)
    }
}

.font, .padding() 이런것들이 다 (View) Modifier다 

 

#  Custom View Modifier 만들기

언제 Custom View Modifier를 만들면 좋을까? 

예를 들어, 내가 앱 내에서 전체적으로 쓰이는 텍스트 스타일이 있다고 해보자.

struct ContentView: View {
            
    var body: some View {
        Text("Zedd")
            .font(.caption2)
            .padding(10)
            .overlay(
                RoundedRectangle(cornerRadius: 15)
                    .stroke(lineWidth: 1)
            )
            .foregroundColor(Color.blue)
    }
}

뭐 이런식...근데 앱내에서 이게 너무 자주 쓰여서

1. 모든 텍스트에 저 modifier를 붙혀주는게 귀찮음

2. 스타일이 변경/추가/삭제되면 모든 텍스트에 붙어있는 스타일을 수정해야함. 

그냥 아래와 같이 

struct ContentView: View {
           
    var body: some View {
        Text("Zedd")
            .asPrimaryCaption2()
    }
}


이렇게 Custom View Modifier를 만들어 적용하면 가독성 증가 및 관리도 쉬워질것이다. 


그렇다면 Custom View Modifier를 만들어보자.

위에서 말했듯이 ViewModifier는 프로토콜이다.

당연히 이를 conform해야 Custom View Modifier를 만들 수 있다. 

1. ViewModifier conform

struct PrimaryCaption2Text: ViewModifier { }

ViewModifier를 conform하는 타입을 만든다.

2. body 정의 및 구현 

ViewModifier는 body 메소드를 요구한다. 

struct PrimaryCaption2Text: ViewModifier {
    
    func body(content: Content) -> some View {
        content
            .font(.caption2)
            .padding(10)
            .overlay(
                RoundedRectangle(cornerRadius: 15)
                    .stroke(lineWidth: 1)
            )
            .foregroundColor(Color.blue)
    }
}

공통스타일을 여기에 정의해준다. 

3. 위 Modifier를 호출해주는 메소드 생성 

이제 만들어야 할 것은 

struct ContentView: View {
           
    var body: some View {
        Text("Zedd")
            .asPrimaryCaption2()
    }
}


저 asPrimaryCaption2이다. 

View를 conform하는 모든 타입들은 해당 modifier를 호출할 수 있어야 하므로

extension View {
    
    func asPrimaryCaption2() -> some View {
        modifier(PrimaryCaption2Text())
    }
}

View를 extension하여 modifier을 만들어준다.

(modifier메소드는 그냥 새로운 modifier를 만들어주는 메소드라고 보면 된다.) 

4. 사용

struct ContentView: View {
           
    var body: some View {
        Text("Zedd")
            .asPrimaryCaption2()
    }
}

 


사실

struct ContentView: View {
            
    var body: some View {
        Text("Zedd")
            .modifier(PrimaryCaption2Text()) // ✅
    }
}

이런식으로 바로 View에 .modifier를 호출해도 되지만

View를 extension하는 방식이 보다 일반적이고 관용적인 접근이라고 한다. (from 애플)


 

# Custom View Modifier를 사용한 버전 분기 (꼼수)

iOS 15에서 SwiftUI에 refreshable modifier가 나왔다.

하지만 내 앱의 최소버전은 iOS 14고.. refreshable을 쓰려면 View를 분리하는 수 밖에 없다. 

(분리하는 것 말고 다른 방법이 있는지 모르겠음..)

분리하기는 싫고..요걸 Custom Modifier로 꼼수를 부릴 수 있지 않을까..싶었다.

ScrollView {
	...
}
.refreshIfPossible()

 이런식..!? 

 

#Custom View Modifier 만들기에서 했던 순서를 그대로 따라해보자. 

1. ViewModifier conform

2. body 정의 및 구현 

3. 위 Modifier를 호출해주는 메소드 생성 

4. 사용

 

1. ViewModifier conform

struct Refresh: ViewModifier {}

2. body 정의 및 구현 

struct Refresh: ViewModifier {

    let completion: () -> Void
    
    init(completion: @escaping () -> Void) {
        self.completion = completion
    }
    
    func body(content: Content) -> some View {
        if #available(iOS 15.0, *) {
            content
                .refreshable { self.completion() }
        } else {
            content
        }
    }
}

그냥 이해하기 쉽게 completion으로 작성했는데,

async/await을 사용한 코드는 하단의 [개선 (feat. concurrency)]에서 다룬다.

3. 위 Modifier를 호출해주는 메소드 생성 

extension View {

    func refreshIfPossible(completion: @escaping () -> Void) -> some View {
        modifier(Refresh(completion: completion))
    }
}

4. 사용

struct ContentView: View {
    
    var body: some View {
        NavigationView {
            List(1..<10) { row in
                Text("Row \(row)")
            }
            .refreshIfPossible {
                print("Zedd")
            }
        }
    }
}

 

[결과]

왼쪽 14.5 / 오른쪽 15.2

 

[고려사항]

당연히 위 꼼수는

iOS 14 -> refresh 기능 X

iOS 15 -> refresh 기능 O

라서 유저를 위한;;; 개발 방법은 아니라고 생각한다.

근데 나는 그냥 개인적으로 만드는 앱이기도 하고 반드시 SwiftUI의 refreshable을 쓰고싶었기 때문에 ^^....... !! 그냥 꼼수...

(날 이렇게 만든건 refreshable을 iOS 15에 내준 애플임을 밝힘)

 

[개선 (feat. concurrency)]

extension View {

    func refreshIfPossible(_ action: @escaping () async -> Void) -> some View {
        modifier(Refresh(action))
    }
}


struct Refresh: ViewModifier {

    let action: () async -> Void
    
    init(_ action: @escaping () async -> Void) {
        self.action = action
    }
    
    func body(content: Content) -> some View {
        if #available(iOS 15.0, *) {
            content
                .refreshable { await action() }
        } else {
            content
        }
    }
}

struct ContentView: View {
    
    var body: some View {
        NavigationView {
            List(1..<10) { row in
                Text("Row \(row)")
            }
            .refreshIfPossible {
                await requestSomething()
            }
        }
    }
}

이렇게 하는거 맞겠지...

반응형