안녕하세요.
이번에는 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 |