How to UICollectionReusableView stretch to Safe Area - ios

I am trying to show HeaderView in my UICollectionView class and I use UICollectionReusableView class for that.
Actually I am showing HeaderView in my CollectionView but It does not reach to safe area.
I use auto layout programmatically and I wrote extension to do that.
Here is my class and extensions that I use in my code:
import Foundation
import UIKit
private let headerIdentifer = "HeaderCell"
class ProfileController: UICollectionViewController {
//MARK: - Properties
//MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureProfileCollectionView()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.isHidden = true
}
//MARK: - Helpers
func configureProfileCollectionView() {
collectionView.backgroundColor = .white
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerIdentifer)
}
}
extension ProfileController {
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerIdentifer, for: indexPath) as! HeaderView
return header
}
}
extension ProfileController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: view.frame.width, height: 300)
}
}
import Foundation
import UIKit
class HeaderView: UICollectionReusableView {
//MARK: - Properties
private lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = .systemBlue
view.addSubview(backButton)
backButton.anchor(top: view.topAnchor, left: view.leftAnchor,
paddingTop: 42, paddingLeft: 16)
backButton.setDimensions(width: 30, height: 30)
return view
}()
private lazy var backButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(named: "baseline_arrow_back_white_24dp")?.withRenderingMode(.alwaysOriginal), for: .normal)
button.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside)
return button
}()
//MARK: - Lifecyle
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(containerView)
containerView.anchor(top: topAnchor, left: leftAnchor, right: rightAnchor, height: 108)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Selectors
#objc func backButtonTapped() {
}
}
import UIKit
extension UIView {
func anchor(top: NSLayoutYAxisAnchor? = nil,
left: NSLayoutXAxisAnchor? = nil,
bottom: NSLayoutYAxisAnchor? = nil,
right: NSLayoutXAxisAnchor? = nil,
paddingTop: CGFloat = 0,
paddingLeft: CGFloat = 0,
paddingBottom: CGFloat = 0,
paddingRight: CGFloat = 0,
width: CGFloat? = nil,
height: CGFloat? = nil) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
if let right = right {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
if let width = width {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if let height = height {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
func center(inView view: UIView, yConstant: CGFloat? = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: yConstant!).isActive = true
}
func centerX(inView view: UIView, topAnchor: NSLayoutYAxisAnchor? = nil, paddingTop: CGFloat? = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
if let topAnchor = topAnchor {
self.topAnchor.constraint(equalTo: topAnchor, constant: paddingTop!).isActive = true
}
}
func centerY(inView view: UIView, leftAnchor: NSLayoutXAxisAnchor? = nil, paddingLeft: CGFloat? = nil, constant: CGFloat? = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: constant!).isActive = true
if let leftAnchor = leftAnchor, let padding = paddingLeft {
self.leftAnchor.constraint(equalTo: leftAnchor, constant: padding).isActive = true
}
}
func setDimensions(width: CGFloat, height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
widthAnchor.constraint(equalToConstant: width).isActive = true
heightAnchor.constraint(equalToConstant: height).isActive = true
}
func addConstraintsToFillView(_ view: UIView) {
translatesAutoresizingMaskIntoConstraints = false
anchor(top: view.topAnchor, left: view.leftAnchor,
bottom: view.bottomAnchor, right: view.rightAnchor)
}
}

The UICollectionReusableView is set up fine I believe and I think an automatic inset is applied to the UICollectionView because of the safe area.
Try one of the two options below in your viewDidLoad and it might work
// Option 1
// Get the inset applied at the top and negate the applied inset
if let top
= UIApplication.shared.windows.first?.safeAreaInsets.top
{
collectionView.contentInset.top = -top
}
// Option 2
// Request the collection view to ignore the default behavior
// to add an inset for safe area
collectionView.contentInsetAdjustmentBehavior = .never

More simple solution. No need to write any code!!!

Related

How to put separate different string values into multiple lines in a UITableView

I have UITableView and within it, I am attempting to display two separate string values, each on a different line. The second string or the amount should be displayed under the first line of text within the table view.
The code below displays how the text within the table view is constrained and other functions where I assume the function of separating the values into different lines will be issues:
func set(bio: Bio){
bioLabel.text = bio.statistics
amountLabel.text = bio.amount
}
func configureBioLabel(){
bioLabel.numberOfLines = 0
bioLabel.textColor = .systemGreen
bioLabel.font = UIFont(name: "Seravek", size: 22)
bioLabel.adjustsFontSizeToFitWidth = true
}
func setBioLabelConstraints(){
bioLabel.translatesAutoresizingMaskIntoConstraints = false
bioLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
bioLabel.anchor(top: nil, leading: leadingAnchor, bottom: nil, trailing: nil, padding: .init(top: 0, left: 50, bottom: 0, right: 0))
bioLabel.heightAnchor.constraint(equalToConstant: 80).isActive = true
bioLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 16/9).isActive = true
}
func configureAmount(){
amountLabel.numberOfLines = 0
amountLabel.textColor = .systemYellow
amountLabel.font = UIFont(name: "Seravek", size: 22)
amountLabel.adjustsFontSizeToFitWidth = true
}
func setAmountLabelConstriants(){
amountLabel.translatesAutoresizingMaskIntoConstraints = false
amountLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
amountLabel.anchor(top: nil, leading: leadingAnchor, bottom: nil, trailing: nil, padding: .init(top: 0, left: 50, bottom: 0, right: 0))
amountLabel.heightAnchor.constraint(equalToConstant: 80).isActive = true
amountLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0).isActive = true
}
Then in this extension, the separate sting values are added to the table view.
extension BioInfo: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return bio.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: BioCellId.bioCellId) as! CustomBioCell
let bios = bio[indexPath.row]
cell.set(bio: bios)
return cell
}
}
struct Bio{
var statistics: String
var amount: String
}
extension BioInfo{
func fetchData() -> [Bio]{
let info = Bio(statistics: "Text", amount: "0")
let info2 = Bio(statistics: "Text", amount: "0")
let info3 = Bio(statistics: "Text", amount: "0")
return [info, info2, info3]
}
}
My table view cells are defined here:
class BioInfo: UICollectionViewCell{
lazy var tableView: UITableView = {
let tblView = UITableView()
tblView.delegate = self
tblView.dataSource = self
tblView.translatesAutoresizingMaskIntoConstraints = false
return tblView
}()
var bio: [Bio] = []
struct BioCellId {
static let bioCellId = "CustomBioCell"
}
override init(frame: CGRect) {
super.init(frame: frame)
bio = fetchData()
setupTableView()
}
func setupTableView() {
addSubview(tableView)
translatesAutoresizingMaskIntoConstraints = false
tableView.anchor(top: topAnchor, leading: leadingAnchor, bottom: nil, trailing: trailingAnchor, padding: .init(top: 350, left: 0, bottom: 300, right: 0),size: .init(width: frame.width, height: 300))
tableView.rowHeight = 100
tableView.register(CustomBioCell.self, forCellReuseIdentifier: BioCellId.bioCellId)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How to fix wrong indexPath returned by didSelectRowAt?

I have a UITableView; without a tableHeaderView tapping a row and triggering didSelectRowAt returns the correct index path.
When I set the tableHeaderView property, didSelectRowAt either does not fire or returns the tappedRow + 2. Where am I going wrong?
Here is my code
class MenuController: UIViewController {
// Mark -- Properties
var tableView: UITableView!
var delegate: HomeControllerDelegate?
var headerView: HeaderView? = nil
var user: User? = nil
// Mark -- Init
override func viewDidLoad() {
super.viewDidLoad()
configureTableView()
if let user = self.user {
populateMenuHeader(email: user.email, firstName: user.firstName, lastName: user.lastName, imageUrl: user.imageUrl)
}
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
//updateHeaderViewHeight(for: tableView.tableHeaderView)
}
func updateHeaderViewHeight(for header: UIView?) {
guard let headerView = headerView else { return }
headerView.frame.size.height = 170
}
func populateMenuHeader(email: String, firstName: String, lastName: String, imageUrl: String) {
headerView?.emailLabel?.text = email
headerView?.nameLabel?.text = "\(firstName) \(lastName)"
let request = ImageRequest(
url: URL(string: imageUrl)!,
processors: [
ImageProcessor.Resize(size: CGSize(width: 70, height: 70)),
ImageProcessor.Circle()
]
)
Nuke.loadImage(with: request, into: headerView!.imageView!)
}
// Mark -- Handlers
func configureTableView() {
// Create Material Header
headerView = HeaderView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 170))
//headerView?.heightAnchor.constraint(equalToConstant: 170).isActive = true
headerView?.translatesAutoresizingMaskIntoConstraints = false
tableView = UITableView()
tableView.delegate = self
tableView.dataSource = self
tableView.tableHeaderView = headerView
tableView.sectionHeaderHeight = 170
tableView.register(MenuOptionCell.self, forCellReuseIdentifier: reuseIdentifier)
tableView.backgroundColor = .darkGray
tableView.separatorStyle = .none
tableView.rowHeight = 80
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
}
}
extension MenuController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! MenuOptionCell
let menuOption = MenuOption(rawValue: indexPath.row)
cell.descriptionLabel.text = menuOption?.description
cell.iconImageView.image = menuOption?.image
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let row = indexPath.row
print("tapped row: \(row)")
let menuOption = MenuOption(rawValue: row)
delegate?.handleMenuToggle(forMenuOption: menuOption)
}
}
class CustomView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(1)
context.move(to: CGPoint(x: 0, y: bounds.height))
context.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
context.strokePath()
}
}
}
class HeaderView : UIView {
var imageView: UIImageView? = nil
var nameLabel: UILabel? = nil
var emailLabel: UILabel? = nil
override init(frame: CGRect) {
super.init(frame: frame)
imageView = UIImageView()
imageView?.translatesAutoresizingMaskIntoConstraints = false
nameLabel = UILabel()
nameLabel?.translatesAutoresizingMaskIntoConstraints = false
nameLabel?.font = UIFont(name: "Avenir-Light", size: 20)
nameLabel?.text = "Test name"
nameLabel?.textColor = .white
emailLabel = UILabel()
emailLabel?.translatesAutoresizingMaskIntoConstraints = false
emailLabel?.textColor = .white
emailLabel?.font = UIFont(name: "Avenir-Light", size: 15)
emailLabel?.text = "testemail#gmail.com"
self.addSubview(imageView!)
self.addSubview(nameLabel!)
self.addSubview(emailLabel!)
let lineView = CustomView(frame: CGRect(x: 0, y: frame.height - 1, width: frame.width, height: 1))
self.addSubview(lineView)
imageView?.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
imageView?.topAnchor.constraint(equalTo: topAnchor, constant: 20).isActive = true
imageView?.widthAnchor.constraint(equalTo: widthAnchor, constant: 70).isActive = true
imageView?.heightAnchor.constraint(equalTo: heightAnchor, constant: 70).isActive = true
nameLabel?.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
nameLabel?.topAnchor.constraint(equalTo: imageView!.bottomAnchor, constant: 10).isActive = true
emailLabel?.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
emailLabel?.topAnchor.constraint(equalTo: nameLabel!.bottomAnchor, constant: 5).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The problem appears to be due to the fact that you are not properly setting the height of the header view. The documentation for tableHeaderView states:
When assigning a view to this property, set the height of that view to a nonzero value. The table view respects only the height of your view's frame rectangle; it adjusts the width of your header view automatically to match the table view's width.
Update your header view code:
Change:
headerView = HeaderView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 170))
//headerView?.heightAnchor.constraint(equalToConstant: 170).isActive = true
headerView?.translatesAutoresizingMaskIntoConstraints = false
...
tableView.sectionHeaderHeight = 170
to just:
headerView = HeaderView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 170))
That's it. No need to mess with constraints. No need to set the unrelated section height. Just give the header view's frame the desired height.

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

Why dont some of my anchor constraints work while others do for the same item?

I am trying to put anchor constraints on a textField contained within a subView. For the leading and top anchors the anchors work but for the bottom and trailing anchors they dont. Im not sure what it could be, I would like some space between the keyboard and the items in my subview as well as some space between the UITextField and the trailing anchor edge of the screen. Below is the code in question
Layout Code:
func setUpLayout(){
//myView
self.myView.translatesAutoresizingMaskIntoConstraints = false
self.myView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
self.myView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
self.myView.heightAnchor.constraint(equalToConstant: 98).isActive = true
self.myView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: 4).isActive = true
//CollectionView
self.collectionView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.leadingAnchor.constraint(equalTo: self.myView.leadingAnchor, constant: 0).isActive = true
self.collectionView.trailingAnchor.constraint(equalTo: self.myView.trailingAnchor, constant: 0).isActive = true
self.collectionView.bottomAnchor.constraint(equalTo: self.myView.bottomAnchor, constant: 4).isActive = true
self.collectionView.topAnchor.constraint(equalTo: self.searchBar.bottomAnchor, constant: 4).isActive = true
//searchbar
self.searchBar.translatesAutoresizingMaskIntoConstraints = false
self.searchBar.topAnchor.constraint(equalTo: self.myView.topAnchor, constant: 0).isActive = true
self.searchBar.leadingAnchor.constraint(equalTo: self.myView.leadingAnchor, constant: 4).isActive = true
self.searchBar.trailingAnchor.constraint(equalTo: self.myView.trailingAnchor, constant: 4).isActive = true
self.searchBar.heightAnchor.constraint(equalToConstant: 45).isActive = true
searchBar.backgroundColor = .white
searchBar.layer.borderWidth = 2
searchBar.layer.borderColor = UIColor.black.cgColor
//PictureView
self.PictureView.translatesAutoresizingMaskIntoConstraints = false
self.PictureView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
self.PictureView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
self.PictureView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
self.PictureView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
self.PictureView.backgroundColor = .green
}
Code in its entirety
import UIKit
class SearchCollectionViewController: UIViewController,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout, UITextFieldDelegate, UICollectionViewDataSource,UIGestureRecognizerDelegate {
var myView: UIView!
var searchBar: UITextField!
var collectionView: UICollectionView!
var PictureView: UIView!
var genericArray:[String] = ["A","B","C","D","E","F","G","Ab","Abc","za"]
var currentGenericArray:[String] = [String]()
var tagsSelected:[String] = [String]()
let keyboardSlider = KeyboardSlider()
override func viewDidLoad() {
super.viewDidLoad()
keyboardSlider.subscribeToKeyboardNotifications(view: view)
myView = UIView(frame: CGRect(x: 0, y: self.view.frame.height, width: self.view.frame.width, height: self.view.frame.height))
searchBar = UITextField(frame: CGRect(x: 0, y: 0, width: self.myView.frame.width, height: self.myView.frame.height))
PictureView = UIView(frame: self.view.frame)
self.view.addSubview(PictureView)
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0)
layout.itemSize = CGSize(width: (UIScreen.main.bounds.width-1)/2, height: (UIScreen.main.bounds.width-1)/2)
layout.scrollDirection = .horizontal
self.collectionView = UICollectionView(frame: CGRect(x: 0, y: self.myView.frame.height, width: self.myView.frame.width, height: 100), collectionViewLayout: layout)
collectionView.backgroundColor = .clear
collectionView.contentInset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
myView.backgroundColor = .clear
collectionView.delegate = self
collectionView.dataSource = self
self.myView.addSubview(collectionView)
self.view.addSubview(myView)
collectionView.register(CollectionCell.self, forCellWithReuseIdentifier: "collectionViewCell")
currentGenericArray = genericArray
searchBar.delegate = self
searchBar.autocorrectionType = .no
searchBar.keyboardType = .default
searchBar.addTarget(self, action: #selector(SearchCollectionViewController.textFieldDidChange), for: .editingChanged)
self.myView.addSubview(searchBar)
let viewTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(viewTapped(gestureRecognizer:)))
viewTapGestureRecognizer.cancelsTouchesInView = false
self.PictureView.addGestureRecognizer(viewTapGestureRecognizer)
self.PictureView.backgroundColor = .purple
self.view.backgroundColor = .green
self.searchBar.becomeFirstResponder()
self.collectionView.allowsSelection = true
setUpLayout()
}
func setUpLayout(){
//myView
self.myView.translatesAutoresizingMaskIntoConstraints = false
self.myView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
self.myView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
self.myView.heightAnchor.constraint(equalToConstant: 98).isActive = true
self.myView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: 4).isActive = true
//CollectionView
self.collectionView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.leadingAnchor.constraint(equalTo: self.myView.leadingAnchor, constant: 0).isActive = true
self.collectionView.trailingAnchor.constraint(equalTo: self.myView.trailingAnchor, constant: 0).isActive = true
self.collectionView.bottomAnchor.constraint(equalTo: self.myView.bottomAnchor, constant: 4).isActive = true
self.collectionView.topAnchor.constraint(equalTo: self.searchBar.bottomAnchor, constant: 4).isActive = true
//searchbar
self.searchBar.translatesAutoresizingMaskIntoConstraints = false
self.searchBar.topAnchor.constraint(equalTo: self.myView.topAnchor, constant: 0).isActive = true
self.searchBar.leadingAnchor.constraint(equalTo: self.myView.leadingAnchor, constant: 4).isActive = true
self.searchBar.trailingAnchor.constraint(equalTo: self.myView.trailingAnchor, constant: 4).isActive = true
self.searchBar.heightAnchor.constraint(equalToConstant: 45).isActive = true
searchBar.backgroundColor = .white
searchBar.layer.borderWidth = 2
searchBar.layer.borderColor = UIColor.black.cgColor
//PictureView
self.PictureView.translatesAutoresizingMaskIntoConstraints = false
self.PictureView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
self.PictureView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
self.PictureView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
self.PictureView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
self.PictureView.backgroundColor = .green
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
keyboardSlider.unsubscribeFromKeyboardNotifications()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
collectionView.reloadData()
return true
}
/// Helper to dismiss keyboard
#objc func didStopEditing() {
}
func textFieldDidEndEditing(_ textField: UITextField) {
UIView.setAnimationCurve(UIViewAnimationCurve.easeInOut)
UIView.animate(withDuration: 0.2) {
self.view.frame.origin.y = 0
}
}
#objc func textFieldDidChange(){
guard(!(searchBar.text?.isEmpty)!) else{
currentGenericArray = genericArray
collectionView.reloadData()
return
}
currentGenericArray = genericArray.filter({letter -> Bool in
if searchBar.text!.count > letter.count{
return false
}
let stringRange = letter.index(letter.startIndex, offsetBy: searchBar.text!.count)
let subword = letter[..<stringRange]
return subword.lowercased().contains(searchBar.text!.lowercased())
})
if currentGenericArray.isEmpty{
print("text being inserted \(searchBar.text!)")
currentGenericArray.append(searchBar.text!)
}
collectionView.reloadData()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if (touch.view?.isDescendant(of: self.collectionView))!{
return false
}
return true
}
var keyboardIsOpen:Bool = false
#objc func viewTapped(gestureRecognizer:UIGestureRecognizer){
if keyboardIsOpen{
myView.isHidden = true
keyboardIsOpen = !keyboardIsOpen
searchBar.resignFirstResponder()
}
else{
myView.isHidden = false
keyboardIsOpen = !keyboardIsOpen
searchBar.becomeFirstResponder()
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0{
return tagsSelected.count
}
else {
return currentGenericArray.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as! CollectionCell
if indexPath.section == 0{
cell.collectionLabel.text = tagsSelected[indexPath.item]
cell.backgroundColor = .blue
cell.collectionLabel.textColor = .white
}
else if indexPath.section == 1{
cell.backgroundColor = .white
cell.collectionLabel.textColor = UIColor.black
cell.collectionLabel.text = currentGenericArray[indexPath.row]
}
cell.layer.masksToBounds = true
cell.layer.cornerRadius = cell.bounds.width/20
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 6
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.section == 1{\
if(tagsSelected.contains(currentGenericArray[indexPath.item])){
}
tagsSelected.append(currentGenericArray[indexPath.item])
for i in 0...genericArray.count-1{
if(currentGenericArray[indexPath.item] == genericArray[i]){
genericArray.remove(at: i)
break
}
}
currentGenericArray.remove(at: indexPath.item)
searchBar.text = ""
collectionView.reloadData()
if collectionView.numberOfItems(inSection: 1)>0{
collectionView.scrollToItem(at: IndexPath(item: 0, section: 1), at: .right, animated: true)
}
}
else if indexPath.section == 0{
currentGenericArray.append(tagsSelected[indexPath.item])
tagsSelected.remove(at: indexPath.item)
collectionView.reloadData()
}
}
var offsetY:CGFloat = 0
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(SearchCollectionViewController.keyboardFrameChangeNotification(notification:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}
#objc func keyboardFrameChangeNotification(notification: Notification) {
if let userInfo = notification.userInfo {
let endFrame = userInfo[UIKeyboardFrameEndUserInfoKey] as? CGRect
let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double ?? 0
let animationCurveRawValue = (userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIViewAnimationOptions.curveEaseInOut.rawValue)
let animationCurve = UIViewAnimationOptions(rawValue: UInt(animationCurveRawValue))
if let _ = endFrame, endFrame!.intersects(self.myView.frame) {
self.offsetY = self.myView.frame.maxY - endFrame!.minY
} else {
if self.offsetY != 0 {
UIView.animate(withDuration: animationDuration, delay: TimeInterval(0), options: animationCurve, animations: {
self.myView.frame.origin.y = self.myView.frame.origin.y + self.offsetY
self.offsetY = 0
}, completion: nil)
}
}
}
}
}
class CollectionCell:UICollectionViewCell{
var collectionLabel: UILabel!
var view:UIView!
override init(frame: CGRect) {
super.init(frame: frame)
collectionLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height))
self.addSubview(collectionLabel)
collectionLabel.textAlignment = .center
collectionLabel.translatesAutoresizingMaskIntoConstraints = false
collectionLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
collectionLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
collectionLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
collectionLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension UIView {
func currentFirstResponder() -> UIResponder? {
if self.isFirstResponder {
return self
}
for view in self.subviews {
if let responder = view.currentFirstResponder() {
return responder
}
}
return nil
}
}
extension Notification.Name{
static let showKeyboard = Notification.Name("showKeyboard")
}
class KeyboardSlider: NSObject {
// variables to hold and process information from the view using this class
weak var view: UIView?
#objc func keyboardWillShow(notification: NSNotification) {
// method to move keyboard up
view?.frame.origin.y = 0 - getKeyboardHeight(notification as Notification)
}
func getKeyboardHeight(_ notification:Notification) -> CGFloat {
// get exact height of keyboard on all devices and convert to float value to return for use
let userInfo = notification.userInfo
let keyboardSize = userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue
return keyboardSize.cgRectValue.height
}
func subscribeToKeyboardNotifications(view: UIView) {
// assigning view to class' counterpart
self.view = view
// when UIKeyboardWillShow do keyboardWillShow function
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: .UIKeyboardWillShow, object: nil)
}
func unsubscribeFromKeyboardNotifications() {
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil)
}
}
For trailing and bottom anchor constraints, it is common that you have to use negative numbers for the constants to achieve what you want.
For example:
searchBar.bottomAnchor.constraint(equalTo: myView.bottomAnchor, constant: 4).isActive = true
Becomes:
searchBar.bottomAnchor.constraint(equalTo: myView.bottomAnchor, constant: -4).isActive = true

Resources