Share Data Binding Between Class and Struct View
@Published
properties work well for sharing data between ObservableObject
s and View
s.
For example:
class ViewModel : ObservableObject {
struct APIError : Identifiable {
var id = UUID()
var message : String
}
@Published var error : APIError?
func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.error = APIError(message: "Error message")
}
}
}
struct ContentView : View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Text("Hello, world!")
.alert(item: $viewModel.error) { item in
Alert(title: Text(item.message))
}
}
.onAppear {
viewModel.apiCall()
}
}
}
You could also do this with a custom Binding, but it's a little messier. The above example would definitely be my go-to.
class ViewModel : ObservableObject {
@Published var errorMessage : String?
var alertBinding : Binding<Bool> {
.init {
self.errorMessage != nil
} set: { newValue in
if !newValue { self.errorMessage = nil }
}
}
func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.errorMessage = "Error!"
}
}
}
struct ContentView : View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Text("Hello, world!")
.alert(isPresented: viewModel.alertBinding) {
Alert(title: Text(viewModel.errorMessage ?? "(unknown)"))
}
}
.onAppear {
viewModel.apiCall()
}
}
}
Binding ViewModel and TextFields with SwiftUI
I think we can simplify it with below code.
class SignInViewModel: ObservableObject{
@Published var username = ""
@Published var password = ""
}
struct SigninView: View {
@ObservedObject var viewModel = SignInViewModel()
var body: some View {
VStack(alignment: .leading, spacing: 15.0){
TextField("username", text: $viewModel.username)
TextField("password", text: $viewModel.password)
Spacer()
Button("Sign In"){
print("User: \(self.viewModel.username)")
print("Pass: \(self.viewModel.password)")
}
}.padding()
}
}
How to use View Binding on custom views
Just inform the root, and whether you want to attach to it
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this)
}
or
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this, true)
}
which inflate method to use will depend on the root layout type in xml.
Why using View Binding is changing the layout?
If I change setContentView(binding.root) to setContentView(R.layout.activity_main), the layout of Android Studio and the device become the same
Most likely caused by how the container
is not passed to the inflater if the view is inflated like this.
You could try the following instead:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
And
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.bind(findViewById(R.id.root))
val rollButton = binding.rollButton
rollButton.setOnClickListener { rollDice() }
SwiftUI - @Binding to a computed property which accesses value inside ObservableObject property duplicates the variable?
If you only look where you can't go, you might just miss the riches below
Breaking single source of truth, and breaching local (private) property of @StateObject
by sharing it via Binding are two places where you can't go.
@EnvironmentObject
or more generally the concept of "shared object" between views are the riches below.
This is an example of doing it without MVVM nonsense:
import SwiftUI
final class EnvState: ObservableObject {@Published var value: Double = 0 }
struct ContentView: View {
@EnvironmentObject var eos: EnvState
var body: some View {
VStack{
ViewA()
ViewB()
}
}
}
struct ViewA: View {
@EnvironmentObject var eos: EnvState
var body: some View {
Text("\(eos.value)").padding()
}
}
struct ViewB: View {
@EnvironmentObject var eos: EnvState
var body: some View {
VStack{
Text("\(eos.value)")
Slider(value: $eos.value, in: 0...1)
}
}
}
Isn't this easier to read, cleaner, less error-prone, with fewer overheads, and without serious violation of fundamental coding principles?
MVVM does not take value type into consideration. And the reason Swift introduces value type is so that you don't pass shared mutable references and create all kinds of bugs.
Yet the first thing MVVM devs do is to introduce shared mutable references for every view and pass references around via binding...
Now to your question:
the only options I see are either using only one ViewModel per Model, or having to pass the Model (or it's properties) between ViewModels through Binding
Another option is to drop MVVM, get rid of all view models, and use @EnvironmentObject
instead.
Or if you don't want to drop MVVM, pass @ObservedObject
(your view model being a reference type) instead of @Binding
.
E.g.;
struct ContentView: View {
@ObservedObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA)
ViewB(value: viewModelA)
}
}
}
On a side note, what's the point of "don't access model directly from view"?
It makes zero sense when your model is value type.
Especially when you pass view model reference around like cookies in a party so everyone can have it.
How can I use the @Binding Wrapper in the following mvvm architecture?
In your scenario it is more appropriate to have User as-a ObservableObject and pass it by reference between stores, as well as use in corresponding views explicitly as ObservedObject.
Here is simplified demo combined from your code snapshot and applied the idea.
Tested with Xcode 11.4 / iOS 13.4
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let user = User(id: UUID(), firstname: "John")
let rootStore = RootStore(user: user)
let storeA = StoreA(user: user)
let storeB = StoreB(user: user)
rootStore.storeA = storeA
rootStore.storeB = storeB
return ContentView().environmentObject(rootStore)
}
}
public class User: ObservableObject {
public var id: UUID?
@Published public var firstname: String
@Published public var birthday: Date
public init(id: UUID? = nil,
firstname: String,
birthday: Date? = nil) {
self.id = id
self.firstname = firstname
self.birthday = birthday ?? Date()
}
}
public class RootStore: ObservableObject {
@Published var storeA: StoreA?
@Published var storeB: StoreB?
@Published var user: User
init(user: User) {
self.user = user
}
}
public class StoreA: ObservableObject {
@Published var user: User
init(user: User) {
self.user = user
}
}
public class StoreB: ObservableObject {
@Published var user: User
init(user: User) {
self.user = user
}
}
struct ContentView: View {
@EnvironmentObject var rootStore: RootStore
var body: some View {
TabView {
ViewA(user: rootStore.user).environmentObject(rootStore.storeA!).tabItem {
Image(systemName: "location.circle.fill")
Text("ViewA")
}.tag(1)
ViewB(user: rootStore.user).environmentObject(rootStore.storeB!).tabItem {
Image(systemName: "waveform.path.ecg")
Text("ViewB")
}.tag(2)
}
}
}
struct ViewA: View {
@EnvironmentObject var storeA: StoreA // keep only if it is needed in real view
@ObservedObject var user: User
init(user: User) {
self.user = user
}
var body: some View {
VStack {
HStack {
Text("Personal Information")
Image(systemName: "info.circle")
}
TextField("First name", text: $user.firstname)
}
}
}
struct ViewB: View {
@EnvironmentObject var storeB: StoreB
@ObservedObject var user: User
init(user: User) {
self.user = user
}
var body: some View {
Text("\(user.firstname)")
}
}
Related Topics
Unsafemutablepointer in Swift as Replacement for Properly Sized C Array in Obj-C
How to Concatenate Optional Swift Strings
Watchos3 Complication That Launches App
Swift 4 - Notification Center Addobserver Issue
How to Make Firebase Database Data the Data Source for Uicollection View
How to Convert Nsnull to Nil in Swift
How to Enumerate a Slice Using the Original Indices
Testing an Executable with Swift
How to Change Gb-2312 Encoding to Utf-8
Understanding the Userdefaults Register Method
Core Data: Could Not Cast Value of Type 'Mytype_Mytype_2' to Mytype
Convert Opengl Shader to Metal (Swift) to Be Used in Cifilter
Swift Error: Missing Argument Label 'Name:' in Call
Wait Until an Asynchronous API Call Is Completed - Swift/Ios
Drawing a 3D Arc and Helix in Scenekit