I have a problem where a change in the View is not updating the underlying object in the model. Idea here is to generate a dynamic list of attributes of all different types (string, date, bool) and in the GUI all looks fine but when hitting the Save button, I can see that the data is not updated. What am I missing here?
Full working demo project below:
//
// ContentView.swift
// AttributeDemo
//
// Created by Max on 28.05.22.
//
import SwiftUI
public enum AttributeType: Codable, CaseIterable{
case int
case string
case datetime
case decimal
case double
case boolean
var stringValue: String {
switch self {
case .int: return "Full numbers"
case .string: return "Text"
case .datetime: return "Date"
case .decimal: return "Decimal"
case .double: return "Double"
case .boolean: return "Yes/No"
}
}
}
public class Attribute: Identifiable, Codable, ObservableObject {
public var id: UUID = UUID()
public var bkey: String = ""
public var tmp_create: Date = Date()
public var itemBkey: String = ""
public var attrType: AttributeType = .string
public var name: String = ""
public var description: String = ""
public var value_int: Int = 0
public var value_string: String = ""
public var value_datetime: Date = Date()
public var value_decimal: Decimal = 0.0
public var value_double: Double = 0.0
public var value_boolean: Bool = false
var userBkey: String = ""
var userToken: String = ""
}
struct ContentView: View {
#State private var attributes: [Attribute] = []
#State private var showingAttributeTypes = false
var body: some View {
VStack{
Button("Add attribute"){
self.showingAttributeTypes.toggle()
}
.confirmationDialog("Select a color", isPresented: $showingAttributeTypes, titleVisibility: .visible) {
Button("Text") {
addAttribute(attributeType: .string)
}
Button("Number") {
addAttribute(attributeType: .decimal)
}
Button("Date") {
addAttribute(attributeType: .datetime)
}
Button("Yes/No") {
addAttribute(attributeType: .boolean)
}
}
ForEach(self.attributes){value in
AttributeView(attribute: value)
}
Button("Save"){
self.attributes.forEach{value in
print(value.attrType.stringValue)
print(value.value_string)
print(value.value_datetime)
print(value.value_boolean)
print("--------------------------------")
}
}
}
}
func addAttribute(attributeType: AttributeType){
var attribute = Attribute()
attribute.attrType = attributeType
self.attributes.append(attribute)
}
}
struct AttributeView: View {
#ObservedObject var attribute: Attribute = Attribute()
#State private var description: String = ""
#State private var value_boolean: Bool = false
#State private var value_string: String = ""
#State private var value_decimal: Decimal = 0.0
#State private var value_double: Double = 0.0
#State private var value_datetime: Date = Date()
var body: some View {
HStack{
FormField(fieldName: "Description", fieldValue: $description)
.keyboardType(.default)
Spacer()
switch(attribute.attrType){
case .boolean:
Toggle(isOn: $value_boolean) {
Label("", image: "")
}
case .string:
TextField("", text: $value_string)
.keyboardType(.default)
case .datetime:
DatePicker(selection: $value_datetime, displayedComponents: .date, label: { Text("") })
case .decimal:
TextField("", value: $value_decimal, format: .number)
.keyboardType(.decimalPad)
case .double:
TextField("", value: $value_double, format: .number)
.keyboardType(.decimalPad)
default:
EmptyView()
}
}
}
}
struct FormField: View {
var fieldName = ""
#Binding var fieldValue: String
var body: some View{
TextField(fieldName, text: $fieldValue)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
To make changes in the View update the underlying object in the model, you could try a small re-structure of your code, where you make Attribute a struct, use an ObservableObject model to keep your array of Attributes, and use them like in this example code:
public struct Attribute: Identifiable, Codable { // <-- here
public var id: UUID = UUID()
public var bkey: String = ""
public var tmp_create: Date = Date()
public var itemBkey: String = ""
public var attrType: AttributeType = .string
public var name: String = ""
public var description: String = ""
public var value_int: Int = 0
public var value_string: String = ""
public var value_datetime: Date = Date()
public var value_decimal: Decimal = 0.0
public var value_double: Double = 0.0
public var value_boolean: Bool = false
var userBkey: String = ""
var userToken: String = ""
}
public class AttributeModel: ObservableObject { // <-- here
#Published var attributes: [Attribute] = [] // <-- here
}
struct ContentView: View {
#StateObject var model = AttributeModel() // <-- here
#State private var showingAttributeTypes = false
var body: some View {
VStack{
Button("Add attribute"){
self.showingAttributeTypes.toggle()
}
.confirmationDialog("Select a color", isPresented: $showingAttributeTypes, titleVisibility: .visible) {
Button("Text") {
addAttribute(attributeType: .string)
}
Button("Number") {
addAttribute(attributeType: .decimal)
}
Button("Date") {
addAttribute(attributeType: .datetime)
}
Button("Yes/No") {
addAttribute(attributeType: .boolean)
}
}
ForEach($model.attributes){ $value in // <-- here
AttributeView(attribute: $value)
}
Button("Save"){
model.attributes.forEach { value in
print("---> \(value)") // <-- here
print("--------------------------------")
}
}
}
}
func addAttribute(attributeType: AttributeType){
var attribute = Attribute()
attribute.attrType = attributeType
model.attributes.append(attribute)
}
}
struct AttributeView: View {
#Binding var attribute: Attribute // <-- here
var body: some View {
HStack{
FormField(fieldName: "Description", fieldValue: $attribute.description)
.keyboardType(.default)
Spacer()
switch(attribute.attrType){
case .boolean:
Toggle(isOn: $attribute.value_boolean) { // <-- here etc...
Label("", image: "")
}
case .string:
TextField("", text: $attribute.value_string)
.keyboardType(.default)
case .datetime:
DatePicker(selection: $attribute.value_datetime, displayedComponents: .date, label: { Text("") })
case .decimal:
TextField("", value: $attribute.value_decimal, format: .number)
.keyboardType(.decimalPad)
case .double:
TextField("", value: $attribute.value_double, format: .number)
.keyboardType(.decimalPad)
default:
EmptyView()
}
}
}
}
Related
I have this struct
struct MyObject {
var name:String
var color:String
var date:Date
init(name:String = "", color: String = "", date:Date = Date()) {
self.name = name
self.color = color
self.date = date
}
Then I have this on ContentView
#State private var temporaryObject = MyObject()
I send this to a view, like
var body: some View {
DoSomething($temporaryObject)
}
This is DoSomething
struct DoSomething: View {
#Binding var temporaryObject:MyObject
init(_ temporaryObject:Binding<MyObject>) {
self._temporaryObject = temporaryObject
}
var body: some View {
Button(action: {
// here is the problem
temporaryObject.name = "kkkk"
print(temporaryObject.name) // is equal to ""
}, label: {
Text("click me")
})
When I click the button, temporaryObject.name, in theory, is changed to "kkkk" but the print line shows it is still equals to empty.
why?
this example code works well for me. Does this code (taken from your question) not work for you?
struct ContentView: View {
#State private var temporaryObject = MyObject()
var body: some View {
VStack {
DoSomething(temporaryObject: $temporaryObject)
Text(temporaryObject.name) // <-- for testing
}
}
}
struct MyObject {
var name:String
var color:String
var date:Date
init(name:String = "", color: String = "", date:Date = Date()) {
self.name = name
self.color = color
self.date = date
}
}
struct DoSomething: View {
#Binding var temporaryObject:MyObject
var body: some View {
Button(action: {
temporaryObject.name = "kkkk"
print(temporaryObject.name) // is equal to "kkkk"
}, label: {
Text("click me")
})
}
}
Let's work our way up to the problem in code.
BASE
struct Animal: Identifiable, Hashable {
var id = UUID()
var name: String
}
//REQUIRED FOR USE IN SCENESTORAGE
extension Set: RawRepresentable where Element: Codable {
public typealias RawValue = String
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let string = String(data: data, encoding: .utf8)
else {
return "[]"
}
return string
}
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = Set(result)
}
}
1. WORKS
struct ContentView: View {
#State private var sampleData = [
Animal(name: "Bird"),
Animal(name: "Cat"),
Animal(name: "Dog"),
Animal(name: "Fish")
]
#State
private var multiSelection = Set<UUID>()
// #SceneStorage("multiSelection")
// private var multiSelection = Set<UUID>()
var body: some View {
NavigationSplitView {
List(sampleData, selection: $multiSelection) { animal in
Text(animal.name)
}
} detail: {
Text("Detail")
}
}
}
2. WORKS
struct ContentView: View {
#State private var sampleData = [
Animal(name: "Bird"),
Animal(name: "Cat"),
Animal(name: "Dog"),
Animal(name: "Fish")
]
// #State
// private var multiSelection = Set<UUID>()
#SceneStorage("multiSelection")
private var multiSelection = Set<UUID>()
var body: some View {
NavigationSplitView {
List(sampleData, selection: $multiSelection) { animal in
Text(animal.name)
}
} detail: {
Text("Detail")
}
}
}
3. WORKS
struct ContentView: View {
#State private var sampleData = [
Animal(name: "Bird"),
Animal(name: "Cat"),
Animal(name: "Dog"),
Animal(name: "Fish")
]
#State
private var multiSelection = Set<UUID>()
// #SceneStorage("multiSelection")
// private var multiSelection = Set<UUID>()
var body: some View {
NavigationSplitView {
List(sampleData, selection: $multiSelection) { animal in
NavigationLink(animal.name, value: animal.id)
}
} detail: {
Text("Detail")
}
}
}
4. BREAKS
struct ContentView: View {
#State private var sampleData = [
Animal(name: "Bird"),
Animal(name: "Cat"),
Animal(name: "Dog"),
Animal(name: "Fish")
]
// #State
// private var multiSelection = Set<UUID>()
#SceneStorage("multiSelection")
private var multiSelection = Set<UUID>()
var body: some View {
NavigationSplitView {
List(sampleData, selection: $multiSelection) { animal in
NavigationLink(animal.name, value: animal.id)
}
} detail: {
Text("Detail")
}
}
}
I can't see, other then a bug, why this wouldn't work. The expect behavior is that we can select the items and they stay selected. However, now the item is directly unselected as if it is not able to store.
How can we store this multiselection in SceneStorage so we can restore it?
The Animals are being init with different UUIDs every app launch, so the restored selected UUIDs cannot be found, one way to fix it is:
struct Animal: Identifiable {
let name: String
var id: String { name }
}
Also, it is preferable to do:
#SceneStorage("multiSelection")
private var multiSelection: Set<Animal.ID> = []
I have a Picker that updates a #Binding value, that is linked to the original #State value. The problem is that it gets updated only the first time I change it, and then it always remains like that. Not only in the sheet, but also in the ContentView, which is the original view in which the #State variable is declared. Here's a video showing the problem, and here's the code:
ContentView:
struct ContentView: View {
#State var cityForTheView = lisbon
#State var showCitySelection = false
var body: some View {
VStack {
//View
}
.sheet(isPresented: $showCitySelection) {
MapView(cityFromCV: $cityForTheView)
}
}
}
MapView:
struct MapView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var cityFromCV : CityModel
#State var availableCities = [String]()
#State var selectedCity = "Lisbon"
var body: some View {
NavigationView {
ZStack {
VStack {
Form {
HStack {
Text("Select a city:")
Spacer()
Picker("", selection: $selectedCity) {
ForEach(availableCities, id: \.self) {
Text($0)
}
}
.accentColor(.purple)
}
.pickerStyle(MenuPickerStyle())
Section {
Text("Current city: \(cityFromCV.cityName)")
}
}
}
}
}
.interactiveDismissDisabled()
.onAppear {
availableCities = []
for ct in cities {
availableCities.append(ct.cityName)
}
}
.onChange(of: selectedCity) { newv in
self.cityFromCV = cities.first(where: { $0.cityName == newv })!
}
}
}
CityModel:
class CityModel : Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var cityName : String
var country : String
var imageName : String
init(cityName: String, country: String, imageName : String) {
self.cityName = cityName
self.country = country
self.imageName = imageName
}
static func == (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
static func < (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
}
var cities = [
lisbon,
CityModel(cityName: "Madrid", country: "Spain", imageName: "Madrid"),
CityModel(cityName: "Barcelona", country: "Spain", imageName: "Barcelona"),
CityModel(cityName: "Paris", country: "France", imageName: "Paris")
]
var lisbon = CityModel(cityName: "Lisbon", country: "Portugal", imageName: "Lisbon")
What am I doing wrong?
The problem are:
at line 35: you use selectedCity to store the selected data from your picker.
Picker("", selection: $selectedCity) {
then at your line 46: you use cityFromCV.cityName to display the data which will always show the wrong data because you store your selected data in the variable selectedCity.
Section {
Text("Current city: \(cityFromCV.cityName)")
}
Solution: Just change from cityFromCV.cityName to selectedCity.
The first part of question is answered. Let's elaborate this example to:
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// leads to that I create a new viewModel every time, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}
My problem is now that when I type something into the TextField and press the return button on the keyboard the text is removed.
This is the most strange way of coding that i seen, how ever I managed to make it work:
I would like say that you can use it as leaning and testing, but not good plan for real app, How ever it was interesting to me to make it working.
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
class CreateNewCardViewModel: ObservableObject, Identifiable, Equatable {
init(_ id: Int) {
self.id = id
}
#Published var id: Int
#Published var definition: String = ""
#Published var show = false
static func == (lhs: CreateNewCardViewModel, rhs: CreateNewCardViewModel) -> Bool {
return lhs.id == rhs.id
}
}
let arrayOfModel: [CreateNewCardViewModel] = [ CreateNewCardViewModel(0), CreateNewCardViewModel(1), CreateNewCardViewModel(2),
CreateNewCardViewModel(3), CreateNewCardViewModel(4), CreateNewCardViewModel(5),
CreateNewCardViewModel(6), CreateNewCardViewModel(7), CreateNewCardViewModel(8),
CreateNewCardViewModel(9) ]
struct ReadModelView: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
struct MainView: View {
#State private var arrayOfModelState = arrayOfModel
#State private var showModel: Int?
#State private var isPresented: Bool = false
var body: some View {
VStack {
ForEach(Array(arrayOfModelState.enumerated()), id:\.element.id) { (index, item) in
Button(action: { showModel = index; isPresented = true }, label: { Text("Show Model " + item.id.description) }).padding()
}
if let unwrappedValue: Int = showModel {
Color.clear
.sheet(isPresented: $isPresented, content: { ReadModelView(viewModel: arrayOfModelState[unwrappedValue]) })
}
}
.padding()
}
}
I'm trying to fill up a Picker with data fetched asynchronously from external API.
This is my model:
struct AppModel: Identifiable {
var id = UUID()
var appId: String
var appBundleId : String
var appName: String
var appSKU: String
}
The class that fetches data and publish is:
class AppViewModel: ObservableObject {
private var appStoreProvider: AppProvider? = AppProvider()
#Published private(set) var listOfApps: [AppModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
self.loading = true
appStoreProvider?.dataProviderAppList { [weak self] (appList: [AppModel]) in
guard let self = self else {return}
DispatchQueue.main.async() {
self.listOfApps = appList
self.loading = false
}
}
}
init() {
fetchAppList()
}
}
The View is:
struct AppView: View {
#ObservedObject var appViewModel: AppViewModel = AppViewModel()
#State private var selectedApp = 0
var body: some View {
ActivityIndicatorView(isShowing: self.appViewModel.loading) {
VStack{
// The Picker doesn't bind with appViewModel
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName).tag(app.appName)
}
}
// The List correctly binds with appViewModel
List {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName.capitalized)
}
}
}
}
}
}
While the List view binds with the observed object appViewModel, the Picker doesn't behave in the same way. I can't realize why. Any help ?
I filed bug report, FB7670992. Apple responded yesterday, suggesting that I confirm this behavior in iOS 14, beta 1. It appears to now have been resolved.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Picker("", selection: $viewModel.wheelPickerValue) {
ForEach(viewModel.objects) { object in
Text(object.string)
}
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
}
Where
struct Object: Identifiable {
let id = UUID().uuidString
let string: String
}
class ViewModel: ObservableObject {
private var counter = 0
#Published private(set) var objects: [Object] = []
#Published var segmentedPickerValue: String = ""
#Published var wheelPickerValue: String = ""
fileprivate func nextSetOfValues() {
let newCounter = counter + 3
objects = (counter..<newCounter).map { value in Object(string: "\(value)") }
let id = objects.first?.id ?? ""
segmentedPickerValue = id
wheelPickerValue = id
counter = newCounter
}
init() {
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
self.nextSetOfValues()
}
timer.fire()
}
}
Results in:
I can't put this into your code because it is incomplete but here is a sample.
Pickers aren't meant to be dynamic. They have to be completely reloaded.
class DynamicPickerViewModel: ObservableObject {
#Published private(set) var listOfApps: [YourModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
loading = true
DispatchQueue.main.async() {
self.listOfApps.append(YourModel.addSample())
self.loading = false
}
}
init() {
fetchAppList()
}
}
struct DynamicPicker: View {
#ObservedObject var vm = DynamicPickerViewModel()
#State private var selectedApp = ""
var body: some View {
VStack{
//Use your loading var to reload the picker when it is done
if !vm.loading{
//Picker is not meant to be dynamic, it needs to be completly reloaded
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.vm.listOfApps){ app in
Text(app.name!).tag(app.name!)
}
}
}//else - needs a view while the list is being loaded/loading = true
List {
ForEach(self.vm.listOfApps){ app in
Text(app.name!.capitalized)
}
}
Button(action: {
self.vm.fetchAppList()
}, label: {Text("fetch")})
}
}
}
struct DynamicPicker_Previews: PreviewProvider {
static var previews: some View {
DynamicPicker()
}
}