Swift中负cornerRadius实现:如何设计带环绕标签的负圆角标签?
Got it, let's break this down into two key parts: creating those tricky negative rounded corners (inward curves) for your tags, then wrapping them around a button seamlessly. UIKit's built-in cornerRadius only handles outward curves, so we'll need to roll our own solution for the inward ones.
Part 1: Implementing Negative Rounded Corners
The core idea here is to use a UIBezierPath to draw a shape that "cuts out" an inward corner, then apply that path as a mask to your tag view. Here's a reusable custom view you can use:
import UIKit class NegativeRoundedTagView: UIView { // Configure the radius of the inward corner var negativeCornerRadius: CGFloat = 8 { didSet { setNeedsLayout() } } // Define which corner should be the inward one enum NegativeCorner { case topLeft, topRight, bottomLeft, bottomRight } var negativeCorner: NegativeCorner = .bottomRight { didSet { setNeedsLayout() } } override func layoutSubviews() { super.layoutSubviews() let path = UIBezierPath() let viewBounds = self.bounds let radius = negativeCornerRadius // Draw the path based on the selected negative corner switch negativeCorner { case .bottomRight: path.move(to: CGPoint(x: viewBounds.minX, y: viewBounds.minY)) path.addLine(to: CGPoint(x: viewBounds.maxX, y: viewBounds.minY)) path.addLine(to: CGPoint(x: viewBounds.maxX, y: viewBounds.maxY - radius)) // Draw the inward arc path.addArc(withCenter: CGPoint(x: viewBounds.maxX - radius, y: viewBounds.maxY - radius), radius: radius, startAngle: 0, endAngle: .pi/2, clockwise: false) path.addLine(to: CGPoint(x: viewBounds.minX, y: viewBounds.maxY)) path.close() case .topLeft: path.move(to: CGPoint(x: viewBounds.maxX, y: viewBounds.minY)) path.addLine(to: CGPoint(x: viewBounds.maxX, y: viewBounds.maxY)) path.addLine(to: CGPoint(x: viewBounds.minX + radius, y: viewBounds.maxY)) path.addArc(withCenter: CGPoint(x: viewBounds.minX + radius, y: viewBounds.maxY - radius), radius: radius, startAngle: .pi/2, endAngle: .pi, clockwise: false) path.addLine(to: CGPoint(x: viewBounds.minX, y: viewBounds.minY)) path.close() case .topRight: path.move(to: CGPoint(x: viewBounds.minX, y: viewBounds.minY)) path.addLine(to: CGPoint(x: viewBounds.minX, y: viewBounds.maxY)) path.addLine(to: CGPoint(x: viewBounds.maxX - radius, y: viewBounds.maxY)) path.addArc(withCenter: CGPoint(x: viewBounds.maxX - radius, y: viewBounds.maxY - radius), radius: radius, startAngle: .pi, endAngle: 3 * .pi/2, clockwise: false) path.addLine(to: CGPoint(x: viewBounds.maxX, y: viewBounds.minY)) path.close() case .bottomLeft: path.move(to: CGPoint(x: viewBounds.maxX, y: viewBounds.minY)) path.addLine(to: CGPoint(x: viewBounds.minX, y: viewBounds.minY)) path.addLine(to: CGPoint(x: viewBounds.minX, y: viewBounds.maxY - radius)) path.addArc(withCenter: CGPoint(x: viewBounds.minX + radius, y: viewBounds.maxY - radius), radius: radius, startAngle: 3 * .pi/2, endAngle: 2 * .pi, clockwise: false) path.addLine(to: CGPoint(x: viewBounds.maxX, y: viewBounds.maxY)) path.close() } // Apply the path as a mask to the view let maskLayer = CAShapeLayer() maskLayer.path = path.cgPath self.layer.mask = maskLayer } }
How This Works
- We define which corner should be inward and set a radius for the curve.
- In
layoutSubviews, we manually draw aUIBezierPaththat skips the outward corner and instead draws an inward arc. - We use this path as a
CAShapeLayermask, so only the area inside the path is visible—creating the negative rounded corner effect.
Part 2: Wrapping Tags Around a Button
Now let's combine these tag views with a button. We'll use a UIStackView to align everything neatly, ensuring the negative corners of the tags fit perfectly against the button's rounded corners.
class TagWrappedButtonView: UIView { let leftTag = NegativeRoundedTagView() let centerButton = UIButton(type: .system) let rightTag = NegativeRoundedTagView() override init(frame: CGRect) { super.init(frame: frame) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { // Configure left tag (inward corner on bottom right) leftTag.negativeCorner = .bottomRight leftTag.backgroundColor = .systemBlue leftTag.addSubview(UILabel(text: "Left Tag", textColor: .white, font: .systemFont(ofSize: 14))) // Configure right tag (inward corner on top left) rightTag.negativeCorner = .topLeft rightTag.backgroundColor = .systemBlue rightTag.addSubview(UILabel(text: "Right Tag", textColor: .white, font: .systemFont(ofSize: 14))) // Configure center button centerButton.setTitle("Action", for: .normal) centerButton.backgroundColor = .systemBlue centerButton.setTitleColor(.white, for: .normal) centerButton.layer.cornerRadius = 8 // Match tag's negative corner radius centerButton.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) // Combine everything in a horizontal stack let stackView = UIStackView(arrangedSubviews: [leftTag, centerButton, rightTag]) stackView.axis = .horizontal stackView.alignment = .center stackView.spacing = 0 // No gap between tags and button stackView.translatesAutoresizingMaskIntoConstraints = false addSubview(stackView) // Set up auto layout constraints NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: topAnchor), stackView.leadingAnchor.constraint(equalTo: leadingAnchor), stackView.trailingAnchor.constraint(equalTo: trailingAnchor), stackView.bottomAnchor.constraint(equalTo: bottomAnchor), leftTag.widthAnchor.constraint(greaterThanOrEqualToConstant: 60), rightTag.widthAnchor.constraint(greaterThanOrEqualToConstant: 60), leftTag.heightAnchor.constraint(equalTo: centerButton.heightAnchor), rightTag.heightAnchor.constraint(equalTo: centerButton.heightAnchor) ]) // Center labels inside tags if let leftLabel = leftTag.subviews.first as? UILabel, let rightLabel = rightTag.subviews.first as? UILabel { leftLabel.translatesAutoresizingMaskIntoConstraints = false rightLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ leftLabel.centerXAnchor.constraint(equalTo: leftTag.centerXAnchor), leftLabel.centerYAnchor.constraint(equalTo: leftTag.centerYAnchor), rightLabel.centerXAnchor.constraint(equalTo: rightTag.centerXAnchor), rightLabel.centerYAnchor.constraint(equalTo: rightTag.centerYAnchor) ]) } } } // Helper extension for quick label creation extension UILabel { convenience init(text: String, textColor: UIColor, font: UIFont) { self.init() self.text = text self.textColor = textColor self.font = font self.textAlignment = .center } }
Key Notes
- Make sure the button's
cornerRadiusmatches the tag'snegativeCornerRadiusfor a seamless fit. - The stack view's
spacingis set to 0 so the tags and button touch directly. - We constrain the tags to match the button's height, keeping the layout uniform.
Bonus Tips
- For vertical wrapping (tags above/below the button), change the stack view's
axisto.verticaland adjust the tags'negativeCornervalues (e.g., top tag uses.bottomLeftand.bottomRight, bottom tag uses.topLeftand.topRight). - Add shadow effects to the tags and button for depth: set
layer.shadowColor,layer.shadowOffset,layer.shadowOpacity, andlayer.shadowRadiuson each view. - Make the tag labels accessible by adding them as properties instead of anonymous subviews if you need to update text dynamically.
内容的提问来源于stack exchange,提问作者Fabian Gr

