The title says it, I'd like to ask why the following does not work, as it should imho.
// ViewController.m
#import "B.h"
...
#implementation ViewController
{
B *bInstance;
}
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])
{
bInstance = [[B alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
[bInstance setBlockCalled:^(NSDictionary *dict) {
NSLog(#"%#", dict[#"key"]);
}];
[self.view addSubview:bInstance];
}
return self;
}
// B.h
#import <UIKit/UIKit.h>
#interface B : UIView
#property (nonatomic, copy) void (^blockCalled)(NSDictionary *);
#end
// B.m
#import "B.h"
#import "A.h"
#implementation B
{
A *aInstance;
void (^blockCalled)(NSDictionary *);
}
#synthesize blockCalled;
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
aInstance = [[A alloc] initWithFrame:frame];
[aInstance setBlockCalled:blockCalled];
[self addSubview:aInstance];
}
return self;
}
#end
// A.h
#import <UIKit/UIKit.h>
#interface A : UIView
#property (nonatomic, copy) void (^blockCalled)(NSDictionary *);
#end
// A.m
#import "A.h"
#implementation A
{
void (^blockCalled)(NSDictionary *);
}
#synthesize blockCalled;
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setFrame:CGRectMake(0, 0, 100, 100)];
[button setBackgroundColor:[UIColor redColor]];
[button addTarget:self action:#selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button];
}
return self;
}
- (void) buttonClicked
{
blockCalled(#{#"key":#"value"});
}
#end
What I want to do is 'traverse up the view hiearchy', and as far as I see it, I'm assigning a block variable with the same arguments, so I'd expect it to work. Any reason why this is a wrong idea?
EDIT: added more complete example as of when this issue might happen.
EDIT2: added MCVE, which I've tested.
After I've checked the MCVE, the code crashes on the line blockCalled(#{#"key":#"value"}); in A.m because the blockCalled is nil.
Updated question: I'd like to know why calling [aInstance setBlockCalled:blockCalled] doesn't set the blockCalled in A, as it seems to me to be the same as
[aInstance setBlockCalled:^(NSDictionary *dict)
{
__strong typeof (self) strongSelf = self;
strongSelf.blockCalled(dict);
}];
#implementation ViewController
{
B *bInstance;
}
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])
{
bInstance = [[B alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
[bInstance setBlockCalled:^(NSDictionary *dict) {
NSLog(#"%#", dict[#"key"]);
}];
[self.view addSubview:bInstance];
}
At the moment of initializing bInstance, you haven't set the block yet. It means
aInstance = [[A alloc] initWithFrame:frame];
[aInstance setBlockCalled:blockCalled];
is called before
[bInstance setBlockCalled:^(NSDictionary *dict) {
NSLog(#"%#", dict[#"key"]);
}];
You should override the block setter in B and call it on A.
// B.m
-(void)setBlockCalled(void(^)(NSDictionary*))passedBlock{
[a setBlockCalled:passedBlock];
}
The code in your question (a) uses ivars and #synthesize statements that are unnecessary; and (b) this code snippet is insufficient to reproduce the crash you describe.
Having said that, there are two possible source of crashes that are suggested by the code sample in the question, namely: (a) the code fails to remove the observer if A is deallocated; and (b) it really should double-check to make sure that blocks are non-nil before trying to call them.
But, consider the following:
// A.h
#interface A : UIView
#property (nonatomic, copy) void (^blockCalled)(NSDictionary *dict);
#end
// A.m
#implementation A
- (instancetype) initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleNotification:) name:#"kNotification" object:nil];
}
return self;
}
- (void)handleNotification:(NSNotification *)notification {
// never just call `blockCalled`; always check to see if not null
if (self.blockCalled) {
// everything is good, so let's call the block
self.blockCalled(notification.userInfo);
}
}
- (void)dealloc {
// never just `addObserver`; make sure to remove the observer when this is deallocated
[[NSNotificationCenter defaultCenter] removeObserver:self name:#"kNotification" object:nil];
}
#end
// B.h
#interface B : UIView
#property (nonatomic, copy) void (^blockCalled)(NSDictionary *dict);
#property (nonatomic, strong) A *aInstance;
#end
// B.m
#implementation B
- (void) someMethod {
// !!!!! why this crashes the app when blockCalled on aInstance is called:
[self.aInstance setBlockCalled:self.blockCalled];
// but this does not crash when the same happens
__weak typeof (self) weakSelf = self;
self.aInstance.blockCalled = ^(NSDictionary *dict) {
__strong typeof (self) strongSelf = weakSelf;
// again, never just call `blockCalled`; always check to see if not null
if (strongSelf.blockCalled) {
strongSelf.blockCalled(dict);
}
};
}
#end
Bottom line, when I fix the two obvious sources of crashes (failing to remove observer and failing to check to make sure the block was non-nil before calling it), and test it with logical scenario, it seems to work fine:
- (void)viewDidLoad {
[super viewDidLoad];
B *b = [[B alloc] init];
b.blockCalled = ^(NSDictionary *dict) {
NSLog(#"%#", dict);
};
A *a = [[A alloc] init];
b.aInstance = a;
[b someMethod];
[self.view addSubview:a];
[self.view addSubview:b];
NSDictionary *dict = #{#"foo": #"bar", #"baz": #"qux"};
[[NSNotificationCenter defaultCenter] postNotificationName:#"kNotification" object:nil userInfo:dict];
}
So, assuming one of these two issues wasn't the source of the problem, you must provide MCVE that we can use to reproduce the crash you describe.
Related
I have a custom UIWebView (EpubWebView), with a custom NSURLCache (EpubCache) for handling requests.
i created a custom delegate for handling request.
EpubCache.h
#protocol EpubCacheDelegate <NSObject>
#required
- (NSCachedURLResponse *)hadleRequest:(NSURLRequest *)request;
#end
#interface EpubCache : NSURLCache
#property (nonatomic, weak) id <EpubCacheDelegate> cacheDelegate;
#end
EpubCache.m
import "EpubCache.h"
#interface EpubCache ()
#end
#implementation EpubCache
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
return [self.cacheDelegate hadleRequest:request];
}
#end
EpubWebView .h
#interface EpubWebView : UIWebView <UIWebViewDelegate, EpubCacheDelegate>
#property (strong, nonatomic) EpubCache *mLocalCache;
#end
EpubWebView.m
- (void) localInit
{
self.mLocalCache = [[EpubCache alloc] init];
self.mLocalCache.cacheDelegate = self;
[NSURLCache setSharedURLCache:self.mLocalCache];
}
- (NSCachedURLResponse *)hadleRequest:(NSURLRequest *)request
{
// return handled request
}
on the other hand i have a navigationcontroller with a tableview and the destination view controller have this webview.
when i ran the app and click on an item in tableview, everything is fine and delegate works as expected.
if i click back and click on other item in tableview, things goes wrong, the cachedResponseForRequest getting called but the hadleRequest wont, i checked and findout that the delegate is null!
i can not figure out what is happening here.
any help would be appreciated.
UPDATE 1
EpubWebView.m
- (id)init
{
self = [super init];
if (self)
{
[self localInit];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self)
{
[self localInit];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if(self)
{
[self localInit];
}
return self;
}
UPDATE 2
the segue of the tableview that bring up the view controller that contain EpubWebView
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSIndexPath *indexPath = [self.collectionView indexPathForCell:sender];
BookViewController *bookController = segue.destinationViewController;
bookController.mBook = booksList[indexPath.row];
}
and BookViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
mWebView = [[EpubWebView alloc] initWithFrame:self.mainView.frame];
[self.mainView addSubView:mWebView];
[mWebView setBook:self.mBook];
}
You need to make a few modifications. First, remove your "localInit" method, then create a new function in EPubWebView:
- (void) setCache: (EpubCache *)localCache
{
localCache.cacheDelegate = self;
}
Now, you can create and hold onto your cache in your BookViewController via these lines in the .m file:
#interface BookViewController ()
#property (strong, nonatomic) EpubCache *mLocalCache;
#end
and change your BookViewController's viewDidLoad method to look like:
- (void) viewDidLoad {
self.mLocalCache = [[EpubCache alloc] init];
// only need to do this once, at viewDidLoad time
[NSURLCache setSharedURLCache:self.mLocalCache ];
[super viewDidLoad];
mWebView = [[EpubWebView alloc] initWithFrame:self.mainView.frame];
[mWebView setCache:self.mLocalCache];
[self.mainView addSubView:mWebView];
[mWebView setBook:self.mBook];
}
I have a button which I'm trying to set the image for when a thread is finished. My code may seem useless to just change the image button but this is for practise and going onto a bigger thing.
Goal: Set the image for the b0 as button_x.png (found in the project).
Issue: The button image is not changing, if I make a connection between the button found on the storyboard to the viewController button declaration I get a crash with -[UIButton setImage:]: unrecognized selector sent to instance 0x8b4c270.
Thank you.
ViewController.h:
#interface ViewController : UIViewController {
IBOutlet UIButton *b0;
Game *instanceOfGame;
}
#property (nonatomic, strong) UIButton *b0;
#property (nonatomic, strong) Game *instanceOfGame;
#end
ViewController.m:
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize instanceOfGame;
#synthesize b0;
static NSString *postName = #"123";
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(post:) name:postName object:nil];
instanceOfGame = [[Game alloc] init];
[instanceOfGame start];
}
- (void) post: (NSNotification *) result {
NSNumber *str = [result object];
if (str == 0) {
[self.b0 performSelectorOnMainThread:#selector(setImage: ) withObject:[UIImage imageNamed:#"button_x.png"] waitUntilDone:NO];
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
Game.h:
#import <Foundation/Foundation.h>
#interface Game : NSObject
#property (nonatomic, strong) NSNumber *choice;
#property (nonatomic, strong) NSNotificationCenter *ns;
-(void) start;
#end
Game.m:
#import "Game.h"
#implementation Game
NSNotificationCenter *ns;
#synthesize ns;
#synthesize choice;
static NSString *postName = #"123";
-(id) init {
if (self = [super init]) {
ns = [NSNotificationCenter defaultCenter];
}
return self;
}
-(void) start {
[self performSelectorInBackground:#selector(loadData) withObject:Nil];
}
-(void) loadData {
choice = 0;
[ns postNotificationName:postName object:choice];
}
#end
UIButton doesn't have a setImage: function, hence the exception being thrown.
It does have this function though:
- (void)setImage:(UIImage *)image forState:(UIControlState)state
Try replacing:
[self.b0 performSelectorOnMainThread:#selector(setImage: ) withObject:[UIImage imageNamed:#"button_x.png"] waitUntilDone:NO];
With a similar sort of implementation done using GCD calling the correct function:
dispatch_async(dispatch_get_main_queue(), ^{
[self.b0 setImage:[UIImage imageNamed:#"button_x.png"] forState:UIControlStateNormal];
});
#Colin Cornaby saying write.
UIButton doesn't have setImage method.
You can also use the same existing code. just update below line
[self.b0 performSelectorOnMainThread:#selector(setImage: ) withObject:[UIImage imageNamed:#"button_x.png"] waitUntilDone:NO];
as
[self performSelectorOnMainThread:#selector(setImage: ) withObject:[UIImage imageNamed:#"ListDropDown.png"] waitUntilDone:NO];
and then define method setImage:
-(void)setImage:(UIImage *)img
{
[self.bo setImage:img forState:UIControlStateNormal];
}
I hope it will work for you
Try this,
if (str == 0) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self.b0 setImage:[UIImage imageNamed:#"button_x.png"] forState:UIControlStateNormal];
});
}
I got a little problem. Here's my source code:
The .h:
#import <UIKit/UIKit.h>
#interface TBNoteViewController : UIViewController <UITextViewDelegate>
#property (nonatomic, copy) NSString *noteKey;
- (IBAction)dismissKeyboard:(id)sender;
- (id)initWithNoteIndex:(int)index;
#end
The .m:
#import "TBNoteViewController.h"
#import "PSPDFTextView.h"
#include <tgmath.h>
#interface TBNoteViewController ()
{
CGRect _keyboardRect;
BOOL _keyboardVisible;
}
#property (nonatomic, strong) UIBarButtonItem *rightBarButton;
#property (nonatomic, strong) PSPDFTextView *textView;
#end
#implementation TBNoteViewController
#synthesize noteKey;
- (id)initWithNoteIndex:(int)index
{
self = [super init];
if (self) {
NSMutableArray *data = [[NSMutableArray alloc] initWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:#"Notes"] copyItems:YES];
self.noteKey = [NSString stringWithFormat:#"%#", [data objectAtIndex:index]];
NSLog(#"1: %#", self.noteKey);
}
return self;
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
NSLog(#"2: %#", self.noteKey);
// Register notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
_rightBarButton = [self.navigationItem rightBarButtonItem];
self.navigationItem.rightBarButtonItem = nil;
PSPDFTextView *textView = [[PSPDFTextView alloc] initWithFrame:self.view.bounds];
textView.delegate = self;
textView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
textView.font = [UIFont systemFontOfSize:17.f];
[self.view addSubview:textView];
self.textView = textView;
[self.textView setTextContainerInset:UIEdgeInsetsMake(5, 12, 5, 12)];
self.textView.alwaysBounceVertical = YES;
self.textView.keyboardAppearance = UIKeyboardAppearanceDark;
textView.text = self.noteKey;
[super viewDidLoad];
// Do any additional setup after loading the view.
}
So my problem is that the property noteKey has a string, but in viewDidLoad it is null.
So, you can see the NSLogs, the output would be (just copied that):
2014-02-13 23:07:21.197 TaskBeater[3205:70b] 1: Hoho.
2014-02-13 23:07:21.199 TaskBeater[3205:70b] 2: (null)
I would highly appreciate if you could help me, I have no idea what's wrong there.
It looks to me like you're instantiating two different "TBNoteViewController" instances.
You're doing one programatically (in code), which is how you're getting the "noteKey" property to appear properly.
Then the second one is created because you loaded it from a XIB file, which is why "viewDidLoad:" gets called.
After getting rid of the extra one that you're calling in code, create an outlet to the one you created via the XIB file, and then you can set properties on that and you should be all set.
Let's see if you guys can find the error here... because I'm really stuck right now.
DBManager.h
#protocol DBManagerDelegate <NSObject>
#optional
- (void) managerDidFinishUpdating:(id)controller;
#end
#interface DBManager : NSObject
#property (strong, nonatomic) id <DBManagerDelegate> delegate;
- (id) init;
#end
DBManager.m
#import "DBManager.h"
#implementation DBManager
- (id)init {
self = [super init];
if (self) {
[[self delegate] managerDidFinishUpdating:self];
}
return self;
}
UIViewController.h
#import <UIKit/UIKit.h>
#import "DBManager.h"
#interface DBViewController : UIViewController <DBManagerDelegate>
#property (nonatomic, retain) DBManager *manager;
#end
UIViewController.m
#import "DBViewController.h"
#implementation DBViewController
- (void)viewDidLoad
{
[super viewDidLoad];
_manager = [[DBMataroManager alloc] init];
[_manager setDelegate:self];
}
- (void)managerDidFinishUpdating:(id)controller {
NSLog(#"Manager did finish");
}
#end
You should write like this,
- (id)initWithDelegate:(id)delegate {
self = [super init];
if (self) {
_delegate = delegate;
[_delegate managerDidFinishUpdating:self];
}
return self;
}
//Call it as follow
_manager = [[DBManager alloc] initWithDelegate:self];
[_manager setDelegate:self];
The delegate method managerDidFinishUpdating: is called within init. You set yourself as the delegate after calling init.
Maybe an initWithDelegate: method would help.
- (instancetype)initWithDelegate:(id< DBManagerDelegate>)delegate {
self = [super init];
if (self) {
self.delegate = delegate;
[self.delegate managerDidFinishUpdating:self];
}
return self;
}
You are trying to use the delegate in the init, but you have not set it until the next statement. The only way to do this would be to set the delegate as part of you init statement.
-(id)initWithDelegate:(id<DBManageDelegate>)delegate {
if ((self = [super init])) {
self.delegate = delegate;
if (delegate && [delegate respondsToSelector:#selector(managerDidFinishUpdating:)]) {
[delegate performSelector:#selector(managerDidFinishUpdating:) withObject:self];
}
}
return self;
}
But if you plan to do some additional things before the manager is updated I would suggest you move all of that outside of the init, perhaps into an updateManager function like this
-(void)updateManager {
// Do the stuff that updates your manager here
if (delegate && [delegate respondsToSelector:#selector(managerDidFinishUpdating:)]) {
[delegate performSelector:#selector(managerDidFinishUpdating:) withObject:self];
}
}
..later in your app
_manager = [[DBMataroManager alloc] init];
[_manager setDelegate:self];
[_manager updateManager];
I'm trying to structure some view-code out of the controller (obviously without using a nib). Therefore, I tried a simple example, but for some reason, I can't add the target to the button in the controller, rest is fine. Here's what I'm trying:
Controller.h:
#import <UIKit/UIKit.h>
#import "IndexView.h"
#interface IndexController : UIViewController
{
}
#property (nonatomic) IndexView *contentView;
#end
Controller.m
#import "IndexController.h"
#import "IndexView.h"
#interface IndexController ()
#end
#implementation IndexController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.contentView = [[IndexView alloc]init];
[self.view addSubview:self.contentView];
[self connectUIElements];
}
return self;
}
- (void) connectUIElements
{
[self.contentView.testButton addTarget:self action:#selector(testButtonClicked) forControlEvents:UIControlEventTouchUpInside];
}
#pragma mark --
#pragma mark UIHandler
- (void) testButtonClicked
{
NSLog(#"testbutton clicked");
}
#end
View.h
#import <UIKit/UIKit.h>
#interface IndexView : UIView
#property (nonatomic, strong) UIButton *testButton;
#end
View.m
#import "IndexView.h"
#implementation IndexView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setUpView];
}
return self;
}
- (id) init
{
self = [super init];
if (self) {
[self setUpView];
}
return self;
}
- (void) setUpView
{
[self setBackgroundColor:[UIColor whiteColor]];
self.testButton = [[UIButton alloc]initWithFrame:CGRectMake(10, 50, 100, 50)];
self.testButton.backgroundColor = [UIColor greenColor];
[self.testButton setTitle:#"hello, world" forState:UIControlStateNormal];
[self addSubview:self.testButton];
}
#end
I'm just exploring possibilities to get closer to a classical MVC pattern and a little farther away from Apples mediator-interpretation. Any idea what's wrong?
You have to create your view using initWithFrame:, which is the default initializer for UIView. (The reason that is does not work with a simple init - but I am guessing here! - might be that the views size is zero and therefore the touch events do not work.)