Search code examples
swiftuidata-sharingcode-separation

SwiftUI: Data sharing between separate views


What is the best practice to share variables between views? My app has only one view. But as it gets more and more complicated, I think I should separate it into several views. Also to separate the methods. I started with something like this:

struct ContentView: View {
    @State var str: String = "String"
    var body: some View {
        VStack(alignment: .leading) {
            Text(str)
            TextField("Input", text: $str)
            Button("button", action: { doSomething() })
        }.padding()
    }
    func doSomething() {
        str = str + " " + str
    }
}

And want to go there:

class GlobalVars: ObservableObject {
    @Published var str: String = "Initial String"
}
struct ContentView: View {
    @ObservedObject var globalvars = GlobalVars()
    var body: some View {
        VStack(alignment: .leading) {
            DisplayView()
            EditView()
            ControlView()
        }.padding()
    }
}
struct DisplayView: View {
    @Binding var str:  String
    var body: some View {
        Text(self.globalvars.str)
    }
}
struct EditView: View {
    @Binding var str:  String
    var body: some View {
        TextField("Input", text: self.$str)
    }
}
struct ControlView: View {
    @Binding var str:  String
    var body: some View {
        Button("button", action: { doSomething() })
    }
}
func doSomething() {
    @Binding var str:  String
    self.str = self.str + " " + self.str
}

I tried with @Published, @ObservedObject and @Binding. But don't get it. Thank you for any pointer in advance.


Solution

  • There are a number of ways to approach this.

    My choice would probably be passing the binding just to the variable that you need access to. That might look like this:

    class GlobalVars: ObservableObject {
        @Published var str: String = "Initial String"
    }
    struct ContentView: View {
        @ObservedObject var globalvars = GlobalVars()
        var body: some View {
            VStack(alignment: .leading) {
                DisplayView(str: globalvars.str) //don't need a binding here since it doesn't get modified
                EditView(str: $globalvars.str)
                ControlView(str: $globalvars.str)
            }.padding()
        }
    }
    struct DisplayView: View {
        var str:  String //don't need a binding here since it doesn't get modified
        var body: some View {
            Text(str)
        }
    }
    struct EditView: View {
        @Binding var str:  String
        var body: some View {
            TextField("Input", text: $str)
        }
    }
    struct ControlView: View {
        @Binding var str:  String
        var body: some View {
            Button("button", action: { doSomething() })
        }
        
        func doSomething() {
            str = str + " " + str
        }
    }
    

    Note that now in ContentView, there's a parameter passed to each of the subviews, containing a binding (using the $ sign) to the GlobalVars str property.

    Also, doSomething got moved into the body of ControlView


    You could also use EnvironmentObject to handle this. I'm personally not as big of a fan of this approach because I'd rather see explicitly where my parameters are going. It also gives the subviews access to the entire ObservableObject, which isn't really necessary. But, it shows you the principal:

    class GlobalVars: ObservableObject {
        @Published var str: String = "Initial String"
    }
    struct ContentView: View {
        @ObservedObject var globalvars = GlobalVars()
        var body: some View {
            VStack(alignment: .leading) {
                DisplayView()
                EditView()
                ControlView()
            }.padding()
            .environmentObject(globalvars)
        }
    }
    struct DisplayView: View {
        @EnvironmentObject var globalvars : GlobalVars
        var body: some View {
            Text(globalvars.str)
        }
    }
    struct EditView: View {
        @EnvironmentObject var globalvars : GlobalVars
        var body: some View {
            TextField("Input", text: $globalvars.str)
        }
    }
    struct ControlView: View {
        @EnvironmentObject var globalvars : GlobalVars
        var body: some View {
            Button("button", action: { doSomething() })
        }
        
        func doSomething() {
            globalvars.str = globalvars.str + " " + globalvars.str
        }
    }
    
    

    Note that now, globalvars is passed to the children by being placed in the view hierarchy with .environmentObject. Each subview has access to it by declaring a property of @EnvironmentObject var globalvars : GlobalVars


    You could also do kind of a hybrid model where you explicitly pass the ObservableObject as a parameter to the child view:

    struct ContentView: View {
        @ObservedObject var globalvars = GlobalVars()
        var body: some View {
            VStack(alignment: .leading) {
                DisplayView(globalvars: globalvars)
            }.padding()
            .environmentObject(globalvars)
        }
    }
    
    struct DisplayView: View {
        @ObservedObject var globalvars : GlobalVars
        var body: some View {
            Text(globalvars.str)
        }
    }