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

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.

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?

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)

Detect UI changes to #Published property

I have a simple SwiftUI App that fetches App Settings using a REST API call and makes it available to the UI using an #ObservedObject with a #Published property. I have several Toggle views that bind to the #Published property.
Each time a Toggle is "toggled", I need to upload the change to the server. So I added a didSet observer to the #Published property and although this works, the didSet property observer also gets fired when I initially fetch the settings from the server.
How can I avoid firing the didSet observer when initially setting the #Published property with settings fetched from the server?
The behaviour you are describing is correct. When you load the setting with your REST API and then set it to the #Published the value gets set, and your function is getting called.
You can add a variable to your class, which stores if the fetch is completed. When you fetched all your results, you set it to true and only then allow didSet function to be executed.
//Set your fetched data previous
var fetchCompleted : Bool = false
//after your api call and fetch result success
fetchCompleted = true
.didSet()
{
if (fetchCompleted)
{
//allow override from variables and store new setting
}
}
Make sure you set your variables with the fetched data first, and then set fetchCompleted to true. Otherwise didSet will called after and fetchCompleted would already be true.
It's difficult without code shown, but you could add an if-condition in your didSet which prevents it from executing your upload, when the Published value is "nil" (initial).

Swift - Unit testing functions that involve IBOutlets?

I am setting up unit testing for my Swift project and am having trouble testing a class function that involves updating IBOutlets.
I have a function, validateUrl, which expects a string to be passed, then validates it. If it is valid, it enables a UIButton, if it is invalid, it disables a UIButton. When I run a test that calls this function, the app crashes on the line of code that enables or disables the UIButton.
The storyboard and controllers both has the proper Test target set.
This line of code:
self.submitButton.enabled = true// Enable Submit Button
Spits out this error:
fatal error: unexpectedly found nil while unwrapping an Optional value
Try this code to initialize the IbOutlets of your view controller:
let yourStoryboard = UIStoryboard(name: "Your_storyboard", bundle: nil)
yourViewController = yourStoryboard.instantiateViewController(withIdentifier: "YourViewController") as! YourViewController
yourViewController.loadView() // This line is the key
You have to initiate the view controller using the storyboard. See the documentation here: https://developer.apple.com/library/ios/documentation/uikit/reference/UIStoryboard_Class/index.html#//apple_ref/occ/instm/UIStoryboard/instantiateViewControllerWithIdentifier:
If you initialize the view controller directly, it will not have any connections because the VC itself does not know of the storyboard in this case.
You may need to add the controllers view to a hierarchy prior to testing to force the controller to load the XIB
let localContainer = UIView(frame:someFrame)
let controllerUnderTest = //instantiate your controller
localContainer.addSubview(controllerUnderTest.view)
//at this point you can test outlets
Otherwise your outlets will be nil as they haven't been connected yet.
Testing IBOutlet's is not the best approach, because:
outlets should be considered implementation details, and should be declared as private
outlets are most of the time connected to pure UI components, and unit tests deal with testing the business logic
testing the value injected into the outlet by another function can be considered somewhat as integration testing. You'd also double the unit tests you have to write by having to test the connected/unconnected outlet scenarios.
In your particular case, I'd recommend instead to test the validator function, but first making it independent of the controller class (if it's not already). Having that function as an input->output one also bring other benefits, like increased reusability.
Once you have tested all the possible scenarios for the validator, validating that the outlet correctly behaves it's just a matter of a quick manual testing: just check if the outlet behaves like the validator returned. UI stuff are better candidates for manual testing, as manual testing can catch other details too (like positioning, colors, etc).
However, if you really want to test the outlet behaviour, one technique that falls into the unit testing philosophy is snapshot testing. There are some libraries available for this, I'd recommend the one from https://github.com/uber/ios-snapshot-test-case/.
A solution I'm using to test classes in Swift is to create the outlets in the setUp() method of the test. For example, I have a UIViewController subclass that has a UIImageView as an outlet. I declare an instance of my View controller subclass a property of my test class, & configure it's outlet in the setUp() method by initializing a new UIImageView.
var viewController = ViewController() // property on the test class
override func setUp() {
super.setUp()
viewController.imageView = UIImageView(image: UIImage(named: "Logo")) // create a new UIImageView and set it as the outlet object
}
Any other outlets can similarly be set in the setUp method, and this way you don't even have to instantiate a storyboard (which, for some reason, despite the fact that I was able to instantiate a UIStoryboard object in my tests, the view controller outlets were still nil).
#talzag In your case iboutlets are nil because they are weak variables, They are supposed to deallocate immediately after instantiation.