I need to implement a mechanism just like react private and public route on swiftUI. Basically I have tens of views and some of these views requires authentication based on user logged in status. So far I have tried to hold current screen in an Environment Object as show in following class
enum Routes {
case screenA,
screenB,
screenC,
screenD,
screenE,
screenF,
screenG,
loginScreen
var isAuthRequired: Bool {
if case . screenA = self {
return true
} else if case . screenD = self {
return true
} else {
return false
}
}
}
class AuthenticatedRoute: ObservableObject {
#Published var currentRoute: Routes
init(){
self.currentRoute = . screenA
}
}
And on my main screen I check every time the current screen change whether user loggedin and current page require authentication.
struct MainView: View {
#StateObject var authenticatedRoute = AuthenticatedRoute()
#EnvironmentObject var userAuth: UserAuth
var body: some View {
mainView()
.environmentObject(authenticatedRoute)
}
#ViewBuilder
func mainView() -> some View {
if (self.authenticatedRoute.currentRoute.isAuthRequired && !userAuth.isLoggedIn) {
LoginView()
}
else {
DefaultTabView()
}
}
}
And this is an example of how I keep changing this environment variable. I change the environment object onAppear event method of view.
struct ScreenA: View {
#EnvironmentObject var authenticatedRoute: AuthenticatedRoute
var body: some View {
NavigationView {
someContent()
}.onAppear {
authenticatedRoute.currentRoute = .screenA
}
}
}
While this approach works for most cases for some reason it behave strange when a screen is in tab navigation. Also I do not feel comfortable with this solution, that I need to change screen name manually on every single page, and checking authentication status in main view. I think it would be better somehow if I can write a kind of interceptor before every page change and check if desired destination requires authentication and if user is authenticated but I could not find a way manage this. I'm relatively new to iOS development and had experience with react native but this should not be so hard to implement in my opinion since this is a requirement for most applications.
So basically I need to implement a private and public router in swiftUI or intercept every page change so I should not modify environment variable on each pages manually and should not check conditions in MainView inside a function.
I can propose another approach, that does not require the class AuthenticatedRoute, I hope it's what you are looking for. The process is:
In the class UserAuth, create a static shared instance, that can be called anywhere in your code and ensures you are always using the same instance.
Create a modifier extending View, that reads the status in UserAuth.shared and shows the necessary view according to whether the authentication is required (LoginView() if the user is not authenticated).
Use the modifier at the outermost container (VStack, NavigationView, whatever) of any view that requires the user to be authenticated.
The example below shows how this can work, if you want to run it:
1. Static UserAuth instance
class UserAuth: ObservableObject {
static let shared = UserAuth() // This is to assure that you refer to the same instance all over the code
#Published private(set) var isLoggedIn = false // Use the variable you already have
func logInOrOff() { // Implement each func as needed
isLoggedIn.toggle()
}
}
2. Create the View modifier
extension View {
#ViewBuilder
func requiresAuthentication() -> some View {
if UserAuth.shared.isLoggedIn { // "shared" is the same instance used by the views
self
} else {
LoginView()
}
}
}
3. Apply the modifier at the bottom of the view that requires authentication
struct Example: View {
#StateObject private var userAuth = UserAuth.shared // Or #EnvironmentObject, as you wish
var body: some View {
VStack {
Text(userAuth.isLoggedIn ? "Now we're good" : "You must log in")
.padding()
Button {
userAuth.logInOrOff()
} label: {
Text("Logoff")
}
}
.requiresAuthentication() // This is what makes your view safe
}
}
struct LoginView: View {
var body: some View {
VStack {
Text("Authentication is required")
.padding()
Button {
UserAuth.shared.logInOrOff()
} label: {
Text("Log in")
}
}
}
}
Related
Description
For programatic navigation you could previously use NavigationLink(isActive:, destination:, label:) which would fire navigation when the isActive param is true. In IOS 16 this became deprecated and NavigationStack, NavigationLink(value:, label:) and NavigationPath was introduced.
To read about the usage of these follow the links:
https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
https://www.hackingwithswift.com/articles/250/whats-new-in-swiftui-for-ios-16 (search for NavigationStack)
My question is how should I use and maintain the array with the content of the navigation stack (like the NavigationPath object) if I'd like to use it in different Views and in their ViewModels?
As you can see in the code below I created a NavigationPath object to hold my navigation stack in the BaseView or BaseView.ViewModel. This way I can do programatic navigation from this BaseView to other pages (Page1, Page2), which is great.
But if I go to Page1 and try to navigate from there to Page2 programatically I need to have access to the original NavigationPath object object, the one that I use in BaseView.
What would be the best way to access this original object?
It is possible that I misunderstand the usage of this new feature but if you have any possible solutions for programatic navigation from a ViewModel I would be glad to see it :)
Code
What I've tried to do:
struct BaseView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationStack(path: $viewModel.paths) {
VStack {
Button("Page 1", action: viewModel.goToPage1)
Button("Page 2", action: viewModel.goToPage2)
}
.navigationDestination(for: String.self) { stringParam in
Page1(stringParam: stringParam)
}
.navigationDestination(for: Int.self) { intParam in
Page2(intParam: intParam)
}
}
}
}
extension BaseView {
#MainActor class ViewModel: ObservableObject {
#Published var paths = NavigationPath()
func goToPage1() {
let param = "Some random string" // gets the parameter from some calculation or async network call
paths.append(param)
}
func goToPage2() {
let param = 19 // gets the parameter from some calculation or async network call
paths.append(param)
}
}
}
struct Page1: View {
#StateObject var viewModel = ViewModel()
let stringParam: String
var body: some View {
VStack {
Button("Page 2", action: viewModel.goToPage2)
}
}
}
extension Page1 {
#MainActor class ViewModel: ObservableObject {
func goToPage2() {
// Need to add value to the original paths variable in BaseView.ViewModel
}
}
}
struct Page2: View {
#StateObject var viewModel = ViewModel()
let intParam: Int
var body: some View {
Text("\(intParam)")
}
}
extension Page2 {
#MainActor class ViewModel: ObservableObject {
}
}
There is no need for MVVM in SwiftUI because the View struct plus property wrappers is already equivalent to a view model object but faster and less error prone. Also in SwiftUI we don't even have access to the traditional view layer - it takes our View data structs, diffs them to create/update/remove UIView/NSView objects, using the best ones for the platform/context. If you use an object for view data instead, then you'll just have the same consistency problems that SwiftUI was designed to eliminate.
Sadly the web (and Harvard University) is filled with MVVM SwiftUI articles by people that didn't bother to learn it properly. Fortunately things are changing:
I was wrong! MVVM is NOT a good choice for building SwiftUI applications (Azam Sharp)
How MVVM devs get MVVM wrong in SwiftUI: From view model to state (Jim Lai)
Stop using MVVM for SwiftUI (Apple Developer Forums)
I have a logging onboarding being finished, and I need to present a HomeView, which knows nothing about previous navigation flow.
var body: some View {
if viewModel.isValidated {
destination()
} else {
LoadingView()
}
Doing it this way I have a navigation bar at the top of destination(). I guess I can hide it, but it would still be the same navigation flow and I need to start a new one. How can I achieve that?(iOS 13)
One way to handle this is with an #Environment object created from a BaseViewModel. The way that this works is to essentially control the state of the presented view from a BaseView or a view controller. I'll attempt to simplify it for you the best I can.
class BaseViewModel: ObservableObject {
#Published var baseView: UserFlow = .loading
init() {
//Handle your condition if already logged in, change
//baseView to whatever you need it to be.
}
enum UserFlow {
case loading, onboarding, login, home
}
}
Once you've setup your BaseViewModel you'll want to use it, I use it in a switch statement with a binding to an #EnvironmentObject so that it can be changed from any other view.
struct BaseView: View {
#EnvironmentObject var appState: BaseViewModel
var body: some View {
Group {
switch appState.userFlow {
case .loading:
LoadingView()
case .onboarding:
Text("Not Yet Implemented")
case .login:
LandingPageView()
case .home:
BaseHomeScreenView().environmentObject(BaseHomeScreenViewModel())
}
}
}
}
Your usage, likely at the end of your register/login flow, will look something like this.
struct LoginView: View {
#EnvironmentObject var appState: BaseViewModel
var body: some View {
Button(action: {appState = .home}, label: Text("Log In"))
}
}
So essentially what's happening here is that you're storing your app flow in a particular view which is never disposed of. Think of it like a container. Whenever you change it, it changes the particular view you want to present. The especially good thing about this is that you can build a separate navigation hierarchy without the use of navigation links, if you wanted.
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
This one I've been researching for a few days, scouring the Swift & SwiftUI docs, SO, forums, etc. and can't seem to find an answer.
Here is the problem;
I have a SwiftUI custom View that does some state determination on a custom API request class to a remote resource. The View handles showing loading states and failure states, along with its body contents being passed through via ViewBuilder so that if the state from the API is successful and the resource data is loaded, it will show the contents of the page.
The issue is, the ViewBuilder contents does not re-render when the subclassed ObservedObject updates. The Object updates in reaction to the UI (when buttons are pressed, etc.) but the UI never re-renders/updates to reflect the change within the subclassed ObservedObject, for example the ForEach behind an array within the subclassed ObservedObject does not refresh when the array contents change. If I move it out of the custom View, the ForEach works as intended.
I can confirm the code compiles and runs. Observers and debugPrint()'s throughout show that the ApiObject is updating state correctly and the View reflects the ApiState change absolutely fine. It's just the Content of the ViewBuilder. In which I assume is because the ViewBuilder will only ever be called once.
EDIT: The above paragraph should have been the hint, the ApiState updates correctly, but after putting extensive logging into the application, the UI was not listening to the publishing of the subclassed ObservedObject. The properties were changing and the state was too, but the UI wasn't being reactive to it.
Also, the next sentence turned out to be false, I tested again in a VStack and the component still didn't re-render, meaning I was looking in the wrong place!
If this is the case, how does VStack and other such elements get around this?
Or is it because my ApiObjectView is being re-rendered on the state change, in which causes the child view to 'reset'? Although in this circumstance I'd expect it to then take on the new data and work as expected anyway, its just never re-rendering.
The problematic code is in the CustomDataList.swift and ApiObjectView.swift below. I've left comments to point in the right direction.
Here is the example code;
// ApiState.swift
// Stores the API state for where the request and data parse is currently at.
// This drives the ApiObjectView state UI.
import Foundation
enum ApiState: String
{
case isIdle
case isFetchingData
case hasFailedToFetchData
case isLoadingData
case hasFailedToLoadData
case hasUsableData
}
// ApiObject.swift
// A base class that the Controllers for the app extend from.
// These classes can make data requests to the remote resource API over the
// network to feed their internal data stores.
class ApiObject: ObservableObject
{
#Published var apiState: ApiState = .isIdle
let networkRequest: NetworkRequest = NetworkRequest(baseUrl: "https://api.example.com/api")
public func apiGetJson<T: Codable>(to: String, decodeAs: T.Type, onDecode: #escaping (_ unwrappedJson: T) -> Void) -> Void
{
self.apiState = .isFetchingData
self.networkRequest.send(
to: to,
onComplete: {
self.apiState = .isLoadingData
let json = self.networkRequest.decodeJsonFromResponse(decodeAs: decodeAs)
guard let unwrappedJson = json else {
self.apiState = .hasFailedToLoadData
return
}
onDecode(unwrappedJson)
self.apiState = .hasUsableData
},
onFail: {
self.apiState = .hasFailedToFetchData
}
)
}
}
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
// Subclassed from the ApiObject, inheriting ObservableObject
import Foundation
import Combine
class CustomDataController: ApiObject
{
#Published public var customData: [CustomDataStruct] = []
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
This is the View that has the problem with re-rendering its ForEach on the ObservedObject change to its bound array property.
// CustomDataList.swift
// This is the SwiftUI View that drives the content to the user as a list
// that displays the CustomDataController.customData.
// The ForEach in this View
import SwiftUI
struct CustomDataList: View
{
#ObservedObject var customDataController: CustomDataController = CustomDataController()
var body: some View
{
ApiObjectView(
apiObject: self.customDataController,
onQuit: {}
) {
List
{
Section(header: Text("Custom Data").padding(.top, 40))
{
ForEach(self.customDataController.customData, id: \.self, content: { customData in
// This is the example that doesn't re-render when the
// customDataController updates its data. I have
// verified via printing at watching properties
// that the object is updating and pushing the
// change.
// The ObservableObject updates the array, but this ForEach
// is not run again when the data is changed.
// In the production code, there are buttons in here that
// change the array data held within customDataController.customData.
// When tapped, they update the array and the ForEach, when placed
// in the body directly does reflect the change when
// customDataController.customData updates.
// However, when inside the ApiObjectView, as by this example,
// it does not.
Text(customData.textProperty)
})
}
}
.listStyle(GroupedListStyle())
}
.navigationBarTitle(Text("Learn"))
.onAppear() {
self.customDataController.fetch()
}
}
}
struct CustomDataList_Previews: PreviewProvider
{
static var previews: some View
{
CustomDataList()
}
}
This is the custom View in question that doesn't re-render its Content.
// ApiObjectView
// This is the containing View that is designed to assist in the UI rendering of ApiObjects
// by handling the state automatically and only showing the ViewBuilder contents when
// the state is such that the data is loaded and ready, in a non errornous, ready state.
// The ViewBuilder contents loads fine when the view is rendered or the state changes,
// but the Content is never re-rendered if it changes.
// The state renders fine and is reactive to the object, the apiObjectContent
// however, is not.
import SwiftUI
struct ApiObjectView<Content: View>: View {
#ObservedObject var apiObject: ApiObject
let onQuit: () -> Void
let apiObjectContent: () -> Content
#inlinable public init(apiObject: ApiObject, onQuit: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content) {
self.apiObject = apiObject
self.onQuit = onQuit
self.apiObjectContent = content
}
func determineViewBody() -> AnyView
{
switch (self.apiObject.apiState) {
case .isIdle:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isFetchingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isLoadingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .hasFailedToFetchData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasFailedToLoadData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasUsableData:
return AnyView(
VStack
{
self.apiObjectContent()
}
)
}
}
var body: some View
{
self.determineViewBody()
}
}
struct ApiObjectView_Previews: PreviewProvider {
static var previews: some View {
ApiObjectView(
apiObject: ApiObject(),
onQuit: {
print("I quit.")
}
) {
EmptyView()
}
}
}
Now, all the above code works absolutely fine, if the ApiObjectView isn't used and the contents placed in the View directly.
But, that is horrendous for code reuse and architecture, this way its nice and neat, but doesn't work.
Is there any other way to approach this, e.g. via a ViewModifier or a View extension?
Any help on this would be really appreciated.
As I said, I can't seem to find anyone with this problem or any resource online that can point me in the right direction to solve this problem, or what might be causing it, such as outlined in documentation for ViewBuilder.
EDIT: To throw something interesting in, I've since added a countdown timer to CustomDataList, which updates a label every 1 second. IF the text is updated by that timer object, the view is re-rendered, but ONLY when the text on the label displaying the countdown time is updated.
Figured it out after pulling my hair out for a week, its an undocumented issue with subclassing an ObservableObject, as seen in this SO answer.
This is particularily annoying as Xcode obviously prompts you to remove the class as the parent class provides that inheritence to ObservableObject, so in my mind all was well.
The fix is, within the subclassed class to manually fire the generic state change self.objectWillChange.send() via the willSet listener on the #Published variable in question, or any you require.
In the examples I provided, the base class ApiObject in the question remains the same.
Although, the CustomDataController needs to be modified as follows:
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
#Published public var customData: [CustomDataStruct] = [] {
willSet {
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
As soon as I added that manual publishing, the issue is resolved.
An important note from the linked answer: Do not redeclare objectWillChange on the subclass, as that will again cause the state not to update properly. E.g. declaring the default
let objectWillChange = PassthroughSubject<Void, Never>()
on the subclass will break the state updating again, this needs to remain on the parent class that extends from ObservableObject directly, either my manual or automatic default definition (typed out, or not and left as inherited declaration).
Although you can still define as many custom PassthroughSubject declarations as you require without issue on the subclass, e.g.
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
var customDataWillUpdate = PassthroughSubject<[CustomDataStruct], Never>()
#Published public var customData: [CustomDataStruct] = [] {
willSet {
// Custom state change handler.
self.customDataWillUpdate.send(newValue)
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
As long as
The self.objectWillChange.send() remains on the #Published properties you need on the subclass
The default PassthroughSubject declaration is not re-declared on the subclass
It will work and propagate the state change correctly.
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