runtime MethodSwizzle 实践之扩展 NIAttributedLabel

时间:2023-12-25 10:33:37

runtime MethodeSwizzle 提供 简单的方法交换已知类的  Method IMP.

Method 可以是 外部可访问的 public 或者 private Method .所谓的属性或私有变量 也不过是 getter/setter Method 而已。

MethodeSwizzle 技术 几乎可以实现你要使用 已知类的所有东西。

so Powerful。

代码实现:

#import <Foundation/Foundation.h>

@interface NSObject (Swizzle)
+ (void)swizzleInstanceSelector:(SEL)originalSelector
withNewSelector:(SEL)newSelector;
@end
#import "NSObject+Swizzle.h"
#import <objc/runtime.h> @implementation NSObject (Swizzle)
+ (void) swizzleInstanceSelector:(SEL)originalSelector
withNewSelector:(SEL)newSelector
{
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method newMethod = class_getInstanceMethod(self, newSelector); BOOL methodAdded = class_addMethod([self class],
originalSelector,
method_getImplementation(newMethod),
method_getTypeEncoding(newMethod)); if (methodAdded) {
class_replaceMethod([self class],
newSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, newMethod);
}
}
@end

考虑通用性,这里使用NSObject 分类实现。

MethodeSwizzle 应用之解决实际问题:

最近使用

NIAttributedLabel 实现 文本渲染,图文混排等功能。还是挺不错的。

它提供简单的方法实现 插入文本链接, 设置delegate 回调 处理链接动作。

NIAttributedLabel.m 内部实现,

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

并在

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

中检测是否触发链接,并触发回调。

///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event]; [self.longPressTimer invalidate];
self.longPressTimer = nil; UITouch* touch = [touches anyObject];
CGPoint point = [touch locationInView:self]; if (nil != self.originalLink) {
if ([self isPoint:point nearLink:self.originalLink]) {
// This old-style method is deprecated, please update to the newer delegate method that supports
// more data types.
NIDASSERT(![self.delegate respondsToSelector:@selector(attributedLabel:didSelectLink:atPoint:)]); if ([self.delegate respondsToSelector:@selector(attributedLabel:didSelectTextCheckingResult:atPoint:)]) {
[self.delegate attributedLabel:self didSelectTextCheckingResult:self.originalLink atPoint:point];
}
}
} self.touchedLink = nil;
self.originalLink = nil; [self setNeedsDisplay];
}

其中

 if (nil != self.originalLink) {
if ([self isPoint:point nearLink:self.originalLink]) {
其中
self.originalLink 用方法
///////////////////////////////////////////////////////////////////////////////////////////////////
- (NSTextCheckingResult *)linkAtPoint:(CGPoint)point {
if (!CGRectContainsPoint(CGRectInset(self.bounds, , -kVMargin), point)) {
return nil;
} CFArrayRef lines = CTFrameGetLines(self.textFrame);
if (!lines) return nil;
CFIndex count = CFArrayGetCount(lines); NSTextCheckingResult* foundLink = nil; CGPoint origins[count];
CTFrameGetLineOrigins(self.textFrame, CFRangeMake(,), origins); CGAffineTransform transform = [self _transformForCoreText];
CGFloat verticalOffset = [self _verticalOffsetForBounds:self.bounds]; for (int i = ; i < count; i++) {
CGPoint linePoint = origins[i]; CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGRect flippedRect = [self getLineBounds:line point:linePoint];
CGRect rect = CGRectApplyAffineTransform(flippedRect, transform); rect = CGRectInset(rect, , -kVMargin);
rect = CGRectOffset(rect, , verticalOffset); if (CGRectContainsPoint(rect, point)) {
CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect),
point.y-CGRectGetMinY(rect));
CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
foundLink = [self linkAtIndex:idx];
if (foundLink) {
return foundLink;
}
}
}
return nil;
}

获得。

这两个条件成立,则触发链接,否则就返回了。??

实际情况可能是 我要检测 链接是否触发,没有触发的话我要自定义动作。

而且这两个方法还都是  NIAttributedLabel 类得私有方法, 举步维艰之际想到了强大的MethodSwizzle

思路:在分类中 定义两个函数  然后分别与 NIAttributedLabel  中的以上两个方法 调换。

#import "NIAttributedLabel+XYNIAttributedLabel.h"
#import <objc/runtime.h>
#import "NSObject+XYSwizzle.h"
@implementation NIAttributedLabel (XYNIAttributedLabel) +(void)load{
[self swizzleInstanceSelector:@selector(linkAtPoint:) withNewSelector:@selector(swizzleLinkAtPoint:)];
[self swizzleInstanceSelector:@selector(isPoint:nearLink:) withNewSelector:@selector(swizzleIsPoint:nearLink:)];
}
-(BOOL)isTriggerLink:(CGPoint )point{
NSTextCheckingResult *textCheckingResult = [self swizzleLinkAtPoint:point];
if (nil != textCheckingResult) {
if ([self swizzleIsPoint:point nearLink:textCheckingResult]) {
return YES;
}
}
return NO;
} -(NSTextCheckingResult *)swizzleLinkAtPoint:(CGPoint)point{
return [self swizzleLinkAtPoint:point];
} -(BOOL)swizzleIsPoint:(CGPoint)point nearLink:(NSTextCheckingResult *)link{
BOOL resulte = [self swizzleIsPoint:point nearLink:link];
return resulte;
}
@end

:上面

+(void)load 方法中 linkAtPoint 、isPoint:nearLink: 有可能会报编译器警告。无法找到相关sel ,因为它们是私有方法。不要理他,这个是在runtime 生效。
我在demo 里有警告,但到了项目里好像没有了。不管它吧。

并提供

-(BOOL)isTriggerLink:(CGPoint )point; 对外调用 检测是否触发链接。

so。 问题得意 轻松解决。

废话一句: 实例方法 在 类对象中保持。

+(void)load{
[self swizzleInstanceSelector:@selector(linkAtPoint:) withNewSelector:@selector(swizzleLinkAtPoint:)];
[self swizzleInstanceSelector:@selector(isPoint:nearLink:) withNewSelector:@selector(swizzleIsPoint:nearLink:)];
}

注: 以上解决方案有一定风险,目前支持NimbusKit-AttributedLabel (1.0.0)

,如果被交换的NIAttributedLabel 方法名字被作者修改,项目又重新更新了库,则没有效果。

以下是新增内容:

 *  新增功能:
*
* ,检测是否触发链接
*
* ,检测是否触发图片链接(原库中包含添加图文混排的方法,但如果没有链接文本,NIAttributedLabel 将会关闭用户交互)
*
* ,判断NIAttributedLabel 是否包含图片
*
* ,插入的图片支持图片链接,且可自定义触发图片链接的回调block
*
* ,支持图片链接、文字链接 混用且数量不限,可以准确定位触发源并自定义block 回调处理

demo 可以这里下载:git clone  https://github.com/githhhh/Test_Pod.git