Summary:
I subclassed
GMSSyncTileLayer
and overwrote
tileForX:y:zoom:
but its only called once no matter how much I pan.
why?
DETAIL
We have implemented our own TileServer which is behind a secure webservice so we need to pass a login token to get the tiles.
The call is asynchronous POST which is quiet common for a webservice call.
Because I had to pass login token in the NSURLSession header I couldnt just pass GET urls to
GMSTileURLConstructor urls = http://<tileserver>/gettile?x=3&y=4&zoom=5
So I subclassed GMSSyncTileLayer
#interface SNSyncTileLayer : GMSSyncTileLayer
overwrote
- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {
When tileForX:y:Zoom: is called the first time I call a webservice to get the tile UIImage.
The UIImage returns on a delegate and is stored in a NSDictionary with key in format TILE_x_y_Zoom.
The call to the WS is asynch so - (UIImage *)tileForX:y:Zoom: always returns nil for that tile the first time its called.
What i've noticed is that tileForX:y:Zoom: is never called again no matter how much I pan back and forth.
For instance at the current zoom I pan across europe.
I see tileForX:y:Zoom: being called once and ws calls being mad and images being stored in my dictionary.
But if i keep panning at the same zoom I come back to Europe and tileForX:y:Zoom: is not called again.
Fix one - clear cache when new tile downloaded
I tried creating a delegate on SNSyncTileLayer and everytime a new tile downloaded it called:
[self.snSyncTileLayer clearTileCache];
But this wipes out ALL tiles and reloads them so as you pan you get a terrible flashing.
My only idea next is to measure how much the map has panned and if its more than half a width or height then to call clearTileCache.
SO the big question why isnt tileForX:y:Zoom: called everytime?
My overridden class
//
// SNSyncTileLayer.m
//
#import "SNSyncTileLayer.h"
#import "SNAppDelegate.h"
#import "GoogleTileRequest.h"
#import "SNGoogleTileRequest.h"
#import "GoogleTileImageResult.h"
#interface SNSyncTileLayer()<SNSeaNetWebServicesManagerDelegate>{
BOOL _debugOn;
}
#property (nonatomic, retain) NSMutableDictionary * tileImageCacheDict;
#end
#implementation SNSyncTileLayer
- (instancetype)init
{
self = [super init];
if (self) {
_tileImageCacheDict = [NSMutableDictionary dictionary];
_debugOn = TRUE;
}
return self;
}
- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom {
if(_debugOn)ImportantLog(#"tileForX:(%lu) y:(%lu) zoom:(%lu)", (unsigned long)x,(unsigned long)y,(unsigned long)zoom);
UIImage *tileImage_ = nil;
//tileImage_ = [UIImage imageNamed:#"EmptyTile1.png"];
NSString * keyForTile_ = [NSString stringWithFormat:#"TILE_%lu_%lu_%lu", (unsigned long)x,(unsigned long)y,(unsigned long)zoom];
id dictObj_ = [self.tileImageCacheDict objectForKey:keyForTile_];
if (dictObj_) {
if([dictObj_ isMemberOfClass:[NSNull class]])
{
if(_debugOn)DebugLog(#"tile has been called before but image not downloaded yet:[%#]",keyForTile_);
}
else if([dictObj_ isMemberOfClass:[UIImage class]])
{
if(_debugOn)DebugLog(#"cached image found in dict_ return it:[%#]",keyForTile_);
tileImage_ = (UIImage *)dictObj_;
}
else{
ErrorLog(#"ITEM IN self.tileImageCacheDict not NSNull or UIImage:[%#]", dictObj_);
}
}else{
if(_debugOn)ImportantLog(#"tileForX: CACHED IMAGE NOT FOUND: DOWNLOAD IT[%#]",keyForTile_);
//-----------------------------------------------------------------------------------
//add in temp object - tyring to check if tileForX:Y:Zoom is called more than once
[self.tileImageCacheDict setObject:[NSNull null] forKey:keyForTile_];
//-----------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------
SNAppDelegate *appDelegate = (SNAppDelegate *)[[UIApplication sharedApplication] delegate];
GoogleTileRequest * googleTileRequest_ = [[GoogleTileRequest alloc]init];
googleTileRequest_.X = [NSNumber numberWithInteger:x];
googleTileRequest_.Y = [NSNumber numberWithInteger:y];
googleTileRequest_.Zoom = [NSNumber numberWithInteger:zoom];
#pragma mark TODO - NOW - thur11dec - load from settings
googleTileRequest_.MapType = #"Dark";
//for general errors
appDelegate.snSeaNetWebServicesManager.delegate = self;
//Request should know what class to return too
googleTileRequest_.delegateForRequest = self;
[appDelegate.snSeaNetWebServicesManager ITileController_GoogleTile:googleTileRequest_];
//-----------------------------------------------------------------------------------
return kGMSTileLayerNoTile;
//-----------------------------------------------------------------------------------
}
return tileImage_;
}
#pragma mark -
#pragma mark SNSeaNetWebServicesManagerDelegate
#pragma mark -
-(void) snSeaNetWebServicesManager:(SNSeaNetWebServicesManager *)SNSeaNetWebServicesManager
wsReponseReceivedForRequest:(SNWebServiceRequest *)snWebServiceRequest_
error:(NSError *)error
{
#pragma mark TODO - NOW - thur11dec2014
if(error){
ErrorLog(#"error:%#",error);
}else{
if(snWebServiceRequest_){
if([snWebServiceRequest_ isMemberOfClass:[SNGoogleTileRequest class]])
{
//Result is JSONModel ivar in Request
if(snWebServiceRequest_.resultObject){
GoogleTileImageResult * googleTileImageResult_= (GoogleTileImageResult *)snWebServiceRequest_.resultObject;
UIImage * responseImage_ = googleTileImageResult_.responseImage;
if(responseImage_){
//-----------------------------------------------------------------------------------
//build the key from the parameters
if(snWebServiceRequest_.bodyJsonModel){
NSDictionary *paramsDict = [snWebServiceRequest_.bodyJsonModel toDictionary];
if(paramsDict){
NSString *keyX_ = [paramsDict objectForKey:#"X"];
NSString *keyY_ = [paramsDict objectForKey:#"Y"];
NSString *keyZoom_ = [paramsDict objectForKey:#"Zoom"];
if(keyX_){
if(keyY_){
if(keyZoom_){
NSString * keyForTile_ = [NSString stringWithFormat:#"TILE_%#_%#_%#", keyX_,keyY_,keyZoom_];
if(_debugOn)ImportantLog(#"TILE DOWNLOADED ADD TO CACHE[%#]",keyForTile_);
[self.tileImageCacheDict setObject:responseImage_ forKey:keyForTile_];
//if(_debugOn)DebugLog(#"[[self.tileImageCacheDict allKeys]count]:%lu", (unsigned long)[[self.tileImageCacheDict allKeys]count]);
//-----------------------------------------------------------------------------------
//I ADDED THIS SO delegate could clearTileCache but causes flashing as ALL tiles get reloaded visible ones and ones downloaded but not on map
if(self.delegate){
if([self.delegate respondsToSelector:#selector(snSyncTileLayer:tileDownloadedForX:Y:Zoom:)]){
[self.delegate snSyncTileLayer:self
tileDownloadedForX:keyX_
Y:keyY_
Zoom:keyZoom_
];
}else {
ErrorLog(#"<%# %#:(%d)> %s delegate[%#] doesnt implement snSyncTileLayer:tileDownloadedForX:Y:Zoom:", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__ ,self.delegate);
}
}else{
ErrorLog(#"<%# %#:(%d)> %s self.delegate is nil", self, [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__);
}
//-----------------------------------------------------------------------------------
}else{
ErrorLog(#"keyZoom_ is nil");
}
}else{
ErrorLog(#"keyY_ is nil");
}
}else{
ErrorLog(#"keyX_ is nil");
}
}else{
ErrorLog(#"paramsDict is nil");
}
}else{
ErrorLog(#"self.downloadingTasksDictionary is nil");
}
//-----------------------------------------------------------------------------------
}else{
ErrorLog(#"responseImage_ is nil");
}
}else{
ErrorLog(#"snWebServiceRequest_.resultJsonModel is nil");
}
}
else {
ErrorLog(#"UNHANDLED snWebServiceRequest_:%#", snWebServiceRequest_.class);
}
}else{
ErrorLog(#"snWebServiceRequest_ is nil");
}
}
}
#end
I haven't done this so I'm not sure, but my guess from reading the documentation is that you should be subclassing GMSTileLayer instead of GMSSyncTileLayer.
GMSSyncTileLayer is designed for cases where you are able to synchronously (ie immediately) return the tile for that location. By returning kGMSTileLayerNoTile, you are specifically indicating that 'there is no tile here', and so it never calls your class again for that location, as you've already responded that there is no tile there. (BTW, your description says you are returning nil, which indicates a transient error, but your code is actually returning kGMSTileLayerNoTile).
The GMSTileLayer class is designed for the asynchronous approach that you're using. If you subclass GMSTileLayer, your requestTileForX:y:zoom:receiver: method should start the background process to fetch the tile. When the tile request succeeds, then it is passed off to the GMSTileReceiver that was provided in that method (you should keep a copy of that receiver along with your request).
Related
When sharing an image from, eg Photos, I want my iOS app extension to appear in the list of apps available (next to Mail, Messages...) in order to receive the shared picture. Reading many sites reveals I have to subclass UIActivity and add it to an App Extension, what I did.
However I don't understand how I should instantiate my subclass, and it never gets called (No NSLog is displayed, item not in the sharing list).
How should I "instruct" iOS to use this subclass ?
myActivity.m:
#import "myActivity.h"
#implementation myActivity
- (NSString *)activityType {
// a unique identifier
return #"com.myapp.uniqueIdentifier";
}
- (NSString *)activityTitle {
// a title shown in the sharing menu
return #"Custom Activity";
}
- (UIImage *)activityImage {
// an image to go with our option
return [UIImage imageNamed:#"MyImage"];
}
+ (UIActivityCategory)activityCategory {
// which row our activity is shown in
// top row is sharing, bottom row is action
NSLog(#"UIActivityCategoryShare");
return UIActivityCategoryShare;
}
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems {
// return YES for anything that our activity can deal with
for (id item in activityItems) {
// we can deal with strings and images
if ([item isKindOfClass:[UIImage class]]) {
return YES;
}
}
// for everything else, return NO
return NO;
}
- (void)prepareWithActivityItems:(NSArray *)activityItems {
// anything we need to prepare, now's the chance
// custom UI, long running calculations, etc
// also: grab a reference to the objects our user wants to share/action
self.activityItems = activityItems;
}
- (UIViewController *)activityViewController {
// return a custom UI if we need it,
// or the standard activity view controller if we don't
return nil;
}
- (void)performActivity {
// the main thing our activity does
// act upon each item here
for (id item in self.activityItems) {
NSLog(#"YEY - someone wants to use our activity!");
NSLog(#"They used this object: %#", [item description]);
}
// notify iOS that we're done here
// return YES if we were successful, or NO if we were not
[self activityDidFinish:YES];
}
#end
And myActivity.h:
#import <UIKit/UIKit.h>
#interface myActivity : UIActivity
#property (nonatomic, strong) NSArray *activityItems;
#end
I need to add Speech Recognition to an App for a Personal Project.
I need the iOS built-in speech recognition framework because it is fast, accurate and it can also recognise your contact names and other information about yourself.
So far, I think I have found the framework which contains the headers for the speech recognition on iOS 8: the SAObjects.framework
I got the headers of Github and added them successfully in my Xcode Project.
The headers I have tried so far are these:
<SAObjects/SASRecognition.h>
<SAObjects/SASStartSpeechDictation.h>
<SAObjects/SASSpeechRecognized.h>
However, I am not sure how to work with them. For instance, these are two possible methods that can fire a Speech Recognition:
SASStartSpeechDictation *object1 = [SASStartSpeechDictation startSpeechDictation];
SASSpeechRecognized *object2 = [SASSpeechRecognized speechRecognized];
When I debug it though, I cannot find any string in any of these objects. So, obviously something is wrong. Maybe I need to set a notification observer?
Another Solution could be to start a Dictation (through the Keyboard)
to a hidden text field (without the keyboard showing).
Like the Activator action for Jailbroken devices, if you are familiar with it.
But I haven't found any methods that can start the Keyboard dictation, or the Activator action source code to find it out.
Maybe someone has experimented with these things and can give me some help?
Please tell me if you need more information about this question :)
Thanks a lot!
So, I managed to find an answer myself. I luckily found a Github repo with some helpful code: https://github.com/erica/useful-things
The code I found is under the appstore unsafe pack/DictationHelper directory. This code helps to use the UIDictationController and start and stop the Dictation, and get the text value. Of course, without any Text Fields...
Important: In order for this to work, you need to have the headers of the UIKit framework, link the framework to the Target and import them in the Project!
However, I modified the code a bit, because the sample code is only available to speak for a specific duration. I needed to stop speaking by pressing a button.
This is the modified code, for anyone who might be interested in the future:
DicationHelper.h:
/*
Erica Sadun, http://ericasadun.com
NOT APP STORE SAFE BUT HANDY
Siri-ready devices only. Will not work in simulator.
Example:
[SpeechHelper speakModalString:#"Please say something"];
[[DictationHelper sharedInstance] dictateWithDuration:5.0f completion:^(NSString *dictationString) {
if (dictationString)
NSLog(#"You said:'%#'", dictationString);
else
NSLog(#"No response");}];
//-> OR: (My modification)
[SpeechHelper speakModalString:#"Please say something"];
[[DictationHelper sharedInstance] startDictation:0 completion:^(NSString *dictationString) {
if (dictationString)
NSLog(#"You said:'%#'", dictationString);
else
NSLog(#"No response");}];
// Then you need to call this to stop the Dictation: [[DictationHelper sharedInstance] stopDictation]
*/
#import <UIKit/UIKit.h>
//#import <Foundation/Foundation.h>
extern NSString *const DictationStringResults;
typedef void (^DictationBlock)(NSString *dictationString);
#interface DictationHelper : NSObject
+ (instancetype) sharedInstance;
- (void) dictateWithDuration: (CGFloat) duration;
- (void) dictateWithDuration: (CGFloat) duration completion:(DictationBlock) completionBlock;
-(void) startDictation:(CGFloat) whatever completion:(DictationBlock) completionBlock;
-(void) stopDictationWithFallback;
#property (nonatomic, readonly) BOOL inUse;
#end
DictationHelper.m
/*
Erica Sadun, http://ericasadun.com
NOT APP STORE SAFE BUT HANDY
Siri-ready devices only. Will not work in simulator.
*/
#import "DictationHelper.h"
#define MAKELIVE(_CLASSNAME_) Class _CLASSNAME_ = NSClassFromString((NSString *)CFSTR(#_CLASSNAME_));
NSString *const DictationStringResults = #"Dictation String Results";
static DictationHelper *sharedInstance = nil;
#class UIDictationController;
#interface UIDictationController
+ (UIDictationController *) sharedInstance;
- (void) startDictation;
- (void) stopDictation;
- (void) preheatIfNecessary;
#end;
#interface DictationHelper () <UITextFieldDelegate>
#end
#implementation DictationHelper
{
UITextField *secretTextField;
id dictationController;
DictationBlock completion;
BOOL handled;
}
- (void) preheat
{
if (!secretTextField)
{
secretTextField = [[UITextField alloc] initWithFrame:CGRectZero];
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
[window addSubview:secretTextField];
secretTextField.inputView = [[UIView alloc] init];
secretTextField.delegate = self;
}
if (!dictationController)
{
MAKELIVE(UIDictationController);
dictationController = [UIDictationController sharedInstance];
[dictationController preheatIfNecessary];
}
}
+ (instancetype) sharedInstance
{
if (!sharedInstance)
{
sharedInstance = [[self alloc] init];
[sharedInstance preheat];
}
return sharedInstance;
}
- (BOOL) textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSString *tftext = textField.text;
tftext = [tftext stringByReplacingCharactersInRange:range withString:string];
[[NSNotificationCenter defaultCenter] postNotificationName:DictationStringResults object:tftext];
if (completion) completion(tftext);
// Treat this dictation as handled
handled = YES;
_inUse = NO;
completion = nil;
// Resign first responder
[textField resignFirstResponder];
return YES;
}
- (void) fallback
{
// 1. Test completion
if (!completion) return;
// 2. Check for handled
if (handled)
{
_inUse = NO;
handled = NO;
return;
}
// 3. Assume the dictation didn't work
completion(nil);
// 4. Reset everything
handled = NO;
_inUse = NO;
completion = nil;
// 5. Resign first responder
[secretTextField resignFirstResponder];
}
-(void) startDictation:(CGFloat) whatever completion:(DictationBlock) completionBlock{
if (completionBlock) completion = completionBlock;
if (_inUse)
{
NSLog(#"Error: Dictation Helper already in use");
return;
}
_inUse = YES;
handled = NO;
secretTextField.text = #"";
[secretTextField becomeFirstResponder];
[[UIDevice currentDevice] playInputClick];
[dictationController startDictation];
}
- (void) dictateWithDuration: (CGFloat) numberOfSeconds
{
if (_inUse)
{
NSLog(#"Error: Dictation Helper already in use");
return;
}
_inUse = YES;
handled = NO;
secretTextField.text = #"";
[secretTextField becomeFirstResponder];
[[UIDevice currentDevice] playInputClick];
[dictationController startDictation];
[self performSelector:#selector(stopDictation) withObject:nil afterDelay:numberOfSeconds];
[self performSelector:#selector(fallback) withObject:nil afterDelay:numberOfSeconds + 1.0f];
}
- (void) dictateWithDuration: (CGFloat) duration completion:(DictationBlock) completionBlock
{
if (completionBlock) completion = completionBlock;
[self dictateWithDuration:duration];
}
- (void) stopDictation
{
[dictationController stopDictation];
}
- (void) stopDictationWithFallback
{
[self performSelector:#selector(stopDictation) withObject:nil afterDelay:0.0];
[self performSelector:#selector(fallback) withObject:nil afterDelay:1.0f];
}
#end
#undef MAKELIVE
I am trying to let my iOS app pull from a text file on a server and display it in a Text view. This works fine if I just do this by setting the url in the viewDidLoad method. But if I do it the way I need to, a button click calls a method that sets the url based on which button is clicked and then populates the Text view also while moving to the view controller, then the Text view does not receive any text.
I am not really sure if this has anything to do with it but could it possibly be because I am using one button that has both a triggered segue to move to the next view controller and then a sent event in order to pull the information? Could it be doing it out of order or something?
Here is my code:
#import "ViewController.h"
#import "STTwitter.h"
#interface ViewController ()
{
STTwitterAPI *twitter;
}
#property (weak, nonatomic) IBOutlet UITextView *scheduleText;
#property (weak, nonatomic) IBOutlet UITextView *tweetText;
#property (weak, nonatomic) IBOutlet UIScrollView *mScrollView;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//pull from server to populate schedule
//[self pullInfo:(1)];
//load tweets
//[self callTwitter];
//set scroll view size
_mScrollView.contentSize = CGSizeMake(320, 300);
}//end viewDidLoad
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}//end didReceiveMemoryWarning
- (void) pullInfo: (int) page
{//pull information from server
NSError* err;
NSURL* url = nil;
if (page == 1)
url = [NSURL URLWithString:#"http://www.mckendree.edu /aecschedule1.txt"];
else if (page == 2)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule2.txt"];
else if (page == 3)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule3.txt"];
else if (page == 4)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule4.txt"];
else if (page == 5)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule5.txt"];
else if (page == 6)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule6.txt"];
else if (page == 7)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule7.txt"];
else if (page == 8)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule8.txt"];
else if (page == 9)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule9.txt"];
else if (page == 10)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule10.txt"];
else if (page == 11)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule11.txt"];
else if (page == 12)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule12.txt"];
else if (page == 13)
url = [NSURL URLWithString:#"http://www.mckendree.edu/aecschedule13.txt"];
NSData *htmlData = [NSData dataWithContentsOfURL:url];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithData:htmlData options:#{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: #(NSUTF8StringEncoding)} documentAttributes:nil error:nil];
//retrieve the text if there was no reading error
if (err != nil)
printf("Error retrieving text");
else
{
[_scheduleText setAttributedText:attrString];
[_scheduleText sizeToFit];
[_scheduleText setTextColor:[UIColor whiteColor]];
}//end else
}//end pullInfo
- (IBAction)settingsClicked:(id)sender
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:#"McK AEC" message:#"Developed by: Sean Boehnke" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {}];
[alert addAction:defaultAction];
[self presentViewController:alert animated:YES completion:nil];
}//end settingsClicked
- (IBAction)time1Clicked:(id)sender
{
[self pullInfo:(1)];
}//end time1Clicked
- (IBAction)time2Clicked:(id)sender
{
[self pullInfo:(2)];
}//end time2Clicked
- (IBAction)time3Clicked:(id)sender
{
[self pullInfo:(3)];
}//end time3Clicked
- (IBAction)time4Clicked:(id)sender
{
[self pullInfo:(4)];
}//end time4Clicked
- (IBAction)time5Clicked:(id)sender
{
[self pullInfo:(5)];
}//end time5Clicked
- (IBAction)time6Clicked:(id)sender
{
[self pullInfo:(6)];
}//end time6Clicked
- (IBAction)time7Clicked:(id)sender
{
[self pullInfo:(7)];
}//end time7Clicked
- (IBAction)time8Clicked:(id)sender
{
[self pullInfo:(8)];
}//end time8Clicked
- (IBAction)time9Clicked:(id)sender
{
[self pullInfo:(9)];
}//end time9Clicked
- (IBAction)time10Clicked:(id)sender
{
[self pullInfo:(10)];
}//end time10Clicked
- (IBAction)time11Clicked:(id)sender
{
[self pullInfo:(11)];
}//end time11Clicked
- (IBAction)time12Clicked:(id)sender
{
[self pullInfo:(12)];
}//end time12Clicked
- (IBAction)time13Clicked:(id)sender
{
[self pullInfo:(13)];
}//end time13Clicked
#end
So based on your description of the issue and your comments there're a few fundamental things that are wrong here. First and foremost, the reason why this will not populate data on your pushed viewController is because you are setting the attributed text on scheduleText which is an instance variable on ViewController. If you are pushing another viewController after those buttons are pressed, then it makes perfect sense that the controller you're pushing would not have this data set because you didn't set it there, you set it on the previous controller. This data has no reason to assume you actually meant to pass it forward. That's just simply not what you asked it to do. If your intent is to pass the data forward, and you have your heart set on storyboard segues you will need to implement prepareForSegue and expose a property you can use to configure the new viewController as it's being prepared for presentation.
Sorry for the wall of text, but this is my recommendation to get through this.
First step, on your storyboard, give all of your buttons hooked up to segues button tags in the preferences tab of the right pane of interface builder. You should tag them 1 through 13 (tradition says start at 0 but this will make the next step simpler for our purposes.)
Next, get rid of all those IBActions for the buttons. We aren't going to need them anymore and they're cluttering your class. You MUST also unhook them in your storyboard after you delete the methods or your app will crash with an error that says "this class is not key value coding-compliant"
Next you need to implement the method -prepareForSegue on your viewController to do the rest of your heavy lifting
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSAttributedString *attrString;
if ([sender isKindOfClass:[UIButton class]]) {
NSUInteger seguePageNumber = ((UIButton *)sender).tag;
attrString = [self pullInfo:seguePageNumber];
}
YourViewControllerClass *vc = (YourViewControllerClass*)[segue destinationViewController];
vc.attributedTextPropertyIExposed = attrString;
//This property needs to be exposed in the new VC's header file. Once the new controller presents you can then use this property to set the string the way you want.
}
As a bonus, here's a pull info method that I referenced above that I find much easier to read.
- (NSAttributedString *)pullInfo:(NSUInteger)pageNumber
{
NSString* urlString = [NSString stringWithFormat:#"http://www.mckendree.edu/aecschedule%lu.txt", (long)pageNumber];
NSURL *url = [NSURL URLWithString:urlString];
NSData *htmlData = [NSData dataWithContentsOfURL:url];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithData:htmlData options:#{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: #(NSUTF8StringEncoding)} documentAttributes:nil error:nil];
return attrString;
}
Given the following code example (iOS 7, Xcode 5):
/**
* SampleProvider Class
*/
typedef void(^RequestCallback)(UIViewController *result);
static NSString * const cControllerRequestNotification = #"controllerRequestNotification";
static NSString * const cRequestClassNameKey = #"className";
static NSString * const cRequestCallbackKey = #"callback";
#interface SampleProvider : NSObject
+ (void)requestControllerForClassName:(NSString *)className completion:(RequestCallback)callback;
#end
#interface SampleProvider ()
- (UIViewController *)controllerForClassName:(NSString *)className;
- (void)didReceiveControllerRequest:(NSNotification *)n;
#end
#implementation SampleProvider
#pragma mark - Overrides
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (id)init {
self = [super init];
if( self ) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didReceiveControllerRequest:) name:cControllerRequestNotification object:nil];
}
return self;
}
#pragma mark - Public API
+ (void)requestControllerForClassName:(NSString *)className completion:(RequestCallback)callback{
NSDictionary *requestInfo = #{ cRequestClassNameKey : className, cRequestCallbackKey : [callback copy] };
[[NSNotificationCenter defaultCenter] postNotificationName:cControllerRequestNotification object:requestInfo];
}
#pragma mark - Private API
- (UIViewController *)controllerForClassName:(NSString *)className {
UIViewController *result = nil;
Class controllerClass = NSClassFromString(className);
if( (nil != controllerClass) && ([controllerClass isSubclassOfClass:[UIViewController class]]) ) {
result = [[controllerClass alloc] init];
}
return result;
}
- (void)didReceiveControllerRequest:(NSNotification *)n {
NSDictionary *requestInfo = [n object];
NSString *className = requestInfo[cRequestClassNameKey];
RequestCallback callback = requestInfo[cRequestCallbackKey];
UIViewController *result = [self controllerForClassName:className];
if( nil != callback ) {
callback(result);
}
}
#end
/**
* SampleViewController Class
*/
#interface SampleViewController : UIViewController
#end
#implementation SampleViewController
#pragma mark - Overrides
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
NSString *className = #"ClassName";
[SampleProvider requestControllerForClassName:className completion:^(UIViewController *result) {
if( nil != result ) {
// Result is valid pointer, not a zombie.
[self.navigationController pushViewController:result animated:YES];
// Result is released, not nil.
} else {
NSLog(#"Unable to load controller with class name: %#", className);
}
}];
}
#end
Why would my UINavigationController fail to take ownership of the callback controller, received by SampleProvider's public class method, even after showing the view?
I'm seeing the following behavior:
The new controller class is properly allocated and returned via the callback method. Upon entering the callback the result parameter is pointing to valid memory.
The new controller is pushed to my UINavigationController's navigation stack.
The newly pushed controller's "viewDidLoad" method is called.
When inspecting the UINavigationController's "viewControllers" property, the newly pushed controller is referenced in the array.
The newly push controller is is deallocated while UINavigationController pushViewController:animated: is still executing.
The new controller is now a zombie.
Thank you for any assistance.
I don't have a clearcut answer because the answer may be in code you haven't posted -- the code you have posted looks valid apart from two observations (which could lead you to an answer):
Should that isKindOfClass be isSubclassOfClass? -isKindOfClass: is an
instance method on NSObject, not a class method.
Calling pushViewController: synchronously during viewDidLoad seems
dangerous. It's quite possible that the state of the view hierarchy
is not stable at that time. That push should happen in response to
some other discrete event, I'd think. Try making that push (or the
entire requestControllerForClassName:) asynchronous via
dispatch_async, as a test, and see if that solves your problem.
I am trying to make a calculator app, but when I press enter nothing is pushed into the array. I have a class called CaculatorBrain where the pushElement method is defined, however (for now) I defined and implemented pushElement method in the view controller.
When I log the operand object as it is typed in the console when enter is pressed the contents of array is nil! Why is that?
#import "CalculatorViewController.h"
#import "CalculatorBrain.h"
#interface CalculatorViewController ()
#property (nonatomic)BOOL userIntheMiddleOfEnteringText;
#property(nonatomic,copy) NSMutableArray* operandStack;
#end
#implementation CalculatorViewController
BOOL userIntheMiddleOfEnteringText;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
-(NSMutableArray*) operandStack {
if (_operandStack==nil) {
_operandStack=[[NSMutableArray alloc]init];
}
return _operandStack;
}
-(CalculatorBrain*)Brain
{
if (!_Brain) _Brain= [[CalculatorBrain alloc]init];
return _Brain;
}
- (IBAction)digitPressed:(UIButton*)sender {
if (self.userIntheMiddleOfEnteringText) {
NSString *digit= [sender currentTitle];
NSString *currentDisplayText=self.display.text;
NSString *newDisplayText= [currentDisplayText stringByAppendingString:digit];
self.display.text=newDisplayText;
NSLog(#"IAm in digitPressed method");
}
else
{
NSString *digit=[sender currentTitle];
self.display.text = digit;
self. userIntheMiddleOfEnteringText=YES;
}
}
-(void)pushElement:(double)operand {
NSNumber *operandObject=[NSNumber numberWithDouble:operand];
[_operandStack addObject:operandObject];
NSLog(#"operandObject is %#",operandObject);
NSLog(#"array contents is %#",_operandStack);
}
- (IBAction)enterPressed {
[self pushElement: [self.display.text doubleValue] ];
NSLog(#"the contents of array is %#",_operandStack);
userIntheMiddleOfEnteringText= NO;
}
It looks like the operand stack is never initialized.
When you directly access _operandStack you don't go through -(NSMutableArray*) operandStack, which is the only place where the operand stack is allocated and initialized. If the array isn't allocated you can't put anything in it, which is why it logs the contents as nil.
I'd recommend using either self.operandStack (which uses the method that checks if _operandStack is nil) everywhere except inside the -(NSMutableArray*) operandStack method, or allocating the operand stack in your viewDidLoad.