keyDown doesn't update NSTextView immediately - swift

I have a keyDown function in my application that is used to capture input from a NSTextView named textInput. Some conversions are done with the input which is appended as a NSAttributedString back into the NSTextView.
This works fine currently, but the problem I have is that the value entered into the textbox on keyDown doesn't get added to the textInput.textStorage?.string, until another key is pressed.
For example if I enter the text abcde and nothing more into textInput, and then inside func keyDown() I try to access textInput.textStorage?.string, it will return abcd.
Here is the function without unnecessary parts:
override func keyDown(with event: NSEvent) {
let bottomBox = textInput.textStorage?.string // This returns one character short of what is actually in the text box
if let bottomBox = bottomBox {
var attribute = NSMutableAttributedString(string: bottomBox)
// Do some stuff here with bottomBox and attribute
// Clear and set attributed string
textInput.textStorage?.mutableString.setString("")
textInput.textStorage?.append(attribute)
}
}
If I were to use keyUp, this isn't a problem, although the problem with keyUp is that if the user holds down the key, the attributes on the NSAttributedString don't get set until the user releases the key.
I though maybe there was a way to programatically release the keyDown event during the keyDown function, or generate a keyUp event, but can't seem to find anything.
Is there a way to fix this?

What I like to do is to use Cocoa Bindings with a property observer. Set up your properties like so:
class MyViewController: NSViewController {
#objc dynamic var textInput: String {
didSet { /* put your handler here */ }
}
// needed because NSTextView only has an "Attributed String" binding
#objc private static let keyPathsForValuesAffectingAttributedTextInput: Set<String> = [
#keyPath(textInput)
]
#objc private var attributedTextInput: NSAttributedString {
get { return NSAttributedString(string: self.textInput) }
set { self.textInput = newValue.string }
}
}
Now bind your text view to attributedTextInput with the "Continuously Updates Value" check box checked:
Et voilà, your property will be immediately updated every time you type a character, and your property's didSet will immediately be called.

Related

Can you save the selection of a master-detail bound NSTableView?

I have a manager class for my data which is configured by two properties, one to set to a category and another to select items which correspond with that category. Based on that it will expose the relevant pieces of data. I am using a couple of different forms or making those selections, including a pair of IndexSets.
My problem is that I would also like to be able to save the selected items for each category, so that whenever the category is changed the items previously selected for it are restored. This is easy to achieve when accessed programmatically, but using bindings to allow a view in a macOS app to be able to provide that configuration unfortunately does not work properly
Changing the category causes the object bound to its selection to empty or 'preserve' the selected items before the category is actually updated. So the actual selection gets overwritten with, with noway I can see to tell the difference between this behaviour and a user action.
Here are the test code I have used for experimenting, with viewDidLoad generating some random test data to roughly mimic the structure o the real class. This does not attempt to save or restore the selection, but simply shows the overwriting behaviour.
class Thing: NSObject {
#objc dynamic var name: String
required init(name: String) {
self.name = name
}
}
class Stuff: NSObject {
#objc dynamic var name: String
#objc dynamic var things: [Thing]
required init(name: String, things: [Thing]) {
self.name = name
self.things = things
}
}
class StuffManager: NSObject {
#objc dynamic var stuff = [Stuff]()
#objc dynamic var stuffIndex = IndexSet() {
didSet {
print("STUFF: ", Array(stuffIndex))
}
}
#objc dynamic var things = [Thing]()
#objc dynamic var thingsIndex = IndexSet() {
didSet {
print("THING: ", Array(thingsIndex))
}
}
}
class ViewController: NSViewController {
#objc dynamic var stuffManager = StuffManager()
override func viewDidLoad() {
super.viewDidLoad()
(1...10).forEach { stuffManager.things.append(Thing(name: "Thing \($0)")) }
(1...9).forEach {
let randomThings = Array(stuffManager.things.shuffled()[0...Int.random(in: 0..<10)])
stuffManager.stuff.append(Stuff(name: "Collection \($0)", things: randomThings))
}
stuffManager.stuff.append(Stuff(name: "Collection 10", things: []))
}
}
In Interface Builder I have a view containing an NSPopButton to select the Stuff, a multiple selection NSTableView to select the Things, and a pair of NSArrayControllers for each. The bindings are:
Stuff Array Controller
Content Array:
Binding to: ViewController, Model Key Path: stuffManager.stuff
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.stuffIndex
Things Array Controller
Content Array:
Binding to: Stuff Array Controller, Controller Key: Selection, Model Key Path: things
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.thingIndex
The two interface objects are bound to these controllers in the standard way, the Content to the arrangedObjects and the Selection Indexes to the selectionIndexes of their respective array controller.
What this test code shows is that when the value in the popup button is changed the THING debug line appears before the STUFF debug line, that is it changes the selection of Things before it changes the Stuff. So any code in the property observer on stuffManager.things to save the new selection will save this change before being aware that the Stuff has changed.
Obviously this behaviour is to avoid the selection being made incorrect by the change to the content, or worse selecting out of bounds if the new content is shorter. But is there any way to detect when this is happening, rather than a user changing the selection? Or a way to override it to gain manual control over the process rather than having to accept the default behaviour of 'Preserve Selection' or the selection being cancelled if that option is disabled?
And what makes it more awkward is if this behaviour only occurs when the selection would change. If the selected Things exist for the new Stuff, or if nothing is selected, then nothing happens to trigger the property observer. Again this is understandable, but it prevents being able to cache the change and then only save the previous one if the Stuff has not changed.
I did wonder if using a separate IndexSet for each Stuff would avoid this problem, because then there would be no need for the NSTableView to manage the selection. I do not like the idea of keeping an IndexSet in the model but would accept it if it worked. But it does not. Again understandable, because the table view has no idea the Selection Indexes binding will be changed. Unless I am missing something?
But I tested this by updating the Stuff class to include the following:
#objc dynamic var selected = IndexSet() {
didSet {
print("THING: ", Array(selected))
}
}
Then changing the Selection Indexes binding of the Things Array Controller to:
Binding to: Stuff Array Controller, Controller Key: selection, Model Key Path: selected
Is what I am trying to achieve impossible? I would not have thought it that strange a thing to want to do, to save and restore a selection, but it seems impossible with bindings.
The only solution I can see is to forgo the master-detail style pattern and instead just maintain a separate [Thing] property in my data manager class, bind the Things Array Controller to this (or even just bind the table directly to the property), then whenever the popup button changes update the new property to match the stuff object.
Something like this in the StuffManager, with the table content bound to availableThings:
#objc dynamic var stuffIndex = IndexSet() {
didSet {
print("STUFF: ", Array(stuffIndex))
availableThings = stuff[stuffIndex.first!].things
}
}
#objc dynamic var availableThings = [Thing]()
It appears there is no way to prevent the NSTableView behaviour of automatically resetting its selection when the content changes. Nor any way to detect when this is happening, as it updates this before updating the selection on the NSPopupButton having changed. So here is how I have written the StuffManager class, adding a property for binding to the tableview so I can control the content changing:
class StuffManager: NSObject {
let defaults: UserDefaults = .standard
var canSaveThingsIndex = true
#objc dynamic var stuff = [Stuff]()
#objc dynamic var stuffIndex = IndexSet() {
didSet {
canSaveThingsIndex = false
if stuffIndex.count > 0 {
availableThings = stuff[stuffIndex.first!].things
let thing = stuff[stuffIndex.first!].name
if let items = defaults.object(forKey: thing) as? [Int] {
thingsIndex = IndexSet(items)
} else if availableThings.count > 0 {
thingsIndex = IndexSet(0..<availableThings.count)
} else {
thingsIndex.removeAll()
}
} else {
availableThings.removeAll()
thingsIndex.removeAll()
}
canSaveThingsIndex = true
}
}
#objc dynamic var things = [Thing]()
#objc dynamic var availableThings = [Thing]()
#objc dynamic var thingsIndex = IndexSet() {
didSet {
if canSaveThingsIndex && stuffIndex.count > 0 {
let thing = stuff[stuffIndex.first!].name
defaults.set(Array(thingsIndex), forKey: thing)
}
}
}
}
The Things Array Controller is now bound as:
Content Array:
Binding to: ViewController, Model Key Path: stuffManager.availableThings
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.thingsIndex
Though without being able to use the master-detail benefits of an NSArrayController they are not needed. Both the NSPopupButton and NSTableView can be bound directly to the StuffManager. And this allows the NSPopupButton's Selected Index can be bound to an Int int he Stuff Manager rather than needing to use an IndexSet despite multiple selections being impossible.
The main feature of the workaround is that because I am manually changing the content I can use the canSaveThingsIndex flag before changing the NSTableView content. So whenever its natural behaviour triggers the thingsIndex property observer, this can be ignored to prevent it overwriting the user's selection. It also avoids the unnecessary saving of a selection immediately after being restored.

Is there a way to pass #Published object as a func argument UIKit?

I'm using UIKit not SwiftUI. I found solutions which all in SwiftUI but not solved my problem.
I have a #Published object like:
#Published var searchText = ""
I'm using that #Published object for search functionality like in following function. Also, I'm trying to reach that function outside of corresponding class which is final class MainViewModel
final class MainViewModel {
#Published var searchText = ""
//subscribes to the searchText 'Publisher'
func searchTextManipulation(searchText: String) {
$searchText
.debounce(for: .seconds(1.0), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] (text) in //(text) is always empty string ""
...
I want to use that function parameter searchText: String as $searchText to search with text come from another screen something like:
final class MainViewController {
let viewModel = MainViewModel()
#Published var searchText = ""
override func viewDidLoad() {
viewModel.searchTextManipulation($searchText: searchText) //but it's not possible.
}
}
Can you show me a workaround for solving that crucial part?
Your code suggests that need to understand how Combine works a little bit better.
When you put together a publisher with a set of operators:
$searchText
.debounce(for: .seconds(1.0), scheduler: RunLoop.main)
.removeDuplicates()
.sink { text in doSomething(text) }
It's like you're putting together a set of pipes. You only have to put them together once. When they are assembled, and you put something into the input side, the pipes will transform the value and deliver it to the output side. If you subscribe to the output side, using sink or assign for example, then you can catch the transformed value and do something with it.
But you only need to build the pipeline once.
After you build the pipeline, you also have to keep ahold of it.
Your searchTextManipulation function is building a pipeline that is immediately destroyed when you leave the function. To prevent that you have to store a subscription (in the form of an AnyCancellable). sink and assign return subscriptions and you should retain those to keep the pipeline from being destroyed.
An #Published property creates a Publisher, with the same name and a $ prefix, that emits a value when the property is initialized or changed. In other words, when you change searchText the system will put the value into the input of a pipeline named $searchText for you.
So a complete example of what it looks like you are trying to do is the playground below.
MainViewModel has two published properties. One is searchText and the other is searchResults. We set up the model so that when searchText changes (with a debounce and uniq) a new value of searchResults is published.
In init we build a pipeline that starts with $searchText. Through the pipeline we transform a search string into an array of search results. (The idea that each step in a pipeline transforms a value from one thing to another is the model I use in my head to decide how to chain operators.)
The assign at the end of the pipeline takes the transformed result and assigns it to the searchResults property.
Note: To ensure our pipeline is not destroyed when init ends, we have to capture and store the subscription done by assign.
With that pipeline in place, whenever code changes searchText the pipeline transform that value into an array of search results, and store the result in searchResults. Since MainViewModel owns the subscription, the pipeline will only be destroyed when the view model instance is destroyed.
So how do you use it? Well, the searchResults get published to a pipeline named $searchResults whenever they change. So all you have to do is listen that pipeline for changes.
In MainViewController we want to listen for those changes. In viewDidLoad we set up a short pipeline that starts with $searchResults, drops the first result (it's sent when searchResults is initialized) and uses sink` to print the results to the console.
Again note: we are constructing the pipeline once, keeping track of it by storing the subscription, and it will run automatically every time a new value is put into the seachResults.
I added a function called userDoesSomething that should be called whenever you want to kick off the search process.
userDoesSomething assigns some text value that came from the user to the model's searchText property.
Because it's #Published, the system sends the new value into the $searchText pipeline created by the view model's init function.
The pipeline transforms the text into and array of search results and assigns the array to searchResults
Because searchResults is #Published it sends the array into the pipeline called $searchResults
The MainViewController created a pipeline to listen to $searchResults and that pipeline runs with the new results.
That pipeline sends the value to a sink that simply prints it to the console.
The bit at the very end of the playground simulates a user sending a string with each of the letters in the English alphabet ('a'-'z') once every 0.2 seconds to userDoesSomething. That kicks of the numbered sequence I described above and gives you something interesting to look at on the Playground console.
import UIKit
import Combine
import PlaygroundSupport
let TheItems = ["The", "Quick", "Brown", "Fox", "Jumped", "Over", "the", "Lazy", "Dogs"]
final class MainViewModel {
#Published var searchText = ""
#Published private(set) var searchResults = []
var subscription: AnyCancellable! = nil
init() {
self.subscription = $searchText
.debounce(for: .seconds(0.1), scheduler: RunLoop.main)
.removeDuplicates()
.map(findItems)
.assign(to: \.searchResults, on: self)
}
}
private func findItems(searchText: String) -> [String] {
if let firstChar = searchText.lowercased().first {
return TheItems.filter { $0.lowercased().contains(firstChar) }
}
return []
}
final class MainViewController: UIViewController {
var searchResultsSubscription : AnyCancellable! = nil
let viewModel = MainViewModel()
override func loadView() {
self.view = UIView(frame: CGRect(x: 0,y: 0,width: 200,height: 200))
}
override func viewDidLoad() {
searchResultsSubscription = viewModel.$searchResults
.dropFirst()
.sink{
// do what you like here when search results change
debugPrint($0)
}
}
func userDoesSomething(generates searchText: String) {
viewModel.searchText = searchText;
}
}
let model = MainViewModel()
let controller = MainViewController()
PlaygroundSupport.PlaygroundPage.current.liveView = controller
(UInt8(ascii: "a")...UInt8(ascii:"z"))
.enumerated()
.publisher
.sink { index, char in
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200 * index)) {
controller.userDoesSomething(generates: String(UnicodeScalar(char)))
}
}
Yes, you can pass a #Published wrapper around, by changing the function signature to expect the projected value of the property wrapper:
func searchTextManipulation(searchText: Published<String>.Publisher) {
...
}
, and later in your viewDidLoad just use the $:
override func viewDidLoad() {
viewModel.searchTextManipulation(searchText: $searchText)
}
Everytime you write $someVar, what the compiler does is to de-sugar it into _someVar.projectedValue, and in case of Published, the projected value is of type Published<T>.Publisher. This is not something specific to SwiftUI, and it's part of the language, so you can freely use it in any parts of your code (assuming it makes sense, ofcourse).

os x nstextfield validation

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.

Intercept setObject functions on class properties in Swift

I have the line var user = BRUser() at the top of my Swift file class BRProfileTableViewCell.
BRUser is a struct with details such as fullName, etc. I set the user value in cellForRowAtIndexPath in my table view controller.
How could I intercept the setUser function to set the label text of the table view cell to the fullName of the user?
It sounds like you don't need to intercept anything, but rather you need to observe changes in the property, and for that, you want Swift's property observers:
var user: BRUser = BRUser() {
willSet {
// do something just before user is set
}
didSet {
// do something just after user is set
}
}

What is the purpose of willSet and didSet in Swift?

Swift has a property declaration syntax very similar to C#'s:
var foo: Int {
get { return getFoo() }
set { setFoo(newValue) }
}
However, it also has willSet and didSet actions. These are called before and after the setter is called, respectively. What is their purpose, considering that you could just have the same code inside the setter?
The point seems to be that sometimes, you need a property that has automatic storage and some behavior, for instance to notify other objects that the property just changed. When all you have is get/set, you need another field to hold the value. With willSet and didSet, you can take action when the value is modified without needing another field. For instance, in that example:
class Foo {
var myProperty: Int = 0 {
didSet {
print("The value of myProperty changed from \(oldValue) to \(myProperty)")
}
}
}
myProperty prints its old and new value every time it is modified. With just getters and setters, I would need this instead:
class Foo {
var myPropertyValue: Int = 0
var myProperty: Int {
get { return myPropertyValue }
set {
print("The value of myProperty changed from \(myPropertyValue) to \(newValue)")
myPropertyValue = newValue
}
}
}
So willSet and didSet represent an economy of a couple of lines, and less noise in the field list.
My understanding is that set and get are for computed properties (no backing from stored properties)
if you are coming from an Objective-C bare in mind that the naming conventions have changed. In Swift an iVar or instance variable is named stored property
Example 1 (read only property) - with warning:
var test : Int {
get {
return test
}
}
This will result in a warning because this results in a recursive function call (the getter calls itself).The warning in this case is "Attempting to modify 'test' within its own getter".
Example 2. Conditional read/write - with warning
var test : Int {
get {
return test
}
set (aNewValue) {
//I've contrived some condition on which this property can be set
//(prevents same value being set)
if (aNewValue != test) {
test = aNewValue
}
}
}
Similar problem - you cannot do this as it's recursively calling the setter.
Also, note this code will not complain about no initialisers as there is no stored property to initialise.
Example 3. read/write computed property - with backing store
Here is a pattern that allows conditional setting of an actual stored property
//True model data
var _test : Int = 0
var test : Int {
get {
return _test
}
set (aNewValue) {
//I've contrived some condition on which this property can be set
if (aNewValue != test) {
_test = aNewValue
}
}
}
Note The actual data is called _test (although it could be any data or combination of data)
Note also the need to provide an initial value (alternatively you need to use an init method) because _test is actually an instance variable
Example 4. Using will and did set
//True model data
var _test : Int = 0 {
//First this
willSet {
println("Old value is \(_test), new value is \(newValue)")
}
//value is set
//Finaly this
didSet {
println("Old value is \(oldValue), new value is \(_test)")
}
}
var test : Int {
get {
return _test
}
set (aNewValue) {
//I've contrived some condition on which this property can be set
if (aNewValue != test) {
_test = aNewValue
}
}
}
Here we see willSet and didSet intercepting a change in an actual stored property.
This is useful for sending notifications, synchronisation etc... (see example below)
Example 5. Concrete Example - ViewController Container
//Underlying instance variable (would ideally be private)
var _childVC : UIViewController? {
willSet {
//REMOVE OLD VC
println("Property will set")
if (_childVC != nil) {
_childVC!.willMoveToParentViewController(nil)
self.setOverrideTraitCollection(nil, forChildViewController: _childVC)
_childVC!.view.removeFromSuperview()
_childVC!.removeFromParentViewController()
}
if (newValue) {
self.addChildViewController(newValue)
}
}
//I can't see a way to 'stop' the value being set to the same controller - hence the computed property
didSet {
//ADD NEW VC
println("Property did set")
if (_childVC) {
// var views = NSDictionaryOfVariableBindings(self.view) .. NOT YET SUPPORTED (NSDictionary bridging not yet available)
//Add subviews + constraints
_childVC!.view.setTranslatesAutoresizingMaskIntoConstraints(false) //For now - until I add my own constraints
self.view.addSubview(_childVC!.view)
let views = ["view" : _childVC!.view] as NSMutableDictionary
let layoutOpts = NSLayoutFormatOptions(0)
let lc1 : AnyObject[] = NSLayoutConstraint.constraintsWithVisualFormat("|[view]|", options: layoutOpts, metrics: NSDictionary(), views: views)
let lc2 : AnyObject[] = NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: layoutOpts, metrics: NSDictionary(), views: views)
self.view.addConstraints(lc1)
self.view.addConstraints(lc2)
//Forward messages to child
_childVC!.didMoveToParentViewController(self)
}
}
}
//Computed property - this is the property that must be used to prevent setting the same value twice
//unless there is another way of doing this?
var childVC : UIViewController? {
get {
return _childVC
}
set(suggestedVC) {
if (suggestedVC != _childVC) {
_childVC = suggestedVC
}
}
}
Note the use of BOTH computed and stored properties. I've used a computed property to prevent setting the same value twice (to avoid bad things happening!); I've used willSet and didSet to forward notifications to viewControllers (see UIViewController documentation and info on viewController containers)
If I've made a mistake anywhere, please edit to fix it!
You can also use the didSet to set the variable to a different value. This does not cause the observer to be called again as stated in Properties guide. For example, it is useful when you want to limit the value as below:
let minValue = 1
var value = 1 {
didSet {
if value < minValue {
value = minValue
}
}
}
value = -10 // value is minValue now.
These are called Property Observers:
Property observers observe and respond to changes in a property’s
value. Property observers are called every time a property’s value is
set, even if the new value is the same as the property’s current
value.
Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/ca/jEUH0.l
I suspect it's to allow for things we would traditionally do with KVO such as data binding with UI elements, or triggering side effects of changing a property, triggering a sync process, background processing, etc, etc.
NOTE
willSet and didSet observers are not called when a property is set in an initializer before delegation takes place
The many well-written existing answers cover the question well, but I'll mention, in some detail, an addition that I believe is worth covering.
The willSet and didSet property observers can be used to call delegates, e.g., for class properties that are only ever updated by user interaction, but where you want to avoid calling the delegate at object initialization.
I'll cite Klaas up-voted comment to the accepted answer:
willSet and didSet observers are not called when a property is first
initialized. They are only called when the property’s value is set
outside of an initialization context.
This is a quite neat as it means e.g. the didSet property is a good choice of launch point for delegate callbacks & functions, for your own custom classes.
As an example, consider some custom user control object, with some key property value (e.g. position in rating control), implemented as a subclass of UIView:
// CustomUserControl.swift
protocol CustomUserControlDelegate {
func didChangeValue(value: Int)
// func didChangeValue(newValue: Int, oldValue: Int)
// func didChangeValue(customUserControl: CustomUserControl)
// ... other more sophisticated delegate functions
}
class CustomUserControl: UIView {
// Properties
// ...
private var value = 0 {
didSet {
// Possibly do something ...
// Call delegate.
delegate?.didChangeValue(value)
// delegate?.didChangeValue(value, oldValue: oldValue)
// delegate?.didChangeValue(self)
}
}
var delegate: CustomUserControlDelegate?
// Initialization
required init?(...) {
// Initialise something ...
// E.g. 'value = 1' would not call didSet at this point
}
// ... some methods/actions associated with your user control.
}
After which your delegate functions can be used in, say, some view controller to observe key changes in the model for CustomViewController, much like you'd use the inherent delegate functions of the UITextFieldDelegate for UITextField objects (e.g. textFieldDidEndEditing(...)).
For this simple example, use a delegate callback from the didSet of the class property value to tell a view controller that one of it's outlets have had associated model update:
// ViewController.swift
Import UIKit
// ...
class ViewController: UIViewController, CustomUserControlDelegate {
// Properties
// ...
#IBOutlet weak var customUserControl: CustomUserControl!
override func viewDidLoad() {
super.viewDidLoad()
// ...
// Custom user control, handle through delegate callbacks.
customUserControl = self
}
// ...
// CustomUserControlDelegate
func didChangeValue(value: Int) {
// do some stuff with 'value' ...
}
// func didChangeValue(newValue: Int, oldValue: Int) {
// do some stuff with new as well as old 'value' ...
// custom transitions? :)
//}
//func didChangeValue(customUserControl: CustomUserControl) {
// // Do more advanced stuff ...
//}
}
Here, the value property has been encapsulated, but generally: in situations like these, be careful not to update the value property of the customUserControl object in the scope of the associated delegate function (here: didChangeValue()) in the view controller, or you'll end up with infinite recursion.
The willSet and didSet observers for the properties whenever the property is assigned a new value. This is true even if the new value is the same as the current value.
And note that willSet needs a parameter name to work around, on the other hand, didSet does not.
The didSet observer is called after the value of property is updated. It compares against the old value. If the total number of steps has increased, a message is printed to indicate how many new steps have been taken. The didSet observer does not provide a custom parameter name for the old value, and the default name of oldValue is used instead.
Getter and setter are sometimes too heavy to implement just to observe proper value changes. Usually this needs extra temporary variable handling and extra checks, and you will want to avoid even those tiny labour if you write hundreds of getters and setters. These stuffs are for the situation.
In your own (base) class, willSet and didSet are quite reduntant , as you could instead define a calculated property (i.e get- and set- methods) that access a _propertyVariable and does the desired pre- and post- prosessing.
If, however, you override a class where the property is already defined, then the willSet and didSet are useful and not redundant!
One thing where didSet is really handy is when you use outlets to add additional configuration.
#IBOutlet weak var loginOrSignupButton: UIButton! {
didSet {
let title = NSLocalizedString("signup_required_button")
loginOrSignupButton.setTitle(title, for: .normal)
loginOrSignupButton.setTitle(title, for: .highlighted)
}
I do not know C#, but with a little guesswork I think I understand what
foo : int {
get { return getFoo(); }
set { setFoo(newValue); }
}
does. It looks very similar to what you have in Swift, but it's not the same: in Swift you do not have the getFoo and setFoo. That is not a little difference: it means you do not have any underlying storage for your value.
Swift has stored and computed properties.
A computed property has get and may have set (if it's writable). But the code in the getter and setter, if they need to actually store some data, must do it in other properties. There is no backing storage.
A stored property, on the other hand, does have backing storage. But it does not have get and set. Instead it has willSet and didSet which you can use to observe variable changes and, eventually, trigger side effects and/or modify the stored value. You do not have willSet and didSet for computed properties, and you do not need them because for computed properties you can use the code in set to control changes.