안녕하세요.
이번에는 SwiftUI에서 레이아웃에 따라 뷰가 재배치되도록 사용성을 고려한 간단한 적응형 Stack 뷰를 만들어보는 시간을 가져볼게요.
이번 글에서는 horizontalSizeClass와 dynamicTypeSize를 사용하니, 잘 모르시면 아래 글을 먼저 읽어보시는 것을 추천드려요.
- horizontalSizeClass, dynamicTypeSize
## 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' 카테고리의 다른 글
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 |