Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lazy Load Property in iOS 5+ with ARC

Problem

I am migrating some legacy code (pre iOS 5) where I lazy load some readonly properties. I want to update this code to iOS 5+ with ARC. But I just learning about ARC.

.h

@property (nonatomic, retain, readonly) NSDateFormatter *timeFormatter;

.m

- (NSDateFormatter *)timeFormatter {
    if (timeFormatter == nil) {
        timeFormatter = [[NSDateFormatter alloc] init];
        [timeFormatter setDateFormat:@"h:mm a"];
    }

    return timeFormatter;
}

What I tried

I have tried to simply update my code, but receive an: Assignment to readonly property.

.h

@property (nonatomic, strong, readonly) NSDateFormatter *timeFormatter;

.m

- (NSDateFormatter *)timeFormatter {
    if (self.timeFormatter == nil) {
        self.timeFormatter = [[NSDateFormatter alloc] init];
        [self.timeFormatter setDateFormat:@"h:mm a"];
    }

    return self.timeFormatter;
}

I also reviewed:

  • ios ARC strong and alloc
  • Thread safe lazy initialization on iOS
  • http://www.cocoanetics.com/2012/02/threadsafe-lazy-property-initialization/

Question

What is the correct way to lazy-load a readonly property in iOS 5+ with ARC? Would appreciate code samples for both .h and .m.

like image 538
Jason McCreary Avatar asked Dec 02 '12 15:12

Jason McCreary


2 Answers

For a custom (lazy) getter method you have to access the instance variable directly (whether you use ARC or not). So you should synthesize the property as

@synthesize timeFormatter = _timeFormatter;

Then your getter method is

- (NSDateFormatter *)timeFormatter {
    if (_timeFormatter == nil) {
        _timeFormatter = [[NSDateFormatter alloc] init];
        [_timeFormatter setDateFormat:@"h:mm a"];
    }

    return _timeFormatter;
}

You only have to add some synchronization mechanism if the property is accessed from multiple threads concurrently, that is also independent of ARC or not.

(Remark: Newer Xcode versions can create a @synthesize statement automatically and use the underscore prefix for instance variables. In this case however, since the property is read-only and you provide a getter method, Xcode does not synthesize the property automatically.)

ADDED: Here is a complete code example for your convenience:

MyClass.h:

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
@property (nonatomic, strong, readonly) NSDateFormatter *timeFormatter;
@end

MyClass.m:

#import "MyClass.h"

@implementation MyClass
@synthesize timeFormatter = _timeFormatter;

- (NSDateFormatter *)timeFormatter {
    if (_timeFormatter == nil) {
        _timeFormatter = [[NSDateFormatter alloc] init];
        [_timeFormatter setDateFormat:@"h:mm a"];
    }

    return _timeFormatter;
}

@end

MORE INFORMATION: In fact, your pre-ARC timeFormatter getter method works without changes also with ARC, if the property is synthesized as

@synthesize timeFormatter; // or: @synthesize timeFormatter = timeFormatter;

The only "mistake" you made was to replace timeFormatter by self.timeFormatter inside the getter method. This creates two problems:

  • Reading self.timeFormatter inside the getter method leads to infinite recursion.
  • Setting self.timeFormatter is not allowed because of the read-only attribute.

So if you just leave the timeFormatter getter method as it was (using the timeFormatter instance variable inside the method) then it works also with ARC.

I would still recommend to prefix instance variables for properties with an underscore as in my code example, because Xcode does it the same way for automatically synthesized properties.

(I hope that this helps and does not increase the confusion!)

like image 135
Martin R Avatar answered Nov 09 '22 11:11

Martin R


Readonly properties are just that: read only. There should be no setters involved. The nice part is, if you redeclare the variable in a class extension (usually with a pair of empty parenthesis), as readwrite (or even just remove the readonly entirely), then you can assign to it within the .m, but classes that import it will see it as readonly.

@interface MyClass ()

@property (nonatomic, strong) NSDateFormatter *timeFormatter;

@end

This redeclaration allows a cleaner way to access and mutate the property internally without resorting to fragile iVar synthesis (which is becoming an antiquity now that the compiler does it for you). You can, or course, still use the iVar as shown in the other answer, but iVar access outside of -init or synthesized getters is unnecessary.*

*As Martin correctly pointed out, even if your assignment had succeeded, you still would have caused an infinite recursion, so iVar access is necessary, unless you explicitly declare a getter, then you may use property access.

like image 24
CodaFi Avatar answered Nov 09 '22 12:11

CodaFi