1

I've been struggling mightily to get multiple Pickers in Form layout to work in SwiftUI.

As a test, I have created a simple view that has some Text labels, some TextFields and two pickers. It is a Core Data app. The first picker is populated by first creating an array. The second is populated directly from Core Data. There doesn't seem to be any difference in function or errors.

The functionality seems to be working, but I get these errors that look like a real problem. Once I click on the Picker, I transition to the picker list without issue. Once I click on a list item, the picker list is dismissed and I return to ContentView with the appropriate choice listed in the picker. However, in both picker cases I get these errors:

1. ForEach, Int, Text> count (10) != its initial count (1). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!

2. [TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information ...and more verbiage.

This is the main view:

struct ContentView: View {

    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(fetchRequest: Clinic.getAllClinics()) var clinics: FetchedResults<Clinic>

    @State var nameArray = Clinic.makeClinicNameArray()

    @State private var selectArrayItem = 0
    @State private var selectCoreDataItem = 0
    @State private var tfOne = "TextField One"
    @State private var tfTwo = "TextField Two"
    @State private var tfThree = "TextField Three"
    @State private var tfFour = "TextField Four"

    var body: some View {
        NavigationView {

            Form {

            //I have include groups because in the real app I have many more than
            //ten views in the parent view
                Group {
                    TextField("enter a value for tfOne", text: $tfOne).foregroundColor(.blue)
                }

                Section(header: Text("From Generated Array:"), footer: Text("Section End")) {
                    Picker(selection: $selectArrayItem, label: Text("Choose Array Clinic")) {
                        ForEach(0 ..< nameArray.count) {
                            Text(self.nameArray[$0])
                        }
                    }
                    .foregroundColor(.red)

                    Picker(selection: $selectCoreDataItem, label: Text("Choose Core Data Clinic")) {
                        ForEach(0 ..< clinics.count) {
                            Text("\(self.clinics[$0].name ?? "Nameless")")
                        }
                    }.foregroundColor(.orange)

                    Text("$selectArrayItem is \(selectArrayItem)")
                    Text("item name is \(self.nameArray[selectArrayItem])")
                }//section 1

                Group {
                    TextField("enter a value for tfTwo", text: $tfTwo).foregroundColor(.blue)
                    TextField("enter a value for tfThree", text: $tfThree).foregroundColor(.blue)
                }

                Section(header: Text("From Core Data:"), footer: Text("Section End")) {
                    Text("$selectCoreDataItem is \(selectCoreDataItem)")
                    Text("item name is \(self.clinics[selectCoreDataItem].name ?? "Nameless")")
                }//section two

                TextField("enter a value for tfFour", text: $tfFour).foregroundColor(.blue)

            }//Form
            .navigationBarTitle(Text("Spinners"))

        }//nav view
    }
} 

After reading many SO posts and Apple docs I still have not found anything that relates to my situation.

For Reference, I had posted a similar question in SO 58784465. In that case changing the outside wrapper from a ScrollView to a Form worked - kind of - but ONLY for one picker. Adding a second picker broke the entire view and data handling. The above app was created just to solve these issues.

enter image description here

And the ManagedObject:

public class Clinic : NSManagedObject, Identifiable {
    @NSManaged public var myID: UUID
    @NSManaged public var name: String?
    @NSManaged public var comment: String?
    @NSManaged public var isShown: Bool
}

extension Clinic {

    static func getAllClinics() -> NSFetchRequest<Clinic> {
        let request: NSFetchRequest<Clinic> = Clinic.fetchRequest() as! NSFetchRequest<Clinic>
        let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
        request.sortDescriptors = [sortDescriptor]
        return request
    }

    static func makeClinicArray() -> [Clinic] {
        let kAppDelegate = UIApplication.shared.delegate as! AppDelegate
        let request: NSFetchRequest<Clinic> = Clinic.fetchRequest() as! NSFetchRequest<Clinic>
        let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
        request.sortDescriptors = [sortDescriptor]

        do {
            let results = try kAppDelegate.persistentContainer.viewContext.fetch(request)
            return results
        } catch {
            print("error retrieving filtered picker1s")
        }
        return [Clinic]()
    }

    static func makeClinicNameArray() -> [String] {

        var returnedArray = [String]()
        let kAppDelegate = UIApplication.shared.delegate as! AppDelegate
        let request: NSFetchRequest<Clinic> = Clinic.fetchRequest() as! NSFetchRequest<Clinic>
        let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
        request.sortDescriptors = [sortDescriptor]

        do {
            let results = try kAppDelegate.persistentContainer.viewContext.fetch(request)
            for result in results {
                returnedArray.append(result.name ?? "Nameless")
            }
        } catch {
            print("error retrieving filtered picker1s")
        }
        return returnedArray
    }

}

Xcode Version 11.2.1 (11B500) Any guidance would be appreciated.

EDIT: Adding Core Data setup in SceneDelegate (AppDelegate code is strictly stock.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)

        let tabby = TabBar().environment(\.managedObjectContext, managedObjectContext)

        window.rootViewController = UIHostingController(rootView: tabby)
        self.window = window
        window.makeKeyAndVisible()
    }
}

And very simple TabBar: struct TabBar: View {

@Environment(\.managedObjectContext) var managedObjectContext
@State private var selectionValue = 1

var body: some View {
    TabView(selection : $selectionValue) {
        ContentView().tabItem ({
            Image(systemName: "house")
            Text("Home")
        })
        .tag(1)

        Utilities().tabItem ({
            Image(systemName: "hammer")
            Text("Utilities")
        })
        .tag(2)

        StartView().tabItem ({
            Image(systemName: "forward.fill")
            Text("Start")
        })
        .tag(3)
    }
}

}

2
  • Error 1 explains two solutions, have you tried that? Commented Nov 17, 2019 at 1:59
  • Yes. No joy. If I specifically state the id: then the picker does not return the chosen item. Seems very weird. At any rate, the ManagedObject IS Identifiable. Commented Nov 17, 2019 at 2:07

1 Answer 1

2

The answer to #1 is almost certainly that you need to use the ForEach initializer with the id parameter, and the type of your selection must match the type of your id.

So, for instance

@State private var selectArrayItem = 0

...

Picker(selection: $selectArrayItem, label: Text("Choose Array Clinic")) {
    ForEach(0 ..< nameArray.count, id: \.self) {
        Text(self.nameArray[$0])
    }
}

will work because selectArrayItem and id are both Int, or

@State private var selectName = "" 

...

Picker(selection: $selectArrayItem, label: Text("Choose Array Clinic")) {
    ForEach(nameArray, id: \.self) {
        Text($0)
    }
}

will work because selectName and id are both String.

As for #2, I'm almost positive it's a bug in SwiftUI. I see the mentioned error message printed when I click on the "Choose Array Clinic" row, and the list of Clinics is presented. You'll notice that after the picker view is pushed onto the navigation stack, the table rows jump slightly shortly after appearing.

The error message and the visual jump make me speculate that SwiftUI is trying to lay out the rows before it's technically part of the navigation stack, which prints the error, and then as soon as it gets added, the view row offsets are recalculated, leading to the jump. None of that is under your control, and happens with the simplest Form/Picker demo code. Best thing is to report it as a bug and live with it for now. Other than the visual jump, there doesn't appear to be any functional problem at this time.

Sign up to request clarification or add additional context in comments.

6 Comments

I see. I had not understood that the ForEach initializer and the Picker selection had to be the same type. When I tried id: I was using the myID attribute which is a UUID. Making both the array and Core Data versions have id: \.self fixes the issue and both now produce the correct results without error number 1 above. Do you have any ideas for the error 2 - TableView layout? My Core Data implementation is pretty much boiler plate but I will edit the question and add it.
I am marking this question as answered since the error 1 was much more significant. I'd still like comments on question 2 if anyone can help.
I'll take a look when I can get back to my computer.
I added a bit at the end about the second error. TL;DR is, probably a SwiftUI bug.
Understood. I will report it. Thanks.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.