6

How would you implement the following pattern in Swift?

The Container class is initialized with a JSON array that contains dictionaries. These dictionaries are used to initialize Entry classes. However, the initialization of the Entry objects happens lazily, when either the entries or the searchEntries property is accessed.

@interface Container

@property (readonly, nonatomic) NSArray *entryDicts;

@property (readonly, nonatomic) NSArray* entries;
@property (readonly, nonatomic) NSDictionary *searchEntries;

@end



@implementation Container

- (instancetype)initWithArray:(NSArray *)array
{
    self = [super init];
    if (self) {
        _entryDicts = array;
    }
    return self;
}

@synthesize entries = _entries;
- (NSArray *)entries
{
    [self loadEntriesIfNeeded];
    return _entries;
}

@synthesize entriesByNumber = _entriesByNumber;
- (NSDictionary *)entriesByNumber
{
    [self loadEntriesIfNeeded];
    return _entriesByNumber;
}

- (void)loadEntriesIfNeeded
{
    if (_entries == nil) {
        // Load entries
        NSMutableArray *entries = [NSMutableArray arrayWithCapacity:[self.entriesDict count]];
        NSMutableDictionary *entriesByNumber = [NSMutableDictionary dictionaryWithCapacity:[self.entriesDict count]];

        [self.entriesDict enumerateKeysAndObjectsUsingBlock:^(NSString *number, NSDictionary *entryDict, BOOL *stop) {
            Entry *entry = [[Entry alloc] initWithDictionary:entryDict container:self];
            [entries addObject:entry];
            entriesByNumber[number] = entry;
        }];

        _entries = [entries copy];
        _entriesByNumber = [entriesByNumber copy];

        // Delete dictionaries
        _entriesDict = nil;
    }
}

@end

9 Answers 9

12

What about this:

class Container {

    lazy var entries: [String] = self.newEntries()

    func newEntries() -> [String] {

        // calculate and return entries

    }

}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks. The opening { is missing after class name, and the return value in the function.
@Evgeny Fixed missing "{". Thanks!
8

It seems that this question has largely been answered, but to circle back to the original post, here is (IMHO) a relatively succinct translation in Swift. The key is that you can chain lazy properties. Note that I used both a class function and a closure - either is fine.

import Swift

println("begin")

class ClassWithLazyProperties {

    lazy var entries:[String] = ClassWithLazyProperties.loadStuff()
    lazy var entriesByNumber:Dictionary<Int, String> = {

        var d = Dictionary<Int, String>()
        for i in 0..<self.entries.count {
            d[i] = self.entries[i]
        }
        return d
    }()

    private class func loadStuff() -> [String] {
        return ["Acai", "Apples", "Apricots", "Avocado", "Ackee", "Bananas", "Bilberries"]
    }

}

let c = ClassWithLazyProperties()
c.entriesByNumber
    // 0: "Acai", 1: "Apples", 2: "Apricots", 3: "Avocado", 4: "Ackee", 5: "Bananas", 6: "Bilberries"]


println("end")

Comments

5

You could use an optional as the instance variable. Then create a function that returns the optional if it exists, and a new object if it does not to simulate lazy loading.

class Lazy {
    var lazyVariable:String?

    func lazilyGetEntries() -> String {
        if let possibleVariable = self.lazyVariable { // optional already exists
            return possibleVariable
        }
        else {                                        // optional does not exist, create it
            self.lazyVariable = String()
            return self.lazyVariable!
        }
    }
}

Comments

4

The way lazy works is that the initializer (or init method) runs only when the variable or property is first accessed. I see one main reason why it won't work (at least straight away) in your code, and that is because you packed two lazy instantiation code into one method (loadEntriesIfNeeded).

To use lazy instantiation, you might need to extend NSMutableArray and NSDictionary and override or create a custom initializer for your lazy instantiation. Then, distribute the code inside loadEntriesIfNeeded into their respective initializers.

In entries initializer:

NSMutableArray *entries = [NSMutableArray arrayWithCapacity:[self.entriesDict count]];
[self.entriesDict enumerateKeysAndObjectsUsingBlock:^(NSString *number, NSDictionary *entryDict, BOOL *stop) {
            Entry *entry = [[Entry alloc] initWithDictionary:entryDict container:self];
            [entries addObject:entry];}];
_entries = [entries copy];

Then in entriesByNumber initializer:

NSMutableDictionary *entriesByNumber = [NSMutableDictionary dictionaryWithCapacity:[self.entriesDict count]];
// Then do fast enumeration accessing self.entries to assign values to entriesByNumber.
// If self.entries is null, the lazy instantiation should kick in, calling the above code
// and populating the entries variable.
_entriesByNumber = [entriesByNumber copy];

Then, you can create your lazy variables by calling on the custom initializers.

@lazy var entries: CustomArray = custominitforarray()
@lazy var entriesByNumber: CustomDictionary = custominitfordictionary()

PS: How come you don't have a property for entriesByNumber? I'm guessing it's private? Please test this out and reply about the result as I'm too lazy to do it myself.

1 Comment

it became just lazy, without "@" in beta4
2

You can use Lazy Stored Properties in Swift to implement the Lazy Instantiation pattern. This is done by adding the @lazy attribute before the declaration of the stored property.

There are two things to keep in mind:

  • Lazy properties must be initialized when declared
  • Lazy properties can only be used on members of a struct or a class (hence why we need to use a DataManager)

Here's some code you can throw into a Playground to see how the @lazy attribute works

// initialize your lazily instantiated data
func initLazyData() -> String[] {
    return ["lazy data"]
}

// a class to manage the lazy data (along with any other data you want)
class DataManager {
    @lazy var lazyData = initLazyData()

    var otherData = "Other data"
}

// when we create this object, the "lazy data" array is not initialized
let manager = DataManager()

// even if we access another property, the "lazy data" array stays nil
manager.otherData += ", more data"
manager

// as soon as we access the "lazy data" array, it gets created
manager.lazyData
manager

For more information, you can check out the Lazy Stored Properties section on the Properties page of the Swift Programming Language Guide. Note that that link is to pre-release documentation.

Comments

0

You indicate a lazy stored property by writing the @lazy attribute before its declaration.”

@lazy var lazyVariable:String? = ""

Please bear in mind, lazy property must have an initialiser.

Comments

0

There is a @lazy attribute in Swift. I found a small post here and I recommend watching the three Swift video from Apple here (Introduction to Swift, Intermediate Swift, Advanced Swift). These videos show a lot of stuff and the advanced one really is advanced...

2 Comments

I've already watched all videos. Unfortunately, @lazy doesn't work here. Check out my code.
@Florian Unfortunately my developer program isn't activated yet so I can't run the code :(
0

I found this in PageBaseApplication

var modelController: ModelController {
    // Return the model controller object, creating it if necessary.
    // In more complex implementations, the model controller may be passed to the view controller.
    if !_modelController {
        _modelController = ModelController()
    }
    return _modelController!
}

var _modelController: ModelController? = nil

similar to what @Brian Tracy mentioned but ussing variables instead of a func

Comments

0

Since the entries property is just an array of the values in entriesByNumber you can do all the loading just in entriesByNumber and just have entries depend on entriesByNumber

lazy var entriesByNumber: [String : Entry] = {
        var ret: [String : Entry] = [:] 
        for (number, entryDict) in entriesDict
        {
            ret[number] = Entry(dictionary: entryDict, container: self)
        }
        return ret
    }

var entries: [Entry] 
{
    get { return self.entriesByNumber.values }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.