I've spent a while researching this and found lots of examples of SwiftUI, MVVM and DI separately but none combined, so I'm assuming I've misunderstood something.
I have a new SwiftUI app and aiming to use the above.
I have the following
A DependencyContainer
protocol ViewControllerFactory {
func makeFirstViewController() -> First_ViewModel
func makeSecondViewController() -> Second_ViewModel
}
class DependencyContainer : ObservableObject {
let database = AppDatabase()
}
extension DependencyContainer: ViewControllerFactory {
func makeFirstViewController() -> First_ViewModel {
return First_ViewModel(appDatabase: database)
}
func makeSecondViewController() -> Second_ViewModel {
return Second_ViewModel()
}
}
In my app entry point I have:
#main
struct MyApp: App {
var body: some Scene {
let container = DependencyContainer()
WindowGroup {
First_View()
.environment(\.container, container)
}
}
}
private struct Container: EnvironmentKey {
static let defaultValue = DependencyContainer()
}
extension EnvironmentValues {
var container: DependencyContainer {
get { self[Container.self] }
set { self[Container.self] = newValue }
}
}
Now I run into problems
How do I use the container in the views?
struct First_View: View{
#Environment(\.container) var container
#ObservedObject private var firstViewModel : First_ViewModel
init(){
_firstViewModel = container.makeFirstViewController()
}
Gives error
"Cannot assign value of type 'First_ViewModel' to type 'ObservedObject<First_ViewModel>'"
If I inject the container like below,
struct First_View: View {
#ObservedObject private var firstViewModel : First_ViewModel
init (container : DependencyContainer){
_firstViewModel = container.makeFirstViewController()
It works but not how I thought it should work
The #Environment() property wrapper type relies on a defined set of key names. You can create your own (this blog post from Use Your Loaf is a good description of how to do this). This is designed for value types, though.
However, as you’ve already declared DependencyContainer as an ObservableObject you can use the #EnvironmentObject property wrapper:
struct MyApp: App {
#StateObject var container = DependencyContainer()
var body: some View {
WindowGroup {
First_View()
.environmentObject(container)
}
}
}
Then, in any child view where you need access, you use the #EnvironmentObject wrapper in your declaration. Note that you have to include the class type; you can have multiple environment objects defined, but a maximum of one of each class.
struct First_View: View {
#EnvironmentObject var container: DependencyContainer
// ...
}
Note also that #EnvironmentObject assumes that it will be able to find an instance of DependencyContainer - if it can’t find one, it will crash. When you’re defining one at the app level that shouldn’t be a problem. However, in your SwiftUI previews you will need to specify a suitable instance or the preview subsystem may crash.
So if you had a static method that prepared a preview-suitable version of your dependencies, you could write:
struct First_View_Previews: PreviewProvider {
static var previews: some View {
First_View()
.environmentObject(DependencyContainer.previewInstance)
}
}
Related
I have a custom ViewModifier which simply returns the same content attached with a onReceive modifier, the onReceive is not triggered, here is a sample code that you can copy, paste and run in XCode:
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var myProperty: Bool = false
}
struct ContentView: View {
#ObservedObject var viewModel: MyViewModel
var body: some View {
Text("Hello, World!")
.modifier(MyOnReceive(viewModel: viewModel))
.onTapGesture {
self.viewModel.myProperty = true
}
}
}
struct MyOnReceive: ViewModifier {
#ObservedObject var viewModel: MyViewModel
func body(content: Content) -> some View {
content
.onReceive(viewModel.$myProperty) { theValue in
print("The Value is \(theValue)") // <--- this is not executed
}
}
}
is SwiftUI designed to disallow onReceive to execute inside a ViewModifier or is it a bug ? I have a view in my real life project that gets bigger with some business logic put inside onReceive, so I need to clean that view by separating it from onReceive.
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
ObservableObject and #Published are part of the Combine framework if you aren't using Combine then you shouldn't be using a class for your view data. Instead, you should be using SwiftUI as designed and avoid heavy objects and either put the data in the efficient View data struct or make a custom struct as follows:
import SwiftUI
struct MyConfig {
var myProperty: Bool = false
mutating func myMethod() {
myProperty = !myProperty
}
}
struct ContentView: View {
#State var config = MyConfig()
var body: some View {
Text("Hello, World!")
.onTapGesture {
config.myMethod()
}
}
}
Old answer:
Try onChange instead
https://developer.apple.com/documentation/swiftui/scrollview/onchange(of:perform:)
.onChange(of: viewModel.myProperty) { newValue in
print("newValue \(newValue)")
}
But please don't use the View Model object pattern in SwiftUI, try to force yourself to use value types like structs for all your data as SwiftUI was designed. Property wrappers like #State will give you the reference semantics you are used to with objects.
I’m looking for the proper pattern and syntax to address my goal of having an ObservableObject instance that I can share amongst multiple views, but while keeping logic associated with it contained to another class. I’m looking to do this to allow different ‘controller’ classes to manipulate the properties of the state without the view needing to know which controller is acting on it (injected).
Here is a simplification that illustrates the issue:
import SwiftUI
class State: ObservableObject {
#Published var text = "foo"
}
class Controller {
var state : State
init(_ state: State) {
self.state = state
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var state = State()
var controller: Controller!
init() {
controller = Controller(state)
}
var body: some View {
VStack {
Text(controller.state.text) // always shows 'foo'
Button("Press Me") {
print(controller.state.text) // prints 'foo'
controller.changeState()
print(controller.state.text) // prints 'bar'
}
}
}
}
I know that I can use my ObservableObject directly and manipulate its properties such that the UI is updated in response, but in my case, doing so prevents me from having different ‘controller’ instances depending on what needs to happen. Please advise with the best way to accomplish this type of scenario in SwiftUI
To make SwiftUI view follow state updates, your controller needs to be ObservableObject.
SwiftUI view will update when objectWillChange is triggered - it's done automatically for properties annotated with Published, but you can trigger it manually too.
Using same publisher of your state, you can sync two observable objects, for example like this:
class Controller: ObservableObject {
let state: State
private var cancellable: AnyCancellable?
init(_ state: State) {
self.state = state
cancellable = state.objectWillChange.sink {
self.objectWillChange.send()
}
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var controller = Controller(State())
I use the same fetch request in multiple top level views under ContentView. I mean the same entity / predicate etc.
Previously I tried doing the request once in ContentView and passing it as an array through several layers of views. However, this stopped changes, deletes etc, being propagated across other views.
I just wondered if there is another approach which would work ?
I'm thinking that some kind of singleton approach might work, but I'm worried about the performance implications, however this might outweigh having to run the request several times.
Also, I wondered about passing a request results rather than array?
However having to pass this around seems ugly.
You can use the Environment to pass your models to children without having to passing an array through several layers of views. You start by creating your own EnvirnomentKey
public struct ModelEnvironmentKey: EnvironmentKey {
public static var defaultValue: [Model] = []
}
public extension EnvironmentValues {
var models: [Model] {
get { self[ModelEnvironmentKey] }
set { self[ModelEnvironmentKey] = newValue }
}
}
public extension View {
func setModels(_ models: [Model]) -> some View {
environment(\.models, models)
}
}
I like using ViewModifiers to setup my environment, following the Single-Responsibility Principle:
struct ModelsLoaderViewModifier: ViewModifier {
#FetchRequest(entity: Model(), sortDescriptors: [])
var models: FetchedResults<Model>
func body(content: Content) -> some View {
content
.setModels(Array(models))
}
}
extension View {
func loadModels() -> some View {
modifier(ModelsLoaderViewModifier)
}
}
I then would add this modifier pretty high on the view hierarchy.
#main
struct BudgetApp: App {
#ObservedObject var persistenceManager = PersistenceManager(usage: .main)
let startup = Startup()
var body: some Scene {
WindowGroup {
ContentView()
.loadModels()
}
}
}
Now ContentView can read from the environment:
struct ContentView: View {
#Environment(\.models) var models
var body: some View {
List {
ForEach(models) { model in
Text(model.name)
}
}
}
}
I have a view model that is parent to other children view models. That is:
public class ViewModel: ObservableObject {
#Published var nav = NavigationViewModel()
#Published var screen = ScreenViewModel()
The other children view model, such as nav and screen, all serve a specific purpose. For example, nav’s responsibility is to keep track of the current screen:
class NavigationViewModel: ObservableObject {
// MARK: Publishers
#Published var currentScreen: Screen = .Timeline
}
The ViewModel is instantiated in the App struct:
#main
struct Appy_WeatherApp: App {
// MARK: Global
var viewModel = ViewModel()
// MARK: -
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
And I declare an #EnvironmentObject for it on any view that needs access to it:
#EnvironmentObject var viewModel: ViewModel
Any view referencing a non-object property of ViewModel that is being #Published whose value changes will result in the view to be re-rendered as expected. However, if the currentScreen #Published property of the NavigationViewModel changes, for example, then the view is not being re-rendered.
I know I can make it work if I separate NavigationViewModel from ViewModel, instantiate it at the app level and use it as its own environment object in any views that access any of its published properties.
My question is whether the above workaround is actually the correct way to handle this, and/or is there any way for views to be subscribed to value changes of properties inside child objects of environment objects? Or is there another way that I’ve not considered that’s the recommended approach for what I’m trying to achieve through fragmentation of view model responsibilities?
There are several ways to achieve this.
Option 1
Using Combine.
import Combine
public class ViewModel: ObservableObject {
#Published var nav = NavigationViewModel()
var anyCancellable: AnyCancellable?
init() {
anyCancellable = nav.objectWillChange.sink { _ in
self.objectWillChange.send()
}
}
}
You basically just listen to whenever your navigationViewModel publishes changes. If so, you tell your views that your ViewModel has changes aswell.
Option 2
I suppose due to the name NavigationViewModel, that you would use it quite often inside other view models?
If that's the case, I would go for a singleton pattern, like so:
class NavigationViewModel: ObservableObject {
static let shared = NavigationViewModel()
private init() {}
#Published var currentScreen: Screen = .Timeline
}
Inside your ViewModel:
public class ViewModel: ObservableObject {
var nav: NavigationViewModel { NavigationViewModel.shared }
}
You can of course also call it inside any View:
struct ContentView: View {
#StateObject var navigationModel = NavigationModel.shared
}
You might have to call objectWillChange.send() after changing publishers.
#Published var currentScreen: Screen = .Timeline {
didSet {
objectWillChange.send()
}
}
Consider we have a RootView and a DetailView. DetailView has it's own BindableObject, let's call it DetailViewModel and we have scenario:
RootView may be updated by some kind of global event e.g. missed
internet connection or by it's own data/view model
When RootView handling event it's
content is updated and this is causes new struct of DetailView to
be created
If DetailViewModel is created by DetailView on init,
there would be another reference of DetailViewModel and it's state (e.g. selected object) will be missed
How can we avoid this situation?
Store all ViewModels as EnvironmentObjects that is basically a singleton pool. This approach is causes to store unneeded objects in memory when they are not used
Pass throw all ViewModels from RootView to it's children and to children of child (has cons as above + painfull dependencies)
Store View independent DataObjects (aka workers) as EnvironmentObjects. In that case where do we store view dependent states that corresponds to Model? If we store it in View it will end up in situation where we cross-changing #States what is forbidden by SwiftUI
Better approach?
Sorry me for not providing any code. This question is on architecture concept of Swift UI where we trying to combine declarative structs and reference objects with data.
For now I don't see da way to keep references that corresponds to appropriate view only and don't keep them in memory/environment forever in their current states.
Update:
Lets add some code to see whats happening if VM is created by it's View
import SwiftUI
import Combine
let trigger = Timer.publish(every: 2.0, on: .main, in: .default)
struct ContentView: View {
#State var state: Date = Date()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ContentDetailView(), label: {
Text("Navigation push")
.padding()
.background(Color.orange)
})
Text("\(state)")
.padding()
.background(Color.green)
ContentDetailView()
}
}
.onAppear {
_ = trigger.connect()
}
.onReceive(trigger) { (date) in
self.state = date
}
}
}
struct ContentDetailView: View {
#ObservedObject var viewModel = ContentDetailViewModel()
#State var once = false
var body: some View {
let vmdesc = "View model uuid:\n\(viewModel.uuid)"
print("State of once: \(once)")
print(vmdesc)
return Text(vmdesc)
.multilineTextAlignment(.center)
.padding()
.background(Color.blue)
.onAppear {
self.once = true
}
}
}
class ContentDetailViewModel: ObservableObject, Identifiable {
let uuid = UUID()
}
Update 2:
It seems that if we store ObservableObject as #State in view (not as ObservedObject) View keeps reference on VM
#State var viewModel = ContentDetailViewModel()
Any negative effects? Can we use it like this?
Update 3:
It seems that if ViewModel kept in View's #State:
and ViewModel is retained by closure with strong reference - deinit will never be executed -> memory leak
and ViewModel is retained by closure with weak reference - deinit invokes every time on view update, all subs will be reseted, but properties will be the same
Mehhh...
Update 4:
This approach also allows you to keep strong references in bindings closures
import Foundation
import Combine
import SwiftUI
/**
static func instanceInView() -> UIViewController {
let vm = ContentViewModel()
let vc = UIHostingController(rootView: ContentView(viewModel: vm))
vm.bind(uiViewController: vc)
return vc
}
*/
public protocol ViewModelProtocol: class {
static func instanceInView() -> UIViewController
var bindings: Set<AnyCancellable> { get set }
func onAppear()
func onDisappear()
}
extension ViewModelProtocol {
func bind(uiViewController: UIViewController) {
uiViewController.publisher(for: \.parent)
.sink(receiveValue: { [weak self] (parent) in
if parent == nil {
self?.bindings.cancel()
}
})
.store(in: &bindings)
}
}
struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
return ViewModel.instanceInView()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
//
}
}
struct RootView: View {
var body: some View {
ModelView<ParkingViewModel>()
.edgesIgnoringSafeArea(.vertical)
}
}
Apple says that we should use ObservableObject for the data that lives outside of SwiftUI. It means you have to manage your data source yourself.
It looks like a single state container fits best for SwiftUI architecture.
typealias Reducer<State, Action> = (inout State, Action) -> Void
final class Store<State, Action>: ObservableObject {
#Published private(set) var state: State
private let reducer: Reducer<State, Action>
init(initialState: State, reducer: #escaping Reducer<State, Action>) {
self.state = initialState
self.reducer = reducer
}
func send(_ action: Action) {
reducer(&state, action)
}
}
You can pass the instance of the store into the environment of your SwiftUI app and it will be available in all views and will store your app state without data losses.
I wrote a blog post about this approach, take a look at it for more information
https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/