import SwiftUI /// A skewed rounded rectangle rendered via shearing a RoundedRectangle. /// - Parameters: /// - shear: Horizontal shear factor. Positive values lean to the right. Typical 0.15 ~ 0.35 /// - cornerRadius: Corner radius of the base rounded rectangle before shear. struct ParallelogramShape: InsettableShape { var shear: CGFloat = 0.25 var cornerRadius: CGFloat = 6 var insetAmount: CGFloat = 0 var animatableData: AnimatablePair { get { AnimatablePair(shear, cornerRadius) } set { shear = newValue.first cornerRadius = newValue.second } } func inset(by amount: CGFloat) -> ParallelogramShape { var copy = self copy.insetAmount += amount return copy } func path(in rect: CGRect) -> Path { // To keep the final shape inside `rect`, we draw a rounded rectangle inset by half of the shear expansion // and apply a shear transform around the vertical center. let h = rect.height let expandX = abs(shear) * h // total width expansion after shearing around center let insetX = expandX / 2 + insetAmount let insetRect = rect.insetBy(dx: insetX, dy: insetAmount) let rr = RoundedRectangle(cornerRadius: max(0, cornerRadius - insetAmount), style: .continuous) var path = rr.path(in: insetRect) // Transform: translate to center, shear, translate back let toCenter = CGAffineTransform(translationX: 0, y: -rect.midY) let shearTransform = CGAffineTransform(a: 1, b: 0, c: shear, d: 1, tx: 0, ty: 0) let back = CGAffineTransform(translationX: 0, y: rect.midY) path = path.applying(toCenter).applying(shearTransform).applying(back) return path } } /// A configurable parallelogram view. /// Example: /// ParallelogramView(width: 36, height: 18, shear: 0.3, cornerRadius: 5, color: .black) public struct ParallelogramView: View { public var width: CGFloat public var height: CGFloat public var shear: CGFloat public var cornerRadius: CGFloat public var color: Color public var stroke: Color? = nil public var lineWidth: CGFloat = 1 public init( width: CGFloat, height: CGFloat, shear: CGFloat = 0.25, cornerRadius: CGFloat = 6, color: Color = .black, stroke: Color? = nil, lineWidth: CGFloat = 1 ) { self.width = width self.height = height self.shear = shear self.cornerRadius = cornerRadius self.color = color self.stroke = stroke self.lineWidth = lineWidth } public var body: some View { let shape = ParallelogramShape(shear: shear, cornerRadius: cornerRadius) Group { if let stroke = stroke, lineWidth > 0 { shape.fill(color) .overlay( shape.stroke(stroke, lineWidth: lineWidth) ) } else { shape.fill(color) } } .frame(width: width, height: height) .accessibilityHidden(true) } } /// A horizontal row that renders a given number of parallelograms. public struct ParallelogramRow: View { public var count: Int public var itemSize: CGSize public var shear: CGFloat public var cornerRadius: CGFloat public var color: Color public var spacing: CGFloat public init( count: Int, itemSize: CGSize, shear: CGFloat = 0.25, cornerRadius: CGFloat = 6, color: Color = .black, spacing: CGFloat = 8 ) { self.count = count self.itemSize = itemSize self.shear = shear self.cornerRadius = cornerRadius self.color = color self.spacing = spacing } public var body: some View { HStack(spacing: spacing) { ForEach(0..<(max(0, count)), id: \.self) { _ in ParallelogramView( width: itemSize.width, height: itemSize.height, shear: shear, cornerRadius: cornerRadius, color: color ) } } .accessibilityElement(children: .ignore) } } #if DEBUG struct Parallelogram_Previews: PreviewProvider { static var previews: some View { Group { // Single parallelogram preview ParallelogramView(width: 20, height: 18, shear: -0.3, cornerRadius: 2, color: .black) .padding() .previewDisplayName("Single") // Row preview (similar to the screenshot) ZStack { Color(white: 0.97) HStack { RoundedRectangle(cornerRadius: 18, style: .continuous) .fill(.white) .shadow(color: Color.black.opacity(0.06), radius: 6, x: 0, y: 2) .overlay( HStack { ParallelogramRow( count: 5, itemSize: CGSize(width: 18, height: 20), shear: -0.3, cornerRadius: 2, color: .black, spacing: 2 ) } .padding(.horizontal, 18) ) .frame(height: 64) .padding() } } .previewDisplayName("Row") } .previewLayout(.sizeThatFits) } } #endif