I have a number of changes to a document-based application which are not undoable (conceptually, think quiz progress) but which should dirty the document so the state is saved when the user quits or closes the document.
Apple's documentation for ReferenceFileDocument states Changes to the document are observed and on change dirty the document state for resaving. but that does not happen for me. (I have tried both changes to a DocumentData class held by my document and to a variable directly within my ReferenceFileDocument subclass, but nothing happens; the document stays clean and does not autosave.)
NSDocument has an updateChangeCount function which is all I want at this point.
All references to dirtying documents I have found involve an undo manager, so in my document class I have implemented
var isDirty: Bool = false
func dirtyDocument(undoManager: UndoManager?){
undoManager?.registerUndo(withTarget: self) { isDirty in
undoManager?.removeAllActions(withTarget: self)
}
if isDirty == false {
isDirty = true
}
}
and any View that makes a noteworthy change contains
#Environment(\.undoManager) var undoManager
and calls document.dirtyDocument(undoManager: undoManager)
This works in that it dirties the document, but leaves me with an enabled 'Undo' action which does nothing BUT which marks the document as 'not dirty' if the user invokes it, which is why I added undoManager?.removeAllActions(withTarget: self)
I now have the functionality I want, apart from not finding a way to disable the undo menu item for this 'undo action' but it still feels hackish. Is there a better way to emulate updateChangeCount without invoking the UndoManager and and immediately discarding its changes?
Scenario: I retrieve data from a server via #ObservableObject/#Publish; within a tabview().
Data is displayed as expected.
However when I return to the tabView from another tab, I data is gone, requiring me to do another fetch which isn't needed.
Here's the subscriber:
struct NYTStatesView: View {
#ObservedObject var dataSource = NYTStatesModel()
...
}
Here's the publisher:
final class NYTStatesModel: ObservableObject {
#Published var revisedNYTStates: RevisedNYTStates!
// ...
}
dataSource.revisedNYTStates is empty upon return to the View.
Question: How do I RETAIN the data so I don't have to always access the server per View display?
When you're switching tabs, dataSource is recreated:
struct NYTStatesView: View {
#ObservedObject var dataSource = NYTStatesModel()
...
}
A solution may be to create dataSource outside the TabView.
For example on the app level:
#main
struct TestApp: App {
#StateObject private var dataSource = NYTStatesModel()
...
}
(or in the SceneDelegate for SwiftUI 1.0)
This is something that I struggled with for a while too. There are a couple of different ways to do what you want.
The first is creating a loader class from somewhere higher up in the view hierarchy and passing it down to where it is needed. For example, you might create the loader in the the app main struct and passing it in as an environment object. IMO, this method is the least scalable and the messiest.
The second option is to either use a singleton or a static property to persist your data. For example you could make your NYTStatesModel a singleton. (I don't condone this type of behavior)
The third (and best IMO) is creating a class that is responsible for caching your results and passing that into the view model from the environment. NSCache will actually dump results when your phone is running low on memory. Here's two articles that will help. The first is how to create a cache and the second is how to create an environment key.
https://www.swiftbysundell.com/articles/caching-in-swift/
https://swiftwithmajid.com/2019/08/21/the-power-of-environment-in-swiftui/
Just use dependency injection to check the cache before the network call. The cache is persisted in the environment.
I am setting up unit testing for my Swift project and am having trouble testing a class function that involves updating IBOutlets.
I have a function, validateUrl, which expects a string to be passed, then validates it. If it is valid, it enables a UIButton, if it is invalid, it disables a UIButton. When I run a test that calls this function, the app crashes on the line of code that enables or disables the UIButton.
The storyboard and controllers both has the proper Test target set.
This line of code:
self.submitButton.enabled = true// Enable Submit Button
Spits out this error:
fatal error: unexpectedly found nil while unwrapping an Optional value
Try this code to initialize the IbOutlets of your view controller:
let yourStoryboard = UIStoryboard(name: "Your_storyboard", bundle: nil)
yourViewController = yourStoryboard.instantiateViewController(withIdentifier: "YourViewController") as! YourViewController
yourViewController.loadView() // This line is the key
You have to initiate the view controller using the storyboard. See the documentation here: https://developer.apple.com/library/ios/documentation/uikit/reference/UIStoryboard_Class/index.html#//apple_ref/occ/instm/UIStoryboard/instantiateViewControllerWithIdentifier:
If you initialize the view controller directly, it will not have any connections because the VC itself does not know of the storyboard in this case.
You may need to add the controllers view to a hierarchy prior to testing to force the controller to load the XIB
let localContainer = UIView(frame:someFrame)
let controllerUnderTest = //instantiate your controller
localContainer.addSubview(controllerUnderTest.view)
//at this point you can test outlets
Otherwise your outlets will be nil as they haven't been connected yet.
Testing IBOutlet's is not the best approach, because:
outlets should be considered implementation details, and should be declared as private
outlets are most of the time connected to pure UI components, and unit tests deal with testing the business logic
testing the value injected into the outlet by another function can be considered somewhat as integration testing. You'd also double the unit tests you have to write by having to test the connected/unconnected outlet scenarios.
In your particular case, I'd recommend instead to test the validator function, but first making it independent of the controller class (if it's not already). Having that function as an input->output one also bring other benefits, like increased reusability.
Once you have tested all the possible scenarios for the validator, validating that the outlet correctly behaves it's just a matter of a quick manual testing: just check if the outlet behaves like the validator returned. UI stuff are better candidates for manual testing, as manual testing can catch other details too (like positioning, colors, etc).
However, if you really want to test the outlet behaviour, one technique that falls into the unit testing philosophy is snapshot testing. There are some libraries available for this, I'd recommend the one from https://github.com/uber/ios-snapshot-test-case/.
A solution I'm using to test classes in Swift is to create the outlets in the setUp() method of the test. For example, I have a UIViewController subclass that has a UIImageView as an outlet. I declare an instance of my View controller subclass a property of my test class, & configure it's outlet in the setUp() method by initializing a new UIImageView.
var viewController = ViewController() // property on the test class
override func setUp() {
super.setUp()
viewController.imageView = UIImageView(image: UIImage(named: "Logo")) // create a new UIImageView and set it as the outlet object
}
Any other outlets can similarly be set in the setUp method, and this way you don't even have to instantiate a storyboard (which, for some reason, despite the fact that I was able to instantiate a UIStoryboard object in my tests, the view controller outlets were still nil).
#talzag In your case iboutlets are nil because they are weak variables, They are supposed to deallocate immediately after instantiation.
Note the below Ember view definition. If I remove the didInsertElement call or comment out the get('controller') call, the setupMultiselect observer never gets called. Is this a feature or a bug? Confused...
Discourse.KbRelatedObjView = Discourse.View.extend({
...
didInsertElement: function() { var self = this;
// for some reason this needs to be here else the observer below never fires
self.get('controller');
},
setupMultiselect: function() { var self = this;
...
}.observes('controller.objPage')
});
I wouldn't say it's a feature or a bug, more like a quirk. It is the expected behavior though. It's noted here.
UNCONSUMED COMPUTED PROPERTIES DO NOT TRIGGER OBSERVERS
If you never get a computed property, its observers will not fire even if its dependent keys change. You can think of the value changing from one unknown value to another.
This doesn't usually affect application code because computed properties are almost always observed at the same time as they are fetched. For example, you get the value of a computed property, put it in DOM (or draw it with D3), and then observe it so you can update the DOM once the property changes.
If you need to observe a computed property but aren't currently retrieving it, just get it in your init method.
I'm calling destroy() on a controller when I leave a certain view (not the viewcontroller but an additional controller that I use).
If I then go back in that same view, the controller is still there, it has the properties:
_didCallDestroy: true
isDestroyed: true
isDestroying: true
all set as expected, but my view is still binding to them. Is it not destroying because it's content still holds data? What could be causing this?
The additional arraycontrollers mentioned above are placed in an array. The solution was to not only destroy the controllers but also reset the array to []:
this.get('dataSets').forEach(function (ds) {
ds.destroy();
});
this.set('dataSets', []);