How to protect against missing EnvironmentObject [duplicate] - swiftui

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 22 hours ago.
Improve this question
Consider the following code, which lists fruits. Is #EnvironmentObject the de-facto and recommended way of handling this scenario?
Is there a way to protect against the fact that you may get a runtime crash here, if the environment object hasn't been injected in?
Would you consider #EnvironmentObject to essentially be a singleton (if set at a high enough level in the view hierarchy) and therefore inherently bad (mutable state everywhere being 'bad').
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
NavigationStack {
ForEach($merchant.fruits, id: \.id) { $fruit in
NavigationLink(destination: FruitDetailView(fruit: $fruit)) {
Text(fruit.name)
}
}
}
}
}

Related

Modularized App and Global State (Android Compose)

I am following the Now In Android sample for modularization and best practices with Jetpack Compose and I am a bit stuck with sharing a simple LocalDate state across several modules/features.
I want to be able to change a selected date in several places, and update the UI accordingly in several places (i.e. AppState, BottomSheet (BottomSheetState), Screen A (Viewmodel) / Screen B (Viewmodel), Dialog (if opened), etc.).
Now considering concepts like Ownership and Encapsulation, I am not sure which would be the preferred way to handle this. Or how I should rethink the logic.
So far I tried:
Hoisting the state in AppState and passing it onto Composables below;
Problem here was that I cannot pass the stateflow on to the VMs due to DI with hiltViewModel() and Compose Navigation. (Example of implementation:)
#OptIn(ExperimentalLifecycleComposeApi::class)
#Composable
internal fun ScreenARoute(viewModel: ScreenAViewModel = hiltViewModel()) {
ScreenA(/***/)
}
#HiltViewModel
class ScreenAViewModel #Inject constructor(
private val userPrefsRepository: UserPrefsRepository,
) : ViewModel() {
/***/
}
Using SavedStateHolder in ViewModels, including the MainActivityViewmodel where the date should be initialized (today);
Problem here was that the state was only updated in the respective VM/SavedStateHolder and not across all places. For example I was able to update the date and state in Screen A and the UI got updated accordingly, other places/screens remained without updates though. (Example of implementation:)
#HiltViewModel
class ScreenAViewModel #Inject constructor(
private val userPrefsRepository: UserPrefsRepository,
val savedStateHandle: SavedStateHandle,
) : ViewModel() {
/***/
val selectedDate: StateFlow<LocalDate> =
savedStateHandle.getStateFlow<LocalDate>(SELECTED_DATE_SAVED_STATE_KEY, LocalDate.now())
.stateIn(
viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = LocalDate.now()
)
fun updateSelectedDate(date: LocalDate) {
savedStateHandle[SELECTED_DATE_SAVED_STATE_KEY] = newDate
/***/
}
Other things I am considering:
Creating a Singleton object that holds the state, which is initialized in the MainActivityViewModel and implemented in the VMs via hilt DI.
Using Room or Preferences to save the state and share across the app per Singleton Repository and DI. But this might be overkill?
The state should survive configuration changes but can be disposed when the app gets killed. Any help will be much appreciated.

Quickest way to dirty a FileReferenceDocument in SwiftUI for macOS

I have a number of changes to a document-based application which are not undoable (conceptually, think quiz progress) but which should dirty the document so the state is saved when the user quits or closes the document.
Apple's documentation for ReferenceFileDocument states Changes to the document are observed and on change dirty the document state for resaving. but that does not happen for me. (I have tried both changes to a DocumentData class held by my document and to a variable directly within my ReferenceFileDocument subclass, but nothing happens; the document stays clean and does not autosave.)
NSDocument has an updateChangeCount function which is all I want at this point.
All references to dirtying documents I have found involve an undo manager, so in my document class I have implemented
var isDirty: Bool = false
func dirtyDocument(undoManager: UndoManager?){
undoManager?.registerUndo(withTarget: self) { isDirty in
undoManager?.removeAllActions(withTarget: self)
}
if isDirty == false {
isDirty = true
}
}
and any View that makes a noteworthy change contains
#Environment(\.undoManager) var undoManager
and calls document.dirtyDocument(undoManager: undoManager)
This works in that it dirties the document, but leaves me with an enabled 'Undo' action which does nothing BUT which marks the document as 'not dirty' if the user invokes it, which is why I added undoManager?.removeAllActions(withTarget: self)
I now have the functionality I want, apart from not finding a way to disable the undo menu item for this 'undo action' but it still feels hackish. Is there a better way to emulate updateChangeCount without invoking the UndoManager and and immediately discarding its changes?

How do I keep data persistent while the App is running?

Scenario: I retrieve data from a server via #ObservableObject/#Publish; within a tabview().
Data is displayed as expected.
However when I return to the tabView from another tab, I data is gone, requiring me to do another fetch which isn't needed.
Here's the subscriber:
struct NYTStatesView: View {
#ObservedObject var dataSource = NYTStatesModel()
...
}
Here's the publisher:
final class NYTStatesModel: ObservableObject {
#Published var revisedNYTStates: RevisedNYTStates!
// ...
}
dataSource.revisedNYTStates is empty upon return to the View.
Question: How do I RETAIN the data so I don't have to always access the server per View display?
When you're switching tabs, dataSource is recreated:
struct NYTStatesView: View {
#ObservedObject var dataSource = NYTStatesModel()
...
}
A solution may be to create dataSource outside the TabView.
For example on the app level:
#main
struct TestApp: App {
#StateObject private var dataSource = NYTStatesModel()
...
}
(or in the SceneDelegate for SwiftUI 1.0)
This is something that I struggled with for a while too. There are a couple of different ways to do what you want.
The first is creating a loader class from somewhere higher up in the view hierarchy and passing it down to where it is needed. For example, you might create the loader in the the app main struct and passing it in as an environment object. IMO, this method is the least scalable and the messiest.
The second option is to either use a singleton or a static property to persist your data. For example you could make your NYTStatesModel a singleton. (I don't condone this type of behavior)
The third (and best IMO) is creating a class that is responsible for caching your results and passing that into the view model from the environment. NSCache will actually dump results when your phone is running low on memory. Here's two articles that will help. The first is how to create a cache and the second is how to create an environment key.
https://www.swiftbysundell.com/articles/caching-in-swift/
https://swiftwithmajid.com/2019/08/21/the-power-of-environment-in-swiftui/
Just use dependency injection to check the cache before the network call. The cache is persisted in the environment.

Cannot use Scene methods for URL, NSUserActivity, and other External Events without using SwiftUI Lifecycle

I am using the new "pure" SwiftUI App and Scene structs instead of an AppDelegate.
Some of my views accept custom url schemes and user activities using onOpenURL(perform:). Everything works as expected.
However, starting with Beta 6, Xcodes gives the following runtime warning:
runtime: SwiftUI: Cannot use Scene methods for URL, NSUserActivity, and other External Events without using SwiftUI Lifecycle. Without SwiftUI Lifecycle, advertising and handling External Events wastes resources, and will have unpredictable results.
What exactly am I doing wrong? What is SwiftUI Lifecycle referring to?
This is what my main App struct looks like.
I am attaching some default modifiers to the main view.
#main
struct MyApp: App {
#StateObject var viewModel = GlobalViewModel()
var body: some Scene {
WindowGroup {
MainView()
.applyingDefaultColors()
.environmentObject(viewModel)
.environmentObject(TranslationProvider())
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
}
SwiftUI Lifecycle refers to views that are rendered as children of a Scene, namely the WindowGroup struct that is normally returned in the body of the App.
The implication is basically that if you attach "auxiliary" views to the Scene, such as Commands for MacOS, they can't use methods like .onContinuedUserActivity and will produce this error. Communicating view state changes in these cases should instead use FocusedValues, where the subscribing view has a #FocusedBinding or #FocusedValue, and the publishing view injects .focusedValue(\.someKeyPath, $interestingState)

Unit Testing - How to structure Firebase mocks? [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 4 years ago.
Improve this question
I'm unit testing my application using the Mockito framework.
TL;DR
Should I create a mock for every possible result of a class and set their behaviours in the #Before or should I create just one mock for every class and then define their behaviours in each test that uses them?
Context
Right now I'm testing the DataRepository, that has getPosts(String postId). DataRepository receives a FirebaseFirestore instance via DI.
getPosts returns a Maybe<List<Post>> with the following code:
fun getPosts(companyId: String): Maybe<List<Post>> {
return Maybe.create { emitter ->
firestore.collection("companies/$companyId/posts").get()
.addOnCompleteListener {
if (it.isSuccessful) {
if (it.result.isEmpty) {
emitter.onComplete()
} else {
emitter.onSuccess(it.result.toObjects(Post::class.java))
}
} else {
emitter.onError(it.exception ?: UnknownError())
}
}
}
}
Problem
To test this function and the various cases (success/empty/failed) I have to mock FirebaseFirestore, then the CollectionReference, then the Task, then the OnCompleteListener (with an argument captor) and so on. Basically everything.
Question
When I mock the task should I
a. Create a successfulTaskMock that I set in the #Before to always be successful and one for the failure? Same thing with a valid/empty validQuerySnapshot/emptyQuerySnapshot?
b. Create only a taskMock and then change its return value directly inside the test? (Make taskMock.isSuccessful return true in the success test and false in the failure test)
I've looked at some GitHub repos: some do a., some do b.
Problem with a. is I would end up with tons of mocks for the same class, but with very clean and readable tests.
Problem with b. is I should setup each mock at the start of every test, thus having a lot of boilerplate code.
Conclusion
I've read that I could use deep mocking to only create the end result of a call (the QuerySnapshot, as opposed to Firestore + Collection + Task etc.) but that seems to be very frowned upon and I can see why.
Maybe there is even a third way that I don't know of. Any suggestion is greatly appreciated.
Thank you very much.