I want to create a "fake" spacing around my UITableViewCell and I'm doing so by inset-ing the contentView's frame by 10 each [I actually am adding a custom view on top of the contentView and inset it by 10]. It looks like the contentView is the only visible view. This looks really well and I'm also setting and adjusting the frame of selectedBackgroundView for my cell so that selecting will only select the "visible" area.
Now the issue of doing so is the following:
If I select a cell, it flashes with UIColor.darkGray as specified by my selectedBackgroundView.
Then for a short period of time within the animation my cell background is invisible entirely before it flashes back to how it was.
This way the animation does not look fluent.
This applies to the content view:
Background Color
darkGray (selectedBackgroundView)
Clear Color
Background Color
Does anybody know if I can fix this behaviour while keeping selection to be a thing?
I created a gif out of the animation: https://imgur.com/vkfA62w
Here is my code:
class BasicTableViewCell : UITableViewCell {
public var basicContentView: UIView = UIView(frame: .zero)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
self.tintColor = UIColor.white
self.contentView.backgroundColor = UIColor.clear
self.basicContentView.backgroundColor = UIColor.barTintColor
self.basicContentView.layer.masksToBounds = false
self.basicContentView.layer.cornerRadius = 10.0
self.basicContentView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.basicContentView)
self.basicContentView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 10.0).isActive = true
self.basicContentView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -10.0).isActive = true
self.basicContentView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 10.0).isActive = true
let bottomConstraint = self.basicContentView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -10.0)
bottomConstraint.priority = UILayoutPriority(999)
bottomConstraint.isActive = true
let selectedView: UIView = UIView(frame: .zero)
selectedView.backgroundColor = UIColor.darkGray
selectedView.layer.cornerRadius = 10.0
selectedView.layer.masksToBounds = false
self.selectedBackgroundView = selectedView
}
override func layoutSubviews() {
super.layoutSubviews()
// only for selectedBackgroundView, contentView raised other issues
let contentViewFrame = self.contentView.frame
let insetContentViewFrame = contentViewFrame.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))
self.selectedBackgroundView?.frame = insetContentViewFrame
}
}
I already know what the issue is, based on: Apples explanation
selectedBackgroundView is added to contentView and then fades away before being removed from the contentView and then basicContentView is suddenly visible again which causes the bug.
Here is my fix
override func setSelected(_ selected: Bool, animated: Bool) {
if selected {
self.basicSelectedBackgroundView.alpha = 1.0
self.basicContentView.insertSubview(self.basicSelectedBackgroundView, at: 0)
} else {
guard self.basicSelectedBackgroundView.superview != nil else {
return
}
if animated {
UIView.animate(withDuration: 0.5, delay: 0.0, options: .allowUserInteraction, animations: {
self.basicSelectedBackgroundView.alpha = 0.0
}) { (finished: Bool) in
if finished {
self.basicSelectedBackgroundView.removeFromSuperview()
}
}
} else {
self.basicSelectedBackgroundView.alpha = 0.0
self.basicSelectedBackgroundView.removeFromSuperview()
}
}
}
I am overriding setSelected and animate it myself.
You probably want to override setSelected in your cell. For example:
override func setSelected(_ selected: Bool, animated: Bool) {
//If we don't get the contentView's backgroundColor here and then reset it after the call to super.setSelected, the contentView's backgroundColor will "disappear" when the cell is selected
let contentViewColor = contentView.backgroundColor
super.setSelected(selected, animated: animated)
contentView.backgroundColor = contentViewColor
}
When a cell is selected, the backgroundColor properties for ALL subviews of the cell are set to the selection color, which overwrites any backgroundColor properties you have set yourself. So you might need to manually set the backgroundColor of your contentView here to get the behaviour you want.
Related
I recently started with iOS development, and I'm currently working on an existing iOS Swift app with the intention of adding additional functionality. The current view contains a custom header and footer view, and the idea is for me to add the new slider with discrete steps in between, which worked. However, now I would also like to add labels to describe the discrete UISlider, for example having "Min" and "Max" to the left and right respectively, as well as the value of current value of the slider:
To achieve this, I was thinking to define a UITableView and a custom cell where I would insert the slider, while the labels could be defined in a row above or below the slider row. In my recent attempt I tried to define the table view and simply add the same slider element to a row, but I'm unsure how to proceed.
In addition, there is no Storyboard, everything has to be done programatically. Here is the sample code for my current version:
Slider and slider view definition:
private var sliderView = UIView()
private var discreteSlider = UISlider()
private let step: Float = 1 // for UISlider to snap in steps
Table view definition:
// temporary table view rows. For testing the table view
private let myArray: NSArray = ["firstRow", "secondRow"]
private lazy var tableView: UITableView = {
let displayWidth: CGFloat = self.view.frame.width
let displayHeight: CGFloat = self.view.frame.height / 3
let yPos = headerHeight
myTableView = UITableView(frame: CGRect(x: 0, y: yPos, width: displayWidth, height: displayHeight))
myTableView.backgroundColor = .clear
myTableView.register(UITableViewCell.self, forCellReuseIdentifier: "MyCell")
myTableView.dataSource = self
myTableView.delegate = self
return myTableView
}()
Loading the views:
private func setUpView() {
// define slider
discreteSlider = UISlider(frame:CGRect(x: 0, y: 0, width: 250, height: 20))
// define slider properties
discreteSlider.center = self.view.center
discreteSlider.minimumValue = 1
discreteSlider.maximumValue = 5
discreteSlider.isContinuous = true
discreteSlider.tintColor = UIColor.purple
// add behavior
discreteSlider.addTarget(self, action: #selector(self.sliderValueDidChange(_:)), for: .valueChanged)
sliderView.addSubviews(discreteSlider) // add the slider to its view
UIView.animate(withDuration: 0.8) {
self.discreteSlider.setValue(2.0, animated: true)
}
//////
// Add the slider, labels to table rows here
// Add the table view to the main view
view.addSubviews(headerView, tableView, footerView)
//////
//current version without the table
//view.addSubviews(headerView, sliderView, footerView)
headerView.title = "View Title". // header configuration
}
Class extension for the table view:
extension MyViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Num: \(indexPath.row)")
print("Value: \(myArray[indexPath.row])")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath as IndexPath)
cell.textLabel!.text = "\(myArray[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
Furthermore, if there is a better solution that the UITableView approach, I would be willing to try. I also started to look over UICollectionView. Thanks!
While you could put these elements in different rows / cells of a table view, that's not what table views are designed for and there is a much better approach.
Create a UIView subclass and use auto-layout constraints to position the elements:
We use a horizontal UIStackView for the "step" labels... Distribution is set to .equalSpacing and we constrain the labels to all be equal widths.
We constrain the slider above the stack view, constraining its Leading and Trailing to the centerX of the first and last step labels (with +/- offsets for the width of the thumb).
We constrain the centerX of the Min and Max labels to the centerX of the first and last step labels.
Here is an example:
class MySliderView: UIView {
private var discreteSlider = UISlider()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let minVal: Int = 1
let maxVal: Int = 5
// slider properties
discreteSlider.minimumValue = Float(minVal)
discreteSlider.maximumValue = Float(maxVal)
discreteSlider.isContinuous = true
discreteSlider.tintColor = UIColor.purple
let stepStack = UIStackView()
stepStack.distribution = .equalSpacing
for i in minVal...maxVal {
let v = UILabel()
v.text = "\(i)"
v.textAlignment = .center
v.textColor = .systemRed
stepStack.addArrangedSubview(v)
}
// references to first and last step label
guard let firstLabel = stepStack.arrangedSubviews.first,
let lastLabel = stepStack.arrangedSubviews.last
else {
// this will never happen, but we want to
// properly unwrap the labels
return
}
// make all step labels the same width
stepStack.arrangedSubviews.dropFirst().forEach { v in
v.widthAnchor.constraint(equalTo: firstLabel.widthAnchor).isActive = true
}
let minLabel = UILabel()
minLabel.text = "Min"
minLabel.textAlignment = .center
minLabel.textColor = .systemRed
let maxLabel = UILabel()
maxLabel.text = "Max"
maxLabel.textAlignment = .center
maxLabel.textColor = .systemRed
// add the labels and the slider to self
[minLabel, maxLabel, discreteSlider, stepStack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
// now we setup the layout
NSLayoutConstraint.activate([
// start with the step labels stackView
// we'll give it 40-pts leading and trailing "padding"
stepStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 40.0),
stepStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -40.0),
// and 20-pts from the bottom
stepStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),
// now constrain the slider leading and trailing to the
// horizontal center of first and last step labels
// accounting for width of thumb (assuming a default UISlider)
discreteSlider.leadingAnchor.constraint(equalTo: firstLabel.centerXAnchor, constant: -14.0),
discreteSlider.trailingAnchor.constraint(equalTo: lastLabel.centerXAnchor, constant: 14.0),
// and 20-pts above the steps stackView
discreteSlider.bottomAnchor.constraint(equalTo: stepStack.topAnchor, constant: -20.0),
// constrain Min and Max labels centered to first and last step labels
minLabel.centerXAnchor.constraint(equalTo: firstLabel.centerXAnchor, constant: 0.0),
maxLabel.centerXAnchor.constraint(equalTo: lastLabel.centerXAnchor, constant: 0.0),
// and 20-pts above the steps slider
minLabel.bottomAnchor.constraint(equalTo: discreteSlider.topAnchor, constant: -20.0),
maxLabel.bottomAnchor.constraint(equalTo: discreteSlider.topAnchor, constant: -20.0),
// and 20-pts top "padding"
minLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
])
// add behavior
discreteSlider.addTarget(self, action: #selector(self.sliderValueDidChange(_:)), for: .valueChanged)
discreteSlider.addTarget(self, action: #selector(self.sliderThumbReleased(_:)), for: .touchUpInside)
}
// so we can set the slider value from the controller
public func setSliderValue(_ val: Float) -> Void {
discreteSlider.setValue(val, animated: true)
}
#objc func sliderValueDidChange(_ sender: UISlider) -> Void {
print("Slider dragging value:", sender.value)
}
#objc func sliderThumbReleased(_ sender: UISlider) -> Void {
// "snap" to discreet step position
sender.setValue(Float(lroundf(sender.value)), animated: true)
print("Slider dragging end value:", sender.value)
}
}
and it ends up looking like this:
Note that the target action for the slider value change is contained inside our custom class.
So, we need to provide functionality so our class can inform the controller when the slider value has changed.
The best way to do that is with closures...
We'll define the closures at the top of our MySliderView class:
class MySliderView: UIView {
// this closure will be used to inform the controller that
// the slider value changed
var sliderDraggingClosure: ((Float)->())?
var sliderReleasedClosure: ((Float)->())?
then in our slider action funcs, we can use that closure to "call back" to the controller:
#objc func sliderValueDidChange(_ sender: UISlider) -> Void {
// tell the controller
sliderDraggingClosure?(sender.value)
}
#objc func sliderThumbReleased(_ sender: UISlider) -> Void {
// "snap" to discreet step position
sender.setValue(Float(lroundf(sender.value)), animated: true)
// tell the controller
sliderReleasedClosure?(sender.value)
}
and then in our view controller's viewDidLoad() func, we setup the closures:
// set the slider closures
mySliderView.sliderDraggingClosure = { [weak self] val in
print("Slider dragging value:", val)
// make sure self is still valid
guard let self = self else {
return
}
// do something because the slider changed
// self.someFunc()
}
mySliderView.sliderReleasedClosure = { [weak self] val in
print("Slider dragging end value:", val)
// make sure self is still valid
guard let self = self else {
return
}
// do something because the slider changed
// self.someFunc()
}
Here's the complete modified class (Edited to include Tap behavior):
class MySliderView: UIView {
// this closure will be used to inform the controller that
// the slider value changed
var sliderDraggingClosure: ((Float)->())?
var sliderReleasedClosure: ((Float)->())?
private var discreteSlider = UISlider()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let minVal: Int = 1
let maxVal: Int = 5
// slider properties
discreteSlider.minimumValue = Float(minVal)
discreteSlider.maximumValue = Float(maxVal)
discreteSlider.isContinuous = true
discreteSlider.tintColor = UIColor.purple
let stepStack = UIStackView()
stepStack.distribution = .equalSpacing
for i in minVal...maxVal {
let v = UILabel()
v.text = "\(i)"
v.textAlignment = .center
v.textColor = .systemRed
stepStack.addArrangedSubview(v)
}
// references to first and last step label
guard let firstLabel = stepStack.arrangedSubviews.first,
let lastLabel = stepStack.arrangedSubviews.last
else {
// this will never happen, but we want to
// properly unwrap the labels
return
}
// make all step labels the same width
stepStack.arrangedSubviews.dropFirst().forEach { v in
v.widthAnchor.constraint(equalTo: firstLabel.widthAnchor).isActive = true
}
let minLabel = UILabel()
minLabel.text = "Min"
minLabel.textAlignment = .center
minLabel.textColor = .systemRed
let maxLabel = UILabel()
maxLabel.text = "Max"
maxLabel.textAlignment = .center
maxLabel.textColor = .systemRed
// add the labels and the slider to self
[minLabel, maxLabel, discreteSlider, stepStack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
// now we setup the layout
NSLayoutConstraint.activate([
// start with the step labels stackView
// we'll give it 40-pts leading and trailing "padding"
stepStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 40.0),
stepStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -40.0),
// and 20-pts from the bottom
stepStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20.0),
// now constrain the slider leading and trailing to the
// horizontal center of first and last step labels
// accounting for width of thumb (assuming a default UISlider)
discreteSlider.leadingAnchor.constraint(equalTo: firstLabel.centerXAnchor, constant: -14.0),
discreteSlider.trailingAnchor.constraint(equalTo: lastLabel.centerXAnchor, constant: 14.0),
// and 20-pts above the steps stackView
discreteSlider.bottomAnchor.constraint(equalTo: stepStack.topAnchor, constant: -20.0),
// constrain Min and Max labels centered to first and last step labels
minLabel.centerXAnchor.constraint(equalTo: firstLabel.centerXAnchor, constant: 0.0),
maxLabel.centerXAnchor.constraint(equalTo: lastLabel.centerXAnchor, constant: 0.0),
// and 20-pts above the steps slider
minLabel.bottomAnchor.constraint(equalTo: discreteSlider.topAnchor, constant: -20.0),
maxLabel.bottomAnchor.constraint(equalTo: discreteSlider.topAnchor, constant: -20.0),
// and 20-pts top "padding"
minLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20.0),
])
// add behavior
discreteSlider.addTarget(self, action: #selector(self.sliderValueDidChange(_:)), for: .valueChanged)
discreteSlider.addTarget(self, action: #selector(self.sliderThumbReleased(_:)), for: .touchUpInside)
// add tap gesture so user can either
// Drag the Thumb or
// Tap the slider bar
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sliderTapped))
discreteSlider.addGestureRecognizer(tapGestureRecognizer)
}
// so we can set the slider value from the controller
public func setSliderValue(_ val: Float) -> Void {
discreteSlider.setValue(val, animated: true)
}
#objc func sliderValueDidChange(_ sender: UISlider) -> Void {
// tell the controller
sliderDraggingClosure?(sender.value)
}
#objc func sliderThumbReleased(_ sender: UISlider) -> Void {
// "snap" to discreet step position
sender.setValue(Float(sender.value.rounded()), animated: true)
// tell the controller
sliderReleasedClosure?(sender.value)
}
#objc func sliderTapped(_ gesture: UITapGestureRecognizer) {
guard gesture.state == .ended else { return }
guard let slider = gesture.view as? UISlider else { return }
// get tapped point
let pt: CGPoint = gesture.location(in: slider)
let widthOfSlider: CGFloat = slider.bounds.size.width
// calculate tapped point as percentage of width
let pct = pt.x / widthOfSlider
// convert to min/max value range
let pctRange = pct * CGFloat(slider.maximumValue - slider.minimumValue) + CGFloat(slider.minimumValue)
// "snap" to discreet step position
let newValue = Float(pctRange.rounded())
slider.setValue(newValue, animated: true)
// tell the controller
sliderReleasedClosure?(newValue)
}
}
along with an example view controller:
class SliderTestViewController: UIViewController {
let mySliderView = MySliderView()
override func viewDidLoad() {
super.viewDidLoad()
mySliderView.translatesAutoresizingMaskIntoConstraints = false
mySliderView.backgroundColor = .darkGray
view.addSubview(mySliderView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's put our custom slider view
// 40-pts from the top with
// 8-pts leading and trailing
mySliderView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
mySliderView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
mySliderView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
// we don't need Bottom or Height constraints, because our custom view's content
// will determine its Height
])
// set the slider closures
mySliderView.sliderDraggingClosure = { [weak self] val in
print("Slider dragging value:", val)
// make sure self is still valid
guard let self = self else {
return
}
// do something because the slider changed
// self.someFunc()
}
mySliderView.sliderReleasedClosure = { [weak self] val in
print("Slider dragging end value:", val)
// make sure self is still valid
guard let self = self else {
return
}
// do something because the slider changed
// self.someFunc()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// start the slider at 4
UIView.animate(withDuration: 0.8) {
self.mySliderView.setSliderValue(4)
}
}
}
Edit 2
If you want to make the slider "tappable area" larger, use a subclassed UISlider and override point(inside, ...).
Example 1 - expand tap area 10-pts on each side, 15-pts top and bottom:
class ExpandedTouchSlider: UISlider {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// expand tap area 10-pts on each side, 15-pts top and bottom
let bounds: CGRect = self.bounds.insetBy(dx: -10.0, dy: -15.0)
return bounds.contains(point)
}
}
Example 2 - expand tap area vertically to superview height:
class ExpandedTouchSlider: UISlider {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var bounds: CGRect = self.bounds
if let sv = superview {
// expand tap area vertically to superview height
let svRect = sv.bounds
let f = self.frame
bounds.origin.y -= f.origin.y
bounds.size.height = svRect.height
}
return bounds.contains(point)
}
}
Example 3 - expand tap area both horizontally and vertically to include entire superview:
class ExpandedTouchSlider: UISlider {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var bounds: CGRect = self.bounds
if let sv = superview {
// expand tap area both horizontally and vertically
// to include entire superview
let svRect = sv.bounds
let f = self.frame
bounds.origin.x -= f.origin.x
bounds.origin.y -= f.origin.y
bounds.size.width = svRect.width
bounds.size.height = svRect.height
}
return bounds.contains(point)
}
}
Note that if you're expanding the tap area horizontally (so the user can tap off the left/right ends of the slider), you'll also want to make sure your percentage / value calculation does not produce a value lower than the min, or higher than the max.
I have a table view with cells.
Overlaying shadows is done, but that looks not like I wanted.
My shadow white round rectangles should stay white. And shadows should overlay below white rectangles. Any suggestions on how to achieve expected behavior?
I added shadow as a separate subview
class ShadowView: UIView {
override var bounds: CGRect {
didSet {
setupShadow()
}
}
private func setupShadow() {
layer.shadowColor = UIColor.red.cgColor
layer.shadowOpacity = 1
layer.shadowRadius = 40
layer.shadowOffset = CGSize(width: 1, height: 10)
layer.masksToBounds = false
}
override func layoutSubviews() {
super.layoutSubviews()
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 5).cgPath
}
}
and then
let shadowView = ShadowView()
addSubview(shadowView)
I wanted something like this. White rectangles are completely white.
The problem, as you are seeing, is that rows (cells) are separate views. If you allow an element to extend outside the cell, it will either overlap or underlap the adjacent views.
Here's a simple example to clarify...
Each cell has a systemYellow view that extends outside its frame on the top and bottom:
If we use Debug View Hierarchy to inspect the layout, it looks something like this:
As we can see, because of the initial z-order, each cell is covering the part of the systemYellow view that is extending up and the part that is extending down overlaps the next cell.
As we scroll a bit, cells are re-drawn at different z-order positions (based on how the tableView re-uses them):
Now we see that some of the systemYellow views overlap the row above, some overlap the row below, and some overlap both.
Inspecting the layout shows us the cells' z-order positions:
If we want to maintain the z-order so that none of the systemYellow views overlap the cell below it, we can add a func to manipulate the z-order positions:
func updateLayout() -> Void {
for c in tableView.visibleCells {
tableView.bringSubviewToFront(c)
}
}
and we need to call that whenever the tableView scrolls (and when the layout changes):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateLayout()
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateLayout()
}
So, the same thing is happening with your layout... the shadows are extending outside the frame of the cell, and either over- or under-lapping the adjacent cells.
If we start by using the same approach to manage the z-order of the cells, we can get this:
So, we're keeping the white rounded-rect views on top of the "shadow above." Of course, now we have the shadows overlapping the bottom of the view.
We can change the rectangle for the .shadowPath to avoid that:
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
r.origin.y += 40
layer.shadowPath = UIBezierPath(roundedRect: r, cornerRadius: 5).cgPath
}
and we get this output:
One more issue though -- if we use the default cell .selectionStyle, we get this:
which is probably not acceptable.
So, we can set the .selectionStyle to .none, and implement setSelected in our cell class. Here, I change the rounded-rect background and the text colors to make it extremely obvious:
Here is some example code -- no #IBOutlet or #IBAction connections needed, so just assign the class of a new table view controller to ShadowTableViewController :
class ShadowView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.shadowColor = UIColor.red.cgColor
layer.shadowOpacity = 1
layer.shadowRadius = 40
layer.masksToBounds = false
layer.cornerRadius = 12
layer.shouldRasterize = true
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
r.origin.y += 40
layer.shadowPath = UIBezierPath(roundedRect: r, cornerRadius: 5).cgPath
}
}
class ShadowCell: UITableViewCell {
let shadowView = ShadowView()
let topLabel = UILabel()
let bottomLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shadowView.backgroundColor = .white
topLabel.font = .boldSystemFont(ofSize: 24.0)
bottomLabel.font = .italicSystemFont(ofSize: 20.0)
bottomLabel.numberOfLines = 0
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 8
stack.addArrangedSubview(topLabel)
stack.addArrangedSubview(bottomLabel)
shadowView.translatesAutoresizingMaskIntoConstraints = false
stack.translatesAutoresizingMaskIntoConstraints = false
shadowView.addSubview(stack)
contentView.addSubview(shadowView)
let mg = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
shadowView.topAnchor.constraint(equalTo: mg.topAnchor),
shadowView.leadingAnchor.constraint(equalTo: mg.leadingAnchor),
shadowView.trailingAnchor.constraint(equalTo: mg.trailingAnchor),
shadowView.bottomAnchor.constraint(equalTo: mg.bottomAnchor),
stack.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: 12.0),
stack.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: 12.0),
stack.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -12.0),
stack.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -12.0),
])
contentView.clipsToBounds = false
self.clipsToBounds = false
self.backgroundColor = .clear
selectionStyle = .none
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
shadowView.backgroundColor = selected ? .systemBlue : .white
topLabel.textColor = selected ? .white : .black
bottomLabel.textColor = selected ? .white : .black
}
}
class ShadowTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.separatorStyle = .none
tableView.register(ShadowCell.self, forCellReuseIdentifier: "shadowCell")
}
func updateLayout() -> Void {
for c in tableView.visibleCells {
tableView.bringSubviewToFront(c)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateLayout()
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateLayout()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "shadowCell", for: indexPath) as! ShadowCell
c.topLabel.text = "Row: \(indexPath.row)"
var s = "Description for row \(indexPath.row)"
if indexPath.row % 3 == 1 {
s += "\nSecond Line"
}
if indexPath.row % 3 == 2 {
s += "\nSecond Line\nThirdLine"
}
c.bottomLabel.text = s
return c
}
}
Note: this is Example Code Only and should not be considered Production Ready.
I am using a UIView.transition to flip a card over.
While troubleshooting it not working, I stumbled upon a way to make it work - but I have no idea why. I am hoping that someone can look at the two code blocks below and help me understand why one works and the other doesn't. It seems very strange to me.
First, here is the code block that actually works. The card flip is visually flawless.
UIView.animate(withDuration: 0.01) {
imageView.alpha = 1.0
imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded()
} completion: { (true) in
UIView.transition(with: imageView, duration: 1.2, options: animation) {
imageView.image = endingImage
imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded()
} completion: { (true) in
if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 }
UIView.animate(withDuration: 0.01) {
self.coveringLabel.backgroundColor = .clear
self.coveringLabel.layoutIfNeeded()
imageView.removeFromSuperview()
self.tickNumLabel.alpha = originalTicketNumAlpha
}
}
}
But I don't understand why I seem to need to wrap the UIView.transition() into the completion handler of a call to UIView.animate() in order for the flip animation to work.
*(Note: If I pull the "imageView.alpha = 1.0" out of the animate() block and place it immediately BEFORE calling UIView.animate() - the flip animation does not occur (with or without the layoutIfNeeded() call). It just toggles the images. *
Now, here is the code that I expected to work - but when I use this code instead of the above code, there is no "flip" transition. The card image just immediately toggles between the face up and face down image. The "UIView.transition" call here is identical to the one in the above code. The only difference here is that it's NOT wrapped into a 0.01 second UIView.animate completion block.
imageView.alpha = 1.0
imageView.layoutIfNeeded()
UIView.transition(with: imageView, duration: 1.2, options: animation) {
imageView.image = endingImage
imageView.layoutIfNeeded() // same behaviour with and without this line
} completion: { (true) in
if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 }
UIView.animate(withDuration: 0.01) {
self.coveringLabel.backgroundColor = .clear
self.coveringLabel.layoutIfNeeded()
imageView.removeFromSuperview()
self.tickNumLabel.alpha = originalTicketNumAlpha
}
}
This transition ends my flipTicket() function. The code that precedes this transition is identical in both cases. I wasn't going to include it because I don't think it's necessary to understand the problem - but then again - what do I know? Here's the stuff that came before the above clips:
func flipTicket() {
let originalTicketNumAlpha = self.tickNumLabel.alpha
self.tickNumLabel.alpha = 0.0
let tempFaceDownImage:UIImage = self.dealTicketState.faceDownImage
let tempFaceUpImage:UIImage = getCurrentFaceUpImage()
var endingImage:UIImage = self.dealTicketState.faceDownImage
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
imageView.alpha = 0.0
self.coveringLabel.alpha = 1.0
self.coveringLabel.backgroundColor = .black
self.coveringLabel.layoutIfNeeded()
var animation:UIView.AnimationOptions = .transitionFlipFromLeft
if faceDownView.alpha == 1.0 {
animation = .transitionFlipFromRight
imageView.image = tempFaceDownImage
endingImage = tempFaceUpImage
} else {
animation = .transitionFlipFromLeft
imageView.image = tempFaceUpImage
}
self.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: self.topAnchor),
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
imageView.layoutIfNeeded()
Background:
This code is part of a custom UI control which represents a playing card. It consists of several subviews. The topmost subview was originally a UIImageView which held the image of the back of the card. This let me simply toggle the alpha for that top view to display the card as either face up or face down. And then I added one more topMost view to the control - a UILabel with "background = .clear" and attached a UITapGestureRecognizer to it. When the control is tapped, this function which is meant to animate the flipping over of the card is called.
To construct the animation, I call the getCurrentFaceUp() function which temporarily sets the alpha of the card's faceDownView to 0 (so I can take a snapshot of the card underneath it as it is currently configured). It returns a UIImage of the "face up" view of the card. I already have a UIImage of the faceDown view. These are the 2 images I need for the transition.
So...then I set the background color of that topMost UILabel to .black, create a new temporary UIImageView and place it on top of the existing control. I set the temporary imageView to initially display whichever one of the 2 images is currently visible on the control. And then I run the flip transition, change the configuration of the background control to match the new state, change the label background back to .clear and dispose of the temporary UIImageView.
(If there's a better way to accomplish this, I'm open to hearing it but the main purpose of this post is to understand why my code appears to be acting strangely.)
When I was looking for a way to animate a card flip, I found a YouTube video that demonstrated the UIView.transition() with the flip animation. It did not require using a UIView.animate() wrapper in order to make it work - so I'm pretty certain that it's me that did something wrong -but I've spent hours testing variations and searching for someone else with this problem and I haven't been able to find the answer.
Thanks very much in advance to anybody that can help me understand what's going on here...
A little tough to tell (didn't try to actually run your code), but I think you may be doing more than you need to.
Take a look at this...
We'll start with two "Card View" subclasses - Front and Back (we'll "style" them in the next step):
class CardFrontView: UIView {
}
class CardBackView: UIView {
}
Then, a "Playing Card View" class, that contains a "Front" view (cyan) and a "Back" view (red). On init, we add the subviews and set the "Front" view hidden. On tap, we'll run the flip transition between the Front and Back views:
class PlayingCardView: UIView {
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .cyan
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
#objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and then here's a simple Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
}
}
The result:
So, next step, we'll add a little styling to the Front and Back views -- no changes to the PlayingCardView functionality... just a couple new lines to set the styling...
Card Front View - with rounded corners, a border and labels at the corners and center:
class CardFrontView: UIView {
var theLabels: [UILabel] = []
var cardID: Int = 0 {
didSet {
theLabels.forEach {
$0.text = "\(cardID)"
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
for i in 1...5 {
let v = UILabel()
v.font = .systemFont(ofSize: 24.0)
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
switch i {
case 1:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 2:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 4:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
default:
v.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
v.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
theLabels.append(v)
}
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
}
}
Looks like this:
Card Back View - with rounded corners, a border and a cross-hatch pattern:
class CardBackView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
layer.masksToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
// simple cross-hatch pattern
let hReplicatorLayer = CAReplicatorLayer()
let vReplicatorLayer = CAReplicatorLayer()
let line = CAShapeLayer()
let pth = UIBezierPath()
pth.move(to: CGPoint(x: 0.0, y: 0.0))
pth.addLine(to: CGPoint(x: 20.0, y: 20.0))
pth.move(to: CGPoint(x: 20.0, y: 0.0))
pth.addLine(to: CGPoint(x: 0.0, y: 20.0))
line.strokeColor = UIColor.yellow.cgColor
line.lineWidth = 1
line.path = pth.cgPath
var instanceCount = Int((bounds.maxX + 0.0) / 20.0)
hReplicatorLayer.instanceCount = instanceCount
hReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)
instanceCount = Int((bounds.maxY + 0.0) / 20.0)
vReplicatorLayer.instanceCount = instanceCount
vReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, 20, 0)
hReplicatorLayer.addSublayer(line)
vReplicatorLayer.addSublayer(hReplicatorLayer)
layer.addSublayer(vReplicatorLayer)
}
}
Looks like this:
Playing Card View - only change is setting the Front Card background color to white, and setting its "ID" to 5:
class PlayingCardView: UIView {
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .white
cardFront.cardID = 5
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
#objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and finally, the same Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
}
}
and here's the new result:
I'm presenting a simple view controller. To follow MVC, I moved my programmatic view code into a separate UIView subclass. I followed the advice here of how to set up the custom view.
This works ok in iPhone. But, on iPad, the view controller is automatically presented with the new post-iOS 13 default modal style .pageSheet, which causes one of my buttons to be too wide. I looked into the view debugger and it's because the width constraint is set to self.bounds.width * 0.7, and self.bounds returns the full width of the iPad (1024 points) at the time the constraint is set, not the actual final view (which is only 704 points wide).
iPhone simulator screenshot
iPad 12.9" simulator screenshot
Three questions:
In general, am I setting up the custom view + view controller correctly? Or is there a best practice I'm not following?
How do I force an update to the constraints so that the view recognizes it's being presented in a .pageSheet modal mode on iPad, and automatically update the width of self.bounds? I tried self.view.setNeedsLayout() and self.view.layoutIfNeeded() but it didn't do anything.
Why is it that the Snooze button is laid out correctly on iPad, but not the Turn Off Alarm button... the Snooze button relies on constraints pinned to self.leadingAnchor and self.trailingAnchor. Why do those constraints correctly recognize the modal view they're being presented in, but self.bounds.width doesn't?
Code:
Xcode project posted here
View Controller using a custom view:
class CustomViewController: UIViewController {
var customView: CustomView {
return self.view as! CustomView
}
override func loadView() {
let customView = CustomView(frame: UIScreen.main.bounds)
self.view = customView // Set view to our custom view
}
override func viewDidLoad() {
super.viewDidLoad()
print("View's upon viewDidLoad is: \(self.view.bounds)") // <-- This gives incorrect bounds
// Add actions for buttons
customView.snoozeButton.addTarget(self, action: #selector(snoozeButtonPressed), for: .touchUpInside)
customView.dismissButton.addTarget(self, action: #selector(dismissButtonPressed), for: .touchUpInside)
}
override func viewDidAppear(_ animated: Bool) {
print("View's bounds upon viewDidAppear is: \(self.view.bounds)") // <-- This gives correct bounds
// This doesn't work
// //self.view.setNeedsLayout()
// // self.view.layoutIfNeeded()
}
#objc func snoozeButtonPressed() {
// Do something here
}
#objc func dismissButtonPressed() {
self.dismiss(animated: true, completion: nil)
}
}
Custom view code:
class CustomView: UIView {
public var snoozeButton: UIButton = {
let button = UIButton(type: .system)
button.isUserInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Snooze", for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitleColor(UIColor.white, for: .normal)
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.white.cgColor
button.layer.cornerRadius = 14
return button
}()
public var dismissButton: UIButton = {
let button = UIButton(type: .system)
button.isUserInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Turn off alarm", for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitleColor(UIColor.white, for: .normal)
button.backgroundColor = UIColor.clear
button.layer.cornerRadius = 14
button.layer.borderWidth = 2
button.layer.borderColor = UIColor.white.cgColor
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
self.backgroundColor = .systemPurple
// Add subviews
self.addSubview(snoozeButton)
self.addSubview(dismissButton)
}
func setupConstraints() {
print("Self.bounds when setting up custom view constraints is: \(self.bounds)") // <-- This gives incorrect bounds
NSLayoutConstraint.activate([
dismissButton.heightAnchor.constraint(equalToConstant: 60),
dismissButton.widthAnchor.constraint(equalToConstant: self.bounds.width * 0.7), // <-- This is the constraint that's wrong
dismissButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
dismissButton.centerYAnchor.constraint(equalTo: self.centerYAnchor),
snoozeButton.heightAnchor.constraint(equalToConstant: 60),
snoozeButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20),
snoozeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
snoozeButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -40)
])
}
}
.. and finally, the underlying view controller doing the presenting:
class BlankViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
let customVC = CustomViewController()
self.present(customVC, animated: true, completion: nil)
}
}
Thanks.
There's no need to reference bounds -- just use a relative width constraint.
Change this line:
dismissButton.widthAnchor.constraint(equalToConstant: self.bounds.width * 0.7), // <-- This gives incorrect bounds
to this:
dismissButton.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.7),
Now, the button will be 70% of the view's width. As an added benefit, it will adjust itself if/when the view width changes, instead of being stuck at a calculated Constant value.
I'm using a table view to display a tree structure. Each cell corresponds to a node that the user can expand or collapse. The level of each node is visualized by having increasingly large indents at the leading edge of the cells. Those indents are set by using layoutMargins. This seems to work well for the cell's label and separators. Here's some code:
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let cellLevel = cellLevelForIndexPath(indexPath)
let insets: UIEdgeInsets = UIEdgeInsetsMake(0.0, CGFloat(cellLevel) * 20.0, 0.0, 0.0)
cell.separatorInset = insets
cell.layoutMargins = insets
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("cellId") as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cellId")
}
cell!.preservesSuperviewLayoutMargins = false
cell!.backgroundColor = UIColor.clearColor()
let cellLevel = cellLevelForIndexPath(indexPath)
if let textlabel = cell!.textLabel {
textlabel.text = "Cell # level \(cellLevel)"
textlabel.textColor = UIColor.blackColor()
}
cell!.selectedBackgroundView = UIView(frame: CGRectZero)
cell!.selectedBackgroundView.backgroundColor = UIColor.cyanColor()
return cell!
}
The resulting table looks like this:
The question I'm facing now is this: how can I elegantly apply the same indent to the cell's .selectedBackgroundView, so that it appears flush with the text and separator line? The end result should look something like this:
Note: I'm currently achieving the desired effect by making the .selectedBackgroundView more complex and adding background-colored subviews of varying size that effectively mask parts of the cell, e.g. like this:
let maskView = UIView(frame: CGRectMake(0.0, 0.0, CGFloat(cellLevel) * 20.0, cell!.bounds.height))
maskView.backgroundColor = tableView.backgroundColor
cell!.selectedBackgroundView.addSubview(maskView)
But I strongly feel that there must be a nicer way to do this.
Figured out a way to make it work. The trick for me was to stop thinking about the .selectedBackgroundView as the visible highlight itself (and thus trying to mask or resize it) and to treat it more like a canvas instead.
Here's what I ended up doing. First a more convenient way to get the appropriate inset for each level:
let tableLevelBaseInset = UIEdgeInsetsMake(0.0, 20.0, 0.0, 0.0)
private func cellIndentForLevel(cellLevel: Int) -> CGFloat {
return CGFloat(cellLevel) * tableLevelBaseInset.left
}
And then in the cellForRowAtIndexPath:
cell!.selectedBackgroundView = UIView(frame: CGRectZero)
cell!.selectedBackgroundView.backgroundColor = UIColor.clearColor()
let highlight = UIView(frame: CGRectOffset(cell!.bounds, cellIndentForLevel(cellLevel), 0.0))
highlight.backgroundColor = UIColor.cyanColor()
cell!.selectedBackgroundView.addSubview(highlight)
Seems to work nicely.
var selectedView = UIView()
override func awakeFromNib() {
super.awakeFromNib()
self.selectedBackgroundView = {
let view = UIView()
view.backgroundColor = UIColor.clear
selectedView.translatesAutoresizingMaskIntoConstraints = false
selectedView.backgroundColor = .hetro_systemGray6
selectedView.roundAllCorners(radius: 8)
view.addSubview(selectedView)
selectedView.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
selectedView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -5).isActive = true
selectedView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
selectedView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
return view
}()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
selectedBackgroundView?.isHidden = false
} else {
selectedBackgroundView?.isHidden = true
}
}