iyOmSd/Title: SwiftUI

[SwiftUI] Previewable, PreviewModifier 프리뷰 데이터 공유하기

냄수 2024. 11. 25. 00:24
반응형

Preview에서 사용가능한 개념을 알아보려합니다.

Preview에서는 동적인 상태 값을 추가하는게 번거로웠었는데요

#Preview {
    TextField(text: .constant("")) {
        Text("입력하세요")
    }
}

예를들어 이런 Binding코드에대한 동적인 값을 넣어주기가 힘들었죠

하지만 그 부분을 해결해준게 @Previewable 매크로 입니다

 

Previewable

 

#Preview {
    @Previewable @State var text: String = ""
    TextField(text: $text) {
        Text("입력하세요")
    }
}

#Preview 클로저 안에 넣어주기만 하면 끝이에요

프리뷰에서만 해당 상태값을 사용할 수 있어요

정말 간편하죠

 

#Preview 클로저 밖에서 쓰면 에러가 납니다

 

매크로를 확장해서보면

저희가 사용한 Previewable 프로퍼티가 

이렇게 적용된걸 볼 수 있어요

 

 

@Previewable은 미리보기에서의 상태설정에 대한 기능이였고 

각 미리보기에 특정 데이터나 Environment를 공유할 수 있는 기능도 있습니다.

 

PreviewModifier

프리뷰에서 재사용할 공유 컨텍스트를 정의할 수 있습니다.(preview끼리 공유가능)

이에 따라 중복된 코드를 제거할 수 있습니다.

 

프로토콜준수에 필요한 함수는 2개가 있습니다.

makeSharedContext, body함수입니다. 

 

makeSharedContext()

프리뷰에 적용할 공유 컨텍스트를 만듭니다. 

여기서 반환된 컨텍스트는 캐시되어 이 유형의 모디파이어를 적용하는 모든 프리뷰의 body 메서드에 전달됩니다.

한번만 호출되며 데이터통신이 필요한 프리뷰인경우 아주 효율적이죠

 

이부분이 필요한 데이터를 캐싱하는 역할을 합니다. 

 

body(content: Content, context: Context) -> some View 

프리뷰로 정의한 View가 전달되고 위에서 정의한 Context를 받아서 처리 할 수 있습니다.

공유할 데이터를 사용하거나, environment를 공유하거나, 공통적으로 뷰에 대한 속성을 정의 할 수 있습니다.

 

 

struct TestModifier: PreviewModifier {
    static func makeSharedContext() async throws -> Item {
        let item = Item()
        print("호출")
        return item
    }
    
        
    func body(content: Content, context: Item) -> some View {
        VStack {
            content
                .environment(context)
            Color.red.frame(height: 100)
        }
    }
}

@Observable
class Item {
    private(set) var numbers: [Int] = (1...20).map { $0 }
    
    func append(_ number: Int) {
        numbers.append(number)
    }
}

PreviewModifier을 준수한 타입을 만들어주고

캐싱할 데이터를 environment로 전달해줬습니다.

TestModifier를 적용한 모든 프리뷰에 일괄적용되니

body쪽에 코드를 적용해주면 중복된 코드가 제거되는 이점을 얻을 수 있습니다. 

 

간편하게 modifier만 넣어주면 배열 데이터를 같이 사용할 수 있습니다.

또한 위 코드에서 빨간 뷰를 정의했으니

TestModifier를 적용한 Preview에는 밑에 빨간 뷰가 적용되있을겁니다. 

#Preview("테스트1", traits: .modifier(TestModifier())) {
    @Previewable @Environment(Item.self) var item
    
    VStack {
        Color.indigo.opacity(0.4)
            .overlay {
                Text("\(item.numbers)")
            }
        Button("+") {
            let newNum = (item.numbers.last ?? 0) + 1
            item.append(newNum)
        }
    }
}

#Preview("테스트2", traits: .modifier(TestModifier())) {
    @Previewable @Environment(Item.self) var item
    
    ScrollView {
        ForEach(item.numbers, id: \.self) {
            Text("\($0)")
        }
    }
}

물론 방법은 많습니다..!

지금예제처럼 Environment에 정의해서 사용하는방법도 있고

간단하게 바로 Modifier body함수에서 적용하는 방법도 있고

쓰는용도에 맞게 개발자가 사용하면 될 것 같아요!

 

 

extension을 이용해서 적용을 더 간편하게 할 수 있어요

extension PreviewTrait where T == Preview.ViewTraits {
    static var testModifier: Self = .modifier(TestModifier())
}
#Preview("테스트1", traits: .modifier(TestModifier())) { ... }

/// extension 적용 
#Preview("테스트1", traits: .testModifier) { ... }

 

 

Xcode16.1 환경에서 테스트를 해보다가

여러개의 프리뷰를 사용해서 적용해도

makeSharedContext가 1번만 호출되야 하는게 기대동작인데

계속 호출되는 현상을 발견했어요

 

로그가 계속찍혀서 이유는 모르겠지만 버그.. 겟죠?!

혹시나 통신을 안해서인가? 해서 

이미지통신도 넣어봣지만 계속 찍히더라구요

 

 

이미지 통신한 모델로 변경된 

전체 코드는 아래와 같습니다!

 

조금의 수정사항으로는 

각 테스트에 해당하는 뷰타입을 만들어줬고 

Environment를 각 뷰 안으로 넣어준정도의 코드정리입니다.

struct ContentView: View {
    @Environment(Item.self) private var item
    
    var body: some View {
        Image(uiImage: UIImage(data: item.image)!)
    }
}


struct ContentView2: View {
    @Environment(Item.self) private var item
    
    var body: some View {
        ScrollView {
            ForEach(item.numbers, id: \.self) {
                Text("\($0)")
            }
        }
    }
}

#Preview("테스트1", traits: .testModifier) {
    ContentView()
}

#Preview("테스트2", traits: .testModifier) {
    ContentView2()
}

// MARK: - 모디파이어 정의

struct TestModifier: PreviewModifier {
    typealias Context = Item
    
    static func makeSharedContext() async throws -> Context {
        print("호출")
        let url =  URL(string: "https://i.pinimg.com/736x/2c/93/3f/2c933fdd5a3b0a5f8a51a0f44eb0f619.jpg")!
        let imageData = try await URLSession.shared.data(from: url).0
        try await Task.sleep(for: .seconds(3))
        print(imageData)
        return Item(image: imageData)
    }
    
    func body(content: Content, context: Context) -> some View {
        VStack {
            content
                .environment(context)
            Color.red.frame(height: 100)
        }
    }
}

// MARK: - 사용 모델 정의

@Observable
class Item {
    let numbers: [Int] = (1...20).map { $0 }
    var image: Data
    
    init(image: Data) {
        self.image = image
    }
}

// MARK: - extension 정의

extension PreviewTrait where T == Preview.ViewTraits {
    static var testModifier: Self = .modifier(TestModifier())
}

 

 

 

 

 

 

 

반응형