I am using jetpack compose with the navigation compose library to navigate from one screen to the next. Usually you would have a ViewModel that would take care of user interactions (e.g.: viewModel.addItem()). In order to fulfill the addItem command i would like to show another screen via navController.navigate(). The ViewModel itself is injected into the Composable via hiltNavGraphViewModel().
Now the question is: How could i inject the NavController into the ViewModel via hilt?
#HiltViewModel
class ScreenViewModel #Inject constructor(
private val navController: NavController // where does it come from?
) : ViewModel() {
fun addItem() {
navController.navigate("add-item-screen")
}
}
The NavController is created via the rememberNavController() method up in the composable hirarchy. I also do not want to pass the controller down the composable hierarchy or use a CompositionLocal. The preferred approach would be to have the controller available in the ViewModel.
if you want to inject it to viewmodel, you should create navigation controller using application context, install it in application (singleton) component and mark it as singleton:
#Module
#InstallIn(SingletonComponent::class)
object NavigationModule {
#Provides
#Singleton
fun provideNavController(#ApplicationContext context: Context) = NavHostController(context).apply {
navigatorProvider.addNavigator(ComposeNavigator())
navigatorProvider.addNavigator(DialogNavigator())
}
}
but in my opinion this is not good practise
Related
I'm having trouble complying with the MVVM with Jetpack Compose, because my ViewModel needs to access a view.
I need to stream a camera source onto a SurfaceView. However, this source has an independant lifecycle : it can be plugged in or removed anytime, while the view is visible or not.
To start streaming, I need to provide the SurfaceView to an external SDK. How can I do that without breaking the MVVVM?
I want to start streaming :
whenever the view appears (first composition), if the source is available
whenever the source becomes available, if the view is visible
What I've done so far is to inject the SurfaceView into the ViewModel
class CameraVM: ViewModel() {
lateinit var surfaceView: SurfaceView
init {
listenForStreamSourceAvailability()
}
// Called when source becomes available
fun startStreaming() {
camera.setupStreaming(surfaceView)
camera.startStreaming()
}
And in CameraView
#Composable
fun CameraView(viewModel: CameraVM = viewModel()) {
val context = LocalContext.current
val localSurfaceView: SurfaceView = remember {
val surfaceView = SurfaceView(context)
// Inject surface view only at first composition
viewModel.surfaceView = surfaceView
surfaceView
}
AndroidView(
factory = {
localSurfaceView
}
)
}
Maybe DisposableEffect could be useful ?
I'm looking to create an EnvironmentObject that can be accessed by the View Model (not just the view).
The Environment object tracks the application session data, e.g. loggedIn, access token etc, this data will be passed into the view models (or service classes where needed) to allow calling of an API to pass data from this EnvironmentObjects.
I have tried to pass in the session object to the initialiser of the view model class from the view but get an error.
how can I access/pass the EnvironmentObject into the view model using SwiftUI?
You can do it like this:
struct YourView: View {
#EnvironmentObject var settings: UserSettings
#ObservedObject var viewModel = YourViewModel()
var body: some View {
VStack {
Text("Hello")
}
.onAppear {
self.viewModel.setup(self.settings)
}
}
}
For the ViewModel:
class YourViewModel: ObservableObject {
var settings: UserSettings?
func setup(_ settings: UserSettings) {
self.settings = settings
}
}
You shouldn't. It's a common misconception that SwiftUI works best with MVVM. MVVM has no place in SwiftUI. You are asking that if you can shove a rectangle to fit a triangle shape. It wouldn't fit.
Let's start with some facts and work step by step:
ViewModel is a model in MVVM.
MVVM does not take value types (e.g.; no such thing in Java) into consideration.
A value type model (model without state) is considered safer than reference type model (model with state) in the sense of immutability.
Now, MVVM requires you to set up a model in such way that whenever it changes, it updates the view in some pre-determined way. This is known as binding.
Without binding, you won't have nice separation of concerns, e.g.; refactoring out model and associated states and keeping them separate from view.
These are the two things most iOS MVVM developers fail:
iOS has no "binding" mechanism in traditional Java sense. Some would just ignore binding, and think calling an object ViewModel automagically solves everything; some would introduce KVO-based Rx, and complicate everything when MVVM is supposed to make things simpler.
Model with state is just too dangerous because MVVM put too much emphasis on ViewModel, too little on state management and general disciplines in managing control; most of the developers end up thinking a model with state that is used to update view is reusable and testable. This is why Swift introduces value type in the first place; a model without state.
Now to your question: you ask if your ViewModel can have access to EnvironmentObject (EO)?
You shouldn't. Because in SwiftUI a model that conforms to View automatically has reference to EO. E.g.;
struct Model: View {
#EnvironmentObject state: State
// automatic binding in body
var body: some View {...}
}
I hope people can appreciate how compact SDK is designed.
In SwiftUI, MVVM is automatic. There's no need for a separate ViewModel object that manually binds to view which requires an EO reference passed to it.
The above code is MVVM. E.g.; a model with binding to view. But because model is value type, so instead of refactoring out model and state as view model, you refactor out control (in protocol extension, for example).
This is official SDK adapting design pattern to language feature, rather than just enforcing it. Substance over form. Look at your solution, you have to use singleton which is basically global. You should know how dangerous it is to access global anywhere without protection of immutability, which you don't have because you have to use reference type model!
TL;DR
You don't do MVVM in java way in SwiftUI. And the Swift-y way to do it is no need to do it, it's already built-in.
Hope more developer see this since this seemed like a popular question.
Below provided approach that works for me. Tested with many solutions started with Xcode 11.1.
The problem originated from the way EnvironmentObject is injected in view, general schema
SomeView().environmentObject(SomeEO())
ie, at first - created view, at second created environment object, at third environment object injected into view
Thus if I need to create/setup view model in view constructor the environment object is not present there yet.
Solution: break everything apart and use explicit dependency injection
Here is how it looks in code (generic schema)
// somewhere, say, in SceneDelegate
let someEO = SomeEO() // create environment object
let someVM = SomeVM(eo: someEO) // create view model
let someView = SomeView(vm: someVM) // create view
.environmentObject(someEO)
There is no any trade-off here, because ViewModel and EnvironmentObject are, by design, reference-types (actually, ObservableObject), so I pass here and there only references (aka pointers).
class SomeEO: ObservableObject {
}
class BaseVM: ObservableObject {
let eo: SomeEO
init(eo: SomeEO) {
self.eo = eo
}
}
class SomeVM: BaseVM {
}
class ChildVM: BaseVM {
}
struct SomeView: View {
#EnvironmentObject var eo: SomeEO
#ObservedObject var vm: SomeVM
init(vm: SomeVM) {
self.vm = vm
}
var body: some View {
// environment object will be injected automatically if declared inside ChildView
ChildView(vm: ChildVM(eo: self.eo))
}
}
struct ChildView: View {
#EnvironmentObject var eo: SomeEO
#ObservedObject var vm: ChildVM
init(vm: ChildVM) {
self.vm = vm
}
var body: some View {
Text("Just demo stub")
}
}
Solution for: iOS 14/15+
Here's how you might interact with an Environment Object from a View Model, without having to inject it on instantiation:
Define the Environment Object:
import Combine
final class MyAuthService: ObservableObject {
#Published private(set) var isSignedIn = false
func signIn() {
isSignedIn = true
}
}
Create a View to own and pass around the Environment Object:
import SwiftUI
struct MyEntryPointView: View {
#StateObject var auth = MyAuthService()
var body: some View {
content
.environmentObject(auth)
}
#ViewBuilder private var content: some View {
if auth.isSignedIn {
Text("Yay, you're all signed in now!")
} else {
MyAuthView()
}
}
}
Define the View Model with methods that take the Environment Object as an argument:
extension MyAuthView {
#MainActor final class ViewModel: ObservableObject {
func signIn(with auth: MyAuthService) {
auth.signIn()
}
}
}
Create a View that owns the View Model, receives the Environment Object, and calls the appropriate method:
struct MyAuthView: View {
#EnvironmentObject var auth: MyAuthService
#StateObject var viewModel = ViewModel()
var body: some View {
Button {
viewModel.signIn(with: auth)
} label: {
Text("Sign In")
}
}
}
Preview it for completeness:
struct MyEntryPointView_Previews: PreviewProvider {
static var previews: some View {
MyEntryPointView()
}
}
I choose to not have a ViewModel. (Maybe time for a new pattern?)
I have setup my project with a RootView and some child views. I setup my RootView with a App object as the EnvironmentObject. Instead of the ViewModel accessing Models, all my views access classes on App. Instead of the ViewModel determining the layout, the view hierarchy determine the layout. From doing this in practice for a few apps, I've found my views are staying small and specific. As an over simplification:
class App: ObservableObject {
#Published var user = User()
let networkManager: NetworkManagerProtocol
lazy var userService = UserService(networkManager: networkManager)
init(networkManager: NetworkManagerProtocol) {
self.networkManager = networkManager
}
convenience init() {
self.init(networkManager: NetworkManager())
}
}
struct RootView: View {
#EnvironmentObject var app: App
var body: some View {
if !app.user.isLoggedIn {
LoginView()
} else {
HomeView()
}
}
}
struct HomeView: View {
#EnvironmentObject var app: App
var body: some View {
VStack {
Text("User name: \(app.user.name)")
Button(action: { app.userService.logout() }) {
Text("Logout")
}
}
}
}
In my previews, I initialize a MockApp which is a subclass of App. The MockApp initializes the designated initializers with the Mocked object. Here the UserService doesn't need to be mocked, but the datasource (i.e. NetworkManagerProtocol) does.
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
Group {
HomeView()
.environmentObject(MockApp() as App) // <- This is needed for EnvironmentObject to treat the MockApp as an App Type
}
}
}
The Resolver library does a nice job to get dependency injection for model classes. It provides a property wrapper #Injected which is very similar in spirit to #EnvironmentObject but works everywhere. So in a model, I would inject a ExampleService like this:
class ExampleModel: ObservableObject {
#Injected var service: ExampleService
// ...
}
This can also be used to resolve dependencies for Views:
struct ExampleView: View {
#ObservedObject var exampleModel: ExampleModel = Resolver.resolve()
var body: some View {
// ...
}
}
An alternative for Views is to use #EnvironmentObject in the SwiftUI view hierarchy, but this gets a little bit cumbersome because you'll have two dependency-injection containers, Resolver/#Injected for everything that's app-wide/service-like and SwiftUI/#EnvironmentObject in the view hierarchy for everything that relates to views/for view models.
Simply create a Singleton and use it wherever you want (view / class / struct / ObservableObject ...)
Creating Class should look like this:
class ApplicationSessionData
{
// this is the shared instance / local copy / singleton
static let singleInstance = ApplicationSessionData()
// save shared mambers/vars here
var loggedIn: Bool = false
var access: someAccessClass = someAccessClass()
var token: String = "NO TOKET OBTAINED YET"
...
}
Using Class/Struct/View should look like this:
struct SomeModel {
// obtain the shared instance
var appSessData = ApplicationSessionData.singleInstance
// use shared mambers/vars here
if(appSessData.loggedIn && appSessData.access.hasAccessToThisView) {
appSessData.token = "ABC123RTY..."
...
}
}
You need to be aware of the pitfalls that exist in Singletons, so you won't fall into one.
Read more here: https://matteomanferdini.com/swift-singleton
I'm looking to create an EnvironmentObject that can be accessed by the View Model (not just the view).
The Environment object tracks the application session data, e.g. loggedIn, access token etc, this data will be passed into the view models (or service classes where needed) to allow calling of an API to pass data from this EnvironmentObjects.
I have tried to pass in the session object to the initialiser of the view model class from the view but get an error.
how can I access/pass the EnvironmentObject into the view model using SwiftUI?
You can do it like this:
struct YourView: View {
#EnvironmentObject var settings: UserSettings
#ObservedObject var viewModel = YourViewModel()
var body: some View {
VStack {
Text("Hello")
}
.onAppear {
self.viewModel.setup(self.settings)
}
}
}
For the ViewModel:
class YourViewModel: ObservableObject {
var settings: UserSettings?
func setup(_ settings: UserSettings) {
self.settings = settings
}
}
You shouldn't. It's a common misconception that SwiftUI works best with MVVM. MVVM has no place in SwiftUI. You are asking that if you can shove a rectangle to fit a triangle shape. It wouldn't fit.
Let's start with some facts and work step by step:
ViewModel is a model in MVVM.
MVVM does not take value types (e.g.; no such thing in Java) into consideration.
A value type model (model without state) is considered safer than reference type model (model with state) in the sense of immutability.
Now, MVVM requires you to set up a model in such way that whenever it changes, it updates the view in some pre-determined way. This is known as binding.
Without binding, you won't have nice separation of concerns, e.g.; refactoring out model and associated states and keeping them separate from view.
These are the two things most iOS MVVM developers fail:
iOS has no "binding" mechanism in traditional Java sense. Some would just ignore binding, and think calling an object ViewModel automagically solves everything; some would introduce KVO-based Rx, and complicate everything when MVVM is supposed to make things simpler.
Model with state is just too dangerous because MVVM put too much emphasis on ViewModel, too little on state management and general disciplines in managing control; most of the developers end up thinking a model with state that is used to update view is reusable and testable. This is why Swift introduces value type in the first place; a model without state.
Now to your question: you ask if your ViewModel can have access to EnvironmentObject (EO)?
You shouldn't. Because in SwiftUI a model that conforms to View automatically has reference to EO. E.g.;
struct Model: View {
#EnvironmentObject state: State
// automatic binding in body
var body: some View {...}
}
I hope people can appreciate how compact SDK is designed.
In SwiftUI, MVVM is automatic. There's no need for a separate ViewModel object that manually binds to view which requires an EO reference passed to it.
The above code is MVVM. E.g.; a model with binding to view. But because model is value type, so instead of refactoring out model and state as view model, you refactor out control (in protocol extension, for example).
This is official SDK adapting design pattern to language feature, rather than just enforcing it. Substance over form. Look at your solution, you have to use singleton which is basically global. You should know how dangerous it is to access global anywhere without protection of immutability, which you don't have because you have to use reference type model!
TL;DR
You don't do MVVM in java way in SwiftUI. And the Swift-y way to do it is no need to do it, it's already built-in.
Hope more developer see this since this seemed like a popular question.
Below provided approach that works for me. Tested with many solutions started with Xcode 11.1.
The problem originated from the way EnvironmentObject is injected in view, general schema
SomeView().environmentObject(SomeEO())
ie, at first - created view, at second created environment object, at third environment object injected into view
Thus if I need to create/setup view model in view constructor the environment object is not present there yet.
Solution: break everything apart and use explicit dependency injection
Here is how it looks in code (generic schema)
// somewhere, say, in SceneDelegate
let someEO = SomeEO() // create environment object
let someVM = SomeVM(eo: someEO) // create view model
let someView = SomeView(vm: someVM) // create view
.environmentObject(someEO)
There is no any trade-off here, because ViewModel and EnvironmentObject are, by design, reference-types (actually, ObservableObject), so I pass here and there only references (aka pointers).
class SomeEO: ObservableObject {
}
class BaseVM: ObservableObject {
let eo: SomeEO
init(eo: SomeEO) {
self.eo = eo
}
}
class SomeVM: BaseVM {
}
class ChildVM: BaseVM {
}
struct SomeView: View {
#EnvironmentObject var eo: SomeEO
#ObservedObject var vm: SomeVM
init(vm: SomeVM) {
self.vm = vm
}
var body: some View {
// environment object will be injected automatically if declared inside ChildView
ChildView(vm: ChildVM(eo: self.eo))
}
}
struct ChildView: View {
#EnvironmentObject var eo: SomeEO
#ObservedObject var vm: ChildVM
init(vm: ChildVM) {
self.vm = vm
}
var body: some View {
Text("Just demo stub")
}
}
Solution for: iOS 14/15+
Here's how you might interact with an Environment Object from a View Model, without having to inject it on instantiation:
Define the Environment Object:
import Combine
final class MyAuthService: ObservableObject {
#Published private(set) var isSignedIn = false
func signIn() {
isSignedIn = true
}
}
Create a View to own and pass around the Environment Object:
import SwiftUI
struct MyEntryPointView: View {
#StateObject var auth = MyAuthService()
var body: some View {
content
.environmentObject(auth)
}
#ViewBuilder private var content: some View {
if auth.isSignedIn {
Text("Yay, you're all signed in now!")
} else {
MyAuthView()
}
}
}
Define the View Model with methods that take the Environment Object as an argument:
extension MyAuthView {
#MainActor final class ViewModel: ObservableObject {
func signIn(with auth: MyAuthService) {
auth.signIn()
}
}
}
Create a View that owns the View Model, receives the Environment Object, and calls the appropriate method:
struct MyAuthView: View {
#EnvironmentObject var auth: MyAuthService
#StateObject var viewModel = ViewModel()
var body: some View {
Button {
viewModel.signIn(with: auth)
} label: {
Text("Sign In")
}
}
}
Preview it for completeness:
struct MyEntryPointView_Previews: PreviewProvider {
static var previews: some View {
MyEntryPointView()
}
}
I choose to not have a ViewModel. (Maybe time for a new pattern?)
I have setup my project with a RootView and some child views. I setup my RootView with a App object as the EnvironmentObject. Instead of the ViewModel accessing Models, all my views access classes on App. Instead of the ViewModel determining the layout, the view hierarchy determine the layout. From doing this in practice for a few apps, I've found my views are staying small and specific. As an over simplification:
class App: ObservableObject {
#Published var user = User()
let networkManager: NetworkManagerProtocol
lazy var userService = UserService(networkManager: networkManager)
init(networkManager: NetworkManagerProtocol) {
self.networkManager = networkManager
}
convenience init() {
self.init(networkManager: NetworkManager())
}
}
struct RootView: View {
#EnvironmentObject var app: App
var body: some View {
if !app.user.isLoggedIn {
LoginView()
} else {
HomeView()
}
}
}
struct HomeView: View {
#EnvironmentObject var app: App
var body: some View {
VStack {
Text("User name: \(app.user.name)")
Button(action: { app.userService.logout() }) {
Text("Logout")
}
}
}
}
In my previews, I initialize a MockApp which is a subclass of App. The MockApp initializes the designated initializers with the Mocked object. Here the UserService doesn't need to be mocked, but the datasource (i.e. NetworkManagerProtocol) does.
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
Group {
HomeView()
.environmentObject(MockApp() as App) // <- This is needed for EnvironmentObject to treat the MockApp as an App Type
}
}
}
The Resolver library does a nice job to get dependency injection for model classes. It provides a property wrapper #Injected which is very similar in spirit to #EnvironmentObject but works everywhere. So in a model, I would inject a ExampleService like this:
class ExampleModel: ObservableObject {
#Injected var service: ExampleService
// ...
}
This can also be used to resolve dependencies for Views:
struct ExampleView: View {
#ObservedObject var exampleModel: ExampleModel = Resolver.resolve()
var body: some View {
// ...
}
}
An alternative for Views is to use #EnvironmentObject in the SwiftUI view hierarchy, but this gets a little bit cumbersome because you'll have two dependency-injection containers, Resolver/#Injected for everything that's app-wide/service-like and SwiftUI/#EnvironmentObject in the view hierarchy for everything that relates to views/for view models.
Simply create a Singleton and use it wherever you want (view / class / struct / ObservableObject ...)
Creating Class should look like this:
class ApplicationSessionData
{
// this is the shared instance / local copy / singleton
static let singleInstance = ApplicationSessionData()
// save shared mambers/vars here
var loggedIn: Bool = false
var access: someAccessClass = someAccessClass()
var token: String = "NO TOKET OBTAINED YET"
...
}
Using Class/Struct/View should look like this:
struct SomeModel {
// obtain the shared instance
var appSessData = ApplicationSessionData.singleInstance
// use shared mambers/vars here
if(appSessData.loggedIn && appSessData.access.hasAccessToThisView) {
appSessData.token = "ABC123RTY..."
...
}
}
You need to be aware of the pitfalls that exist in Singletons, so you won't fall into one.
Read more here: https://matteomanferdini.com/swift-singleton
I'm developing an iOS application using Xamarin.iOS (Code only, no storyboards) and I wonder what the best way to send data back to the original uiviewcontroller when I pop from the navigationcontroller.
In android I use StartActivityForResult and then override OnResult, but I can't find a similar way for iOS.
I know there's overrides for ViewDidLoad, ViewDidAppear, etc, what I'm looking for is some kind of ViewDidGetPoppedBackTo (hope you get it).
Or is there another better way to achieve this?
NavigationController keeps track of all the ViewControllers as an array: NavigationController.ViewControllers
You can get an existing instance of the ViewController Type from this array via following code:
(You may write this method in BaseViewController if you have it.)
public T InstanceFromNavigationStack<T> () where T : UIViewController
{
return (T)NavigationController.ViewControllers.FirstOrDefault(v => v is T);
}
Then use it like :
var myVCInstance = InstanceFromNavigationStack<MyTargetViewController>();
if(myVCInstance != null)
{
//Assign a value like
myVCInstance.MyVariable = "MyValue";
//Or call a method like
myVCInstance.MethodToReloadView("MyValue")
}
//Go Back Navigation Code
//Then here write your navigation logic to go back.
This not only helps passing data in Previous ViewController, but Any ViewController in the stack. Simply pass the Type of it to get an Instance from Stack.
NOTE: This should work if your Navigation stack doesn't have multiple instance of the same ViewController Type.
Use this way
ViewController viewController = (ViewController)NavigationController.TopViewController;
viewController.SendData(myevent);
Create method SendData in your ToViewController this method is called first when navigationg back and your data send to your previous ViewController.
Another option that I've started using is EventHandler methods. Here is an example I use to populate a UITextField in the parent view controller with the selection from a UITableView (child view controller) and then close the child.
Define the EventHandler method in your parent view controller:
void LocationLookup_OnSelected(object sender, EventArgs e)
{
chosenLocation = (MKPlacemark)sender;
planLocation.Text = chosenLocation.Name;
this.ParentViewController.DismissViewController(true, null);
}
Pass the EventHandler method from the parent as a property of the child.
public partial class LocationLookupViewController : UITableViewController
{
private event EventHandler OnSelected;
public LocationLookupViewController(EventHandler OnSelected)
{
this.OnSelected = OnSelected;
}
...
}
Call the EventHandler passing in the object / data that you need in the parent
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
...
OnSelected(response?.MapItems[0].Placemark, new EventArgs());
}
Note - The above class and function are incomplete but should provide you with an idea of how this technique works.
I think I misunderstand something about MVC. I'm trying to do the following:
public class ControllerA : Controller
{
public ActionResult Index()
{
// do code
// perform action on ControllerB - something like:
// RedirectToAction("Action", "ControllerB");
// CARRY ON with more code
}
}
public class ControllerB : Controller
{
public void Action()
{
// do code
}
}
Obviously RedirectToAction("Action", "ControllerB"); isn't working. So how do I do it? I guess I could have all controllers that need to use Action() inherit from ControllerB but that feels a really bad way to do it. Please help!
You have to return the ActionResult from RedirectToAction()
return RedirectToAction("Action", "ControllerB");
is what you need to do if you want RedirectToAction to actually redirect to an action. After you clarified what "isn't working" means to you I think you should just have all controllers inherit from a base. That is a pretty standard approach.
public class ControllerA : ControllerB
{
public ActionResult Index()
{
// do code
Action();
// CARRY ON with more code
}
}
public class ControllerB : Controller
{
public void Action()
{
// do code
}
}
I believe the controller you are currently executing is an instance of the class so you would need to make an instance of controller B to be able to execute anything on it, so what you are trying to do there just won't really work without a hack.
I think however there is 2 methods to better get the results i think you are after:
1) Make a 'ControllerBase' class and have all controllers inherit from it instead of from 'Controller' then any shared code you can add into the base class (as a static method perhaps) and then all controllers can access it as a member nice and easy.
2) As MVC will make a straight up DLL you can add in new classes as you need, eg add a new project folder like 'Globals' add a new class file called 'Funcs' and there you have a static lib that you can access from anywhere, Funcs.SendEmail(); etc
If i'm off the mark ok! happy coding anyway heh
I have injected controllers with a factory method from the controller factory as a delegate (Func CreateController), and used that to create sub-controllers, such as in this circumstance. There's plenty of ways to accomplish your goal, but I think that might be quick way to get what you want working.