Search code examples
iosswiftif-statementcastingnstableviewcell

How to go about changing the assignment of a custom cell variable based on if and else-if statements after the variable is declared? Swift


I’m trying to have a custom cell whom’s variable assignment changes based on if and else-if statements after the variable is declared.

This is to have the custom cell, which shows business hours of a selected business, be able to pick the custom table view cell that fits with the amount of open and closed time range pairs that I would like to show for the week for that business (as some businesses have more than one in one day).

However, I keep getting an error message saying:

Cannot assign value of type 'DetailsHoursContentsForAtLeastOneDayWithTwoOpenCloseTimeRangePairsTableViewCell' to type ‘DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell'

at this line of code:

detailsHoursContentsCustomCell = scrollableCitiesRestaurantDetailsTableView.dequeueReusableCell(withIdentifier: "DetailsHoursContentsForAtLeastOneDayWithTwoOpenCloseTimeRangePairsTableViewCell", for: indexPath) as! DetailsHoursContentsForAtLeastOneDayWithTwoOpenCloseTimeRangePairsTableViewCell

which is the line of code within the conditional statement of the first else-if statement, which changes the variable assignment of the custom cell variable I’m using. This line of code has a comment above it with the error message it got in the code snippet further below in this post. The conditional statement in the if-statement before this else-if statement, sets the value as the same value that the custom cell variable starts out as, and doesn’t get this type of error message, or any type of error message in it’s conditional statement.

I also get this same error message in the other else-if statements’ conditional statements, where the custom cell variable value is changed. There are also comments above these lines of code saying the error message that was received for that particular line of code in the code snippet further below.

What I’ve tried:

-Investigated whether using “as?” or “as” instead of force casting (using “as!”) in the lines of code where I’m getting these same types of error messages, might work, but it seemed to me like it wouldn’t.

How can I fix this error message, and/or how can I go about doing what I’m trying to do if the way I’m currently trying to do it won’t work (with the “correct” code being used)?

Thanks!

The code is below.

Code:

ViewController.swift:

import UIKit

//*Some code for a view controller for a table view file.*

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    if indexPath.row == 0 {
        
        //*Code for this cell goes here.*
        
        return custumCell
    }

    //*More "indexPath.row" code goes here for other table view cells being used.*

    if indexPath.row == 10 {
        
        var detailsHoursContentsCustomCell = scrollableCitiesRestaurantDetailsTableView.dequeueReusableCell(withIdentifier: "DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell", for: indexPath) as! DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell
        
        //If number of open hour time rnage pairs for all days is one.
        if selectedVenueDetailViewInfo.hours.numberOfOpenHoursTimeRangesAsStringTypeOfTableViewCellToUse == "OneTimeRange" {
            
            detailsHoursContentsCustomCell = scrollableCitiesRestaurantDetailsTableView.dequeueReusableCell(withIdentifier: "DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell", for: indexPath) as! DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell
        }
        
        //Else If number of open hour time rnage pairs for at least one day is two.
        else if selectedVenueDetailViewInfo.hours.numberOfOpenHoursTimeRangesAsStringTypeOfTableViewCellToUse == "TwoTimeRanges" {
            
            //Below is the line of code where I get the error message: "Cannot assign value of type 'DetailsHoursContentsForAtLeastOneDayWithTwoOpenCloseTimeRangePairsTableViewCell' to type 'DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell'".
            detailsHoursContentsCustomCell = scrollableCitiesRestaurantDetailsTableView.dequeueReusableCell(withIdentifier: "DetailsHoursContentsForAtLeastOneDayWithTwoOpenCloseTimeRangePairsTableViewCell", for: indexPath) as! DetailsHoursContentsForAtLeastOneDayWithTwoOpenCloseTimeRangePairsTableViewCell
        }
        
        //Else If number of open hour time rnage pairs for at least one day is three.
        else if selectedVenueDetailViewInfo.hours.numberOfOpenHoursTimeRangesAsStringTypeOfTableViewCellToUse == "ThreeTimeRanges" {
            
            //Below is the line of code where I get the error message: "Cannot assign value of type 'DetailsHoursContentsForAtLeastOneDayWithThreeOpenCloseTimeRangePairsTableViewCell' to type 'DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell'".
            detailsHoursContentsCustomCell = scrollableCitiesRestaurantDetailsTableView.dequeueReusableCell(withIdentifier: "DetailsHoursContentsForAtLeastOneDayWithThreeOpenCloseTimeRangePairsTableViewCell", for: indexPath) as! DetailsHoursContentsForAtLeastOneDayWithThreeOpenCloseTimeRangePairsTableViewCell
            
        }
        
        //Else If number of open hour time rnage pairs for at least one day is more than three.
        else if selectedVenueDetailViewInfo.hours.numberOfOpenHoursTimeRangesAsStringTypeOfTableViewCellToUse == "GreaterThanThreeTimeRanges" {
            
            //Below is the line of code where I get the error message: "Cannot assign value of type 'DetailsHoursContentsForAtLeastOneDayWithGreaterThanThreeOpenCloseTimeRangePairsTableViewCell' to type 'DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell'".
            detailsHoursContentsCustomCell = scrollableCitiesRestaurantDetailsTableView.dequeueReusableCell(withIdentifier: "DetailsHoursContentsForAtLeastOneDayWithGreaterThanThreeOpenCloseTimeRangePairsTableViewCell", for: indexPath) as! DetailsHoursContentsForAtLeastOneDayWithGreaterThanThreeOpenCloseTimeRangePairsTableViewCell
        }
        
        //Setting hours as labels.
        detailsHoursContentsCustomCell.sundayHoursOpenWithoutDayLabel.text = selectedVenueDetailViewInfo.hours.sundayOpenHoursWithoutDay
        detailsHoursContentsCustomCell.mondayHoursOpenWithoutDayLabel.text = selectedVenueDetailViewInfo.hours.mondayOpenHoursWithoutDay
        detailsHoursContentsCustomCell.tuesdayHoursOpenWithoutDayLabel.text = selectedVenueDetailViewInfo.hours.tuesdayOpenHoursWithoutDay
        detailsHoursContentsCustomCell.wednesdayHoursOpenWithoutDayLabel.text = selectedVenueDetailViewInfo.hours.wednesdayOpenHoursWithoutDay
        detailsHoursContentsCustomCell.thursdayHoursOpenWithoutDayLabel.text = selectedVenueDetailViewInfo.hours.thursdayOpenHoursWithoutDay
        detailsHoursContentsCustomCell.fridayHoursOpenWithoutDayLabel.text = selectedVenueDetailViewInfo.hours.fridayOpenHoursWithoutDay
        detailsHoursContentsCustomCell.saturdayHoursOpenWithoutDayLabel.text = selectedVenueDetailViewInfo.hours.saturdayOpenHoursWithoutDay
        
        return detailsHoursContentsCustomCell
    }
}

//*Some Extensions.*

The part of the model used for the business hours data that is shown in the detail view, after a restaurant is selected:

DetailViewModel.swift:

import Foundation

//*Other code for getting data such as name of restaurant, street address of restaurant, etc. for the detail view not using Codable. Am using NSDictionary.*

struct OpenHoursForDaysOfWeek {
    var mondayOpenHoursWithoutDay: String
    var tuesdayOpenHoursWithoutDay: String
    var wednesdayOpenHoursWithoutDay: String
    var thursdayOpenHoursWithoutDay: String
    var fridayOpenHoursWithoutDay: String
    var saturdayOpenHoursWithoutDay: String
    var sundayOpenHoursWithoutDay: String
    var numberOfOpenHoursTimeRangesAsStringTypeOfTableViewCellToUse: String
}

My Table View Cell Layouts:

AllDaysWithOneTimeRangeTableViewCell

AtLeastOneDayWithTwoTimeRangesTableViewCell

AtLeastOneDayWithThreeTimeRangesTableViewCell


Solution

  • Without seeing your UI design, I'm assuming:

    • the various cell types have some "common" UI elements
    • the differences are related to hours of operation?

    It is likely you could use a single cell design, and then show/hide the different UI elements as needed.

    For example:

    enter image description here

    That image shows 3 different prototype cells.

    However, the bottom cell has 3 "rows" in a vertical UIStackView. We could easily turn that into the first cell by hiding the 2nd and 3rd "row" and setting the yellow and cyan label text.


    If you feel your layout / design is too complex, and you want to stick with multiple cell prototypes, then I'd suggest having the cell classes handle parsing the data...

    So each cell could have a function like this:

        public func fillTheData(_ venue: VenueStruct) {
            sundayHoursOpenWithoutDayLabel.text = venue.hours.sundayOpenHoursWithoutDay
            mondayHoursOpenWithoutDayLabel.text = venue.hours.mondayOpenHoursWithoutDay
            tuesdayHoursOpenWithoutDayLabel.text = venue.hours.tuesdayOpenHoursWithoutDay
            wednesdayHoursOpenWithoutDayLabel.text = venue.hours.wednesdayOpenHoursWithoutDay
            thursdayHoursOpenWithoutDayLabel.text = venue.hours.thursdayOpenHoursWithoutDay
            fridayHoursOpenWithoutDayLabel.text = venue.hours.fridayOpenHoursWithoutDay
            saturdayHoursOpenWithoutDayLabel.text = venue.hours.saturdayOpenHoursWithoutDay
        }
    

    Presumably, each cell type would have different labels.text to set.

    Then your cellForRowAt looks like this:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        if indexPath.row == 0 {
            //*Code for this cell goes here.*
            return custumCell
        }
        
        //*More "indexPath.row" code goes here for other table view cells being used.*
        
        if indexPath.row == 10 {
            
            //If number of open hour time rnage pairs for all days is one.
            if selectedVenueDetailViewInfo.hours.numberOfOpenHoursTimeRangesAsStringTypeOfTableViewCellToUse == "OneTimeRange" {
                let cell = tableView.dequeueReusableCell(withIdentifier: "DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell", for: indexPath) as! DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell
                cell.fillData(selectedVenueDetailViewInfo)
                return cell
            }
            
            //Else If number of open hour time rnage pairs for at least one day is two.
            else if selectedVenueDetailViewInfo.hours.numberOfOpenHoursTimeRangesAsStringTypeOfTableViewCellToUse == "TwoTimeRanges" {
                
                //Below is the line of code where I get the error message: "Cannot assign value of type 'DetailsHoursContentsForAtLeastOneDayWithTwoOpenCloseTimeRangePairsTableViewCell' to type 'DetailsHoursContentsForAllDaysWithOneOpenCloseTimeRangePairTableViewCell'".
                let cell = tableView.dequeueReusableCell(withIdentifier: "DetailsHoursContentsForAtLeastOneDayWithTwoOpenCloseTimeRangePairsTableViewCell", for: indexPath) as! DetailsHoursContentsForAtLeastOneDayWithTwoOpenCloseTimeRangePairsTableViewCell
                cell.fillData(selectedVenueDetailViewInfo)
                return cell
            }
            
            // and so on
        }
    }
    

    Edit - after comments and seeing the layout goal...

    Now that I've seen your layout, this is a simpler task. It can be done with a single cell class, and no need to show/hide elements at all.

    Two options:

    • format each set of hours as an Attributed String
    • use stack views

    Each has its benefits and drawbacks... using an attributed string in a single label would be nice:

    enter image description here

    except --- there are differences in attributed strings between iOS versions. Somebody who is more familar with it might know how to work around that.

    Using stack views is pretty straightforward.

    Here's how it would look with yellow backgrounds so we can see the label framing:

    enter image description here

    and without the yellow:

    enter image description here

    I don't know how you are getting your data, so I made a Venue struct that looks like this:

    struct VenueStruct {
        // assuming there are some other properties
        var venueName: String = "Some Venue"
        var venueAddress: String = "123 Main Street"
        // etc...
        
        // array of operating hours
        //  this will be filled with 7 [String] arrays (one for each day of the week)
        //  each of those arrays will have 1 or more time ranges
        var hoursArray: [[String]] = []
    }
    

    Example hoursArrays - when filled with data:

    ["11:00 am - 10:00 pm"]
    ["11:00 am - 10:00 pm"]
    ["11:00 am - 10:00 pm"]
    ["11:00 am - 10:00 pm"]
    ["11:00 am - 10:00 pm"]
    ["11:00 am - 10:00 pm"]
    ["11:00 am - 10:00 pm"]
    -----
    ["7:00 am - 11:00 am", "12:00 pm - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    ["7:00 am - 11:00 am", "12:00 pm - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    -----
    ["7:00 am - 11:00 am", "12:00 pm - 4:00 pm", "5:00 pm - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    ["7:00 am - 11:00 am", "12:00 pm - 4:00 pm", "5:00 pm - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    ["10:00 am - 10:00 pm"]
    -----
    

    So, here's a sample cell class - all via code, so no Storyboard or @IBOutlet connections:

    class StackHoursCell: UITableViewCell {
        static let identifier: String = "StackHoursCell"
        
        let daysFont: UIFont = .systemFont(ofSize: 15.0, weight: .regular)
        let hoursFont: UIFont = .systemFont(ofSize: 15.0, weight: .regular)
        
        let outerStackView: UIStackView = {
            let v = UIStackView()
            v.axis = .vertical
            v.spacing = 8
            return v
        }()
        
        var dayNames: [String] = [
            "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
        ]
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            // we want to get the longest "day string" so we can align the time "column"
            //  we could use "Wednesday" but let's assume we may localize the app and
            //  "Wednesday" may not be the longest string
            
            // we could try German for example where "Donnerstag" ("Thursday") will be the longest
            //var calendar = Calendar.current
            //calendar.locale = Locale(identifier: "de_DE")
            //dayNames = calendar.weekdaySymbols
            
            var maxDayWidth: CGFloat = 0
            let v = UILabel()
            v.font = hoursFont
            dayNames.forEach { sDay in
                v.text = sDay
                v.sizeToFit()
                maxDayWidth = max(maxDayWidth, v.frame.width)
            }
            
            // fill the vertical stack view with 7 "rows"
            //  - horizontal stack views, each with Day name and Hours labels as arranged subviews
            for _ in 1...7 {
                let rowStack = UIStackView()
                rowStack.alignment = .top
                // "padding" so we have space to the time column
                rowStack.spacing = 32.0
                // day name label
                let v1 = UILabel()
                v1.font = daysFont
                v1.widthAnchor.constraint(equalToConstant: maxDayWidth).isActive = true
                // hours label
                let v2 = UILabel()
                v2.font = hoursFont
                v2.numberOfLines = 0
                v2.setContentHuggingPriority(.required, for: .vertical)
                v2.setContentCompressionResistancePriority(.required, for: .vertical)
                rowStack.addArrangedSubview(v1)
                rowStack.addArrangedSubview(v2)
                outerStackView.addArrangedSubview(rowStack)
                
                // during development, so we can see the label frames
                //[v1, v2].forEach { v in
                //  v.backgroundColor = .yellow
                //}
            }
            
            outerStackView.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(outerStackView)
            
            let g = contentView.layoutMarginsGuide
            NSLayoutConstraint.activate([
                // constrain outerStackView to the layoutMarginsGuid, with some
                //  left / top / bottom padding -- adjust to suit
                outerStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 24.0),
                outerStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
                outerStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                outerStackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
            ])
        }
        
        public func fillTheData(_ venue: VenueStruct) {
            var str: String = ""
            var row: Int = 0
            for (d, a) in zip(dayNames, venue.hoursArray) {
                // always safely unwrap optionals
                if let rowStack = outerStackView.arrangedSubviews[row] as? UIStackView {
                    if let v1 = rowStack.arrangedSubviews[0] as? UILabel,
                       let v2 = rowStack.arrangedSubviews[1] as? UILabel {
                        v1.text = d
                        str = ""
                        a.forEach { hStr in
                            str += hStr
                            if hStr != a.last {
                                str += "\n"
                            }
                        }
                        v2.text = str
                    }
                }
                row += 1
            }
        }
        
    }
    

    a simple sample data class to generate some sample values:

    class SampleVenueData: NSObject {
    
        // various hours strings... just to make it easier
        //  to generate some sample data
        func hoursString(_ s: String) -> String {
            var str = ""
            switch s {
            case "11to10":
                str = "11:00 am - 10:00 pm"
            case "7to11":
                str = "7:00 am - 11:00 am"
            case "12to10":
                str = "12:00 pm - 10:00 pm"
            case "10to10":
                str = "10:00 am - 10:00 pm"
            case "12to4":
                str = "12:00 pm - 4:00 pm"
            case "5to10":
                str = "5:00 pm - 10:00 pm"
            default:
                str = "24 Hours"
            }
            return str
        }
    
        func sampleData() -> [VenueStruct] {
            
            var venueData: [VenueStruct] = []
            
            // some sample data
            var v: VenueStruct!
            
            v = VenueStruct()
            v.hoursArray = []
            for _ in 0..<7 {
                v.hoursArray.append([hoursString("11to10")])
            }
            venueData.append(v)
            
            v = VenueStruct()
            v.hoursArray = []
            
            v.hoursArray.append([hoursString("7to11"), hoursString("12to10")])
            v.hoursArray.append([hoursString("10to10")])
            v.hoursArray.append([hoursString("7to11"), hoursString("12to10")])
            v.hoursArray.append([hoursString("10to10")])
            v.hoursArray.append([hoursString("10to10")])
            v.hoursArray.append([hoursString("10to10")])
            v.hoursArray.append([hoursString("10to10")])
            
            venueData.append(v)
            
            v = VenueStruct()
            v.hoursArray = []
            
            v.hoursArray.append([hoursString("7to11"), hoursString("12to4"), hoursString("5to10")])
            v.hoursArray.append([hoursString("10to10")])
            v.hoursArray.append([hoursString("7to11"), hoursString("12to4"), hoursString("5to10")])
            v.hoursArray.append([hoursString("10to10")])
            v.hoursArray.append([hoursString("10to10")])
            v.hoursArray.append([hoursString("10to10")])
            v.hoursArray.append([hoursString("10to10")])
            
            venueData.append(v)
    
            v = VenueStruct()
            v.hoursArray = []
            
            v.hoursArray.append([hoursString("7to11"), hoursString("5to10")])
            v.hoursArray.append([hoursString("7to11"), hoursString("5to10")])
            v.hoursArray.append([hoursString("7to11"), hoursString("12to4"), hoursString("5to10")])
            v.hoursArray.append([hoursString("7to11"), hoursString("5to10")])
            v.hoursArray.append([hoursString("7to11"), hoursString("12to4"), hoursString("5to10")])
            v.hoursArray.append([hoursString("10to10")])
            v.hoursArray.append([hoursString("10to10")])
            
            venueData.append(v)
            
            // replicate it a few times to confirm scrolling works
            venueData.append(contentsOf: venueData)
            venueData.append(contentsOf: venueData)
    
            return venueData
        }
        
    }
    

    and a sample view controller:

    class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
        
        var myData: [VenueStruct] = []
        
        let tableView = UITableView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            tableView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(tableView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
            
            tableView.register(StackHoursCell.self, forCellReuseIdentifier: StackHoursCell.identifier)
            
            tableView.dataSource = self
            tableView.delegate = self
            
            // let's get some sample data
            myData = SampleVenueData().sampleData()
            
            myData.forEach { v in
                v.hoursArray.forEach { h in
                    print(h)
                }
                print("-----")
            }
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return myData.count
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: StackHoursCell.identifier, for: indexPath) as! StackHoursCell
            c.fillTheData(myData[indexPath.row])
            return c
        }
        
    }