I have created a quiet simple Picker and want to use an EnvironmentObject as datasource, but always when I use it, the Picker is springing back to default.
class HobbiesOfUser: ObservableObject {
#Published var hobbies = ["Swimming", "Football", "Hockey", "Rugby", "Other"]}
struct ContentView: View {
#EnvironmentObject var hobbiesOfUser: HobbiesOfUser
// #State var hobbiesOfUser = ["Swimming", "Football", "Hockey", "Rugby", "Other"]
var body: some View {
Picker(selection: $hobbiesOfUser.hobbies, label: Text("")) {
ForEach(0 ..< hobbiesOfUser.hobbies.count) {
Text(self.hobbiesOfUser.hobbies[$0]).tag($0)
}.labelsHidden()
}
}
}
I have tried to choose another form of Picker, but it has the same problem
Selection should be one item of array, not the complete array, as below
#EnvironmentObject var hobbiesOfUser: HobbiesOfUser
#State private var selected = "Swimming"
var body: some View {
Picker(selection: $selected, label: Text("")) {
ForEach(0 ..< hobbiesOfUser.hobbies.count) {
Text(self.hobbiesOfUser.hobbies[$0]).tag($0)
}.labelsHidden()
}
}
Related
I have a SwiftUI form in a MyFormSubView.swift where I have multiple #State variables representing individual fields like Text, etc. My issue is my parent view "ContentView.swift" also needs access to this information, and other subviews "OtherView.swift" also would benefit from access for display or editing. My current approach, is to change all the #State to #Binding, which creates a headache because some forms could have up to 20 fields with some optional... what is the best way to handle this? Is there a way to simply pass an object and have that be 'editable'?
Approaches:
(Current, problem approach) Have multiple individual variables declared as #State
in the ContentView.swift, and pass each individual variable into
MyFormSubView.swift with those variables having #Binding in front of
them that are mapped to swiftui elements to show up as 'placeholder
text' in textboxes, etc. This is bad as I have potentially up to 30 fields with some being optional.
(What I Think I Desire) Have identifiable model with all the
fields (and maybe pass this model into the MyFormSubView.swift, and
if it's possible, bind to it and just have it such that each field
is $mymodel.field1, $mymodel.field2, etc... which eliminates the
need to have 30+ variables passed into this thing.
(Maybe Better?) Use an #ObservableObject.
Is #2 possible? Or is there an even better way? Sample code would be great!
There are several ways to pass data like this across Views. Here is a quick implementation outlining 4 approaches.
You can use an #ObservableObject to reference a class with all of your data inside. The variables are #Published, which allows the View to update in the same way a #State variable would.
You can use an #StateObject. This is the same as #ObservableObject, except it will only initialize once and if the view re-renders the variable will persist (whereas an #ObservedObject would reinitialize). Read more about the difference here.
You can use an #EnvironmentObject. This is the same as #ObservedObject, except it is stored in the Environment, so you don't have to manually pass it between views. This is best when you have a complex view hierarchy and not every view needs a reference to the data.
You can create a custom Model and use a #State variable.
All of these methods work, but based on your description, I'd say the 2nd method is probably best for your situation.
class DataViewModel: ObservableObject {
#Published var text1: String = "One"
#Published var text2: String = "Two"
#Published var text3: String = "Three"
}
struct DataModel {
var text1: String = "Uno"
var text2: String = "Dos"
var text3: String = "Tres"
}
struct AppView: View {
var body: some View {
MainView()
.environmentObject(DataViewModel())
}
}
struct MainView: View {
#StateObject var dataStateViewModel = DataViewModel()
#ObservedObject var dataObservedViewModel = DataViewModel()
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#State var dataStateModel = DataModel()
#State var showSheet: Bool = false
#State var showOtherView: Bool = false
var body: some View {
VStack(spacing: 20) {
Text(dataStateViewModel.text1)
.foregroundColor(.red)
Text(dataObservedViewModel.text2)
.foregroundColor(.blue)
Text(dataEnvironmentViewModel.text3)
.foregroundColor(.green)
Text(dataStateModel.text1)
.foregroundColor(.purple)
Button(action: {
showSheet.toggle()
}, label: {
Text("Button 1")
})
.sheet(isPresented: $showSheet, content: {
FormView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
.environmentObject(dataEnvironmentViewModel) // Sheet is a new environment
})
Button(action: {
showOtherView.toggle()
}, label: {
Text("Button 2")
})
if showOtherView {
ThirdView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
}
}
}
}
struct FormView: View {
#StateObject var dataStateViewModel: DataViewModel
#ObservedObject var dataObservedViewModel: DataViewModel
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#Binding var dataStateModel: DataModel
#Environment(\.presentationMode) var presentationMode
var body: some View {
Form(content: {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("BACK")
})
Text("EDIT TEXT FIELDS:")
TextField("Placeholder 1", text: $dataStateViewModel.text1)
.foregroundColor(.red)
TextField("Placeholder 2", text: $dataObservedViewModel.text2)
.foregroundColor(.blue)
TextField("Placeholder 3", text: $dataEnvironmentViewModel.text3)
.foregroundColor(.green)
TextField("Placeholder 4", text: $dataStateModel.text1)
.foregroundColor(.purple)
})
}
}
struct ThirdView: View {
#StateObject var dataStateViewModel: DataViewModel
#ObservedObject var dataObservedViewModel: DataViewModel
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#Binding var dataStateModel: DataModel
var body: some View {
VStack(spacing: 20) {
Text(dataStateViewModel.text1)
.foregroundColor(.red)
Text(dataObservedViewModel.text2)
.foregroundColor(.blue)
Text(dataEnvironmentViewModel.text3)
.foregroundColor(.green)
Text(dataStateModel.text1)
.foregroundColor(.purple)
}
}
}
Use ObservalbeObject, here is simple example how you can share data:
Step 1 - Add some kind of state:
class AppState: ObservableObject {
#Published var value: String = ""
}
Step 2 - Pass state to ContentView via setting enviromentObject
ContentView()
.environmentObject(AppState())
Step 3 - Now AppState will be available in all child views of ContentView, so here is the code for ContentView and OtherView. OtherView has the TextField, text from will be saved to AppState and you can be able to see it, when you press 'back' from OtherView.
ContentView:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
Text("Content View")
.padding()
NavigationLink(
destination: OtherView()
) {
Text("Open other view")
}
Text("Value from other view: \(appState.value)")
}
}
}
}
OtherView:
import SwiftUI
struct OtherView: View {
#EnvironmentObject var appState: AppState
#State var value: String = ""
var body: some View {
VStack {
Text("Other View")
.padding()
TextField("Enter value", text: Binding<String>(
get: { self.value },
set: {
self.value = $0
appState.value = $0
}
))
.frame(width: 200)
.padding()
}
}
}
This is just simple example, for more complex cases you can take a look on VIPER or MVVM patterns in Swift UI. For example, here:
https://www.raywenderlich.com/8440907-getting-started-with-the-viper-architecture-pattern
https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios
I get this error every time I launch a secondary picker.
The first picker works okay.
However when I switch pickers & scroll, I get the following:
Here is my entire code (written as a test of this problem):
import SwiftUI
struct ContentView: View {
#State private var selectedItem = 0
#State private var isMainPickerHidden = false
#State private var isSecondaryPickerHidden = true
var colors = ["Red", "Green", "Blue", "Tartan"]
var sizes = ["Tiny", "Small", "Medium", "Large", "Super Size"]
var body: some View {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
ZStack {
VStack {
Picker(selection: $selectedItem, label: Text("Please choose a color")) {
ForEach(colors.indices, id: \.self) {
Text(self.colors[$0])
}
}.hiddenConditionally(isHidden: isMainPickerHidden)
Text("You selected: \(colors[selectedItem])")
.hiddenConditionally(isHidden: isMainPickerHidden)
}
VStack {
Picker(selection: $selectedItem, label: Text("Please choose a size")) {
ForEach(sizes.indices, id: \.self) {
Text(self.sizes[$0])
}
}.hiddenConditionally(isHidden: isSecondaryPickerHidden)
Text("You selected: \(sizes[selectedItem])")
.hiddenConditionally(isHidden: isSecondaryPickerHidden)
Spacer()
Button(action: {
isSecondaryPickerHidden = !isSecondaryPickerHidden
isMainPickerHidden = !isMainPickerHidden
}) {
Text("Switch Pickers")
}.padding()
}
}
}
}
}
// =========================================================================================================
extension View {
func hiddenConditionally(isHidden: Bool) -> some View {
isHidden ? AnyView(hidden()) : AnyView(self)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What is the correct syntax for the ForEach{} to avoid this problem?
This is because you use the same selectedItem for both pickers.
If in the second picker you select the last item (index 4) and then you switch to the first picker (max index = 3), then in this line:
Text("You selected: \(colors[selectedItem])")
you'll try accessing the index which is out of range.
To fix this you can use a separate #State variable for each picker:
struct ContentView: View {
#State private var selectedColorIndex = 0
#State private var selectedSizeIndex = 0
Picker(selection: $selectedColorIndex, label: Text("Please choose a color")) {
Picker(selection: $selectedSizeIndex, label: Text("Please choose a size")) {
The following code creates new controls every time a button is pressed at runtime, the problem is that the picker selection is set to the same state.
How can I create new controls with different state variables so they can operate separately ?
struct ContentView: View {
#State private var numberOfControlls = 0
#State var selection: String="1"
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
Picker(selection: self.$selection, label:
Text("Picker") {
Text("1").tag(1)
Text("2").tag(2)
}
}
}
}
}
How can I create new controls with different state variables so they can operate separately ?
Separate control into standalone view with own state (or view model if/when needed).
Here is a demo:
struct ContentView: View {
#State private var numberOfControlls = 0
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
ControlView()
}
}
}
}
struct ControlView: View {
#State var selection: String="1"
var body: some View {
Picker(selection: self.$selection, label:
Text("Picker")) {
Text("1").tag(1)
Text("2").tag(2)
}
}
}
I am pulling my hair out trying to figure out how to get my picker to show the already stored CoreData value. I want it to show on the right side of the picker as if the user just selected it. I have tried adding self.updatedItemAttribute = self.editItem.attribute ?? "" prior to the picker to set the initial value but that does not build. I also tried defining it in #State (e.g. #State var updatedItemAttribute: String = self.editItem.attribute) but that does not build either. If I add a TextField prior to the picker it will set the value, but I do not want to have a TextField with the value just to get it to set. Any ideas on how I get updatedItemAttribute set to editItem.attribute just prior to the picker? Thanks.
import CoreData
import SwiftUI
struct EditItemView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
#ObservedObject var editItem: Item
#State var updatedItemName: String = ""
#State var updatedItemAttribute: String = ""
let attributes = ["Red", "Purple", "Yellow", "Gold"]
var body: some View {
NavigationView {
Form {
Section {
TextField("Name of item", text: $updatedItemName)
.onAppear {
self.updatedItemName = self.editItem.name ?? ""
}
Picker("Test attribute", selection: self.$updatedItemAttribute) {
ForEach(attributes, id: \.self) {
Text($0)
.onAppear {
self.updatedItemAttribute = self.editItem.attribute ?? ""
}
}
}
}
...
You have to this in init as shown below
struct EditItemView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
#ObservedObject var editItem: Item
#State private var updatedItemName: String
#State private var updatedItemAttribute: String
init(editItem item: Item) { // << updated here
self.editItem = item
self._updatedItemName = State<String>(initialValue: item.name ?? "")
self._updatedItemAttribute = State<String>(initialValue: item.attribute ?? "")
}
let attributes = ["Red", "Purple", "Yellow", "Gold"]
var body: some View {
NavigationView {
Form {
Section {
TextField("Name of item", text: $updatedItemName)
Picker("Test attribute", selection: self.$updatedItemAttribute) {
ForEach(attributes, id: \.self) {
Text($0)
}
}
}
}
}
}
}
I can't find how to add some element in a picker view in SwiftUI, in my sample, I want add "Z" value in picker when I click the button.
struct ContentView: View {
#State var values: [String] = ["A", "B", "C"]
#State private var selectedValue = 0
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedValue, label: Text("Value")) {
ForEach(0 ..< values.count) {
Text(self.values[$0])
}
}
}
Button(action: {
self.values.append("Z")
}, label: {
Text("Add")
})
}.navigationBarTitle("Select a value")
}
}
When I click on the button, Z is added to "values" array but Picker is not refreshed.
Thank you :)
You must identify values by id for SwiftUI to make it's changes detectable:
ForEach(0 ..< self.values.count, id: \.self) {
Text(self.values[$0])
}
This way SwiftIU knowns it should rebuild the picker on change.
Tip: You can use elements directly like this:
ForEach(values, id: \.self) {
Text($0)
}
Don't forget to change the selectedValue type and value to match with the dataSource IF you followed the tip above:
#State private var selectedValue = "A"
Change selectedValue from int to String
#State private var selectedValue = "A"
add the parameter id and the tag modifier
Picker(selection: $selectedValue, label: Text("Value")) {
ForEach(values, id: \.self) {
Text($0).tag(String($0))
}
}