How to publish change from async function - swiftui

I have class conforms to ObservableObject with
#Published var fileContent = ""
defined. Further I have getFileContent() async function returning String.
If I call function like this
Task {
fileContent = await getFileContent(forMeasurementID: id, inContext: context)
}
code is compiled and app works fine but XCode is complaining "purple" error "Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.". I've tried to elaborate with receive(on:) but no succeess so far.
I will appreciate any hint. Thanks.

Well, in async/await world it is better to use MainActor, e.g.
Task {
let content = await getFileContent(forMeasurementID: id, inContext: context)
await MainActor.run {
// fileContent will be updated on the Main Thread
fileContent = content
}
}

You cannot change the state of your app (change a #Published var) unless you are in the main thread.
Here is how your code works:
Task {
let content = await getFileContent(forMeasurementID: id, inContext: context)
DispatchQueue.main.async {
fileContent = content
}
}

Related

Swiftui FileManager/URLSession not writing to documentDirectory when running as background task

Hope you're doing well!
I've built an app that generates a view from a .csv file that I have hosted on my website. I've previously managed to get everything working as expected where I called the csv from the website and wrote the contents directly to a variable and then processed it from there. Obviously this wasn't good practice as the app started mis-behaving when the internet couldn't be accessed (despite writing in connectivity checks).
I've now built out the app to call the URL, save the csv with Filemanager, then when the app refreshes, it will use FileManager.default.replaceItemAt to replace the previous version if there is internet connectivity, if not the app builds from the previously stored .csv
This all works fine when the app is running, however I'm running into issues with the background processing task. It seems the app doesn't have permissions to write with FileManager when it is executed from the background task. Is there an additional step I'm missing when using this in background tasks? I've attempted to use FileManager.default.removeItem followed by FileManager.default.copyItem instead of replaceItemAt but it doesn't seem to make a difference as expected.
UPDATE 22/06 - Still scouring the internet for similar issues or examples I think I might be going down the wrong rabbit hole here. This could be issues with the way the new background task has been configured for retrieving data from my website, although the background tasks worked fine before there seems to be a bit more legwork needed for this method to work as a background task.
func handleAppRefresh(task: BGProcessingTask) {
//Schedules another refresh
scheduleAppRefresh()
DispatchQueue.global(qos: .background).async {
pullData()
print("BG Background Task fired")
}
pullData() will call loadCSV() and then do some data processing. At the moment I'm just using a print straight after loadCSV() is called to validate if the downloads etc are successful.
// Function to pass the string above into variables set in the csvevent struct
func loadCSV(from csvName: String) -> [CSVEvent] {
var csvToStruct = [CSVEvent]()
// Creates destination filepath & filename
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
//Create URL to the source file to be downloaded
let fileURL = URL(string: "https://example.com/testcsv.csv")!
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("File downloaded Successfully. Response: \(statusCode)")
}
do {
let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: tempLocalUrl)
} catch (let writeError) {
print("Error creating a file \(destinationFileUrl) : \(writeError)")
}
} else {
print("Error" )
}
}
task.resume()
let data = readCSV(inputFile: "testcsv.csv")
var rows = data.components(separatedBy: "\n")
rows.removeFirst()
// Iterates through each row and sets values
for row in rows {
let csvColumns = row.components(separatedBy: ",")
let csveventStruct = CSVEvent.init(raw: csvColumns)
csvToStruct.append(csveventStruct)
}
print("LoadCSV has run and created testcsv.csv")
return csvToStruct
}
Any help or pointers to why these files aren't being updated in background tasks but are working fine in app would be massively appreciated!
Thanks in advance.
EDIT: adding new BGProcessingTask
func handleAppRefresh(task: BGProcessingTask) {
//Schedules another refresh
print("BG Background Task fired")
scheduleAppRefresh()
Task.detached {
do {
let events = try await loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
print(events)
pullData(events: events)
} catch {
print(error)
}
}
}
The problem is not the background task per se, the problem is the asynchronous behavior of downloadTask. readCSV is executed before the data is downloaded.
In Swift 5.5 and later async/await provides asynchronous behavior but the code can be written continuously.
func loadCSV(from csvName: String) async throws -> [CSVEvent] {
var csvToStruct = [CSVEvent]()
// Creates destination filepath & filename
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
//Create URL to the source file to be downloaded
let fileURL = URL(string: "https://example.com/testcsv.csv")!
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL)
let (url, response) = try await session.download(for: request)
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("File downloaded Successfully. Response: \(statusCode)")
}
let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: url)
let data = readCSV(inputFile: "testcsv.csv")
var rows = data.components(separatedBy: "\n")
rows.removeFirst()
// Iterates through each row and sets values
for row in rows {
let csvColumns = row.components(separatedBy: ",")
let csveventStruct = CSVEvent.init(raw: csvColumns)
csvToStruct.append(csveventStruct)
}
print("LoadCSV has run and created testcsv.csv")
return csvToStruct
}
To call the function you have to wrap it in a detached Task which replaces the GCD queue
Task.detached {
do {
let events = try await loadCSV(csvName: "Foo")
print("BG Background Task fired")
} catch {
print(error)
}
}

Dart Testing with Riverpod StateNotifierProvider and AsyncValue as state

This is my first app with Dart/Flutter/Riverpod, so any advice or comment about the code is welcome.
I'm using Hive as embedded db so the initial value for the provider's state is loaded asynchronously and using an AsyncValue of riverpod to wrapped it.
The following code works but I've got some doubts about the testing approach, so I would like to confirm if I'm using the Riverpod lib as It supposed to be used.
This is my provider with its deps (Preferences is a HiveObject to store app general config data):
final hiveProvider = FutureProvider<HiveInterface>((ref) async {
return await App.setUp();
});
final prefBoxProvider = FutureProvider<Box<Preferences>>((ref) async {
final HiveInterface hive = await ref.read(hiveProvider.future);
return hive.openBox<Preferences>("preferences");
});
class PreferencesNotifier extends StateNotifier<AsyncValue<Preferences>> {
late Box<Preferences> prefBox;
PreferencesNotifier(Future<Box<Preferences>> prefBoxFuture): super(const AsyncValue.loading()) {
prefBoxFuture.then((value) {
prefBox = value;
_loadCurrentPreferences();
});
}
void _loadCurrentPreferences() {
Preferences pref = prefBox.get(0) ?? Preferences();
state = AsyncValue.data(pref);
}
Future<void> save(Preferences prefs) async {
await prefBox.put(0, prefs);
state = AsyncValue.data(prefs);
}
Preferences? get preferences {
return state.when(data: (value) => value,
error: (_, __) => null,
loading: () => null);
}
}
final preferencesProvider = StateNotifierProvider<PreferencesNotifier, AsyncValue<Preferences>>((ref) {
return PreferencesNotifier(ref.read(prefBoxProvider.future));
});
And the following is the test case, I'm mocking the Hive box provider (prefBoxProvider):
class Listener extends Mock {
void call(dynamic previous, dynamic value);
}
Future<Box<Preferences>> prefBoxTesting() async {
final hive = await App.setUp();
Box<Preferences> box = await hive.openBox<Preferences>("testing_preferences");
await box.clear();
return box;
}
void main() {
test('Preferences value changes', () async {
final container = ProviderContainer(overrides: [
prefBoxProvider.overrideWithValue(AsyncValue.data(await prefBoxTesting()))
],);
addTearDown(() {
container.dispose();
Hive.deleteBoxFromDisk("testing_preferences");
});
final listener = Listener();
container.listen<AsyncValue<Preferences>>(
preferencesProvider,
listener,
fireImmediately: true,
);
verify(listener(null, const TypeMatcher<AsyncLoading>())).called(1);
verifyNoMoreInteractions(listener);
// Next line waits until we have a value for preferences attribute
await container.read(preferencesProvider.notifier).stream.first;
verify(listener(const TypeMatcher<AsyncLoading>(), const TypeMatcher<AsyncData>())).called(1);
Preferences preferences = Preferences.from(container.read(preferencesProvider.notifier).preferences!);
preferences.currentListName = 'Lista1';
await container.read(preferencesProvider.notifier).save(preferences);
verify(listener(const TypeMatcher<AsyncData>(), const TypeMatcher<AsyncData>())).called(1);
verifyNoMoreInteractions(listener);
final name = container.read(preferencesProvider.notifier).preferences!.currentListName;
expect(name, equals('Lista1'));
});
}
I've used as reference the official docs about testing Riverpod and the GitHub issue related with AsyncValues
Well, I found some problems to verify that the listener is called with the proper values, I used the TypeMatcher just to verify that the state instance has got the proper type and I check ("manually") the value of the wrapped object's attribute if It's the expected one. Is there a better way to achieve this ?
Finally, I didn't find too many examples with StateNotifier and AsyncValue as state type, Is there a better approach to implement providers that are initialized with deferred data ?
I didn't like too much my original approach so I created my own Matcher to compare wrapped values in AsyncValue instances:
class IsWrappedValueEquals extends Matcher {
final dynamic value;
IsWrappedValueEquals(this.value);
#override
bool matches(covariant AsyncValue actual, Map<dynamic, dynamic> matchState) =>
equals(actual.value).matches(value, matchState);
#override
Description describe(Description description) => description.add('Is wrapped value equals');
}
In the test, the final part is a bit different:
Preferences preferences = Preferences.from(container.read(preferencesProvider.notifier).preferences!);
preferences.currentListName = 'Lista1';
await container.read(preferencesProvider.notifier).save(preferences);
// the following line is the new one
verify(listener(IsWrappedValueEquals(Preferences()), IsWrappedValueEquals(preferences))).called(1);
verifyNoMoreInteractions(listener);
}
I prefer my custom Matcher to the original code, but I feel that there are too many custom code to test something, apparently, common.
If anyone can tell me a better solution for this case, It'd be great.

Is there a solution to asynchronously wait for C++ data on Flutter's invokeMethod?

Currently, this is how I read from C++ using Flutter:
final Uint8List result = await platform.invokeMethod(Common.MESSAGE_METHOD, {"message": buffer});
It is handled by Kotlin like this:
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == MESSAGE_METHOD) {
val message: ByteArray? = call.argument<ByteArray>("message")
//... //response = Read something FROM C++
result.success(response)
Since this happens in the main thread, if I take too much time to answer, I make Flutter's UI slow.
Is there a solution to get C++ data in an async way?
I know that Flutter has support for event channels to send data back from C++ to Flutter. But what about just requesting the data on the Flutter side and waiting for it to arrive in a Future, so I can have lots of widgets inside a FutureBuilder that resolves to something when ready?
If reading something from C++ is a heavy process, You can use AsysncTask to perform it in the background for android.
internal class HeavyMsgReader(var result: MethodChannel.Result) : AsyncTask<ByteArray?, Void?, String?>() {
override fun doInBackground(vararg message: ByteArray?): String {
//... //response = Read something FROM C++
return "response"
}
override fun onPostExecute(response: String?) {
result.success(response)
}
}
Calling async task:
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == MESSAGE_METHOD) {
val message: ByteArray? = call.argument<ByteArray>("message")
HeavyMsgReader(result).execute(message);
Hopefully this will work
import 'dart:async';
Future<Uint8List> fetchData(buffer) async {
final Uint8List result = await platform.invokeMethod(Common.MESSAGE_METHOD, {"message": buffer});
return result;
}
And just call it, like this
fetchData(buffer).then((result) => {
print(result)
}).catchError(print);
Proof that its working:
import 'dart:async';
Future<String> fetchUserOrder() async {
await Future.delayed(Duration(seconds: 5));
return 'Callback!';
}
Future<void> main() async {
fetchUserOrder().then((result) => {
print(result)
}).catchError(print);
while(true){
print('main_thread_running');
await Future.delayed(Duration(seconds: 1));
}
}
output:
main_thread_running
main_thread_running
main_thread_running
main_thread_running
main_thread_running
Callback!
main_thread_running
main_thread_running
...

How can I change the filename in a SwiftUI Document-based app?

I've been using the new template for a document-based SwiftUI app. While you get a lot of file-management "for free" in the new template, as it stands in the iOS version users have to back out of the file to the file browser to change the filename. I want to create an opportunity for the user to rename the file while it is open.
Here's a sample project focused on the issue: https://github.com/stevepvc/DocumentRenamer
In the code, I've added to the template code a simple UI with a textfield for the user to enter a new name. When the user hits the "rename" button, the app checks to see if the URL with that name component is available, appending a suffix if necessary to create a target url.
func getTargetURL() -> URL {
let baseURL = self.fileurl.deletingLastPathComponent()
print("filename: \(self.filename)")
print("fileURL: \(self.fileurl)")
print("BaseURL: \(baseURL)")
var target = URL(fileURLWithPath: baseURL.path + "/\(filename).exampletext")
var nameSuffix = 1
while (target as NSURL).checkPromisedItemIsReachableAndReturnError(nil) {
target = URL(fileURLWithPath: baseURL.path + "/\(filename)-\(nameSuffix).sermon")
print("Checking: \(target)")
nameSuffix += 1
}
print("Available Target: \(target)")
return target
}
It then attempts to rename the file, and this is when I am stuck. I have tried several methods, most recently the following:
func changeFilename(){
let target = getTargetURL()
var rv = URLResourceValues()
let newFileName = target.deletingPathExtension().lastPathComponent
rv.name = newFileName
do {
if fileurl.startAccessingSecurityScopedResource(){
try fileurl.setResourceValues(rv)
fileurl.stopAccessingSecurityScopedResource()
}
} catch {
print("Error:\(error)")
}
}
But I keep getting the following error:
Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file “Untitled” in the folder “DocumentRenamer”."
I have also tried this without the startAccessingSecurityScopedResource() check, and alternatively have tried creating a helper class as follows:
class FileMover: NSObject {
func moveFile(originalURL: URL, updatedURL:URL) -> Bool {
let coordinator = NSFileCoordinator(filePresenter: nil)
var writingError: NSError? = nil
var success : Bool = true
print("moving file")
coordinator.coordinate(writingItemAt: originalURL, options: NSFileCoordinator.WritingOptions.forMoving, error: &writingError, byAccessor: { (coordinatedURL) in
do {
try FileManager.default.moveItem(at: coordinatedURL, to: updatedURL)
success = true
print("file moved")
} catch {
print(error)
success = false
}
})
return success
}
}
But using this method locks up the app entirely. It's possible that there is something about iCloud permissions going on there, but I think I've have those set up appropriately.
It appears to work fine in the simulator, but not when run on a device.
What is the correct method for renaming a file in the app's container?

StateHasChanged() does not reload page

Issue:
As mentioned in Title, StateHasChanged does not re-render the page
Objective:
I want to Refresh the page when a button is clicked
Current Code
<button #onclick="CreatePlayer">Create User</button>
#functions {
string username;
[CascadingParameter]
Task<AuthenticationState> authenticationStateTask { get; set; }
async Task CreatePlayer()
{
var authState = await authenticationStateTask;
var user = authState.User;
var player = await PlayerData.GetByEmail(user.Identity.Name);
if (player == null)
{
player = new Player()
{
Email = user.Identity.Name,
UserName = username
};
await PlayerData.Create(player);
}
await Task.Delay(50);
StateHasChanged();
}
}
Just for the record, I add my comment in an answer :
StateHasChanged just inform the component that something changes in is state, that doesn't rerender it. The component choose by itself if it has to rerender or not. You can override ShouldRender to force the component to rerender on state changed.
#code {
bool _forceRerender;
async Task CreatePlayer()
{
var authState = await authenticationStateTask;
var user = authState.User;
var player = await PlayerData.GetByEmail(user.Identity.Name);
if (player == null)
{
player = new Player()
{
Email = user.Identity.Name,
UserName = username
};
await PlayerData.Create(player);
}
_forceRerender = true;
StateHasChanged();
}
protected override bool ShouldRender()
{
if (_forceRerender)
{
_forceRerender = false;
return true;
}
return base.ShouldRender();
}
}
On the one hand, you tell the compiler that she should create an event handler for the click event, named CreatePlayer: #onclick="CreatePlayer . This attribute compiler directive, behind the scenes, creates an EventCallback<Task> handler for you, the implication of which is that you do not need to use StateHasChanged in your code at all, as this method ( StateHasChanged ) is automatically called after UI events take place.
On the other hand, you tell the compiler that the type of the button should be set to "submit". This is wrong of course... You can't have it both. Setting the type attribute to "submit", normally submit form data to the server, but In Blazor it is prevented to work that way by code in the JavaScript portion of Blazor. Do you want to submit a form data to the server ? Always recall Blazor is an SPA Application. No submit ?
Your code should be:
<button #onclick="CreatePlayer" >Create User</button>
Just for the records, ordinarily you should inject the AuthenticationStateProvider object into your components, like this:
#inject AuthenticationStateProvider AuthenticationStateProvider
and then retrieve the AuthenticationState object. This is how your code may be rewritten:
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;