Creating a custom view when a tableview cell is selected - ios

How would I create a new view that displays unique information for the selection of a specific tableview cell? For example, if row 1 is selected then the new image will have unique information to that cell which would be different from the view if row 2 was selected.
Here is the code for my tableview.
import UIKit
struct Data {
var sectionTitle = String()
var rowTitles = [String]()
}
class ViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableView: UITableView!
var dataArray = [Data(sectionTitle: "section 1", rowTitles: ["row 1", "row 2", "row 3"]),
Data(sectionTitle: "section 2", rowTitles: ["row 1"]),
Data(sectionTitle: "section 3", rowTitles: ["row 1", "row 2"])
]
var searchArray = [Data]()
var searching = false
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
return searchArray[section].rowTitles.count
} else {
return dataArray[section].rowTitles.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "searchCell")
cell?.textLabel?.lineBreakMode = NSLineBreakMode.byWordWrapping
cell?.textLabel?.numberOfLines = 3
if searching {
cell?.textLabel?.text = self.searchArray[indexPath.section].rowTitles[indexPath.row]
} else {
cell?.textLabel?.text = self.dataArray[indexPath.section].rowTitles[indexPath.row]
}
return cell!
}
func numberOfSections(in tableView: UITableView) -> Int {
if searching {
return searchArray.count
} else {
return dataArray.count
}
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return dataArray[section].sectionTitle
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let index = indexPath.row
let data = dataArray[index]
let destVC = SecondViewController(data: data)
navigationController?.pushViewController(destVC, animated: true)
}
}
extension ViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchBar.text == nil || searchBar.text == "" {
searching = false
view.endEditing(true)
tableView.reloadData()
} else {
searching = true
searchArray = dataArray.filter({$0.sectionTitle.lowercased().contains(searchBar.text!.lowercased()) || $0.rowTitles.map{$0.lowercased()}.contains(searchBar.text!.lowercased())})
tableView.reloadData()
}
}
}
Here is my second view controller which shows my generic stack view that I would like to change to display different images and labels depending on the cell that is selected in the tableview.
import UIKit
class SecondViewController: UIViewController {
var data: Data!
var dataView: DataView { return self.view as! DataView}
init(data: Data) {
self.data = data
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func loadView() {
self.view = DataView(frame: UIScreen.main.bounds)
}
}
class DataView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .white
setupViews()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
self.addSubview(stack)
stack.distribution = .fillProportionally
stack.spacing = 10
}
func setupConstraints() {
stack.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
stack.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
stack.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
stack.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
dataImage.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
dataImage.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 60).isActive = true
dataImage.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: -60).isActive = true
}
let dataNameLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 20, weight: .bold)
label.textAlignment = .center
label.numberOfLines = 2
label.text = "Organization"
return label
} ()
let dataDescriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
label.textAlignment = .center
label.numberOfLines = 8
label.text = "Description"
return label
} ()
let dataImage: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "")?.withRenderingMode(.alwaysOriginal)
imageView.backgroundColor = .gray
imageView.layer.cornerRadius = 8
imageView.contentMode = .scaleAspectFill
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.heightAnchor.constraint(equalToConstant: 260).isActive = true
return imageView
} ()
let visitButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Visit", for: .normal)
return button
} ()
lazy var stack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [dataImage, dataNameLabel, dataDescriptionLabel, visitButton])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
return stack
} ()
}
The objective is to have a user select a tableview cell, and for the stack view to display specific information that corresponds to that cell. How would I create this?

I'm not sure if you're asking how to pass information to a new view controller when an item in your TableViewController has been selected.
But I'm interpreting your question, as you know how to pass information, but you want to pass different information, maybe an image to your second view controller, based on which TableViewCell was selected.
The way that I would do this, is to have an array with images in your SecondViewController. When an item is selected in your TableViewController, also pass the indexPath.row to the SecondViewController. From there, you can load the image from your imagesArray at the indexPath.row that you have passed to it. You can do this by having an integer value in your SecondViewController, that is populated right before you segue.

Related

How could UILabel always be nil -- Unexpectedly found nil while implicitly unwrapping an Optional value

As many people encountered, I tried to build tableView. I found many similar questions but it seems answers are not helping. I would be very grateful if anyone could help me. The problem I encountered:
Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
This is a description Xcode gives me
Here's what I did:
(1) I connected Labels in the storyboard to the class it related to, which should be right as it's not hollow.
(2) I used tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath), and I tried to print cell I got, all cells aren't nil and belongs to CollegeTableViewCell, which is correct.
(3) I changed the identifier of tableViewCell to Cell which matches, and I changed it's class to CollegeTableViewCell too.
My program crashed directly when it executes following code. I only works when I make labels optional. So the problem is what did I do wrong so that labels in cell are always nil?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CollegeTableViewCell
let college = colleges[indexPath.row]
cell.collegeName.text = college.name // <-CRASH
cell.collegeGeo.text = college.city + ", " + college.state
return cell
}
Following is my CollegeTableViewCell class:
class CollegeTableViewCell: UITableViewCell {
#IBOutlet weak var collegeName: UILabel!
#IBOutlet weak var collegeGeo: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
EDIT: more codes related to this problem.
class CollegeChooseViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableView: UITableView!
var colleges = [CollegeInfo]()
let searchController = UISearchController(searchResultsController: nil)
let collegeApiUrl = "https://api.collegeai.com/v1/api/autocomplete/colleges?api_key=b47484dd6e228ea2cc5e1bf6ca&query="
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.register(CollegeTableViewCell.self, forCellReuseIdentifier: "Cell")
getColleges(contentInSearch: "MIT")
}
func getColleges(contentInSearch: String) {
guard let url = URL(string: (collegeApiUrl + contentInSearch)) else { return }
URLSession.shared.fetchData(for: url) {(result: Result<Initial, Error>) in
switch result {
case .success(let initial):
self.colleges = initial.collegeList
DispatchQueue.main.async {
self.tableView.reloadData()
}
case .failure(let error):
print("failed fetching college list from API: \(error)")
}
}
}
}
extension URLSession {
func fetchData<T: Decodable>(for url: URL, completion: #escaping (Result<T, Error>) -> Void) {
self.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
}
if let data = data {
do {
let object = try JSONDecoder().decode(T.self, from: data)
completion(.success(object))
} catch let decoderError {
completion(.failure(decoderError))
}
}
}.resume()
}
}
extension CollegeChooseViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CollegeTableViewCell
let college = colleges[indexPath.row]
cell.collegeName.text = college.name // <-CRASH
cell.collegeGeo.text = college.city + ", " + college.state
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(colleges.count)
return colleges.count
}
}
class CollegeTableViewCell: UITableViewCell {
#IBOutlet weak var collegeName: UILabel!
#IBOutlet weak var collegeGeo: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(true, animated: true)
}
}
This is a sample of your tableview programmatically way... If I don't know where your data comes from, I used a simulation of your arrays... conform your controller to UITableViewDelegate and Datasource:
class YourController: UIViewController, UITableViewDelegate, UITableViewDataSource
Now set tableView and constraints
var name = ["Mike", "Jhon", "Carl", "Steve", "Elon", "Bill", "Bruce"] // simulation of your array
var city = ["Milano", "New Yor", "Paris", "Los Angeles", "Madrid", "Amsterdam", "Tokyo"] // simulation of your array
var state = ["Italia", "USA", "France", "USA", "Spain", "Holland", "Japan"] // simulation of your array
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkBlue
tableView.backgroundColor = .white
tableView.register(CollegeTableViewCell.self, forCellReuseIdentifier: "cellId") // register cell
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.separatorColor = .lightGray
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// this is my extension to configure navigation bar, you can configure it as you want
configureNavigationBar(largeTitleColor: .red, backgoundColor: .black, tintColor: .red, title: "Sample", preferredLargeTitle: true)
}
After that set your tableView Delegate and DataSource:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
// Mark: - set number of rows with your array.count
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return name.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let name = name[indexPath.row]
let city = city[indexPath.row]
let state = state[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! CollegeTableViewCell
cell.collegeName.text = name
cell.collegeGeo.text = "\(city), \(state)"
return cell
}
This is how your cell look like:
class CollegeTableViewCell: UITableViewCell {
let collegeName: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 16, weight: .semibold)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let collegeGeo: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 14, weight: .semibold)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .ultraDark
let stackView = UIStackView(arrangedSubviews: [collegeName, collegeGeo]) // use stack view for automatic table view dimension
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 2
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20).isActive = true
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20).isActive = true
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
this is the result:
EDIT based on new information full code and Json decoder:
struct CollegeInfo: Decodable {
let collegeList: [MyDataResults]
}
struct MyDataResults: Decodable {
let id: String
let name: String
let city: String
let state: String
}
class tableController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var myData = [MyDataResults]() // simulation of your array
let urlString = "https://api.collegeai.com/v1/api/autocomplete/colleges?api_key=b47484dd6e228ea2cc5e1bf6ca&query="
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkBlue
tableView.backgroundColor = .white
tableView.register(CollegeTableViewCell.self, forCellReuseIdentifier: "cellId") // register cell
tableView.delegate = self
tableView.dataSource = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.separatorColor = .lightGray
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// this is my extension to configure navigation bar, you can configure it as you want
configureNavigationBar(largeTitleColor: .red, backgoundColor: .black, tintColor: .red, title: "Sample", preferredLargeTitle: true)
fetchJson { [weak self] (res) in
switch res {
case .success(let dataResults):
dataResults.forEach { (dataresult) in
self?.myData.removeAll()
DispatchQueue.main.asyncAfter(deadline: .now() + 0) {
self?.myData = dataresult.collegeList
self?.tableView.reloadData()
}
}
case .failure(let err):
print("Failed to fetch json", err)
}
}
}
fileprivate func fetchJson(completion: #escaping (Result<[CollegeInfo], Error >) -> ()) {
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { data, resp, err in
if let err = err {
completion(.failure(err))
return
}
do {
guard let data = data else { return }
let results = try JSONDecoder().decode(CollegeInfo.self, from: data)
//succesful
completion(.success([results]))
} catch let jsonErr {
completion(.failure(jsonErr))
}
}.resume()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
// Mark: - set number of rows with your array.count
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let myResults = myData[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! CollegeTableViewCell
cell.collegeName.text = myResults.name
cell.collegeGeo.text = "\(myResults.city), \(myResults.state)"
return cell
}
The cell:
class CollegeTableViewCell: UITableViewCell {
let collegeName: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 16, weight: .semibold)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let collegeGeo: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 14, weight: .semibold)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .ultraDark
let stackView = UIStackView(arrangedSubviews: [collegeName, collegeGeo]) // use stack view for automatic table view dimension
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 2
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20).isActive = true
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20).isActive = true
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The result:
you're using the wrong bundle name for dequeueReusableCell.
instead of cell use CollegeTableViewCell
it should be :
let cell = tableView.dequeueReusableCell(withIdentifier: "CollegeTableViewCell", for: indexPath) as! CollegeTableViewCell

Swift UISwitch Button with Label

I am receiving data from API . The data field is displayed with two Label control, Id and status . Here is the screenshot .
I have another view where I created switch button and label programatically. I want to change the status to false when user turn on to switch button but it now updating the value . Here is the code and function i defined .
class DetailsViewController : UIViewController{
#IBOutlet private weak var tableView: UITableView!
var changeStatus: ((Bool, String) -> Void)?
var identifier = ""
private let switchControl: UISwitch = {
let switchControl = UISwitch()
switchControl.translatesAutoresizingMaskIntoConstraints = false
switchControl.isOn = false
return switchControl
}()
#IBOutlet weak var customSwitch: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.addSubview(switchControl)
switchControl.addTarget(self, action: #selector(changeSwitchControl), for: .valueChanged)
let safeArea = view.safeAreaLayoutGuide
switchControl.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
switchControl.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true
setUpUI()
}
#objc
private func changeSwitchControl() {
changeStatus?(switchControl.isOn, identifier)
}
}
Here is code in View Controller .
class ViewController: UIViewController {
// ! Mark is means it not null the value will in story board
// use lazy property to tell compiler instance value of the datasource self class and execuate the controller code to set the value of tableview datasource
private let viewModel = ViewModel()
private var subcriber = Set<AnyCancellable>()
private var storiesTrue = [String]()
#Published private(set) var stories = [Rover]()
private var datasourceStories = [Rover]()
private lazy var tableView: UITableView = {
let tableview = UITableView()
tableview.translatesAutoresizingMaskIntoConstraints = false// adding constrains
tableview.dataSource = self
tableview.showsVerticalScrollIndicator = false
tableview.register(StoryCell.self, forCellReuseIdentifier: StoryCell.identifier)
return tableview
}()
override func viewDidLoad() {
super.viewDidLoad()
setUpUI()
setUpBinding()
tableView.dataSource = self
tableView.delegate = self
self.tableView.rowHeight = 44;
// display the second view controller using push methods
/*let detail = DetailsViewController()
detail.name "Mohammad"
navigationController?.pushViewController(detail, animated: true)*/
}
private func setUpUI() {
view.backgroundColor = .white
view.addSubview(tableView)// adding hierracy key like adding into story board
// creating constrains with safe area
let safeArea = view.safeAreaLayoutGuide
tableView.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor).isActive = true
}
private func setUpBinding(){
viewModel
.$rovers
.receive(on : RunLoop.main)
.sink {[weak self]_ in
self?.tableView.reloadData()
}
.store(in: &subcriber)
viewModel.getStories()
}
private func getStatus(by identifier: String) -> Bool {
return storiesTrue.contains(identifier)
}
}
extension ViewController: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.rovers.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: StoryCell.identifier , for: indexPath) as? StoryCell
else{ return UITableViewCell()}
let row = indexPath.row
let Id = viewModel.getId(by: row)
let title = viewModel.getTitle(by: row)
let identifier = viewModel.getIdentifier(by: indexPath.row)
let status = getStatus(by: identifier) ? "active" : "false"
cell.configureCell(Id: Id,title: title,statusString: status)
return cell
}
}
extension ViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
viewModel.getStories()
}
}
extension ViewController : UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let second = DetailsViewController()
navigationController?.pushViewController(second, animated: true)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 44;
}
}
Here is the view controller to defined the UI properties .
class StoryCell: UITableViewCell {
static let identifier = "StoryCell"
private lazy var storyIdLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .left
return label
}()
public lazy var storyTitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .left
return label
}()
private lazy var statusStory: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .left
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureCell(Id: Int,title: String,statusString: String) {
storyIdLabel.text = "Id: \(String(Id))"
storyTitleLabel.text = "Status :\(title)"
statusStory.text = "Status:\(statusString)"
}
/* func configureCell(Id: Int) {
storyIdLabel.text = "Id: \(String(Id))"
}*/
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
private func setUpUI() {
contentView.addSubview(storyTitleLabel)
// constraints
let safeArea = contentView.safeAreaLayoutGuide
storyTitleLabel.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
storyTitleLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true
storyTitleLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true
contentView.addSubview(storyIdLabel)
let safeArea1 = contentView.safeAreaLayoutGuide
storyIdLabel.topAnchor.constraint(equalTo: storyTitleLabel.topAnchor).constant = 5
storyIdLabel.bottomAnchor.constraint(equalTo: safeArea1.bottomAnchor).isActive = true
storyIdLabel.leadingAnchor.constraint(equalTo: safeArea1.leadingAnchor).isActive = true
storyIdLabel.trailingAnchor.constraint(equalTo: safeArea1.trailingAnchor).isActive = true
/* contentView.addSubview(statusStory)
let safeArea2 = contentView.safeAreaLayoutGuide
statusStory.topAnchor.constraint(equalTo: statusStory.topAnchor).isActive = true
statusStory.bottomAnchor.constraint(equalTo: safeArea2.bottomAnchor).isActive = true
statusStory.leadingAnchor.constraint(equalTo: safeArea2.leadingAnchor).isActive = true
statusStory.trailingAnchor.constraint(equalTo: safeArea2.trailingAnchor).isActive = true*/
}
}
You have to use tableView.reloadData() after updating info that you want to show. Without running this function, data in table view won't be updated.

Expand/Collapse indexed tableView cells with searchController

I have a tableView with different subcategories ("Algrebra","Biology","Chemistry") who are indexed and searchable via the searchController. I want to put these subcategories inside multiple categories ("Urgent","Important","Not Important") and expand/collapse them on click. I also want to have the categories indexed (instead of the subcategories) but keep the subcategories searchable via the searchController.
I don't know how to implement it properly with my code.
Here's my code:
CategoryController
class CategoryController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UISearchResultsUpdating {
private var searchController = UISearchController()
let categories = ["Urgent", "Important", "Not Important"]
let subcategories = [
Add(category: "Algrebra", categoryImg: #imageLiteral(resourceName: "Algebra.png")),
Add(category: "Biology", categoryImg: #imageLiteral(resourceName: "Biology.png")),
Add(category: "Chemistry", categoryImg: #imageLiteral(resourceName: "Chemistry.png")),
]
private var sectionTitles = [String]()
private var filteredSectionTitles = [String]()
private var sortedCategory = [(key: String, value: [Add])]()
private var filteredCategory = [(key: String, value: [Add])]()
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return table }()
override func viewDidLoad() {
super.viewDidLoad()
//TABLEVIEW
tableView.rowHeight = 50
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.register(CategoryCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.sectionIndexColor = .black
tableView.sectionIndexBackgroundColor = .lightGray
tableView.sectionIndexTrackingBackgroundColor = .gray
tableView.allowsMultipleSelection = false
//SEARCHCONTROLLER
self.searchController = UISearchController(searchResultsController: nil)
self.searchController.searchResultsUpdater = self
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchBar.placeholder = "Search for your category"
self.searchController.hidesNavigationBarDuringPresentation = false
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
self.navigationItem.title = "Tasks"
navigationController?.navigationBar.prefersLargeTitles = true
self.searchController.searchBar.searchTextField.textColor = .label
let groupedList = Dictionary(grouping: self.subcategories, by: { String($0.category.prefix(1)) })
self.sortedCategory = groupedList.sorted{$0.key < $1.key}
for tuple in self.sortedCategory {
self.sectionTitles.append(tuple.key)
}
}
//VIEWDIDLAYOUT
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
/// TABLEVIEW
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
return self.filteredSectionTitles[section]
} else {
return self.sectionTitles[section]
}
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
return self.filteredSectionTitles
} else {
return self.sectionTitles
}
}
func numberOfSections(in tableView: UITableView) -> Int {
if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
return self.filteredSectionTitles.count
} else {
return self.sectionTitles.count
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.searchController.isActive && !self.filteredCategory.isEmpty {
return self.filteredCategory[section].value.count
} else {
return self.sortedCategory[section].value.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for:indexPath) as UITableViewCell
cell.imageView?.contentMode = .scaleAspectFit
if self.searchController.isActive && !self.filteredCategory.isEmpty {
cell.textLabel?.text = self.filteredCategory[indexPath.section].value[indexPath.row].category
cell.imageView?.image = self.filteredCategory[indexPath.section].value[indexPath.row].categoryImg
} else {
cell.textLabel?.text = self.sortedCategory[indexPath.section].value[indexPath.row].category
cell.imageView?.image = self.sortedCategory[indexPath.section].value[indexPath.row].categoryImg
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let currentCell = tableView.cellForRow(at: indexPath)! as UITableViewCell
Add.details.category = (currentCell.textLabel?.text)!
let secondVC = DateController()
navigationController?.pushViewController(secondVC, animated: true)
print(Add.details.category)
}
func updateSearchResults(for searchController: UISearchController) {
guard let text = self.searchController.searchBar.text else {
return
}
let filteredCategory = self.sortedCategory.flatMap { $0.value.filter { $0.category.contains(text) } }
let groupedCategory = Dictionary(grouping: filteredCategory, by: { String($0.category.prefix(1)) } )
self.filteredCategory = []
self.filteredCategory = groupedCategory.sorted{ $0.key < $1.key }
self.filteredSectionTitles = []
for tuple in self.filteredCategory {
self.filteredSectionTitles.append(tuple.key)
}
self.tableView.reloadData()
}
}
CategoryCell
class CategoryCell: UITableViewCell {
var cellImageView = UIImageView()
var cellLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: "cell")
cellImageView.translatesAutoresizingMaskIntoConstraints = false
cellImageView.contentMode = .scaleAspectFit
cellImageView.tintColor = .systemPink
contentView.addSubview(cellImageView)
cellLabel.translatesAutoresizingMaskIntoConstraints = false
cellLabel.font = UIFont.systemFont(ofSize: 20)
contentView.addSubview(cellLabel)
NSLayoutConstraint.activate([
cellImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
cellImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
cellImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
cellImageView.widthAnchor.constraint(equalToConstant: 44),
cellLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
cellLabel.leadingAnchor.constraint(equalTo: cellImageView.trailingAnchor, constant: 10),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
Add(DataStruct)
struct Add {
static var details: Add = Add()
var category: String = ""
func getDict() -> [String: Any] {
let dict = ["category": self.category,
] as [String : Any]
return dict
}
}
Couple tips that should help...
First, let's change some naming.
You're using your "Categories" of "Urgent","Important","Not Important" as Sections ... and your "subcategories" would be more accurately described as "Categories".
We can also think of the Sections as perhaps Category Status
So, we'll create an enum like this:
enum CategoryStatus: Int, CustomStringConvertible {
case urgent
case important
case notimportant
var description : String {
switch self {
case .urgent: return "Urgent"
case .important: return "Important"
case .notimportant: return "Not Important"
}
}
var star : UIImage {
switch self {
case .urgent: return UIImage(named: "star") ?? UIImage()
case .important: return UIImage(named: "halfstar") ?? UIImage()
case .notimportant: return UIImage(named: "emptystar") ?? UIImage()
}
}
}
And we'll add a "status" property to the Category struct:
struct MyCategory {
var name: String = ""
var categoryImg: UIImage = UIImage()
var status: CategoryStatus = .important
}
Now, we can work through the process using "plain language":
start by sorting the entire category list by name
when we type a search string, we can filter that list by "name contains search"
when can then group that list by status
So if we start with:
Biology : .important
Chemistry : .urgent
Algebra : .urgent
we can sort on name and get
Algebra : .urgent
Biology : .important
Chemistry : .urgent
then group by status
.urgent
Algebra
Chemistry
.important
Biology
If we have typed "b" in the search field, we start with our sorted ALL list, and filter it:
Algebra : .urgent
Biology : .important
then group by status
.urgent
Algebra
.important
Biology
Another tip: instead of using a "full list" and a "filtered list", along with a bunch of
if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
blocks, use a single sorted, filtered and grouped list.
That list will then be set to either A) the FULL list (if there is no search text entered) or B) the Filtered list
Here is a complete example you can try out. I used a bunch of random topics as Categories, and used numbers in circles for each category image, and I used pngs of star, halfstar and emptystar.
Please note this is Example Code Only!. It is not meant to be, and should not be considered to be, "Production Ready":
enum CategoryStatus: Int, CustomStringConvertible {
case urgent
case important
case notimportant
var description : String {
switch self {
case .urgent: return "Urgent"
case .important: return "Important"
case .notimportant: return "Not Important"
}
}
var star : UIImage {
switch self {
case .urgent: return UIImage(named: "star") ?? UIImage()
case .important: return UIImage(named: "halfstar") ?? UIImage()
case .notimportant: return UIImage(named: "emptystar") ?? UIImage()
}
}
}
struct MyCategory {
var name: String = ""
var categoryImg: UIImage = UIImage()
var status: CategoryStatus = .important
}
class CategoryController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UISearchResultsUpdating {
private var searchController = UISearchController()
// array of ALL Categories, sorted by name
private var sortedCategories: [MyCategory] = []
// this will be either ALL items, or the filtered items
// grouped by Status
private var sortedByStatus = [(key: CategoryStatus, value: [MyCategory])]()
private let tableView = UITableView()
private let noMatchesLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .yellow
v.text = "NO Matches"
v.textAlignment = .center
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
var items: [MyCategory] = []
// this will be our list of MyCategory objects (they'll be sorted later)
let itemNames: [String] = [
"Algebra",
"Chemistry",
"Biology",
"Computer Sciences",
"Physics",
"Earth Sciences",
"Geology",
"Political Science",
"Psychology",
"Nursing",
"Economics",
"Agriculture",
"Communications",
"Engineering",
"Foreign Lanuages",
"English Language",
"Literature",
"Libary Sciences",
"Social Sciences",
"Visual Arts",
]
// create our array of MyCategory
// setting every 3rd one to .urgent, .important or .notimportant
for (str, i) in zip(itemNames, 0...30) {
let status: CategoryStatus = CategoryStatus.init(rawValue: i % 3) ?? .important
var img: UIImage = UIImage()
if let thisImg = UIImage(named: str) {
img = thisImg
} else {
if let thisImg = UIImage(systemName: "\(i).circle") {
img = thisImg
}
}
items.append(MyCategory(name: str, categoryImg: img, status: status))
}
// sort the full list of categories by name
self.sortedCategories = items.sorted{$0.name < $1.name}
//TABLEVIEW
tableView.rowHeight = 50
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.sectionIndexColor = .black
tableView.sectionIndexBackgroundColor = .lightGray
tableView.sectionIndexTrackingBackgroundColor = .gray
tableView.allowsMultipleSelection = false
tableView.dataSource = self
tableView.delegate = self
tableView.register(CategoryCell.self, forCellReuseIdentifier: CategoryCell.reuseIdentifier)
tableView.register(MySectionHeaderView.self, forHeaderFooterViewReuseIdentifier: MySectionHeaderView.reuseIdentifier)
//SEARCHCONTROLLER
self.searchController = UISearchController(searchResultsController: nil)
self.searchController.searchResultsUpdater = self
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchBar.placeholder = "Search for your category"
self.searchController.hidesNavigationBarDuringPresentation = false
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
self.navigationItem.title = "Tasks"
navigationController?.navigationBar.prefersLargeTitles = true
self.searchController.searchBar.searchTextField.textColor = .label
// add the no-matches view
noMatchesLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(noMatchesLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
noMatchesLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.7),
noMatchesLabel.heightAnchor.constraint(equalToConstant: 120.0),
noMatchesLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
noMatchesLabel.topAnchor.constraint(equalTo: tableView.frameLayoutGuide.topAnchor, constant: 40.0),
])
noMatchesLabel.isHidden = true
// call updateSearchResults to build the initial non-filtered data
updateSearchResults(for: searchController)
}
/// TABLEVIEW
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: MySectionHeaderView.reuseIdentifier) as! MySectionHeaderView
v.imageView.image = self.sortedByStatus[section].key.star
v.label.text = self.sortedByStatus[section].key.description
return v
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
// first char of each section title
return (sortedByStatus.map { $0.key.description }).compactMap { String($0.prefix(1)) }
}
func numberOfSections(in tableView: UITableView) -> Int {
return self.sortedByStatus.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sortedByStatus[section].value.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CategoryCell.reuseIdentifier, for:indexPath) as! CategoryCell
cell.cellLabel.text = self.sortedByStatus[indexPath.section].value[indexPath.row].name
cell.cellImageView.image = self.sortedByStatus[indexPath.section].value[indexPath.row].categoryImg
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// get Category object from data
let thisCategory: MyCategory = self.sortedByStatus[indexPath.section].value[indexPath.row]
print("selected:", thisCategory.name, "status:", thisCategory.status)
}
func updateSearchResults(for searchController: UISearchController) {
var filteredList: [MyCategory] = []
if let text = self.searchController.searchBar.text, !text.isEmpty {
// we have text to search for, so filter the list
filteredList = self.sortedCategories.filter { $0.name.localizedCaseInsensitiveContains(text) }
} else {
// no text to search for, so use the full list
filteredList = self.sortedCategories
}
// filteredList is now either ALL Categories (no search text entered), or
// ALL Categories filtered by search text
// create a dictionary of items grouped by status
let groupedList = Dictionary(grouping: filteredList, by: { $0.status })
// order the grouped list by status
self.sortedByStatus = groupedList.sorted{$0.key.rawValue < $1.key.rawValue}
// show noMatchesLabel if we have NO matching Categories
noMatchesLabel.isHidden = self.sortedByStatus.count != 0
// reload the table
self.tableView.reloadData()
}
}
// simple cell with image view and label
class CategoryCell: UITableViewCell {
static let reuseIdentifier: String = String(describing: self)
var cellImageView = UIImageView()
var cellLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
cellImageView.translatesAutoresizingMaskIntoConstraints = false
cellImageView.contentMode = .scaleAspectFit
cellImageView.tintColor = .systemPink
contentView.addSubview(cellImageView)
cellLabel.translatesAutoresizingMaskIntoConstraints = false
cellLabel.font = UIFont.systemFont(ofSize: 20)
contentView.addSubview(cellLabel)
NSLayoutConstraint.activate([
cellImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
cellImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
cellImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
cellImageView.widthAnchor.constraint(equalToConstant: 44),
cellLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
cellLabel.leadingAnchor.constraint(equalTo: cellImageView.trailingAnchor, constant: 10),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
// simple reusable section header with image view and label
class MySectionHeaderView: UITableViewHeaderFooterView {
static let reuseIdentifier: String = String(describing: self)
let imageView = UIImageView()
let label = UILabel()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
imageView.contentMode = .scaleAspectFit
label.font = .systemFont(ofSize: 20.0, weight: .bold)
[imageView, label].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: 24.0),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
imageView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
imageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 12.0),
label.topAnchor.constraint(equalTo: g.topAnchor),
label.bottomAnchor.constraint(equalTo: g.bottomAnchor),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Here's how it looks when launched:
then, as we type "t" "e" "ra":
Edit
The for (str, i) in zip(itemNames, 0...30) { block in the example code was just an easy way to generate some example items.
To use this in your code, you would likely do something like this:
let items = [
MyCategory(name: "Algebra", categoryImg: #imageLiteral(resourceName: "Algebra.png"), status: .urgent),
MyCategory(name: "Biology", categoryImg: #imageLiteral(resourceName: "Biology.png"), status: .important),
MyCategory(name: "Chemistry", categoryImg: #imageLiteral(resourceName: "Chemistry.png"), status: .notimportant),
// and so on
]

How to tell if UITableView in custom UISearchBar is touched?

I am trying to create a custom UISearchBar that is placed as the titleView of a navigationController. Using the following code, the suggestionTableView of suggestions appears perfectly; however, It does not recognize any taps. In fact, it is like the suggestionTableView isn't even there because my taps are being sent to another view under the suggestion suggestionTableView. I was told that I could use hitTest(...) to catch these touches, but I don't know how I would implement this in my SuggestionSearchBar or in my ViewController. How can I send these touches to the suggestionTableView?
SuggestionSearchBar
class SuggestionSearchBar: UISearchBar, UISearchBarDelegate {
var suggestionTableView = UITableView(frame: .zero)
let allPossibilities: [String]!
var possibilities = [String]()
//let del: UISearchBarDelegate!
init(del: UISearchBarDelegate, dropDownPossibilities: [String]) {
self.allPossibilities = dropDownPossibilities
super.init(frame: .zero)
isUserInteractionEnabled = true
delegate = del
searchTextField.addTarget(self, action: #selector(searchBar(_:)), for: .editingChanged)
searchTextField.addTarget(self, action: #selector(searchBarCancelButtonClicked(_:)), for: .editingDidEnd)
sizeToFit()
addTableView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func addTableView() {
let cellHeight = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "").frame.height
suggestionTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
suggestionTableView.backgroundColor = UIColor.clear
//suggestionTableView.separatorStyle = .none
suggestionTableView.tableFooterView = UIView()
addSubview(suggestionTableView)
suggestionTableView.delegate = self
suggestionTableView.dataSource = self
suggestionTableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
suggestionTableView.topAnchor.constraint(equalTo: bottomAnchor),
suggestionTableView.rightAnchor.constraint(equalTo: rightAnchor),
suggestionTableView.leftAnchor.constraint(equalTo: leftAnchor),
suggestionTableView.heightAnchor.constraint(equalToConstant: cellHeight*6),
])
hideSuggestions()
}
func showSuggestions() {
let sv = suggestionTableView.superview
sv?.clipsToBounds = false
suggestionTableView.isHidden = false
}
func hideSuggestions() {
suggestionTableView.isHidden = true
}
#objc func searchBar(_ searchBar: UISearchBar) {
print(searchBar.text?.uppercased() ?? "")
showSuggestions()
possibilities = allPossibilities.filter {$0.contains(searchBar.text?.uppercased() ?? "")}
print(possibilities.count)
suggestionTableView.reloadData()
if searchBar.text == "" || possibilities.count == 0 {
hideSuggestions()
}
}
#objc func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
hideSuggestions()
}
}
extension SuggestionSearchBar: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return possibilities.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = suggestionTableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.backgroundColor = UIColor(red: 0.25, green: 0.25, blue: 0.25, alpha: 0.75)
if traitCollection.userInterfaceStyle == .light {
cell.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.75)
}
cell.textLabel?.text = possibilities[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//add method that fills in and searches based on the text in that indexpath.row
print("selected")
}
}
ViewController
import UIKit
class ViewController: UIViewController {
lazy var searchBar = SuggestionSearchBar(del: self, dropDownPossibilities: ["red","green","blue","yellow"])
override func viewDidLoad() {
super.viewDidLoad()
setUpUI()
}
func setUpUI() {
setUpSearchBar()
}
}
extension ViewController: UISearchBarDelegate {
func setUpSearchBar() {
searchBar.searchBarStyle = UISearchBar.Style.prominent
searchBar.placeholder = "Search"
searchBar.sizeToFit()
searchBar.isTranslucent = false
searchBar.backgroundImage = UIImage()
searchBar.delegate = self
navigationItem.titleView = searchBar
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
print(searchBar.text!)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.endEditing(true)
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
}
}
Reviewing your provided code, I can get the UI to work properly and even get the UITableViewDelegate callbacks inside SuggestionSearchBar.
Here are the changes
Suggestion Search Bar
class SuggestionSearchBar: UISearchBar, UISearchBarDelegate {
var suggestionTableView = UITableView(frame: .zero)
let allPossibilities: [String]!
var possibilities = [String]()
var fromController: UIViewController?
//let del: UISearchBarDelegate!
init(del: UISearchBarDelegate, dropDownPossibilities: [String], fromController: UIViewController) {
self.fromController = fromController
self.allPossibilities = dropDownPossibilities
super.init(frame: .zero)
isUserInteractionEnabled = true
delegate = del
searchTextField.addTarget(self, action: #selector(searchBar(_:)), for: .editingChanged)
searchTextField.addTarget(self, action: #selector(searchBarCancelButtonClicked(_:)), for: .editingDidEnd)
sizeToFit()
addTableView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func addTableView() {
guard let view = fromController?.view else {return}
let cellHeight = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "").frame.height
suggestionTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
suggestionTableView.backgroundColor = UIColor.clear
//suggestionTableView.separatorStyle = .none
suggestionTableView.tableFooterView = UIView()
view.addSubview(suggestionTableView)
// addSubview(suggestionTableViewse
suggestionTableView.delegate = self
suggestionTableView.dataSource = self
suggestionTableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
suggestionTableView.topAnchor.constraint(equalTo: view.topAnchor),
suggestionTableView.rightAnchor.constraint(equalTo: view.rightAnchor),
suggestionTableView.leftAnchor.constraint(equalTo: view.leftAnchor),
suggestionTableView.heightAnchor.constraint(equalToConstant: cellHeight*6),
])
hideSuggestions()
}
func showSuggestions() {
let sv = suggestionTableView.superview
sv?.clipsToBounds = false
suggestionTableView.isHidden = false
}
func hideSuggestions() {
suggestionTableView.isHidden = true
}
#objc func searchBar(_ searchBar: UISearchBar) {
print(searchBar.text?.uppercased() ?? "")
showSuggestions()
possibilities = allPossibilities.filter {$0.contains(searchBar.text?.lowercased() ?? "")}
print(possibilities.count)
suggestionTableView.reloadData()
if searchBar.text == "" || possibilities.count == 0 {
hideSuggestions()
}
}
#objc func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
hideSuggestions()
}
}
ViewController
class ViewController: UIViewController {
lazy var searchBar = SuggestionSearchBar(del: self, dropDownPossibilities: ["red","green","blue","yellow"], fromController: self)
override func viewDidLoad() {
super.viewDidLoad()
setUpUI()
}
func setUpUI() {
setUpSearchBar()
}
}
To summarise the changes, the code above tried to add suggestionTableView
to the SearchBarView which is not possible so I initialized SearchBarView with the reference to the parent ViewController which is stored as
var fromController: UIViewController?
This property is later used in addTableView()
private func addTableView() {
guard let view = fromController?.view else {return}
let cellHeight = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "").frame.height
suggestionTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
suggestionTableView.backgroundColor = UIColor.clear
//suggestionTableView.separatorStyle = .none
suggestionTableView.tableFooterView = UIView()
view.addSubview(suggestionTableView)
// addSubview(suggestionTableViewse
suggestionTableView.delegate = self
suggestionTableView.dataSource = self
suggestionTableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
suggestionTableView.topAnchor.constraint(equalTo: view.topAnchor),
suggestionTableView.rightAnchor.constraint(equalTo: view.rightAnchor),
suggestionTableView.leftAnchor.constraint(equalTo: view.leftAnchor),
suggestionTableView.heightAnchor.constraint(equalToConstant: cellHeight*6),
])
hideSuggestions()
}
There is another small change
possibilities = allPossibilities.filter {$0.contains(searchBar.text?.lowercased() ?? "")}
in #objc func searchBar(_ searchBar: UISearchBar) {
Result
As long as you are adding the UITableView as a subview to the SearchBar or UINavigationBar, you will keep finding these touch issues.
A possible way to handle this would be have an empty container UIView instance at the call site (ViewController in your case) and ask SuggestionsSearchBar to add it's tableView inside that container.
SuggestionSearchBar(
del: self,
suggestionsListContainer: <UIStackView_Inside_ViewController>,
dropDownPossibilities: ["red","green","blue","yellow"]
)
SuggestionsSearchBar will still manage everything about the tableView's dataSource, delegate, it's visibility etc. It just needs a view that can hold it's tableView from the call site.
UPDATE
I'm highlighting only the relevant parts that need to change - everything else remains the same.
class SuggestionSearchBar: UISearchBar, UISearchBarDelegate {
init(del: UISearchBarDelegate, suggestionsListContainer: UIStackView, dropDownPossibilities: [String]) {
//// All the current setUp
addTableView(in: suggestionsListContainer)
}
private func addTableView(in container: UIStackView) {
//// All the current setUp
container.addArrangedSubview(suggestionTableView)
NSLayoutConstraint.activate([
suggestionTableView.heightAnchor.constraint(equalToConstant: cellHeight*6),
/// We need to assign only height here
/// top, leading, trailing will be driven by container at call site
])
}
}
class ViewController: UIViewController {
lazy var suggestionsListContainer: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
lazy var searchBar = SuggestionSearchBar(
del: self,
suggestionsListContainer: suggestionsListContainer,
dropDownPossibilities: ["red","green","blue","yellow"]
)
func setUpUI() {
setUpSearchBar()
setUpSuggestionsListContainer()
}
func setUpSuggestionsListContainer() {
self.view.addSubview(suggestionsListContainer)
NSLayoutConstraint.activate([
suggestionsListContainer.topAnchor.constraint(equalTo: self.view.topAnchor),
suggestionsListContainer.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
suggestionsListContainer.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
/// Height is not needed as it will be driven by tableView's height
])
}
}

Rows in table view do not expand the width of it and are stacked Swift

I have spent an hour trying to figure out what I'm doing wrong but no success. below is the code and an image of the result. All the rows appear one on top of the other in the first row of the table. and the row does not expand the width of the table as I have set it in the constraints. What am I doing wrong? Thank you.
The table view class:
class TableViewListType: UITableView {
override init(frame: CGRect, style: UITableView.Style) {
super.init(frame: frame, style: style)
translatesAutoresizingMaskIntoConstraints = false
allowsSelection = true
allowsMultipleSelection = false
allowsSelectionDuringEditing = true
allowsMultipleSelectionDuringEditing = true
dragInteractionEnabled = false
backgroundColor = .clear
separatorColor = .white
separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
indicatorStyle = .white
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The row classes for the table view:
class SuperRowForProfileAttributesTable: UITableViewCell {
//MARK: - Properties.
internal let firstLabel: LabelForRowInList = {
let label = LabelForRowInList(frame: .zero)
return label
}()
internal let secondLabel: LabelForRowInList = {
let label = LabelForRowInList(frame: .zero)
return label
}()
internal let thirdLabel: LabelForRowInList = {
let label = LabelForRowInList(frame: .zero)
return label
}()
internal let fourthLabel: LabelForRowInList = {
let label = LabelForRowInList(frame: .zero)
return label
}()
//MARK: - Init.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
translatesAutoresizingMaskIntoConstraints = false
clipsToBounds = true
backgroundColor = .clear
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Functions.
internal func setupViews() {
addSubview(firstLabel)
addSubview(secondLabel)
addSubview(thirdLabel)
addSubview(fourthLabel)
let firstConstraints = [
firstLabel.topAnchor.constraint(equalTo: topAnchor),
firstLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
firstLabel.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1/3),
firstLabel.widthAnchor.constraint(equalTo: widthAnchor)
]
NSLayoutConstraint.activate(firstConstraints)
let secondConstraints = [
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor),
secondLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
secondLabel.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1/3),
secondLabel.widthAnchor.constraint(equalTo: widthAnchor)
]
NSLayoutConstraint.activate(secondConstraints)
let thirdConstraints = [
thirdLabel.topAnchor.constraint(equalTo: secondLabel.bottomAnchor),
thirdLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
thirdLabel.trailingAnchor.constraint(equalTo: centerXAnchor),
thirdLabel.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1/3),
]
NSLayoutConstraint.activate(thirdConstraints)
let fourthConstraints = [
fourthLabel.topAnchor.constraint(equalTo: secondLabel.bottomAnchor),
fourthLabel.leadingAnchor.constraint(equalTo: centerXAnchor),
fourthLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
fourthLabel.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1/3)
]
NSLayoutConstraint.activate(fourthConstraints)
}
}
class RowForExperienceInProfileTable: SuperRowForProfileAttributesTable {
//MARK: - Properties.
internal var valueForExperienceRow: ExperienceModelForProfileAttributes! {
didSet {
firstLabel.text = valueForExperienceRow.jobTitle
secondLabel.text = valueForExperienceRow.companyName
thirdLabel.text = valueForExperienceRow.startedWork
fourthLabel.text = valueForExperienceRow.finishedWork
}
}
}
class RowForSkillInProfileTable: SuperRowForProfileAttributesTable {
//MARK: - Properties.
internal var valueForSkillRow: SkillsModelForProfileAttributes! {
didSet {
firstLabel.text = valueForSkillRow.skill
}
}
//MARK: - Init.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupViews() {
addSubview(firstLabel)
let firstConstraints = [
firstLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
firstLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
firstLabel.widthAnchor.constraint(equalTo: widthAnchor, constant: 0),
firstLabel.heightAnchor.constraint(equalTo: heightAnchor, constant: 0)
]
NSLayoutConstraint.activate(firstConstraints)
}
}
class RowForEducationInProfileTable: SuperRowForProfileAttributesTable {
//MARK: - Properties.
internal var valueForEducationRow: EducationModelForProfileAttributes! {
didSet {
firstLabel.text = valueForEducationRow.institutionName
secondLabel.text = valueForEducationRow.degreeName
thirdLabel.text = valueForEducationRow.startedStudy
fourthLabel.text = valueForEducationRow.finishedStudy
}
}
}
The VC:
fileprivate var experienceForProfile = [ExperienceModelForProfileAttributes(jobTitle: "Tester", companyName: "Testing Company", startedWork: "May 2019", finishedWork: "October 2019"), ExperienceModelForProfileAttributes(jobTitle: "Welder", companyName: "Welding Company", startedWork: "January 2018", finishedWork: "May 2020")]
fileprivate var skillsForProfile = [SkillsModelForProfileAttributes]()
fileprivate var educationForProfile = [EducationModelForProfileAttributes]()
fileprivate lazy var tableForAttributes: TableViewListType = {
let table = TableViewListType(frame: .zero, style: .plain)
table.delegate = self
table.dataSource = self
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.isHidden = true
tableForAttributes.register(RowForExperienceInProfileTable.self, forCellReuseIdentifier: firstIDForTable)
tableForAttributes.register(RowForSkillInProfileTable.self, forCellReuseIdentifier: secondIDForTable)
tableForAttributes.register(RowForEducationInProfileTable.self, forCellReuseIdentifier: thirdIDForTable)
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var numberOfRows = 0
switch selectedMimicIndex {
case 0: numberOfRows = experienceForProfile.count
case 1: numberOfRows = skillsForProfile.count
case 2: numberOfRows = educationForProfile.count
default: print("nu such rows for attributes table in own profile")
}
return numberOfRows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch selectedMimicIndex {
case 0: let cell = tableView.dequeueReusableCell(withIdentifier: firstIDForTable, for: indexPath) as! RowForExperienceInProfileTable
cell.valueForExperienceRow = experienceForProfile[indexPath.row]
return cell
case 1: let cell = tableView.dequeueReusableCell(withIdentifier: secondIDForTable, for: indexPath) as! RowForSkillInProfileTable
cell.valueForSkillRow = skillsForProfile[indexPath.row]
return cell
case 2: let cell = tableView.dequeueReusableCell(withIdentifier: thirdIDForTable, for: indexPath) as! RowForEducationInProfileTable
cell.valueForEducationRow = educationForProfile[indexPath.row]
return cell
default: return UITableViewCell()
}
}
The selectedMimicIndex value is changed the didSelectItem() function of the collection view that I have; a cv that controls what cells the table displays. reloadData() is called here after the Int value is changed when the user changes the selected cv cell.
Also notice that the second row is much taller that it should be; ignores the height that I have specified.
For every label you need to set and add it to contentView
firstLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(firstLabel)
also the most bottom label to bottom of cell
fourthLabel.bottomAnchor.constraint(equalTo:self.contentView.bottomAnchor, constant:-20)

Resources