The main problem that a programmer encounters in the implementation of a control element is the alignment of the correct logic of the operation of this element.
Research problem
As defined in the documentation, UIControl is a class that implements a common behavior for visual elements that are able to respond in a certain way to user actions. So, change the visual presentation, behavior, initiate processes, etc. What you need to have and how to implement it? There is an obvious answer to the first question - the states and the logic of transitions between them. The second question is a bit more complicated ...
Solution to the problem
Many ingenuous developers end up creating a method that is usually called update () and writing the epic in the subjunctive mood in it, to put it simply:
if ... { element1.property = value1 ... } else if ... { element1.property = value2 ... } ...
It still did not go anywhere, the code is consistent and readable. But, if a fashionable grenade comes across to a
monkey , everything ends in a pitiable still:
')
RAC(element1, hidden) = [RACSignal combineLatest:@[ self.textField1.rac_textSignal ] reduce:^(NSString *password) { return @((!password.length >= 1)); }]; RAC(element2, hidden) = [RACSignal combineLatest:@[ self.textField1.rac_textSignal ] reduce:^(NSString *password) { return @(!(password.length >= 2)); }]; RAC(element3, hidden) = [RACSignal combineLatest:@[ self.textField1.rac_textSignal ] reduce:^(NSString *password) { return @(!(password.length >= 3)); }]; RAC(element4, hidden) = [RACSignal combineLatest:@[ self.textField1.rac_textSignal ] reduce:^(NSString *password) { if(password.length == PIN_LENGTH) { [self activateNextField]; return @(NO); } else return @(YES); }];
And the more states, the stronger it all looks like endless circles of hell.
Solution out of the box
UIControl and its successors, respectively, use the following state update mechanism:
The first thing that the update method does is read the current state, and more specifically the UIControlState state property. It is a bitmask of the state units described in enum UIControlState. It is important to note, and how many mistakenly do, that this property should be
calculated , not stored. Those. the real state of the object should form the description of this state, and not vice versa.
Further, the values associated with the obtained state are pulled out of the container and are applied.
The state update process is initiated after a change in the state factor. For example, in a boolean variable:
open var isEnabled: Bool { didSet { if oldValue != isEnabled {
UIControlState has a reserved bitmask portion for creating additional states - UIControlStateApplication. If you want to add a state for any system control, then you can choose any value from this interval.
extension UIControlState { static let custom = UIControlState(rawValue: 1 << 16) } let button = UIButton(type: .custom) let title = "Title for custom state" button.setTitle(title, for: .custom) button.title(for: .custom) == title
But for some reason, the Apple developers, giving us the opportunity to create our own states, did not provide an API to manage them.
My case
My task was to implement the control for entering the pin-code. The task is rather trivial, so you try to complicate it.
Given the above problem, I decided to write something that Apple did not declare in a public interface, and maybe a little more)
So I created a class add-on over UIControl - QUIckControl. It provides the ability to set values for a specific state (or a set of states) for a specific object.
func setValue(_ value: Any?, forTarget: NSObject, forKeyPath: String, for: QUICState)
As can be seen from the semantics of the method, it is based on KVC. The problem of validating keys in swift 3 is already solved, and in ObjC it is easily solved by adding define macros.
Before setting the value for the user state, this state must be registered using the method:
func register(_ state: UIControlState, forBoolKeyPath keyPath: String, inverted: Bool)
If your control has entered the state for which you did not set values, then the default value will be applied. The default value is determined at the time of first setting the value for a particular key. Formally, you can redefine it using the state .normal in partial compliance mode (see below), since .normal is contained absolutely in any condition.
In order to simplify the configuration of states and not duplicate values, a QUICState state description structure was created. Now it contains 6 modes for assessing compliance with the current state:
- full compliance mode
- partial matching mode
- nonconformity mode
- matching mode at least one state unit
- total nonconformity mode
- user defined mode
Each mode has its own priority, to determine the primary value in the case of multiple compliance.
Since the state is updated immediately after changing the state factor (boolean variable), the possibility of making multiple transitions, without immediately applying the changes, is created:
func beginTransition() // func endTransition() // func commitTransition() // func performTransition(withCommit commit: Bool = default, transition: () -> Void) //
Conclusion
This API allows you to quickly set up states and create dependencies between control `s and not only:
control.setValue(true, forTarget: otherControl, forKeyPath: "enabled", forAllStatesContained: [.filled, .valid])
What, for example, is a frequent use case for input forms.
As a result, I got what I wanted, but with elements of reactivity. Automatic programming is rather inconvenient, but with the right approach reliable enough. No wonder this programming style is used from games and controllers to various analyzers and AI.
→ Implementation of PinCodeControl and all the code can be viewed
here .