How do you install a tableHeaderView on a UITableView? - ios

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
}
}
}

Related

Removing Shadow Underneath Semi-Transparent UIView

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()
}
}

UIKit-Swift custom UIButton does not trigger action on tap

I have defined a "RadioButton like" UIButton. To achieve this I have added a subview inside a UIButton that I change its color on .touchUpInside event.
The problem is that actions is not being triggered.
Here is the code of my RadioButton:
final class RadioButton: UIButton {
let stateView = UIView()
var isActive: Bool = false {
didSet {
if isActive == true {
stateView.backgroundColor = #colorLiteral(red: 0, green: 0.5839999914, blue: 0.5289999843, alpha: 1)
} else {
stateView.backgroundColor = .clear
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
convenience init() {
self.init(frame: .zero)
configure()
}
private func configureColorSubview() {
stateView.backgroundColor = .white
stateView.isUserInteractionEnabled = false
stateView.layer.cornerRadius = 10
stateView.translatesAutoresizingMaskIntoConstraints = false
stateView.widthAnchor.constraint(equalToConstant: 20).isActive = true
stateView.heightAnchor.constraint(equalToConstant: 20).isActive = true
stateView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
stateView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
}
private func configure() {
isUserInteractionEnabled = true
addSubview(stateView)
configureColorSubview()
stateView.layer.zPosition = -1
sendSubviewToBack(stateView)
layer.cornerRadius = 20
layer.borderWidth = 2
layer.borderColor = UIColor.darkGray.cgColor
translatesAutoresizingMaskIntoConstraints = false
widthAnchor.constraint(equalToConstant: 40).isActive = true
heightAnchor.constraint(equalToConstant: 40).isActive = true
}
}
This is the code of the view where a RadioButton is wrapped with a label:
final class RadioButtonLabelView: UILabel {
let radioBtn: RadioButton = {
let r = RadioButton()
return r
}()
let label: UILabel = {
let label = UILabel()
let text = "Text"
let attributedText = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 22, weight: .semibold), NSAttributedString.Key.foregroundColor: UIColor.black])
label.attributedText = attributedText
return label
}()
let stack: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.distribution = .fill
stack.spacing = 20
return stack
}()
override init(frame: CGRect) {
super.init(frame: frame)
}
convenience init(text: String) {
self.init(frame: .zero)
setupView(text: text)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView(text: String) {
addSubview(stack)
stack.addArrangedSubview(radioBtn)
stack.addArrangedSubview(label)
label.text = text
stack.anchor(topAnchor: topAnchor, trailingAnchor: trailingAnchor, bottomAnchor: bottomAnchor, leadingAnchor: leadingAnchor)
}
}
And here is the final modal view code:
final class SelectHourModalView: UIViewController {
lazy var buttonToOptionsConstraint: NSLayoutConstraint = button.topAnchor.constraint(greaterThanOrEqualTo: radioButtons.bottomAnchor, constant: 50)
lazy var buttonToPicker: NSLayoutConstraint = button.topAnchor.constraint(greaterThanOrEqualTo: datePicker.bottomAnchor, constant: 50)
var isShowPicker: Bool = false {
didSet {
if isShowPicker {
showPicker()
} else {
hidePicker()
}
}
}
let radioButtons: RadioButtonOptionsView = {
let rb = RadioButtonOptionsView()
return rb
}()
let datePicker: UIDatePicker = {
let date = UIDatePicker()
date.datePickerMode = .dateAndTime
date.setValue(UIColor.black, forKeyPath: "textColor")
return date
}()
let button: IoTaxiBtn = {
let button = IoTaxiBtn(text: "Seleccionar", color: #colorLiteral(red: 0, green: 0.5839999914, blue: 0.5289999843, alpha: 1), insets: UIEdgeInsets(top: 10, left: 0, bottom: 8, right: 0), font: UIFont.systemFont(ofSize: 25, weight: .bold), corner: 3)
button.shadow(color: UIColor.gray.cgColor, opacity: 1, offset: CGSize(width: .zero, height: 2), radius: 5)
button.isUserInteractionEnabled = false
return button
}()
private lazy var rbCollection: [RadioButtonLabelView] = {
let rbc = [radioButtons.btnLabelNow, radioButtons.btnLabel1, radioButtons.btnLabel20, radioButtons.btnLabel30, radioButtons.btnLabelCustom]
return rbc
}()
override func viewDidLoad() {
super.viewDidLoad()
setupFront()
setActions()
initialViewConfiguration()
}
private func setupFront() {
view.backgroundColor = .white
[radioButtons, datePicker, button].forEach {
view.addSubview($0)
}
radioButtons.anchor(topAnchor: view.topAnchor, trailingAnchor: view.trailingAnchor, bottomAnchor: nil, leadingAnchor: view.leadingAnchor, padding: .init(top: 15, left: 20, bottom: .zero, right: 20))
datePicker.anchor(topAnchor: radioButtons.bottomAnchor, trailingAnchor: view.trailingAnchor, bottomAnchor: nil, leadingAnchor: view.leadingAnchor, padding: .init(top: 20, left: 20, bottom: .zero, right: 20), size: .init(width: .zero, height: 190))
button.anchor(topAnchor: nil, trailingAnchor: view.trailingAnchor, bottomAnchor: nil, leadingAnchor: view.leadingAnchor, padding: .init(top: .zero, left: 20, bottom: .zero, right: 20))
buttonToPicker.isActive = true
buttonToOptionsConstraint.isActive = false
}
private func setActions() {
rbCollection.forEach {
$0.radioBtn.addTarget(self, action: #selector(setHour), for: .touchUpInside)
}
print(radioButtons.btnLabelCustom.radioBtn.actions(forTarget: self, forControlEvent: .touchUpInside) ?? ["Nada"])
}
#objc private func setHour(_ sender: RadioButton) {
print("tap")
rbCollection.forEach {
$0.radioBtn.isActive = false
}
sender.isActive.toggle()
}
}
//MARK: - Constraints management
//TODO: Animations
private extension SelectHourModalView {
func initialViewConfiguration() {
radioButtons.btnLabelCustom.radioBtn.isActive = true
isShowPicker = true
}
func showPicker() {
buttonToOptionsConstraint.isActive = false
buttonToPicker.isActive = true
datePicker.isHidden = false
}
func hidePicker() {
buttonToPicker.isActive = true
buttonToOptionsConstraint.isActive = true
datePicker.isHidden = true
}
}
final class RadioButtonOptionsView: UIView {
let btnLabelNow: RadioButtonLabelView = {
let btnLabel = RadioButtonLabelView(text: "Recógeme ahora")
return btnLabel
}()
let btnLabel20: RadioButtonLabelView = {
let btnLabel = RadioButtonLabelView(text: "Recógeme en 20 minutos")
return btnLabel
}()
let btnLabel30: RadioButtonLabelView = {
let btnLabel = RadioButtonLabelView(text: "Recógeme en 30 minutos")
return btnLabel
}()
let btnLabel1: RadioButtonLabelView = {
let btnLabel = RadioButtonLabelView(text: "Recógeme en una hora")
return btnLabel
}()
let btnLabelCustom: RadioButtonLabelView = {
let btnLabel = RadioButtonLabelView(text: "Hora personalizada")
return btnLabel
}()
let stackContainer: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.distribution = .fill
stack.spacing = 8
return stack
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
addSubview(stackContainer)
[btnLabelNow, btnLabel20, btnLabel30, btnLabel1, btnLabelCustom].forEach {
stackContainer.addArrangedSubview($0)
}
stackContainer.anchor(topAnchor: topAnchor, trailingAnchor: trailingAnchor, bottomAnchor: bottomAnchor, leadingAnchor: leadingAnchor)
}
}
This is the UI debugger:
What is the problem? The stack view has the same height as the RadioButton and the stateView inside RadioButton has -1 zIndex and is no user interactable.
Inherit RadioButtonLabelView with UIView instead of UILabel will resolve your issue
final class RadioButtonLabelView: UIView {
//....
}

Transparent gradient not working in UIView Class

I'm trying to add a transparent gradient to UIView in UIView Class but it doesn't work.
class RecipesDetailsView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
layoutUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var containerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = .white
let gradientMaskLayer = CAGradientLayer()
gradientMaskLayer.frame = containerView.bounds
gradientMaskLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor]
gradientMaskLayer.locations = [0, 1]
containerView.layer.mask = gradientMaskLayer
containerView.fadeView(style: .bottom, percentage: 0.5)
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
lazy var startCookingButton: UIButton = {
let startCookingButton = UIButton(type: .system)
startCookingButton.setTitle("Start cooking", for: .normal)
startCookingButton.setTitleColor(.white, for: .normal)
startCookingButton.backgroundColor = .CustomGreen()
startCookingButton.layer.cornerRadius = 8.0
startCookingButton.translatesAutoresizingMaskIntoConstraints = false
startCookingButton.titleLabel?.font = UIFont(name: "AvenirNext-Bold", size: 14)
return startCookingButton
}()
lazy var saveButton: UIButton = {
let saveButton = UIButton(type: .system)
saveButton.setTitleColor(.customDarkGray(), for: .normal)
saveButton.setTitle("Save", for: .normal)
saveButton.setImage(UIImage(systemName: "heart"), for: .normal)
saveButton.imageEdgeInsets = UIEdgeInsets(top: 0,left: -5,bottom: 0,right: 0)
saveButton.titleEdgeInsets = UIEdgeInsets(top: 0,left: 0,bottom: 0,right: -5)
saveButton.titleLabel?.font = UIFont(name: "AvenirNext-Bold", size: 14)
saveButton.tintColor = .customDarkGray()
saveButton.backgroundColor = .clear
saveButton.translatesAutoresizingMaskIntoConstraints = false
return saveButton
}()
func setupContainerViewConstraints() {
NSLayoutConstraint.activate([
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
containerView.heightAnchor.constraint(equalToConstant: frame.width / 5)
])
}
func setupStartCookingButton() {
NSLayoutConstraint.activate([
startCookingButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
startCookingButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -32),
startCookingButton.heightAnchor.constraint(equalToConstant: 55),
startCookingButton.widthAnchor.constraint(equalToConstant: frame.width * (2.5/4))
])
}
func setupSaveButtonConstraints() {
NSLayoutConstraint.activate([
saveButton.centerYAnchor.constraint(equalTo: startCookingButton.centerYAnchor),
saveButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
saveButton.heightAnchor.constraint(equalTo: startCookingButton.heightAnchor),
saveButton.widthAnchor.constraint(equalToConstant: frame.width * (1.2/4))
])
}
func addSubviews() {
addSubview(containerView)
containerView.addSubview(startCookingButton)
containerView.addSubview(saveButton)
}
func layoutUI() {
addSubviews()
setupContainerViewConstraints()
setupStartCookingButton()
setupSaveButtonConstraints()
}
}
What I want to get:
What I get from my code:
Layers do not "auto-size" with their views, so you need to keep that gradient layer as a property and update its frame when the view layout changes.
Add this property:
private var gradientMaskLayer: CAGradientLayer!
then, in lazy var containerView: UIView = change:
let gradientMaskLayer = CAGradientLayer()
to:
gradientMaskLayer = CAGradientLayer()
then, add this func:
override func layoutSubviews() {
super.layoutSubviews()
gradientMaskLayer.frame = bounds
}
Edit
However, that will apply the gradient mask to containerView AND its subviews (the buttons), which is probably not what you want.
So, change your addSubviews() func to:
func addSubviews() {
addSubview(containerView)
// add buttons to self, not to containerView
//containerView.addSubview(startCookingButton)
//containerView.addSubview(saveButton)
addSubview(startCookingButton)
addSubview(saveButton)
}
Edit 2
Here is a complete implementation, with the view controller's background set to red:
class TestViewController: UIViewController {
var rv: RecipesDetailsView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// with the way you are setting up the layout,
// we need to add the view here where we know the
// frame has been setup
if rv == nil {
let w = view.frame.width
let h = w / 5.0 * 2.0
let t = view.frame.height - h
rv = RecipesDetailsView(frame: CGRect(x: 0.0, y: t, width: w, height: h))
view.addSubview(rv)
}
}
}
class RecipesDetailsView: UIView {
// add this var / property
private var gradientMaskLayer: CAGradientLayer!
override init(frame: CGRect) {
super.init(frame: frame)
layoutUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
// layers do not follow frame changes, so update here
gradientMaskLayer.frame = bounds
}
lazy var containerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = .white
gradientMaskLayer = CAGradientLayer()
gradientMaskLayer.frame = containerView.bounds
gradientMaskLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor]
gradientMaskLayer.locations = [0, 1]
containerView.layer.mask = gradientMaskLayer
//containerView.fadeView(style: .bottom, percentage: 0.5)
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
lazy var startCookingButton: UIButton = {
let startCookingButton = UIButton(type: .system)
startCookingButton.setTitle("Start cooking", for: .normal)
startCookingButton.setTitleColor(.white, for: .normal)
//startCookingButton.backgroundColor = .CustomGreen()
startCookingButton.backgroundColor = .systemGreen
startCookingButton.layer.cornerRadius = 8.0
startCookingButton.translatesAutoresizingMaskIntoConstraints = false
startCookingButton.titleLabel?.font = UIFont(name: "AvenirNext-Bold", size: 14)
return startCookingButton
}()
lazy var saveButton: UIButton = {
let saveButton = UIButton(type: .system)
//saveButton.setTitleColor(.customDarkGray(), for: .normal)
saveButton.setTitleColor(.darkGray, for: .normal)
saveButton.setTitle("Save", for: .normal)
saveButton.setImage(UIImage(systemName: "heart"), for: .normal)
saveButton.imageEdgeInsets = UIEdgeInsets(top: 0,left: -5,bottom: 0,right: 0)
saveButton.titleEdgeInsets = UIEdgeInsets(top: 0,left: 0,bottom: 0,right: -5)
saveButton.titleLabel?.font = UIFont(name: "AvenirNext-Bold", size: 14)
//saveButton.tintColor = .customDarkGray()
saveButton.tintColor = .darkGray
saveButton.backgroundColor = .clear
saveButton.translatesAutoresizingMaskIntoConstraints = false
return saveButton
}()
func setupContainerViewConstraints() {
NSLayoutConstraint.activate([
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
containerView.heightAnchor.constraint(equalToConstant: frame.width / 5)
])
}
func setupStartCookingButton() {
NSLayoutConstraint.activate([
startCookingButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
startCookingButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -32),
startCookingButton.heightAnchor.constraint(equalToConstant: 55),
startCookingButton.widthAnchor.constraint(equalToConstant: frame.width * (2.5/4))
])
}
func setupSaveButtonConstraints() {
NSLayoutConstraint.activate([
saveButton.centerYAnchor.constraint(equalTo: startCookingButton.centerYAnchor),
saveButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
saveButton.heightAnchor.constraint(equalTo: startCookingButton.heightAnchor),
saveButton.widthAnchor.constraint(equalToConstant: frame.width * (1.2/4))
])
}
func addSubviews() {
addSubview(containerView)
// add buttons to self, not to containerView
//containerView.addSubview(startCookingButton)
//containerView.addSubview(saveButton)
addSubview(startCookingButton)
addSubview(saveButton)
}
func layoutUI() {
addSubviews()
setupContainerViewConstraints()
setupStartCookingButton()
setupSaveButtonConstraints()
}
}
Result:

Multilinelabel inside multiple stackviews inside UITableViewCell

I have view hierarchy like below;
UITableViewCell ->
-> UIView -> UIStackView (axis: vertical, distribution: fill)
-> UIStackView (axis: horizontal, alignment: top, distribution: fillEqually)
-> UIView -> UIStackView(axis:vertical, distribution: fill)
-> TwoLabelView
My problem is that labels don't get more than one line. I read every question in SO and also tried every possibility but none of them worked. On below screenshot, on the top left box, there should be two pair of label but even one of them isn't showing.
My Question is that how can I achieve multiline in the first box (both for left and right)?
If I change top stack views distribution to fillProportionally, labels get multiline but there will be a gap between last element of first box and the box itself
My first top stack views
//This is the Stackview used just below UITableViewCell
private let stackView: UIStackView = {
let s = UIStackView()
s.distribution = .fill
s.axis = .vertical
s.spacing = 10
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
//This is used to create two horizontal box next to each other
private let myStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fillEqually
s.spacing = 10
s.axis = .horizontal
//s.alignment = .center
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
UILabel Class:
fileprivate class FixAutoLabel: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
}
}
#IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 0.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 0.0
#IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
#IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
fileprivate var firstLabel: FixAutoLabel!
fileprivate var secondLabel: FixAutoLabel!
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
func setUpView() {
firstLabel = FixAutoLabel()
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
firstLabel.numberOfLines = 0
firstLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
secondLabel = FixAutoLabel()
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
secondLabel.numberOfLines = 1
secondLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
addSubview(firstLabel)
addSubview(secondLabel)
// we're going to set the constraints
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// pin both labels' left-edges to left-edge of self
firstLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
secondLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
// pin both labels' right-edges to right-edge of self
firstLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
secondLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
// pin firstLabel to the top of self + topMargin (padding)
firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin).isActive = true
// pin top of secondLabel to bottom of firstLabel + verticalSpacing
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing).isActive = true
// pin bottom of self to bottom of secondLabel + bottomMargin (padding)
bottomAnchor.constraint(equalTo: secondLabel.bottomAnchor, constant: bottomMargin).isActive = true
// call common "refresh" func
updateView()
}
func updateView() {
firstLabel.preferredMaxLayoutWidth = self.bounds.width
secondLabel.preferredMaxLayoutWidth = self.bounds.width
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
firstLabel.sizeToFit()
secondLabel.sizeToFit()
setNeedsUpdateConstraints()
}
override open var intrinsicContentSize : CGSize {
// just has to have SOME intrinsic content size defined
// this will be overridden by the constraints
return CGSize(width: 1, height: 1)
}
}
UIView -> UIStackView class
class ViewWithStack: UIView {
let verticalStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fillEqually
s.spacing = 10
s.axis = .vertical
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.white
self.layer.cornerRadius = 6.0
self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)
addSubview(verticalStackView)
let lessThan = verticalStackView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: 0)
lessThan.priority = UILayoutPriority(1000)
lessThan.isActive = true
verticalStackView.leftAnchor.constraint(equalTo: self.leftAnchor,constant: 0).isActive = true
verticalStackView.rightAnchor.constraint(equalTo: self.rightAnchor,constant: 0).isActive = true
verticalStackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
verticalStackView.isLayoutMarginsRelativeArrangement = true
}
convenience init(orientation: NSLayoutConstraint.Axis,labelsArray: [UIView]) {
self.init()
verticalStackView.axis = orientation
for label in labelsArray {
verticalStackView.addArrangedSubview(label)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Example Controller Class (This is a minimized version of the whole project):
class ViewController: UIViewController, UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let viewWithStack = BoxView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.delegate = self
tableView.dataSource = self
tableView.register(TableViewCell.self, forCellReuseIdentifier: "myCell")
tableView.rowHeight = UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "myCell") as! TableViewCell
if (indexPath.row == 0) {
cell.setup(viewWithStack: self.viewWithStack)
} else {
cell.backgroundColor = UIColor.black
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//return 500
if ( indexPath.row == 0) {
return UITableView.automaticDimension
} else {
return 40
}
}
}
EDIT I created a minimal project then I found that my problem is that my project implements heightForRow function which overrides UITableViewAutomaticDimension so that It gives wrong height for my component. I think I should look how to get height size of the component? because I can't delete heightForRow function which solves my problem.
Example Project Link https://github.com/emreond/tableviewWithStackView/tree/master/tableViewWithStackViewEx
Example project has ambitious layouts when you open view debugger. I think when I fix them, everything should be fine.
Here is a full example that should do what you want (this is what I mean by a minimal reproducible example):
Best way to examine this is to:
create a new project
create a new file, named TestTableViewController.swift
copy and paste the code below into that file (replace the default template code)
add a UITableViewController to the Storyboard
assign its Custom Class to TestTableViewController
embed it in a UINavigationController
set the UINavigationController as Is Initial View Controller
run the app
This is what you should see as the result:
I based the classes on what you had posted (removed unnecessary code, and I am assuming you have the other cells working as desired).
//
// TestTableViewController.swift
//
// Created by Don Mag on 10/21/19.
//
import UIKit
class SideBySideCell: UITableViewCell {
let horizStackView: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 10
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForReuse() {
horizStackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
}
func commonInit() -> Void {
contentView.backgroundColor = UIColor(white: 0.8, alpha: 1.0)
contentView.addSubview(horizStackView)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
horizStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
horizStackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
horizStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
horizStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
])
}
func addViewWithStack(_ v: ViewWithStack) -> Void {
horizStackView.addArrangedSubview(v)
}
}
class TestTableViewController: UITableViewController {
let sideBySideReuseID = "sbsID"
override func viewDidLoad() {
super.viewDidLoad()
// register custom SideBySide cell for reuse
tableView.register(SideBySideCell.self, forCellReuseIdentifier: sideBySideReuseID)
tableView.separatorStyle = .none
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell
let twoLabelView1 = TwoLabelView()
twoLabelView1.firstLabelText = "Text for first label on left-side."
twoLabelView1.secondLabelText = "10.765,00TL"
let twoLabelView2 = TwoLabelView()
twoLabelView2.firstLabelText = "Text for second-first label on left-side."
twoLabelView2.secondLabelText = "10.765,00TL"
let twoLabelView3 = TwoLabelView()
twoLabelView3.firstLabelText = "Text for the first label on right-side."
twoLabelView3.secondLabelText = "10.765,00TL"
let leftStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView1, twoLabelView2])
let rightStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView3])
cell.addViewWithStack(leftStackV)
cell.addViewWithStack(rightStackV)
return cell
}
// create ViewWithStack using just a simple label
let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell
let v = UILabel()
v.text = "This is row \(indexPath.row)"
let aStackV = ViewWithStack(orientation: .vertical, labelsArray: [v])
cell.addViewWithStack(aStackV)
return cell
}
}
#IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 0.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 0.0
#IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
#IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
fileprivate var firstLabel: UILabel = {
let v = UILabel()
return v
}()
fileprivate var secondLabel: UILabel = {
let v = UILabel()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
func setUpView() {
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
firstLabel.numberOfLines = 0
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
secondLabel.numberOfLines = 1
addSubview(firstLabel)
addSubview(secondLabel)
// we're going to set the constraints
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// Note: recommended to use Leading / Trailing rather than Left / Right
NSLayoutConstraint.activate([
// pin both labels' left-edges to left-edge of self
firstLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
secondLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
// pin both labels' right-edges to right-edge of self
firstLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
secondLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// pin firstLabel to the top of self + topMargin (padding)
firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin),
// pin top of secondLabel to bottom of firstLabel + verticalSpacing
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing),
// pin bottom of self to >= (bottom of secondLabel + bottomMargin (padding))
bottomAnchor.constraint(greaterThanOrEqualTo: secondLabel.bottomAnchor, constant: bottomMargin),
])
}
func updateView() -> Void {
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
}
}
class ViewWithStack: UIView {
let verticalStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fill
s.spacing = 10
s.axis = .vertical
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.white
self.layer.cornerRadius = 6.0
// self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)
addSubview(verticalStackView)
NSLayoutConstraint.activate([
// constrain to all 4 sides
verticalStackView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
verticalStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
verticalStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
verticalStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
])
verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
verticalStackView.isLayoutMarginsRelativeArrangement = true
}
convenience init(orientation: NSLayoutConstraint.Axis, labelsArray: [UIView]) {
self.init()
verticalStackView.axis = orientation
for label in labelsArray {
verticalStackView.addArrangedSubview(label)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How to properly ensure a custom UITableViewCell can be reused

I have a UITableViewController that is rendering out a custom UITableViewCell'.
This cells are related to chat messages, as such the config is almost identical, apart from how the elements are constrained.
bot cell is: avatar > message
user cell is message < avatar
I was hoping to combine these in a single custom cell and simply switch on an origin property on the model, allowing me to choose which constraints I am applying.
This worked for 5 or 6 messages, until however I ran a test with 30 messages and some cells started to inherit both sets of anchors or even just random properties that should be assigned to the other cell.
I can see the errors suggest the constraints are invalid and I believe this is due to the cell not being prepared for reuse correctly.
(
"<NSLayoutConstraint:0x600002533930 UIImageView:0x7fb401514d40.leading == UILayoutGuide:0x600003f18e00'UIViewLayoutMarginsGuide'.leading (active)>",
"<NSLayoutConstraint:0x600002526990 UITextView:0x7fb40200a200'I am a Person.'.leading == UILayoutGuide:0x600003f18e00'UIViewLayoutMarginsGuide'.leading + 15 (active)>",
"<NSLayoutConstraint:0x6000025271b0 UITextView:0x7fb40200a200'I am a Person.'.trailing == UIImageView:0x7fb401514d40.leading - 15 (active)>"
)
ChatMessageCell
class ChatMessageCell: UITableViewCell {
fileprivate var content: ChatMessage? {
didSet {
guard let text = content?.text else { return }
messageView.text = text
guard let origin = content?.origin else { return }
setupSubViews(origin)
}
}
fileprivate var messageAvatar: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.layer.cornerRadius = 35 / 2
imageView.layer.masksToBounds = true
return imageView
}()
fileprivate var messageView: UITextView = {
let textView = UITextView()
textView.isScrollEnabled = false
textView.isSelectable = false
textView.sizeToFit()
textView.layoutIfNeeded()
textView.contentInset = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
textView.layer.cornerRadius = 10
textView.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setContent(as content: ChatMessage) {
self.content = content
}
override func prepareForReuse() {
content = nil
}
}
extension ChatMessageCell {
fileprivate func setupSubViews(_ origin: ChatMessageOrigin) {
let margins = contentView.layoutMarginsGuide
[messageAvatar, messageView].forEach { v in contentView.addSubview(v) }
switch origin {
case .system:
messageAvatar.image = #imageLiteral(resourceName: "large-bot-head")
messageAvatar.anchor(
top: margins.topAnchor, leading: margins.leadingAnchor, size: CGSize(width: 35, height: 35)
)
messageView.anchor(
top: margins.topAnchor, leading: messageAvatar.trailingAnchor, bottom: margins.bottomAnchor, trailing: margins.trailingAnchor,
padding: UIEdgeInsets(top: 5, left: 15, bottom: 0, right: 15)
)
case .user:
let userContentBG = UIColor.hexStringToUIColor(hex: "00f5ff")
messageAvatar.image = UIImage.from(color: userContentBG)
messageAvatar.anchor(
top: margins.topAnchor, trailing: margins.trailingAnchor, size: CGSize(width: 35, height: 35)
)
messageView.layer.backgroundColor = userContentBG.cgColor
messageView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]
messageView.anchor(
top: margins.topAnchor, leading: margins.leadingAnchor, bottom: margins.bottomAnchor, trailing: messageAvatar.leadingAnchor,
padding: UIEdgeInsets(top: 5, left: 15, bottom: 0, right: 15)
)
}
}
}
ChatController
class ChatController: UITableViewController {
lazy var viewModel: ChatViewModel = {
let viewModel = ChatViewModel()
return viewModel
}()
fileprivate let headerView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .white
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.reloadData = { [weak self] in
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
configureViewHeader()
configureTableView()
registerTableCells()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.contentInset = UIEdgeInsets(top: 85, left: 0, bottom: 0, right: 0)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.history.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = viewModel.history[indexPath.row]
let cell = tableView.dequeueReusableCell(withClass: ChatMessageCell.self)
cell.setContent(as: item)
cell.layoutSubviews()
return cell
}
}
extension ChatController {
fileprivate func configureViewHeader() {
let margins = view.safeAreaLayoutGuide
view.addSubview(headerView)
headerView.anchor(
top: margins.topAnchor, leading: margins.leadingAnchor, trailing: margins.trailingAnchor,
size: CGSize(width: 0, height: 70)
)
}
fileprivate func configureTableView() {
tableView.tableFooterView = UIView()
tableView.allowsSelection = false
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 200
tableView.separatorStyle = .none
tableView.backgroundColor = UIColor.clear
}
fileprivate func registerTableCells() {
tableView.register(cellWithClass: ChatMessageCell.self)
}
}
An example of how the view changes on scroll can be seen here....
My Extensions are applied with
#discardableResult
func anchor(top: NSLayoutYAxisAnchor? = nil, leading: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, trailing: NSLayoutXAxisAnchor? = nil, padding: UIEdgeInsets = .zero, size: CGSize = .zero) -> AnchoredConstraints {
translatesAutoresizingMaskIntoConstraints = false
var anchoredConstraints = AnchoredConstraints()
if let top = top {
anchoredConstraints.top = topAnchor.constraint(equalTo: top, constant: padding.top)
}
if let leading = leading {
anchoredConstraints.leading = leadingAnchor.constraint(equalTo: leading, constant: padding.left)
}
if let bottom = bottom {
anchoredConstraints.bottom = bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom)
}
if let trailing = trailing {
anchoredConstraints.trailing = trailingAnchor.constraint(equalTo: trailing, constant: -padding.right)
}
if size.width != 0 {
anchoredConstraints.width = widthAnchor.constraint(equalToConstant: size.width)
}
if size.height != 0 {
anchoredConstraints.height = heightAnchor.constraint(equalToConstant: size.height)
}
[anchoredConstraints.top, anchoredConstraints.leading, anchoredConstraints.bottom, anchoredConstraints.trailing, anchoredConstraints.width, anchoredConstraints.height].forEach { $0?.isActive = true }
return anchoredConstraints
}
In your ChatMessageCell class, move:
[messageAvatar, messageView].forEach { v in contentView.addSubview(v) }
from setupSubViews(...) to init(...). With your current code, setupSubViews is being called every time you set the content. You only want to add the subviews to the cell's contentView when the cell is initialized.
From there, you need to check how you're adding constraints. If your .anchor(...) func / extension is first removing any existing constraints, you should be ok.
Edit:
Here is another option - you may find it easier to work with.
Since you have the same subviews, set up two arrays of constraints. Then activate or deactivate the appropriate set (as well as setting colors, corners, etc):
class ChatMessageCell: UITableViewCell {
fileprivate var content: ChatMessage? {
didSet {
guard let text = content?.text else { return }
messageView.text = text
guard let origin = content?.origin else { return }
setupSubViews(origin)
}
}
fileprivate var messageAvatar: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.layer.cornerRadius = 35 / 2
imageView.layer.masksToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
fileprivate var messageView: UITextView = {
let textView = UITextView()
textView.isScrollEnabled = false
textView.isSelectable = false
textView.sizeToFit()
textView.layoutIfNeeded()
textView.contentInset = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
textView.layer.cornerRadius = 10
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
fileprivate var systemConstraints = [NSLayoutConstraint]()
fileprivate var userConstraints = [NSLayoutConstraint]()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func setContent(as content: ChatMessage) {
self.content = content
}
func commonInit() -> Void {
backgroundColor = .clear
let margins = contentView.layoutMarginsGuide
[messageAvatar, messageView].forEach { v in contentView.addSubview(v) }
systemConstraints = [
messageAvatar.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0.0),
messageView.leadingAnchor.constraint(equalTo: messageAvatar.trailingAnchor, constant: 15.0),
messageView.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: -15),
]
userConstraints = [
messageView.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 15.0),
messageAvatar.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 0.0),
messageAvatar.leadingAnchor.constraint(equalTo: messageView.trailingAnchor, constant: 15),
]
NSLayoutConstraint.activate([
// messageAvatar width/height/top is the same for each origin "type"
messageAvatar.topAnchor.constraint(equalTo: margins.topAnchor, constant: 0.0),
messageAvatar.heightAnchor.constraint(equalToConstant: 35),
messageAvatar.widthAnchor.constraint(equalToConstant: 35),
// messageView width/height/top is the same for each origin "type"
messageView.topAnchor.constraint(equalTo: margins.topAnchor, constant: 5.0),
messageView.bottomAnchor.constraint(equalTo: margins.bottomAnchor, constant: 0.0),
])
}
}
extension ChatMessageCell {
fileprivate func setupSubViews(_ origin: ChatMessageOrigin) {
switch origin {
case .system:
NSLayoutConstraint.deactivate(userConstraints)
NSLayoutConstraint.activate(systemConstraints)
messageView.backgroundColor = .white
messageAvatar.backgroundColor = .red
messageView.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]
default:
NSLayoutConstraint.deactivate(systemConstraints)
NSLayoutConstraint.activate(userConstraints)
messageView.backgroundColor = .cyan
messageAvatar.backgroundColor = .blue
messageView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]
}
}
}
Note: I'm using Swift 4.1, so there are a couple of syntax changes (but they'll be obvious).
When you have two different layouts of cells, having two different classes of cells would be another way to handle your issue.
ChatMessageCell
class ChatMessageCell: UITableViewCell {
fileprivate var content: ChatMessage? {
didSet {
guard let text = content?.text else { return }
messageView.text = text
}
}
//...
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor.clear
setupSubViews()
}
fileprivate func setupSubViews() {
[messageAvatar, messageView].forEach { v in contentView.addSubview(v) }
}
//...
}
class UserMessageCell: ChatMessageCell {
fileprivate override func setupSubViews() {
super.setupSubViews()
let margins = contentView.layoutMarginsGuide
let userContentBG = UIColor.hexStringToUIColor(hex: "00f5ff")
messageAvatar.image = UIImage.from(color: userContentBG)
messageAvatar.anchor(
top: margins.topAnchor, trailing: margins.trailingAnchor, size: CGSize(width: 35, height: 35)
)
messageView.layer.backgroundColor = userContentBG.cgColor
messageView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]
messageView.anchor(
top: margins.topAnchor, leading: margins.leadingAnchor, bottom: margins.bottomAnchor, trailing: messageAvatar.leadingAnchor,
padding: UIEdgeInsets(top: 5, left: 15, bottom: 0, right: 15)
)
}
}
class SystemMessageCell: ChatMessageCell {
fileprivate override func setupSubViews() {
super.setupSubViews()
let margins = contentView.layoutMarginsGuide
messageAvatar.image = #imageLiteral(resourceName: "large-bot-head")
messageAvatar.anchor(
top: margins.topAnchor, leading: margins.leadingAnchor, size: CGSize(width: 35, height: 35)
)
messageView.anchor(
top: margins.topAnchor, leading: messageAvatar.trailingAnchor, bottom: margins.bottomAnchor, trailing: margins.trailingAnchor,
padding: UIEdgeInsets(top: 5, left: 15, bottom: 0, right: 15)
)
}
}
ChatController
class ChatController: UITableViewController {
//...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = viewModel.history[indexPath.row]
let cell: ChatMessageCell
switch item.origin {
case .system:
cell = tableView.dequeueReusableCell(withClass: SystemMessageCell.self)
case .user:
cell = tableView.dequeueReusableCell(withClass: UserMessageCell.self)
}
cell.setContent(as: item)
cell.layoutSubviews()
return cell
}
}
extension ChatController {
//...
fileprivate func registerTableCells() {
tableView.register(cellWithClass: SystemMessageCell.self)
tableView.register(cellWithClass: UserMessageCell.self)
}
}

Resources