Two way binding to multiple instances - swift

I have a view with a ForEach with multiple instances of another view. I want to be able to:
Click a button in the main view and trigger validation on the nested views, and
On the way back, populate an array with the results of the validations
I've simplified the project so it can be reproduced. Here's what I have:
import SwiftUI
final class AddEditItemViewModel: ObservableObject {
#Published var item : String
#Published var isValid : Bool
#Published var doValidate: Bool {
didSet{
print(doValidate) // This is never called
validate()
}
}
init(item : String, isValid : Bool, validate: Bool) {
self.item = item
self.isValid = isValid
self.doValidate = validate
}
func validate() { // This is never called
isValid = Int(item) != nil
}
}
struct AddEditItemView: View {
#ObservedObject var viewModel : AddEditItemViewModel
var body: some View {
Text(viewModel.item)
}
}
final class AddEditProjectViewModel: ObservableObject {
let array = ["1", "2", "3", "nope"]
#Published var countersValidationResults = [Bool]()
#Published var performValidation = false
init() {
for _ in array {
countersValidationResults.append(false)
}
}
}
struct ContentView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
#State var result : Bool = false
var body: some View {
VStack {
ForEach(
viewModel.countersValidationResults.indices, id: \.self) { i in
AddEditItemView(viewModel: AddEditItemViewModel(
item: viewModel.array[i],
isValid: viewModel.countersValidationResults[i],
validate: viewModel.performValidation
)
)
}
Button(action: {
viewModel.performValidation = true
result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: AddEditProjectViewModel())
}
}
When I change the property in the main view, the property doesn't change in the nested views, even though it's a #Published property.
Since this first step is not working, I haven't even been able to test the second part (updating the array of books with the validation results)
I need the setup to be like this because if an item is not valid, that view will show an error message, so the embedded views need to know whether they are valid or not.
UPDATE:
My issue was that you can't seem to be able to store Binding objects in view models, only in views, so I moved my properties to the view, and it works:
import SwiftUI
final class AddEditItemViewModel: ObservableObject {
#Published var item : String
init(item : String) {
self.item = item
print("item",item)
}
func validate() -> Bool{
return Int(item) != nil
}
}
struct AddEditItemView: View {
#ObservedObject var viewModel : AddEditItemViewModel
#Binding var doValidate: Bool
#Binding var isValid : Bool
init(viewModel: AddEditItemViewModel, doValidate:Binding<Bool>, isValid : Binding<Bool>) {
self.viewModel = viewModel
self._doValidate = doValidate
self._isValid = isValid
}
var body: some View {
Text("\(viewModel.item): \(isValid.description)").onChange(of: doValidate) { _ in isValid = viewModel.validate() }
}
}
struct ContentView: View {
#State var performValidation = false
#State var countersValidationResults = [false,false,false,false] // had to hard code this here
#State var result : Bool = false
let array = ["1", "2", "3", "nope"]
// init() {
// for _ in array {
// countersValidationResults.append(false) // For some weird reason this appending doesn't happen!
// }
// }
var body: some View {
VStack {
ForEach(array.indices, id: \.self) { i in
AddEditItemView(viewModel: AddEditItemViewModel(item: array[i]), doValidate: $performValidation, isValid: $countersValidationResults[i])
}
Button(action: {
performValidation.toggle()
result = countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
Text(countersValidationResults.description)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

I'm having trouble reconciling the question with the example code and figuring out what's supposed to be happening. Think that there are a few issues going on.
didSet will not get called on #Published properties. You can (SwiftUI - is it possible to get didSet to fire when changing a #Published struct?) but the gist is that it's not a normal property, because of the #propertyWrapper around it
You say in your question that you want a "binding", but you never in fact us a Binding. If you did want to bind the properties together, you should look into using either #Binding or creating a binding without the property wrapper. Here's some additional reading on that: https://swiftwithmajid.com/2020/04/08/binding-in-swiftui/
You have some circular logic in your example code. Like I said, it's a little hard to figure out what's a symptom of the code and what you're really trying to achieve. Here's an example that strips away a lot of the extraneous stuff going on and functions:
struct AddEditItemView: View {
var item : String
var isValid : Bool
var body: some View {
Text(item)
}
}
final class AddEditProjectViewModel: ObservableObject {
let array = ["1", "2", "3"]// "nope"]
#Published var countersValidationResults = [Bool]()
init() {
for _ in array {
countersValidationResults.append(false)
}
}
func validate(index: Int) { // This is never called
countersValidationResults[index] = Int(array[index]) != nil
}
}
struct ContentView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
#State var result : Bool = false
var body: some View {
VStack {
ForEach(
viewModel.countersValidationResults.indices, id: \.self) { i in
AddEditItemView(item: viewModel.array[i], isValid: viewModel.countersValidationResults[i])
}
Button(action: {
viewModel.array.enumerated().forEach { (index,_) in
viewModel.validate(index: index)
}
result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
}
}
}
Note that it your array, if you include the "nope" item, not everything validates, since there's a non-number, and if you omit it, everything validates.
In your case, there really wasn't the need for that second view model on the detail view. And, if you did have it, at least the way you had things written, it would have gotten you into a recursive loop, as it would've validated, then refreshed the #Published property on the parent view, which would've triggered the list to be refreshed, etc.
If you did get in a situation where you needed to communicate between two view models, you can do that by passing a Binding to the parent's #Published property by using the $ operator:
class ViewModel : ObservableObject {
#Published var isValid = false
}
struct ContentView : View {
#ObservedObject var viewModel : ViewModel
var body: some View {
VStack {
ChildView(viewModel: ChildViewModel(isValid: $viewModel.isValid))
}
}
}
class ChildViewModel : ObservableObject {
var isValid : Binding<Bool>
init(isValid: Binding<Bool>) {
self.isValid = isValid
}
func toggle() {
isValid.wrappedValue.toggle()
}
}
struct ChildView : View {
#ObservedObject var viewModel : ChildViewModel
var body: some View {
VStack {
Text("Valid: \(viewModel.isValid.wrappedValue ? "true" : "false")")
Button(action: {
viewModel.toggle()
}) {
Text("Toggle")
}
}
}
}

Related

SwiftUI Navigation View goes back in hierarchy

I have a problem with Navigation View hierarchy.
All screens in my app use the same ViewModel.
When a screen inside navigation link updates the ViewModel (here it is called DataManager), the navigation view automatically goes back to the first screen, as if the "Back" button was pressed.
Here's what it looks like
I tried to shrink my code as much as I could
struct DataModel: Identifiable, Codable {
var name: String
var isPinned: Bool = false
var id: String = UUID().uuidString
}
class DataManager: ObservableObject {
#Published private(set) var allModules: [DataModel]
var pinnedModules: [DataModel] {
allModules.filter { $0.isPinned }
}
var otherModules: [DataModel] {
allModules.filter { !$0.isPinned }
}
func pinModule(id: String) {
if let moduleIndex = allModules.firstIndex(where: { $0.id == id }) {
allModules[moduleIndex].isPinned = true
}
}
func unpinModule(id: String) {
if let moduleIndex = allModules.firstIndex(where: { $0.id == id }) {
allModules[moduleIndex].isPinned = false
}
}
static let instance = DataManager()
fileprivate init() {
allModules =
[DataModel(name: "One"),
DataModel(name: "Two"),
DataModel(name: "Three"),
DataModel(name: "Four"),
DataModel(name: "Five")]
}
}
struct ModulesList: View {
#StateObject private var dataStorage = DataManager.instance
var body: some View {
NavigationView {
List {
Section("Pinned") {
ForEach(dataStorage.pinnedModules) { module in
ModulesListCell(module: module)
}
}
Section("Other") {
ForEach(dataStorage.otherModules) { module in
ModulesListCell(module: module)
}
}
}
}
}
fileprivate struct ModulesListCell: View {
let module: DataModel
var body: some View {
NavigationLink {
SingleModuleScreen(module: module)
} label: {
Text(module.name)
}
}
}
}
struct SingleModuleScreen: View {
#State var module: DataModel
#StateObject var dataStorage = DataManager.instance
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(module.name)
.font(.title)
Button {
dataStorage.pinModule(id: module.id)
} label: {
Text("Pin")
}
}
}
}
}
I can guess because when your dataStorage changed, the ModulesList will be redrawn, that cause all current ModulesListCell removed from memory.
Your cells are NavigationLink and when it destroyed, the navigation stack doesn't keep the screen that's being linked.
I would recommend to watch this wwdc https://developer.apple.com/videos/play/wwdc2021/10022/ and you will know how to manage your view identity properly when your data's changed.
When you tap Pin, your otherModules array is recreated and you do not have the view in Navigation Stack from where you navigated. Thus you are going back automatically, which is desired behaviour. So the solution is Don't destroy your array from where your NavigationLink is created. Make a temporary published array, load Other modules from that array and change the array onAppear like below:
As a workaround which is working in my end:
Add this line in DataManger:
#Published var tempOtherModules:[DataModel] = []
Change your ModulesList like below
struct ModulesList: View {
#StateObject private var dataStorage = DataManager.instance
var body: some View {
NavigationView {
List {
Section("Pinned") {
ForEach(dataStorage.pinnedModules) { module in
ModulesListCell(module: module)
}
}
Section("Other") {
ForEach(dataStorage.tempOtherModules) { module in
ModulesListCell(module: module)
}
}
}.onAppear {
dataStorage.tempOtherModules = dataStorage.otherModules
}
}
}
}

Making custom get {} set{} to work like dynamic proxy/shortcut to different objects in Array. (SwiftUI) [duplicate]

This question already has answers here:
How to change a value of struct that is in array?
(2 answers)
Closed 1 year ago.
I'm trying to achieve a two way binding-like functionality.
I have a model with an array of identifiable Items, var selectedID holding a UUID of selected Item, and var proxy which has get{} that looks for an Item inside array by UUID and returns it.
While get{} works well, I can't figure out how to make proxy mutable to change values of selected Item by referring to proxy.
I have tried to implement set{} but nothing works.
import SwiftUI
var words = ["Aaaa", "Bbbb", "Cccc"]
struct Item: Identifiable {
var id = UUID()
var word: String
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(word: "One"), Item(word: "Two"), Item(word: "Three")]
#Published var selectedID: UUID?
var proxy: Item? {
set {
// how to set one property of Item?, but not the whole Item here?
}
get {
let index = items.firstIndex(where: { $0.id == selectedID })
return index != nil ? items[index!] : nil
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
// monitoring
MonitorkVue(model: model)
//selections
HStack {
ForEach(model.items.indices, id:\.hashValue) { i in
SelectionVue(item: $model.items[i], model: model)
}
}
}.padding()
}
}
struct MonitorkVue: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text(model.proxy?.word ?? "no proxy")
// 3rd: cant make item change by referring to proxy
// in order this to work, proxy's set{} need to be implemented somehow..
Button {
model.proxy?.word = words.randomElement()!
} label: {Text("change Proxy")}
}
}
}
struct SelectionVue: View {
#Binding var item: Item
#ObservedObject var model: Model
var body: some View {
VStack {
Text(item.word).padding()
// 1st: making selection
Button {
model.selectedID = item.id } label: {Text("SET")
}.disabled(item.id != model.selectedID ? false : true)
// 2nd: changing item affects proxy,
// this part works ok
Button {
item.word = words.randomElement()!
}label: {Text("change Item")}
}
}
}
Once you SET selection you can randomize Item and proxy will return new values.
But how to make it works the other way around when changing module.proxy.word = "Hello" would affect selected Item?
Does anyone knows how to make this two-way shortct?
Thank You
Here is a correction and some fix:
struct Item: Identifiable {
var id = UUID()
var word: String
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(word: "One"), Item(word: "Two"), Item(word: "Three")]
#Published var selectedID: UUID?
var proxy: Item? {
get {
if let unwrappedIndex: Int = items.firstIndex(where: { value in (selectedID == value.id) }) { return items[unwrappedIndex] }
else { return nil }
}
set(newValue) {
if let unwrappedItem: Item = newValue {
if let unwrappedIndex: Int = items.firstIndex(where: { value in (unwrappedItem.id == value.id) }) {
items[unwrappedIndex] = unwrappedItem
}
}
}
}
}

SwiftUI+Combine - Dynamicaly subscribing to a dict of publishers

In my project i hold a large dict of items that are updated via grpc stream. Inside the app there are several places i am rendering these items to UI and i would like to propagate the realtime updates.
Simplified code:
struct Item: Identifiable {
var id:String = UUID().uuidString
var name:String
var someKey:String
init(name:String){
self.name=name
}
}
class DataRepository {
public var serverSymbols: [String: CurrentValueSubject<Item, Never>] = [:]
// method that populates the dict
func getServerSymbols(serverID:Int){
someService.fetchServerSymbols(serverID: serverID){ response in
response.data.forEach { (name,sym) in
self.serverSymbols[name] = CurrentValueSubject(Item(sym))
}
}
}
// background stream that updates the values
func serverStream(symbols:[String] = []){
someService.initStream(){ update in
DispatchQueue.main.async {
self.serverSymbols[data.id]?.value.someKey = data.someKey
}
}
}
}
ViewModel:
class SampleViewModel: ObservableObject {
#Injected var repo:DataRepository // injection via Resolver
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
func getItem(item:String) -> Item {
guard let item = repo.serverSymbols[item] else { return Item(name:"N/A")}
return ItemPublisher(item: item).data
}
}
class ItemPublisher: ObservableObject {
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:CurrentValueSubject<Item, Never>){
item
.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
Main View with subviews:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item: viewModel.getItem(item: item))
}
}
}
}
struct FavoriteCardView: View {
var item:Item
var body: some View {
VStack {
Text(item.name)
Text(item.someKey) // dynamic value that should receive the updates
}
}
}
I must've clearly missed something or it's a completely wrong approach, however my Item cards do not receive any updates (i verified the backend stream is active and serverSymbols dict is getting updated). Any advice would be appreciated!
I've realised i've made a mistake - in order to receive the updates i need to pass down the ItemPublisher itself. (i was incorrectly returning ItemPublisher.data from my viewModel's method)
I've refactored the code and make the ItemPublisher provide the data directly from my repository using the item key, so now each card is subscribing individualy using the publisher.
Final working code now:
class SampleViewModel: ObservableObject {
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
}
MainView and CardView:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item)
}
}
}
}
struct FavoriteCardView: View {
var itemName:String
#ObservedObject var item:ItemPublisher
init(_ itemName:String){
self.itemName = itemName
self.item = ItemPublisher(item:item)
}
var body: some View {
let itemData = item.data
VStack {
Text(itemData.name)
Text(itemData.someKey)
}
}
}
and lastly, modified ItemPublisher:
class ItemPublisher: ObservableObject {
#Injected var repo:DataRepository
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:String){
self.data = Item(name:item)
if let item = repo.serverSymbols[item] {
self.data = item.value
item.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
}

How to use Bind an Associative Swift enum?

I have a GroupView that accepts a binding as a parameter because I want the GroupView to modify the data in the enum.
Can some help me on how to accomplish this?
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
GroupView(group: /* What do i put here? */) // <----------------
}
}
}
struct GroupView: View {
#Binding var group: Group
var body: some View {
Text("Hello World")
}
}
class ViewModel : ObservableObject {
#Published var instruction: Instruction!
init() {
instruction = .group(Group(groupTitle: "A Group struct"))
}
}
enum Instruction {
case group(Group)
}
struct Group { var groupTitle: String }
Well, this certainly will work... but probably there's a better approach to your problem. But no one is in a better position than you, to determine that. So I'll just answer your question about how to pass a binding.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
GroupView(group: viewModel.groupBinding)
}
}
}
class ViewModel : ObservableObject {
#Published var instruction: Instruction!
init() {
instruction = .group(Group(groupTitle: "A Group struct"))
}
var groupBinding: Binding<Group> {
return Binding<Group>(get: {
if case .group(let g) = self.instruction {
return g
} else {
return Group(groupTitle: "")
}
}, set: {
self.instruction = .group($0)
})
}
}

How to delete an object in SwiftUI which is marked with #ObjectBinding?

I want to delete an object which is marked as #ObjectBinding, in order to clean up some TextFields for example.
I tried to set the object reference to nil, but it didn't work.
import SwiftUI
import Combine
class A: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var text = "" { didSet { didChange.send() } }
}
class B {
var property = "asdf"
}
struct DetailView : View {
#ObjectBinding var myObject: A = A() //#ObjectBinding var myObject: A? = A() -> Gives an error.
#State var mySecondObject: B? = B()
var body: some View {
VStack {
TextField($myObject.text, placeholder: Text("Enter some text"))
Button(action: {
self.test()
}) {
Text("Clean up")
}
}
}
func test() {
//myObject = nil
mySecondObject = nil
}
}
If I try to use an optional with #ObjectBinding, I'm getting the Error
"Cannot convert the value of type 'ObjectBinding' to specified type
'A?'".
It just works with #State.
Regards
You can do something like this:
class A: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var form = FormData() { didSet { didChange.send() } }
struct FormData {
var firstname = ""
var lastname = ""
}
func cleanup() {
form = FormData()
}
}
struct DetailView : View {
#ObjectBinding var myObject: A = A()
var body: some View {
VStack {
TextField($myObject.form.firstname, placeholder: Text("Enter firstname"))
TextField($myObject.form.lastname, placeholder: Text("Enter lastname"))
Button(action: {
self.myObject.cleanup()
}) {
Text("Clean up")
}
}
}
}
I absolutely agree with #kontiki , but you should remember to don't use #State when variable can get outside. #ObjectBinding right way in this case. Also all new way of memory management already include optional(weak) if they need it.
Check this to get more information about memory management in SwiftUI
Thats how to use #ObjectBinding
struct DetailView : View {
#ObjectBinding var myObject: A
and
DetailView(myObject: A())