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.
Related
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?
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.
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
.
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.
I'm trying to test a react component.
var Component = React.createClass({
componentDidMount: function () {
return this.setState({
name: 'blabla'
});
},
render: function () {
return (
<h1>{this.state.name}</h1>
);
}
});
Is there a way, during testing, to mock what componentDidMount returns or does? That would leave me to test it on it's own and just test the component render behaviour.
Thanks!
I prefer the following approach, but requires using ES6 classes.
// component.jsx
class Component extends React.Component {
componentDidMount() { return this.setState({name: 'blabla'}); }
render() { return (<h1>{this.state.name}</h1>); }
}
//component-spec.jsx
describe('Component', () => {
it('does stuff', () => {
let ComponentTest = class extends Component {
componentDidMount() {
// your override here
}
};
let component = TestUtils.renderIntoDocument(<ComponentTest />);
//expect(component...).toEqual(...)
});
});
The point is to create an on demand ChildClass inheriting the OriginalClass,
do whatever overrides and then TestUtils.renderIntoDocument(<ChildClass />)
The idea here, if I understand correctly, is that you're trying to stub out a function before a component is rendered in your test. In your case, componentWillMount is only called once in a component's lifecycle, immediately before the component is rendered. So you can't just render the component and then stub out the function, it must be done before the render occurs.
Let's take these components for example:
parent.js
var Child = require('./child.js');
var Parent = React.createClass({
render : function () {
return (
<div className="parent">
<Child/>
</div>
);
}
});
module.exports = Parent;
child.js
var Child = React.createClass({
test : function () {
return true;
},
render : function () {
if (this.test) {
throw('boom');
}
return (
<div className="child">
Child
</div>
);
}
});
module.exports = Child;
Here, we would want to stub out the test function before our Child component is rendered, otherwise, it will blow up.
I have been able to do this using jasmine-react. These helper functions provide some useful functionality when running tests, almost to the point where TestUtils can be ditched completely.
jasmineReact.render(component, [container]) will render an instance of component into the DOM node specified in [container]. This is like TestUtils.renderIntoDocument(), except it renders the component into an attached DOM node instead of a detached DOM node. It will also perform the necessary cleaning operations when the test is finished.
jasmineReact.spyOnClass(componentClass, functionName) will stub out a particular function belonging to a component class. This behavior is maintained until the end of the test, which means that you can call this function before a component is rendered. This, if I understand correctly, is what you're looking for.
So, using these two helper functions, I can write a test for the code shown above that looks something like this:
var React = require('react/addons'),
Parent = require('./parent.js'),
Child = require('./child.js'),
jasmineReact = require('jasmine-react-helpers');
describe('Parent', function () {
it('does not blow up when rendering', function () {
jasmineReact.spyOnClass(Child, 'test').and.returnValue(false);
var parentInstance = jasmineReact.render(<Parent/>, document.body); //does not blow up
expect(parentInstance).toBeTruthy(); //passes
});
});
Let me know if you have any questions.
I've found two ways to go about this (i'm sure there are more).
1) I've used sinon-chai and required in the base element class and then use rewireify to put a set a spy on the componentWillMount method. This works but not sure what test suites you're using.
2) Probably the easier way. Is to just use the TestUtils to get an instance of the component and then just manually run the componentWillMount method.
That second way would probably look something like (forgive the pesudo code):
it('should call state when it first mounts', function () {
var Component = require('../my-component');
var component = TestUtils.renderIntoDocument(<Component />);
component.setState({name: null});
component.componentWillMount();
expect(component.state.name).to.equal('blabla');
});