I run this code inside my viewDidLoad method to fetch data from Firebase to put it in a UIPageViewController
#interface MapViewController () <RoutesPageDelegate>
#property (weak, nonatomic) IBOutlet MKMapView *mapView;
#property (weak, nonatomic) RoutesPageViewController *routesPageViewController;
#property (weak, nonatomic) FIRFirestore *db;
#end
#implementation MapViewController
- (void) viewDidLoad {
[super viewDidLoad];
self.db = [FIRFirestore firestore];
for (UIViewController *obj in self.childViewControllers) {
if ([obj isKindOfClass:[RoutesPageViewController class]]) {
self.routesPageViewController = (RoutesPageViewController *)obj;
self.routesPageViewController.routesPageDelegate = self;
}
}
FIRCollectionReference *routesRef = [self.db collectionWithPath:#"routes"];
[routesRef getDocumentsWithCompletion:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (error != nil) {
// TODO: handle error
} else {
NSMutableArray<RouteModel*> *routes = [NSMutableArray array];
// For each route
for (FIRDocumentSnapshot *document in snapshot.documents) {
RouteModel *route = [[RouteModel alloc] init];
route.title = document.data[#"title"];
route.color = document.data[#"color"];
route.city = document.data[#"city"];
[routes addObject:route];
}
[self.routesPageViewController setRoutes:routes];
}
}];
}
And this is the called setRoutes method:
- (void) setRoutes:(NSMutableArray<RouteModel *> *)routes {
self.routes = routes;
NSMutableArray<RoutePageViewController *> * routeViewControllers = [NSMutableArray array];
for (RouteModel * route in routes) {
[routeViewControllers addObject:[self viewControllerAtIndex:[routes indexOfObject:route]]];
}
[self setViewControllers:routeViewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
When the setRoutes method gets executed it throws the error in the image below, saying that it cannot dereference it:
The setRoutes methods gets executed inside a block.
And I get this weird thread stack:
How can I solve this?
Your problem is here:
- (void) setRoutes:(NSMutableArray<RouteModel *> *)routes {
self.routes = routes;
invoking self.routes implicity calles the setter setRoutes which causes a recursive infinite calls as indicated by your stack.
At the time the block is passed onto getDocumentsWithCompletion method is executed, routes array has already been deallocated and set to nil because no one is retaining it anywhere outside the block.
You should either move it into the block or declare it as a class property so it won't be thrown out of memory while class instance is alive.
[routesRef getDocumentsWithCompletion:^(FIRQuerySnapshot *snapshot, NSError *error) {
NSMutableArray<RouteModel*> *routes = [NSMutableArray array];
...
[self.routesPageViewController setRoutes:routes];
}];
After the update:
Doing self.routes = routes invokes setRoutes: which in turn is causing a loop in your code. You should change it to:
- (void)setRoutes:(NSMutableArray<RouteModel *> *)routes {
if (_routes != routes) {
_routes = routes;
...
}
}
Related
I have a ContentPageViewController class, it has the IBOutlet stuff. I write my getter of ContentPageViewController in the ViewController like the following code.
ContentPageViewController.h
#interface ContentPageViewController : UIViewController
#property (weak, nonatomic) IBOutlet UILabel *busName;
#property (weak, nonatomic) IBOutlet UILabel *busTime;
#property (weak, nonatomic) IBOutlet UILabel *busType;
#end
ViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
// instantiation from a storyboard
ContentPageViewController *page = [self.storyboard instantiateViewControllerWithIdentifier:#"ContentPageViewController"];
self.page = page;
// send url request
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"https://api.apb-shuttle.info/now" ]];
[self sendURLRequest:request];
// add the view of ContendPageViewController into ViewController
[self.view addSubview:self.page.view];
}
// It works if i remove the following code
- (ContentPageVC *)page
{
if (_page) _page = [[ContentPageViewController alloc] init];
return _page;
}
Nothing happened when I updated it. And it gave me a nil.
- (void)updateUI
{
// I got null here
NSLog("%#", self.page.busName)
// The spacing style font
NSDictionary *titleAttributes = #{
NSKernAttributeName: #10.0f
};
NSDictionary *attributes = #{
NSKernAttributeName: #5.0f
};
self.page.busName.attributedText = [[NSMutableAttributedString alloc] initWithString:bus.name
attributes:titleAttributes];
self.page.busTime.attributedText = [[NSMutableAttributedString alloc] initWithString:bus.depart
attributes:titleAttributes];
self.page.busType.attributedText = [[NSMutableAttributedString alloc] initWithString:bus.note
attributes:attributes];
}
The following code is when I called the updateUI:
- (void)sendURLRequest:(NSURLRequest *)requestObj
{
isLoading = YES;
[RequestHandler PerformRequestHandler:requestObj withCompletionHandler:^(NSDictionary *data, NSError *error) {
if (!error) {
bus = [JSONParser JSON2Bus:data];
// Add the bus object into the array.
[self.busArray addObject: bus];
[[NSOperationQueue mainQueue] addOperationWithBlock: ^{
[self updateUI];
isLoading = NO;
}];
} else {
NSLog(#"%#", [error localizedDescription]);
}
}];
}
But it worked if I removed the getter above.
I have no idea how it works, please give me some hint. Thanks.
Check your IBOutlet is connected.
Check the method you are calling isn't called before the view is created from the storyboard/nib
EDIT
The lines of code that you added, are overriding your getter. And every time you call self.page, your creating a new instance!
// It works if i remove the following code
- (ContentPageVC *)page
{
if (_page) _page = [[ContentPageViewController alloc] init];
return _page;
}
It should be like so:
// It works if i remove the following code
- (ContentPageVC *)page
{
if (!_page) _page = [[ContentPageViewController alloc] init]; // Added the ! mark, only if nil you would create a new instance.
return _page;
}
Plus you are calling alloc init on it, so Its not the same instance from storyboard!
So you should do this:
- (ContentPageVC *)page
{
if (!_page) _page = [self.storyboard instantiateViewControllerWithIdentifier:#"ContentPageViewController"];
return _page;
}
And remove this lines of code:
// instantiation from a storyboard
ContentPageViewController *page = [self.storyboard instantiateViewControllerWithIdentifier:#"ContentPageViewController"];
self.page = page;
Every time you call "self.page" the override getter function will call. and return the same instance.
Is there anyway to check the value of 'type' variable with completionHandler.
-(void)sendApiMethod:(NSString*)apiName ApiType:(NSString*)type
{
[SendAPI setAPIWithName:#"APIName" completionHandler:^(NSArray *errors) {
if([type isEqualToString:#"Login"])
{
/// Call Some Other function
}
}];
}
I wrote a small piece of code to verify if works (only reading your question I would say yes as Droppy)
I added all there code in a ViewController in a Simple View App.
some assumption:
- all code there for sake of semplicity ....
- I have added a singleton as it seems You are calling a class method.
- instance method is a bit rough, it simply saves name and block
- I added a typedef for blocks to better reading it.
#import "ViewController.h"
typedef void (^CompletionBlock)(NSArray *errors);
#interface SendAPI : NSObject
-(void)setAPIWithName:(NSString*)name completionHandler: (CompletionBlock)completionHandler;
+(void)setAPIWithName:(NSString*)name completionHandler: (CompletionBlock)completionHandler;
+(SendAPI*)sharedInstance;
#property (strong) CompletionBlock completionBlock;
#property (strong) NSString * name;
#end
#implementation SendAPI : NSObject
static SendAPI * _singleton = nil;
+(SendAPI*)sharedInstance
{
if (_singleton == nil)
{
_singleton = [[SendAPI alloc] init];
}
return _singleton;
}
-(void)setAPIWithName:(NSString*)name completionHandler: (CompletionBlock)completionHandler;
{
self.completionBlock = completionHandler;
self.name = [name copy];
__weak SendAPI * weakRef = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSError* err = [NSError errorWithDomain: #"delayed"
code:1111
userInfo: #{#"info": self.name}
];
weakRef.completionBlock(#[err]);
});
}
+(void)setAPIWithName:(NSString*)name completionHandler: (CompletionBlock)completionHandler;
{
[[SendAPI sharedInstance]setAPIWithName:name completionHandler:completionHandler];
}
#end
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self sendApiMethod:#"HELLO" ApiType: #"Login"];
}
-(void)sendApiMethod:(NSString*)apiName ApiType:(NSString*)type{
[SendAPI setAPIWithName:#"APIName" completionHandler:^(NSArray *errors) {
if([type isEqualToString:#"Login"])
{
/// Call Some Other function
NSLog(#"%#", errors);
}
}];
}
it does LOG correctly
Have a question about blocks in objective-c.
For example I have a list of actions.
I'm initializing an array of blocks:
self.actions = #[
^() { [self showObject:self.object_1]; },
^() { [self showObject:self.object_2]; },
^() { [self showObject:self.object_3]; }
];
And calling them when some row is pressed:
- (void)pressedRowAtIndex:(NSInteger)index {
if (index < actions.count) {
void (^action)() = [actions objectAtIndex:index];
if (action != nil) {
action();
}
}
}
And all works fine without problem. But when I init my actions array by using initWithObjects method:
self.actions = [NSArray alloc] initWithObjects:
^() { [self showObject:self.object_1]; },
^() { [self showObject:self.object_2]; },
^() { [self showObject:self.object_3]; },
nil
];
Than I get crash trying to get action by index by using objectAtIndex method of NSArray class.
I understand the difference between this inits. First one don't increase reference count like first do. But can someone explain why it crash?
Edit:
All that I've found. Maybe I'm nub and somewhere else is another useful information.
There is no crash info in terminal:
Code for Onik IV:
Small example:
#interface ViewController () {
NSArray *actions;
}
#property (nonatomic, strong) NSString *object1;
#property (nonatomic, strong) NSString *object2;
#property (nonatomic, strong) NSString *object3;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
actions = [[NSArray alloc] initWithObjects:
^() { [self showObject:self.object1];},
^() { [self showObject:self.object2]; },
^() {[self showObject:self.object3]; },
nil];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.object1 = #"object 1";
self.object2 = #"object 2";
self.object3 = #"object 3";
void(^firsSimpleBlock)(void) = [actions lastObject];
firsSimpleBlock();
void(^simpleBlock)(void) = [actions firstObject];
simpleBlock();
}
-(void)showObject:(NSString *)object
{
NSLog(#"Show: %#",object);
}
#end
Try something like this.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
(^someBlock)(void) = ^void(void){
self.object1;
};
actions = [[NSArray alloc] initWithObjects:
[someBlock copy],
[someOtherBlock copy],
[anotherBlock copy],
nil];
}
Blocks are allocated on the stack and are therefor removed when the frame is removed from the stack leading to sail pointers for all pointers pointing to that block. When you allocate a object with the literal "#" sign the object is allocated in a pool so all literals that are the "same" point to the same instance and are never deallocated.
NSString *a = #"A";
NSString *b = #"A";
points to the same instance of a string, while:
NSString *a = [NSString stringWithFormat:#"A"];
NSString *b = [NSString stringWithFormat:#"A"];
are two different objects.
So it works when you are creating a literal array but when you add the blocks dynamically they will be removed when its time to use them therefor the BAD_ACCESS. Solution is to send "copy" message to the block that will copy it to the heap and the block will not be released.
It´s the same, you must have another kind of problem (sintax?).
Try this:
#interface ViewController ()
#property (nonatomic, strong) NSString *object1;
#property (nonatomic, strong) NSString *object2;
#property (nonatomic, strong) NSString *object3;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.object1 = #"object 1";
self.object2 = #"object 2";
self.object3 = #"object 3";
NSArray *actions = #[^() { [self showObject:self.object1];},
^() { [self showObject:self.object2]; },
^() {[self showObject:self.object3]; }
];
NSArray *secondActions = [[NSArray alloc] initWithObjects:
^() { [self showObject:self.object1];},
^() { [self showObject:self.object2]; },
^() { [self showObject:self.object3];},
nil
];
void(^firsSimpleBlock)(void) = [actions lastObject];
firsSimpleBlock();
void(^simpleBlock)(void) = [secondActions firstObject];
simpleBlock();
}
-(void)showObject:(NSString *)object
{
NSLog(#"Show: %#",object);
}
#end
It seems if I do something like:
NSMutableArray *randomSelection = [[NSMutableArray alloc] init];
Then this needs to be in a function, and I can't modify it later using a different function.
I tried just instantiating it in the .h file,
#interface ViewController:
{
NSMutableArray *Values;
}
But then when I try to append to it during runtime, nothing happens. I try to append to it with this:
int intRSSI = [RSSI intValue];
NSString* myRSSI = [#(intRSSI) stringValue];
[Values addObject:myRSSI];
But the array remains empty when I do this.
How can I fix this?
The recommended way is to create a property;
// ViewController.h
#interface ViewController : UIViewController
{
}
#property (nonatomic, strong) NSMutableArray *values;
#end
Then override the getter for that property, to lazy-initialize it, i.e. the array will be allocated and initialized on first call of the NSMutableArray property's getter:
// ViewController.m
#interface ViewController ()
#end
#implementation ViewController
- (NSMutableArray *)values
{
if (!_values) {
_values = [[NSMutableArray alloc] init];
}
return _values;
}
- (void)viewDidLoad
{
[super viewDidLoad];
//int intRSSI = [RSSI intValue];
//NSString *myRSSI = [#(intRSSI) stringValue];
//[self.values addObject:myRSSI];
// Keep it simple:
[self.values addObject:RSSI];
}
#end
New to the site and Obj C. Attempting to get a pitch value from Device Motion (working), put into an array with the most recent 60 values (not working) and select the maximum value within the array. With each new pitch value from the device, new pitch value is added to the array and the 61st value is dropped. When I hook up my phone and run, I get the log values for the pitch and maxPitch; however, I am not getting the array of 60 values so I don't believe it is working properly. Any help is greatly appreciated.
I believe the problem may be in the line : if (pitchArray.count <= 60) {
[pitchArray addObject:[NSString stringWithFormat:#"%.2gº", motion.attitude.pitch * kRadToDeg]];
Here is the full code:
#import "ViewController.h"
#import <CoreMotion/CoreMotion.h>
#define kRadToDeg 57.2957795
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UILabel *pitchLabel;
#property (nonatomic, strong) CMMotionManager *motionManager;
#end
#implementation ViewController
- (CMMotionManager *)motionManager
{
if (!_motionManager) {
_motionManager = [CMMotionManager new];
[_motionManager setDeviceMotionUpdateInterval:1/60];
}
return _motionManager;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMDeviceMotion *motion, NSError *error) {
self.pitchLabel.text = [NSString stringWithFormat:#"%.2gº", motion.attitude.pitch * kRadToDeg];
NSMutableArray *pitchArray = [NSMutableArray array];
pitchArray = [[NSMutableArray alloc] initWithCapacity:60];
if (pitchArray.count <= 60) {
[pitchArray addObject:[NSString stringWithFormat:#"%.2gº", motion.attitude.pitch * kRadToDeg]];
}
else {
[pitchArray removeObjectAtIndex:0];
}
NSNumber *maxPitch = [pitchArray valueForKeyPath:#"#max.intValue"];
NSLog(#"%#",pitchArray);
NSLog(#"Max Pitch Value = %d",[maxPitch intValue]);
}];
}
#end
You keep allocating a new array every time you get a new pitch value. So you shall define the pitch array as a property and allocate it before your motion update handler. Your code would be:
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UILabel *pitchLabel;
#property (nonatomic, strong) CMMotionManager *motionManager;
#property (nonatomic, strong) NSMutableArray *pitchArray;
#end
#implementation ViewController
- (CMMotionManager *)motionManager
{
if (!_motionManager) {
_motionManager = [CMMotionManager new];
[_motionManager setDeviceMotionUpdateInterval:1/60];
}
return _motionManager;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.pitchArray = [[NSMutableArray alloc] initWithCapacity:60];
[self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMDeviceMotion *motion, NSError *error) {
self.pitchLabel.text = [NSString stringWithFormat:#"%.2gº", motion.attitude.pitch * kRadToDeg];
if (self.pitchArray.count <= 60) {
[self.pitchArray addObject:[NSString stringWithFormat:#"%.2gº", motion.attitude.pitch * kRadToDeg]];
}
else {
[self.pitchArray removeObjectAtIndex:0];
}
NSNumber *maxPitch = [self.pitchArray valueForKeyPath:#"#max.intValue"];
NSLog(#"%#",self.pitchArray);
NSLog(#"Max Pitch Value = %d",[maxPitch intValue]);
}];
}
Ah, simple error. It wasn't looping so I changed the if/else statement to while. The code works now and outputs the 60 item array and max value.