UIBezierPath not cleared on redraw - ios

I have custom UIView in which I draw a UIBezierPath. This view is used in a UITableViewCell. So when I scroll, the custom view's bezier path gets redrawn. The problem is that the new path draws over old path drawings (the context isn't cleared properly). I'm calling setNeedsDisplay() on the table cell fetch and I've also set clearsContextBeforeDrawing = true for the view. The only thing that clears up the old drawing is calling context.clear(rect) but this isn't at all ideal since I lose the background (it becomes black).
Any ideas on how to fix this?
class CustomView: UIView {
var percentage: CGFloat = 0 {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
clearsContextBeforeDrawing = true
contentMode = .redraw
clipsToBounds = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
// NOTE: don't really like doing this
let context = UIGraphicsGetCurrentContext()!
context.clear(rect)
let center = CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2)
let path = UIBezierPath(arcCenter: center,
radius: self.bounds.size.width/2-10,
startAngle: 0.5 * .pi,
endAngle: (-2.0 * self.percentage + 0.5) * .pi,
clockwise: false)
path.lineWidth = 4
UIColor.red.setStroke()
path.stroke()
}
}
This is where my cell and custom uiview gets set.
override func tableView(_ tableView: UITableView, cellForItemAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let num = list[indexPath.item]
let percentage = CGFloat(num) / 10
cell.customView.percentage = percentage // Custom view should get redrawn after this call
return cell
}

I think the problem is merely that your drawing is huge and strays outside the cell because your views do not clip to bounds. That's incoherent. Each cell needs to draw just its own content.
In other words, the overlapping you are seeing has nothing to do with the custom view drawing; it has to do with the cell drawing. You are infecting the neighboring cells with your drawing.
By the way, you say:
but this isn't at all ideal since I lose the background (it becomes black)."
You can fix that by saying self.backgroundColor = .clear in your init.

Related

How to get UIImageView's subview (a UIView) to cover it entirely?

I have a UIImageView placed on the storyboard, and am trying to programmatically add a UIView as a subview to it, and have that UIView match the parent UIImageView's size and position exactly so that it covers it.
The UIView is a custom class of UIView thats drawing a CAShapeLayer that fills its frame, if that matters at all.
In addition to the following code, I've also tried "redOverlay.center = parentImage.center" without success.
In my viewdidload() i have:
let redOverlay = RedOverlay(frame: CGRect(x: 0.0, y: 0.0, width: parentImage.bounds.width, height: parentImage.bounds.height))
parentImage.addSubview(redOverlay)
And here is my RedOverlay subclass of UIView if that makes a difference:
import UIKit
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat.pi / 180.0
}
}
class RedOverlay: UIView {
var path: UIBezierPath!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.darkGray.withAlphaComponent(0.5)
pie()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func pie() {
path = UIBezierPath()
path.move(to: CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2))
path.addArc(withCenter: CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2),
radius: self.bounds.size.width/2,
startAngle: CGFloat(215).toRadians(),
endAngle: CGFloat(90).toRadians(),
clockwise: true)
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.opacity = 0.5
self.layer.addSublayer(shapeLayer)
}
}
The result I get actually works perfectly on iPhone 6, but anything larger like 6s or ipads, and the child UIView is actually noticeably smaller than the UIImageView. In the iPad, the UIView sits well to the left, while in the 6s it is only a bit left.
Basically, it seems like the UIView being generated isnt as wide as the image, even though parentImage.bounds.width was used to determine the UIView's width. I do have "Clip to Bounds" and Aspect Fit set in IB so I don't know why the image would be bigger than it's bounds if thats possible at all.
Whats the cleanest way to get this subview to match the parent view in all device sizes?
EDIT: The solution I found was to call my function pie() (that draws the CAShapeLayer), by overriding layoutSubviews and calling pie() there so that my shapeLayer is updated whenever the view changes. I removed .addSublayer(shapeLayer) from pie() and now I call it in viewdidload in case layoutsubviews being called repeatedly might cause many layers to be created.
override func layoutSubviews() {
super.layoutSubviews()
pie()
}

CollectionViewCell Draw Rect

I'm trying to draw a simple outlined circle inside my collection view cells. For some reason, only the first cell is being draw, the rest are not showing.
class UserCell: UICollectionViewCell {
override func draw(_ rect: CGRect) {
let center = CGPoint(x: self.center.x - 1, y: 41)
let circularPath = UIBezierPath()
circularPath.addArc(withCenter: center, radius: 36, startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: true)
UIColor.red.setStroke()
circularPath.lineWidth = 2
circularPath.stroke()
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
What am I missing here?
Collection view cells are configured for display using UICollectionViewDataSource method cellForItemAt(). The cells are reused and won't automatically redraw for each 'new' cell. Instead of overriding draw(rect), add subviews to the cell and configure the subviews in cellForItemAt().
You might want to define your center point differently. The center point is specified in points in the coordinate system of its superview. Either try convert the cell's center point from its superview's coordinate system or use the bounds of the cell instead and adjust the x and y values accordingly.
let center = self.convert(self.center, from: self.superview)
let center = CGPoint(x: bounds.midX, y: bounds.midY)
Created a new class conforming to UIView(), added the bezierPath info inside its draw function. Then subclassed this class inside the collectionView cell. Works as expected.

Is it possible that UICollectionViewCell gradient background layer render issue happens in case of rotation?

So I have a custom UICollectionViewController, which contains simple rectangle cells (UICollectionViewCell). I would like to customize these with rounded corners and gradient background color. I could achieve this by setting the proper parameters for cells in the viewcontroller.
However, in case of rotation, my cells "collapse" just like there is something wrong with the rendering. Do you think I am missing something, or should I do this in a completely different way?
 Code
override func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = self.collectionView
.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier,
for: indexPath) as? AllCollectionViewCell else {
fatalError("The dequeued cell is not an instance of AllCollectionViewCell.")
}
// Configure the cell
cell.layer.cornerRadius = 15;
cell.layer.masksToBounds = true
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.colors = [UIColor.red.withAlphaComponent(0.3).cgColor,
UIColor.red.withAlphaComponent(0.7).cgColor]
gradientLayer.frame = cell.bounds
cell.backgroundView = UIView()
cell.backgroundView?.layer.addSublayer(gradientLayer)
return cell
}
The problem is not absolutely predictable, but it happens very often after rotation (going back to portrait after landscape view) and scrolling.
Expected
https://i.imgur.com/tGvauZG.png
Problem
https://i.imgur.com/DRPZjcD.png
i've tried it but i didn't get that UI bug. may you send me your code and let me check if you want.
but generally i for a better practice i think, you should move cell UI code on it's cell class AllCollectionViewCell
I tried to set the properties in the cell class itself (AllCollectionViewCell) as suggested by canister_exister and Mohamed Shaban, and it solved it partially:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.cornerRadius = 15;
self.layer.masksToBounds = true
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.5)
self.backgroundView = UIView()
self.backgroundView?.layer.addSublayer(gradientLayer)
}
However, the frames of gradientLayer just didn't meet my expectations, so I searched a bit more further, and found out, that it should be placed into another function. So I moved the gradientLayer to a class property, then overriden this function:
override func layoutSubviews() { self.gradientLayer.frame = self.bounds }
Thank you for the helpful answers!

How to make UICollectionViewCells have rounded corners and drop shadows?

I've seen some other answers to similar questions, but none of the solutions have worked for me, even with some tweaking.
As the title suggests, I would like my UICollectionViewCells to have rounded corners and drop shadows (or shadows on all sides except the top). What I have done so far is to add two views to my UICollectionViewCell - mainView, which displays the cell's necessary content and has rounded corners, and shadowView, which has the shadow. Neither view is a subview of the other, they exist together in the same cell. They are both the exact same dimensions, and mainView is obviously displayed on top of shadowView. Here is my current code implementation:
let mainView = cell.viewWithTag(1003) as! UIView
mainView.layer.cornerRadius = 10.0
mainView.layer.borderWidth = 1.0
mainView.layer.borderColor = UIColor.clear.cgColor
mainView.layer.masksToBounds = true
let shadowView = cell.viewWithTag(1002) as! UIView
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0, height: 2.0)
shadowView.layer.shadowRadius = 2.0
shadowView.layer.shadowOpacity = 0.5
shadowView.layer.masksToBounds = false
shadowView.layer.shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: mainView.layer.cornerRadius).cgPath
Here is what this code produces:
One would think that mainView's corners aren't rounded enough, and that the shadow isn't big enough.
However, upon removing shadowView and setting the UICollectionView's background to black, you can see that mainView's corners are actually quite rounded:
So this implies that the issue is with shadowView's shadow size. However, I have tried increasing the shadow offsets and the the shadow radius, which did nothing. Most changes either produced a very thick shadow around mainView, decreased mainView's rounding even more, or did nothing.
Here is an image of the relevant UITableViewCell's view hierarchy in the Storyboard:
Here is the Cell code:
class CollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
layer.shadowColor = UIColor.lightGray.cgColor
layer.shadowOffset = CGSize(width: 0, height: 2.0)
layer.shadowRadius = 5.0
layer.shadowOpacity = 1.0
layer.masksToBounds = false
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: contentView.layer.cornerRadius).cgPath
layer.backgroundColor = UIColor.clear.cgColor
contentView.layer.masksToBounds = true
layer.cornerRadius = 10
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is the usage code:
import UIKit
private let reuseIdentifier = "Cell"
class CollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.register(CollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
(self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout).itemSize = CGSize(width: 100, height: 100)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.backgroundColor = .white
return cell
}
}
Here is the result:
Update
Thanks for the chat on Hangouts. I downloaded your code and mainView and shadowView was nil.
I noticed that you're not comfortable with CollectionViewCells.
With that being said. I'm more than happy to introduce our friend Runtime Attributes
Without touching your code. Select a view and add your runtime attribute key and set the value.
Output:
Go ahead, and do the work for the shadow and the rest.
The mainView is rounded, but the shadowView is taking all the credit.
You need to enable clipsToBounds on both views:
mainView.clipsToBounds = true

Swift: Button round corner breaks contraints

I have a tableview with custom cell loaded via xib and in that cell I have status button which bottom right corner should be rounded. The button has constraints Trailing/Leading/Bottom space to superview=0 and height=30.
Without rounding it is working perfectly, as soon as I round one corner for example bottom right the constraints breaks
self.btnStatus.roundCorners(corners: [.bottomRight], radius: 7.0, borderWidth: nil, borderColor: nil)
Some guys here suggesting to call layoutSubviews() but it didn't helped me.
To be more specific I've created simple project where you can have a look into whole project.
Correct Link
ButtonRoundCorner.zip
You can get more reliable results by subclassing your button and placing your "rounding" code by overriding its layoutSubviews() function.
First, if you want to add a border, you don't want to add multiple "border sublayers" ... so change your UIView extension to this:
extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat, borderWidth: CGFloat?, borderColor: UIColor?) {
let maskPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds
maskLayer.path = maskPath.cgPath
self.layer.mask = maskLayer
self.layer.masksToBounds = true
if (borderWidth != nil && borderColor != nil) {
// remove previously added border layer
for layer in layer.sublayers! {
if layer.name == "borderLayer" {
layer.removeFromSuperlayer()
}
}
let borderLayer = CAShapeLayer()
borderLayer.frame = self.bounds;
borderLayer.path = maskPath.cgPath;
borderLayer.lineWidth = borderWidth ?? 0;
borderLayer.strokeColor = borderColor?.cgColor;
borderLayer.fillColor = UIColor.clear.cgColor;
borderLayer.name = "borderLayer"
self.layer.addSublayer(borderLayer);
}
}
}
Next, add a UIButton subclass:
class RoundedButton: UIButton {
var corners: UIRectCorner?
var radius = CGFloat(0.0)
var borderWidth = CGFloat(0.0)
var borderColor: UIColor?
override func layoutSubviews() {
super.layoutSubviews()
// don't apply mask if corners is not set, or if radius is Zero
guard let _corners = corners, radius > 0.0 else {
return
}
roundCorners(corners: _corners, radius: radius, borderWidth: borderWidth, borderColor: borderColor)
}
}
This gives you a couple benefits: 1) It will update its mask layer frame when the button frame changes (rotating the device, for example), and 2) you could set these values either from your custom cell class or from cellForRowAt.
Either way, change your btnStatus class from UIButton to RoundedButton - both in your storyboard and the #IBOutlet connection.
Then change your CustomTableViewCell to this:
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var btnStatus: RoundedButton!
override func awakeFromNib() {
super.awakeFromNib()
// set up corner maskign
btnStatus.corners = .bottomRight
btnStatus.radius = 7.0
// set if desired
// btnStatus.borderWidth = 2.0
// btnStatus.borderColor = .blue
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
And finally, your cellForRowAt function becomes:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomTableViewCell
return cell
}
That should do it...
I looked at your code. The problem is that your roundCorners function is depending on your view's bounds to set your layer's properties and you are calling roundCorners in awakeFromNib at which point your cell has the same bounds as in the NIB file because autolayout has not been calculated yet. You need to move your roundCorners call into layoutSubviews, so it gets called after autolayout is done computing your bounds.
import UIKit
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var btnStatus: UIButton!
override func layoutSubviews() {
super.layoutSubviews()
self.btnStatus.roundCorners(corners: [.bottomRight], radius: 7.0, borderWidth: nil, borderColor: nil)
}
}
EDIT also add cell.setNeedsLayout() to cellforRowAt to force layoutSubviews to be called before the cell is drawn for the first time.
Maybe your button is overlapping since it doesn't have an upper constraint? Try adding a bright border and see if you can see the top part, if not try adding a constraint to the top. Also, instead of giving it a static height, try making it a proportional height to the object above it (maybe the screen size of your device is causing it to overlap)

Resources