본문 바로가기

iOS/SwiftUI

Alignment Guides

 

💫 Custom alignment guides

Apple 공식문서

 

Apple Developer Documentation

 

developer.apple.com

private struct FirstThirdAlignment: AlignmentID {
    static func defaultValue(in context: ViewDimensions) -> CGFloat {
        context.height / 3
    }
}

extension VerticalAlignment {
    static let firstThird = VerticalAlignment(FirstThirdAlignment.self)
}

해당 뷰 높이의 3분의 1지점을 정렬하는 FirstThirdAlignment를 새로 만들고 VerticalAlignment에서 스태틱 변수로 추가해준다.

struct LayeredHorizontalStripes: View {
    var body: some View {
        ZStack(alignment: Alignment(horizontal: .center, vertical: .firstThird)) {
            horizontalStripes(color: .blue)
                .frame(width: 180, height: 90)
            horizontalStripes(color: .green)
                .frame(width: 70, height: 60)
        }
    }

    private func horizontalStripes(color: Color) -> some View {
        VStack(spacing: 1) {
            ForEach(0..<3) { _ in color }
        }
    }
}

추가한 firstThird를 ZStack의 VerticalAlignment로 지정하면 이미지처럼 각각의 뷰의 3분의 1지점의 높이가 정렬이된다.

 

 

+) custom center alignment

https://useyourloaf.com/blog/swiftui-stack-custom-center-alignment/

 

💫 활용

CustomRadioButton

https://swiftui-lab.com/alignment-guides/

 

Alignment Guides in SwiftUI - The SwiftUI Lab

Alignment guides are a powerful tool that helps layout our SwiftUI views. They often save us from having to use more complex options (e.g, view preferences)

swiftui-lab.com

extension VerticalAlignment {
    private struct CustomRadioButtonVerticalAlignment: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[VerticalAlignment.center]
        }
    }
    
    static let customRadioButtonVerticalAlignment = VerticalAlignment(CustomRadioButtonVerticalAlignment.self)
}

struct CustomRadioButton: View {
    @State private var index = 0
    
    private let weekdays = ["월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"]
    
    var body: some View {
        HStack(alignment: .customRadioButtonVerticalAlignment) {
            Image(systemName: "checkmark.circle.fill")
                .foregroundColor(.blue)
            
            VStack(spacing: 8) {
                ForEach(weekdays.indices, id: \.self) { index in
                    if self.index == index {
                        Text("\(weekdays[index])")
                            .alignmentGuide(.customRadioButtonVerticalAlignment) { dimensions in
                                dimensions[VerticalAlignment.center]
                            }
                    } else {
                        Text("\(weekdays[index])")
                            .onTapGesture {
                                withAnimation {
                                    self.index = index
                                }
                            }
                    }
                }
            }
        }
    }
}

 

CustomGridTable

extension HorizontalAlignment {
    private struct CustomGridTableHorizontalAlignment: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[.leading]
        }
    }
    
    static let customGridTableHorizontalAlignment = HorizontalAlignment(CustomGridTableHorizontalAlignment.self)
}

struct CustomGridTable: View {
    var body: some View {
        VStack(alignment: .customGridTableHorizontalAlignment) {
            rowTable(title: "Name", content: "이기회")
            rowTable(title: "Address", content: "Korea")
            rowTable(title: "favorite", content: "🍎")
        }
    }
    
    private func rowTable(title: String, content: String) -> some View {
        HStack {
            Text(title)
                .fontWeight(.bold)
            
            Text(content)
                .alignmentGuide(.customGridTableHorizontalAlignment) { dimensions in
                    dimensions[.leading]
                }
        }
    }
}

 

CustomGraphView

extension HorizontalAlignment {
    private struct CustomGraphViewHorizontalAlignment: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[HorizontalAlignment.center]
        }
    }
    
    static let customGraphViewHorizontalAlignment = HorizontalAlignment(CustomGraphViewHorizontalAlignment.self)
}

extension VerticalAlignment {
    private struct CustomGraphViewVerticalAlignment: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[VerticalAlignment.center]
        }
    }
    
    static let customGraphViewVerticalAlignment = VerticalAlignment(CustomGraphViewVerticalAlignment.self)
}

extension Alignment {
    static let customGraphViewAlignment = Alignment(horizontal: .customGraphViewHorizontalAlignment, vertical: .customGraphViewVerticalAlignment)
}

CustomGraphViewHorizontalAlignmentCustomGraphViewVerticalAlignment를 만들고 이를 조합한 customGraphViewAlignment 스태틱 변수를 사용한다.

struct CustomGraphView: View {
    @State private var graphMaxHeight = CGFloat.zero
    
    @State private var graphZeroHeight = CGFloat.zero
    
    @State private var graphWidth = CGFloat.zero
    
    private var barMaxHeight: CGFloat { graphZeroHeight - graphMaxHeight }
    
    private let yAxisCount = 9
    
    private let maxValue: Int = 100
    
    private let weekdays = ["월", "화", "수", "목", "금", "토", "일"]
    
    private let weekdata = [70, 100, 45, 79, 62, 20, 20]
    
    var body: some View {
        GeometryReader { geometry in
            let size = geometry.size
            
            ZStack(alignment: .customGraphViewAlignment) {
                yAxisView(size: size)
                
                xAxisView(size: size)
            }
        }
        .frame(width: 300, height: 400)
    }
    
    private func yAxisView(size: CGSize) -> some View {
        VStack {
            HStack {
                yAxisLabel("\(maxValue)", size: size)
                
                yAxisBar()
                    .overlay(
                        GeometryReader { geometry in
                            let frame = geometry.frame(in: .global)
                            
                            Color.clear
                                .onAppear {
                                    graphWidth = frame.size.width
                                    graphMaxHeight = frame.origin.y
                                }
                        }
                    )
            }
            
            Spacer()
            
            ForEach(0..<yAxisCount, id: \.self) { i in
                HStack {
                    let totalYAxisCount = yAxisCount + 1
                    let currentIndex = i + 1
                    let valueGap = maxValue - 0
                    yAxisLabel("\(maxValue - currentIndex * valueGap / totalYAxisCount)", size: size)
                    
                    yAxisBar()
                }
                
                Spacer()
            }
            
            HStack {
                yAxisLabel("0", size: size)
                
                yAxisBar()
                    .alignmentGuide(.customGraphViewHorizontalAlignment) { $0[.trailing] }
                    .alignmentGuide(.customGraphViewVerticalAlignment) { $0[.top] }
                    .overlay(
                        GeometryReader { geometry in
                            let frame = geometry.frame(in: .global)
                            
                            Color.clear
                                .onAppear {
                                    graphZeroHeight = frame.origin.y
                                }
                        }
                    )
            }
        }
        .frame(height: size.height)
    }
    
    private func yAxisLabel(_ label: String, size: CGSize) -> some View {
        Text(label)
            .frame(width: size.width / 10, alignment: .trailing)
    }
    
    private func yAxisBar() -> some View {
        Rectangle()
            .frame(height: 1)
            .foregroundColor(.gray)
    }
    
    private func xAxisView(size: CGSize) -> some View {
        HStack {
            ForEach(weekdays.indices, id: \.self) { index in
                let height = CGFloat(weekdata[index]) * barMaxHeight / 100
                
                VStack {
                    Rectangle()
                        .frame(width: 15, height: height - 1)
                        .foregroundColor(.blue)
                        .frame(maxHeight: .infinity, alignment: .bottom)
                        .alignmentGuide(.customGraphViewVerticalAlignment) { $0[.bottom] }
                    
                    Text("\(weekdays[index])")
                }
                
                if index != weekdays.count - 1 {
                    Spacer()
                }
            }
        }
        .padding(.horizontal)
        .frame(width: graphWidth)
        .alignmentGuide(.customGraphViewHorizontalAlignment) { $0[.trailing] }
    }
}