I have a WKInterfaceController with a WKInterfaceTable that lists some events a user has recorded in my app.
A user can tap a row of the table to see more details about that event. To accomplish this, I've overridden contextForSegue(withIdentifier:in:rowIndex:) on the WKInterfaceController that contains the table, so tapping a row modally presents a detail view of that row in a new WKInterfaceController called EventDetailController.
The modal presentation is defined on the Storyboard. I can't use push presentation because the WKInterfaceController with the WKInterfaceTable is a page among multiple instances of WKInterfaceController at the top level of my app.
Here's the main issue:
Within the EventDetailController, there's a Delete button to destroy the record that the table row represents.
When a user taps the Delete button, I present an alert that allows the user to confirm or cancel the delete action.
Once the user confirms the record deletion, I want to dismiss the EventDetailController since it's no longer relevant, because it represents a deleted record.
Here's the IBAction defined on EventDetailController that gets called when the Delete button is tapped:
#IBAction func deleteButtonTapped(_ sender: WKInterfaceButton) {
let deleteAction = WKAlertAction(title: "Delete", style: .destructive) {
// delete the record
// as long as the delete was successful, dismiss the detail view
self.dismiss()
}
let cancelAction = WKAlertAction(title: "Cancel", style: .cancel) {
// do nothing
}
presentAlert(withTitle: "Delete Event",
message: "Are you sure you want to delete this event?",
preferredStyle: .alert,
actions: [deleteAction, cancelAction])
}
The problem is that watchOS doesn't seem to allow this. When testing this code, the EventDetailController does not dismiss. Instead, an error message is logged in the console:
[WKInterfaceController dismissController]:434: calling dismissController from a WKAlertAction's handler is not valid. Called on <Watch_Extension.EventDetailController: 0x7d1cdb90>. Ignoring
I've tried some weird workarounds to try to trick the EventDetailController into dismissing, like firing a notification when the event is deleted and dismissing the EventDetailController from a function that's called from an observer of the notification, but that doesn't work either.
At this point I'm thinking there's some correct way I'm supposed to be able to dismiss a WKInterfaceController, or in other words reverse the contextForSegue(withIdentifier:in:rowIndex:) call, but I don't know what it is.
When I call dismiss() directly in the IBAction, instead of in a WKAlertAction handler, it works fine, but I don't like this implementation since it doesn't allow the user to confirm the action first.
I feel like an idiot, but I figured out the solution.
The answer was in Apple's WKInterfaceController.dismiss() documentation the whole time (emphasis added):
Call this method when you want to dismiss an interface controller that you presented modally. Always call this method from your WatchKit extension’s main thread.
All I had to do differently was call self.dismiss() on the main thread.
Here's my updated code for the delete action, which now works as expected:
let deleteAction = WKAlertAction(title: "Delete", style: .destructive) {
// delete the record
// as long as the delete was successful, dismiss the detail view
DispatchQueue.main.async {
self.dismiss()
}
}
Hopefully this will save someone else some troubleshooting time!
Related
I've read about cancelling fetch requests by using AbortController.abort(). Is there a way to start a request again without aborting it after calling this command?
For example, in this demo from MDN, once Cancel download has been clicked, clicking Download video will trigger the fetch again, but immediately abort it.
Is there a way to allow this request again without aborting it? So, in this case, how could you click Download video to begin the download, click Cancel download to cancel the download, and then click Download video again to start the download again? For example, if the user clicked Cancel download on accident...
You can't.
An AbortController or its signal can not be reused nor reseted. If you need to "reset" it, you have to create a new AbortController instance and use that instead.
I think this is by design. Otherwise it could get messy e.g. if you hand over the controller or signal to some external library and suddenly they could remotely un-abort your internal state.
For example, in this demo from MDN, once Cancel download has been clicked, clicking Download video will trigger the fetch again, but immediately abort it.
They fixed the example. After you click Cancel download you will be able to start a new download and cancel it again, over and over. In order to achieve that the Download button instantiate a new AbortController every time, so you get a fresh signal to abort every time:
downloadBtn.addEventListener('click', fetchVideo);
function fetchVideo() {
controller = new AbortController();
signal = controller.signal;
// ...
So it's ok to instantiate new AbortControllers for each request that you may wish to cancel.
I know this might be kind of late, but I'm leaving this answer anyways in case someone needs it.
I don't know if this is the most optimal approach, but in order to keep doing fetch requests (with 'the same' signal) I had to create a new AbortController instance for each request.
In my case (all code being contained inside a class declaration), I deleted and created a new instance every time, like so:
class Foo Extends Bar {
abort_controller_instance = false;
constructor(params){
super(params);
this.resetOrStartAbortController();
}
resetOrStartAbortController(){
if(this.abort_controller_instance){
delete this.abort_controller_instance;
}
this.abort_controller_instance = new AbortController();
}
abortFetchRequest(){
if(this.abort_controller_instance){
this.abort_controller_instance.abort();
this.resetOrStartAbortController();
}
}
...
}
Probably it's not the most elegant solution, but it works.
Regards!
Basically need to build a warning modal , when user tries to move from current page/screen to another page , showing there are some saved changes .
Any implementations using redux and redux saga
Sagas are the lib for this - they watch for any action of a specified type. Navigation will take two actions: one to indicate that navigation is about to happen (which the saga will watch) and one to actually update the current page. The saga watches for actions of the first type and shows a warning dialog if the data has changed.
Ex:
function showWarning(action) {
if (/* data has been changed but not saved */) {
displayWarningDialog(action.pageToNavigateTo)
}
else {
// action that updates the page/location
completeNavigation(action.pageToNavigateTo)
}
}
function* mySaga() {
// NAVIGATE_TO_PAGE_X are the actions that get fired when a user changes pages
yield takeEvery("NAVIGATE_TO_PAGE_1", showWarning)
yield takeEvery("NAVIGATE_TO_PAGE_2", showWarning)
}
There is the amazing Redux DevTools for state debugging. This tool was built by Redux author himself.
Here are its features
Lets you inspect every state and action payload
Lets you go back in time by “cancelling” actions
If you change the reducer code, each “staged” action will be
re-evaluated
If the reducers throw, you will see during which action this
happened, and what the error was
With persistState() store enhancer, you can persist debug sessions
across page reloads
I've thought about this recently as well and been thinking about writing some form of middleware to intercept routing actions.
When intercepting a routing action, the middleware could determine if application state indicates the user is editing some unsaved data, and if so, dispatch a different action instead. That action should reduce state and cause a warning to render. The user could then confirm wanting to continue navigating by dispatching an action also intercepted by the middleware to continue the routing process.
So I have a table that shows the entries. Users click on a button to open a fragment page to edit the data.
app.datasources.SystemOrders.selectKey(widget.datasource.item._key);
app.showDialog(app.pageFragments.SystemOrders_Edit);
This part works fine.
I have changed my datasource to Manual Save Mode to be able to utilize the "email notification for changes" functions that are used in the Project Tracker sample. So that a user can make changes, hit a Save (Close) Button and an email goes out showing the changes.
The problem is that when the user closes the fragment, the table does not update (they have the same datasource). When I was in automatic save mode, I was able to utilize the following to force the table to reload so it reflected any changes:
var datasource = app.datasources.SystemOrders_HideComplete;
datasource.load();
app.closeDialog();
So I figured I just needed to add the widget.datasource.saveChanges(); option for the Close Button.
widget.datasource.saveChanges();
var datasource = app.datasources.SystemOrders_HideComplete;
datasource.load();
app.closeDialog();
Unfortunately, when I use the above I get the following error and the table seems like it gets stuck trying to reload (the spinner keeps spinning).
"Cannot query records on models with changes."
I'm assuming this is maybe because the datasource hasn't finished saving the new change, before trying to reload the datasouce?
How can I have the Save (Close) Button:
Save the changes
Close the dialog
Refresh the table so it reflects the changes?
Thank you for your help.
You can try to refresh datasource in save callback(assuming that you are actually sharing datasource between page fragment and page):
widget.datasource.saveChanges(function() {
widget.datasource.load();
app.closeDialog();
});
That error will happen if your datasource is trying to refresh while you have unsaved changes.
I know your code block calls a client-side saveChanges before loading the datasource again, but you may need to check to make sure that the datasource is not being reloaded elsewhere at the same time.
If that hasn't fixed it, try passing the values to a server-side script and save the changes after assigning the properties, like this:
Client:
function handleSuccess() {
// ensures that records are saved, now reload
app.models.YourModel.YourDatasource.load();
}
google.script.run.withSuccessHandler(handleSuccess).addItem(field_value1, field_value2, field_value3);
Server:
function addItem(field_value1, field_value2, field_value3) {
// create new items
var newItem = app.models.YourModel.newRecord();
// assign values
newItem.field1 = field_value1;
newItem.field2 = field_value2;
newItem.field3 = field_value3;
// save
app.saveRecords([newItem]);
}
I've found this method to be more reliable than manipulating changes from the client script.
I have a query that is observed on viewWillAppear on View Controller A
let query = FIRDatabase.database().reference().child("tags").
queryOrdered(byChild: "users/uid").queryEqual(toValue: userId)
In the same view controller, in viewDidDisappear I remove this observer.
So let's say I push into another view controller from View Controller A.
When I come back to View Controller A, the observer returns a snapshot, even though my data on the backend hasn't changed.
I only want a snapshot returning if there's been some actual change to my database. How do I solve this problem? Thanks.
One answer is to simply let the observer do it's job and update your data in the background.
So if the user is on controllerA the observer updates a datasource (an array) so then the UI is updated from that array.
When the user switches to controllerB, the observer can still update the array, but don't update the UI in controllerA since there's no need to.
When the user switches back to A you'll have current data available so just reload the tableView (assuming iOS here) from the array.
This solution reduces the 'polling' nature and let's Firebase do the heavy lifting to notify your app when it needs to. You're just reloading the tableViews from an array when that controller becomes active.
Edit
The idea here is to add the observer once - perhaps when the view loads the first time only (viewDidLoad) or maybe in your app delegate. Once you add the observer it will update your dataSource arrays when data changes so when you move from view to view the only action needed will be to reload the tableView from the updated array.
There are times when you may want to remove an observer but it doesn't sound like you need to do that from your question. So - attach the observers once and let them update the dataSource arrays as the data changes. Reload your tableViews when switching views.
You have put the query in viewWillAppear, which means every time you come to viewController A, this query will be executed irrespective of you have removed the observer or not.
Try putting the same query in viewDidLoad which means, the query will be called once and don't remove the observer anywhere. Now the query would be called only when data gets changed in firebase.
So I'm trying to build a tool that will allow me and other users to all open the same .swf, and then I as the Admin user am able to interact with mine while they all see my mouse movements and button clicks etc on theirs.
I'm using BlazeDS to manage this and I'm getting data sent back and forth etc - no difficulties there. The issue I'm running into is this:
In an "Admin" instance, I click a button. I capture that X and Y, then tell Blaze to tell my clients to dispatch a Click event at that X and Y. On my client side, I get that data and dispatch a Click event at that X and Y - but the click is actually caught at the stage level. The click on my client side takes place UNDER all of my buttons and other content - so the whole thing fails.
Does this make sense? Is there a way to tell it to start the click event at the top?
If you are unable to architect the loaded swf's to use a better architecture you could try something a little more hackish to get buttons working.
Have a look at the methods getObjectsUnderPoint and areInaccessibleObjectsUnderPoint of the DisplayObjectContainer. Combined with hasEventListener you should be able to emulate what you want.
Here is some untested pseudo-code:
function detectClick(pt:Point):void
{
var objsUnderPoint:Array = containerStage.getObjectsUnderPoint(pt);
var clickable:Array = [];
for each(dispObj:DisplayObject in objsUnderPoint)
{
if(dispObj.hasEventListener(MouseEvent.CLICK))
{
clickable.push(dispObj);
}
}
if(clickable.length)
{
// sort on depth here
// that might be tricky since you'll be looking at grandchildren
// and not just children but it is doable.
var topMostClickable:DisplayObject = ???
topMostClickable.dispatchEvent(new MouseEvent(MouseEvent.CLICK, true, false));
}
}
areInaccessibleObjectsUnderPoint is important if you think their might be security restrictions (e.g. cross-domain issues) so you can debug if things go wrong.
Also note that you may want to (or need to) fill in more details of the MouseEvent (like the proper target, localX, localyY etc.)
Sounds like you need to set focus to the top most component of the x and y positions before dispatching the click event.
That said, I wonder what the use case is for something like this; as opposed to using a screen sharing tool such as Connect.
This seems like a poor way to implement what you are trying to do. If you "click" in the admin tool you are probably actually triggering some event. Why not trigger that event instead of sending the mouse click?
I'd just keep a map of actions and then when something happens in the Admin interface send the key to the action.
e.g.
In the admin:
myButton.addEventListener(MouseEvent.CLICK, handleClickButton);
function handleClickButton(event:MouseEvent):void
{
doSomeAction();
sendTriggerToClient(MyActions.SOME_TRIGGER);
}
In the client:
var actionMap:Object = {};
actionMap[MyActions.SOME_TRIGGER] = doSomeAction;
function receiveTriggerFromAdmin(trigger:String):void
{
var responseFunc:Function = actionMap[trigger];
responseFunc();
}
I hope that pseudo-code makes sense. Just abstract what happens as a result of a click into a separate function (doSomeAction) and then have the admin send a message before calling that function. In the client wait for any trigger that comes through and map it to the same function.