📝

【SwiftUI】ヘッダーの左右中央にビューを配置する(Layoutプロトコル)

に公開
struct ContentView: View {
    var body: some View {
        Header {
            Text("CenterContent")
        } leftContent: {
            Text("LeftContent")
        } rightContent: {
            Text("RightContent")
        }
        .border(.pink)
        .padding(.horizontal)
    }
}

struct Header<C1: View, C2: View, C3: View>: View {
    var idealSpacing: CGFloat = 100
    
    @ViewBuilder let centerContent: C1
    @ViewBuilder let leftContent: C2
    @ViewBuilder let rightContent: C3
    
    var body: some View {
        HeaderLayout(idealSpacing: idealSpacing) {
            leftContent; centerContent; rightContent
        }
    }
}

/// subviews[0]: leftContent, subviews[1]: centerContent, subviews[2]: rightContent
///
struct HeaderLayout: Layout {
    var idealSpacing: CGFloat
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        guard subviews.count == 3 else { fatalError() }
        
        if let width = proposal.width {
            let proposal = ProposedViewSize(width: proposal.width, height: nil)
            
            let subviewSizes = subviews.map { $0.sizeThatFits(proposal) }
            
            let additionalSpacing = max(subviewSizes[0].width, subviewSizes[2].width) - min(subviewSizes[0].width, subviewSizes[2].width)
            
            let minWidth = subviewSizes.reduce(0) { $0 + $1.width } + additionalSpacing + 20
            
            let width = max(width, minWidth)
            let height = subviewSizes.max { $0.height < $1.height }!.height
            
            return .init(width: width, height: height)
        } else {
            let leftContentSize = subviews[0].sizeThatFits(.unspecified)
            let centerContentSize = subviews[1].sizeThatFits(.unspecified)
            let rightContentSize = subviews[2].sizeThatFits(.unspecified)
            
            let additionalSpacing = max(leftContentSize.width, rightContentSize.width) - min(leftContentSize.width, rightContentSize.width)
            
            let spacing = idealSpacing * 2 + additionalSpacing
            let width = leftContentSize.width + centerContentSize.width + rightContentSize.width + spacing
            let height = max(leftContentSize.height, centerContentSize.height, rightContentSize.height)
            
            return .init(width: width, height: height)
        }
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        guard subviews.count == 3 else { fatalError() }
        
        let proposal = ProposedViewSize(width: proposal.width, height: nil)
        
        let leftContentPosition = CGPoint(x: bounds.minX, y: bounds.midY)
        subviews[0].place(at: leftContentPosition, anchor: .leading, proposal: proposal)
        
        let centerContentPosition = CGPoint(x: bounds.midX, y: bounds.midY)
        subviews[1].place(at: centerContentPosition, anchor: .center, proposal: proposal)
        
        let rightContentPosition = CGPoint(x: bounds.maxX, y: bounds.midY)
        subviews[2].place(at: rightContentPosition, anchor: .trailing, proposal: proposal)
    }
}

Discussion