ObservedObject not triggering view redraw - swiftui

I've a view which requires data obtaining from an api.
My understanding of SwiftUI is that by using onAppear the api will be called, viewmodel attribute updated, and the StateObject changed which will trigger a View redraw.
The issue is that no redraw is taking place.
I can see an api call occuring, and adding debug into after the decoded data is used I can see a lot of data is returned.
I've removed a lot of code to make the logic easier to follow (below).
Replacing #StateObject with #ObservedObject and passing into the view from a parent makes no difference either.
Thanks
struct FactoryDetailView: View {
var factory: Factory
#StateObject var factoryDetailsViewModel: FactoryDetailsViewModel()
var body: some View {
VStack {
Text(factory.name)
ForEach(factoryDetailsViewModel.details) { det in
Text(det)
}
}
.onAppear { factoryDetailsViewModel.loadDetails(factory) }
}
}
The viewmodel:
class FactoryDetailsViewModel: ApiViewModel {
#Published var details: [ String ]
func loadDetails(factory: Factory) {
// Do api call...
self.objectWillChange.send()
self.details = decodedResultsFromApiCall
self.objectWillChange.send()
}
class ApiViewModel: ObservableObject {
}

Well... removed details might be the reason of the issue, but in general approach should look like following
struct FactoryDetailView: View {
...
// assume it is a type and here there is initialization
#StateObject var factoryDetailsViewModel = FactoryDetailsViewModel()
...
now about self.objectWillChange.send() - don't call it, modifying published property it is called automatically
func loadDetails(factory: Factory) {
// Do api call...
{
// this is inside (!) API callback
DispatchQueue.main.async {
// update published property on main queue, always.
self.details = decodedResultsFromApiCall
}
}
}

This has the answer:
#Published property wrapper not working on subclass of ObservableObject
The issue I'm seeing is the same - subclassing an ObservableObject.
I've now got a solution working by '#Published var api_response' in the parent class and removing #Published from attributes in subclasses (although leaving '#Published' in them doesn't seem to cause any side effects so they may as well remain if only to document the intent).
Thanks for the responses.

Related

SwiftUI: How wrong is to call the objectWillChange.send() after the change in the model data happened?

I'm trying to better understand the mechanism how SwiftUI gets notified about changes in observed data and invalidates UI.
In particular, I wonder if something won't work correctly (e.g. animations?) when objectWillChange.send() is called on ObservableObject after the underlying data model have changed, and more importantly why is that.
If it doesn't matter when the synthesized publisher notifies its observers after the change, then why is it named objectWillChange and not objectDidChange?
Let's say I have a view model like this the following and it observes changes in some model via a delegate method (the code is simplified for the example purposes):
protocol ModelDelegate: AnyObject {
func modelDidChange() {
}
class Model {
weak var delegate: ModelDelegate?
var stroredString: String {
didSet { self.delegate?.modelDidChange() }
}
}
class ViewModel: ObservableObject {
private let model: Model = Model(delegate: self)
var calculatedProperty: String { model.storedString }
}
extension ViewModel: ModelDelegate {
func modelDidChange() {
// we get called when the change has already happened.
// is it wrong to publish the change via objectWillChange?
self.objectWillChange.send()
}
}
And then the View looks like this:
struct MyView: View {
#ObservedObject private var viewModel: ViewModel
var body: some View {
Text(self.viewModel.calculatedProperty)
}
}
Question
Can I expect that something's not going to work correctly when objectWillChange.send() is called on ObservableObject after the underlying data model have changed? If yes, in which case and more importantly, why is that?
If it doesn't matter when the synthesized publisher notifies its observers after the change, then why is it named objectWillChange and not objectDidChange?

How to cancel process() in SwiftUI from different class

I am running a binary file using a swift process() in my DataModel class. The user can see the process running from the ViewTasks() struct. How can I give the user the ability to cancel (terminate) the process?
Here is what I have tried:
DataModel class:
class DataModel : ObservableObject {
#Published var task = Process()
func RunTask() {
// add arguments to task here
do {
task.arguments = [args]
try task.run()
} catch {}
}
Here is the ViewTask() class:
struct ViewTask: View {
#ObservedObject var datamodel: DataModel
var body: some View {
///if user presses a cancel button, cancel the task running
datamodel.task.terminate()
}
This doesn't work because the task can't find the arguments. But when I don't make task an #Published variable, and it only exists locally in that Do loop, it works fine.
Maybe I need to declare/initialize it differently in the datamodel?
Thanks
Ok I figured out how to have a process (on background or main thread) visible through an observable object.
class DataModel : ObservableObject {
#Published var task = Process()
func RunTask() {
// add arguments to task here
do {
let temptask = Process()
temptask.arguments = [args]
// set observable object to this process
self.task = temptask
try task.run()
} catch {}
}
This will allow any other view to call datamodel.task.terminate() and it works just fine. If the process is in a background thread, simply use DispatchQueue.main.async{self.task = temptask} to set the observable task as the locally made process.

Couldn't cast SwiftUI.ExtensionDelegate to ExtensionDelegate

Working working with a SwiftUI Architecture WatchOS app, if you want to use ExtensionDelegate you need to create your own. I have done this, but when I try to actually access the delegate in the code, I am getting the following error message Could not cast value of type SwiftUI.ExtensionDelegate' (0x7fff8441b480) to 'TestMe_WatchKit_Extension.ExtensionDelegate' (0x10c3b36d0).
I have defined the ExtensionDelegate as -
class ExtensionDelegate: NSObject, WKExtensionDelegate {
var meetingStatistics: MeetingStatistics = MeetingStatistics()
override init(){
super.init()
}
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
print("applicationDidFinishLaunching for watchOS")
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
}
in my #main - I have the following:
#main
struct WatchApp: App {
#WKExtensionDelegateAdaptor(ExtensionDelegate.self) var delegate
// code
}
When I try to access the delegate as -
let delegate = WKExtension.shared().delegate as! ExtensionDelegate I get the above error.
The WKExtension.shared().delegate is not your ExtensionDelegate but internal SwiftUI.ExtensionDelegate (as stated), which provides your adapter delegate. You must use (pass everywhere) only instance of adapter delegate provided for your via
#WKExtensionDelegateAdaptor(ExtensionDelegate.self) var delegate
Update:
To use your delegate you have to pass it as argument next to view, eg as environment (so you can use it in any subview as #Environment(\.appDelegate) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.appDelegate, delegate)
}
}
and
struct DelegateKey: EnvironmentKey {
typealias Value = ExtensionDelegate?
static let defaultValue: ExtensionDelegate? = nil
}
extension EnvironmentValues {
var appDelegate: DelegateKey.Value {
get {
return self[DelegateKey.self]
}
set {
self[DelegateKey.self] = newValue
}
}
}
Well, - in the meantime I collected more experience. For my application delegate it is important that it is a singleton (only one instance exists). I realised that I have two.
#NSApplicationDelegateAdaptor(AppDelegate.self) var applicationDelegate
in the SwiftUI App struct initialises the first. To this App struct I added a init() method.
init() {
AppDelegate.shared = self.applicationDelegate
}
That initialises my AppDelegate static variable "shared" very early in the programme.
I removed all #Environment based access to the delegate, the key, the default value etc.. Global access is insured by
AppDelegate.shared
The issue with the self-defined #Environment variables is, that they can only be set with an .environment(_ keyPath:, _ value:) modifier on a view. However, my delegate is accessed before the first view sets the environment variable. Hence, my AppDelegate shared variable is now an ordinary static variable which I set myself as early as possible: in the init() method of the App struct.
I have the same problem in macOS. After declaring a class as the application delegate using in the #main App struct:
#NSApplicationDelegateAdaptor(AppDelegate.self) var applicationDelegate
The AppDelegate is correctly initialised when the Application launches.
I declare the environment key:
struct DelegateKey: EnvironmentKey {
typealias Value = AppDelegate
static let defaultValue: AppDelegate = AppDelegate()
}
extension EnvironmentValues {
var appDelegate: DelegateKey.Value {
get {
return self[DelegateKey.self]
}
set {
self[DelegateKey.self] = newValue
}
}
}
to access the variable via
#Environment(\.appDelegate) var appDelegate
That is fine for all SwiftUI environments.
The problem is that the general access to the application delegate via
NSApp.delegate
does not work any longer.
The solution is to declare a static variable in the AppDelegate class itself:
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
#Environment(\.appDelegate) static var shared
}
This makes the instantiated AppDelegate object accessible from everywhere via:
AppDelegate.shared
.

Swift protocol delegate unexpectedly found nil in UIViewController

I have declared a protocol in one view controller and want to call a function in it's presenting view controller. I am not using storyboards.
This delegate!.setWishListButton() causes an error unexpectedly found nil while unwrapping an optional. I know I shouldn't use ! but it's for testing since my delegate function is never called.
I know there are many questions about this but none that matches my usage. I have a similar pattern in a UIView which works fine.
Here is my code
protocol SetWishListButtonDelegate {
func setWishListButton()
}
class MyViewController: UIViewController {
var delegate : SetWishListButtonDelegate?
------
func toggleWishListButton() {
delegate?.setWishListButton()
}
In another view controller
class anotherViewController: UIViewController, SetWishListButtonDelegate {
let myViewController = MyViewController()
override func viewDidLoad() {
super.viewDidLoad()
myViewController.delegate = self
}
func setWishListButton() {
print("DELEGATE METHOD FIRED")
wishListButton.isSelected = true
}
This worked after I removed a second declaration of my view controller in a function where it was presented by a button press.

Hover event in graph Chart.js

Goodnight
Possible to implement something like that? in chartjs http://grab.by/C1T6
canvas.mouseover = function(e){
var activePoints = MyCharts.getBarsAtEvent(e);
console.log(activePoints);
};
so it is impossible to catch event(
The chart is wrapped in a div, why dont you just put a move over on that element
$(function() {
$("#element").mouseover(function(e) { /* implementation */ });
});
So a little trick I found is you can use the customTooltip option to call your own function. It takes one object as a parameter, and is either false, or the segment that is being hovered over.