The following code shows an iOS 13 SwiftUI Toggle example. It runs on a device (iPhone XR), but shows an error in the log when the toggle is tapped. I only observe this on a device, not the live preview.
import SwiftUI
struct ContentView: View {
#State private var foo = false
var body: some View {
Form{
Toggle(isOn: $foo, label: {
Text("Label")
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The error is:
2019-09-23 12:59:01.468146-0500 Demo[640:40285] invalid mode 'kCFRunLoopCommonModes' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution.
Any advice is appreciated.
This error also occurs for me with a toggle on the iPhone 8 simulator.
No crash experienced.
The cells having the toggles would not properly resize when moved, but this was avoided by adding a Bool and toggling it as part of the cell move function { eg, triggerRefresh.toggle() } This unexpected user behavior may or may not be related to the toggle error.
Same as C K,
No crash experienced.
I got the value of the switch using a separate Bool var
var stateOfSwitch = false
And used a separate #IBAction for the switch like so..
#IBAction func toggled(_ sender: UISwitch) {
stateOfSwitch = ! stateOfSwitch
}
Remember to link the IBAction to the switch element in your interface builder.
And use the stateOfSwitch variable to implement any logic you have to implement based on the switch.
I am aware this is a work around, hoping this won't be needed soon.
Not sure how to do this in SwiftUI.
Related
I'm using Google Firebase's Cloud Firestore service with a SwiftUI project that details strategies in a video game on a per-map basis. I'm using a View, ViewModel, Model architecture. It might also be worth noting that I'm using the Swift Package Manager to import the APIs rather than CocoaPods.
I have a ScrollView with a few maps fetched from a Firestore Collection. Tapping on the map will present the user with another ScrollView listing the strategies fetched from a separate collection in Firestore. The issue I'm experiencing is that, whenever a UserDefault is changed (either using UserDefaults.standard.setValue(...) or #AppStorage("settingName") var...) the contents of the ScrollView disappear. There's no relationship between the Firestore data and UserDefaults (nothing being saved from one to the other).
This only happens to the detail view, not the main view, despite the code being copied and pasted from one ViewModel swift file to the other.
Here's the StratsViewModel.swift code:
class StratsViewModel: ObservableObject {
#Published var strats = [Strat]()
private var db = Firestore.firestore()
func fetchData(forMap: String) {
if strats.isEmpty {
db.collection("maps/\(forMap)/strats").getDocuments { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
return
}
self.strats = documents.map { (queryDocumentSnapshot) -> Strat in
let data = queryDocumentSnapshot.data()
let id = queryDocumentSnapshot.documentID
let name = data["name"] as? String ?? ""
let map = data["map"] as? String ?? ""
let type = data["type"] as? String ?? ""
let side = data["side"] as? String ?? ""
let strat = Strat(id: id, name: name, map: map, type: type, side: side)
return strat
}
}
}
}
}
In the MapsDetailView.swift (presented by MapsView.swift) ScrollView, the strategies are displayed like so:
struct MapsDetailView: View {
var map: Map
#ObservedObject private var viewModel = StratsViewModel()
var body: some View {
ScrollView {
ForEach(viewModel.strats, id: \.self) { strat in
NavigationLink(destination: StratView()) {
StratCell(strat: strat)
}
}
}
.onAppear() {
self.viewModel.fetchData(forMap: map.id)
}
}
}
If I were to add a button to the view, for example, that set any value to any key in UserDefaults, the dozens of items in the ScrollView disappear.
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
UserDefaults.standard.setValue("test", forKey: "testKey")
} label: {
Text("Don't press me!")
}
}
}
Using the TabBar to navigate to another view, and then back to the MapDetailView will trigger the .onAppear method however, adding a breakpoint to the ForEach line shown above, reveals that this loop doesn't run again until the view is entirely dismissed and reopened.
The issue also crops up when switching tabs in the TabBar as, onAppear, each view sets a tabSelection key to remember which tab the user last selected when the app is killed. Switching from one tab, back to the MapsDetailView tab will remove all cells in the ScrollView.
Any thoughts? Happy to share more source code if necessary.
When you initialize the viewModel, you should use #StateObject instead of #ObservedObject. The two property wrappers are almost the same, except the #StateObject will make sure that the object stays "alive" when the view is updated / re-rendered, rather than re-initializing. Here are some great resources on this topic:
What is #StateObject?
#ObservedObject vs #StateObject vs #EnvironmentObject
I play with NativeScript on Wear OS2.
I just took the NS HelloWorld (typescript non frameworks) and I added the Firebase plugin to get a simple text from the notification and display it in textbox.
All okay, except when the app resumes from background, in that case the UI does not render the text change.
Debugging, I see the notification payload coming up to my typescript page instance, no errors... But the UI doesn't render it.
If I run the same app on a phone, all works as expected also resuming from background. Same code, no one line difference.
I am wondering if NativeScript does support Wear OS...
Any thoughts about that?
Thanks, Fabio.
Some code...
//main-page.ts
export function navigatingTo(args: EventData) {
//here Firebase is already init, all right...
(<Page>args.object).bindingContext = (new ViewModel).init();
}
//view-model.ts
import { Observable } from "tns-core-modules/data/observable";
import { NotificationService } from "./notification-service";
export class ViewModel extends Observable {
private _message: string = "Waiting message...";
init(): ViewModel {
//this callback is always invoked as expected
//also when the app is killed and comes back to life
NotificationService.onMessage = (firebaseMessage: any) => {
const text = firebaseMessage['text'] || firebaseMessage['data']['text'];
console.log('text' + text);
this.message = text; //that's okay, see setter below
}
return this;
}
get message(): string {
return this._message;
}
set message(value: string) {
if (this._message !== value) {
this._message = value;
//to this point the UI should update... BUT
//it works only IF the app is in foreground
//AND was never killed/backgrounded before.
//(bug on wear-os only)
this.notifyPropertyChange("message", value);
}
}
}
//main-page.xml
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
<StackLayout class="p-20">
<Label text="{{ message }}" class="h2 text-center" textWrap="true" />
</StackLayout>
</Page>
I'm creating a new watchOS app using SwiftUI and Combine trying to use a MVVM architecture, but when my viewModel changes, I can't seem to get a Text view to update in my View.
I'm using watchOS 6, SwiftUI and Combine. I am using #ObservedObject and #Published when I believe they should be used, but changes aren't reflected like I would expect.
// Simple ContentView that will push the next view on the navigation stack
struct ContentView: View {
var body: some View {
NavigationLink(destination: NewView()) {
Text("Click Here")
}
}
}
struct NewView: View {
#ObservedObject var viewModel: ViewModel
init() {
viewModel = ViewModel()
}
var body: some View {
// This value never updates
Text(viewModel.str)
}
}
class ViewModel: NSObject, ObservableObject {
#Published var str = ""
var count = 0
override init() {
super.init()
// Just something that will cause a property to update in the viewModel
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.count += 1
self?.str = "\(String(describing: self?.count))"
print("Updated count: \(String(describing: self?.count))")
}
}
}
Text(viewModel.str) never updates, even though the viewModel is incrementing a new value ever 1.0s. I have tried objectWillChange.send() when the property updates, but nothing works.
Am I doing something completely wrong?
For the time being, there is a solution that I luckily found out just by experimenting. I'm yet to find out what the actual reason behind this. Until then, you just don't inherit from NSObject and everything should work fine.
class ViewModel: ObservableObject {
#Published var str = ""
var count = 0
init() {
// Just something that will cause a property to update in the viewModel
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.count += 1
self?.str = "\(String(describing: self?.count))"
print("Updated count: \(String(describing: self?.count))")
}
}
}
I've tested this and it works.
A similar question also addresses this issue of the publisher object being a subclass of NSObject. So you may need to rethink if you really need an NSObject subclass or not. If you can't get away from NSObject, I recommend you try one of the solutions from the linked question.
First time writing an AsyncTask and I seem to have a subtle design flaw that prevents both ProgressDialog, ProgressBar, and even Log.d() from working properly. I suspect that somehow I am not actually creating a new thread/task.
Short: the symptoms
A ProgressDialog is created in the constructor, and the code orders Android to show it in onPreExecute(), but the dialog never shows.
onProgressUpdate() is supposed to execute whenever I call publishProgress() from within doInBackground(), correct? Well, it doesn't. Instead, it executes when doInBackground() completes.
Long: investigations
Things I have verified through the emulator and, when possible, on a phone:
onPreExecute() is definitely called
the progress bar is not reset until after doInBackground() completes
update_dialog.show() is definitely executed, but the dialog does not appear unless I remove the .dismiss() in onPostExecute(); I imagine dialog is, like the progress bar, not shown until after doInBackground() completes, but is naturally immediately dismissed
the code will happily show dialogs when no computation is involved
doInBackground() definitely invokes publishProgress()
when it does, onProgressUpdate() does not execute immediately! that is, I have a breakpoint in the function, and the debugger does not stop there until after doInBackground() completes! (perhaps this is a phenomenon of the debugger, rather than doInBackground(), but I observe the same symptoms on a mobile device)
the progress bar gets updated... only after doInBackground() completes everything
similarly, the Log.d() data shows up in Android Monitor only after doInBackground() completes everything
and of course the dialog does not show up either in the emulator or on a device (unless I remove .dismiss() from onPostExecute())
Can anyone help find the problem? Ideally I'd like a working dialog, but as Android has deprecated that anyway I'd be fine with a working progress bar.
Code
Here are the essentials, less the details of computation &c.:
Where I call the AsyncTask from the main thread:
if (searching) { // this block does get executed!
Compute_Task my_task = new Compute_Task(overall_context, count);
my_task.execute(field, count, max_x, max_y);
try { result = my_task.get(); } catch (Exception e) { }
}
The AsyncTask itself:
private class Compute_Task extends AsyncTask<Object, Integer, Integer> {
public Compute_Task(Context context, int count) {
super();
current_positions = 0;
update_dialog = new ProgressDialog(context);
update_dialog.setIndeterminate(true);
update_dialog.setCancelable(false);
update_dialog.setTitle("Thinking");
update_dialog.setMessage("Please wait");
}
protected void onPreExecute() {
super.onPreExecute();
update_dialog.show();
ProgressBar pb = ((ProgressBar) ((Activity) overall_context).findViewById(R.id.progress_bar));
pb.setMax(base_count);
pb.setProgress(0);
}
protected void onPostExecute() {
super.onPostExecute();
update_dialog.dismiss();
}
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
ProgressBar pb = ((ProgressBar) ((Activity) overall_context).findViewById(R.id.progress_bar));
pb.setMax(base_count);
pb.incrementProgressBy(1);
Log.d(tag, values[0].toString());
}
protected Integer doInBackground(Object... params) {
Integer result = compute_scores(
(boolean[][]) params[0], (Integer) params[1], (Integer) params[2], (Integer) params[3], 0)
);
return result;
}
public int compute_scores(boolean[][] field, int count, int max_x, int max_y, int level) {
int result, completed = 0;
switch(count) {
// LOTS of computation goes on here,
// including a recursive call where all params are modified
if (level == 0)
publishProgress(++completed);
}
}
ProgressDialog update_dialog;
}
Turns out this is basically the same issue as the one given here. The "fatal" line is this one:
try { result = my_task.get(); } catch (Exception e) { }
Apparently this puts the UI thread into deep sleep. One should not use get() with an AsyncTask unless one is willing to suspend the UI thread. We have to perform a little magic in onPostExecute() instead.
Although it turns out that this was a duplicate, I didn't find it until after I wrote it, because I didn't realize the thread was blocking.
When I instantiate a new StageVideo instsance with stage.stageVideos[0] everything works great, but when i leave my view that's displaying the video it sticks on the stage. So when i goto another view it's still showing on the stage in the background. I tried setting sv = null, removing event listeners...etc.
I've created a StageVideoDisplay class which is instantiated in mxml like: and on view initialization i call a play() method:
if ( _path )
{
//...
// Connections
_nc = new NetConnection();
_nc.connect(null);
_ns = new NetStream(_nc);
_ns.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus);
_ns.client = this;
// Screen
_video = new Video();
_video.smoothing = true;
// Video Events
// the StageVideoEvent.STAGE_VIDEO_STATE informs you whether
// StageVideo is available
stage.addEventListener(StageVideoAvailabilityEvent.STAGE_VIDEO_AVAILABILITY,
onStageVideoState);
// in case of fallback to Video, listen to the VideoEvent.RENDER_STATE
// event to handle resize properly and know about the acceleration mode running
_video.addEventListener(VideoEvent.RENDER_STATE, videoStateChange);
//...
}
On video stage event:
if ( stageVideoInUse ) {
try {
_rc = new Rectangle(0,0,this.width,this.height);
_sv.viewPort = _rc;
} catch (e:Error) {}
} else {
try {
_video.width = this.width;
_video.height = this.height;
} catch (e:Error) {}
}
And on stage video availability event:
protected function toggleStageVideo(on:Boolean):void
{
// To choose StageVideo attach the NetStream to StageVideo
if (on)
{
stageVideoInUse = true;
if ( _sv == null )
{
try {
_sv = stage.stageVideos[0];
_sv.addEventListener(StageVideoEvent.RENDER_STATE, stageVideoStateChange);
_sv.attachNetStream(_ns);
_sv.depth = 1;
} catch (e:Error) {}
}
if (classicVideoInUse)
{
// If you use StageVideo, remove from the display list the
// Video object to avoid covering the StageVideo object
// (which is always in the background)
stage.removeChild ( _video );
classicVideoInUse = false;
}
} else
{
// Otherwise attach it to a Video object
if (stageVideoInUse)
stageVideoInUse = false;
classicVideoInUse = true;
try {
_video.attachNetStream(_ns);
stage.addChildAt(_video, 0);
} catch (e:Error) {}
}
if ( !played )
{
played = true;
_ns.play(path);
}
}
What happens is in the view when i navigator.popView(), the stageVideo remains on the stage, even in other views, and when returning to that view to play a different stream the same stream is kind of "burned" on top. I can not figure out a way to get rid of it! Thanks in advance!
In Flash Player 11, Adobe added the dispose() method to the NetStream class.
This is useful to clear the Video or StageVideo object when you're done with it.
When you call the dispose() method at runtime, you may get an exception indicating that there is no property named dispose on the NetStream object.
This occurs because Flash Builder is not compiling the app with the proper SWF version. To fix that, just add this to your compiler directives:
-swf-version=13
In the new Flash Builder 4.7, we hopefully won't have to specify the SWF version to use the newer Flash Player features.
This seems to be the best solution, but if you can't use Flash 11 (or the latest Adobe AIR), some other work arounds would be:
set the viewPort of stage video object to a rectangle that has width=0 and height=0
since stage video appears underneath your app, you can always cover the viewport (with a background color or some DisplayObject)
Ok the issue was actually that, even though it seemed like stage video was in use since i got the "Accelerated" message in status, that it was actually the video render even running and classic video was actually in use. The only thing I needed to do was add stage.removeChild( _video ) to the close() event in the class!!