字符串包含'\ n'

问题描述

我在应用程序内部开发了Whatsapp样式功能。从JSON解析消息,然后在UITableView内部将消息创建为文本(+可选图像)消息(每个消息是一个自定义单元格)。 基于使用boundingRect方法计算文本框架,使用Bezier路径绘制消息气泡。后来,UILabel和UIImage被添加为UIStackview的子视图,并且StackView和消息气泡视图都被限制在容器视图中。

有时文本包含'\ n'时,UILabel要么被剪切(带有'...'),要么在消息气泡视图下方向下流动,具体取决于堆栈视图的底部锚点优先级(高于或低于UILabel的锚定优先级)包含优先级的内容),但其他包含换行符的消息会正确显示。我的猜测是,字符串的框架计算会将'\ n'视为2个字符而不是换行符。

当我尝试在操场上测试相同的代码(布局更简单,只有UILabel和气泡视图,没有容器视图,没有表格视图,没有约束)时,一切似乎都可以正常工作,气泡会自行扩展以适应添加了换行符。

基于this thread,我尝试用sizeThatFits方法替换代码,结果仍然相同。最终,我最终数出了字符串中'\ n'的出现并手动增加了框架的高度,但是它同时影响了坏消息和好消息,目前它们周围有多余的空间。

以下是屏幕截图,相关代码和控制台日志。希望它将帮助某人弄清楚这一点。

编辑:将messageView的宽度从UIScreen.main.bounds.width * 0.73更改为UIScreen.main.bounds.width * 0.8可解决此问题。但是我仍然不知道为什么它只影响特定的消息。感谢您提供有关此方面的更多信息。

ChatMessageModel.swift

fileprivate func setText(_ label: ClickableUILabel,_ text: String,_ shouldLimitSize: Bool,_  shouldOpenLinks: Bool) {
    
    ...

    // set text frame
    let textFrameHeight: CGFloat = shouldLimitSize ? 40.0 : .greatestFiniteMagnitude
    
    let constraintRect = CGSize(width: innerContentWidth,height: textFrameHeight)
    
    let boundingBox = text.boundingRect(with: constraintRect,options: .usesLineFragmentOrigin,attributes: [.font: label.font!],context: nil)
    
    // width must have minimum value for short text to appear centered
    let widthCeil = ceil(boundingBox.width)
    let constraintWidthWithInset = constraintRect.width - 30
    
    var height: CGFloat
    
    if text.isEmpty {
        height = 0
    } else {
        // min value of 40
        height = max(ceil(boundingBox.height),40) + 5
    }
  
    // ***** This part fixes bad messages but messes up good messages ****

    // add extra height for newLine inside text
    if let newLineCount = label.text?.countInstances(of: "\n"),newLineCount > 0 {
        LOG("found \n")
        height += CGFloat((newLineCount * 8))
    }

    label.frame.size = CGSize(width:max(widthCeil,constraintWidthWithInset),height: height)
    label.setContentHuggingPriority(UILayoutPriority(200),for: .horizontal)
}
fileprivate func setTextBubble(_ label: UILabel,_ image: String?,_ video: String?,_ shouldLimitSize: Bool) -> CustomRoundedCornerRectangle  {
        
        // configure bubble size
        
        var contentHeight = CGFloat()
        
        if imageDistribution! == .alongsideText {
            contentHeight = max(label.frame.height,contentImageView.frame.height)
        } else {
            contentHeight = label.frame.height + contentImageView.frame.height + 20
        }
        
        // messages with no text on main feed should have smaller width
        let width: CGFloat = shouldLimitSize && (label.text ?? "").isEmpty ? 150.0 : UIScreen.main.bounds.width * 0.73
        let bubbleFrame = CGRect(x: 0,y: 0,width: width,height: contentHeight + 20)
        
        let messageView = CustomRoundedCornerRectangle(frame: bubbleFrame)
        messageView.heightAnchor.constraint(equalToConstant: bubbleFrame.size.height).isActive = true
        messageView.widthAnchor.constraint(equalToConstant: bubbleFrame.size.width).isActive = true
        messageView.translatesAutoresizingMaskIntoConstraints = false
        
        self.messageViewFrame = bubbleFrame
        
        return messageView
    }
fileprivate func layoutSubviews(_ containerView: UIView,_ messageView: CustomRoundedCornerRectangle,_ timeLabel: UILabel,_ profileImageView: UIImageView,_ profileName: UILabel,_ label: UILabel,_ contentImageView: CustomImageView,_ imagePlacement: imagePlacement) {
        
        // container view
        containerView.addSubview(messageView)
        containerView.translatesAutoresizingMaskIntoConstraints = false
        
        containerView.autoSetDimension(.width,toSize: UIScreen.main.bounds.width * 0.8)
        containerView.autoPinEdge(.bottom,to: .bottom,of: messageView)
        messageView.autoPinEdge(.top,to: .top,of: containerView,withOffset: 23)
        
        // time label
        containerView.addSubview(timeLabel)
        timeLabel.autoPinEdge(.bottom,of: messageView)
        timeLabel.autoPinEdge(.leading,to: .leading,withOffset: -2)
        
        // profile image
        containerView.addSubview(profileImageView)
        profileImageView.autoPinEdge(.trailing,to: .trailing,withOffset: 15)
        profileImageView.autoPinEdge(.top,withOffset: 30)
        messageView.autoPinEdge(.trailing,of: profileImageView,withOffset: 15)
        
        // profile name
        containerView.addSubview(profileName)
        profileName.autoAlignAxis(.horizontal,toSameAxisOf: timeLabel)
        profileName.autoPinEdge(.trailing,of: messageView,withOffset: -2)
        
        if isSameAuthor {
            profileName.isHidden = true
            profileImageView.isHidden = true
        }
        
        // content stack view
        let contenStackView = UIStackView(forAutoLayout: ())
        messageView.addSubview(contenStackView)
        
        if imageDistribution! == .alongsideText {
            contenStackView.axis = NSLayoutConstraint.Axis.horizontal
            contenStackView.alignment = UIStackView.Alignment.center
        } else {
            contenStackView.axis = NSLayoutConstraint.Axis.vertical
            contenStackView.alignment = UIStackView.Alignment.trailing
        }
        
        contenStackView.spacing = 5.0
        contenStackView.autoPinEdge(.leading,withOffset: 15)
        contenStackView.autoPinEdge(.trailing,withOffset: -40)
        contenStackView.autoPinEdge(.top,withOffset: 10)

        let bottomConstraint = contenStackView.bottomAnchor.constraint(equalTo: messageView.bottomAnchor,constant: -10)
        bottomConstraint.priority = UILayoutPriority(800)
        bottomConstraint.isActive = true
        
        
        //Add Chat image and Message
        contenStackView.addArrangedSubview(contentImageView)
        if imagePlacement == .alongsideText || !label.text!.isEmpty { // do not insert empty labels if above text
            contenStackView.addArrangedSubview(label)
        }
    }

CustromRoundedCorenerRectangle.swift

class CustomRoundedCornerRectangle: UIView {
    lazy var shapeLayer = CAShapeLayer()
    var frameToUse: CGRect?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup(frame: CGRect(x: 0,width: 300,height: 100))
    }
    
    func setup(frame: CGRect) {
        
        // keep frame for later use
        frameToUse = frame
        
        // create CAShapeLayer
                
        // apply other properties related to the path
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 1.0
        shapeLayer.strokeColor = UIColor(red: 212/255,green: 212/255,blue: 212/255,alpha: 1.0).cgColor
        shapeLayer.position = CGPoint(x: 0,y: 0)
        
        // add the new layer to our custom view
        self.layer.addSublayer(shapeLayer)
    }
    
    func updateBezierPath(frame: CGRect) {
        let path = UIBezierPath()
        let largeCornerRadius: CGFloat = 18
        let smallCornerRadius: CGFloat = 10
        let upperCornerSpacerRadius: CGFloat = 2
        let imageToArcSpace: CGFloat = 5
        var rect = frame
        
        // bezier frame is smaller than messageView frame
        rect.size.width -= 20
        
        // move to starting point
        path.move(to: CGPoint(x: rect.minX + smallCornerRadius,y: rect.maxY))
       
        // draw bottom left corner
        path.addArc(withCenter: CGPoint(x: rect.minX + smallCornerRadius,y: rect.maxY - smallCornerRadius),radius: smallCornerRadius,startAngle: .pi / 2,// straight down
                    endAngle: .pi,// straight left
                    clockwise: true)
        
        // draw left line
        path.addLine(to: CGPoint(x: rect.minX,y: rect.minY + smallCornerRadius))
        
        // draw top left corner
        path.addArc(withCenter: CGPoint(x: rect.minX + smallCornerRadius,y: rect.minY + smallCornerRadius),startAngle: .pi,// straight left
            endAngle: .pi / 2 * 3,// straight up
            clockwise: true)
        
        // draw top line
        path.addLine(to: CGPoint(x: rect.maxX - largeCornerRadius,y: rect.minY))
        
        // draw concave top right corner
        // first arc
        path.addArc(withCenter: CGPoint(x: rect.maxX + largeCornerRadius,y: rect.minY + upperCornerSpacerRadius),radius: upperCornerSpacerRadius,startAngle: .pi / 2 * 3,// straight up
            endAngle: .pi / 2,// straight left
            clockwise: true)
        
        // second arc
        path.addArc(withCenter: CGPoint(x: rect.maxX + largeCornerRadius + imageToArcSpace,y: rect.minY + largeCornerRadius + upperCornerSpacerRadius * 2 + imageToArcSpace),radius: largeCornerRadius + imageToArcSpace,startAngle: CGFloat(240.0).toRadians(),// up with offset
            endAngle: .pi,// straight left
            clockwise: false)
        
        // draw right line
        path.addLine(to: CGPoint(x: rect.maxX,y: rect.maxY - smallCornerRadius))
        
        // draw bottom right corner
        path.addArc(withCenter: CGPoint(x: rect.maxX - smallCornerRadius,startAngle: 0,// straight right
            endAngle: .pi / 2,// straight down
                    clockwise: true)
        
        // draw bottom line to close the shape
        path.close()
        
        shapeLayer.path = path.cgPath
    }
}

extension CGFloat {
    func toRadians() -> CGFloat {
        return self * CGFloat(Double.pi) / 180.0
    }
}

CustomChatTableViewCell.swift

class ChatMessageCell: UITableViewCell {
    
    let horizontalInset: CGFloat = 30.0
    let bottomInset: CGFloat = 10.0
    var topInset: CGFloat = 5.0
    
    var didSetupConstraints = false
 
    var messageObject: ChatMessageModel?
    weak var delegate: Notify?
    
    
    override init(style: UITableViewCell.CellStyle,reuseIdentifier: String?) {
        super.init(style: style,reuseIdentifier: reuseIdentifier)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    // what we will call from our tableview method
    func configure(with item: ChatItem?,previousItem: ChatItem?,delegate: Notify?) {
 
        if let safeItem = item {

            messageObject = ChatMessageModel().createMessage(chatItem: safeItem,previousItem: previousItem,shouldLimitSize: false,shouldAddMediaTap: true,imagePlacement: .aboveText,shouldOpenLinks: true)
            messageObject?.delegate = delegate

            let messageContainerView = messageObject?.containerView
            contentView.addSubview(messageContainerView!)
            contentView.backgroundColor = .clear
            backgroundColor = .clear
            selectionStyle = .none
            
            // pin together messages from same author
            if safeItem.user?.name == previousItem?.user?.name {
                topInset = -10.0
            } else {
                topInset = 5.0
            }
            
            messageContainerView?.autoPinEdge(toSuperviewEdge: .top,withInset: topInset)
            messageContainerView?.autoAlignAxis(.vertical,toSameAxisOf: contentView,withOffset: 0)
            messageContainerView?.autoPinEdge(toSuperviewEdge: .bottom,withInset: bottomInset)
        }
    }
    
    override func prepareForReuse() {
        messageObject?.containerView.removeFromSuperview()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // redraw message background        
        messageObject?.messageView?.updateBezierPath(frame: (messageObject!.messageView!.frameToUse!))
    }
}

已删除邮件的日志:

(
"<NSLayoutConstraint:0x600000294960 Sport5.CustomRoundedCornerRectangle:0x7f9af3c9e990.height == 89   (active)>","<NSLayoutConstraint:0x6000002dc8c0 V:[Sport5.CustomRoundedCornerRectangle:0x7f9af3c9e990]-(0)-|   (active,names: '|':UIView:0x7f9af3ce99a0 )>","<NSLayoutConstraint:0x6000002ddef0 V:|-(23)-[Sport5.CustomRoundedCornerRectangle:0x7f9af3c9e990]   (active,"<NSLayoutConstraint:0x600000237890 V:|-(-10)-[UIView:0x7f9af3ce99a0]   (active,names: '|':UITableViewCellContentView:0x7f9af3cdd730 )>","<NSLayoutConstraint:0x600000237610 UIView:0x7f9af3ce99a0.bottom == UITableViewCellContentView:0x7f9af3cdd730.bottom - 10   (active)>","<NSLayoutConstraint:0x600000203ca0 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7f9af3cdd730.height == 108   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000294960 Sport5.CustomRoundedCornerRectangle:0x7f9af3c9e990.height == 89   (active)>

以换行符显示的消息的日志显示正常(存在宽度问题,但我认为这与此问题无关)

(
"<NSLayoutConstraint:0x600003de94a0 Sport5.CustomImageView:0x7fc7fd4c0540.width == 273.24   (active)>","<NSLayoutConstraint:0x600003deaf80 Sport5.CustomRoundedCornerRectangle:0x7fc7fd4e2730.width == 302.22   (active)>","<NSLayoutConstraint:0x600003d3fde0 H:|-(15)-[UIStackView:0x7fc7ff2d8430]   (active,names: '|':Sport5.CustomRoundedCornerRectangle:0x7fc7fd4e2730 )>","<NSLayoutConstraint:0x600003d3fe30 UIStackView:0x7fc7ff2d8430.trailing == Sport5.CustomRoundedCornerRectangle:0x7fc7fd4e2730.trailing - 40   (active)>","<NSLayoutConstraint:0x600003de9d10 'UISV-canvas-connection' UIStackView:0x7fc7ff2d8430.leading == _UILayoutSpacer:0x60000219f660'UISV-alignment-spanner'.leading   (active)>","<NSLayoutConstraint:0x600003deba20 'UISV-canvas-connection' H:[Sport5.CustomImageView:0x7fc7fd4c0540]-(0)-|   (active,names: '|':UIStackView:0x7fc7ff2d8430 )>","<NSLayoutConstraint:0x600003dea8f0 'UISV-spanning-boundary' _UILayoutSpacer:0x60000219f660'UISV-alignment-spanner'.leading <= Sport5.CustomImageView:0x7fc7fd4c0540.leading   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600003de94a0 Sport5.CustomImageView:0x7fc7fd4c0540.width == 273.24   (active)>

减少消息

cut down message

好消息,现在有更多空间

good message,now with extra space

错误消息标签约束

bad message label constraints

错误的消息堆栈约束

bad message stack constraints

好的消息标签约束

good message label constraints

良好的消息堆栈约束

good message stack constraints

解决方法

我认为,如果让自动布局处理所有大小调整,效果会更好。无需依靠计算文本边界框大小。

以下是带有一些示例数据的示例:

enter image description here

,然后滚动查看一些没有内容图像的消息:

enter image description here

我使用的代码:


样本结构和数据

struct MyMessageStruct {
    var time: String = " "
    var name: String = " "
    var profileImageName: String = ""
    var contentImageName: String = ""
    var message: String = " "
}

class SampleData: NSObject {
    let sampleStrings: [String] = [
        "First message with short text.","Second message with longer text that should cause word wrapping in this cell.","Third message with some embedded newlines.\nThis line comes after a newline (\"\\n\"),so we can see if that works the way we want.","Message without content image.","Longer Message without content image.\n\nWith a pair of embedded newline (\"\\n\") characters giving us a \"blank line\" in the message text.","The sixth message,also without a content image."
    ]
    
    lazy var sampleData: [MyMessageStruct] = [
        MyMessageStruct(time: "08:36",name: "Bob",profileImageName: "pro1",contentImageName: "content1",message: sampleStrings[0]),MyMessageStruct(time: "08:47",contentImageName: "content2",message: sampleStrings[1]),MyMessageStruct(time: "08:59",name: "Joe",profileImageName: "pro2",contentImageName: "content3",message: sampleStrings[2]),MyMessageStruct(time: "09:06",name: "Steve",profileImageName: "pro3",contentImageName:         "",message: sampleStrings[3]),MyMessageStruct(time: "09:21",message: sampleStrings[4]),MyMessageStruct(time: "09:45",message: sampleStrings[5]),]
}

表视图控制器

class ChatTableViewController: UITableViewController {
    
    var myData: [MyMessageStruct] = SampleData().sampleData
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // register the cell
        tableView.register(ChatMessageCell.self,forCellReuseIdentifier: "chatCell")
        
        tableView.separatorStyle = .none
        tableView.backgroundView = GrayGradientView()
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    override func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    override func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "chatCell",for: indexPath) as! ChatMessageCell
        
        // don't show the profile image if this message is from the same person
        //  as the previous message
        var isSameAuthor = false
        if indexPath.row > 0 {
            if myData[indexPath.row].name == myData[indexPath.row - 1].name {
                isSameAuthor = true
            }
        }
        
        cell.fillData(myData[indexPath.row],isSameAuthor: isSameAuthor)
        
        return cell
    }
    
}

单元格类

您可能需要调整间距,但是解释布局的注释应使更改位置清晰明了。

class ChatMessageCell: UITableViewCell {
    
    let timeLabel = UILabel()
    let nameLabel = UILabel()
    let profileImageView = RoundImageView()
    let bubbleView = CustomRoundedCornerRectangle()
    let stackView = UIStackView()
    let contentImageView = UIImageView()
    let messageLabel = UILabel()
    
    var contentImageHeightConstraint: NSLayoutConstraint!
    
    override init(style: UITableViewCell.CellStyle,reuseIdentifier: String?) {
        super.init(style: style,reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        
        [timeLabel,nameLabel,profileImageView,bubbleView,stackView,contentImageView,messageLabel].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        // MARK: add cell elements
        
        contentView.addSubview(timeLabel)
        contentView.addSubview(nameLabel)
        contentView.addSubview(profileImageView)
        contentView.addSubview(bubbleView)

        bubbleView.addSubview(stackView)
        
        stackView.addArrangedSubview(contentImageView)
        stackView.addArrangedSubview(messageLabel)

        // MARK: cell element constraints
        
        // make constraints relative to the default cell margins
        let g = contentView.layoutMarginsGuide
        
        NSLayoutConstraint.activate([
            
            // timeLabel Top: 0 / Leading: 20
            timeLabel.topAnchor.constraint(equalTo: g.topAnchor,constant: 0.0),timeLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor,constant: 20.0),// nameLabel Top: 0 / Trailing: 30
            nameLabel.topAnchor.constraint(equalTo: g.topAnchor,nameLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor,constant: -30.0),// profile image
            //  Top: bubbleView.top + 6
            profileImageView.topAnchor.constraint(equalTo: bubbleView.topAnchor,constant: 6.0),//  Trailing: 0 (to contentView margin)
            profileImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor,//  Width: 50 / Height: 1:1 (to keep it square / round)
            profileImageView.widthAnchor.constraint(equalToConstant: 50.0),profileImageView.heightAnchor.constraint(equalTo: profileImageView.widthAnchor),// bubbleView
            //  Top: timeLabel.bottom + 4
            bubbleView.topAnchor.constraint(equalTo: timeLabel.bottomAnchor,constant: 4.0),//  Leading: timeLabel.leading + 16
            bubbleView.leadingAnchor.constraint(equalTo: timeLabel.leadingAnchor,constant: 16.0),//  Trailing: profile image.leading - 4
            bubbleView.trailingAnchor.constraint(equalTo: profileImageView.leadingAnchor,constant: -4.0),//  Bottom: contentView.bottom
            bubbleView.bottomAnchor.constraint(equalTo: g.bottomAnchor,// stackView (to bubbleView)
            //  Top / Bottom: 12
            stackView.topAnchor.constraint(equalTo: bubbleView.topAnchor,constant: 12.0),stackView.bottomAnchor.constraint(equalTo: bubbleView.bottomAnchor,constant: -12.0),//  Leading / Trailing: 16
            stackView.leadingAnchor.constraint(equalTo: bubbleView.leadingAnchor,stackView.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor,constant: -16.0),])
        
        // contentImageView height ratio - will be changed based on the loaded image
        // we need to set its Priority to less-than Required or we get auto-layout warnings when the cell is reused
        contentImageHeightConstraint = contentImageView.heightAnchor.constraint(equalTo: contentImageView.widthAnchor,multiplier: 2.0 / 3.0)
        contentImageHeightConstraint.priority = .defaultHigh
        contentImageHeightConstraint.isActive = true

        // messageLabel minimum Height: 40
        // we need to set its Priority to less-than Required or we get auto-layout warnings when the cell is reused
        let c = messageLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0)
        c.priority = .defaultHigh
        c.isActive = true
        
        // MARK: element properties
        
        stackView.axis = .vertical
        stackView.spacing = 6
        
        // set label fonts and alignment here
        timeLabel.font = UIFont.systemFont(ofSize: 14,weight: .regular)
        nameLabel.font = UIFont.systemFont(ofSize: 14,weight: .bold)
        timeLabel.textColor = .gray
        nameLabel.textColor = UIColor(red: 0.175,green: 0.36,blue: 0.72,alpha: 1.0)
        
        // for now,I'm just setting the message label to right-aligned
        //  likely using RTL
        messageLabel.textAlignment = .right
        
        messageLabel.numberOfLines = 0
        
        contentImageView.backgroundColor = .blue
        contentImageView.contentMode = .scaleAspectFit
        contentImageView.layer.cornerRadius = 8
        contentImageView.layer.masksToBounds = true
        
        profileImageView.contentMode = .scaleToFill
        
        // MARK: cell background
        backgroundColor = .clear
        contentView.backgroundColor = .clear
    }
    
    func fillData(_ msg: MyMessageStruct,isSameAuthor: Bool) -> Void {
        timeLabel.text = msg.time
        nameLabel.text = msg.name
        
        nameLabel.isHidden = isSameAuthor
        profileImageView.isHidden = isSameAuthor
        
        if !isSameAuthor {
            if !msg.profileImageName.isEmpty {
                if let img = UIImage(named: msg.profileImageName) {
                    profileImageView.image = img
                }
            }
        }
        if !msg.contentImageName.isEmpty {
            contentImageView.isHidden = false
            if let img = UIImage(named: msg.contentImageName) {
                contentImageView.image = img
                let ratio = img.size.height / img.size.width
                contentImageHeightConstraint.isActive = false
                contentImageHeightConstraint = contentImageView.heightAnchor.constraint(equalTo: contentImageView.widthAnchor,multiplier: ratio)
                contentImageHeightConstraint.priority = .defaultHigh
                contentImageHeightConstraint.isActive = true
            }
        } else {
            contentImageView.isHidden = true
        }
        messageLabel.text = msg.message
    }
}

其他课程

对于“聊天气泡视图”,“圆角图像视图”和“渐变背景视图”

class CustomRoundedCornerRectangle: UIView {
    lazy var shapeLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }
    
    func setup() {
        // apply properties related to the path
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 1.0
        shapeLayer.strokeColor = UIColor(red: 212/255,green: 212/255,blue: 212/255,alpha: 1.0).cgColor
        shapeLayer.position = CGPoint(x: 0,y: 0)
        
        // add the new layer to our custom view
        //self.layer.addSublayer(shapeLayer)
        self.layer.insertSublayer(shapeLayer,at: 0)
    }
    
    override func layoutSubviews() {
        
        let path = UIBezierPath()
        let largeCornerRadius: CGFloat = 18
        let smallCornerRadius: CGFloat = 10
        let upperCornerSpacerRadius: CGFloat = 2
        let imageToArcSpace: CGFloat = 5
        let rect = bounds
        
        // move to starting point
        path.move(to: CGPoint(x: rect.minX + smallCornerRadius,y: rect.maxY))
        
        // draw bottom left corner
        path.addArc(withCenter: CGPoint(x: rect.minX + smallCornerRadius,y: rect.maxY - smallCornerRadius),radius: smallCornerRadius,startAngle: .pi / 2,// straight down
                    endAngle: .pi,// straight left
                    clockwise: true)
        
        // draw left line
        path.addLine(to: CGPoint(x: rect.minX,y: rect.minY + smallCornerRadius))
        
        // draw top left corner
        path.addArc(withCenter: CGPoint(x: rect.minX + smallCornerRadius,y: rect.minY + smallCornerRadius),startAngle: .pi,// straight left
                    endAngle: .pi / 2 * 3,// straight up
                    clockwise: true)
        
        // draw top line
        path.addLine(to: CGPoint(x: rect.maxX - largeCornerRadius,y: rect.minY))
        
        // draw concave top right corner
        // first arc
        path.addArc(withCenter: CGPoint(x: rect.maxX + largeCornerRadius,y: rect.minY + upperCornerSpacerRadius),radius: upperCornerSpacerRadius,startAngle: .pi / 2 * 3,// straight up
                    endAngle: .pi / 2,// straight left
                    clockwise: true)
        
        // second arc
        path.addArc(withCenter: CGPoint(x: rect.maxX + largeCornerRadius + imageToArcSpace,y: rect.minY + largeCornerRadius + upperCornerSpacerRadius * 2 + imageToArcSpace),radius: largeCornerRadius + imageToArcSpace,startAngle: CGFloat(240.0).toRadians(),// up with offset
                    endAngle: .pi,// straight left
                    clockwise: false)
        
        // draw right line
        path.addLine(to: CGPoint(x: rect.maxX,y: rect.maxY - smallCornerRadius))
        
        // draw bottom right corner
        path.addArc(withCenter: CGPoint(x: rect.maxX - smallCornerRadius,startAngle: 0,// straight right
                    endAngle: .pi / 2,// straight down
                    clockwise: true)
        
        // draw bottom line to close the shape
        path.close()
        
        shapeLayer.path = path.cgPath
    }
}

extension CGFloat {
    func toRadians() -> CGFloat {
        return self * CGFloat(Double.pi) / 180.0
    }
}

class RoundImageView: UIImageView {
    override func layoutSubviews() {
        layer.masksToBounds = true
        layer.cornerRadius = bounds.size.height * 0.5
    }
}

class GrayGradientView: UIView {
    private var gradLayer: CAGradientLayer!
    
    override class var layerClass: AnyClass {
        return CAGradientLayer.self
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        
        let myColors: [UIColor] = [
            UIColor(white: 0.95,alpha: 1.0),UIColor(white: 0.90,]
        
        gradLayer = self.layer as? CAGradientLayer
        
        // assign the colors (we're using map to convert UIColors to CGColors
        gradLayer.colors = myColors.map({$0.cgColor})
        
        // start at the top
        gradLayer.startPoint = CGPoint(x: 0.25,y: 0.0)
        
        // end at the bottom
        gradLayer.endPoint = CGPoint(x: 0.75,y: 1.0)
        
    }
}

以及示例图片(点击查看完整尺寸):

content1.png content2.png content3.png

pro1.png pro2.png pro3.png

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...