runtime MethodSwizzle 實踐之擴充套件 NIAttributedLabel

hhhker發表於2014-12-02

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, 0, -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(0,0), origins);

  CGAffineTransform transform = [self _transformForCoreText];
  CGFloat verticalOffset = [self _verticalOffsetForBounds:self.bounds];

  for (int i = 0; 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, 0, -kVMargin);
    rect = CGRectOffset(rect, 0, 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 方法名字被作者修改,專案又重新更新了庫,則沒有效果。

以下是新增內容:

 *  新增功能:
 *
 *  1,檢測是否觸發連結
 *
 *  2,檢測是否觸發圖片連結(原庫中包含新增圖文混排的方法,但如果沒有連結文字,NIAttributedLabel 將會關閉使用者互動)
 *
 *  3,判斷NIAttributedLabel  是否包含圖片
 *
 *  4,插入的圖片支援圖片連結,且可自定義觸發圖片連結的回撥block
 *
 *  5,支援圖片連結、文字連結 混用且數量不限,可以準確定位觸發源並自定義block 回撥處理

demo 可以這裡下載:git clone  https://github.com/githhhh/Test_Pod.git

 

 

 

 

相關文章