cdts/xdts-ios 3/TreeHole/Code/Utility/WLUnitField/WLUnitField.m
2023-07-27 09:20:00 +08:00

775 lines
25 KiB
Objective-C

//
// WLUnitField.m
// WLUnitField
//
// Created by wayne on 16/11/22.
// Copyright © 2016年 wayne. All rights reserved.
//
#import "WLUnitField.h"
#import "WLUnitFieldTextRange.h"
#ifdef NSFoundationVersionNumber_iOS_9_x_Max
NSNotificationName const WLUnitFieldDidBecomeFirstResponderNotification = @"WLUnitFieldDidBecomeFirstResponderNotification";
NSNotificationName const WLUnitFieldDidResignFirstResponderNotification = @"WLUnitFieldDidResignFirstResponderNotification";
#else
NSString *const WLUnitFieldDidBecomeFirstResponderNotification = @"WLUnitFieldDidBecomeFirstResponderNotification";
NSString *const WLUnitFieldDidResignFirstResponderNotification = @"WLUnitFieldDidResignFirstResponderNotification";
#endif
@interface WLUnitField ()
@property (nonatomic, strong) NSMutableArray <NSString*>*characterArray;
@property (nonatomic, strong) CALayer *cursorLayer;
@end
@implementation WLUnitField
{
UIColor *mBackgroundColor;
CGContextRef mCtx;
NSString *mMarkedText;
}
@dynamic text;
@synthesize textContentType = _textContentType;
@synthesize secureTextEntry = _secureTextEntry;
@synthesize enablesReturnKeyAutomatically = _enablesReturnKeyAutomatically;
@synthesize keyboardType = _keyboardType;
@synthesize returnKeyType = _returnKeyType;
@synthesize autocapitalizationType = _autocapitalizationType;
@synthesize autocorrectionType = _autocorrectionType;
@synthesize inputDelegate = _inputDelegate;
@synthesize selectedTextRange = _selectedTextRange;
@synthesize markedTextStyle = _markedTextStyle;
@synthesize tokenizer = _tokenizer;
#pragma mark - Life
- (instancetype)initWithInputUnitCount:(NSUInteger)count {
return [self initWithStyle:WLUnitFieldStyleBorder inputUnitCount:count];
}
- (instancetype)initWithStyle:(WLUnitFieldStyle)style inputUnitCount:(NSUInteger)count {
if (self = [super initWithFrame:CGRectZero]) {
NSCAssert(count > 0, @"WLUnitField must have one or more input units.");
NSCAssert(count <= 8, @"WLUnitField can not have more than 8 input units.");
_style = style;
_inputUnitCount = count;
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
_inputUnitCount = 4;
[self initialize];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
_inputUnitCount = 4;
[self initialize];
}
return self;
}
- (void)initialize {
[self setBackgroundColor:[UIColor clearColor]];
self.opaque = NO;
_characterArray = [NSMutableArray array];
_secureTextEntry = NO;
_unitSpace = 12;
_unitSize = CGSizeMake(44, 44);
_borderRadius = 0;
_borderWidth = 1;
_textFont = [UIFont systemFontOfSize:22];
_keyboardType = UIKeyboardTypeNumberPad;
_returnKeyType = UIReturnKeyDone;
_enablesReturnKeyAutomatically = YES;
_autoResignFirstResponderWhenInputFinished = NO;
_textColor = [UIColor darkGrayColor];
_tintColor = [UIColor lightGrayColor];
_trackTintColor = [UIColor orangeColor];
_cursorColor = [UIColor orangeColor];
mBackgroundColor = mBackgroundColor ?: [UIColor clearColor];
_autocorrectionType = UITextAutocorrectionTypeNo;
_autocapitalizationType = UITextAutocapitalizationTypeNone;
self.cursorLayer.backgroundColor = _cursorColor.CGColor;
WLUnitFieldTextPosition *point = [WLUnitFieldTextPosition positionWithOffset:0];
UITextRange *aNewRange = [WLUnitFieldTextRange rangeWithStart:point end:point];
[self setSelectedTextRange:aNewRange];
if (@available(iOS 12.0, *)) {
_textContentType = UITextContentTypeOneTimeCode;
}
[self.layer addSublayer:self.cursorLayer];
[self setNeedsDisplay];
}
#pragma mark - Property
- (NSString *)text {
if (_characterArray.count == 0) return nil;
return [_characterArray componentsJoinedByString:@""];
}
- (void)setText:(NSString *)text {
[_characterArray removeAllObjects];
[text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
if (self.characterArray.count < self.inputUnitCount)
[self.characterArray addObject:substring];
else
*stop = YES;
}];
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
/**
Supporting iOS12 SMS verification code, setText will be called when verification code input.
*/
if (_characterArray.count >= _inputUnitCount) {
if (_autoResignFirstResponderWhenInputFinished == YES) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self resignFirstResponder];
}];
}
[_inputDelegate textDidChange:self];
return;
}
}
- (CALayer *)cursorLayer {
if (!_cursorLayer) {
_cursorLayer = [CALayer layer];
_cursorLayer.hidden = YES;
_cursorLayer.opacity = 1;
mMarkedText = nil;
CABasicAnimation *animate = [CABasicAnimation animationWithKeyPath:@"opacity"];
animate.fromValue = @(0);
animate.toValue = @(1.5);
animate.duration = 0.5;
animate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animate.autoreverses = YES;
animate.removedOnCompletion = NO;
animate.fillMode = kCAFillModeForwards;
animate.repeatCount = HUGE_VALF;
[_cursorLayer addAnimation:animate forKey:nil];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self layoutIfNeeded];
self.cursorLayer.position = CGPointMake(CGRectGetWidth(self.bounds) / self.inputUnitCount / 2, CGRectGetHeight(self.bounds) / 2);
}];
}
return _cursorLayer;
}
- (void)setSecureTextEntry:(BOOL)secureTextEntry {
_secureTextEntry = secureTextEntry;
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
#if TARGET_INTERFACE_BUILDER
- (void)setInputUnitCount:(NSUInteger)inputUnitCount {
inputUnitCount = MAX(1, MIN(8, inputUnitCount));
_inputUnitCount = inputUnitCount;
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
- (void)setStyle:(NSUInteger)style {
_style = style;
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
#endif
- (void)setUnitSpace:(NSUInteger)unitSpace {
if (unitSpace < 2) unitSpace = 0;
_unitSpace = unitSpace;
[self _resize];
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
- (void)setTextFont:(UIFont *)textFont {
if (textFont == nil) {
_textFont = [UIFont systemFontOfSize:22];
} else {
_textFont = textFont;
}
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
- (void)setTextColor:(UIColor *)textColor {
if (textColor == nil) {
_textColor = [UIColor blackColor];
} else {
_textColor = textColor;
}
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
- (void)setBorderRadius:(CGFloat)borderRadius {
if (borderRadius < 0) return;
_borderRadius = borderRadius;
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
- (void)setBorderWidth:(CGFloat)borderWidth {
if (borderWidth < 0) return;
_borderWidth = borderWidth;
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
- (void)setBackgroundColor:(UIColor *)backgroundColor {
mBackgroundColor = backgroundColor;
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
- (void)setTintColor:(UIColor *)tintColor {
_tintColor = tintColor;
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
- (void)setTrackTintColor:(UIColor *)trackTintColor {
_trackTintColor = trackTintColor;
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
- (void)setCursorColor:(UIColor *)cursorColor {
_cursorColor = cursorColor;
_cursorLayer.backgroundColor = _cursorColor.CGColor;
[self _resetCursorStateIfNeeded];
}
- (void)setUnitSize:(CGSize)unitSize {
_unitSize = unitSize;
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
}
#pragma mark- Event
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self becomeFirstResponder];
}
#pragma mark - Override
- (CGSize)intrinsicContentSize {
return CGSizeMake(_inputUnitCount * (_unitSize.width + _unitSpace) - _unitSpace ,
_unitSize.height);
}
- (CGSize)sizeThatFits:(CGSize)size {
return [self intrinsicContentSize];
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (BOOL)becomeFirstResponder {
BOOL result = [super becomeFirstResponder];
[self _resetCursorStateIfNeeded];
if (result == YES) {
[self sendActionsForControlEvents:UIControlEventEditingDidBegin];
[[NSNotificationCenter defaultCenter] postNotificationName:WLUnitFieldDidBecomeFirstResponderNotification object:nil];
}
return result;
}
- (BOOL)canResignFirstResponder {
return YES;
}
- (BOOL)resignFirstResponder {
BOOL result = [super resignFirstResponder];
[self _resetCursorStateIfNeeded];
if (result) {
[self sendActionsForControlEvents:UIControlEventEditingDidEnd];
[[NSNotificationCenter defaultCenter] postNotificationName:WLUnitFieldDidResignFirstResponderNotification object:nil];
}
return result;
}
- (void)drawRect:(CGRect)rect {
/*
* 绘制的线条具有宽度,因此在绘制时需要考虑该因素对绘制效果的影响。
*/
CGSize unitSize = CGSizeMake((rect.size.width + _unitSpace) / _inputUnitCount - _unitSpace, rect.size.height);
mCtx = UIGraphicsGetCurrentContext();
[self _fillRect:rect unitSize:unitSize];
[self _drawBorder:rect unitSize:unitSize];
[self _drawText:rect unitSize:unitSize];
[self _drawTrackBorder:rect unitSize:unitSize];
}
#pragma mark- Private
/**
在 AutoLayout 环境下重新指定控件本身的固有尺寸
`-drawRect:`方法会计算控件完成自身的绘制所需的合适尺寸,完成一次绘制后会通知 AutoLayout 系统更新尺寸。
*/
- (void)_resize {
[self invalidateIntrinsicContentSize];
}
/**
绘制背景色,以及剪裁绘制区域
@param rect 控件绘制的区域
*/
- (void)_fillRect:(CGRect)rect unitSize:(CGSize)unitSize {
[mBackgroundColor setFill];
CGFloat radius = _style == WLUnitFieldStyleBorder ? _borderRadius : 0;
if (_unitSpace < 2) {
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius];
CGContextAddPath(mCtx, bezierPath.CGPath);
} else {
for (int i = 0; i < _inputUnitCount; ++i) {
CGRect unitRect = CGRectMake(i * (unitSize.width + _unitSpace),
0,
unitSize.width,
unitSize.height);
unitRect = CGRectInset(unitRect, _borderWidth * 0.5, _borderWidth * 0.5);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:unitRect cornerRadius:radius];
CGContextAddPath(mCtx, bezierPath.CGPath);
}
}
CGContextFillPath(mCtx);
}
/**
绘制边框
边框的绘制分为两种模式:连续和不连续。其模式的切换由`unitSpace`属性决定。
当`unitSpace`值小于 2 时,采用的是连续模式,即每个 input unit 之间没有间隔。
反之,每个 input unit 会被边框包围。
@see unitSpace
@param rect 控件绘制的区域
@param unitSize 单个 input unit 占据的尺寸
*/
- (void)_drawBorder:(CGRect)rect unitSize:(CGSize)unitSize {
CGRect bounds = CGRectInset(rect, _borderWidth * 0.5, _borderWidth * 0.5);
if (_style == WLUnitFieldStyleBorder) {
[self.tintColor setStroke];
CGContextSetLineWidth(mCtx, _borderWidth);
CGContextSetLineCap(mCtx, kCGLineCapRound);
if (_unitSpace < 2) {
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:_borderRadius];
CGContextAddPath(mCtx, bezierPath.CGPath);
for (int i = 1; i < _inputUnitCount; ++i) {
CGContextMoveToPoint(mCtx, (i * unitSize.width), 0);
CGContextAddLineToPoint(mCtx, (i * unitSize.width), (unitSize.height));
}
} else {
for (int i = (int)_characterArray.count; i < _inputUnitCount; i++) {
CGRect unitRect = CGRectMake(i * (unitSize.width + _unitSpace),
0,
unitSize.width,
unitSize.height);
unitRect = CGRectInset(unitRect, _borderWidth * 0.5, _borderWidth * 0.5);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:unitRect cornerRadius:_borderRadius];
CGContextAddPath(mCtx, bezierPath.CGPath);
}
}
CGContextDrawPath(mCtx, kCGPathStroke);
}
else {
[self.tintColor setFill];
for (int i = (int)_characterArray.count; i < _inputUnitCount; i++) {
CGRect unitLineRect = CGRectMake(i * (unitSize.width + _unitSpace),
unitSize.height - _borderWidth,
unitSize.width,
_borderWidth);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:unitLineRect cornerRadius:_borderRadius];
CGContextAddPath(mCtx, bezierPath.CGPath);
}
CGContextDrawPath(mCtx, kCGPathFill);
}
}
/**
绘制文本
当处于密文输入模式时,会用圆圈替代文本。
@param rect 控件绘制的区域
@param unitSize 单个 input unit 占据的尺寸
*/
- (void)_drawText:(CGRect)rect unitSize:(CGSize)unitSize {
if ([self hasText] == NO) return;
NSDictionary *attr = @{NSForegroundColorAttributeName: _textColor,
NSFontAttributeName: _textFont};
for (int i = 0; i < _characterArray.count; i++) {
CGRect unitRect = CGRectMake(i * (unitSize.width + _unitSpace),
0,
unitSize.width,
unitSize.height);
CGFloat yOffset = _style == WLUnitFieldStyleBorder ? 0 : _borderWidth;
if (_secureTextEntry == NO) {
NSString *subString = [_characterArray objectAtIndex:i];
CGSize oneTextSize = [subString sizeWithAttributes:attr];
CGRect drawRect = CGRectInset(unitRect,
(unitRect.size.width - oneTextSize.width) / 2,
(unitRect.size.height - oneTextSize.height) / 2);
drawRect.size.height -= yOffset;
[subString drawInRect:drawRect withAttributes:attr];
} else {
CGRect drawRect = CGRectInset(unitRect,
(unitRect.size.width - _textFont.pointSize / 2) / 2,
(unitRect.size.height - _textFont.pointSize / 2) / 2);
drawRect.size.height -= yOffset;
[_textColor setFill];
CGContextAddEllipseInRect(mCtx, drawRect);
CGContextFillPath(mCtx);
}
}
}
/**
绘制跟踪框,如果指定的`trackTintColor`为 nil 则不绘制
@param rect 控件绘制的区域
@param unitSize 单个 input unit 占据的尺寸
*/
- (void)_drawTrackBorder:(CGRect)rect unitSize:(CGSize)unitSize {
if (_trackTintColor == nil) return;
if (_style == WLUnitFieldStyleBorder) {
if (_unitSpace < 2) return;
[_trackTintColor setStroke];
CGContextSetLineWidth(mCtx, _borderWidth);
CGContextSetLineCap(mCtx, kCGLineCapRound);
for (int i = 0; i < _characterArray.count; i++) {
CGRect unitRect = CGRectMake(i * (unitSize.width + _unitSpace),
0,
unitSize.width,
unitSize.height);
unitRect = CGRectInset(unitRect, _borderWidth * 0.5, _borderWidth * 0.5);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:unitRect cornerRadius:_borderRadius];
CGContextAddPath(mCtx, bezierPath.CGPath);
}
CGContextDrawPath(mCtx, kCGPathStroke);
}
else {
[_trackTintColor setFill];
for (int i = 0; i < _characterArray.count; i++) {
CGRect unitLineRect = CGRectMake(i * (unitSize.width + _unitSpace),
unitSize.height - _borderWidth,
unitSize.width,
_borderWidth);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:unitLineRect cornerRadius:_borderRadius];
CGContextAddPath(mCtx, bezierPath.CGPath);
}
CGContextDrawPath(mCtx, kCGPathFill);
}
}
- (void)_resetCursorStateIfNeeded {
dispatch_async(dispatch_get_main_queue(), ^{
self->_cursorLayer.hidden = !self.isFirstResponder || self->_cursorColor == nil || self->_inputUnitCount == self->_characterArray.count;
if (self->_cursorLayer.hidden) return;
CGSize unitSize = CGSizeMake((self.bounds.size.width + self->_unitSpace) / self->_inputUnitCount - self->_unitSpace, self.bounds.size.height);
CGRect unitRect = CGRectMake(self->_characterArray.count * (unitSize.width + self->_unitSpace),
0,
unitSize.width,
unitSize.height);
unitRect = CGRectInset(unitRect,
unitRect.size.width / 2 - 1,
(unitRect.size.height - self->_textFont.pointSize) / 2);
CGFloat yOffset = self->_style == WLUnitFieldStyleBorder ? 0 : self->_borderWidth;
unitRect.size.height -= yOffset;
[CATransaction begin];
[CATransaction setDisableActions:NO];
[CATransaction setAnimationDuration:0];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
self->_cursorLayer.frame = unitRect;
[CATransaction commit];
});
}
#pragma mark - UIKeyInput
- (BOOL)hasText {
return _characterArray != nil && _characterArray.count > 0;
}
- (void)insertText:(NSString *)text {
if ([text isEqualToString:@"\n"]) {
[self resignFirstResponder];
return;
}
if ([text isEqualToString:@" "]) {
return;
}
if (_characterArray.count >= _inputUnitCount) {
if (_autoResignFirstResponderWhenInputFinished == YES) {
[self resignFirstResponder];
}
return;
}
if ([self.delegate respondsToSelector:@selector(unitField:shouldChangeCharactersInRange:replacementString:)]) {
if ([self.delegate unitField:self shouldChangeCharactersInRange:NSMakeRange(self.text.length, text.length) replacementString:text] == NO) {
return;
}
}
[_inputDelegate textWillChange:self];
NSRange range;
for (int i = 0; i < text.length; i += range.length) {
range = [text rangeOfComposedCharacterSequenceAtIndex:i];
[_characterArray addObject:[text substringWithRange:range]];
}
if (_characterArray.count >= _inputUnitCount) {
[_characterArray removeObjectsInRange:NSMakeRange(_inputUnitCount, _characterArray.count - _inputUnitCount)];
if (_autoResignFirstResponderWhenInputFinished == YES) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self resignFirstResponder];
}];
}
}
[self sendActionsForControlEvents:UIControlEventEditingChanged];
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
[_inputDelegate textDidChange:self];
}
- (void)deleteBackward {
if ([self hasText] == NO)
return;
[_inputDelegate textWillChange:self];
[_characterArray removeLastObject];
[self sendActionsForControlEvents:UIControlEventEditingChanged];
[self setNeedsDisplay];
[self _resetCursorStateIfNeeded];
[_inputDelegate textDidChange:self];
}
/**
Supporting iOS12 SMS verification code, keyboardType must be UIKeyboardTypeNumberPad to localizable.
Must set textContentType to UITextContentTypeOneTimeCode
*/
// UITextInput implement.
#pragma mark - UITextInput
/* Methods for manipulating text. */
- (nullable NSString *)textInRange:(WLUnitFieldTextRange *)range {
return self.text;
}
- (void)replaceRange:(WLUnitFieldTextRange *)range withText:(NSString *)text {
}
// selectedRange is a range within the markedText
- (void)setMarkedText:(nullable NSString *)markedText selectedRange:(NSRange)selectedRange {
mMarkedText = markedText;
}
- (void)unmarkText {
if (self.text.length >= self.inputUnitCount) {
mMarkedText = nil;
return;
}
[self insertText:mMarkedText];
mMarkedText = nil;
}
/* The end and beginning of the the text document. */
- (UITextPosition *)beginningOfDocument {
return [WLUnitFieldTextPosition positionWithOffset:0];
}
- (UITextPosition *)endOfDocument {
if (self.text.length == 0) {
return [WLUnitFieldTextPosition positionWithOffset:0];
}
return [WLUnitFieldTextPosition positionWithOffset:self.text.length - 1];
}
/* A tokenizer must be provided to inform the text input system about text units of varying granularity. */
- (id<UITextInputTokenizer>)tokenizer {
if (!_tokenizer) {
_tokenizer = [[UITextInputStringTokenizer alloc] initWithTextInput:self];
}
return _tokenizer;
}
// Nil if no marked text.
- (UITextRange *)markedTextRange {
return nil;
}
/* Methods for creating ranges and positions. */
- (nullable UITextRange *)textRangeFromPosition:(WLUnitFieldTextPosition *)fromPosition toPosition:(WLUnitFieldTextPosition *)toPosition {
NSRange range = NSMakeRange(MIN(fromPosition.offset, toPosition.offset), ABS(toPosition.offset - fromPosition.offset));
return [WLUnitFieldTextRange rangeWithRange:range];
}
- (nullable UITextPosition *)positionFromPosition:(WLUnitFieldTextPosition *)position offset:(NSInteger)offset {
NSInteger end = position.offset + offset;
if (end > self.text.length || end < 0)
return nil;
return [WLUnitFieldTextPosition positionWithOffset:end];
}
- (nullable UITextPosition *)positionFromPosition:(WLUnitFieldTextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset {
return [WLUnitFieldTextPosition positionWithOffset:position.offset + offset];
}
/* Simple evaluation of positions */
- (NSComparisonResult)comparePosition:(WLUnitFieldTextPosition *)position toPosition:(WLUnitFieldTextPosition *)other {
if (position.offset < other.offset) return NSOrderedAscending;
if (position.offset > other.offset) return NSOrderedDescending;
return NSOrderedSame;
}
- (NSInteger)offsetFromPosition:(WLUnitFieldTextPosition *)from toPosition:(WLUnitFieldTextPosition *)toPosition {
return toPosition.offset - from.offset ;
}
/* Layout questions. */
- (nullable UITextPosition *)positionWithinRange:(UITextRange *)range farthestInDirection:(UITextLayoutDirection)direction { return nil; }
- (nullable UITextRange *)characterRangeByExtendingPosition:(WLUnitFieldTextPosition *)position inDirection:(UITextLayoutDirection)direction { return nil; }
/* Writing direction */
- (UITextWritingDirection)baseWritingDirectionForPosition:(WLUnitFieldTextPosition *)position inDirection:(UITextStorageDirection)direction { return UITextWritingDirectionNatural; }
- (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection forRange:(UITextRange *)range {}
/* Geometry used to provide, for example, a correction rect. */
- (NSArray<UITextSelectionRect *> *)selectionRectsForRange:(WLUnitFieldTextRange *)range { return nil; }
- (CGRect)firstRectForRange:(WLUnitFieldTextRange *)range { return CGRectNull; }
- (CGRect)caretRectForPosition:(WLUnitFieldTextPosition *)position { return CGRectNull; }
/* Hit testing. */
- (nullable UITextRange *)characterRangeAtPoint:(CGPoint)point { return nil; }
- (nullable UITextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(WLUnitFieldTextRange *)range { return nil; }
- (nullable UITextPosition *)closestPositionToPoint:(CGPoint)point { return nil; }
@end