TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

.NavigationDestination "String error"

Forums > SwiftUI

@khr  

Hello,

I am trying to write my first project by using the different examples from this site. But .NavigationDestination is giving me an error I don't know how to fix. The error is "Cannot convert value of type '([String], HeatControl) -> EditHeatControlView' to expected argument type '(HeatControl) -> EditHeatControlView'". I do understand that it has something to do with converting a string to something else.

My code:

import SwiftUI
import SwiftData

struct HeatControlView: View {
    @Query var heatcontrols: [HeatControl]
    @Environment(\.modelContext) var modelContext
    @State private var path = [HeatControl]()

    var body: some View {
        NavigationStack(path: $path) {
            List {
                ForEach(heatcontrols) { heatcontrol in
                    NavigationLink(value: heatcontrol) {
                        VStack(alignment: .leading) {
                            Text(heatcontrol.name)
                                .font(.headline)

                            Text(heatcontrol.date.formatted(date: .long, time: .omitted))
                        }
                    }
                }
                .onDelete(perform: deleteHeatControls)
            }
            .navigationTitle("Varmekontrol")
            .navigationDestination(for: HeatControl.self, destination: EditHeatControlView.init) // 
        }
    }

    func deleteHeatControls(_ indexSet: IndexSet) {
        for index in indexSet {
            let heatcontrol = heatcontrols[index]
            modelContext.delete(heatcontrol)
        }
    }

    func addHeatControls() {
        let heatcontrol = HeatControl()
        modelContext.insert(heatcontrol)
        path = [heatcontrol]
    }
}

#Preview {
    HeatControlView()
}

EditHeatControlView

import SwiftUI
import SwiftData

struct EditHeatControlView: View {
    var tempok = ["Ja", "Nej"]
    @Bindable var heatcontrol: HeatControl

    let formatter: NumberFormatter = {
            let formatter = NumberFormatter()
            formatter.numberStyle = .decimal
            return formatter
        }()

    var body: some View {
        Form {
            DatePicker("Dato", selection: $heatcontrol.date)
            TextField("Ret", text: $heatcontrol.name)
            TextField("Temperatur", value: $heatcontrol.temp, formatter: formatter)
                .keyboardType(.decimalPad)
            Picker("Er temperaturen OK?", selection: $heatcontrol.tempOK) {
                ForEach(tempok, id: \.self) {temp in
                    Text(temp)
                }
            }
            TextField("Fejlkilde", text: $heatcontrol.error)
            TextField("Udført af", text: $heatcontrol.doneBy)
        }
        .navigationTitle("Tilføj/Ændre Varmeholdelsesskema")
        .navigationBarTitleDisplayMode(.inline)
    }
}

#Preview {
    do {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try ModelContainer(for: HeatControl.self, configurations: config)

        let example = HeatControl(date: .now, name: "", temp: 0, tempOK: "", error: "", doneBy: "")
        return EditHeatControlView(heatcontrol: example)
            .modelContainer(container)
    } catch {
        fatalError("Failed to create model container.")
    }
}

3      

What does this error even mean?

Cannot convert value of type ([String], HeatControl) -> EditHeatControlView'
to expected argument type '(HeatControl) -> EditHeatControlView'".

Let's first look at a simple example. Maybe you have received compiler errors like this:

Cannot convert value of type String to expected argument type Int

This is easy to understand. You are trying to use a String in a place where Swift needs an Int.

Next level of difficulty:

Cannot convert value of type [String] to expected argument type [CarPart]

This is trickier, but carefully read the error message. Swift is expecting you to provide an array of CarPart objects. Instead you're providing an array of Strings

Next level of difficulty:

Cannot convert value of type () -> String to expected argument type (String) -> String

This is trickier, but carefully read the signature.
Swift is expecting a function that takes a String as an argument and returns a String, but you're providing a function that requires no arguments and returns a String.

Remember, in Swift Bool, Double, String, and Ints are all data types. But functions are ALSO types.

Now let's look at your example in detail.

expected argument type (HeatControl) -> EditHeatControlView

What is this type? A String? A Bool? It's neither. It is a function that takes a HeatControl type as an argument, performs some magic, and returns another type, an EditHeatControlView. This is what Swift is expecting you to provide.

But what are you providing?

([String], HeatControl) -> EditHeatControlView

You're providing a function that provides two parameters: an array of String, and a HeatControl object. This function returns an EditHeatControlView.

So where in your code do you define such a function? It's not obvious, but your EditHeatControlView is a struct and Swift creates an initializer for you when you define a struct. (It's a bit nerdy, but the formal name is: synthesized memberwise initializer.)

// Swift automatically creates an initializer for you. 
// The initializer is a function that takes two parameters, an array of String, and a HeatControl object
// The function returns another object, namely an EditHeatControlView
// --------------------------
struct EditHeatControlView: View {
    var tempok = ["Ja", "Nej"]              // <- Array of String
    @Bindable var heatControl: HeatControl  // <- HeatControl object

Now as you pointed out, the navigation destination is the source of your problem. It's expecting a destination view, but you are providing an initializer function that returns a view.

// Your code
.navigationDestination(for: HeatControl.self, destination: EditHeatControlView.init)    //  <-- here you provide an initializer

// Instead try
.navigationDestination(for: HeatControl.self) { selectedControl in
    // selectedControl is the HeatControl that is tapped by the user.        
    // It's passed in as a parameter.
    // Use the selectedControl to create a shiny new EditHeatControlView. Sweet!
    EditHeatControlView( heatControl: selectedControl)  // <--  Not tested. 
}

Also consider

If your temperature variable (tempok) is used only within the EditHeatControlView, consider making it a private variable. Then your default initializer will not require an array of String.

struct EditHeatControlView: View {
    private var tempok = ["Ja", "Nej"]      // <- Make this private
    @Bindable var heatControl: HeatControl  // <- HeatControl object

Fortsätt på din väg med datorkodning (Keep coding!)


For additional support about reading function signatures
See -> Required Skill: Reading Signatures

3      

@khr  

Thank you for helping me.

I have read and partialy understood your answer so I have some homework to do.. :)

I have used code from the project "iTour" found here: https://www.hackingwithswift.com/quick-start/swiftdata/swiftdata-tutorial-building-a-complete-project

Can you explain to me where my code is different from the guide? I have tried to write my code as close to his.

Thank you.

3      

In @twoStraw's solution for iTour his EditDestinationView struct is defined like this:

struct EditDestinationView: View {
    @Bindable var destination: Destination
    @State private var newSightName = ""  // <-- Uppmärksamhet! This is private!

    //. ..... snip ......

Notice how the newSightName is private to this struct. It can only be defined, or changed by code inside the struct. You do not pass in an inital value for newSightName when you initialize the struct.

In your code, you define an EditHeatControlView almost, but not quite, the same:

struct EditHeatControlView: View {
    var tempok = ["Ja", "Nej"]              // <-- Uppmärksamhet! This is public.
    @Bindable var heatControl: HeatControl  // <- HeatControl object

    // ..... snip ......

The synthesized memberwise initializer tells Swift that when you initialize this view, you will provide

  1. an array of String
  2. a (bindable) HeatControl object

Yet, in your navigationDestination, you tell SwiftUI that you are providing a HeatControl object (the one that your user tapped on). But you DO NOT PROVIDE the array of String.

// Your destination code...
.navigationDestination(for: HeatControl.self, destination: EditHeatControlView.init)  
//...............................................................^^^^^ EditHeatControlView.init expects an array of String

The error message you received says the same thing.

Cannot convert value of type
([String], HeatControl) -> EditHeatControlView
to expected argument type (HeatControl) -> EditHeatControlView

You are providing this:

([String], HeatControl) -> EditHeatControlView
.....^^^ this requires array of String

SwiftUI expects this:

(HeatControl) -> EditHeatControlView
...^^^ this does not provide array of String

3      

@khr  

I am glad you are taking your time to give these thorough explanations. Especialy to a newbee like me.. :)

But I need you to try to explain why @twoStraw's example are working without a private variable. I tried the first part of his code and, as I can see, there is no variable defined. newSightName is used later in his example. Both adding samples and adding a new destination works.

Here are the code for Contentview.

import SwiftUI
import SwiftData

struct ContentView: View {
    @Query var destinations: [Destination]
    @Environment(\.modelContext) var modelContext
    @State private var path = [Destination]()

    var body: some View {
        NavigationStack(path: $path) {
            List {
                ForEach(destinations) { destination in
                    NavigationLink(value: destination) {
                        VStack(alignment: .leading) {
                            Text(destination.name)
                                .font(.headline)

                            Text(destination.date.formatted(date: .long, time: .omitted))
                        }
                    }
                }
                .onDelete(perform: deleteDestinations)
            }
            .navigationTitle("iTour")
            .navigationDestination(for: Destination.self, destination: EditDestinationView.init)
            .toolbar {
                Button("Add Samples", action: addSamples)
                Button("Add Destination", systemImage: "plus", action: addDestination)
            }
        }
    }

    func addSamples() {
        let rome = Destination(name: "Rome")
        let florence = Destination(name: "Florence")
        let naples = Destination(name: "Naples")
        modelContext.insert(rome)
        modelContext.insert(florence)
        modelContext.insert(naples)
    }

    func deleteDestinations(_ indexSet: IndexSet) {
        for index in indexSet {
            let destination = destinations[index]
            modelContext.delete(destination)
        }
    }

    func addDestination() {
        let destination = Destination()
        modelContext.insert(destination)
        path = [destination]
    }
}

#Preview {
    ContentView()
}

And for EditDestinationView.

import SwiftUI
import SwiftData

struct EditDestinationView: View {
    @Bindable var destination: Destination

    var body: some View {
        Form {
            TextField("Name", text: $destination.name)
            TextField("Details", text: $destination.details, axis: .vertical)
            DatePicker("Date", selection: $destination.date)

            Section("Priority") {
                Picker("Priority", selection: $destination.priority) {
                    Text("Meh").tag(1)
                    Text("Maybe").tag(2)
                    Text("Must").tag(3)
                }
                .pickerStyle(.segmented)
            }
        }
        .navigationTitle("Edit Destination")
        .navigationBarTitleDisplayMode(.inline)
    }
}

#Preview {
    do {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try ModelContainer(for: Destination.self, configurations: config)

        let example = Destination(name: "Example Destination", details: "Example details go here and will automatically expand vertically as they are edited.")
        return EditDestinationView(destination: example)
            .modelContainer(container)
    } catch {
        fatalError("Failed to create model container.")
    }
}

3      

@CR7  

If you want to make both syntaxes work, here is the code you should replace

.navigationDestination(for: HashableStruct.self, destination: HashableStructView.init)

This gives an error, as there might be another variable in the code. from this to this:

.navigationDestination(for: HashableStruct.self) { Hashable in
  HashableStructView(variable: value)
}

3      

there's a type mismatch in the navigationDestination argument. You need to pass a single HeatControl object to EditHeatControlView, but the provided closure has a different signature.

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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

Reply to this topic…

You need to create an account or log in to reply.

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.