Struct not changing with Binding - swiftui

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")
})
}
}

Related

Can’t change color of structure member through another structure

This is a mini version of the whole code affected by this problem. It has to make color of the city changing by button. Button is changing color of country, and city belong to this country changing it’s color as well. The problem is that structure City related with Country by Binding, and Country related with structure CountryColor by Binding too. Other way it doesn’t works. Using structure with #Binding as a #Binding requires to use $member within property initializer. Any ideas how can it be solved?
import SwiftUI
struct ContentView: View {
#State var countryColor = CountryColor()
var body: some View {
VStack {
Field()
ChangeColor(countryColor: $countryColor)
}
}
}
struct ChangeColor: View {
#Binding var countryColor: CountryColor
var body: some View {
Button("Change color") {
countryColor.color = Color.random()
}
}
}
struct Field: View {
#State var countryList = CountryList() ///here is the problem
var body: some View {
CityList(countryList: $countryList)
}
}
struct Country {
#State var name: String
#Binding var color: Color
}
struct City: View {
#State var name: String
#Binding var belong: Country
var body: some View {
ZStack {
Text(name).foregroundColor(belong.color)
}
}
}
struct CountryList {
#Binding var countryColor: CountryColor
var Countries: [Country] = []
init(countryColor: Binding<CountryColor>) {
self._countryColor = countryColor
self.Countries = [
Country(name: "USA", color: $countryColor.color)
]
}
}
struct CityList: View {
#Binding var countryList: CountryList
var Cities: [City] = []
init(countryList: Binding<CountryList>) {
self._countryList = countryList
self.Cities = [
City(name: "New-York", belong: $countryList.Countries[0])
]
}
var body: some View {
VStack {
ForEach(Cities, id:\.name) { value in
City(name: value.name, belong: value.$belong)
}
}
}
}
struct CountryColor {
var color = Color.purple
}
extension Color {
static func random() -> Color {
return Color(red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1))
}
}

SwiftUI - Binding var updates the original value only the first time, and then it doesn't update anymore

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.

Creating an edit detail view sheet that needs confirmation in SwiftUI

On Xcode 13 Beta 3, I am trying to find a good solution for an edit detail view presented in a sheet that needs to explicitly be confirmed.
In the DetailEditView, I initialise a #State property (editingModel) which is initialised from a #Binding (model) that I hand down.
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
//...
When I tap/press the confirm button in my sheet, I want to assign the altered editingModel to the passed model.
Button {
#warning("My expectation (saving changes by assigning `editingModel` to `model`) fails here…")
model = editingModel
isEditing = false
} label: {
Text("Done")
}
//...
While I do not have any build errors, the code does not work as expected–and I don't understand why. Look out for my #warning: that's where my code does not work as expected.
For all I know this could be a bug in the Xcode 13 Beta–or am I misunderstanding something fundamentally?
Here's all the code:
import SwiftUI
//MARK: - Main
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelStore)
}
}
}
//MARK: - Views
struct ContentView: View {
#SceneStorage("selection") var selection: Model.ID?
var body: some View {
NavigationView {
SidebarView(selection: $selection)
DetailView(modelSelection: $selection)
}
}
}
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var selection: Model.ID?
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $selection)
} label: {
Text(modelItem.id)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var modelSelection: Model.ID?
#State private var isEditing = false
var body: some View {
Form {
Text(modelBinding.wrappedValue.id)
}
.sheet(isPresented: $isEditing) {
DetailEditView(model: modelBinding, isEditing: $isEditing)
}
.toolbar {
ToolbarItem {
Button {
isEditing = true
} label: {
Label("Edit", systemImage: "pencil")
}
}
}
}
var modelBinding: Binding<Model> {
$modelStore[modelSelection]
}
}
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.id)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
#warning("My expectation (saving changes by assigning `editingModel` to `model`) fails here…")
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
//MARK: - Store
class ModelStore: ObservableObject {
#Published var models: [Model] = Model.mockModelArray()
subscript(modelId: Model.ID?) -> Model {
get {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
return models[modelIndex]
}
}
if models.isEmpty {
return Model(id: UUID().uuidString)
} else {
return models[0]
}
}
set(newValue) {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
models[modelIndex] = newValue
}
}
}
}
}
//MARK: - Models
struct Model: Identifiable {
var id: String
static func mockModel() -> Model {
Model(id: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
At first, do not edit id of Model. Instead use a new property and edit it.
//MARK: - Models
struct Model: Identifiable {
let id = UUID()
var content: String
static func mockModel() -> Model {
Model(content: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
For the first time you are in DetailView, selected model is not among the $modelStore.models. You need to send the first object of `` to the DetailsView.
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView(selection: $modelStore.models.first!)
.environmentObject(modelStore)
}
}
}
When you choose a model from SidebarView, the model in DetailView does not get updated. Send $modelItem to DetailView instead.
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $modelItem)
} label: {
Text(modelItem.content)
}
}
}
}
}
In DetailView, remove modelBinding and send modelSelection to DetailEditView.
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.content)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
All the code
#main
struct so_multipleSheetsApp: App {
#StateObject private var modelStore = ModelStore()
var body: some Scene {
WindowGroup {
ContentView(selection: $modelStore.models.first!)
.environmentObject(modelStore)
}
}
}
//MARK: - Views
struct ContentView: View {
#Binding var selection: Model
var body: some View {
NavigationView {
SidebarView()
DetailView(modelSelection: $selection)
}
}
}
struct SidebarView: View {
#EnvironmentObject var modelStore: ModelStore
var body: some View {
List {
ForEach($modelStore.models) { $modelItem in
NavigationLink {
DetailView(modelSelection: $modelItem)
} label: {
Text(modelItem.content)
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var modelStore: ModelStore
#Binding var modelSelection: Model
#State private var isEditing = false
var body: some View {
Form {
Text(modelSelection.content)
}
.sheet(isPresented: $isEditing) {
DetailEditView(model: $modelSelection, isEditing: $isEditing)
}
.toolbar {
ToolbarItem {
Button {
isEditing = true
} label: {
Label("Edit", systemImage: "pencil")
}
}
}
}
}
struct DetailEditView: View {
#Binding var model: Model
#Binding var isEditing: Bool
#State private var editingModel: Model
init(model: Binding<Model>, isEditing: Binding<Bool>) {
self._model = model
self._isEditing = isEditing
self._editingModel = State(initialValue: model.wrappedValue)
}
var body: some View {
VStack {
Form {
TextField("Model Id", text: $editingModel.content)
}
Spacer()
Divider()
HStack {
Button {
isEditing = false
} label: {
Text("Cancel")
}
Spacer()
Button {
model = editingModel
isEditing = false
} label: {
Text("Done")
}
}
.padding()
}
}
}
//MARK: - Store
class ModelStore: ObservableObject {
#Published var models: [Model] = Model.mockModelArray()
subscript(modelId: Model.ID?) -> Model {
get {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
return models[modelIndex]
}
}
if models.isEmpty {
return Model(content: UUID().uuidString)
} else {
return models[0]
}
}
set(newValue) {
if let id = modelId {
if let modelIndex = models.firstIndex(where: { $0.id == id }) {
models[modelIndex] = newValue
}
}
}
}
}
//MARK: - Models
struct Model: Identifiable {
let id = UUID()
var content: String
static func mockModel() -> Model {
Model(content: UUID().uuidString)
}
static func mockModelArray() -> [Model] {
var array = [Model]()
for _ in 0..<5 {
array.append(mockModel())
}
return array
}
}
Now upon confirmation the selected model is edited in all the views.

How can I have multiple instance of a Class/Model in SwiftUI?

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()
}
}

SwiftUI Picker desn't bind with ObservedObject

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()
}
}