Search code examples
iosobjective-cruntimeobjective-c-runtime

Objective-C Runtime changing readonly property of initialized object


i have got an issue, i can't change frame.size.width in 3rd party lib. Could not find any normal solution to change the width, so i decided to execute objc/runtime.

I've got ViewController and its property DTAttributedTextView *v.

@interface DTAttributedTextView : UIScrollView
{
 // ivars needed by subclasses
 DTAttributedTextContentView *_attributedTextContentView;
}

 @property (nonatomic, strong) NSAttributedString *attributedString;
 @property (nonatomic, DT_WEAK_PROPERTY) IBOutlet 
 id<DTAttributedTextContentViewDelegate> textDelegate;
 @property (nonatomic, strong) IBOutlet UIView *backgroundView;
 ....
@end

v got @property ( .., readonly) DTAttributedTextContentView *attributedTextContentView

 @interface DTAttributedTextContentView : UIView
 {
      NSAttributedString *_attributedString;
      DTCoreTextLayoutFrame *_layoutFrame;

      UIEdgeInsets _edgeInsets;

      NSMutableDictionary *customViewsForAttachmentsIndex;

      BOOL _flexibleHeight;

      // for layoutFrame
      NSInteger _numberOfLines;
      NSLineBreakMode _lineBreakMode;
      NSAttributedString *_truncationString;
}

attributedTextContentView got @property DTCoreTextLayoutFrame *layoutFrame

 @interface DTCoreTextLayoutFrame : NSObject 
 {
   CGRect _frame;

   NSArray *_lines;
   NSArray *_paragraphRanges;

   NSArray *_textAttachments;
   NSAttributedString *_attributedStringFragment;
 }

So basically i need to change

self.v.attributedTextContentView.layoutFrame.frame.size.width

for a pitty i cant use

objc_setAssociatedObject(self.v.attributedTextContentView.layoutFrame,@"frame.size.width",@200,OBJC_ASSOCIATION_ASSIGN);

nor

objc_setAssociatedObject(self.v.attributedTextContentView.layoutFrame,@"frame",CGRectMake(0,0,200,1000),OBJC_ASSOCIATION_ASSIGN);

because i can't access ivars by dot notation, nor send CGStruct as CGFloat required event if &.

As another solution to this situation i see creating object by object using runtime and then change the pointer. Maybe some steps could be done using copy. My problem is that im total newbie in objc/runtime and also documentation is really poor. Im struggling to learn this significant technology so i'm intentionally do not solve the exact problem using other options.

Any help would be highly appreciated. Thanx in advance.


Solution

  • It seems that you are doing somethings wrong, but if you really need to set value to another instance ivar (this could lead to unpredictable behavior), this is what you need to know:

    1) associated object is not a part of instance, so if you add associated object with key that equal property/ivar name it will not change the value of property/ivar

    2) you cannot change the part of structure if it this structure is a property of object

    3) you can get access to ivars by calling valueForKey: / setValue:forKey: if accessInstanceVariablesDirectly property is not overriden.

    4) you need to wrap your structure to NSValue before passing to setValue:forKey:

    So, the result code should look like this:

    DTCoreTextLayoutFrame *layoutFrame = self.v.attributedTextContentView.layoutFrame;
    CGRect frame = [[layoutFrame valueForKey:@"_frame"] CGRectValue];
    frame.size.width = 200.0;
    NSValue* frameValue = [NSValue valueWithCGRect:frame];
    [layoutFrame setValue:frameValue forKey:@"_frame"];
    

    For more information about KVC, check Key-Value Coding Programming Guide.

    Update:

    If you want to use runtime functions, you need to:

    1) get offset of instance variable (all objective-c objects are structures)

    2) create pointer to the ivar that you are interested in

    3) read from / write to this pointer directly

    This is the code:

    DTCoreTextLayoutFrame *layoutFrame = self.v.attributedTextContentView.layoutFrame;    
    Ivar ivar = class_getInstanceVariable([DTCoreTextLayoutFrame class], "_frame");
    ptrdiff_t offset = ivar_getOffset(ivar);
    CGRect *framePtr = (__bridge void*)layoutFrame + offset;
    CGRect frame = *framePtr;
    frame.size.width = 100;
    *framePtr = frame;