My issue is so specific that I have to give the code for this, sorry if the code is too convoluted or complicated, I am a bit new to UIKit since I have been learning SwiftUI more instead of UIKit.
I have a really complicated UITableView
header with some nice animations and such, and I put a UISegmentedControl
on top. However, as soon as a UITableViewCell
gets behind the UISegmentedControl
, it stops working and user interaction capabilities are lost with the UISegmentedControl
.
This is too hard to explain without visual context and code, so here it is.
As you can see, at the top, the user can interact with the UISegmentedControl freely, but when I start scrolling and table view cells are going behind the UISegmentedControl
(as expected), the segmented control stops responding to user touch input completely.
Again, excuse me for the long (and probably bad) code, I am still learning.
This is the code for the UITableView that I wrote:
class TertiaryProfileScroll: UITableViewController {
var segmentedControl: UISegmentedControl!
var testBlurView: UIVisualEffectView!
var headerTitle: UIStackView!
var blurView: UIVisualEffectView!
var scoreRect: UIView!
var scoreLabel: UILabel!
var originalBlurRect: CGRect!
var originalTitleRect: CGRect!
var originalTestRect: CGRect!
override func viewDidLoad() {
super.viewDidLoad()
let headerView = SecondaryStretchyTableHeaderView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 488))
let image = UIImage(named: "background_image")
UIGraphicsBeginImageContextWithOptions(CGSize(width: 350, height: 350), false, 4)
image!.draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 350, height: 350)))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
tableView.allowsSelection = false
tableView.separatorStyle = .none
headerView.imageView.image = newImage
tableView.tableHeaderView = headerView
addTitle()
blurView.contentView.isUserInteractionEnabled = true
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard tableView.tableHeaderView != nil else { return }
let headerView = self.tableView.tableHeaderView as! SecondaryStretchyTableHeaderView
let headerGeometry = self.geometry(view: headerView, scrollView: scrollView)
let titleGeometry = self.geometry(view: headerTitle, scrollView: scrollView)
(tableView.tableHeaderView as! SecondaryStretchyTableHeaderView).containerView
.alpha = CGFloat(sqrt(headerGeometry.largeTitleWeight))
(tableView.tableHeaderView as! SecondaryStretchyTableHeaderView).imageContainer.alpha = CGFloat(sqrt(headerGeometry.largeTitleWeight))
let largeTitleOpacity = (max(titleGeometry.largeTitleWeight, 0.5) - 0.5) * 2
let tinyTitleOpacity = 1 - min(titleGeometry.largeTitleWeight, 0.5) * 2
headerTitle.alpha = CGFloat(sqrt(largeTitleOpacity))
blurView.contentView.subviews[1].alpha = CGFloat(sqrt(tinyTitleOpacity))
if let vfxSubview = blurView.subviews.first(where: {
String(describing: type(of: $0)) == "_UIVisualEffectSubview"
}) {
vfxSubview.backgroundColor = UIColor.systemBackground.withAlphaComponent(0)
}
if let vfxBackdrop = blurView.subviews.first(where: {
String(describing: type(of: $0)) == "_UIVisualEffectBackdropView"
}) {
vfxBackdrop.alpha = CGFloat(1 - sqrt(titleGeometry.largeTitleWeight))
}
var blurFrame = blurView.frame
var titleFrame = headerTitle.frame
blurFrame.origin.y = max(originalBlurRect.minY, originalBlurRect.minY + titleGeometry.blurOffset)
titleFrame.origin.y = originalTitleRect.minY + 364
blurView.frame = blurFrame
headerTitle.frame = titleFrame
headerView.scrollViewDidScroll(scrollView: scrollView)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
1000
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
cell?.textLabel?.text = String(indexPath.row)
cell?.layer.zPosition = -1000
view.sendSubviewToBack(cell!)
view.bringSubviewToFront(tableView.tableHeaderView!)
return cell!
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: false)
}
func addTitle() {
let blurEffect = UIBlurEffect(style: UITraitCollection.current.userInterfaceStyle == .dark ? .dark : .light)
blurView = UIVisualEffectView(effect: blurEffect)
blurView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44 + 38 + tableView.safeAreaInsets.top)
segmentedControl = UISegmentedControl(items: secondaryProfilePages)
segmentedControl.selectedSegmentIndex = 0
segmentedControl.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width - 32, height: 30)
let scoreSize = ("+\(String(9999))" as NSString).boundingRect(with: CGSize(width: UIScreen.main.bounds.size.width, height: CGFloat.greatestFiniteMagnitude), options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin], attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)], context: nil).size
scoreLabel = PaddingLabel(withInsets: 2.5, 2.5, 5, 5)
scoreLabel.text = "+\(String(9999))"
scoreLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
scoreLabel.textColor = .black
scoreLabel.backgroundColor = .white
scoreLabel.layer.masksToBounds = true
scoreLabel.layer.cornerRadius = 5
let smallScoreLabel = PaddingLabel(withInsets: 2.5, 2.5, 5, 5)
smallScoreLabel.text = "+\(String(9999))"
smallScoreLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
smallScoreLabel.textColor = .label
smallScoreLabel.backgroundColor = UITraitCollection.current.userInterfaceStyle == .dark ? .white : .black
smallScoreLabel.layer.masksToBounds = true
smallScoreLabel.layer.cornerRadius = 5
let nsText = "OmerFlame" as NSString?
let bigLabelSize = nsText?.boundingRect(with: CGSize(width: UIScreen.main.bounds.size.width - 32 - scoreLabel.intrinsicContentSize.width, height: CGFloat.greatestFiniteMagnitude), options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin], attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 34, weight: .black)], context: nil).size
let smallLabelSize = nsText?.boundingRect(with: CGSize(width: UIScreen.main.bounds.size.width - 32 - scoreLabel.intrinsicContentSize.width, height: CGFloat.greatestFiniteMagnitude), options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin], attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18, weight: .bold)], context: nil).size
let largeLabel = UILabel(frame: CGRect(x: 0, y: 0, width: bigLabelSize!.width, height: bigLabelSize!.height))
let smallLabel = UILabel(frame: CGRect(x: 0, y: 0, width: smallLabelSize!.width, height: smallLabelSize!.height))
largeLabel.text = "OmerFlame"
largeLabel.font = .systemFont(ofSize: 34, weight: .black)
largeLabel.textColor = .white
largeLabel.adjustsFontSizeToFitWidth = true
largeLabel.minimumScaleFactor = 0.2
largeLabel.allowsDefaultTighteningForTruncation = true
largeLabel.numberOfLines = 1
smallLabel.text = "OmerFlame"
smallLabel.font = .systemFont(ofSize: 18, weight: .bold)
smallLabel.textColor = .label
smallLabel.adjustsFontSizeToFitWidth = true
smallLabel.minimumScaleFactor = 0.1
smallLabel.allowsDefaultTighteningForTruncation = true
smallLabel.numberOfLines = 1
largeLabel.translatesAutoresizingMaskIntoConstraints = false
smallLabel.translatesAutoresizingMaskIntoConstraints = false
headerTitle = UIStackView(frame: CGRect(x: 0, y: 0, width: largeLabel.frame.size.width + 5 + scoreLabel.intrinsicContentSize.width, height: max(largeLabel.frame.size.height, scoreLabel.intrinsicContentSize.height)))
headerTitle.axis = .horizontal
headerTitle.alignment = .center
headerTitle.distribution = .equalCentering
headerTitle.addArrangedSubview(largeLabel)
headerTitle.addArrangedSubview(scoreLabel)
let smallHeaderTitle = UIStackView(frame: CGRect(x: 0, y: 0, width: smallLabel.frame.size.width + 5 + smallScoreLabel.intrinsicContentSize.width, height: max(smallLabel.frame.size.height, smallScoreLabel.intrinsicContentSize.height)))
smallHeaderTitle.axis = .horizontal
smallHeaderTitle.alignment = .center
smallHeaderTitle.distribution = .equalCentering
smallHeaderTitle.addArrangedSubview(smallLabel)
smallHeaderTitle.addArrangedSubview(smallScoreLabel)
blurView.contentView.addSubview(headerTitle)
blurView.contentView.addSubview(smallHeaderTitle)
blurView.contentView.addSubview(segmentedControl)
tableView.tableHeaderView!.addSubview(blurView)
blurView.translatesAutoresizingMaskIntoConstraints = false
blurView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
blurView.heightAnchor.constraint(equalTo: tableView.tableHeaderView!.heightAnchor).isActive = true
blurView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width).isActive = true
largeLabel.translatesAutoresizingMaskIntoConstraints = false
smallLabel.translatesAutoresizingMaskIntoConstraints = false
scoreLabel.translatesAutoresizingMaskIntoConstraints = false
scoreLabel.leadingAnchor.constraint(equalTo: largeLabel.trailingAnchor, constant: 5).isActive = true
smallHeaderTitle.translatesAutoresizingMaskIntoConstraints = false
smallScoreLabel.translatesAutoresizingMaskIntoConstraints = false
smallScoreLabel.leadingAnchor.constraint(equalTo: smallLabel.trailingAnchor, constant: 5).isActive = true
smallScoreLabel.bottomAnchor.constraint(equalTo: smallLabel.bottomAnchor).isActive = true
smallHeaderTitle.centerXAnchor.constraint(equalTo: blurView.contentView.centerXAnchor).isActive = true
smallHeaderTitle.heightAnchor.constraint(equalToConstant: max(smallLabel.frame.size.height, smallScoreLabel.intrinsicContentSize.height)).isActive = true
smallHeaderTitle.widthAnchor.constraint(equalToConstant: smallLabel.frame.size.width + 5 + smallScoreLabel.intrinsicContentSize.width).isActive = true
smallHeaderTitle.bottomAnchor.constraint(equalTo: segmentedControl.topAnchor, constant: -4).isActive = true
headerTitle.translatesAutoresizingMaskIntoConstraints = false
headerTitle.bottomAnchor.constraint(equalTo: segmentedControl.topAnchor, constant: -8).isActive = true
headerTitle.widthAnchor.constraint(equalToConstant: largeLabel.frame.size.width + 5 + scoreLabel.intrinsicContentSize.width).isActive = true
headerTitle.heightAnchor.constraint(equalToConstant: max(largeLabel.frame.size.height, scoreLabel.intrinsicContentSize.height)).isActive = true
largeLabel.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor, constant: 16).isActive = true
originalBlurRect = blurView.frame
originalTitleRect = headerTitle.frame
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
segmentedControl.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor, constant: -8).isActive = true
segmentedControl.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width - 32).isActive = true
segmentedControl.heightAnchor.constraint(equalToConstant: 30).isActive = true
segmentedControl.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor, constant: 16).isActive = true
view.bringSubviewToFront(smallHeaderTitle)
view.bringSubviewToFront(segmentedControl)
segmentedControl.layer.zPosition = 1000
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.bringSubviewToFront(blurView)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.userInterfaceStyle == .dark {
(blurView.contentView.subviews[1] as! UIStackView).arrangedSubviews[1].backgroundColor = .white
((blurView.contentView.subviews[1] as! UIStackView).arrangedSubviews[1] as! UILabel).textColor = .black
} else {
(blurView.contentView.subviews[1] as! UIStackView).arrangedSubviews[1].backgroundColor = .black
((blurView.contentView.subviews[1] as! UIStackView).arrangedSubviews[1] as! UILabel).textColor = .white
}
}
}
extension TertiaryProfileScroll {
struct HeaderGeometry {
let width: CGFloat
let headerHeight: CGFloat
let elementsHeight: CGFloat
let headerOffset: CGFloat
let blurOffset: CGFloat
let elementsOffset: CGFloat
let largeTitleWeight: Double
}
func geometry(view: UIView, scrollView: UIScrollView) -> HeaderGeometry {
let safeArea = scrollView.safeAreaInsets
let minY = -(scrollView.contentOffset.y + scrollView.safeAreaInsets.top)
let hasScrolledUp = minY > 0
let hasScrolledToMinHeight = -minY >= 450 - 44 - safeArea.top
let headerHeight = hasScrolledUp ?
(tableView.tableHeaderView as! SecondaryStretchyTableHeaderView).containerView.frame.size.height + minY + 38 : (tableView.tableHeaderView as! SecondaryStretchyTableHeaderView).containerView.frame.size.height + 38
let elementsHeight = (tableView.tableHeaderView as! SecondaryStretchyTableHeaderView).frame.size.height + minY
let headerOffset: CGFloat
let blurOffset: CGFloat
let elementsOffset: CGFloat
let largeTitleWeight: Double
if hasScrolledUp {
headerOffset = -minY
blurOffset = -minY
elementsOffset = -minY
largeTitleWeight = 1
} else if hasScrolledToMinHeight {
headerOffset = -minY - 450 + 44 + safeArea.top
blurOffset = -minY - 450 + 44 + safeArea.top
elementsOffset = headerOffset / 2 - minY / 2
largeTitleWeight = 0
} else {
headerOffset = 0
blurOffset = 0
elementsOffset = -minY / 2
let difference = 450 - 44 - safeArea.top + minY
largeTitleWeight = difference <= 44 + 1 ? Double(difference / (44 + 1)) : 1
}
return HeaderGeometry(width: (tableView.tableHeaderView as! SecondaryStretchyTableHeaderView).frame.size.width, headerHeight: headerHeight, elementsHeight: elementsHeight, headerOffset: headerOffset, blurOffset: blurOffset, elementsOffset: elementsOffset, largeTitleWeight: largeTitleWeight)
}
}
class SecondaryStretchyTableHeaderView: UIView {
var imageContainerHeight = NSLayoutConstraint()
var imageContainerBottom = NSLayoutConstraint()
var imageViewHeight = NSLayoutConstraint()
var imageViewBottom = NSLayoutConstraint()
var imageViewTop = NSLayoutConstraint()
var containerView: UIView!
var imageContainer: UIView!
var imageView: UIImageView!
var largeTitleOpacity = Double()
var tinyTitleOpacity = Double()
var largeLabel: UILabel!
var tinyLabel: UILabel!
var containerViewHeight = NSLayoutConstraint()
var stack: UIStackView!
var title: StretchyHeaderTitle!
override init(frame: CGRect) {
super.init(frame: frame)
createViews()
setViewConstraints()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func createViews() {
// Container View
containerView = UIView()
self.addSubview(containerView)
imageContainer = UIView()
imageContainer.backgroundColor = UIColor(hex: "d55161")
imageContainer.contentMode = .scaleAspectFill
imageContainer.clipsToBounds = true
containerView.addSubview(imageContainer)
// ImageView for background
imageView = UIImageView()
imageView.backgroundColor = UIColor(hex: "d55161")
imageView.contentMode = .scaleAspectFill
imageContainer.addSubview(imageView)
}
func setViewConstraints() {
// UIView Constraints
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalTo: containerView.widthAnchor),
self.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
self.heightAnchor.constraint(equalTo: containerView.heightAnchor)
])
// Container View Constraints
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.widthAnchor.constraint(equalTo: imageContainer.widthAnchor).isActive = true
containerViewHeight = containerView.heightAnchor.constraint(equalTo: self.heightAnchor)
containerViewHeight.isActive = true
// ImageView Constraints
imageContainer.translatesAutoresizingMaskIntoConstraints = false
imageContainerBottom = imageContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
imageContainerBottom.isActive = true
imageContainerHeight = imageContainer.heightAnchor.constraint(equalTo: containerView.heightAnchor)
imageContainerHeight.isActive = true
imageView.translatesAutoresizingMaskIntoConstraints = false
imageViewBottom = imageView.bottomAnchor.constraint(equalTo: imageContainer.bottomAnchor, constant: -50)
imageViewBottom.isActive = true
imageViewTop = imageView.topAnchor.constraint(equalTo: imageContainer.topAnchor, constant: 50)
imageViewTop.isActive = true
imageView.centerXAnchor.constraint(equalTo: imageContainer.centerXAnchor).isActive = true
}
func scrollViewDidScroll(scrollView: UIScrollView) {
containerViewHeight.constant = scrollView.contentInset.top
let offsetY = -(scrollView.contentOffset.y + scrollView.contentInset.top)
containerView.clipsToBounds = offsetY <= 0
imageContainerBottom.constant = offsetY >= 0 ? 0 : -offsetY / 2
imageContainerHeight.constant = max(offsetY + scrollView.contentInset.top, scrollView.contentInset.top)
imageContainer.clipsToBounds = offsetY <= 0
imageViewBottom.constant = (offsetY >= 0 ? 0 : -offsetY / 2) - 50
imageViewTop.constant = (offsetY >= 0 ? 0 : -offsetY / 2) + 50
}
}
extension UIColor {
// MARK: - Initialization
convenience init?(hex: String) {
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
var rgb: UInt32 = 0
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 1.0
let length = String(hexSanitized).count
guard Scanner(string: hexSanitized).scanHexInt32(&rgb) else { return nil }
if length == 6 {
r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
b = CGFloat(rgb & 0x0000FF) / 255.0
} else if length == 8 {
r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
a = CGFloat(rgb & 0x000000FF) / 255.0
} else {
return nil
}
self.init(red: r, green: g, blue: b, alpha: a)
}
// MARK: - Computed Properties
var toHex: String? {
return toHex()
}
// MARK: - From UIColor to String
func toHex(alpha: Bool = false) -> String? {
guard let components = cgColor.components, components.count >= 3 else {
return nil
}
let r = Float(components[0])
let g = Float(components[1])
let b = Float(components[2])
var a = Float(1.0)
if components.count >= 4 {
a = Float(components[3])
}
if alpha {
return String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255))
} else {
return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255))
}
}
}
I think I gave all of the code related to my issue. If I missed something, tell me. I know that the code is really long, but I am totally clueless as to what I'm supposed to do. Any reply is welcome! I also apologize for potentially repeated code, I am posting this in a hurry.
Thank you!
OK - I got your code to run.
The problem is that your Segmented Control is extending outside the bounds of the table header view.
I think it would make much more sense to keep all of your UI elements that are part of the "stretchy header" inside the header class, so this is not how I would recommend doing this, but this should give you back your segmented control interaction:
In your SecondaryStretchyTableHeaderView
class, add this var / property:
weak var segControl: UISegmentedControl?
In addTitle()
in your TertiaryProfileScroll
class, add this:
// your existing code
blurView.contentView.addSubview(headerTitle)
blurView.contentView.addSubview(smallHeaderTitle)
blurView.contentView.addSubview(segmentedControl)
tableView.tableHeaderView!.addSubview(blurView)
// add this
if let v = tableView.tableHeaderView as? SecondaryStretchyTableHeaderView {
// give our custom header view a reference to the segemented control
v.segControl = segmentedControl
}
Back in your SecondaryStretchyTableHeaderView
class, add this func:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard isUserInteractionEnabled,
!isHidden,
alpha >= 0.01,
let sc = segControl
else { return nil }
// if we tap outside the bounds,
// but on the segmented control
// return the segmented control
let convertedPoint = sc.convert(point, from: self)
if let v = sc.hitTest(convertedPoint, with: event) {
return v
}
guard self.point(inside: point, with: event) else { return nil }
return self
}
That will allow you to interact with the segmented control, even when it is outside the bounds of the table header view.
As a side note, it appears you're setting .layer.zPosition
where you don't need to. In cellForRowAt
I commented out these lines:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
cell?.textLabel?.text = String(indexPath.row)
//cell?.layer.zPosition = -1000
//view.sendSubviewToBack(cell!)
//view.bringSubviewToFront(tableView.tableHeaderView!)
return cell!
}
and also commented out this line (at the end of addTitle()
):
//segmentedControl.layer.zPosition = 1000
and I don't see any difference.
User contributions licensed under CC BY-SA 3.0