I'm trying to make a mixed picker in swift ui. Means: first part is made of values from a constant, second part is made of core data entity values.
My Code so far
Structs
struct StandardArten: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
}
struct Constants {
static let standardKontakte = [
StandardArten(name: "Hausarzt")
]
}
picker
Picker("Kategorie", selection: $kategorie) {
Text("Bitte wählen...").tag(Optional<UUID>(nil))
ForEach(Constants.standardKontakte, id: \.id) { kat in
Text(kat.name).tag(kat.id)
}
Divider()
ForEach(adressKategorien, id:\.id) { kat in
Text(kat.name ?? "ohne Titel").tag(kat.id)
}
}
The picker is displayed properly (see image), but selecting "Hausarzt" the picker "jumps back" to the old value.
So what am I doing wrong here?
Additional information:
$kategorie is declared as #State var kategorie: UUID?
Related
I have a multilevel model like:
class TaskDto : Identifiable, Equatable, ObservableObject {
var id: String
#Published var fileName:String
#Published var name: String
.... more stuff
#Published var parameters : JobParametersDto }
And
class JobParametersDto : Identifiable, Equatable, ObservableObject {
var id: String
#Published var blobs: [BlobRequestDto]
#Published var commands:[CommandDto]
#Published var mainTask: String
}
And so on, several embedded levels.
There is a number of views, each for one structure
struct BlobView: View {
#ObservedObject var taskDto: TaskDto
static let blobNames = [
"Product",
"Id",
"Name",
"Version",
"Limit",
"Unzip"]
let columns = Array(repeating: GridItem(.flexible(minimum: 100)), count: filterNames.count)
#State private var scrollViewContentSize: CGSize = .zero
var body: some View {
GridView(title: "Blob Names")
{
LazyVGrid(columns: columns) {
ForEach (Self.blobNames, id: \.self) {
blobName in
Text("\(blobName)").bold()
}
ForEach($taskDto.parameters.blobs ) {
blob in
if(! blob.Removed.wrappedValue) {
TextField("", text: f blob.BlobId)
TextField("", text: blob.BlobName)
TextField("", text: blob.BlobVersion)
Toggle("", isOn: filter.Unzip)
}
}
} //grid
}
}
The problem is when any field in the model changes the views do not reflect the change. For example I have a Toggle at the end of the View code. It does not update when I click on it until I click on another field.
Obviously this arrangement is not very valid. What is the right way to organize a model with embedded models in SiwftUI ?
In Swift and SwiftUI we use structs for the model types. This solves alot of the consistency issues we had with objects in ObjC. See Choosing Between Structures and Classes (Apple Developer Documentation)
We usually use a single object to persist or sync these model structs, usually its an environment object, a singleton, there are 2 singletons: one shared and one preview.
When using listRowBackground on a SwiftUI List there is no longer any highlighting of the selected item. Using a ButtonStyle for the NavigationLink does not work either.
Are there any sane workaround for this?
Example code:
struct ContentView: View {
struct ContentSection: Identifiable {
let id = UUID()
let title: String
let items: [String]
}
var sections = [
ContentSection(title: "Lorem", items: ["Dolor", "Sit", "Amed"]),
ContentSection(title: "Ipsum", items: ["Consectetur", "Adipiscing", "Elit"])
]
var body: some View {
NavigationView {
List {
ForEach(sections) { section in
Section {
ForEach(section.items, id: \.self) { item in
NavigationLink(destination: Text(item)) {
Text(item)
}
.listRowBackground(Color.orange.ignoresSafeArea())
}
} header: {
Text(section.title)
}
}
}
.listStyle(GroupedListStyle())
}
}
}
Although it is not documented in Apple's documentation, setting a .listRowBackground will wisely remove selection behaviour. What should happen if you set a background of Color.grey which matches the default selection color? Should Apple pick a different color now? How can they be sure the contrast is high enough for the user to tell if the selection is active?
Fortunately you can implement your own selection behaviour using List(selection:, content:) and then comparing the item being rendered in ForEach with the current selected item and changing the background yourself:
struct ContentView: View {
#State var selection: Int?
var body: some View {
List(selection: $selection) {
ForEach(1...5, id: \.self) { i in
Text(i, format: .number)
.listRowBackground(i == selection ? Color.red.opacity(0.5) : .white)
.tag(i)
}
}
}
}
Here it is in action:
Hopefully you can see what I'm trying to achieve from the code below but simply put, I'm trying to update .selectedTown which is binded to my Picker. The row tapped on will bind to .selectedTown which will then update the Text 'Your selected town is: [.selectedTown]'
However, the selected row is not binding and the text remains 'Your selected town is: '
struct ContentView: View {
struct Town: Identifiable {
let name: String
let id = UUID()
}
private var towns = [
Town(name: "Bristol"),
Town(name: "Oxford"),
Town(name: "Portsmouth"),
Town(name: "Newport"),
Town(name: "Glasgow"),
]
#State private var selectedTown: String = ""
var body: some View {
NavigationView {
VStack {
Form {
Section {
Picker("", selection: $selectedTown) {
ForEach(towns, id: \.id) {
Text("\($0.name)")
}
}
.pickerStyle(.inline)
.labelsHidden()
} header: {
Text("Random Towns")
}
}
Text("Your selected town is: \(selectedTown)")
.padding()
}
.navigationTitle("Random")
}
}
}
Hopefully this is just a small fix but I've tried for what seems a day to find a solutino and am now stuck. Any help would be gratefully received,
Simon
The types don't match. your array is a towns: [Town] and your selectedTown: String
Option 1 is to change the variable
#State private var selectedTown: Town = Town(name: "Sample")
Option 2 is to add a tag
Text("\($0.name)").tag($0.name)
Option 3 is change the variable and the tag
#State private var selectedTown: Town? = nil
Text("\($0.name)").tag($0 as? Town)
The "best" option depends on what you use selectedTown for.
The type of selection should be same as picked item or use tag, like below
Picker("", selection: $selectedTown) {
ForEach(towns, id: \.id) {
Text("\($0.name)").tag($0.name) // << here !!
}
}
Tested with Xcode 13.2 / iOS 15.2
hi am having issues with the picker view in swiftui
i have created one file with just a class like this
import Foundation
import SwiftUI
class something: ObservableObject {
#Published var sel = 0
}
and then I created 2 views
import SwiftUI
struct ContentView: View {
#StateObject var hihi: something
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(characters[hihi.sel])
}
now(hihi: something())
}
}
}
struct now: View {
#StateObject var hihi: something
var body: some View {
Text("\(hihi.sel)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(hihi: something())
}
}
now the problem am facing is that the code compiles but the picker ain't working it won't change to any other value in the array I have provided it recoils back to its original value provided that is 0th index "Makima" and it won't select any other option, why so?
please help
There are three problems, the main one being the mismatching selection.
In the Picker, your selection is based on the string value for each character. This is because the ForEach identifies each Text by the name string, since you used id: \.self.
However, your something model (which ideally should start with a capital letter by convention) has a numeric selection. Because the items in the Picker have String IDs, and this is an Int, the selection can't be set.
You can change your model to this:
class something: ObservableObject {
#Published var sel = "Makima"
}
Which also requires a slight change in the body:
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel) // Now getting string directly
}
now(hihi: something())
}
Notice we now have two views showing the selected character - but only the top one updates. The bottom one may now be redundant (the now view), but I'll show you how you can get it working anyway. This is where we encounter the 2nd problem:
You are creating a new instance of something() when passing it to now (again, should start with a capital). This means that the current instance of hihi stored in ContentView is not passed along. You are just creating a new instance of something, which uses the default value. This is completely independent from the hihi instance.
Replace:
now(hihi: something())
With:
now(hihi: hihi)
The final problem, which may not be as visible, is that you shouldn't be using #StateObject in now, since it doesn't own the object/data. Instead, the object is passed in, so you should use #ObservedObject instead. Although the example now works even without this change, you will have issues later on when trying to change the object within the now view.
Replace #StateObject in now with #ObservedObject.
Full answer (something is initialized in ContentView only for convenience of testing):
struct ContentView: View {
#StateObject var hihi: something = something()
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel)
}
now(hihi: hihi)
}
}
}
struct now: View {
#ObservedObject var hihi: something
var body: some View {
Text(hihi.sel)
}
}
class something: ObservableObject {
#Published var sel = "Makima"
}
SwiftUI Pickers appear not to work in iOS 14 as they used to.
The lower one works perfectly, while the first one produces this error:
"displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract."
Is there anything I am missing? I don't know how to get the upper version to work again?
It does work if I change the picker style to the Wheel, however I prefer to use the "DefaultPickerStyle", where it works like a Navigationlink revealing the list of options in a second page.
struct WeekdayPickerViewNEW1 : View{
#Binding var selection: Int
var text: String
var body: some View {
Picker(selection: self.$selection, label: Text(self.text)){
Text("1").tag(1)
Text("2").tag(2)
Text("3").tag(3)
}
}
}
struct WeekdayPickerViewNEW2 : View{
#State var test: Int = 0
var text: String
var body: some View {
Picker(selection: self.$test, label: Text(self.text)){
Text("A").tag(1)
Text("B").tag(2)
Text("C").tag(3)
}
}
}