UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: ForEach Enum with occasional headers.

Forums > SwiftUI

Hi all Hacking with Swift friend, I am a near complete beginner trying to build a questionnaire archive/collation tool for my profession. It's going pretty well overall!

I am creating a list of navigation links in the present code (similar code will also be used for selectable list that generates views). Since there are many views I have used a ForEach to generate the navigationLink, however I need to organise them according to section headers. The ForEach requires .allCases and this means headers are produced for all navigation links instead of just a few. I can't work out the syntax to bind the correct header to each case from the enum.

This is the enum (sorry for long/awful names):

enum Step2AssessmentTypes: CaseIterable  {
        case PressurePainDetectionThreshold, ColdHypersensitivity, S_LANSS, DN4Questionnaire, CentralSensitivityInventory, PCS, TSK, PainSelf_EfficacyQuestionnaire, TraumaticInjuriesDistressScale, PatientHealthQuestionnaire9, HADS, PTSDChecklist_CivilianVersion

    var Step2Name : String {
        switch self {
        case .PressurePainDetectionThreshold:
            return "Pressure Pain Detection Threshold"
        case .ColdHypersensitivity:
            return "Cold Hypersensitivity"
        case .S_LANSS:
            return "S-LANSS"
        case .DN4Questionnaire:
            return "DN4 Questionnaire"
        case .CentralSensitivityInventory:
            return "Central Sensitivity Inventory"
        case .PCS:
            return "PCS"
        case .TSK:
            return "TSK"
        case .PainSelf_EfficacyQuestionnaire:
            return "Pain Self-Efficacy Questionnaire"
        case .TraumaticInjuriesDistressScale:
            return "Traumatic Injuries Distress Scale"
        case .PatientHealthQuestionnaire9:
            return "Patient Health Questionnaire - 9"
        case .HADS:
            return "HADS"
        case .PTSDChecklist_CivilianVersion:
            return "PTSD Checklist - Civilian Version"
        }
    }
    }

Then the viewbuilder:

@ViewBuilder func Step2AssessmentViewForType(type: Step2AssessmentTypes) -> some View {
        switch type {
        case .PressurePainDetectionThreshold:
            PressurePainDetectionThresholdView()
        case .ColdHypersensitivity:
            ColdHypersensitivityView()
        case .S_LANSS:
            S_LANSSView()
        case .DN4Questionnaire:
            DN4QuestionnaireView()
        case .CentralSensitivityInventory:
            CentralSensitivityInventoryView()
        case .PCS:
            PCSView()
        case .TSK:
            TSKView()
        case .PainSelf_EfficacyQuestionnaire:
            PainSelf_EfficacyQuestionnaireView()
        case .TraumaticInjuriesDistressScale:
            TraumaticInjuriesDistressScaleView()
        case .PatientHealthQuestionnaire9:
            PatientHealthQuestionnaire9View()
        case .HADS:
            HADSView()
        case .PTSDChecklist_CivilianVersion:
            PTSDChecklist_CivilianVersionView()
        }
    }

Finally, my attempt at assigning section headers to certain enum cases:

var body: some View {
    ScrollView {
    VStack (alignment: .leading, spacing: 10){
        ForEach(Step2AssessmentTypes.allCases, id: \.self) { assessment in
            Section(header: Text(assessment(.PressurePainDetectionThreshold("0"), .ColdHypersensitivity("1"), .CentralSensitivityInventory("2"), .PCS("3"), .TraumaticInjuriesDistressScale("4")))){
                NavigationLink(destination: Step2AssessmentViewForType(type: assessment)) {
                    Text(assessment.Step2Name)
                        .font(.title3)
                        .fontWeight(.medium)
                        .foregroundColor(.secondary)
                    Spacer()
                    Image(systemName: "arrow.right.circle")
                        .font(Font.title3.weight(.medium))
                        .foregroundColor(.secondary)
                }.buttonStyle(PlainButtonStyle())
            }
            .padding(10)
            .padding(.leading, 10)
            .frame(
                minWidth: 0,
                maxWidth: .infinity,
                alignment: .leading)
            }
    }
    }.padding()
}

This is returning the error message "he compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions".

I've been at this for hours and this is all I have come up with, any and all help is appreciated.

2      

I don't really understand what you are trying to do with this:

Text(assessment(.PressurePainDetectionThreshold("0"), .ColdHypersensitivity("1"), .CentralSensitivityInventory("2"), .PCS("3"), .TraumaticInjuriesDistressScale("4"))))

None of your Step2AssessmentTypes cases take associated values, so what are the ("0"), ("1"), etc for? For that matter, I don't understand what assessment(...) is all about either. assessment is the parameter to the content closure of the ForEach, it shouldn't be taking parameters itself.

And it's not quite clear to me if you want to show all the assessment types (because you are iterating allCases) but only have section headers for some of them, or you only want to show the assessment types that get a section header (because you only include 5 of the 12 types in the Section element).

At any rate, here's an attempt at what I think you are trying to do. Hopefully it's close enough that you can tweak it into shape and make it work.

enum Step2Assessment: String, RawRepresentable, CaseIterable, Identifiable {
    case PressurePainDetectionThreshold = "Pressure Pain Detection Threshold"
    case ColdHypersensitivity = "Cold Hypersensitivity"
    case S_LANSS = "S-LANSS"
    case DN4Questionnaire = "DN4 Questionnaire"
    case CentralSensitivityInventory = "Central Sensitivity Inventory"
    case PCS = "PCS"
    case TSK = "TSK"
    case PainSelf_EfficacyQuestionnaire = "Pain Self-Efficacy Questionnaire"
    case TraumaticInjuriesDistressScale = "Traumatic Injuries Distress Scale"
    case PatientHealthQuestionnaire9 = "Patient Health Questionnaire - 9"
    case HADS = "HADS"
    case PTSDChecklist_CivilianVersion = "PTSD Checklist - Civilian Version"

    //computed property to get name, which is just the case's rawValue
    var name: String { self.rawValue }

    //Identifiable so we can use in ForEach
    var id: String { self.rawValue }

    //so we can loop through just those cases which get headers
    static var headerCases: [Step2Assessment] {
        [.PressurePainDetectionThreshold, 
         .ColdHypersensitivity, 
         .CentralSensitivityInventory, 
         .PCS, 
         .TraumaticInjuriesDistressScale]
    }

    @ViewBuilder
    var view: some View {
        switch self {
        case .PressurePainDetectionThreshold:
            PressurePainDetectionThresholdView()
        case .ColdHypersensitivity:
            ColdHypersensitivityView()
        case .S_LANSS:
            S_LANSSView()
        case .DN4Questionnaire:
            DN4QuestionnaireView()
        case .CentralSensitivityInventory:
            CentralSensitivityInventoryView()
        case .PCS:
            PCSView()
        case .TSK:
            TSKView()
        case .PainSelf_EfficacyQuestionnaire:
            PainSelf_EfficacyQuestionnaireView()
        case .TraumaticInjuriesDistressScale:
            TraumaticInjuriesDistressScaleView()
        case .PatientHealthQuestionnaire9:
            PatientHealthQuestionnaire9View()
        case .HADS:
            HADSView()
        case .PTSDChecklist_CivilianVersion:
            PTSDChecklist_CivilianVersionView()
        }
    }
}

struct AssessmentView: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 10) {
                ForEach(Step2Assessment.headerCases) { assessment in
                    Section(header: Text(assessment.name)) {
                        NavigationLink(destination: assessment.view) {
                            Text(assessment.name)
                                .font(.title3)
                                .fontWeight(.medium)
                                .foregroundColor(.secondary)
                            Spacer()
                            Image(systemName: "arrow.right.circle")
                                .font(Font.title3.weight(.medium))
                                .foregroundColor(.secondary)
                        }.buttonStyle(PlainButtonStyle())
                    }
                    .padding(10)
                    .padding(.leading, 10)
                    .frame(
                        minWidth: 0,
                        maxWidth: .infinity,
                        alignment: .leading)
                }
            }
        }.padding()
    }
}

Some notes:

  1. I renamed Step2AssessmentTypes to Step2Assessment to make it better fit with Swift's naming conventions. I left the indiivdual cases as is.
  2. I've conformed the Step2Assessment enum to RawRepresentable using a String type so that you can eliminate the need for a Step2Name computed variable by just returning the rawValue from a computed property name.
  3. I conformed Step2Assessment to Identifiable so we can use it in a SwiftUI ForEach. That's not strictly necessary, since we could just use id: \.self in the ForEach. I just like it better this way.
  4. I added a headerCases static computed property to only return those cases of Step2Assessment which get headers, so we don't have to loop through all of them and pick out which ones to give a header.
  5. I changed your Step2AssessmentViewForType(type:) function to a computed property view on the Step2Assessment enum to keep everything encapsulated in one place.
  6. Most of your View's body then remains the same, just with the necessary adjustments to accomodate the preceding changes.

3      

Thankyou very much, that's made my code much clearer. Sorry you are right, I miscommunicated what I wanted. From how you said it, the first option is what I am trying to do:

And it's not quite clear to me if you want to show all the assessment types (because you are iterating allCases) but only have section headers for some of them, or you only want to show the assessment types that get a section header (because you only include 5 of the 12 types in the Section element).

I need all of (allCases for) the Step2Assessment names and views to make a full list, but the headers are only for some of Step2Assessment. What you have done makes all of the variables adhere to headerCases.

The "0", "1", "2", "3" was me trying to check if it worked... very confusing given how I wrote it.

These are the String s I want to pass into the header (different to the name), where the empty Strings ("") are to be ignored in headerCases:

var Step2Header : String {
            switch self {
            case .PressurePainDetectionThreshold:
                return "Nociceptive/Physiological"
            case .ColdHypersensitivity:
                return "Peripheral Neuropathic"
            case .S_LANSS:
                return ""
            case .DN4Questionnaire:
                return ""
            case .CentralSensitivityInventory:
                return "Central Nociplastic"
            case .PCS:
                return "Cognitive"
            case .TSK:
                return ""
            case .PainSelf_EfficacyQuestionnaire:
                return ""
            case .TraumaticInjuriesDistressScale:
                return "Emotional"
            case .PatientHealthQuestionnaire9:
                return ""
            case .HADS:
                return ""
            case .PTSDChecklist_CivilianVersion:
                return ""
            }
        }

Any further help and guidance you could provide would be much appreciated!

I'll try to learn as much as I can from your changes! Thanks again.

2      

Thanks for the clarification. Try this:

import SwiftUI

enum Step2Assessment: String, RawRepresentable, CaseIterable, Identifiable {
    case PressurePainDetectionThreshold = "Pressure Pain Detection Threshold"
    case ColdHypersensitivity = "Cold Hypersensitivity"
    case S_LANSS = "S-LANSS"
    case DN4Questionnaire = "DN4 Questionnaire"
    case CentralSensitivityInventory = "Central Sensitivity Inventory"
    case PCS = "PCS"
    case TSK = "TSK"
    case PainSelf_EfficacyQuestionnaire = "Pain Self-Efficacy Questionnaire"
    case TraumaticInjuriesDistressScale = "Traumatic Injuries Distress Scale"
    case PatientHealthQuestionnaire9 = "Patient Health Questionnaire - 9"
    case HADS = "HADS"
    case PTSDChecklist_CivilianVersion = "PTSD Checklist - Civilian Version"

    //computed property to get name, which is just the case's rawValue
    var name: String { self.rawValue }

    //Identifiable so we can use in ForEach
    var id: String { self.rawValue }

    //computed property to return the value, if any, a Section header should
    //have for each case
    var headerTitle: String? {
        //return the desired header text for sections that get headers
        switch self {
        case .PressurePainDetectionThreshold:
            return "Nociceptive/Physiological"
        case .ColdHypersensitivity:
            return "Peripheral Neuropathic"
        case .CentralSensitivityInventory:
            return "Central Nociplastic"
        case .PCS:
            return "Cognitive"
        case .TraumaticInjuriesDistressScale:
            return "Emotional"
        //and return nil for those that don't get a section header
        default:
            return nil
        }
    }

    @ViewBuilder
    var view: some View {
        switch self {
        case .PressurePainDetectionThreshold:
            PressurePainDetectionThresholdView()
        case .ColdHypersensitivity:
            ColdHypersensitivityView()
        case .S_LANSS:
            S_LANSSView()
        case .DN4Questionnaire:
            DN4QuestionnaireView()
        case .CentralSensitivityInventory:
            CentralSensitivityInventoryView()
        case .PCS:
            PCSView()
        case .TSK:
            TSKView()
        case .PainSelf_EfficacyQuestionnaire:
            PainSelf_EfficacyQuestionnaireView()
        case .TraumaticInjuriesDistressScale:
            TraumaticInjuriesDistressScaleView()
        case .PatientHealthQuestionnaire9:
            PatientHealthQuestionnaire9View()
        case .HADS:
            HADSView()
        case .PTSDChecklist_CivilianVersion:
            PTSDChecklist_CivilianVersionView()
        }
    }
}

struct AssessmentView: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 10) {
                ForEach(Step2Assessment.allCases) { assessment in
                    NavigationLink(destination: assessment.view) {
                        Text(assessment.name)
                            .font(.title3)
                            .fontWeight(.medium)
                            .foregroundColor(.secondary)
                        Spacer()
                        Image(systemName: "arrow.right.circle")
                            .font(Font.title3.weight(.medium))
                            .foregroundColor(.secondary)
                    }.buttonStyle(PlainButtonStyle())
                    .showSectionHeader(title: assessment.headerTitle)
                    .padding(10)
                    .padding(.leading, 10)
                    .frame(
                        minWidth: 0,
                        maxWidth: .infinity,
                        alignment: .leading)
                }
            }
        }.padding()
    }
}

//MARK: - View Modifier
//custom ViewModifier
struct ShowSectionHeader: ViewModifier {
    let title: String?

    func body(content: Content) -> some View {
        if let sectionTitle = title {
            Section(header: Text(sectionTitle)) {
                content
            }
        } else {
            content
        }
    }
}

//so we can use nice .showSectionHeader(title: "blah blah") syntax instead of having to do
//the uglier .modifier(ShowSectionHeader(title: "blah blah"))
extension View {
    func showSectionHeader(title: String?) -> some View {
        modifier(ShowSectionHeader(title: title))
    }
}

Here's how it works:

  1. There's now a headerTitle computed property on the Step2Assessment enum, which returns a String?. Those enum cases that get a header return the desired text for the header, those that don't return nil.
  2. I created a custom ViewModifier called showSectionHeader that takes a String? parameter. It just passes through the content if the string is nil but wraps it in a Section with the header being a Text element containing the title if it's not.
  3. We're back to iterating through allCases but we supply the value of our assessment.headerTitle computed property for each case to our custom showSectionHeader modifier and change the UI accordingly. So if assessment.headerTitle is nil, we just display the NavigationLink with the padding and frame modifiers attached directly to it. If assessment.headerTitle is not nil, then the NavigationLink gets wrapped in a Section and the padding and frame modifiers are attached to it.

And here's what it looks like in my simulator:

AssessmentView screenshot from Xcode 12 simulator

Now, you should be aware that what this does is essentially:

Section: HeaderTitle
    case name with header
case name without header
case name without header
Section: HeaderTitle
    case name with header
case name without header
etc...

If, instead, you want something like:

Section: HeaderTitle
    case name with header
    case name without header
    case name without header
Section: HeaderTitle
    case name with header
    case name without header
etc...

then that's a whole different kettle of fish and you would need to restructure your data in a more hierarchical fashion.

3      

OMG yes thankyou this works great. It's all coming together a lot better now thankyou. I think what you have given me there should work great.

In regards to your final comment about how it's actually working:

Now, you should be aware that what this does is essentially:

Section: HeaderTitle
    case name with header
case name without header
case name without header
Section: HeaderTitle
    case name with header
case name without header
etc...

If, instead, you want something like:

Section: HeaderTitle
    case name with header
    case name without header
    case name without header
Section: HeaderTitle
    case name with header
    case name without header
etc...

then that's a whole different kettle of fish and you would need to restructure your data in a more hierarchical fashion.

Will it matter if I do one or the other? What is the fundamental difference between those two organisations if they are just generating navigation links?

Thanks so much again.

2      

I think it mostly depends on how it's being presented in the app. Like SwiftUI might format items in a Section a little differently than items that aren't in a Section. Mostly I think that would probably matter in a List context (and depending on what ListStyle is applied) or in a Form but you are using a VStack in a ScrollView so it might not matter at all. Just something to be aware of as you tweak your GUI.

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.