구현하려던 UI | 구현한 UI(BoxLayout) | 추가 구현한 UI(BoxRowLayout) | 추가 구현한 UI(BoxGridLayout) |
---|---|---|---|
BoxView를 BoxType으로 나누어 UI 적용
enum BoxType {
case large // 노랑색, 금액 나오는 가장 큰 박스
case normal // 검정색, 작은 박스
case recentPayment // 검정색, 하단 가장 최근 결제
}
struct BoxView: View {
var title: String
var subTitle: String
var boxType: BoxType
var largeImageSize: CGFloat = 40
var smallImageSize: CGFloat = 30
var body: some View {
GeometryReader { geometryProxy in
VStack(alignment: .leading, content: {
Text(title)
.font(boxType == .normal ? .headline: .system(size: 26).bold())
.foregroundColor(boxType == .large ? .black : .white)
.offset(x: 10)
.padding(EdgeInsets(top: 16, leading: 0, bottom: 1, trailing: 0))
Text(subTitle)
.font(.subheadline)
.foregroundColor(boxType == .large ? Color.gray : Color.white.opacity(0.5))
.offset(x: 10)
VStack(alignment: .trailing, content: {
Image(systemName: "camera")
.resizable()
.scaledToFit()
.frame(width: boxType == .large ? largeImageSize : smallImageSize, height: boxType == .large ? largeImageSize : smallImageSize)
.foregroundColor(Color.white.opacity(0.5))
})
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .bottomTrailing
)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 16))
})
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.background(boxType == .large ? Color.yellow : Color.black)
}
}
}
// BoxView 자체에 radius와 shadow를 주기 위한 modifier
struct BoxModifier: ViewModifier {
func body(content: Content) -> some View {
content
.cornerRadius(12)
.shadow(color: Color(.systemGray4), radius: 8)
}
}
stack뷰의 조합으로 만들어보려 했는데, 각각의 frame 사이즈를 이런 직접적으로 적용하는건 비효율적인 것 같아서 Layout으로 수정
VStack {
HStack {
BoxView(title: "4,200원", subTitle: "카카오페이머니", boxType: .large).modifier(BoxModifier())
.frame(width: (UIScreen.main.bounds.width - 40)*2/3)
VStack {
BoxView(title: "선택하기", subTitle: "송금", boxType: .normal).modifier(BoxModifier())
BoxView(title: "선택하기", subTitle: "결제", boxType: .normal).modifier(BoxModifier())
}
}
BoxView(title: "iTunes&App Store", subTitle: "3,000원", boxType: .recentPayment)
.modifier(BoxModifier())
.frame(height: 140)
}
.frame(height: 420, alignment: .top)
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
WWDC 영상 참고
// Layout, ProposedViewSize 16.0부터 가능
struct BoxLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard !subviews.isEmpty else { return }
let spacing: CGFloat = 10
let minSize = CGSize(width: (bounds.width - spacing * 2) / 3, height: (bounds.height - spacing * 2) / 3)
let midSize = CGSize(width: bounds.width, height: minSize.height)
let maxSize = CGSize(width: minSize.width * 2 + spacing, height: minSize.height * 2 + spacing)
let x = bounds.minX
let y = bounds.minY
var sizeProposal = ProposedViewSize(maxSize)
for index in subviews.indices {
switch index {
case 0:
subviews[index].place(at: CGPoint(x: x, y: y),
anchor: .topLeading,
proposal: sizeProposal)
case 1:
sizeProposal = ProposedViewSize(minSize)
subviews[index].place(at: CGPoint(x: x + maxSize.width + spacing, y: y),
anchor: .topLeading,
proposal: sizeProposal)
case 2:
sizeProposal = ProposedViewSize(minSize)
subviews[index].place(at: CGPoint(x: x + maxSize.width + spacing, y: y + minSize.height + spacing),
anchor: .topLeading,
proposal: sizeProposal)
case 3:
sizeProposal = ProposedViewSize(midSize)
subviews[index].place(at: CGPoint(x: x, y: y + maxSize.height + spacing),
anchor: .topLeading,
proposal: sizeProposal)
default:
break
}
}
}
}
BoxLayout {
BoxView(title: "4,200원", subTitle: "카카오페이머니", boxType: .large).modifier(BoxModifier())
BoxView(title: "선택하기", subTitle: "송금", boxType: .normal).modifier(BoxModifier())
BoxView(title: "선택하기", subTitle: "결제", boxType: .normal).modifier(BoxModifier())
BoxView(title: "iTunes&App Store", subTitle: "3,000원", boxType: .recentPayment).modifier(BoxModifier())
}
.padding()
.frame(height: 420)
struct BoxRowLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard !subviews.isEmpty else { return }
let subviewsCount: CGFloat = 3
let spacing: CGFloat = 10
let size = CGSize(width: (bounds.width - spacing * 2) / subviewsCount, height: bounds.height)
var x = bounds.minX
for index in subviews.indices {
subviews[index].place(at: CGPoint(x: x, y: bounds.minY),
anchor: .topLeading,
proposal: ProposedViewSize(size))
x += size.width + spacing
}
}
}
VStack(spacing: 10) {
BoxView(title: "16P", subTitle: "카카오페이포인트", boxType: .recentPayment).modifier(BoxModifier())
.frame(height: 140)
BoxRowLayout {
BoxView(title: "선택하기", subTitle: "송금", boxType: .normal).modifier(BoxModifier())
BoxView(title: "선택하기", subTitle: "결제", boxType: .normal).modifier(BoxModifier())
BoxView(title: "iTunes&App Store", subTitle: "3,000원", boxType: .normal).modifier(BoxModifier())
}
.frame(height: 140)
BoxRowLayout {
BoxView(title: "선택하기", subTitle: "송금", boxType: .normal).modifier(BoxModifier())
BoxView(title: "선택하기", subTitle: "결제", boxType: .normal).modifier(BoxModifier())
BoxView(title: "iTunes&App Store", subTitle: "3,000원", boxType: .normal).modifier(BoxModifier())
}
.frame(height: 140)
}
.padding()
struct BoxGridLayout: Layout {
let rowitemCount: Int
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard !subviews.isEmpty else { return }
let spacing: CGFloat = 10
let length = (bounds.width - spacing * CGFloat((rowitemCount - 1))) / CGFloat(rowitemCount)
var x = bounds.minX
var y = bounds.minY
for index in subviews.indices {
subviews[index].place(at: CGPoint(x: x, y: y),
anchor: .topLeading,
proposal: ProposedViewSize(CGSize(width: length, height: length)))
if index % rowitemCount == rowitemCount - 1 {
x = bounds.minX
y += length + spacing
} else {
x += length + spacing
}
}
}
}
BoxGridLayout(rowitemCount: 3) {
BoxView(title: "선택하기", subTitle: "송금", boxType: .normal).modifier(BoxModifier())
BoxView(title: "선택하기", subTitle: "결제", boxType: .normal).modifier(BoxModifier())
BoxView(title: "iTunes&App Store", subTitle: "3,000원", boxType: .normal).modifier(BoxModifier())
BoxView(title: "선택하기", subTitle: "송금", boxType: .normal).modifier(BoxModifier())
BoxView(title: "선택하기", subTitle: "결제", boxType: .normal).modifier(BoxModifier())
BoxView(title: "iTunes&App Store", subTitle: "3,000원", boxType: .normal).modifier(BoxModifier())
BoxView(title: "선택하기", subTitle: "결제", boxType: .normal).modifier(BoxModifier())
}
.padding()