Removing Shadow Underneath Semi-Transparent UIView - ios

I’ve been trying to add a drop shadow to a semi transparent UIView but the drop shadow is showing up underneath the view. Basically anywhere inside the outline of the view, I don't want to see any shadows. The location icon has no styling.
// Basic Shadow
self.myView.layer.shadowColor = UIColor.black.cgColor
self.myView.layer.shadowOpacity = 0.3
self.myView.layer.shadowOffset = CGSize(width: 0, height: 3)
self.myView.layer.shadowRadius = 0

The easiest way to do this is to use a custom UIView subclass with two CAShapeLayers...
For the "shadow" layer path, use a rounded-rect UIBezierPath that is slightly taller than the view, so it extends below the bottom.
Here's a quick example...
Custom View Class
class CustomView: UIView {
public var translucentColor: UIColor = .white.withAlphaComponent(0.7) { didSet { setNeedsLayout() } }
public var borderColor: UIColor = .init(red: 0.73, green: 0.84, blue: 0.96, alpha: 1.0) { didSet { setNeedsLayout() } }
public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
public var shadowColor: UIColor = .black.withAlphaComponent(0.3) { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
public var offset: CGFloat = 10 { didSet { setNeedsLayout() } }
private let shadowLayer = CAShapeLayer()
private let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(shadowLayer)
layer.addSublayer(topLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
// rounded-rect path for visible border
let pth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
// translucent rounded-rect bordered properties
topLayer.path = pth.cgPath
topLayer.fillColor = translucentColor.cgColor
topLayer.lineWidth = borderWidth
topLayer.strokeColor = borderColor.cgColor
// rounded-rect path for "shadow" border
r.size.height += offset
let spth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
shadowLayer.path = spth.cgPath
shadowLayer.fillColor = UIColor.clear.cgColor
shadowLayer.lineWidth = borderWidth
shadowLayer.strokeColor = shadowColor.cgColor
}
}
Example Controller Class
class CustomViewTestVC: UIViewController {
let gradView = BasicGradientView()
let customView = CustomView()
// let's add a label between the gradient view and the custom view
// so we can confirm it's translucent
let testLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textAlignment = .center
v.textColor = .systemBlue
v.font = .systemFont(ofSize: 34.0, weight: .bold)
v.text = "This is a test to confirm that the view and the \"shadow\" are both translucent while the border is opaque." // Tap anywhere to toggle this label's visibility."
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[gradView, testLabel, customView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
gradView.widthAnchor.constraint(equalToConstant: 312.0),
gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor, multiplier: 1.0),
gradView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testLabel.widthAnchor.constraint(equalTo: gradView.widthAnchor, constant: -4.0),
testLabel.heightAnchor.constraint(equalTo: gradView.heightAnchor, constant: 0.0),
testLabel.centerXAnchor.constraint(equalTo: gradView.centerXAnchor),
testLabel.centerYAnchor.constraint(equalTo: gradView.centerYAnchor),
customView.widthAnchor.constraint(equalTo: gradView.widthAnchor, constant: -90.0),
customView.heightAnchor.constraint(equalTo: gradView.heightAnchor, constant: -90.0),
customView.centerXAnchor.constraint(equalTo: gradView.centerXAnchor),
customView.centerYAnchor.constraint(equalTo: gradView.centerYAnchor),
])
gradView.endPoint = CGPoint(x: 1.0, y: 1.0)
gradView.colors = [
.red, .yellow, .cyan,
]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testLabel.isHidden.toggle()
}
}
Basic Gradient View
class BasicGradientView: UIView {
public var colors: [UIColor] = [.white, .black] { didSet { setNeedsLayout() } }
public var startPoint: CGPoint = CGPoint(x: 0.0, y: 0.0) { didSet { setNeedsLayout() } }
public var endPoint: CGPoint = CGPoint(x: 1.0, y: 0.0) { didSet { setNeedsLayout() } }
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
private var gLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
}
override func layoutSubviews() {
super.layoutSubviews()
gLayer.colors = colors.compactMap( {$0.cgColor })
gLayer.startPoint = startPoint
gLayer.endPoint = endPoint
}
}
This is the output -- tap anywhere to toggle the UILabel visibility:
Then add your imageView on top (or as a subview of the custom view):
Edit - to answer comment
We can get a shadow to show only on the outside by:
replacing the "fake-shadow shape layer" with a CALayer
using the bezier path as the layer's .shadowPath
creating a bezier path with a "hole" cut in it
use that path as a CAShapeLayer path
and then masking the shadow layer with that CAShapeLayer
Like this:
Here are updates to the above code as examples. Both classes are very similar, with the same custom properties that can be changed from their defaults. I've also added a UIImageView as a subview, to produce this output:
as before, tapping anywhere will toggle the UILabel visibility:
CustomViewA Class
class CustomViewA: UIView {
public var translucentColor: UIColor = .white.withAlphaComponent(0.5) { didSet { setNeedsLayout() } }
public var borderColor: UIColor = .init(red: 0.739, green: 0.828, blue: 0.922, alpha: 1.0) { didSet { setNeedsLayout() } }
public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
public var shadowColor: UIColor = .black.withAlphaComponent(0.3) { didSet { setNeedsLayout() } }
public var shadowOpacity: Float = 0.3
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 8.0) { didSet { setNeedsLayout() } }
// shadowRadius is not used, but this allows us to treat both CustomViewA and CustomViewB the same
public var shadowRadius: CGFloat = 0 { didSet { setNeedsLayout() } }
public var image: UIImage? {
didSet {
imageView.image = image
}
}
private let imageView = UIImageView()
private let shadowLayer = CAShapeLayer()
private let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(shadowLayer)
layer.addSublayer(topLayer)
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
// rounded-rect path for visible border
let pth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
// translucent rounded-rect bordered properties
topLayer.path = pth.cgPath
topLayer.fillColor = translucentColor.cgColor
topLayer.lineWidth = borderWidth
topLayer.strokeColor = borderColor.cgColor
// rounded-rect path for "shadow" border
r.size.height += shadowOffset.height
let spth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
shadowLayer.path = spth.cgPath
shadowLayer.fillColor = UIColor.clear.cgColor
shadowLayer.lineWidth = borderWidth
shadowLayer.strokeColor = shadowColor.cgColor
}
}
CustomViewB Class
class CustomViewB: UIView {
public var translucentColor: UIColor = .white.withAlphaComponent(0.5) { didSet { setNeedsLayout() } }
public var borderColor: UIColor = .init(red: 0.739, green: 0.828, blue: 0.922, alpha: 1.0) { didSet { setNeedsLayout() } }
public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
public var shadowOpacity: Float = 0.7
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 10.0) { didSet { setNeedsLayout() } }
public var shadowRadius: CGFloat = 6 { didSet { setNeedsLayout() } }
public var image: UIImage? {
didSet {
imageView.image = image
}
}
private let imageView = UIImageView()
private let shadowLayer = CALayer()
private let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(shadowLayer)
layer.addSublayer(topLayer)
// add a square (1:1) image view, 1/2 the width of self
// centered horizontally and vertically
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
// rounded-rect path for visible border
let pth = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
// translucent rounded-rect bordered properties
topLayer.path = pth.cgPath
topLayer.fillColor = translucentColor.cgColor
topLayer.lineWidth = borderWidth
topLayer.strokeColor = borderColor.cgColor
// we're going to mask the shadow layer with a "cutout" of the rounded rect
// the shadow is going to spread outside the bounds,
// so the "outer" path needs to be larger
// we'll make it plenty large enough
let bpth = UIBezierPath(rect: bounds.insetBy(dx: -bounds.width, dy: -bounds.height))
bpth.append(pth)
bpth.usesEvenOddFillRule = true
let maskLayer = CAShapeLayer()
maskLayer.fillRule = .evenOdd
maskLayer.path = bpth.cgPath
shadowLayer.mask = maskLayer
shadowLayer.shadowPath = pth.cgPath
shadowLayer.shadowOpacity = shadowOpacity
shadowLayer.shadowColor = shadowColor.cgColor
shadowLayer.shadowRadius = shadowRadius
shadowLayer.shadowOffset = shadowOffset
}
}
Example Controller Class - uses the BasicGradientView class above
class CustomViewTestVC: UIViewController {
let gradViewA = BasicGradientView()
let gradViewB = BasicGradientView()
let customViewA = CustomViewA()
let customViewB = CustomViewB()
// let's add a label between the gradient view and the custom view
// so we can confirm it's translucent
let testLabelA: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textAlignment = .center
v.textColor = .systemRed
v.font = .systemFont(ofSize: 32.0, weight: .regular)
return v
}()
let testLabelB: UILabel = {
let v = UILabel()
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[gradViewA, gradViewB, testLabelA, testLabelB, customViewA, customViewB].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradViewA.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
gradViewA.widthAnchor.constraint(equalToConstant: 300.0),
gradViewA.heightAnchor.constraint(equalTo: gradViewA.widthAnchor, multiplier: 1.0),
gradViewA.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testLabelA.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: -4.0),
testLabelA.heightAnchor.constraint(equalTo: gradViewA.heightAnchor, constant: 0.0),
testLabelA.centerXAnchor.constraint(equalTo: gradViewA.centerXAnchor),
testLabelA.centerYAnchor.constraint(equalTo: gradViewA.centerYAnchor),
customViewA.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: -84.0),
customViewA.heightAnchor.constraint(equalTo: gradViewA.heightAnchor, constant: -84.0),
customViewA.centerXAnchor.constraint(equalTo: gradViewA.centerXAnchor),
customViewA.centerYAnchor.constraint(equalTo: gradViewA.centerYAnchor),
gradViewB.topAnchor.constraint(equalTo: gradViewA.bottomAnchor, constant: 8.0),
gradViewB.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: 0.0),
gradViewB.heightAnchor.constraint(equalTo: gradViewB.widthAnchor, multiplier: 1.0),
gradViewB.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testLabelB.widthAnchor.constraint(equalTo: testLabelA.widthAnchor, constant: -0.0),
testLabelB.heightAnchor.constraint(equalTo: testLabelA.heightAnchor, constant: 0.0),
testLabelB.centerXAnchor.constraint(equalTo: gradViewB.centerXAnchor),
testLabelB.centerYAnchor.constraint(equalTo: gradViewB.centerYAnchor),
customViewB.widthAnchor.constraint(equalTo: customViewA.widthAnchor, constant: 0.0),
customViewB.heightAnchor.constraint(equalTo: customViewA.heightAnchor, constant: 0.0),
customViewB.centerXAnchor.constraint(equalTo: gradViewB.centerXAnchor),
customViewB.centerYAnchor.constraint(equalTo: gradViewB.centerYAnchor),
])
// let's setup the gradient views the same
gradViewA.colors = [
.init(red: 0.242, green: 0.591, blue: 0.959, alpha: 1.0),
.init(red: 0.113, green: 0.472, blue: 0.866, alpha: 1.0)
]
gradViewA.endPoint = CGPoint(x: 1.0, y: 1.0)
gradViewB.colors = gradViewA.colors
gradViewB.endPoint = gradViewA.endPoint
// let's give the two test labels the same properties
testLabelB.numberOfLines = testLabelA.numberOfLines
testLabelB.textAlignment = testLabelA.textAlignment
testLabelB.textColor = testLabelA.textColor
testLabelB.font = testLabelA.font
let s = "This is a test to confirm that the view and the \"shadow\" are both translucent while the border is opaque."
testLabelA.text = "CustomViewA\n" + s
testLabelB.text = "CustomViewB\n" + s
// set the .image property of both custom views
if let img = UIImage(named: "marker") {
customViewA.image = img
customViewB.image = img
} else {
if let img = UIImage(systemName: "mappin.and.ellipse")?.withTintColor(.white, renderingMode: .alwaysOriginal) {
customViewA.image = img
customViewB.image = img
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testLabelA.isHidden.toggle()
testLabelB.isHidden.toggle()
}
}

Related

How do you install a tableHeaderView on a UITableView?

After a decade, I suspected no one has actually asked this question directly. There are many questions asking how to fix a tableHeaderView layout problem caused by rotation for example. But the real question is, how did Apple intend for this to work?
Auto-layout does not seem to play ball with tableHeaderView, as you can see in this almost 9 year old post
Is it possible to use AutoLayout with UITableView's tableHeaderView?
I have been doing iOS development daily, since 2011 and I have never come across API so poorly documented.
Given that auto-layout is such a pickle to work with when installing a tableHeaderView, I decided last week to use the old school method of autoresizing masks. It has been 4 full days and it still isn't working for me. This is quite humbling and I wanted to reach out to you guys, to ask this simple question.
How do you install a tableHeaderView, properly, using autoresizing masks (no auto-layout) ?
My failed attempt
final class EventDetailTableHeaderView: UIView {
private let titleContainer: TitleContainerView
private let subtitleContainer: SubtitleContainerView
init(_ width: CGFloat, event: CloudEvent) {
let size = CGSize(width: width, height: 0)
let frame = CGRect(origin: .zero, size: size)
titleContainer = TitleContainerView(frame: frame, text: event.title)
subtitleContainer = SubtitleContainerView(frame: frame, text: event.displayString)
super.init(frame: frame)
backgroundColor = StyleKit.wDOWhite
autoresizingMask = [.flexibleWidth]
setupSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupSubviews() {
setupTitleContiner()
setupSubtitleContainer()
}
private func setupTitleContiner() {
addSubview(titleContainer)
titleContainer.autoresizingMask = [.flexibleWidth]
titleContainer.backgroundColor = StyleKit.wDOWhite
}
private func setupSubtitleContainer() {
addSubview(subtitleContainer)
subtitleContainer.autoresizingMask = [.flexibleWidth]
subtitleContainer.backgroundColor = StyleKit.wDOBlue
}
override func layoutSubviews() {
super.layoutSubviews()
positionSubtitleContainer()
frame = CGRect(
origin: .zero,
size: calculateSize()
)
}
private func positionSubtitleContainer() {
subtitleContainer.frame.origin.y = titleContainer.frame.height
}
private func calculateSize() -> CGSize {
CGSize(
width: frame.width,
height: calculateHeightOfSubviews()
)
}
private func calculateHeightOfSubviews() -> CGFloat {
let titleContainerHeight = titleContainer.frame.height
let subtitleContainerHeight = subtitleContainer.frame.height
return titleContainerHeight + subtitleContainerHeight
}
}
final class TitleContainerView: UIView {
private static let font = FontManagement.fontWithStyle(.heavy, withSize: 32.0)
private let label: UILabel = {
let label = UILabel()
label.autoresizingMask = [.flexibleWidth]
label.numberOfLines = 0
label.backgroundColor = StyleKit.wDOWhite
label.font = TitleContainerView.font
label.textColor = StyleKit.wDOBlue
return label
}()
convenience init(frame: CGRect, text: String) {
let font = TitleContainerView.font
let labelFrame = TitleContainerView.establishLabelFrame(frame, text, font)
var frame = frame
frame.size.height = TitleContainerView.establishHeight(labelFrame)
self.init(frame: frame)
label.text = text
label.frame = labelFrame
}
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(label)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private static let insets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
override func layoutSubviews() {
super.layoutSubviews()
let font = label.font!
let text = label.text ?? ""
label.frame = Self.establishLabelFrame(frame, text, font)
frame.size.height = Self.establishHeight(label.frame)
}
private static func establishLabelFrame(_ frame: CGRect, _ text: String, _ font: UIFont) -> CGRect {
let size = establishLabelSize(frame, text, font)
let origin = establishLabelOrigin(frame, size)
return CGRect(origin: origin, size: size)
}
private static func establishLabelSize(_ frame: CGRect, _ text: String, _ font: UIFont) -> CGSize {
let width = frame.width - TitleContainerView.insets.left - TitleContainerView.insets.right
let height = text.height(withConstrainedWidth: width, font: font)
return CGSize(
width: width,
height: height
)
}
private static func establishLabelOrigin(_ frame: CGRect, _ size: CGSize) -> CGPoint {
CGPoint(
x: (frame.width - size.width) / 2.0,
y: (frame.height - size.height) / 2.0
)
}
private static func establishHeight(_ labelFrame: CGRect) -> CGFloat {
labelFrame.size.height + TitleContainerView.insets.top + TitleContainerView.insets.bottom
}
}
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height)
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView = EventDetailTableView(frame: .zero, style: .plain)
tableView?.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView!)
let width = view.bounds.width
let tableHeaderView = EventDetailTableHeaderView(width, event: event)
tableHeaderView.layoutIfNeeded()
tableView?.tableHeaderView = tableHeaderView
NSLayoutConstraint.activate([
view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: tableView!.topAnchor),
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: tableView!.trailingAnchor),
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: tableView!.leadingAnchor),
view.bottomAnchor.constraint(equalTo: tableView!.bottomAnchor)
])
}
While I agree it seems like there would be a more straight-forward way of implementing an auto-height-sizing tableHeaderView, a common approach is to use auto-layout and an extension like this:
extension UITableView {
func sizeHeaderToFit() {
guard let headerView = tableHeaderView else { return }
let newHeight = headerView.systemLayoutSizeFitting(CGSize(width: frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
var frame = headerView.frame
// avoids infinite loop!
if newHeight.height != frame.height {
frame.size.height = newHeight.height
headerView.frame = frame
tableHeaderView = headerView
}
}
}
We call that from within viewDidLayoutSubviews():
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.sizeHeaderToFit()
}
Here is a complete example, which should come pretty close to your layout:
class TestViewController: UIViewController {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
let hView = EventDetailTableHeaderView(titleText: "Street Dance Championships", subTitleText: "4 June 2019 | 8:30 AM to 5:30 PM | Sports Wales National Centre | Cardiff")
tableView.tableHeaderView = hView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.sizeHeaderToFit()
}
}
extension TestViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
}
extension UITableView {
func sizeHeaderToFit() {
guard let headerView = tableHeaderView else { return }
let newHeight = headerView.systemLayoutSizeFitting(CGSize(width: frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
var frame = headerView.frame
// avoids infinite loop!
if newHeight.height != frame.height {
frame.size.height = newHeight.height
headerView.frame = frame
tableHeaderView = headerView
}
}
}
class TitleContainerView: UIView {
private static let font: UIFont = .systemFont(ofSize: 32, weight: .heavy)
let label: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textColor = UIColor(red: 0.044, green: 0.371, blue: 0.655, alpha: 1.0)
v.font = TitleContainerView.font
return v
}()
convenience init(text: String) {
self.init(frame: .zero)
label.text = text
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = UIColor(red: 0.93, green: 0.94, blue: 0.95, alpha: 1.0)
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
}
}
class SubtitleContainerView: UIView {
private static let font: UIFont = .systemFont(ofSize: 20, weight: .bold)
let label: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textColor = .white
v.font = SubtitleContainerView.font
return v
}()
convenience init(text: String) {
self.init(frame: .zero)
label.text = text
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = UIColor(red: 0.044, green: 0.371, blue: 0.655, alpha: 1.0)
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
}
}
class EventDetailTableHeaderView: UIView {
var titleView: TitleContainerView!
var subTitleView: SubtitleContainerView!
convenience init(titleText: String, subTitleText: String) {
self.init(frame: .zero)
titleView = TitleContainerView(text: titleText)
subTitleView = SubtitleContainerView(text: subTitleText)
commonInit()
}
func commonInit() -> Void {
titleView.translatesAutoresizingMaskIntoConstraints = false
subTitleView.translatesAutoresizingMaskIntoConstraints = false
addSubview(titleView)
addSubview(subTitleView)
// this avoids auto-layout complaints
let titleViewTrailingConstraint = titleView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
titleViewTrailingConstraint.priority = UILayoutPriority(rawValue: 999)
let subTitleViewBottomConstraint = subTitleView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
subTitleViewBottomConstraint.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
titleView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
titleView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
titleViewTrailingConstraint,
subTitleView.topAnchor.constraint(equalTo: titleView.bottomAnchor, constant: 0.0),
subTitleView.leadingAnchor.constraint(equalTo: titleView.leadingAnchor, constant: 0.0),
subTitleView.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: 0.0),
subTitleViewBottomConstraint,
])
}
}
and the output looks like this:
Edit -- same output, but using auto-layout only for adding the tableView to the main view.
Class names prefixed with RM_ (for Resizing Mask):
class RM_TestViewController: UIViewController {
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
let hView = RM_EventDetailTableHeaderView(titleText: "Street Dance Championships", subTitleText: "4 June 2019 | 8:30 AM to 5:30 PM | Sports Wales National Centre | Cardiff")
tableView.tableHeaderView = hView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.rm_sizeHeaderToFit()
}
}
extension RM_TestViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
}
extension UITableView {
func rm_sizeHeaderToFit() {
guard let headerView = tableHeaderView as? RM_EventDetailTableHeaderView else { return }
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
// avoids infinite loop!
if headerView.myHeight != headerView.frame.height {
headerView.frame.size.height = headerView.myHeight
tableHeaderView = headerView
}
}
}
class RM_TitleContainerView: UIView {
private static let font: UIFont = .systemFont(ofSize: 32, weight: .heavy)
let label: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textColor = UIColor(red: 0.044, green: 0.371, blue: 0.655, alpha: 1.0)
v.font = RM_TitleContainerView.font
// during dev, so we can see the label frame
//v.backgroundColor = .green
return v
}()
convenience init(text: String) {
self.init(frame: .zero)
label.text = text
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = UIColor(red: 0.93, green: 0.94, blue: 0.95, alpha: 1.0)
addSubview(label)
label.frame.origin = CGPoint(x: 8, y: 8)
}
override func layoutSubviews() {
super.layoutSubviews()
label.frame.size.width = bounds.width - 16
let sz = label.systemLayoutSizeFitting(CGSize(width: label.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
label.frame.size.height = sz.height
}
var myHeight: CGFloat {
get {
return label.frame.height + 16.0
}
}
}
class RM_SubtitleContainerView: UIView {
private static let font: UIFont = .systemFont(ofSize: 20, weight: .bold)
let label: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textColor = .white
v.font = RM_SubtitleContainerView.font
// during dev, so we can see the label frame
//v.backgroundColor = .systemYellow
return v
}()
convenience init(text: String) {
self.init(frame: .zero)
label.text = text
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = UIColor(red: 0.044, green: 0.371, blue: 0.655, alpha: 1.0)
addSubview(label)
label.frame.origin = CGPoint(x: 8, y: 8)
}
override func layoutSubviews() {
super.layoutSubviews()
label.frame.size.width = bounds.width - 16
let sz = label.systemLayoutSizeFitting(CGSize(width: label.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
label.frame.size.height = sz.height
}
var myHeight: CGFloat {
get {
return label.frame.height + 16.0
}
}
}
class RM_EventDetailTableHeaderView: UIView {
var titleView: RM_TitleContainerView!
var subTitleView: RM_SubtitleContainerView!
convenience init(titleText: String, subTitleText: String) {
self.init(frame: .zero)
titleView = RM_TitleContainerView(text: titleText)
subTitleView = RM_SubtitleContainerView(text: subTitleText)
commonInit()
}
func commonInit() -> Void {
addSubview(titleView)
addSubview(subTitleView)
// initial height doesn't matter
titleView.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: 8)
subTitleView.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: 8)
titleView.autoresizingMask = [.flexibleWidth]
subTitleView.autoresizingMask = [.flexibleWidth]
}
override func layoutSubviews() {
super.layoutSubviews()
// force subviews to update
titleView.setNeedsLayout()
subTitleView.setNeedsLayout()
titleView.layoutIfNeeded()
subTitleView.layoutIfNeeded()
// get subview heights
titleView.frame.size.height = titleView.myHeight
subTitleView.frame.origin.y = titleView.frame.maxY
subTitleView.frame.size.height = subTitleView.myHeight
}
var myHeight: CGFloat {
get {
return subTitleView.frame.maxY
}
}
}

Strange black corners when using draw(_ rect:) function

If I put the code for my UIBezierPath inside the draw(_ rect:) function I get these strange very thin black corners around the view and the tail. When dragging the view (e.g. within a presented view controller) these thin lines also start flickering. I assume it's a weird rendering bug. Does anyone know if there is a way to fix this?
class RenderingView: UIView {
lazy var backgroundView: UIView = {
let view = UIView()
view.layer.cornerRadius = 8
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var shadowView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var textLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Rendering Bug"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
backgroundColor = .clear
backgroundView.backgroundColor = .yellow
layer.borderWidth = 0
setupLayout()
}
private func setupLayout() {
[shadowView, backgroundView, textLabel].forEach(addSubview)
backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
backgroundView.topAnchor.constraint(equalTo: topAnchor).isActive = true
backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
shadowView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor).isActive = true
shadowView.topAnchor.constraint(equalTo: backgroundView.topAnchor).isActive = true
shadowView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor).isActive = true
shadowView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor).isActive = true
textLabel.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: 10).isActive = true
textLabel.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor, constant: -10).isActive = true
textLabel.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 10).isActive = true
textLabel.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -10).isActive = true
}
override func draw(_ rect: CGRect) {
shapeBackground()
}
private func shapeBackground() {
let tailLayer = CAShapeLayer()
let bezierPath = UIBezierPath(roundedRect: CGRect(x: backgroundView.bounds.minX,
y: backgroundView.bounds.minY,
width: backgroundView.bounds.width,
height: backgroundView.bounds.height - 12),
cornerRadius: 8)
let shadowBezierPath = UIBezierPath(roundedRect: CGRect(x: backgroundView.bounds.minX + 5,
y: backgroundView.bounds.minY + 10,
width: backgroundView.bounds.width - 10,
height: backgroundView.bounds.height - 12 - 10),
cornerRadius: 8)
[bezierPath, shadowBezierPath].forEach {
$0.move(to: CGPoint(x: backgroundView.bounds.midX - 12, y: backgroundView.bounds.maxY - 12))
$0.addLine(to: CGPoint(x: backgroundView.bounds.midX, y: backgroundView.bounds.maxY))
$0.addLine(to: CGPoint(x: backgroundView.bounds.midX + 12, y: backgroundView.bounds.maxY - 12))
$0.fill()
$0.close()
}
tailLayer.path = bezierPath.cgPath
tailLayer.fillColor = UIColor.white.cgColor
shadowView.layer.shadowPath = shadowBezierPath.cgPath
shadowView.layer.cornerRadius = 8
backgroundView.layer.masksToBounds = true
backgroundView.layer.mask = tailLayer
}
}
EDIT:
Turns out I had to use addClip() on the bezier paths to get rid of these corners
Not sure if this is what you are aiming for, but it looks flawless when you move the shapeBackground() method out of draw(_ rect:) and do some slight modifications.
I modified some of your drawing routines inside shapeBackground(), and moved the function into layoutSubviews() to calculate position of the tail from the frame made by constraints. I also added some variables for tailWidth, & tailHeight.
Like so:
class RenderingView: UIView {
lazy var backgroundView: UIView = {
let view = UIView()
view.layer.cornerRadius = 8
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var shadowView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var textLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "No Rendering Bug"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
backgroundColor = .clear
backgroundView.backgroundColor = .systemTeal
//backgroundView.frame.size = CGSize(width: 100, height: 100)
layer.borderWidth = 0
setupLayout()
}
private func setupLayout() {
[shadowView, backgroundView, textLabel].forEach(addSubview)
backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
backgroundView.topAnchor.constraint(equalTo: topAnchor).isActive = true
backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
shadowView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor).isActive = true
shadowView.topAnchor.constraint(equalTo: backgroundView.topAnchor).isActive = true
shadowView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor).isActive = true
shadowView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor).isActive = true
textLabel.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: 10).isActive = true
textLabel.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor, constant: -10).isActive = true
textLabel.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 10).isActive = true
textLabel.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -10).isActive = true
}
override func didMoveToWindow() {
super.didMoveToWindow()
}
override func layoutSubviews() {
super.layoutSubviews()
shapeBackground(color: UIColor.systemTeal)
}
override func draw(_ rect: CGRect) {
super.draw(rect)
}
private func shapeBackground(color: UIColor) {
let tailLayer = CAShapeLayer()
tailLayer.name = "tailLayer"
let tailWidth: CGFloat = 16
let tailHeight: CGFloat = 10
let bezierPath = UIBezierPath()
let shadowBezierPath = UIBezierPath()
[bezierPath, shadowBezierPath].forEach {
$0.move(to: CGPoint(x: 0, y: 0))
$0.addLine(to: CGPoint(x: tailWidth / 2, y: tailHeight))
$0.addLine(to: CGPoint(x: tailWidth, y: 0))
$0.fill()
$0.close()
}
tailLayer.path = bezierPath.cgPath
tailLayer.fillColor = color.cgColor
shadowView.layer.shadowPath = shadowBezierPath.cgPath
shadowView.layer.cornerRadius = 8
print(backgroundView.bounds.width)
tailLayer.frame = CGRect(x: (backgroundView.bounds.width - tailWidth) / 2,
y: backgroundView.bounds.maxY,
width: tailWidth,
height: tailHeight)
backgroundView.layer.masksToBounds = false
backgroundView.layer.addSublayer(tailLayer)
}
}

How to change color only a fraction of the total in swift?

I'm doing a QR code scan. I use UIView to change the background color of the screen to translucent color. However, I want to make the background color of the place that scans the QR code transparent. What should I do?
class QRcodeScannerViewController : UIViewController, AVCaptureMetadataOutputObjectsDelegate {
#IBOutlet weak var qrcodeView: UIView!
#IBOutlet weak var header: UINavigationBar!
#IBOutlet weak var flash: UIButton!
#IBOutlet weak var qrcodeScanArea: UIImageView!
var previewLayer: AVCaptureVideoPreviewLayer!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
self.qrcodeView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
view.layer.insertSublayer(previewLayer, at: 0)
view.bringSubviewToFront(flash)
view.bringSubviewToFront(header)
header.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
header.isTranslucent = true
header.backgroundColor = UIColor.clear
header.shadowImage = UIImage()
Current My QRcode ScanView
Here's the view I want:
I don't know which part I reverse, but only the color I want is black and the rest is transparent.
view.addSubview(qrcodeView)
let shape = CGRect(x: 0, y: 0, width: 200, height: 200)
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: shape, cornerRadius: 0).cgPath
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
qrcodeView.layer.mask = maskLayer
I made and applied the class by referring to the answers below. But it doesn't work for me.
let focusview = FocusView.init(frame: qrcodeView.frame)
focusview.areaBackground = UIColor.black
view.addSubview(focusview)
I did something similar ... actually pretty much the same thing, some time ago.
I've dug out the code I used and have posted it here for your reference
fileprivate let focusSize: CGSize = CGSize(width: 218, height: 150)
fileprivate let focusCornerRadius: CGFloat = 10.0
class FocusView: UIView {
var areaBackground: UIColor? {
set {
maskedFocusView.backgroundColor = newValue
}
get {
return maskedFocusView.backgroundColor
}
}
fileprivate let maskedFocusView: MaskedFocusView = {
return MaskedFocusView()
}()
let focusView: UIView = {
let view = UIView()
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 2
view.layer.cornerRadius = focusCornerRadius
// view.layer.shadowColor = UIColor.white.cgColor
// view.layer.shadowRadius = 5.0
// view.layer.shadowOpacity = 0.9
// view.layer.shadowOffset = CGSize.zero
view.layer.masksToBounds = true
return view
}()
override func awakeFromNib() {
super.awakeFromNib()
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() {
backgroundColor = UIColor.darkGray.withAlphaComponent(0.5)
translatesAutoresizingMaskIntoConstraints = false
addSubview(maskedFocusView)
addSubview(focusView)
setupFocusViewConstraints()
setupMaskedFocusViewConstraints()
}
func setupFocusViewConstraints() {
NSLayoutConstraint.activate(
focusView.centerXAnchor.constraint(equalTo: centerXAnchor),
focusView.centerYAnchor.constraint(equalTo: centerYAnchor)
)
let regularFocusViewConstraints = [
focusView.widthAnchor.constraint(equalToConstant: focusSize.width),
focusView.heightAnchor.constraint(equalToConstant: focusSize.height)
]
NSLayoutConstraint.activate(regularFocusViewConstraints)
}
func setupMaskedFocusViewConstraints() {
NSLayoutConstraint.activate(
maskedFocusView.centerXAnchor.constraint(equalTo: centerXAnchor),
maskedFocusView.centerYAnchor.constraint(equalTo: centerYAnchor),
maskedFocusView.topAnchor.constraint(equalTo: topAnchor),
maskedFocusView.bottomAnchor.constraint(equalTo: bottomAnchor),
maskedFocusView.leadingAnchor.constraint(equalTo: leadingAnchor),
maskedFocusView.trailingAnchor.constraint(equalTo: trailingAnchor)
)
}
}
// MARK: - Masked focus view
fileprivate class MaskedFocusView: UIView {
let maskLayer: CAShapeLayer = CAShapeLayer()
override func awakeFromNib() {
super.awakeFromNib()
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() {
backgroundColor = UIColor.darkGray.withAlphaComponent(0.5)
maskLayer.backgroundColor = UIColor.clear.cgColor
layer.mask = maskLayer
translatesAutoresizingMaskIntoConstraints = false
}
override func layoutSubviews() {
super.layoutSubviews()
let width = bounds.width
let height = bounds.height
let x = (width - focusSize.width) / 2
let y = (height - focusSize.height) / 2
let focusRect = CGRect(x: x, y: y, width: focusSize.width, height: focusSize.height)
let fullRect = CGRect(origin: bounds.origin, size: bounds.size)
let path = CGMutablePath()
path.addPath(UIBezierPath(rect: fullRect).cgPath)
path.addPath(UIBezierPath(roundedRect: focusRect, cornerRadius: focusCornerRadius).reversing().cgPath)
maskLayer.path = path
maskLayer.fillRule = .evenOdd
}
}
// MARK: - Layout constraints extension
extension NSLayoutConstraint {
/// A helper function to activate layout constraints.
static func activate(_ constraints: NSLayoutConstraint? ...) {
for case let constraint in constraints {
guard let constraint = constraint else {
continue
}
(constraint.firstItem as? UIView)?.translatesAutoresizingMaskIntoConstraints = false
constraint.isActive = true
}
}
}
extension Array where Element: NSLayoutConstraint {
func activate() {
forEach {
if !$0.isActive {
$0.isActive = true
}
}
}
func deactivate() {
forEach {
if $0.isActive {
$0.isActive = false
}
}
}
}

Swift - set cornerRadius and shadow at the same time

I'm trying to set a cornerRadius and a shadow(bottom + right) to my UICollectionViewCell but my code doesn't work. Neither the cornerRadius nor the shadow does appear..
Here's my code:
class MainWishlistCell: UICollectionViewCell {
let wishlistImage: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.image = UIImage(named: "logoGroß")
return v
}()
let wishlistLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.text = "Main Wishlist"
v.font = UIFont(name: "AvenirNext-DemiBold", size: 18)
v.textColor = .darkGray
v.textAlignment = .center
return v
}()
func addShadow() {
let cornerRadius: CGFloat = 5
self.wishlistImage.layer.shadowPath = UIBezierPath(roundedRect: self.wishlistImage.bounds, cornerRadius: cornerRadius).cgPath
self.wishlistImage.layer.shadowRadius = cornerRadius
self.wishlistImage.layer.shadowOffset = .zero
self.wishlistImage.layer.shadowOpacity = 0.3
self.wishlistImage.layer.shadowRadius = 10
self.wishlistImage.layer.cornerRadius = cornerRadius
self.wishlistImage.layer.shadowColor = UIColor.black.cgColor
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addShadow()
contentView.addSubview(wishlistImage)
contentView.addSubview(wishlistLabel)
// constrain view to all 4 sides
NSLayoutConstraint.activate([
wishlistImage.topAnchor.constraint(equalTo: contentView.topAnchor),
wishlistImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
wishlistImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
wishlistImage.heightAnchor.constraint(equalToConstant:150),
wishlistLabel.topAnchor.constraint(equalTo: wishlistImage.bottomAnchor,constant: 1),
wishlistLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
wishlistLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
wishlistLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
}
}
Just add one line of code into addShadow() function:
layer.masksToBounds = false ---> which set the shadow of the view's layer.
Then add corner radius to your cell:
contentview.clipsToBounds = true
The problem is at that point
addShadow()
contentView.addSubview(wishlistImage)
contentView.addSubview(wishlistLabel)
The image bounds is zero
self.wishlistImage.layer.shadowPath = UIBezierPath(roundedRect:
self.wishlistImage.bounds,
cornerRadius: cornerRadius).cgPath
So you need
var once = true
override func layoutSubviews() {
super.layoutSubviews()
if once {
addShadow()
once = false
}
}

How to change borders of UIPageControl dots?

Changing the colours is pretty straightforward, but is it possible to change the border of all unselected dots?
Ex:
dot.layer.borderWidth = 0.5
dot.layer.borderColor = UIColor.blackColor()
Yes This can be done..
Actually its pretty simple.
For iOS 14 Apple has introduced a great customization, where you can set custom images and even set background
let pageControl = UIPageControl()
pageControl.numberOfPages = 5
pageControl.backgroundStyle = .prominent
pageControl.preferredIndicatorImage = UIImage(systemName: "bookmark.fill")
pageControl.setIndicatorImage(UIImage(systemName: "heart.fill"), forPage: 0)
For prior to iOS 14:-
The Pagecontrol is composed of many Subviews which you can access. self.pageControl.subviews returns you [UIView] i.e array of UIView's.
After you get a single view you can add border to it , change its borderColor, change its border width, transform the dot size like scaling it.. All those properties that a UIView has can be used.
for index in 0..<array.count{ // your array.count
let viewDot = weakSelf?.pageControl.subviews[index]
viewDot?.layer.borderWidth = 0.5
viewDot?.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
if (index == indexPath.row){ // indexPath is the current indexPath of your selected cell or view in the collectionView i.e which needs to be highlighted
viewDot?.backgroundColor = UIColor.black
viewDot?.layer.borderColor = UIColor.black.cgColor
}
else{
viewDot?.backgroundColor = UIColor.white
viewDot?.layer.borderColor = UIColor.black.cgColor
}
}
and it looks like this
And remember you do not need to set weakSelf?.pageControl.currentPage = indexPath.row.Do let me know in case of any problem.. Hope this solves your problem.
All the best
iOS 14 allows setting indicator image with SFSymbol here's my subclassing of UIPageControl
class BorderedPageControl: UIPageControl {
var selectionColor: UIColor = .black
override var currentPage: Int {
didSet {
updateBorderColor()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
currentPageIndicatorTintColor = selectionColor
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func updateBorderColor() {
if #available(iOS 14.0, *) {
let smallConfiguration = UIImage.SymbolConfiguration(pointSize: 8.0, weight: .bold)
let circleFill = UIImage(systemName: "circle.fill", withConfiguration: smallConfiguration)
let circle = UIImage(systemName: "circle", withConfiguration: smallConfiguration)
for index in 0..<numberOfPages {
if index == currentPage {
setIndicatorImage(circleFill, forPage: index)
} else {
setIndicatorImage(circle, forPage: index)
}
}
pageIndicatorTintColor = selectionColor
} else {
subviews.enumerated().forEach { index, subview in
if index != currentPage {
subview.layer.borderColor = selectionColor.cgColor
subview.layer.borderWidth = 1
} else {
subview.layer.borderWidth = 0
}
}
}
}
}
Extension for set pagecontrol indicator border / Swift 3
extension UIImage {
class func outlinedEllipse(size: CGSize, color: UIColor, lineWidth: CGFloat = 1.0) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
context.setStrokeColor(color.cgColor)
context.setLineWidth(lineWidth)
let rect = CGRect(origin: .zero, size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)
context.addEllipse(in: rect)
context.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
USE:
let image = UIImage.outlinedEllipse(size: CGSize(width: 7.0, height: 7.0), color: .lightGray)
self.pageControl.pageIndicatorTintColor = UIColor.init(patternImage: image!)
self.pageControl.currentPageIndicatorTintColor = .lightGray
If anybody wants to CustomUIPageControl, then might need this
#IBDesignable
class CustomPageControl: UIView {
var dotsView = [RoundButton]()
var currentIndex = 0
#IBInspectable var circleColor: UIColor = UIColor.orange {
didSet {
updateView()
}
}
#IBInspectable var circleBackgroundColor: UIColor = UIColor.clear {
didSet {
updateView()
}
}
#IBInspectable var numberOfDots: Int = 7 {
didSet {
updateView()
}
}
#IBInspectable var borderWidthSize: CGFloat = 1 {
didSet {
updateView()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func updateView() -> Void {
for v in self.subviews{
v.removeFromSuperview()
}
dotsView.removeAll()
let stackView = UIStackView()
stackView.axis = NSLayoutConstraint.Axis.horizontal
stackView.distribution = UIStackView.Distribution.fillEqually
stackView.alignment = UIStackView.Alignment.center
stackView.spacing = 10
stackView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(stackView)
//Constraints
stackView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
NSLayoutConstraint.activate([
stackView.heightAnchor.constraint(equalToConstant: 20.0)
])
stackView.removeFullyAllArrangedSubviews()
for i in 0..<numberOfDots {
let button:RoundButton = RoundButton(frame: CGRect.zero)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.heightAnchor.constraint(equalToConstant: 10.0),
button.widthAnchor.constraint(equalToConstant: 10.0),
])
button.tag = i
button.layer.borderWidth = 1
// button.backgroundColor = circleBackgroundColor
// button.layer.borderWidth = borderWidthSize
// button.layer.borderColor = circleColor.cgColor
button.addTarget(self, action:#selector(self.buttonClicked), for: .touchUpInside)
stackView.addArrangedSubview(button)
dotsView.append(button)
}
}
func updateCurrentDots(borderColor : UIColor, backColor : UIColor, index : Int){
for button in dotsView{
if button == dotsView[index]{
button.backgroundColor = backColor
button.layer.borderColor = borderColor.cgColor
}else{
button.backgroundColor = .clear
button.layer.borderColor = borderColor.cgColor
}
}
}
#objc func buttonClicked() {
print("Button Clicked")
}
class RoundButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
}
override func layoutSubviews() {
self.layer.cornerRadius = self.frame.size.width / 2
}
}
extension UIStackView {
func removeFully(view: UIView) {
removeArrangedSubview(view)
view.removeFromSuperview()
}
func removeFullyAllArrangedSubviews() {
arrangedSubviews.forEach { (view) in
removeFully(view: view)
}
}
}
You can use either Programmatically or Stoaryboard
To update the current dots calls this function
self.pageControl.updateCurrentDots(borderColor: .white, backColor: .white, index:1)

Resources