6

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:

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.

Community
  • 1
  • 1
Jason McCreary
  • 71,546
  • 23
  • 135
  • 174
  • I don't think you need to set readonly property to dateformatter in any situation. – Dinesh Raja Dec 02 '12 at 15:36
  • It worked prior to ARC. The readonly flag prevented the *setter* and allowed me to override the *getter* with my initialization code. I'm fairly certain I pulled the original from an code in an Apple Demo Project. – Jason McCreary Dec 02 '12 at 15:45
  • @JasonMcCreary, I don't think you quite understand yet. This line in the getter: `self.timeFormatter = [[NSDateFormatter alloc] init];` is assignment to a readonly property (obviously not allowed). It has nothing to do with ARC, and even less to do with Apple's code – CodaFi Dec 02 '12 at 19:14
  • @CodaFi, it is clear I don't understand. Hence my question :). Forget my example. How can I replicate what I had before, noting the encapsulation of `timeFormatter` in the original code (done through `readonly`). – Jason McCreary Dec 02 '12 at 19:39

2 Answers2

14

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!)

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • So I could leave my property declaration as is? – Jason McCreary Dec 02 '12 at 16:05
  • @JasonMcCreary: Yes, "nonatomic, strong, readonly" is OK. With ARC, even "retain" is a synonym for "strong", but "strong" is preferred. – Martin R Dec 02 '12 at 16:05
  • @JasonMcCreary: My initial answer was not completely correct, you cannot omit the `@synthesize` in your case. I have updated the answer accordingly. – Martin R Dec 02 '12 at 17:01
  • 1
    +1 this is the way to go - Xcode/LLVM does, by omitting the need of a manually declared iVar obviously fool people into thinking that iVars were not installed anymore. – Till Dec 02 '12 at 19:20
  • I tried this, but an getting: *Use of undeclared identifier '_timeFormatter'*. – Jason McCreary Dec 02 '12 at 19:43
  • @JasonMcCreary: Have you added `@synthesize timeFormatter = _timeFormatter;` in the @implementation block of the .m file? – Martin R Dec 02 '12 at 19:55
  • @JasonMcCreary: I have added a complete code example to my answer. It compiles without errors in my test project. – Martin R Dec 02 '12 at 20:00
  • Thanks. The `@synthesize timeFormatter = _timeFormatter;` was the missing link. – Jason McCreary Dec 04 '12 at 02:35
7

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.

CodaFi
  • 43,043
  • 8
  • 107
  • 153
  • I understand the `readonly` property (see comments on original question). I'm unclear on your proposed solution. Could you elaborate. Also, is there a need for `dispatch_sync()` as proposed in the articles I referenced? – Jason McCreary Dec 02 '12 at 15:46
  • The compiler is comaining at your method because you're trying to ***set*** a property which you yourself declared as **read** (aka, get), only. If you paste that bit of code at the top of your .m (usually below your #imports, but above the main @implementation block), then it will locally re-declare the property's memory qualifiers. – CodaFi Dec 02 '12 at 15:51
  • @CodaFi: The OP wanted to use a custom getter method (for lazy creation of the date formatter) instead of the synthesized getter. How can you do that without accessing the the ivar? `if (self.timeFormatter == nil)` inside the getter method for `timeFormatter` immediately gives infinite recursion. Or did I overlook something? – Martin R Dec 02 '12 at 18:28
  • I saw nothing about custom getters, I saw something about trying to write a getter, then getting errors because of a readonly property. My answer, in turn, solved his getter issues. And if he were actually ***using *** a custom getter as you say, he can safely access the property without causing recursion, otherwise you're right. – CodaFi Dec 02 '12 at 18:53
  • In the past, I did what @Martin R described. I am trying to replicate this for iOS 5+ with ARC. – Jason McCreary Dec 02 '12 at 18:57
  • 2
    +1 as that is the way to go to override the readonly attribute. Used this pattern in the past for hacking UIKit's readonly properties every now and then. This however does not answer the question, still I found it a good hint to explain the use of a category to make a property read-write. – Till Dec 02 '12 at 19:17
  • Thanks for your answer. However, it did not address my specific question. Nonetheless, I'll keep your code in mind for the future when creating private, readonly properties. – Jason McCreary Dec 04 '12 at 02:36