4

I have a lazy property in swift that has a callback that looks like this.

lazy var apiClient: MyApiClient =
    {

        var apiClient : MyApiClient = MyApiClient()

        apiClient.detailSearchFinishedCallBack = {
            (detailModel : DetailModel!) in

        }

        return apiClient
    }()

I have another lazy load property that I would like to access inside the closure that looks like this:

  lazy var loadingView : LoadingView =
    {
        var loadingView : LoadingView = LoadingView()
        loadingView.frame = CGRectMake(0, 0, 200, 200)
        loadingView.center = self.view.center

        return loadingView
    }()

However, I'm unable to reference the loading view inside the closure. In objective c, this would look something like this.

-(LoadingView *)loadingView
{
    if(!_loadingView)
    {
        _loadingView = [LoadingView new];
        _loadingView.frame = CGRectMake(0, 0, 200, 200);
        _loadingView.center = self.view.center;
    }
    return _loadingView;
}

-(MyApiClient *)apiClient
{
    if(!_apiClient)
    {
        _apiClient = [MyApiClient new];

        __weak FeedViewController *_self = self;

        self.apiClient.detailSearchFinishedCallBack = ^(DetailModel  *detailModel)
        {
              [_self.loadingView stopAnimating];
        };
    }

    return _apiClient;
}

Would someone be kind enough to show me the equivalent of this in Swift?

UPDATE:

  lazy var apiClient: MyApiClient = {
            let apiClient: MyApiClient = MyApiClient()
            apiClient.detailSearchFinishedCallBack = { [weak self] (detailModel: DetailModel!) in

                println(self?.loadingView.frame)

                return
            }
            return apiClient
            }()

So I went ahead and tried implementing the proposed solution but I get a compilation error. Specifically I'm running into this error:

Command /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc failed with exit code 1

I'm not sure if this is something to do with my swift configuration in my project or not but I'm using the bridging header to import my objective c header files to be exposed to Swift. I can't think of anything else that could be causing this but any help would be appreciated to resolve this.

6
  • See stackoverflow.com/questions/24468336/… Commented Feb 27, 2015 at 4:42
  • 1
    @MikeTaverne If your point in referencing that other article was to address the weakSelf pattern, I don't think that's the key issue here. I think the issue is having a variable's default value configured in a closure which references another property. Personally, I gave up on closures for the default values when this sort of interdependency exists, moving it to init and/or viewDidLoad as appropriate. That bypasses the problem. Commented Feb 27, 2015 at 5:32
  • @Rob I trust your judgment on this. I have not tried this myself but your explanation makes sense. Thanks. Commented Feb 27, 2015 at 6:30
  • The Update really asks the question what 'self' refers to. It should probably be apiClient and that does not have access to 'loadingView', no? Commented Feb 28, 2015 at 3:52
  • @zic10 By the way, you mention a compiler crash. Yes, I've noticed cryptic and inexplicable crashes when using closures to provide default values for properties. Worse, in my tests (which was with singleton patterns), the problem wasn't always replicable. I don't think Swift's implementation of closures as default values is very robust. Commented Feb 28, 2015 at 15:21

2 Answers 2

3

The syntax would be:

lazy var apiClient: MyApiClient = {
    let apiClient: MyApiClient = MyApiClient()
    apiClient.detailSearchFinishedCallBack = { [weak self] detailModel in
        self?.loadingView?.stopAnimating()
    }
    return apiClient
}()

At the time I originally answered this question, the Swift compiler had problem interpreting the above syntax, so I proposed the following ugly workaround outlined below. But you can now use the above simple, logical syntax.


Original answer:

You are using a closure to provide a default value for a property. But in the Setting a Default Property Value with a Closure or Function section of the Initialization Chapter of The Swift Programming Language says:

Note: If you use a closure to initialize a property, remember that the rest of the instance has not yet been initialized at the point that the closure is executed. This means that you cannot access any other property values from within your closure, even if those properties have default values. You also cannot use the implicit self property, or call any of the instance’s methods.

Having said that, one could make an argument that the compiler should make allowances for lazy variables, but it doesn't.


You say you want an implementation of this Objective-C code:

-(MyApiClient *)apiClient
{
    if(!_apiClient)
    {
        _apiClient = [MyApiClient new];

        __weak FeedViewController *_self = self;

        self.apiClient.detailSearchFinishedCallBack = ^(DetailModel  *detailModel)
        {
              [_self.loadingView stopAnimating];
        };
    }

    return _apiClient;
}

Perhaps you could consider a more literal Swift conversion, using private stored property which is handled by a computed property:

private var _apiClient: MyApiClient!

var apiClient: MyApiClient {
    if _apiClient == nil {
        _apiClient = MyApiClient()

        _apiClient.detailSearchFinishedCallBack = { [weak self] detailModel in
            if let loadingView = self?.loadingView {
                loadingView.stopAnimating()
            }
        }
    }

    return _apiClient
}

Note, this is not thread safe (but your Objective-C rendition isn't either), but it accomplishes what you asked for.

As an aside there are other approaches to solve this:

  • You could move the initialization code from these closures and into some other logical shared initialization routine, e.g. viewDidLoad or what have you. This way, the order in which things are initialized is explicit and unambiguous.

  • You could further decouple the API from the UI by having the API post notifications that the view could observe.

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

2 Comments

Lazy properties are special case: you can use self inside initialization closures, because lazy property can't be accessed before object is fully initialized
@Silmaril - Agreed. At the time I originally wrote this answer, the compiler choked on that syntax, hence the ugly workaround. But you can now use the syntax that the OP proposed in his question.
1

I'm not sure what you unable to do.

Actually, Xcode has some problem about using self in lazy var x:Type = { ... }() pattern. For example, it doesn't auto-complete the properties or methods, but it works.

This code compiles and works in Xcode 6.3 Beta2:

class LoadingView: UIView {}
class DetailModel {}

class MyApiClient {
    var detailSearchFinishedCallBack: ((DetailModel!) -> Void)?
}

class MyViewController1: UIViewController {

    override func viewDidLoad() {
        self.apiClient.detailSearchFinishedCallBack?(DetailModel())
    }

    lazy var apiClient: MyApiClient = {
        let apiClient: MyApiClient = MyApiClient()
        apiClient.detailSearchFinishedCallBack = { [weak self] (detailModel: DetailModel!) in

            println(self?.loadingView.frame)

            return
        }
        return apiClient
    }()

    lazy var loadingView: LoadingView = {
        let loadingView: LoadingView = LoadingView(frame: CGRectMake(0, 0, 200, 200))
        loadingView.center = self.view.center
        return loadingView
    }()
}

As for Xcode 6.1.1, for some reasons, the compiler crashes. It seems, we have to reference the self at least once outside of the closure.

        self // <- reference `self` in anyway

        apiClient.detailSearchFinishedCallBack = { [weak self] (detailModel: DetailModel!) in
            println(_self?.loadingView.frame)
            return
        }

Finally, if you feel something unsafe, you can do straight-forward equivalent to Objective-C:

class MyViewController2: UIViewController {

    private var _apiClient: MyApiClient? = nil
    var apiClient: MyApiClient {
        if _apiClient == nil {
            let apiClient : MyApiClient = MyApiClient()
            apiClient.detailSearchFinishedCallBack = { [weak self] (detailModel : DetailModel!) in
                println(self?.loadingView.frame)
                return
            }
            _apiClient = apiClient
        }
        return _apiClient!
    }

    private var _loadingView: LoadingView? = nil
    var loadingView : LoadingView {
        if _loadingView == nil {
            let loadingView : LoadingView = LoadingView(frame: CGRectMake(0, 0, 200, 200))
            loadingView.center = self.view.center
            _loadingView = loadingView
        }
        return _loadingView!
    }
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.