Using superview in the class paints the entire screen but I only want to paint the smaller subview (whose dimensions are defined by v.frame).
How do I have the class operate only on the "current view"?
#IBOutlet var storyboardView: UIView!
func selectView() {
var v = UIView()
switch viewchoice {
case false:
v = View1class()
case true:
v = View2class()
}
v.frame = CGRect(x: 20, y: 50, width: storyboardView.frame.width - 40, height: storyboardView.frame.height - 100)
storyboardView.addSubview(v)
}
class View1class: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
superview!.backgroundColor = .red
}
}
class View2class: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
superview!.backgroundColor = .blue
}
}
I tried using self instead of superview but that doesn't work.
EDIT: If I wanted to paint the entire screen, could I skip creating the subview and assign the class to storyboardView?
If you override draw(_ rect: CGRect) in a subclass, it is up to you to draw the view content. Setting .backgroundColor there will have no effect.
This is a common structure for a UIView subclass:
class View1Class: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.backgroundColor = .red
// do any other view setup here, such as
// adding subviews, adding sublayers, etc
}
}
Edit
Here is an example of two custom views - the first one uses shape layers, the second one overrides draw():
MyFirstView class
class MyFirstView: UIView {
let boxLayer = CAShapeLayer()
let circleLayer = CAShapeLayer()
let lineLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.backgroundColor = .systemYellow
layer.addSublayer(boxLayer)
layer.addSublayer(circleLayer)
layer.addSublayer(lineLayer)
boxLayer.fillColor = UIColor.red.cgColor
circleLayer.fillColor = UIColor.blue.cgColor
lineLayer.strokeColor = UIColor.green.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
// centered rectangle, half height and width
let halfWidth: CGFloat = bounds.width * 0.5
let halfHeight: CGFloat = bounds.height * 0.5
let boxRect: CGRect = CGRect(x: halfWidth * 0.5, y: halfHeight * 0.5, width: halfWidth, height: halfHeight)
let boxPath: UIBezierPath = UIBezierPath(rect: boxRect)
boxLayer.path = boxPath.cgPath
// circle centered in box, 3/4ths of the shorter of width or height
let wh: CGFloat = min(boxRect.width, boxRect.height) * 0.75
let circleRect: CGRect = CGRect(x: boxRect.midX - wh * 0.5, y: boxRect.midY - wh * 0.5, width: wh, height: wh)
let circlePath: UIBezierPath = UIBezierPath(ovalIn: circleRect)
circleLayer.path = circlePath.cgPath
// diagonal line from top-left to bottom-right
let linePath: UIBezierPath = UIBezierPath()
linePath.move(to: .zero)
linePath.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
lineLayer.path = linePath.cgPath
}
}
MySecondView class
class MySecondView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.backgroundColor = .systemTeal
}
override func draw(_ rect: CGRect) {
// centered rectangle, half height and width
let halfWidth: CGFloat = rect.width * 0.5
let halfHeight: CGFloat = rect.height * 0.5
let boxRect: CGRect = CGRect(x: halfWidth * 0.5, y: halfHeight * 0.5, width: halfWidth, height: halfHeight)
let boxPath: UIBezierPath = UIBezierPath(rect: boxRect)
UIColor.red.setFill()
boxPath.fill()
// circle centered in box, 3/4ths of the shorter of width or height
let wh: CGFloat = min(boxRect.width, boxRect.height) * 0.75
let circleRect: CGRect = CGRect(x: boxRect.midX - wh * 0.5, y: boxRect.midY - wh * 0.5, width: wh, height: wh)
let circlePath: UIBezierPath = UIBezierPath(ovalIn: circleRect)
UIColor.blue.setFill()
circlePath.fill()
// diagonal line from top-left to bottom-right
let linePath: UIBezierPath = UIBezierPath()
linePath.move(to: .zero)
linePath.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
UIColor.green.setStroke()
linePath.stroke()
}
override func layoutSubviews() {
setNeedsDisplay()
}
}
Example view controller
class CustomViewsViewController: UIViewController {
let firstView = MyFirstView()
let secondView = MySecondView()
override func viewDidLoad() {
super.viewDidLoad()
[firstView, secondView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// firstView Top / Leading / Trailing to view (safe-area)
firstView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
firstView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
firstView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// firstView Height
firstView.heightAnchor.constraint(equalToConstant: 150.0),
// secondView Leading / Trailing to view (safe-area)
secondView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
secondView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
// secondView Top 20-pts from firstView Bottom
secondView.topAnchor.constraint(equalTo: firstView.bottomAnchor, constant: 20.0),
// secondView Bottom to View Bottom (safe-area)
secondView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
}
}
Output - first view has .systemYellow background, second view has .systemTeal background:
and rotated:
Use self instead of superView.
self.backgroundColor = .red
I develop a Whatsapp style feature inside our app. messages are parsed from JSON and then created as text (+ optional image) messages inside a UITableView (each message is a custom cell).
The message bubble is drawn using Bezier Path, based on the calculation of the text frame using the boundingRect method. Later the UILabel and UIImage are added as subviews of a UIStackview, and both the StackView and the message bubble view are constrained to a container view.
Sometimes when the text contains '\n' the UILabel is either getting cut (with '...') or flows down below the message bubble view, depending on the stack view's bottom anchor priority (higher/lower than the UILabel's content hugging priority), but other messages that contain newlines appear correctly. My guess is that the string's frame calculation treats the '\n' as 2 characters instead of a newline.
When I tried testing the same code in a playground (with a simpler layout, just UILabel and bubble view, no container views, no tableview and no constraints) everything seemed to work fine and the bubble would expand itself to adapt to the added newlines.
Based on this thread I tried replacing the code with the sizeThatFits Method, still the same result. Eventually, I ended up counting the occurrences of '\n' inside the string and manually adding height to the frame, but it affects both the bad messages and the good messages, which by now has extra space surrounding them.
Screenshots, relevant code and console logs are attached below. Hopefully, it will help someone to figure this out.
Edit: changing the width of messageView from UIScreen.main.bounds.width * 0.73 to UIScreen.main.bounds.width * 0.8 fixed the issue. However I still can't figure out why it affected only specific messages. I'll be grateful for any further information regarding this.
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, to: .top, of: messageView)
timeLabel.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: -2)
// profile image
containerView.addSubview(profileImageView)
profileImageView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: 15)
profileImageView.autoPinEdge(.top, to: .top, of: containerView, withOffset: 30)
messageView.autoPinEdge(.trailing, to: .leading, of: profileImageView, withOffset: 15)
// profile name
containerView.addSubview(profileName)
profileName.autoAlignAxis(.horizontal, toSameAxisOf: timeLabel)
profileName.autoPinEdge(.trailing, to: .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, to: .leading, of: messageView, withOffset: 15)
contenStackView.autoPinEdge(.trailing, to: .trailing, of: messageView, withOffset: -40)
contenStackView.autoPinEdge(.top, to: .top, of: messageView, 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, y: 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), radius: 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, y: rect.maxY - smallCornerRadius), radius: 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!))
}
}
log of cut down message:
(
"<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, names: '|':UIView:0x7f9af3ce99a0 )>",
"<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)>
log of a message with newline that was displayed ok (there is a width issue, but I don't think it has something to do with this issue)
(
"<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
I think you'll find it works much better if you let auto-layout handle all of the sizing. No need to rely calculating text bounding box sizes.
Here is an example with some sample data:
and, after scrolling to see some messages without content images:
The code I used:
Sample Struct and Data
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", name: "Bob", profileImageName: "pro1", 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", name: "Bob", profileImageName: "pro1", contentImageName: "", message: sampleStrings[4]),
MyMessageStruct(time: "09:45", name: "Joe", profileImageName: "pro2", contentImageName: "", message: sampleStrings[5]),
]
}
Table View Controller
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
}
}
Cell Class
You'll probably want to tweak the spacing, but the comments explaining the layout should make it clear where to make changes.
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, constant: 0.0),
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, constant: 0.0),
// 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, constant: 0.0),
// 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, constant: 16.0),
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
}
}
Additional Classes
For the "chat bubble view," "rounded corners image view," and "gradient background view"
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), radius: 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, y: rect.maxY - smallCornerRadius), radius: 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, alpha: 1.0),
]
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)
}
}
And sample images (click for full sizes):
content1.png content2.png content3.png
pro1.png pro2.png pro3.png
I am not looking out for code snippet. I am just curious to know how to develop a UI component which is shown below. I have multiple thoughts on creating it, but would love to know the best approach
Create a label and do some operations on its layer
Set background image and add text on label
Set image which has text on it
What will be the good approach to develop it. Any suggestions please.
You want to display a single line of text. You can use a UILabel for that.
You have a shape you want to fill. You can use a CAShapeLayer for that. It's best to wrap the shape layer in a UIView subclass so that UIKit can lay it out properly.
You want to put the text over the shape, so use a parent view to combine the label and the shape as subviews.
import UIKit
class TagView: UIView {
init() {
super.init(frame: .zero)
addSubview(background)
addSubview(label)
background.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
label.setContentHuggingPriority(.defaultHigh + 10, for: .horizontal)
label.setContentHuggingPriority(.defaultHigh + 10, for: .vertical)
NSLayoutConstraint.activate([
background.heightAnchor.constraint(equalTo: label.heightAnchor, constant: 4),
background.centerYAnchor.constraint(equalTo: label.centerYAnchor),
background.widthAnchor.constraint(equalTo: label.widthAnchor, constant: 20),
background.centerXAnchor.constraint(equalTo: label.centerXAnchor, constant: -2),
leadingAnchor.constraint(equalTo: background.leadingAnchor),
trailingAnchor.constraint(equalTo: background.trailingAnchor),
topAnchor.constraint(equalTo: background.topAnchor),
bottomAnchor.constraint(equalTo: background.bottomAnchor),
])
}
let label = UILabel()
private let background = BackgroundView()
private class BackgroundView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
override func layoutSubviews() {
super.layoutSubviews()
layoutShape()
}
private func layoutShape() {
let layer = self.layer as! CAShapeLayer
layer.fillColor = #colorLiteral(red: 0.731529057, green: 0.8821037412, blue: 0.9403864741, alpha: 1)
layer.strokeColor = nil
let bounds = self.bounds
let h2 = bounds.height / 2
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: h2))
path.addLine(to: CGPoint(x: h2, y: 0))
path.addLine(to: CGPoint(x: bounds.maxX - h2, y: 0))
path.addArc(withCenter: CGPoint(x: bounds.maxX - h2, y: h2), radius: h2, startAngle: -.pi/2, endAngle: .pi/2, clockwise: true)
path.addLine(to: CGPoint(x: h2, y: bounds.maxY))
path.close()
layer.path = path.cgPath
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
import PlaygroundSupport
let root = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
root.backgroundColor = .white
PlaygroundPage.current.liveView = root
let tag = TagView()
tag.translatesAutoresizingMaskIntoConstraints = false
tag.label.text = "CURRENT"
root.addSubview(tag)
NSLayoutConstraint.activate([
tag.centerXAnchor.constraint(equalTo: root.centerXAnchor),
tag.centerYAnchor.constraint(equalTo: root.centerYAnchor),
])
Result:
How to draw about three circle in horizontally area with main and ring color in rectangle. I need to create custom button with this circles, something like this:
Is there any good way to do this?
We can design such kind of views with UIStackView in very ease manner.
Take a stackView, set its alignment to center, axis to horizontal and distribution to fill. Create a UILabel/UIButton/UIImageView or even UIView and add rounded radius and border to it. Finally, add those views to the main stackView.
Try this.
override func viewDidLoad() {
super.viewDidLoad()
//Setup stackView
let myStackView = UIStackView()
myStackView.axis = .horizontal
myStackView.alignment = .center
myStackView.distribution = .fillEqually
myStackView.spacing = 8
view.addSubview(myStackView)
//Setup circles
let circle_1 = circleLabel()
let circle_2 = circleLabel()
let circle_3 = circleLabel()
myStackView.addArrangedSubview(circle_1)
myStackView.addArrangedSubview(circle_2)
myStackView.addArrangedSubview(circle_3)
myStackView.translatesAutoresizingMaskIntoConstraints = false
myStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0).isActive = true
myStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0).isActive = true
}
func circleLabel() -> UILabel {
let label = UILabel()
label.backgroundColor = UIColor.red
label.layer.cornerRadius = 12.5
label.layer.masksToBounds = true
label.layer.borderColor = UIColor.orange.cgColor
label.layer.borderWidth = 3.0
label.widthAnchor.constraint(equalToConstant: 25.0).isActive = true
label.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
return label
}
To make a Single Circle like that, you need to make use of UIBezierPath and CAShapeLayer .
let outerCirclePath = UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: CGFloat(50), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let outerCircleShapeLayer = CAShapeLayer()
outerCircleShapeLayer.path = outerCirclePath.cgPath
outerCircleShapeLayer.fillColor = UIColor.white.cgColor
outerCircleShapeLayer.lineWidth = 3.0
view.layer.addSublayer(outerCircleShapeLayer)
// Drawing the inner circle
let innerCirclePath = UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: CGFloat(40), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let innerCircleShapeLayer = CAShapeLayer()
innerCircleShapeLayer.path = innerCirclePath.cgPath
innerCircleShapeLayer.fillColor = UIColor.blue.cgColor
view.layer.addSublayer(innerCircleShapeLayer)
I have attached an image below for the Playground version of it .
Just play around with arcCenter and radius values and you will get the desired output
My team helped me and here is solution to create this with dynamically changing state of circles (with different stroke and fill colors):
import UIKit
#IBDesignable
class CirclesButton: UIControl {
#IBInspectable
var firstCircle: Bool = false {
didSet {
setNeedsDisplay()
}
}
#IBInspectable
var secondCircle: Bool = false {
didSet {
setNeedsDisplay()
}
}
#IBInspectable
var thirdCircle: Bool = false {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
// get context
guard let context = UIGraphicsGetCurrentContext() else { return }
// make configurations
context.setLineWidth(1.0);
context.setStrokeColor(UIColor.white.cgColor)
context.setFillColor(red: 0.0, green: 0.58, blue: 1.0, alpha: 1.0)
// find view center
let dotSize:CGFloat = 11.0
let viewCenter = CGPoint(x: rect.midX, y: rect.midY)
// find personal dot rect
var dotRect = CGRect(x: viewCenter.x - dotSize / 2.0, y: viewCenter.y - dotSize / 2.0, width: dotSize, height: dotSize)
if secondCircle {
context.fillEllipse(in: dotRect)
}
context.strokeEllipse(in: dotRect)
// find global notes rect
dotRect = CGRect(x: viewCenter.x - dotSize * 1.5 - 4.0, y: viewCenter.y - dotSize / 2.0, width: dotSize, height: dotSize)
if firstCircle {
context.fillEllipse(in: dotRect)
}
context.strokeEllipse(in: dotRect)
// find music rect
dotRect = CGRect(x: viewCenter.x + dotSize / 2.0 + 4.0, y: viewCenter.y - dotSize / 2.0, width: dotSize, height: dotSize)
if thirdCircle {
context.setFillColor(red: 0.0, green: 1.0, blue: 0.04, alpha: 1.0)
context.fillEllipse(in: dotRect)
}
context.strokeEllipse(in: dotRect)
}
}
It will looks like: CirclesButton
Сode:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let buttonSize: CGFloat = 80
let firstButton = CustomButton(position: CGPoint(x: 0, y: 0), size: buttonSize, color: .blue)
self.view.addSubview(firstButton)
let secondButton = CustomButton(position: CGPoint(x: firstButton.frame.maxX, y: 0), size: buttonSize, color: .blue)
self.view.addSubview(secondButton)
let thirdButton = CustomButton(position: CGPoint(x: secondButton.frame.maxX, y: 0), size: buttonSize, color: .green)
self.view.addSubview(thirdButton)
}
}
class CustomButton: UIButton {
init(position: CGPoint, size: CGFloat, color: UIColor) {
super.init(frame: CGRect(x: position.x, y: position.y, width: size, height: size))
self.backgroundColor = color
self.layer.cornerRadius = size / 2
self.clipsToBounds = true
self.layer.borderWidth = 4.0 // make it what ever you want
self.layer.borderColor = UIColor.white.cgColor
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
You can handle button tapped like:
override func viewDidLoad() {
super.viewDidLoad()
firstButton.addTarget(self, action: #selector(handleFirstButton), for: .touchUpInside)
}
#objc func handleFirstButton(sender: UIButton) {
print("first button tapped")
}
Best and Universal Solution for **Button or Label creation (Fully Dynamic)**
var x = 10
var y = 5
var buttonHeight = 40
var buttonWidth = 40
for i in 0..<3 {
let roundButton = UIButton(frame: CGRect(x: x, y: y, width: buttonWidth, height: buttonHeight))
roundButton.setTitle("Butt\(i)", for: .normal)
roundButton.layer.cornerRadius = roundButton.bounds.size.height/2
yourButtonBackView.addSubview(roundButton)
x = x + buttonWidth + 10
if x >= Int(yourButtonBackView.frame.width - 30) {
y = y + buttonHeight + 10
x = 10
}
}
I'm trying to draw some dashed lines on an app but it only draws on main.storyboard with IBDesignable. When I run the app on iOS simulator, nothing shows. What's happening?
The code to draw:
#IBDesignable
class AnalogView: UIView {
fileprivate let thickHorizontalLayer = CAShapeLayer()
fileprivate let thinHorizontalLayer = CAShapeLayer()
#IBInspectable var thickYCoord = 50.0
#IBInspectable var thinYCoord = 52.5
override init(frame: CGRect) {
super.init(frame: frame)
let thickDashesPath = UIBezierPath()
thickDashesPath.move(to: CGPoint(x: 0, y: thickYCoord)) //left
thickDashesPath.addLine(to: CGPoint(x: 340, y: thickYCoord)) //right
//thickHorizontalLayer.frame = frame
thickHorizontalLayer.path = thickDashesPath.cgPath
thickHorizontalLayer.strokeColor = UIColor.black.cgColor //dashes color
thickHorizontalLayer.lineWidth = 20
thickHorizontalLayer.lineDashPattern = [ 1, 83.5 ]
//thickHorizontalLayer.lineDashPhase = 0.25
self.layer.addSublayer(thickHorizontalLayer)
let thinDashesPath = UIBezierPath()
thinDashesPath.move(to: CGPoint(x: 0, y: thinYCoord)) //esquerda
thinDashesPath.addLine(to: CGPoint(x: 340, y: thinYCoord)) //direita
//thinHorizontalLayer.frame = frame
thinHorizontalLayer.path = thinDashesPath.cgPath
thinHorizontalLayer.strokeColor = UIColor.black.cgColor
thinHorizontalLayer.lineWidth = 15.0
thinHorizontalLayer.fillColor = UIColor.clear.cgColor
thinHorizontalLayer.lineDashPattern = [ 0.5, 7.95]
//thinHorizontalLayer.lineDashPhase = 0.25
self.layer.addSublayer(thinHorizontalLayer)
You need to put the common code in a routine that is called by both init(frame:) and init(coder:):
#IBDesignable
class AnalogView: UIView {
fileprivate let thickHorizontalLayer = CAShapeLayer()
fileprivate let thinHorizontalLayer = CAShapeLayer()
#IBInspectable var thickYCoord: CGFloat = 50.0
#IBInspectable var thinYCoord: CGFloat = 52.5
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
let thickDashesPath = UIBezierPath()
thickDashesPath.move(to: CGPoint(x: 0, y: thickYCoord)) //left
thickDashesPath.addLine(to: CGPoint(x: 340, y: thickYCoord)) //right
thickHorizontalLayer.path = thickDashesPath.cgPath
thickHorizontalLayer.strokeColor = UIColor.black.cgColor //dashes color
thickHorizontalLayer.lineWidth = 20
thickHorizontalLayer.lineDashPattern = [ 1, 83.5 ]
self.layer.addSublayer(thickHorizontalLayer)
let thinDashesPath = UIBezierPath()
thinDashesPath.move(to: CGPoint(x: 0, y: thinYCoord)) //esquerda
thinDashesPath.addLine(to: CGPoint(x: 340, y: thinYCoord)) //direita
thinHorizontalLayer.path = thinDashesPath.cgPath
thinHorizontalLayer.strokeColor = UIColor.black.cgColor
thinHorizontalLayer.lineWidth = 15.0
thinHorizontalLayer.fillColor = UIColor.clear.cgColor
thinHorizontalLayer.lineDashPattern = [ 0.5, 7.95]
self.layer.addSublayer(thinHorizontalLayer)
}
}
I'd also suggest declaring an explicit type for your #IBInspectable types, or else you won't be able to adjust them in IB.
Personally, rather than hard coding the path width, I'd update it when the layout changes. Also, if you're going to make those properties #IBDesignable, you really want to update the paths if they change.
#IBDesignable
class AnalogView: UIView {
fileprivate let thickHorizontalLayer = CAShapeLayer()
fileprivate let thinHorizontalLayer = CAShapeLayer()
#IBInspectable var thickYCoord: CGFloat = 50.0 { didSet { updatePaths() } }
#IBInspectable var thinYCoord: CGFloat = 52.5 { didSet { updatePaths() } }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
layer.addSublayer(thickHorizontalLayer)
layer.addSublayer(thinHorizontalLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
}
private func updatePaths() {
let thickDashesPath = UIBezierPath()
thickDashesPath.move(to: CGPoint(x: bounds.origin.x, y: thickYCoord)) //left
thickDashesPath.addLine(to: CGPoint(x: bounds.origin.x + bounds.size.width, y: thickYCoord)) //right
thickHorizontalLayer.path = thickDashesPath.cgPath
thickHorizontalLayer.strokeColor = UIColor.black.cgColor //dashes color
thickHorizontalLayer.lineWidth = 20
thickHorizontalLayer.lineDashPattern = [1.0, NSNumber(value: Double(bounds.size.width - 1) / 4 - 1.0) ]
let thinDashesPath = UIBezierPath()
thinDashesPath.move(to: CGPoint(x: bounds.origin.x, y: thinYCoord)) //esquerda
thinDashesPath.addLine(to: CGPoint(x: bounds.origin.x + bounds.size.width, y: thinYCoord)) //direita
thinHorizontalLayer.path = thinDashesPath.cgPath
thinHorizontalLayer.strokeColor = UIColor.black.cgColor
thinHorizontalLayer.lineWidth = 15.0
thinHorizontalLayer.fillColor = UIColor.clear.cgColor
thinHorizontalLayer.lineDashPattern = [0.5, NSNumber(value: Double(bounds.size.width - 1) / 40 - 0.5)]
}
}
You might want to adjust the dashing to span the width, too (I'm not sure if you wanted a consistent scale or for it to span the width). But hopefully this illustrates the idea.