-2

I'm trying to initiate an NSObject subclass called FormObject in a JavascriptCore block. The FormObject is supposed to be nil until I set it in the JavascriptCore block. I need to set it in this block because After I show a UIActionSheet I save this FormObject in the UIActionSheetDelegate method - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex

Here is the code:

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (actionSheet.tag == kSaveFormConfirmationActionSheetTag) {
        if ([ButtonTitle isEqualToString:NSLocalizedString(@"Yes", @"Yes")]) {
            NSLog(@"Saving form data");
            NSLog(@"%@",formData);
            NSLog(@"%@",formData.dictionary);
            if (formData) {
                [[AutoFillManager defaultManager] saveFormData:formData];
                formData = nil;
            }
        }
        else if ([ButtonTitle isEqualToString:NSLocalizedString(@"Never for this Website", @"Never for this Website")]) {
            NSString *formURLString = [formData.urlString copy];
            formData = nil;
            formData = [[FormObject alloc] initWithDisabledSite:formURLString];
            [[AutoFillManager defaultManager] saveFormData:formData];
            formData = nil;

        }
        else {
            self.formData = nil;
        }
    }
}



- (JSContext *)getCurrentJavascriptContext {
    JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"JSTools" ofType:@"js"];
    NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];
    [context evaluateScript:scriptString];
    context[@"print"] = ^(NSString *string) {
        NSLog(@"%@",string);
    };

    __weak AutoFillManager *afm = [AutoFillManager defaultManager];
    __weak UIToolbar *weakToolbar = toolbar;
    __weak typeof(self) weakSelf = self;

    context[@"receiveForm"] = ^(NSString *urlString, NSString *username, NSString *usernameFieldID, NSString *usernameFieldName, NSString *password, NSString *passwordFieldID, NSString *passwordFieldName) {

        NSURL *url = [NSURL URLWithString:urlString];

        for (NSDictionary *savedForm in [afm savedForms]) {
            FormObject *form = [[FormObject alloc] initWithFormDictionary:savedForm];
            if ([[url host] isEqualToString:form.urlString] && [username isEqualToString:form.username] && form.neverForThisSite == NO) {
#ifdef DEBUG
                NSLog(@"Site already saved");
#endif
                return;
            }

            if ([[url host] isEqualToString:form.urlString] && form.neverForThisSite == YES) {
#ifdef DEBUG
                NSLog(@"Never for this site");
#endif
                return;
            }
        }

        formData = [[FormObject alloc] initWithUsername:username withUsernameID:usernameFieldID withUsernameName:usernameFieldName withPassword:password withPasswordID:passwordFieldID withPasswordName:passwordFieldName withURLString:[url host]];
        UIActionSheet *saveFormConfirmationActionSheet = [[UIActionSheet alloc] initWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Would you like to save this password?", @"Would you like to save this password?")] delegate:weakSelf cancelButtonTitle:NSLocalizedString(@"Not Now", @"Not Now") destructiveButtonTitle:nil otherButtonTitles:NSLocalizedString(@"Yes", @"Yes"), NSLocalizedString(@"Never for this Website", @"Never for this Website"), nil];
        saveFormConfirmationActionSheet.tag = kSaveFormConfirmationActionSheetTag;
        [saveFormConfirmationActionSheet showFromToolbar:weakToolbar];
    };

    return context;
}

I know the issue Is because of

formData = [[FormObject alloc] initWithUsername:username withUsernameID:usernameFieldID withUsernameName:usernameFieldName withPassword:password withPasswordID:passwordFieldID withPasswordName:passwordFieldName withURLString:[url host]];

If I add the specifier __weak Xcode will warn me to change it to __block. If I change it to __block then formData will still be nil afterwards.

How can I set the ivar in this block properly?

SOLUTION

The solution was to make the ivar a property and use weak self to set the property.

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
    if ([ButtonTitle isEqualToString:NSLocalizedString(@"Never for this Website", @"Never for this Website")]) {
            NSString *formURLString = [self.formData.urlString copy];
            self.formData = nil;
            self.formData = [[FormObject alloc] initWithDisabledSite:formURLString];
            [[AutoFillManager defaultManager] saveFormData:self.formData];
            self.formData = nil;

        }
        else {
            self.formData = nil;
        }
    }
}


- (JSContext *)getCurrentJavascriptContext {
    JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"JSTools" ofType:@"js"];
    NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];
    [context evaluateScript:scriptString];
    context[@"print"] = ^(NSString *string) {
        NSLog(@"%@",string);
    };

    __weak AutoFillManager *afm = [AutoFillManager defaultManager];
    __weak UIToolbar *weakToolbar = toolbar;
    __weak typeof(self) weakSelf = self;

    context[@"receiveForm"] = ^(NSString *urlString, NSString *username, NSString *usernameFieldID, NSString *usernameFieldName, NSString *password, NSString *passwordFieldID, NSString *passwordFieldName) {

        NSURL *url = [NSURL URLWithString:urlString];

        for (NSDictionary *savedForm in [afm savedForms]) {
            FormObject *form = [[FormObject alloc] initWithFormDictionary:savedForm];
            if ([[url host] isEqualToString:form.urlString] && [username isEqualToString:form.username] && form.neverForThisSite == NO) {
#ifdef DEBUG
                NSLog(@"Site already saved");
#endif
                return;
            }

            if ([[url host] isEqualToString:form.urlString] && form.neverForThisSite == YES) {
#ifdef DEBUG
                NSLog(@"Never for this site");
#endif
                return;
            }
        }

        weakSelf.formData = [[FormObject alloc] initWithUsername:username withUsernameID:usernameFieldID withUsernameName:usernameFieldName withPassword:password withPasswordID:passwordFieldID withPasswordName:passwordFieldName withURLString:[url host]];
        UIActionSheet *saveFormConfirmationActionSheet = [[UIActionSheet alloc] initWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Would you like to save this password?", @"Would you like to save this password?")] delegate:weakSelf cancelButtonTitle:NSLocalizedString(@"Not Now", @"Not Now") destructiveButtonTitle:nil otherButtonTitles:NSLocalizedString(@"Yes", @"Yes"), NSLocalizedString(@"Never for this Website", @"Never for this Website"), nil];
        saveFormConfirmationActionSheet.tag = kSaveFormConfirmationActionSheetTag;
        [saveFormConfirmationActionSheet showFromToolbar:weakToolbar];
    };

    return context;
}
Maximilian Litteral
  • 3,059
  • 2
  • 31
  • 41
  • 2
    You will create retain cycle only if use `self` inside block. Why everybody afraid retain cycles in blocks as fire? – Cy-4AH Jan 19 '14 at 09:22
  • No. I have used instruments and setting this ivar to something from nil causes a retain cycle. – Maximilian Litteral Jan 19 '14 at 09:23
  • editing out the 1 line setting it causes the app to run about 100MB higher than it should. so its very important to figure this out – Maximilian Litteral Jan 19 '14 at 09:24
  • 1
    @Cy-4AH not really,retain cycle will crash your app, so we have to handle it seriously. – johnMa Jan 19 '14 at 09:24
  • 3
    I think you have to show a little more code. – Martin R Jan 19 '14 at 09:29
  • Its a huge method but has nothing to do with this real. just setting formData which is a ivar of the view controller will make it so the view controller is not released. So i need to find a way to set it without __Weak, __Block and have it not have a retain cycle – Maximilian Litteral Jan 19 '14 at 09:32
  • 2
    You do need to show more code as Martin says. You've said Xcode gives you various warnings/errors if you try things - show the code you tried and the messages Xcode gave. The standard solution to your issue is to use a weak reference, you say Xcode won't let you do that - folk need to see your code to figure out why. – CRD Jan 19 '14 at 09:42

1 Answers1

6

Accessing an instance variable inside a block will cause the block to capture a reference to the object that owns the instance variable - self.

You can avoid this by converting the instance variable to a property, creating a weak reference to self, and setting the property on the weak self.

Alternatively you can use -> to directly access the ivar, again on a weak self, but I prefer the former solution.

jrturton
  • 118,105
  • 32
  • 252
  • 268
  • It seems like we also have to keep a local strong reference to weak reference so it won't be nilled randomly. Where should we do that? at the first line of the block? Prior to block, to the main object `__strong __typeof(_self)strongSelf = weakSelf;` – frankish Jan 25 '14 at 16:20
  • You only need to do that if you think it is likely to happen, or if you care. In most use cases, if the object in question has been deallocated, then you're not bothered - it's typically a view controller that's been removed or something. Making a strong reference is clutter in that case. – jrturton Jan 25 '14 at 21:31
  • ...and in any case, never _prior_ to the block! That would reintroduce the retain cycle. – jrturton Jan 25 '14 at 21:33