You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中负cornerRadius实现:如何设计带环绕标签的负圆角标签?

Swift: Button Wrapped with Negative Rounded Corner Tags

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 a UIBezierPath that skips the outward corner and instead draws an inward arc.
  • We use this path as a CAShapeLayer mask, 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 cornerRadius matches the tag's negativeCornerRadius for a seamless fit.
  • The stack view's spacing is 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 axis to .vertical and adjust the tags' negativeCorner values (e.g., top tag uses .bottomLeft and .bottomRight, bottom tag uses .topLeft and .topRight).
  • Add shadow effects to the tags and button for depth: set layer.shadowColor, layer.shadowOffset, layer.shadowOpacity, and layer.shadowRadius on 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 06:20:28