Is there a way decouple views from view models like the following? - swiftui

My target is 2 thing:
1. to make a view depending on a view model protocol not a concrete class.
2. a sub view gets the view model from the environment instead of passing it through the view hierarchy
I've mentioned my goals so if there's a totally different way to achieve them, I'm open to suggestion.
Here's what've tried and failed of course and raised weird error:
struct ContentView: View {
var body: some View {
NavigationView {
MyView()
}
}
}
struct MyView: View {
#EnvironmentObject var viewModel: some ViewModelProtocol
var body: some View {
HStack {
TextField("Enter something...", text:$viewModel.text)
Text(viewModel.greetings)
}
}
}
//MARK:- View Model
protocol ViewModelProtocol: ObservableObject {
var greetings: String { get }
var text: String { get set }
}
class ConcreteViewModel: ViewModelProtocol {
var greetings: String { "Hello everyone..!" }
#Published var text = ""
}
//MARK:- Usage
let parent = ContentView().environmentObject(ConcreteViewModel())

Yes there is, but it's not very pretty.
You're running into issues, since the compiler can't understand how it's ever supposed to infer what type that that some protocol should be.
The reason why some works in declaring your view, is that it's inferred from the type of whatever you supply to it.
If you make your view struct take a generic viewmodel type, then you can get this up and compiling.
struct MyView<ViewModel: ViewModelProtocol>: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
Text(viewModel.greetings)
}
}
the bummer here, is that you now have to declare the type of viewmodel whenever you use this view, like so:
let test: MyView<ConcreteViewModel> = MyView()

Related

How to create a new view model to pass to a detail view from a list without breaking MVVM?

I'm looking for some help or clarification on whether my current understanding of SwiftUI breaks MVVM or not. This is part of a code challenge for a job I desperately need so really appreciate any comments.
My main view pulls in models from my view model characterManager to populate a list. Each item in the list contains a NavigationLink to navigate to a detail view. I need to pass the model to the detail view and build a view model for the detail view to use but I'm not sure where this view model should be created. Currently I'm creating the new CharacterDetailViewModel in the CharacterListView and passing to the detail view's init function. I'm not sure if it is right to create a view model directly in the view.
Here's my main view where the list is populated using character models and passed into a detail view with a new CharacterDetailViewModel:
struct CharacterListView: View {
#StateObject var characterManager = CharacterManager()
#StateObject var realmManager = RealmManager()
var body: some View {
NavigationStack {
if characterManager.isLoading {
ProgressView()
} else {
List(characterManager.characters) { character in
NavigationLink {
CharacterDetailView(characterViewModel: CharacterDetailViewModel(character: character))
} label: {
Text(character.name)
}
}
.navigationTitle("Characters")
}
}
.environmentObject(characterManager)
.environmentObject(realmManager)
}
}
And this is the detail view where the view model is set in the init
struct CharacterDetailView: View {
#EnvironmentObject var characterManager: CharacterManager
#ObservedObject var viewModel: CharacterDetailViewModel
init(characterViewModel: CharacterDetailViewModel) {
self.viewModel = characterViewModel
}
var body: some View {
...
}
}
Is this a legal way to do what I'm trying to achieve or is there a better way to do this? The company I'm applying to uses MVVM with SwiftUI so I have to stick with this architecture.
I would make it simple and directly use the Character model into the application without the use if a view model. In SwiftUI, view is the View Model so in most cases you don't need to create separate VM for each screen. I would use the following approach.
#MainActor
class CharacterManager: ObservableObject {
#Published var characters: [Character] = []
func fetchCharacters() {
characters = [Character(name: "Character 1"), Character(name: "Character 2")]
}
}
struct Character: Identifiable {
let id = UUID()
let name: String
}
struct CharacterDetailView: View {
let character: Character
var body: some View {
Text(character.name)
.font(.largeTitle)
}
}
struct ContentView: View {
#StateObject private var characterManager = CharacterManager()
var body: some View {
NavigationStack {
List(characterManager.characters) { character in
NavigationLink {
CharacterDetailView(character: character)
} label: {
Text(character.name)
}
}.onAppear {
characterManager.fetchCharacters()
}
}
}
}

SwiftUI parent viewModel containing a nested array of observed objects does not update

(You can skip this part and just look at the code.) I'm creating a complicated form. The form creates, say, a Post object, but I want to be able to create several Comment objects at the same time. So I have a Post form and a Comment form. In my Post form, I can fill out the title, description, etc., and I can add several Comment forms as I create more comments. Each form has an #ObservedObject viewModel of its own type. So I have one parent Post #ObservedObject viewModel, and another #ObservedObject viewModel for the array of the Comment objects which is also a #ObservedObject viewModel.
I hope that made some sense -- here is code to minimally reproduce the issue (unrelated to Posts/Comments). The objective is to make the count of the "Childish" viewModels at the parent level count up like how they count up for the "Child" view.
import Combine
import SwiftUI
final class ParentScreenViewModel: ObservableObject {
#Published var childScreenViewModel = ChildScreenViewModel()
}
struct ParentScreen: View {
#StateObject private var viewModel = ParentScreenViewModel()
var body: some View {
Form {
NavigationLink(destination: ChildScreen(viewModel: viewModel.childScreenViewModel)) {
Text("ChildishVMs")
Spacer()
Text("\(viewModel.childScreenViewModel.myViewModelArray.count)") // FIXME: this count is never updated
}
}
}
}
struct ParentScreen_Previews: PreviewProvider {
static var previews: some View {
ParentScreen()
}
}
// MARK: - ChildScreenViewModel
final class ChildScreenViewModel: ObservableObject {
#Published var myViewModelArray: [ChildishViewModel] = []
func appendAnObservedObject() {
objectWillChange.send() // FIXME: does not work
myViewModelArray.append(ChildishViewModel())
}
}
struct ChildScreen: View {
#ObservedObject private var viewModel: ChildScreenViewModel
init(viewModel: ChildScreenViewModel = ChildScreenViewModel()) {
self.viewModel = viewModel
}
var body: some View {
Button {
viewModel.appendAnObservedObject()
} label: {
Text("Append a ChildishVM (current num: \(viewModel.myViewModelArray.count))")
}
}
}
struct ChildScreen_Previews: PreviewProvider {
static var previews: some View {
ChildScreen()
}
}
final class ChildishViewModel: ObservableObject {
#Published var myProperty = "hey!"
}
ParentView:
ChildView:
I can't run this in previews either -- seems to need to be run in the simulator. There are lots of questions similar to this one but not quite like it (e.g. the common answer of manually subscribing to the child's changes using Combine does not work). Would using #EnvironmentObject help somehow? Thanks!
First get rid of the view model objects, we don't use those in SwiftUI. The View data struct is already the model for the actual views on screen e.g. UILabels, UITables etc. that SwiftUI updates for us. It takes advantage of value semantics to resolve consistency bugs you typically get with objects, see Choosing Between Structures and Classes. SwiftUI structs uses property wrappers like #State to make these super-fast structs have features like objects. If you use actual objects on top of the View structs then you are slowing down SwiftUI and re-introducing the consistency bugs that Swift and SwiftUI were designed to eliminate - which seems to me is exactly the problem you are facing. So it of course is not a good idea to use Combine to resolve consistency issues between objects it'll only make the problem worse.
So with that out of the way, you just need correct some mistakes in your design. Model types should be structs (these can be arrays or nested structs) and have a single model object to manage the life-cycle and side effects of the struct. You can have structs within structs and use bindings to pass them between your Views when you need write access, if you don't then its simply a let and SwiftUI will automatically call body whenever a View is init with a different let from last time.
Here is a basic example:
struct Post: Identifiable {
let id = UUID()
var text = ""
}
class Model: ObservableObject {
#Published var posts: [Post] = []
// func load
// func save
// func delete a post by ID
}
struct ModelController {
static let shared = ModelController()
let model = Model()
//static var preview: ModelController {
// ...
//}()
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ModelController.shared.model)
}
}
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
ForEach($model.posts) { $post in
ContentView2(post: post)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(ModelController.shared.preview)
}
}
struct ConventView2: View {
#Binding var post: Post
var body: some View {
TextField("Enter Text", text: $post.text)
}
}
For a more detail check out Apple's Fruta and Scrumdinger samples.

SwiftUI pass Binding by ref to a child ViewModel

In SwiftUI, I am trying to create some binding between a parent ViewModel and a child ViewModel, here is a simplified example of my scenario:
The parent component:
class ParentViewModel : ObservableObject {
#Published var name = "John Doe"
func updateName() {
self.name = "Jonnie Deer"
}
}
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
ChildView(name: $viewModel.name)
// tapping the button the text on parent view is updated but not in child view
Button("Update", action: viewModel.updateName)
}
}
}
The child component:
class ChildViewModel : ObservableObject {
var name: Binding<String>
var displayName: String {
get {
return "Hello " + name.wrappedValue
}
}
init(name: Binding<String>) {
self.name = name
}
}
struct ChildView: View {
#StateObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
init(name: Binding<String>) {
_viewModel = StateObject(wrappedValue: ChildViewModel(name: name))
}
}
So, as stated in the comments, when I tap the button on the parent component the name is not getting updated in ChildView, as if the binding is lost...
Is there any other way to update view model with the updated value? say something like getDerivedStateFromProps in React (becuase when tapping the button the ChildView::init method is called with the new name.
Thanks.
Apple is very big on the concept of a Single Source of Truth(SSoT), and keeping it in mind will keep you from getting into the weeds in code like this. The problem you are having is that while you are using a Binding to instantiate the child view, you are turning around and using it as a #StateObject. When you do that, you are breaking the connection as #StateObject is supposed to sit at the top of the SSoT hierarchy. It designates your SSoT. Otherwise, you have two SSoTs, so you can only update one. The view model in ChildView should be an #ObservedObject so that it connects back up the hierarchy. Also, you can directly instantiate the ChildViewModel when you call ChildView. The initializer just serves to decouple things. Your views would look like this:
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
// You can directly use the ChildViewModel to instantiate the ChildView
ChildView(viewModel: ChildViewModel(name: $viewModel.name))
Button("Update", action: viewModel.updateName)
}
}
}
struct ChildView: View {
// Make this an #ObservedObject not a #StateObject
#ObservedObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
}
Neither view model is changed.
Get rid of the view model objects and do #State var name = “John” in ParentView and #Binding var name: String in ChildView. And pass $name into ChildView’s init which gives you write access as if ParentView was a view model object.
By using #State and #Binding you get the reference type semantics you want inside a value type which is the power of SwiftUI. If you just use objects you lose that benefit and have more work to do.
We usually only use ObservableObject for model data but we can also use it for loaders/fetchers where we want to tie some controller behaviour to the view lifecycle but for data transient to a view we always use #State and #Binding. You can extract related vars into their own struct and use mutating funcs for other logic and thus have a single #State struct used by body instead of multiple. This way it can still be testable like a view model object in UIKit would be.

What is the difference between #EnvironmentObject and #ObservedObject?

I have been reading about the property wrappers in SwiftUI and I see that they do a great job, but one thing which I really don't get is the difference between #EnvironmentObject and #ObservedObject.
From what I learned so far, I see that #EnvironmentObject is used when we have an object that is needed in various places in our app but we don't need to pass it through all of them. For example if we have hierarchy A -> B -> C -> D and the object is created at A, it is saved in the environment so that we can pass it directly from A to D, if D needs it.
If we use #ObservedObject which is created at A and needs to be passed to D, then we need to go through B and C as well.
But I still don't know how to decide which one to use.
Here are 2 example projects which I made:
struct ContentView2: View {
var order = Order2()
var body: some View {
VStack {
EditView2()
DisplayView2()
}
.environmentObject(order)
}
}
struct EditView2: View {
#EnvironmentObject var user: Order2
var body: some View {
HStack{
TextField("Fruit", text: $user.item)
}
}
}
struct DisplayView2: View {
#EnvironmentObject var user: Order2
var body: some View {
VStack{
Text(user.item)
}
}
}
class Order2: ObservableObject {
#Published var item = "Orange"
}
and
struct ContentView: View {
var order = Order()
var body: some View {
VStack {
EditView(order: order)
DisplayView(order: order)
}
}
}
struct EditView: View {
#ObservedObject var order: Order
var body: some View {
HStack{
TextField("Fruit", text: $order.item)
}
}
}
struct DisplayView: View {
#ObservedObject var order: Order
var body: some View {
VStack{
Text(order.item)
}
}
}
class Order: ObservableObject {
#Published var item = "Apple"
}
Both codes do the same update of the view. Also both ContentViews, pass an Order object. The difference is that Environment passes .environmentObject(order) and Observed passes it directly EditView(order: order). For me, both do same job, only their declaration is different, therefore I would appreciate some explanation or a better example.
As you've noticed an #ObservedObject needs to be passed from view to view. It may be better for a simple view hierarchy when you don't have too many views.
Let's assume you have the following hierarchy:
ViewA -> ViewB -> ViewC -> ViewD
Now if you want your #ObservedObject from the ViewA to be in the ViewB there's no problem with passing it directly in init.
But what if you want it in the ViewD as well? And what if you don't need it in the ViewB and ViewC?
With an #ObservedObject you'd need to manually pass it from the ViewA to the ViewB and then to the ViewC, and then to the ViewD. And you'd need to declare it in every child view.
With an #EnvironmentObject it's easy - just pass it to the top-level view:
ViewA().environmentObject(someObservableObject)
Then you only declare it in the view that uses it - this may make your code more readable.
Note
Every object in the environment (view hierarchy) can access the injected #EnvironmentObject. If you don't want this (privacy is important) you may need to pass it as an #ObservedObject instead.

Classes and Observable Object

I'm trying to make a data model class that can be referenced by different views. The data model has a function that can modify one of its published variables. However, this function is called inside one view, the change it makes to the published variable is not reflected in other views which also reference the class. The most simple example I can come up with is this:
struct ContentView: View {
var body: some View {
VStack {
TextView()
ButtonView()
}
}
}
struct TextView: View {
#ObservedObject var data = Data()
var body: some View {
Text(data.currentWord)
}
}
struct ButtonView: View {
#ObservedObject var data = Data()
var body: some View {
Button(action: {self.data.randomWord()}) {
Text("Random word")
}
}
}
class Data: ObservableObject {
#Published var currentWord = "Cat"
func randomWord() {
let word = ["Cat", "Dog", "Mouse", "Horse"].randomElement()!
print(word)
currentWord = word
}
}
Both the ButtonView and TextView reference the same class, and the ButtonView calls the 'Data' class's method 'randomWord' which modifies its 'currentWord' published variable. However, the change to this variable is not reflected in the Text of the TextView which also references the 'Data' class.
I think I'm not understanding something about classes and observableObject correctly. Would anyone be kind enough to point out my mistake here?
You create two different instance of Data in your subviews, instead you need to share one, so create it in ContentView and pass to subviews as below
struct ContentView: View {
#ObservedObject var data = Data()
var body: some View {
VStack {
TextView(data: data)
ButtonView(data: data)
}
}
}
struct TextView: View {
#ObservedObject var data: Data
var body: some View {
Text(data.currentWord)
}
}
struct ButtonView: View {
#ObservedObject var data: Data
var body: some View {
Button(action: {self.data.randomWord()}) {
Text("Random word")
}
}
}
Also, as variant, for such scenario can be used EnvironmentObject pattern. There are a lot of examples here on SO you can find about environment objects usage - just search by keywords.