os x nstextfield validation - swift

I have a variable in my NSViewController:
dynamic var roll_rate:Double = 0.0
I attach it to my NSTextField:
Model Key Path shows error, but it is working: When i changed value in field, variable changed too. But what means:
Validates Immediately and how do i show and check validation errors for field.
I tried implement method validateRoll_rate, but it didn't call when value changed.

Generic solution (work with or without bindings) One way of dealing with this is based on the response here
Basically you use the controlTextDidChange(notification:) delegate method of NSTextField and you implement your validation code in it.
override func controlTextDidChange (notification: NSNotification) {
guard let textField = notification.object as? NSTextField else { return }
// test here, replace the dummy test below with something useful
if textField.stringValue != "expected value" {
myTextFieldOutlet.backgroundColor = NSColor.red
myErrorLabelOutlet.stringValue = "Error !!!"
} else {
// everything OK, reset the background color and error label to the normal state
....
}
}
Obviously myTextFieldOutlet is an outlet linked to your text field and myErrorLabelOutlet is an outlet to a conveniently placed label used to show errors (blank if no error should be presented)
Bindings oriented solution Be sure Validates immediately is selected in Interface Builder and implement the following method in the class where the binding is made (Tuning View Controller in your example)
override func validateValue(_ ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey inKey: String) throws {
// test here, replace the dummy test below with something useful
if roll_rate > 10.0 {
throw NSError(domain: "your-domain", code: 100, userInfo: [NSLocalizedDescriptionKey: "Error, roll rate too high"])
}
}
When the error is thrown, the user will be presented with the standard sheet announcing the error and the option to cancel the change or correct it.
If Continuously updates value is selected in Interface Builder the method above will be called for each keystroke in the text field, otherwise only after pressing Enter or loosing focus.
Note: For a full understanding on how updating values through bindings work, including what Validates immediately does, see the docs here.

Related

how to detach a listener in a local scope?

I want to detach a snapshotListener in a viewController when a button is pressed. I was reading other stack over flow questions and the documentation and they were calling the remove method for the listener in the same function. I tried to do that in my situation but my snapshotListener just didn't end up working at all.
Here's my function and block of code that I want to tweak.
#objc func doneTapped() {
let updateListener = db.collection("school_users/\(user?.uid)/events").whereField("event_name", isEqualTo: navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.eventName = querySnapshot!.documents.map { document in
return EventName(eventName: (document.get("event_name") as! String))
}
self.db.document("school_users/\(self.user?.uid)/events/\(self.docIDUneditableTextF.text!)").updateData(["event_date": self.dateEditableTextF.text, "event_cost": self.costEditableTextF.text, "for_grades": self.gradesEditableTextF.text]) { (error) in
if let error = error {
print("There was an error updating the document: \(error)")
} else {
print("The document was successfully updated."
}
}
}
}
dateEditableTextF.resignFirstResponder()
dateEditableTextF.isEnabled = false
costEditableTextF.resignFirstResponder()
costEditableTextF.isEnabled = false
gradesEditableTextF.resignFirstResponder()
gradesEditableTextF.isEnabled = false
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
}
I tried calling updateListener.remove() but it made my snapshotListener not work at all, also when the document updates, the print statement is never ending, is that also because the listener is still active or is that a different issue?
addSnapshotListener will give you updates whenever the data changes. I'm unclear why you would want to immediately remove the listener, if you really did want to receive updates -- and, as you pointed out, immediately removing it will basically cause to it not function at all. Perhaps post a link to some of those posts/documentation where you saw the code you're referencing and someone can give insight into what's happening.
My suspicion is that you don't actually need the updates to the data. In that case, you can just use .getDocuments() instead. See the Firestore documentation here about different ways to get data: https://firebase.google.com/docs/firestore/query-data/get-data
The second problem (the infinite print) is related to the first. Because you have a listener, which will return updates when the data changes, when you do your second database call (your updateData), that updates your data, triggering the listener again. This will keep looping because they will keep calling each other. This is another sign that perhaps you don't actually want a listener, but a single call to get the data. If you do in fact want updates, you'll have to find a way to decouple your second request so that you don't get in the loop.
Update based on comments: (Example of removing the listener in a different function)
On your view, view controller, etc, declare a property for the listener:
class MyViewController : UIViewController {
private var documentListener: ListenerRegistration? //assuming that ListenerRegistration is the correct type here, but you can check the current type of your updateListener to check
}
Then, in your function, set your listener to that:
documentListener = db.collection("school_users/\(user?.uid)/events").whereField("event_name", isEqualTo: navigationItem.title).addSnapshotListener()...
Then, later (like in viewDidDisappear), you can remove it:
documentListener?.remove()

Customizing sandboxed NSSavePanel alert

I am validating urls from NSSavePanel using the delegate's panel(_:validate) method, throwing error in case of invalid url. In such case the NSSavePanel presents an alert, which I want to customize (meaning present some human readable description) depending on the error thrown, keeping the save panel window open and then letting you choose another path.
LocalizedError works just fine when not using App Sandbox but in a sandboxed app the getter for error description is never called and the message in the alert is generic "Operation couldn't be completed. (#yourErrorType)", which I guess is somehow caused by the different inheritance chain for sandboxed NSSavePanels.
I am struggling figuring a way around this - is it possible to customize the alert somehow while still keeping the app sandboxed?
Addendum: Permissions for User Selected File => r/w. Running the following example produces different alerts with/without sandbox.
func runSavePanel()
{
let panel = NSSavePanel()
let delegate = SavePanelDelegate()
panel.delegate = delegate
_ = panel.runModal()
}
class SavePanelDelegate: NSObject, NSOpenSavePanelDelegate {
func panel(_ sender: Any, validate url: URL) throws {
throw CustomError.whatever
}
}
enum CustomError: LocalizedError {
case whatever
var errorDescription: String? {
get {
return "my description"
}
}
}
So, after a bit of further digging I can tell the solution of the riddle finally although I can only guess the reasons why it was made tricky by Apple. Apparently NSError exclusively needs to be used. The customization has to be done in userInfo, say
let userInfo = [NSLocalizedDescriptionKey: "yourLocalizedDescription", NSLocalizedRecoverySuggestionErrorKey: "yourSuggestion"]
throw NSError(domain: "whatever", code: 0, userInfo: userInfo)
etc. By the way subclassing NSError doesn't work, the Sandbox will just happily ignore you :)

Customize "Add" button in MultivaluedSection

I'm trying to change the "Add" button of a MultivaluedSection in Eureka. The current behavior is that when you click on the "Add" button, it creates a new empty cell in the MultivaluedSection.
What I would like to achieve, would be that when a user click on the "Add" button, it shows up a PushRow where the user can choose the initial value of the cell.
I had no luck with any of the two way I tried to get this behavior.
I first tried to create a custom class where I could completely change the MultivaluedSecion behavior :
class ExerciseMultivaluedSection : MultivaluedSection {
override public var addButtonProvider: ((MultivaluedSection) -> ButtonRow) = { _ in
let button = ButtonRow {
$0.title = "MyCustomAddButton"
$0.cellStyle = .value1
}.cellUpdate { cell, _ in
cell.textLabel?.textAlignment = .left
}
// Here i would link my button to a function
// that would trigger a PushRow, maybe through a segue ?
return button
}
required public init() {
super.init()
}
required public init<S>(_ elements: S) where S : Sequence, S.Element == BaseRow {
super.init(elements)
}
required public init(multivaluedOptions: MultivaluedOptions, header: String, footer: String, _ initializer: (MultivaluedSection) -> Void) {
super.init(header: header, footer: footer, {section in initializer(section as! ExerciseMultivaluedSection) })
}
}
However, it did not work because of this error : "Cannot override with a stored property 'addButtonProvider'"
Then, I tried to change the addButtonProvider at run time, but it did nothing :
let exerciseSection = MultivaluedSection(multivaluedOptions:[.Delete,.Reorder,.Insert],header:"Exercises")
exerciseSection.tag = "exercise"
exerciseSection.multivaluedRowToInsertAt = {idx in
let newRow = LabelRow(){row in
row.value = "TestValue"
let deleteAction = SwipeAction(style: .destructive, title: "DEL"){action,row,completion in
completion?(true)
}
row.trailingSwipe.actions = [deleteAction]
}
return newRow
}
exerciseSection.addButtonProvider = {section in
let addBtn = ButtonRow("Test add"){ row in
row.title = "Custom add button"
}
print("Custom add button" )
return addBtn
}
Even after that, my add button still shows "Add", and my print function never gets called. Why is that ?
Also, is one of these two ways a good one ? If not, what would be the "correct way" to achieve that ?
I'm using XCode 9.4.1 with iOS 11.4
If you are willing to fork and slightly change the Eureka source code, a very few changes will allow you to use a PushRow, or a custom ButtonRow, or any other Row as the AddButtonProvider in a MultiValuedSection.
The few necessary changes are documented in this following GitHub commit which shows the before and after source code changes:
https://github.com/TheCyberMike/Eureka/commit/bfcba0dd04bf0d11cb6ba526235ae4c10c2d73fd
In particular, using a PushRow can be tricky as the add button. I added information and example code to the Eureka GitHub site in the following issue's comments:
https://github.com/xmartlabs/Eureka/issues/1812
Subsequent comments may be added there by other users of Eureka.
=== From #1812 comment ===
I made a pretty simple change to Eureka in my app's fork to allow any Row to be used as the Add row. It allows the default ButtonRow to be used as-is and handled automatically. But it also allows any other row such as a PushRow to be used as the AddButtonProvider.
In my app, in multiple places, when an end-user presses the add button, I present them a popup list of choices they may add. They choose one, and I add that choice to the MultiValuedSection.
The simple changes are in this forked Eureka commit in my app:
https://github.com/TheCyberMike/Eureka/commit/bfcba0dd04bf0d11cb6ba526235ae4c10c2d73fd
Although I have not tested it in my own app, this change should also support allowing a custom ButtonRow for the MVS. But I think you will need to handle the tableview's .insert in an .onCellSelection in your own code for that custom ButtonRow. The statements you need to do that are in both the commit code above and the example below.
Using a PushRow as an AddButtonProvider is somewhat tricky, since the PushRow invokes a separate view controller to present the list, then returns to the original form's view controller.
So you must be sure NOT to rebuild the original form and its MultiValuedSection when viewWillAppear() and viewDidAppear() are called upon returning from the PushRow view controller.
Also, the choice itself is made within the PushRow view controller. The PushRow handles preserving that choice back from the PushRow view controller. But the .onChange is invoked while the PushRow view controller is still active. I used a DispatchQueue.main.async closure to handle deferring the tableview .insert call to when the original form's view controller is active.
To prevent the PushRow from showing the last choice made in its right-most grayed accessory field, one must nil out its .value. However, that triggers a .onChange too, which if you are not careful can cause an infinite loop situation. I just used a simple if statement to make sure the PushRow's value is not nil to prevent that loop (yes it could also be a guard statement).
Be sure to use the [weak self]'s to prevent memory leaks.
Below is an adapted example of usage code from my eContact Collect app (https://github.com/TheCyberMike/eContactCollect-iOS), where things like languages and data entry fields and email accounts are chosen from pre-defined lists. Again, this code WILL NOT WORK unless the cited source code changes are make.
self.mForm = form
form +++ MultivaluedSection(multivaluedOptions: [.Insert, .Delete, .Reorder], header: NSLocalizedString("Shown languages in order to-be-shown", comment:"")) { mvSection in
mvSection.tag = "mvs_langs"
mvSection.showInsertIconInAddButton = false
mvSection.addButtonProvider = { [weak self] section in
return PushRow(){ row in
// REMEMBER: CANNOT rebuild the form from viewWillAppear() ... the form must remain intact during the PushRow's view
// controller life cycle for this to work
row.title = NSLocalizedString("Select new language to add", comment:"")
row.tag = "add_new_lang"
row.selectorTitle = NSLocalizedString("Choose one", comment:"")
row.options = langOptionsArray
}.onChange { [weak self] chgRow in
// PushRow has returned a value selected from the PushRow's view controller;
// note our context is still within the PushRow's view controller, not the original FormViewController
if chgRow.value != nil { // this must be present to prevent an infinite loop
guard let tableView = chgRow.cell.formViewController()?.tableView, let indexPath = chgRow.indexPath else { return }
DispatchQueue.main.async {
// must dispatch this so the PushRow's SelectorViewController is dismissed first and the UI is back at the main FormViewController
// this triggers multivaluedRowToInsertAt() below
chgRow.cell.formViewController()?.tableView(tableView, commit: .insert, forRowAt: indexPath)
}
}
}
} // end of addButtonProvider
mvSection.multivaluedRowToInsertAt = { [weak self] index in
// a verified-new langRegion code was chosen by the end-user; get it from the PushRow
let fromPushRow = self!.mForm!.rowBy(tag: "add_new_lang") as! PushRow
let langRegionCode:String = fromPushRow.value!
// create a new ButtonRow based upon the new entry
let newRow = self!.makeLangButtonRow(forLang: langRegionCode)
fromPushRow.value = nil // clear out the PushRow's value so this newly chosen item does not remain "selected"
fromPushRow.reload() // note this will re-trigger .onChange in the PushRow so must ignore that re-trigger else infinite loop
return newRow // self.rowsHaveBeenAdded() will get invoked after this point
}
}

How to test a function that gets into the main thread in Swift with RxSwift and XCTest?

I came across this problem when testing my View:
In my ViewModel I call to an asynchronous operation and when the response arrives, I use a PublishSubject to produce a change in my View. In my View, I call DispatchQueue.main.async in order to hide or show a button.
ViewModel
let refreshButtons = PublishSubject<Bool>(true)
refreshButtons.onNext(true)
View
model.refreshButtons.asObservable()
.subscribe(onNext: {
[unowned self] success in
self.updateButtons(success)
})
.addDisposableTo(disposable)
private func updateButtons(_ show:Bool) {
DispatchQueue.main.async{
button.isHidden = !show
}
}
Now I don't know how to unit test that refreshButtons.onNext(true) will hide or show my button.
The solutions I can think of are:
Overriding the method and having an async expectation, but for that I need to make the method public, what I don't want, or
Dispatching the main queue in my ViewModel and not in the view, what it sounds odd to me, but might me ok.
How can I solve this?
Thank you in advance.
You could use an async expectation based on a predicate in your unit test to wait an see if the button is not hidden anymore.
func testButtonIsHidden() {
// Setup your objects
let view = ...
let viewModel = ...
// Define an NSPredicate to test your expectation
let predicate = NSPredicate(block: { input, _ in
guard let _view = input as? MyView else { return false }
return _view.button.isHidden == true
})
// Create an expectation that will periodically evaluate the predicate
// to decided whether it's fulfilled or not
_ = expectation(for: predicate, evaluatedWith: view, handler: .none)
// Call the method that should generate the behaviour you are expecting.
viewModel.methodThatShouldResultInButtonBeingHidden()
// Wait for the
waitForExpectationsWithTimeout(1) { error in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
Something worth noting is that the value you pass to the NSPredicate should be a class. That is because classes are passed by reference, so value inside the predicate block will be the same as the one touched by your view model. If you were to pass a struct or enum though, which are passed by copy, the predicate block would receive a copy of the value as it is at the time of running the setup code, and it will always fail.
If instead you prefer to use UI tests as suggested by #Randall Wang in his answer, then this post might be useful for you: "How to test UI changes in Xcode 7". Full disclosure, I wrote that post.
First of all, You don't need test private method
If you want to test if the button is hidden or not,try UI testing
here is the WWDC of UI testing.
https://developer.apple.com/videos/play/wwdc2015/406/

EXC_BAD_ACCESS (code=2) when using NSNumberFormatter

I'm having a problem, which I can't figure out for the life of me. I've searched the internet, trying to understand Swifts's EXC_BAD_ACCESS, but nothing seemed to help.
The following code is quite long, but most of the time the comments are all the information needed to understand the item of relevance.
I have a class CalculatorController, which contains the following relevant methods and properties:
import UIKit
class CalculatorController: UIViewController {
// the actual `#IBOutlet` which is never accessed directly
#IBOutlet private weak var _mainDisplay: UILabel!
// an instance of `MainDisplayMicroController`
// holds a reference to `_mainDisplay`
// is used to manipulate `_mainDisplay` in a controlled way
private var mainDisplay: MainDisplayMicroController!
override func viewDidLoad() {
super.viewDidLoad()
// connects `mainDisplay` with `_mainDisplay`
mainDisplay = MainDisplayMicroController(label: _mainDisplay)
// sets `_mainDisplay`'s `text` property to "0"
mainDisplay.content = .Number(0)
//...
}
//...
}
In order to manage _mainDisplay in a certain way, I have created a class MainDisplayMicroController, which on the one hand contains a reference to the the UILabel itself, and on the other hand contains methods and properties, which perform actions on the UILabel:
import UIKit
class MainDisplayMicroController {
// used to express what `label.text` is currently showing
private enum DisplayState {
case ShowingNumber
case ShowingConstant
case ShowingErrorMessage
case Unknown
}
// holds the current state of what `label.text` is showing
private var state = DisplayState.Unknown
// used to pass different types of values in and out of this class
enum ContentType {
case Number(Double)
case Constant(String)
case ErrorMessage(String)
case Unknown(Any?)
}
// holds the reference to the label which is being manipulated/managed
private var label: UILabel?
// makes `label`'s `text` property directly accessible, as `label` is `private`
var text: String? {
get {
return label?.text
}
set {
label?.text = newValue
removeLeadingZeros()
transformToInteger()
}
}
// a property to allow controlled retrieval and manipulation of `label.text`
// uses `ContentType` to make clear what the information in `label.text` is/ is supposed to be
var content: ContentType {
get {
switch state {
case .ShowingNumber:
if let string = text {
if let value = NSNumberFormatter().numberFromString(string)?.doubleValue {
return .Number(value)
}
}
case .ShowingConstant:
if let symbol = text {
return .Constant(symbol)
}
case .ShowingErrorMessage:
if let message = text {
return .ErrorMessage(message)
}
default:
break
}
state = .Unknown
return .Unknown(text)
}
set {
switch newValue {
case .Number(let value):
text = "\(value)"
state = .ShowingNumber
removeLeadingZeros()
transformToInteger()
case .Constant(let symbol):
text = symbol
state = .ShowingConstant
case .ErrorMessage(let message):
text = message
state = .ShowingErrorMessage
case .Unknown(let thing):
text = "Error: Passed unknown value: \(thing)"
state = .ShowingErrorMessage
}
}
}
// removes the ".0" from `label.text`, if it is a whole number
private func transformToInteger() {
if state == .ShowingNumber {
switch content {
case .Number(let value):
if round(value) == value {
var doubleString = "\(value)"
if doubleString.rangeOfString("e") == nil {
dropLast(doubleString)
dropLast(doubleString)
}
text = doubleString
}
default:
break
}
}
}
// removes leading "0"s from `label.text` if they are redundant
private func removeLeadingZeros() {
if state == .ShowingNumber {
switch content {
case .Number(let displayedValue):
content = .Number(displayedValue)
default:
break
}
}
}
//...
}
Now, when I run the code I get the following error:
From what I've read on EXC_BAD_ACCESS, the error often occurs when trying to call methods on released objects. I've tried using NSZombieto check the issue, but I didn't find anything (probably due to my incompetence when using NSZombie).
If I try to follow what is happening by logic, I come to following conclusion:
mainDisplay is set successfully in viewDidLoad()
mainDisplay.content is called
in the content's setter the switch-statement executes the .Number case
text and state are successfully set
removeLeadingZeros() is called
the switch-statement accesses content's getter
the switch-statement in content's getter executes the .ShowingNumber case
the if-statements resolve to true, finally trying to evaluate the NSNumberFormatter expression
the EXC_BAD_ACCESS occurs
Does anyone know why this is happening? Does it have to do with me manipulating an #IBOutlet in a different class?
Any help is greatly appreciated!
Here are links to the complete CalculatorController and MainDisplayMicroController.
Update #1:
As #abdullah suggested I have tried directing the NSNumberFormatter expression in to multiple expressions. I still get the error though:
Update #2:
I've removed all references and external classes, to make it as simple as possible, while maintaining the same functionality.
All of the methods and properties defined in MainDisplayMicroController have been moved to CalculatorModel.
These methods and properties now access the original #IBOutlet, not any reference to it.
But still when trying to run it I get EXC_BAD_ACCESS(code=2) at the same line of code.
I'm just super confused, as it can't have anything to do with weird references, or objects being released too soon anymore.
Here's the complete code for the new CalculatorController.
Update #3:
I've removed the NSNumberFormatter line, by changing it to:
Now I get the following error though:
I assume there's some fundamental problem with the code, so I'm scrapping it. But thanks for all the help, and attempts at figuring this out.
Update #4:
This is what I get when adding a breakpoint on throw for all exceptions:
I don't really see anything in that line that can cause a crash. I suggest you do the following:
Make a clean build (clean, nuke your derived data folder, then build) and see if the crash persists
If the crash persists, set a breakpoint on throw for all exceptions to see which operation in the callstack caused the crash, and take it from there
#WarrenBurton is on to something.
Take your line that crashes out of your big class and run it in the playground and it works fine:
let string = "1.213"
if let value = NSNumberFormatter().numberFromString(string)?.doubleValue
{
println("value = \(value)")
}
Displays the result
value = 1.213
Where is your variable "string" defined in your class?
Notice that string is blue, like a keyword, not black, like other local variables.
I'd try local variable string ==> myString
just to know for sure.
Just 'cuz I was seeing the same thing and noticed no one had commented past your last edit (and maybe a fellow Googler for this issue will see this someday):
For both of our situations the issue is infinite recursion - we're calling a method from itself infinitely. That's the bug. The implication in the crash of NSNumberFormatter is a red herring.