I originally had an issue where the UITableViewCells would have multiple shadows because I was making a call to add a shadow to the contentView in dequeueReusableCell. After learning that this reused cells, I figured the proper way to initialize the shadows on the tableViewCells was in the initialization function.
In my custom table view cell class I call:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addShadow()
}
Now the shadows are the incorrect size and I need help figuring out the proper way to set the shadow size to match the content.
Here is my shadow function, which is added as a UIView extension:
func addShadow() {
self.layer.masksToBounds = false
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOpacity = 0.23
self.layer.shadowRadius = 2.0
self.layer.shadowOffset = CGSize(width: 1.0, height: 1.0)
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 8, height: 8)).cgPath
}
Here is a screenshot showing the initial shadows and then the images that are loaded in their place:
The images are put onto the tableview cells with this function, which is called in the dequeueReusableCell function:
func makeBackgroundImageCall(anime: Anime) {
guard let url = anime.getCoverImageOriginalUrl() else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
if (error != nil) {
print(error as Any)
return
}
guard let image = UIImage(data: data!) else { return }
DispatchQueue.main.async() { [weak self] in
guard let contentView = self?.contentView else { return }
let imageView = UIImageView(image: image)
contentView.addSubview(imageView)
imageView.pin(to: contentView) // sets constraints to fill parent
}
}.resume()
}
Lastly, here is my dequeueReusableCell function:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseID) as! AnimeTableCell
if let anime = trendingAnimes[indexPath.section] {
cell.makeBackgroundImageCall(anime: anime)
}
return cell
}
Another note is that this is all programatic, there's no storyboard so this is why I'm using init instead of awakeFromNib.
How can I get the shadow to properly overlay the uiTableViewCell and its content image?
mylayer.frame = self.bounds;
try to set bound for the shadow layer.
You are setting the shadowPath when your cell is first initialized, and doesn't have its final size yet.
From your example it looks like your cells are rectangular. In this case you don't need to set a shadowPath at all, and the shadow will fit the shape and size of the cell.
If you do want to define a custom shadowPath, you will need to create it after the cell is resized to fit its contents.
Related
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
ViewController.swift
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "mycell") as! CustomTableViewCell
cell.backgroundColor = UIColor.clear
cell.nameLbl.text = "\(adminNmaes[indexPath.row])"
cell.aboutLbl.text = "\(aboutarray[indexPath.row])"
if indexPath.row == 0
{
cell.view1.roundCorners(corners: [.topLeft,.topRight], radius: 10)
// tableView.reloadData()
}
return cell
}
extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
i setup custom cell class using autolayout and used an extension to apply cornerradius for only 1st cell. i aslo want to give corner radius for bottom left and bottom right for the footer and also give it a padding of 10. any help is appreciated.thanks in advance.
As of iOS11, CALayer has property maskedCorners:
var maskedCorners: CACornerMask { get set }
In your case you would use:
cell.contentView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMinYCorner]
This masks the top left and right corners to whatever cornerRadius you've set.
Also you need to make sure that the contentView is clipping its subviews so that they can conform to the shape
cell.contentView.clipsToBounds = true
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)
I have a UITableView inside of a UIViewController. I'm attempting to apply a gradient over a background image in a UITableViewCell. I can get a gradient to draw, but it's drawing a clean line between the colors instead of a gradient.
In my cellForRowAt, I assign an object to my CustomTableViewCell, like so:
let cellIdentifier = "Cell"
// all the standard data source, delegate stuff for uitableviews
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! CustomTableViewCell
let scheduleItem = scheduleData[indexPath.row]
cell.scheduleStruct = scheduleItem
return cell
}
Over in CustomTableViewCell, I've got this property declared:
public var scheduleStruct: FullScheduleStruct? {
didSet {
configureCell()
}
// emptyView to throw on top of a background image
var gradientView: UIView?
I'm also using Kingfisher to pull images off of the internet, which is what's going on with the ImageResource(downloadURL: url) part. The configureCell() method called when scheduleStruct is set like this:
private func configureCell() {
// get the background image
if let url = scheduleStruct?.show.image?.original {
let imageUrl = ImageResource(downloadURL: url)
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.kf.setImage(with: imageUrl)
// configure the gradientView
gradientView = UIView(frame: backgroundImageView.frame)
let gradient = CAGradientLayer()
gradient.frame = gradientView!.frame
gradient.colors = [UIColor.clear.cgColor, UIColor.white.cgColor]
// gradient.locations = [0.0, 1.0]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 0, y: 1)
gradientView!.layer.insertSublayer(gradient, at: 0)
backgroundImageView.insertSubview(gradientView!, at: 0)
}
Any suggestions re: where my mistake lies are greatly appreciated. Thank you for reading.
At David Shaw's suggestion, I commented out this line in configureCell()
// backgroundImageView.kf.setImage(with: imageUrl)
and the gradient drew properly. I deduced I had a timing issue because I'm getting the image asynchronously.
I looked through the method signatures and saw there's one with a completion handler, so I used it instead and it worked:
backgroundImageView.kf.setImage(with: imageUrl, completionHandler: { (image, error, cacheType, imageUrl) in
// do stuff
})
...so I did this.
I've a tableView and I'm trying to add black mask on each image in the cell not on the whole cell just on the image. But I have two issues;
-It is masking the half cell from the left to the middle and each I scroll it is becoming darker. So please where would be my issue?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! PlacesTableViewCell
cell.backgroundImage.sd_setImageWithURL(NSURL(string: place.Image), placeholderImage: nil, options: .HighPriority)
}
class PlacesTableViewCell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
maskArrangement()
}
func maskArrangement(){
var maskLayer = CALayer()
maskLayer.frame = CGRectMake(0, 0, backgroundImage.frame.size.width, backgroundImage.frame.size.height)
maskLayer.backgroundColor = UIColor(white: 0.0, alpha: 0.5).CGColor
//backgroundImage.layer.mask = maskLayer
backgroundImage.layer.addSublayer(maskLayer)
}
}
Every time cell is dequeued you are adding new sublayer to your backgroundImage. In this way it is getting darker when you scroll (when you dequeued your cells). I think you can add to your PlacesTableViewCell class at awakeFromNib method if it has a .xib file.
Meanwhile your image's frame should be half of the cell. Use autolayout to fix it's frame (for instance, set width and height constraints) in your PLacesTableViewCell class.
In #Shripada 's way:
class PlacesTableViewCell: UITableViewCell {
var maskLayer : CALayer
override func layoutSubviews() {
super.layoutSubviews()
maskArrangement()
}
func maskArrangement(){
if maskLayer == nil {
maskLayer.frame = CGRectMake(0, 0, backgroundImage.frame.size.width, backgroundImage.frame.size.height)
maskLayer.backgroundColor = UIColor(white: 0.0, alpha: 0.5).CGColor
//backgroundImage.layer.mask = maskLayer
backgroundImage.layer.addSublayer(maskLayer)
}
}
}
I haven't tried this code. But your solution can be like that.