View is getting replaced while Scrolling in UITableView - ios

I am an Android Developer and very new to iOS App Development. I am trying to build a simple chat system with only one line of data in each cell.
I am using a custom UIView class to generate a bubble and a UILabel and an UIImageView programmatically.
When I run the app for the first time, everything looks good. Please see the image below:
But, the problem occurs when I scroll the UITableView Upwards and I don't understand why the bubbles shift themselves towards the right side. See the image below:
Some post in SO said that it is one of the feature of UITableView.
How should I solve this?
If you need any code snippets from any part of the App, please comment below.
PS: I am using Xcode 9 and iOS 11.4 as testing device and Swift 4 programming.
Any help would be appreciated. Thanks.
Below is BubbleView Class:
EDIT
import UIKit
import Foundation
class BubbleView: UIView {
var incomingColor = UIColor(white: 0.9, alpha: 1)
var outgoingColor = UIColor(red: 0.09, green: 0.54, blue: 1, alpha: 1)
var isIncoming: Bool = false
init(isIncoming: Bool) {
self.isIncoming = isIncoming
super.init(frame: UIScreen.main.bounds);
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
}
override func draw(_ rect: CGRect) {
let width = rect.width
let height = rect.height
let bezierPath = UIBezierPath()
if self.isIncoming == true {
bezierPath.move(to: CGPoint(x: 22, y: height))
bezierPath.addLine(to: CGPoint(x: width - 17, y: height))
bezierPath.addCurve(to: CGPoint(x: width, y: height - 17), controlPoint1: CGPoint(x: width - 7.61, y: height), controlPoint2: CGPoint(x: width, y: height - 7.61))
bezierPath.addLine(to: CGPoint(x: width, y: 17))
bezierPath.addCurve(to: CGPoint(x: width - 17, y: 0), controlPoint1: CGPoint(x: width, y: 7.61), controlPoint2: CGPoint(x: width - 7.61, y: 0))
bezierPath.addLine(to: CGPoint(x: 21, y: 0))
bezierPath.addCurve(to: CGPoint(x: 4, y: 17), controlPoint1: CGPoint(x: 11.61, y: 0), controlPoint2: CGPoint(x: 4, y: 7.61))
bezierPath.addLine(to: CGPoint(x: 4, y: height - 11))
bezierPath.addCurve(to: CGPoint(x: 0, y: height), controlPoint1: CGPoint(x: 4, y: height - 1), controlPoint2: CGPoint(x: 0, y: height))
bezierPath.addLine(to: CGPoint(x: -0.05, y: height - 0.01))
bezierPath.addCurve(to: CGPoint(x: 11.04, y: height - 4.04), controlPoint1: CGPoint(x: 4.07, y: height + 0.43), controlPoint2: CGPoint(x: 8.16, y: height - 1.06))
bezierPath.addCurve(to: CGPoint(x: 22, y: height), controlPoint1: CGPoint(x: 16, y: height), controlPoint2: CGPoint(x: 19, y: height))
incomingColor.setFill()
} else {
bezierPath.move(to: CGPoint(x: width - 22, y: height))
bezierPath.addLine(to: CGPoint(x: 17, y: height))
bezierPath.addCurve(to: CGPoint(x: 0, y: height - 17), controlPoint1: CGPoint(x: 7.61, y: height), controlPoint2: CGPoint(x: 0, y: height - 7.61))
bezierPath.addLine(to: CGPoint(x: 0, y: 17))
bezierPath.addCurve(to: CGPoint(x: 17, y: 0), controlPoint1: CGPoint(x: 0, y: 7.61), controlPoint2: CGPoint(x: 7.61, y: 0))
bezierPath.addLine(to: CGPoint(x: width - 21, y: 0))
bezierPath.addCurve(to: CGPoint(x: width - 4, y: 17), controlPoint1: CGPoint(x: width - 11.61, y: 0), controlPoint2: CGPoint(x: width - 4, y: 7.61))
bezierPath.addLine(to: CGPoint(x: width - 4, y: height - 11))
bezierPath.addCurve(to: CGPoint(x: width, y: height), controlPoint1: CGPoint(x: width - 4, y: height - 1), controlPoint2: CGPoint(x: width, y: height))
bezierPath.addLine(to: CGPoint(x: width + 0.05, y: height - 0.01))
bezierPath.addCurve(to: CGPoint(x: width - 11.04, y: height - 4.04), controlPoint1: CGPoint(x: width - 4.07, y: height + 0.43), controlPoint2: CGPoint(x: width - 8.16, y: height - 1.06))
bezierPath.addCurve(to: CGPoint(x: width - 22, y: height), controlPoint1: CGPoint(x: width - 16, y: height), controlPoint2: CGPoint(x: width - 19, y: height))
outgoingColor.setFill()
}
bezierPath.close()
bezierPath.fill()
}
}
And, The tableView thingy:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BiddingHistoryIdentifier", for: indexPath) as! BiddingHistoryTableViewCell
let row = indexPath.row
var strNew: [String] = splitString[row].components(separatedBy: "-")
let strFirst = strNew[0].trimmingCharacters(in: .whitespacesAndNewlines)
let strSecond = strNew[1].trimmingCharacters(in: .whitespacesAndNewlines)
if !strFirst.elementsEqual(VendorLoginSetterGetter.strName.trimmingCharacters(in: .whitespacesAndNewlines)) {
let text = strSecond
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 18)
label.textColor = .black
label.text = text
let constraintRect = CGSize(width: 0.66 * cell.frame.width,
height: .greatestFiniteMagnitude)
let boundingBox = text.boundingRect(with: constraintRect,
options: .usesLineFragmentOrigin,
attributes: [.font: label.font],
context: nil)
label.frame.size = CGSize(width: ceil(boundingBox.width),
height: ceil(boundingBox.height))
let bubbleSize = CGSize(width: label.frame.width + 28,
height: label.frame.height + 20)
let bubbleView = BubbleView(isIncoming: true)
bubbleView.frame.size = bubbleSize
bubbleView.backgroundColor = .clear
//bubbleView.center = cell.center
bubbleView.frame.origin.y = (cell.frame.size.height / 2) - 20
bubbleView.frame.origin.x = 50
cell.addSubview(bubbleView)
label.center = bubbleView.center
cell.addSubview(label)
let imageView = UIImageView(frame: CGRect(x: 8, y: 0, width: 35, height: 35))
imageView.image = UIImage(named: "ic_vendor_black.png")
imageView.frame.origin.y = (cell.frame.size.height / 2) - 15
cell.addSubview(imageView)
}
else {
let text = strSecond
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 18)
label.textColor = .white
label.text = text
let constraintRect = CGSize(width: 0.66 * cell.frame.width,
height: .greatestFiniteMagnitude)
let boundingBox = text.boundingRect(with: constraintRect,
options: .usesLineFragmentOrigin,
attributes: [.font: label.font],
context: nil)
label.frame.size = CGSize(width: ceil(boundingBox.width),
height: ceil(boundingBox.height))
let bubbleSize = CGSize(width: label.frame.width + 28,
height: label.frame.height + 20)
let bubbleView = BubbleView(isIncoming: false)
bubbleView.frame.size = bubbleSize
bubbleView.backgroundColor = .clear
//bubbleView.center = cell.center
bubbleView.frame.origin.y = (cell.frame.size.height / 2) - 20
bubbleView.frame.origin.x = cell.frame.size.width - 200
cell.addSubview(bubbleView)
label.center = bubbleView.center
cell.addSubview(label)
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 28, height: 28))
imageView.image = UIImage(named: "ic_user_black.png")
imageView.frame.origin.y = (cell.frame.size.height / 2) - 15
imageView.frame.origin.x = cell.frame.size.width - 8
cell.addSubview(imageView)
}
return cell
}
Code Updated as Requested.

You need to move your code that adds the subviews to the cell from the cellForRowAt delegate to inside their respective cell classes.
The problem right now like #PeteMorris said, is that your cells which will be reused will already have the subviews which you added when it was dequeued the first time. So whenever the same cell is dequeued, subviews are added to it again.
The elegant solution would be to move the code that sets up the view to inside your cell class.
If you are looking for a hotfix you could remove all the subviews from the cell in prepareForReuse. (This method is called every time your cell is reused before dequeue-ing)
Your reuse method would look like this. (This method is inside your cell class)
override func prepareForReuse() {
super.prepareForReuse()
for view in subviews {
view.removeFromSuperview()
}
}

Related

Swift Tabbar with a custom shape in the middle

i want to make this shape in swift .
As you can see, the tabbar has a raised center button. However, this is not the only thing as there should be a real hole in the tabbar so that it is transparent there.
How can I create such a hole inside a tabbar? And then put a raised, round button in that hole?
I would gladly appreciate any help regarding my question.
i am trying but cannot achieve the above result.
import Foundation
import UIKit
#IBDesignable
class AppTabBar: UITabBar {
private var shapeLayer: CALayer?
override func draw(_ rect: CGRect) {
self.addShape()
}
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = #colorLiteral(red: 0.9782002568, green: 0.9782230258, blue: 0.9782107472, alpha: 1)
shapeLayer.lineWidth = 0.5
// The below 4 lines are for shadow above the bar. you can skip them if you do not want a shadow
shapeLayer.shadowOffset = CGSize(width:0, height:0)
shapeLayer.shadowRadius = 10
shapeLayer.shadowColor = UIColor.gray.cgColor
shapeLayer.shadowOpacity = 0.3
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
func createPath() -> CGPath {
let height2: CGFloat = self.frame.height
let height: CGFloat = 86.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
let startXpoint = centerWidth - height + 57
let endXpoint = (centerWidth + height - 45)
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: startXpoint , y: 0))
// path.addCurve(to: CGPoint(x: centerWidth, y: height - 40),
// controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height - 40))
path.addCurve(to: CGPoint(x: centerWidth, y: height / 1.6),
controlPoint1: CGPoint(x: startXpoint - 5, y: height2 / 3), controlPoint2: CGPoint(x: (centerWidth - 10), y: height2 / 1.6))
path.addCurve(to: CGPoint(x: (centerWidth + height / 2.9 ), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height - 40), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
for member in subviews.reversed() {
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event) else { continue }
return result
}
return nil
}
}
extension UITabBar {
override open func sizeThatFits(_ size: CGSize) -> CGSize {
var sizeThatFits = super.sizeThatFits(size)
sizeThatFits.height = 74
return sizeThatFits
}
}

CAShapeLayer animation issue

I have a CAShapeLayer (below) which is used to draw a bubble. Which all works fine, as per designs but when i start resizing it within a tableView with tableView.beginUpdates() and tableView.endUpdates(). I end up having the old layer in black momentarily. which is looks awful. I am really puzzled on how to solve this. It kinda feels that layers are black, and the fill paints over them like a mask.
I have tried several things as to add/removing the layer in different places. etc. but result is always this.
video to describe it
override func draw(_ rect: CGRect) {
self.layer.backgroundColor = UIColor.clear.cgColor
let fillColor: UIColor = self.shapeColor
let shapeLayer = CAShapeLayer()
shapeLayer.contentsScale = UIScreen.main.scale
let path = UIBezierPath()
shapeLayer.fillColor = fillColor.cgColor
let width = self.bounds.size.width
let height = self.bounds.size.height
path.move(to: CGPoint(x: 3.02, y: height * 0.5))
path.move(to: CGPoint(x: 3.02, y: 24.9))
path.addLine(to: CGPoint(x: 3.02, y: 14.62))
path.addLine(to: CGPoint(x: 3.02, y: 14.62))
path.addCurve(to: CGPoint(x: 17.64, y: -0), controlPoint1: CGPoint(x: 3.02, y: 6.55),controlPoint2: CGPoint(x: 9.57, y: -0))
path.addLine(to: CGPoint(x: width - 15.85, y: 0))
path.addLine(to: CGPoint(x: width - 15.85, y: 0))
path.addCurve(to: CGPoint(x: width, y: 14.62), controlPoint1: CGPoint(x: width - 7.77, y: 0), controlPoint2: CGPoint(x: width, y: 6.55))
path.addLine(to: CGPoint(x: width, y: height - 15.1))
path.addLine(to: CGPoint(x: width, y: height - 15.1))
path.addCurve(to: CGPoint(x: width - 15.85, y: height), controlPoint1: CGPoint(x: width, y: height - 7.03), controlPoint2: CGPoint(x: width - 7.77, y: height))
path.addLine(to: CGPoint(x: 17.64, y: height))
path.addLine(to: CGPoint(x: 17.64, y: height))
path.addCurve(to: CGPoint(x: 8.69, y: height - 3.56), controlPoint1: CGPoint(x: 14.39, y: height),controlPoint2: CGPoint(x: 11.24, y: height - 1.57))
path.addLine(to: CGPoint(x: 8.79, y: height - 3.46))
path.addCurve(to: CGPoint(x: 0, y: height), controlPoint1: CGPoint(x: 7.27, y: height - 1.27), controlPoint2: CGPoint(x: 3.84, y: height + 0.51))
path.addCurve(to: CGPoint(x: 3.02, y: height - 14.1), controlPoint1: CGPoint(x: 4.06, y: height - 4.22), controlPoint2: CGPoint(x: 3.02, y: height - 9.62))
path.close()
if self.reverted {
let mirrorOverXOrigin: CGAffineTransform = CGAffineTransform(scaleX: -1.0, y: 1.0);
let translate: CGAffineTransform = CGAffineTransform(translationX: width, y: 0);
// Apply these transforms to the path
path.apply(mirrorOverXOrigin)
path.apply(translate)
}
path.fill()
shapeLayer.path = path.cgPath
shapeLayer.rasterizationScale = UIScreen.main.scale
shapeLayer.shouldRasterize = true
if let bubbleLayer = self.bubbleLayer {
self.layer.replaceSublayer(bubbleLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.bubbleLayer = shapeLayer
}
shapeLayer.path = path.cgPath
shapeLayer.rasterizationScale = UIScreen.main.scale
shapeLayer.shouldRasterize = true
self.layer.insertSublayer(shapeLayer, at: 0)
self.bubbleLayer?.removeFromSuperlayer()
self.bubbleLayer = shapeLayer
}
your solution cannot work. Draw(_ :) is not called during animation block. It is draw after tableView.endUpdates(), after UITableView ends animation.
Draw(_ :) - I don't think you should use it for your needs at all.
If I can recommend you some solution:
1) In awakeFromNib:
override func awakeFromNib() {
super.awakeFromNib()
self.contentView.backgroundColor = self.shapeColor
self.contentView.layer.cornerRadius = 15
/** make left view and attach it to the left bottom anchors with fixed size and draw into this view the shape of your left arrow***/
self.leftArrowView.isHidden = self.isReverted
/** make right view and attach it to the right bottom anchors with fixed size and draw into this view the shape of your right arrow***/
self.rightArrowView.isHidden = !self.isReverted
}
2) In prepareForReuse:
override func prepareForReuse() {
super.prepareForReuse()
self.leftArrowView.isHidden = self.isReverted
self.rightArrowView.isHidden = !self.isReverted
}
And this should cause your cell will change size smoothly.

Unable to create custom msg lbl with chat bubble in chat table view. (all programmatically)

I have 1 class creating bezier bubble (working)
In tableviewcell class I am creating msg label programmatically using function
I use that function in view controller with table view
Images below are from same code.
Array used for data is ["a","aa","aaa","aaaa","aaaaa"]
Issue = The msg label is not showing/ being created
I have given all required things. The function visible in code is being directly called in tableview's cellforrowat function.
Image 1 = [Hierarchy view 1 after clicking button] (https://ibb.co/QfvsV3C)
Image 2 = [Hierarchy view 2 before clicking button] (https://ibb.co/L0pz2kz)
msg lbl code :
func drawMsgLbl(text: String)
{
//var msg = UILabel()
//msg = UILabel()
//msg?.lineBreakMode = .byWordWrapping
msg.numberOfLines = 0
msg.font = UIFont.init(name: "Avenir", size: 20)
msg.text = text
let txtWidth = text.size(OfFont: msg.font)
msg.textColor = .black
msg.backgroundColor = .yellow
// msg.frame = CGRect(x: 0, y: 0, width: msg.intrinsicContentSize.width, height: msg.intrinsicContentSize.height)
let constraintRect = CGSize(width: (0.66 * self.frame.width),
height: .greatestFiniteMagnitude)
let boundingBox = text.boundingRect(with: constraintRect,
options: .usesLineFragmentOrigin,
attributes: [.font: msg.font],
context: nil)
msg.frame.size = CGSize(width: ceil(boundingBox.width),
height: ceil(boundingBox.height))
print("Bounnding Width = \(boundingBox.width)")
print("msgIntrinsicWidth = \(msg.intrinsicContentSize.width)")
print("Text Width = \(txtWidth)")
print(text)
msg.frame.origin = CGPoint(x: (self.frame.size.width)-((msg.frame.size.width))-20, y: (self.frame.height)+10)
//msg?.center = self.center
let bubbleSize = CGSize(width: ((msg.frame.size.width)+30), height: ((msg.frame.size.height)+20))
let incomingBubblee = incomingBubble()
let outgoingBubblee = outgoingBubble()
outgoingBubblee.frame.size = bubbleSize
outgoingBubblee.frame.origin = CGPoint(x: (frame.size.width)-((msg.frame.size.width))-30, y: frame.size.height)
outgoingBubblee.backgroundColor = .clear
self.addSubview(outgoingBubblee)
self.addSubview(msg)
self.backgroundColor = .gray
}
I want to generate a msg label with width and height depending upon the text it has with a Bezier path that is being used (its code is placed separately, and the code is working fine)
Here is a very simple example, using auto-layout constraints to allow the cells / rows to auto-size their heights.
All via code - no #IBOutlets or prototype cells - so just assign a UITableViewController to this class (ChatBubbleTestTableViewController)
// simple "Message" struct
struct MyMessage {
var incoming: Bool = false
var message: String = ""
}
class ChatBubbleView: UIView {
let bubbleLayer = CAShapeLayer()
let chatLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
v.text = "Sample text"
return v
}()
// if it's an incoming message, background will be gray and bubble left-aligned
// otherwise background will be green and bubble right-alinged
var incoming = false
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add the bubble layer
layer.addSublayer(bubbleLayer)
// add the label
addSubview(chatLabel)
// constrain the label with 12-pts padding on all 4 sides
NSLayoutConstraint.activate([
chatLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
chatLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
chatLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
chatLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
])
}
override func layoutSubviews() {
super.layoutSubviews()
let width = bounds.size.width
let height = bounds.size.height
let bezierPath = UIBezierPath()
// NOTE: this bezier path is from
// https://medium.com/#dima_nikolaev/creating-a-chat-bubble-which-looks-like-a-chat-bubble-in-imessage-the-advanced-way-2d7497d600ba
if incoming {
bezierPath.move(to: CGPoint(x: 22, y: height))
bezierPath.addLine(to: CGPoint(x: width - 17, y: height))
bezierPath.addCurve(to: CGPoint(x: width, y: height - 17), controlPoint1: CGPoint(x: width - 7.61, y: height), controlPoint2: CGPoint(x: width, y: height - 7.61))
bezierPath.addLine(to: CGPoint(x: width, y: 17))
bezierPath.addCurve(to: CGPoint(x: width - 17, y: 0), controlPoint1: CGPoint(x: width, y: 7.61), controlPoint2: CGPoint(x: width - 7.61, y: 0))
bezierPath.addLine(to: CGPoint(x: 21, y: 0))
bezierPath.addCurve(to: CGPoint(x: 4, y: 17), controlPoint1: CGPoint(x: 11.61, y: 0), controlPoint2: CGPoint(x: 4, y: 7.61))
bezierPath.addLine(to: CGPoint(x: 4, y: height - 11))
bezierPath.addCurve(to: CGPoint(x: 0, y: height), controlPoint1: CGPoint(x: 4, y: height - 1), controlPoint2: CGPoint(x: 0, y: height))
bezierPath.addLine(to: CGPoint(x: -0.05, y: height - 0.01))
bezierPath.addCurve(to: CGPoint(x: 11.04, y: height - 4.04), controlPoint1: CGPoint(x: 4.07, y: height + 0.43), controlPoint2: CGPoint(x: 8.16, y: height - 1.06))
bezierPath.addCurve(to: CGPoint(x: 22, y: height), controlPoint1: CGPoint(x: 16, y: height), controlPoint2: CGPoint(x: 19, y: height))
bezierPath.close()
} else {
bezierPath.move(to: CGPoint(x: width - 22, y: height))
bezierPath.addLine(to: CGPoint(x: 17, y: height))
bezierPath.addCurve(to: CGPoint(x: 0, y: height - 17), controlPoint1: CGPoint(x: 7.61, y: height), controlPoint2: CGPoint(x: 0, y: height - 7.61))
bezierPath.addLine(to: CGPoint(x: 0, y: 17))
bezierPath.addCurve(to: CGPoint(x: 17, y: 0), controlPoint1: CGPoint(x: 0, y: 7.61), controlPoint2: CGPoint(x: 7.61, y: 0))
bezierPath.addLine(to: CGPoint(x: width - 21, y: 0))
bezierPath.addCurve(to: CGPoint(x: width - 4, y: 17), controlPoint1: CGPoint(x: width - 11.61, y: 0), controlPoint2: CGPoint(x: width - 4, y: 7.61))
bezierPath.addLine(to: CGPoint(x: width - 4, y: height - 11))
bezierPath.addCurve(to: CGPoint(x: width, y: height), controlPoint1: CGPoint(x: width - 4, y: height - 1), controlPoint2: CGPoint(x: width, y: height))
bezierPath.addLine(to: CGPoint(x: width + 0.05, y: height - 0.01))
bezierPath.addCurve(to: CGPoint(x: width - 11.04, y: height - 4.04), controlPoint1: CGPoint(x: width - 4.07, y: height + 0.43), controlPoint2: CGPoint(x: width - 8.16, y: height - 1.06))
bezierPath.addCurve(to: CGPoint(x: width - 22, y: height), controlPoint1: CGPoint(x: width - 16, y: height), controlPoint2: CGPoint(x: width - 19, y: height))
bezierPath.close()
}
bubbleLayer.fillColor = incoming ? UIColor(white: 0.90, alpha: 1.0).cgColor : UIColor.green.cgColor
bubbleLayer.path = bezierPath.cgPath
}
}
class ChatCell: UITableViewCell {
let bubbleView: ChatBubbleView = {
let v = ChatBubbleView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var leadingOrTrailingConstraint = NSLayoutConstraint()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add the bubble view
contentView.addSubview(bubbleView)
// constrain top / bottom with 12-pts padding
// constrain width to lessThanOrEqualTo 2/3rds (66%) of the width of the cell
NSLayoutConstraint.activate([
bubbleView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12.0),
bubbleView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12.0),
bubbleView.widthAnchor.constraint(lessThanOrEqualTo: contentView.widthAnchor, multiplier: 0.66),
])
}
func setData(_ message: MyMessage) -> Void {
// set the label text
bubbleView.chatLabel.text = message.message
// tell the bubble view whether it's an incoming or outgoing message
bubbleView.incoming = message.incoming
// left- or right-align the bubble view, based on incoming or outgoing
leadingOrTrailingConstraint.isActive = false
if message.incoming {
leadingOrTrailingConstraint = bubbleView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12.0)
} else {
leadingOrTrailingConstraint = bubbleView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12.0)
}
leadingOrTrailingConstraint.isActive = true
}
}
class ChatBubbleTestTableViewController: UITableViewController {
let theData: [MyMessage] = [
MyMessage(incoming: false, message: "A short message."),
MyMessage(incoming: true, message: "A medium length message, longer than short."),
MyMessage(incoming: false, message: "A long message. This one should be long enough to wrap onto multiple lines, showing that this message bubble cell will auto-size itself to the message content."),
MyMessage(incoming: true, message: "Another short message."),
MyMessage(incoming: false, message: "Another medium length message, longer than short."),
MyMessage(incoming: true, message: "Another long message. This one should be long enough to wrap onto multiple lines, showing that this message bubble cell will auto-size itself to the message content."),
]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(ChatCell.self, forCellReuseIdentifier: "ChatCell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return theData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatCell", for: indexPath) as! ChatCell
cell.setData(theData[indexPath.row])
return cell
}
}
The result:
and note that it auto-sizes when the cell changes size (such as when you rotate the device):
EDIT
The problem with your code...
In ChatCell.swift, you have:
func commonInit()
{
contentView.addSubview(bubbleView)
NSLayoutConstraint.activate([
bubbleView.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
bubbleView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
bubbleView.widthAnchor.constraint(lessThanOrEqualTo: contentView.widthAnchor, multiplier: 0.66)
])
}
bubbleView needs to be constrained to the cell's contentView:
func commonInit()
{
contentView.addSubview(bubbleView)
NSLayoutConstraint.activate([
bubbleView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12.0),
bubbleView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12.0),
bubbleView.widthAnchor.constraint(lessThanOrEqualTo: contentView.widthAnchor, multiplier: 0.66)
])
}
EDIT 2
The most straight-forward way to change the bubble background colors...
In ChatBubbleView class, remove this line (at the end of layoutSubviews():
bubbleLayer.fillColor = isIncoming ? UIColor(white: 0.90, alpha: 1.0).cgColor : UIColor.green.cgColor
In ChatCell class, add two properties:
var incomingColor: UIColor = UIColor(white: 0.90, alpha: 1.0)
var outgoingColor: UIColor = UIColor.green
Those will be your "default" colors.
Also in ChatCell class, in setData(), add this line:
bubbleView.bubbleLayer.fillColor = message.incoming ? incomingColor.cgColor : outgoingColor.cgColor
Then, in TestVC in cellForRowAt, you can do this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell",for: indexPath) as! ChatCell
cell.incomingColor = .cyan
cell.outgoingColor = .yellow
cell.setData(theData[indexPath.row])
return cell
}
I'll leave it up to you as to how you want to implement color selection by the user.

Swift 4 view with curved center

i have difficulties to create UI. I tried to create custom view, but i can't see even the background of this view. Here is my sample code, i took it from another post, but i don't know how to change it to my case.
I do use SnapKit for my UI elements.
My View:
lazy var greenView: CurvedView = {
let view = CurvedView()
view.backgroundColor = #colorLiteral(red: 0.08778516203, green: 0.7643524408, blue: 0.1997725368, alpha: 1)
DispatchQueue.main.async {
view.snp.makeConstraints{ (make) -> Void in
make.top.equalTo(self.shipView.snp.bottom).offset(100)
make.left.right.equalTo(self.scrollContentView)
make.height.equalTo(200)
}
}
return view
}()
Tried to create custom View:
class CurvedView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
StyleKitName.drawCanvas1(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 400), resizing: .aspectFit)
backgroundColor = .red
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
UPDATED green customView with PaintCode
public class StyleKitName : UIView {
//// Drawing Methods
#objc dynamic public class func drawCanvas1(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 240, height: 120), resizing: ResizingBehavior = .aspectFit) {
//// General Declarations
let context = UIGraphicsGetCurrentContext()!
//// Resize to Target Frame
context.saveGState()
let resizedFrame: CGRect = resizing.apply(rect: CGRect(x: 0, y: 0, width: 240, height: 120), target: targetFrame)
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 240, y: resizedFrame.height / 120)
//// Color Declarations
let color = #colorLiteral(red: 0.3411764801, green: 0.6235294342, blue: 0.1686274558, alpha: 1)
let color2 = #colorLiteral(red: 0.7450980544, green: 0.1568627506, blue: 0.07450980693, alpha: 1)
//// Rectangle Drawing
let rectanglePath = UIBezierPath()
rectanglePath.move(to: CGPoint(x: 48, y: 103))
rectanglePath.addLine(to: CGPoint(x: 191, y: 103))
rectanglePath.addLine(to: CGPoint(x: 191, y: 9))
rectanglePath.addLine(to: CGPoint(x: 48, y: 9))
rectanglePath.addLine(to: CGPoint(x: 48, y: 103))
rectanglePath.close()
color.setFill()
rectanglePath.fill()
//// Bezier Drawing
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 96, y: 9))
bezierPath.addCurve(to: CGPoint(x: 110, y: 15), controlPoint1: CGPoint(x: 104.11, y: 9.42), controlPoint2: CGPoint(x: 107, y: 12))
bezierPath.addCurve(to: CGPoint(x: 123, y: 23), controlPoint1: CGPoint(x: 114.16, y: 19.16), controlPoint2: CGPoint(x: 115.63, y: 23))
bezierPath.addCurve(to: CGPoint(x: 136, y: 15), controlPoint1: CGPoint(x: 129.95, y: 23), controlPoint2: CGPoint(x: 132, y: 19))
bezierPath.addCurve(to: CGPoint(x: 152, y: 9), controlPoint1: CGPoint(x: 139.3, y: 11.7), controlPoint2: CGPoint(x: 145.35, y: 9))
bezierPath.addCurve(to: CGPoint(x: 96, y: 9), controlPoint1: CGPoint(x: 166.7, y: 9), controlPoint2: CGPoint(x: 96, y: 9))
bezierPath.addLine(to: CGPoint(x: 148.61, y: 9))
color2.setFill()
bezierPath.fill()
context.restoreGState()
}
#objc(StyleKitNameResizingBehavior)
public enum ResizingBehavior: Int {
case aspectFit /// The content is proportionally resized to fit into the target rectangle.
case aspectFill /// The content is proportionally resized to completely fill the target rectangle.
case stretch /// The content is stretched to match the entire target rectangle.
case center /// The content is centered in the target rectangle, but it is NOT resized.
public func apply(rect: CGRect, target: CGRect) -> CGRect {
if rect == target || target == CGRect.zero {
return rect
}
var scales = CGSize.zero
scales.width = abs(target.width / rect.width)
scales.height = abs(target.height / rect.height)
switch self {
case .aspectFit:
scales.width = min(scales.width, scales.height)
scales.height = scales.width
case .aspectFill:
scales.width = max(scales.width, scales.height)
scales.height = scales.width
case .stretch:
break
case .center:
scales.width = 1
scales.height = 1
}
var result = rect.standardized
result.size.width *= scales.width
result.size.height *= scales.height
result.origin.x = target.minX + (target.width - result.width) / 2
result.origin.y = target.minY + (target.height - result.height) / 2
return result
}
}
}
https://d.radikal.ru/d30/1902/c5/c07cac5532d0.png
This is the basic way to use that PaintCode generated code...
class StyleView: UIView {
override func draw(_ rect: CGRect) {
StyleKitName.drawCanvas1()
}
}
class StyleTestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let v = StyleView()
v.frame = CGRect(x: 0, y: 0, width: 240, height: 120)
v.center = view.center
v.backgroundColor = .blue
view.addSubview(v)
}
}
Result:

Mask UIView (UICollectionView) correctly

Ok, I need to mask a horizontal UICollectionViewso it looks like a ribbon. My web dev counterpart got it done with an SVG that he used as a masking layer.
Here is my current situation:
And here is what I need (Photo from our web app):
Small detail in image spacing aside, I have an SVG like this:
Which I can successfully convert into UIBezierPath code (Paintcode):
let pathPath = UIBezierPath()
pathPath.move(to: CGPoint(x: 0.05, y: 0))
pathPath.addCurve(to: CGPoint(x: 162.02, y: 3.8), controlPoint1: CGPoint(x: 0.05, y: 2.1), controlPoint2: CGPoint(x: 72.57, y: 3.8))
pathPath.addCurve(to: CGPoint(x: 324, y: 0), controlPoint1: CGPoint(x: 251.48, y: 3.8), controlPoint2: CGPoint(x: 324, y: 2.1))
pathPath.addLine(to: CGPoint(x: 324, y: 150.2))
pathPath.addCurve(to: CGPoint(x: 162.02, y: 154), controlPoint1: CGPoint(x: 324, y: 152.3), controlPoint2: CGPoint(x: 251.48, y: 154))
pathPath.addCurve(to: CGPoint(x: 0.05, y: 150.2), controlPoint1: CGPoint(x: 72.57, y: 154), controlPoint2: CGPoint(x: 0.05, y: 152.3))
pathPath.addCurve(to: CGPoint(x: 0, y: 0.17), controlPoint1: CGPoint(x: 0.05, y: 148.1), controlPoint2: CGPoint(x: 0, y: 0.17))
pathPath.usesEvenOddFillRule = true
UIColor.lightGray.setFill()
pathPath.fill()
Now what do I do?
You should use the mask property of your UICollectionView by setting it to a view whose alpha channel indicates what part of the UICollectionView you want to mask out. In outline it will probably be something like:
// If you don't have a custom subclass of UICollectionView... you can handle the resize in the
// UICollectionViewController or whatever view contoller is handling your view.
//
// If you do have a custom subclass of UICollectionView... you could do something similar
// in layoutSubviews.
class MyViewController : UICollectionViewController {
// recreate the mask after the view lays out it's subviews
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let maskImage = createMaskingImage(size: self.view.bounds.size) {
let maskView = UIView(frame: self.view.bounds)
maskView.layer.contents = maskImage
self.view.mask = maskView
}
}
}
// create a masking image for a view of the given size
func createMaskingImage(size: CGSize) -> UIImage? {
let drawingBounds = CGRect(origin: CGPoint.zero, size: size)
UIGraphicsBeginImageContext(size)
// We're going to jump down to the level of cgContext for scaling
// You might be able to do this from the level of UIGraphics, but I don't know how so...
// Implicitly unwrapped optional, but if we can't get a CGContext we're in trouble
let cgContext = UIGraphicsGetCurrentContext()!
let maskingPath = createMaskingPath()
let pathBounds = maskingPath.bounds;
cgContext.saveGState()
// Clearing the image may not strictly be necessary
cgContext.clear(drawingBounds)
// Scale the context so that when we draw the path it fits in the drawing bounds
// Could just use "size" here instead of drawingBounds.size, but I think this makes it a
// little more explicit that we're matching up two rects
cgContext.scaleBy(x: drawingBounds.size.width / pathBounds.size.width,
y: drawingBounds.size.height / pathBounds.size.height)
cgContext.setFillColor(UIColor.lightGray.cgColor)
cgContext.addPath(maskingPath.cgPath)
cgContext.fillPath(using: .evenOdd)
cgContext.restoreGState()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func createMaskingPath() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0.05, y: 0))
path.addCurve(to: CGPoint(x: 162.02, y: 3.8), controlPoint1: CGPoint(x: 0.05, y: 2.1), controlPoint2: CGPoint(x: 72.57, y: 3.8))
path.addCurve(to: CGPoint(x: 324, y: 0), controlPoint1: CGPoint(x: 251.48, y: 3.8), controlPoint2: CGPoint(x: 324, y: 2.1))
path.addLine(to: CGPoint(x: 324, y: 150.2))
path.addCurve(to: CGPoint(x: 162.02, y: 154), controlPoint1: CGPoint(x: 324, y: 152.3), controlPoint2: CGPoint(x: 251.48, y: 154))
path.addCurve(to: CGPoint(x: 0.05, y: 150.2), controlPoint1: CGPoint(x: 72.57, y: 154), controlPoint2: CGPoint(x: 0.05, y: 152.3))
path.addCurve(to: CGPoint(x: 0, y: 0.17), controlPoint1: CGPoint(x: 0.05, y: 148.1), controlPoint2: CGPoint(x: 0, y: 0.17))
return path
}
/*
This is a UICollectionView, which clips normally on the left and right,
but allows some extra space horizontally.
A typical example is you need to clip the scrolling items but you
still need to allow shadows.
*/
import Foundation
import UIKit
class CustomClipCollectionView: UICollectionView {
private lazy var extraSpaceOnBaseButStillClipSidesNormally: CALayer = {
let l = CALayer()
l.backgroundColor = UIColor.black.cgColor
return l
}()
override func layoutSubviews() {
extraSpaceOnBaseButStillClipSidesNormally.frame = bounds.insetBy(
dx: 0, dy: -10)
layer.mask = extraSpaceOnBaseButStillClipSidesNormally
super.layoutSubviews()
}
}

Resources