如何解决字符串包含'\ 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)>
错误的消息堆栈约束
好的消息标签约束
解决方法
我认为,如果让自动布局处理所有大小调整,效果会更好。无需依靠计算文本边界框大小。
以下是带有一些示例数据的示例:
,然后滚动查看一些没有内容图像的消息:
我使用的代码:
样本结构和数据
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
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。