SwiftUI

Custom Adaptive StackView (feat. ViewBuilder)

Phililip
728x90

안녕하세요.

 

이번에는 SwiftUI에서 레이아웃에 따라 뷰가 재배치되도록 사용성을 고려한 간단한 적응형 Stack 뷰를 만들어보는 시간을 가져볼게요.

 

 

이번 글에서는 horizontalSizeClass와 dynamicTypeSize를 사용하니, 잘 모르시면 아래 글을 먼저 읽어보시는 것을 추천드려요.

- horizontalSizeClass, dynamicTypeSize

 

horizontalSizeClass, dynamicTypeSize

안녕하세요. 이번에는 SwiftUI EnvironmentValues 중 horizontalSizeClass 프로퍼티와 dynamicTypeSize 프로퍼티에 대해 알아볼게요. # 1. horizontalSizeClass 끝에 Class라는 이름이 붙긴 했지만, 이 친구는 프..

phillip5094.tistory.com

 


 

## 1. Horizontal Size에 따른 적응형 뷰

아래처럼 horizontalSizeClass 환경 변수를 사용해서 Stack 뷰 축을 바꿔줄 수 있어요.

struct CompactStack<Content>: View where Content: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        if horizontalSizeClass == .compact {
            VStack { content }
        } else {
            HStack { content }
        }
    }
}
struct ContentView: View {    
    var body: some View {
        CompactStack {
            Text("01:00:05:12")
            Button(role: .destructive) {
            } label: {
                Label("Reset", systemImage: "clock.arrow.circlepath")
            }
        }
    }
}

 

결과는 아래처럼 나오겠죠??ㅎㅎ

 

hoizontalSizeClass = compact hoizontalSizeClass = regular

 

 

 

 

## 2. Dynamic Type Size에 따른 적응형 뷰

아래처럼 Dynamic Type size에 따라서 뷰를 다르게 구성시킬 수도 있어요.

struct AccessibleStack<Content>: View where Content: View {
    @Environment(\.dynamicTypeSize) var dynamicTypeSize
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        if dynamicTypeSize.isAccessibilitySize {
            VStack { content }
        } else {
            HStack { content }
        }
    }
}
struct ContentView: View {
    var body: some View {
        AccessibleStack {
            Text("01:00:05:12")
            Button(role: .destructive) {
            } label: {
                Label("Reset", systemImage: "clock.arrow.circlepath")
            }
        }
    }
}

 

dynamicTypeSize = large dynamicTypeSize = accessibility large

 

 

 

## 3. HorizontalSizeClass & DynaimcTypeSize에 따른 적응형 뷰

horizontalSizeClass와 DynamicTypeSize 값을 모두 고려한 적응형 뷰를 만들어줄 수도 있습니다.

 

위에서는 뷰를 다르게 구성이 될 조건문이 body 안에 직접 들어가 있었는데, 이 조건을 사용자가 직접 커스텀할 수 있도록 ConditionHandler라는 이름으로 따로 빼줄게요.

struct AdaptiveStack<Content: View>: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    @Environment(\.dynamicTypeSize) var dynamicTypeSize
    
    public typealias ConditionHandler = (UserInterfaceSizeClass?, DynamicTypeSize) -> Bool   
    private let condition: ConditionHandler    ✅
    private let content: Content
    
    public init(condition: @escaping ConditionHandler, @ViewBuilder content:() -> Content) {
        self.condition = condition
        self.content = content()
    }
    
    public var body: some View {
        if condition(horizontalSizeClass, dynamicTypeSize) {    ✅
            VStack { content }
        } else {
            HStack { content }
        }
    }
}

 

그럼 사용자는 직접 handler를 구현해서 뷰에 넘겨주기만 하면 되니 사용성이 좋아졌습니다ㅎㅎ

struct ContentView: View {
    private func compactXXXLarge(horizontalSizeClass: UserInterfaceSizeClass?, dynamicTypeSize: DynamicTypeSize) -> Bool {
        horizontalSizeClass == .compact && dynamicTypeSize >= .xxxLarge
    }
    
    var body: some View {
        AdaptiveStack(condition: compactXXXLarge) {    ✅
            Text("01:00:05:12")
            Button(role: .destructive) {
            } label: {
                Label("Reset", systemImage: "clock.arrow.circlepath")
            }
        }
    }
}

 

 

그런데 사용자 입장에서 생각해보면, 간단한 조건일 경우에도 매번 handler를 구현해야 하니 귀찮을 수도 있겠죠??

 

 

그래서 기본적인 handler는 내부적으로 static function으로 구현하고 각 static function을 적절히 사용할 수 있도록 enum도 제공하면 사용성이 좋아질 것 같아요.

extension AdaptiveStack {
    static private func compact(horizontalSizeClass: UserInterfaceSizeClass?, dynamicTypeSize: DynamicTypeSize) -> Bool {
        horizontalSizeClass == .compact
    }
    
    static private func regular(horizontalSizeClass: UserInterfaceSizeClass?, dynamicTypeSize: DynamicTypeSize) -> Bool {
        horizontalSizeClass == .regular
    }
    
    static private func accessible(horizontalSizeClass: UserInterfaceSizeClass?, dynamicTypeSize: DynamicTypeSize) -> Bool {
        dynamicTypeSize.isAccessibilitySize
    }
    
    static private func compactAccessible(horizontalSizeClass: UserInterfaceSizeClass?, dynamicTypeSize: DynamicTypeSize) -> Bool {
        horizontalSizeClass == .compact &&
        dynamicTypeSize.isAccessibilitySize
    }
    
    static private func regularAccessible(horizontalSizeClass: UserInterfaceSizeClass?, dynamicTypeSize: DynamicTypeSize) -> Bool {
        horizontalSizeClass == .regular &&
        dynamicTypeSize.isAccessibilitySize
    }
    
    public enum Condition {
        case compact
        case regular
        case accessible
        case compactAccessible
        case regularAccessible

    }

    static private func handler(_ condition: Condition) -> ConditionHandler {
        switch condition {
        case .compact:
            return compact
        case .regular:
            return regular
        case .accessible:
            return accessible
        case .compactAccessible:
            return compactAccessible
        case .regularAccessible:
            return regularAccessible
        }
    }
}

 

 

enum을 사용하는 initializer도 추가해주면 아래처럼 쉽게 사용이 가능합니다ㅎㅎ

extension AdaptiveStack {
    public init(condition: Condition, @ViewBuilder content:() -> Content) {
        self.init(condition: AdaptiveStack.handler(condition), content: content)
    }
}
struct ContentView: View {    
    var body: some View {
        AdaptiveStack(condition: .compact) {    ✅
            Text("01:00:05:12")
            Button(role: .destructive) {
            } label: {
                Label("Reset", systemImage: "clock.arrow.circlepath")
            }
        }
    }
}

 

 

마지막으로..

 

AdaptiveStack 뷰가 출력하는 HStack, VStack도 커스텀하고 싶을 수가 있겠죠?? (alignment, spacing 등등)

 

요런 값들까지 파라미터로 받아주면 좋겠죠?? 😎

struct AdaptiveStack<Content: View>: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    @Environment(\.dynamicTypeSize) var dynamicTypeSize
    
    public typealias ConditionHandler = (UserInterfaceSizeClass?, DynamicTypeSize) -> Bool
    
    
    private let horizontalAlignment: HorizontalAlignment	✅
    private let horizontalSpacing: CGFloat?    			✅
    private let verticalAlignment: VerticalAlignment		✅
    private let verticalSpacing: CGFloat?			✅
    
    private let condition: ConditionHandler
    private let content: Content
    
    public init(horizontalAlignment: HorizontalAlignment = .center,    ✅
                horizontalSpacing: CGFloat? = nil,
                verticalAlignment: VerticalAlignment = .center,
                verticalSpacing: CGFloat? = nil,
                condition: @escaping ConditionHandler,
                @ViewBuilder content:() -> Content) {
        self.horizontalAlignment = horizontalAlignment
        self.horizontalSpacing = horizontalSpacing
        self.verticalAlignment = verticalAlignment
        self.verticalSpacing = verticalSpacing
        self.condition = condition
        self.content = content()
    }
    
    public var body: some View {
        if condition(horizontalSizeClass, dynamicTypeSize) {
            VStack(alignment: horizontalAlignment, spacing: verticalSpacing) {    ✅
                content
            }
        } else {
            HStack(alignment: verticalAlignment, spacing: horizontalSpacing) {    ✅
                content
            }
        }
    }
}

 

👍 👍 👍

 

 

 

## 참고

- https://useyourloaf.com/blog/swiftui-adaptive-stack-views/

 

SwiftUI Adaptive Stack Views

How do you adapt your SwiftUI layouts for varying dynamic type size and available horizontal space?

useyourloaf.com

 


 

사용성과 확장성을 고려해서 커스텀 적응형 뷰를 만들어본 것에 의의를 가지려고 합니다ㅎㅎ

 

이번 글은 여기서 마무리.

 

 

 

반응형

'SwiftUI' 카테고리의 다른 글

MotionScape  (0) 2022.05.06
Dismiss Presented View  (0) 2022.04.25
horizontalSizeClass, dynamicTypeSize  (0) 2022.04.17
Gesture  (0) 2022.04.01
__printChanges  (0) 2022.03.23