Xcode: How would I do a one-off UIAlertView? - iphone

I have an app that I want to bring up an alert view to ask them if they can review the app?
Any help appreciated!

Don't. That is bad UX. Instead, try the following:
have an "About" section in your app containing your brand, and a link to the review page
have a "Review This App" button in a non-distracting location at a corner/side of the screen where it doesn't break the user experience

Something like this would do it:
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"UIAlertView" message:#"<Alert message>" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"OK", nil];
[alert show];
[alert release];

* Important Update
Apple has added an extra rule for apps being submitted to the app store.
We can no longer store arbitrary data files in the Documents folder. Only content generated by the user like a text file they manually typed and saved or a photo they took with the camera can be saved in the Documents folder.
We are now expected to store our app generated files in the Library/Cache folder instead of the Documents folder. In addition, we should mark the files we don't want to be sync to iCloud with the skip backup attribute.
Failure to comply with this will result in an app being rejected by Apple.
Reason: The Documents folder is now used for syncing to iCloud. iCloud does a sync every few minutes and if we were to have megabytes of our app generated files stored in the Documents folder, it would get mixed up with the user's own iCloud synced files.
Update the MediaDirectory.m files with these new code to be safe:
+ (NSString *) mediaPathForFileName:(NSString *) fileName
{
NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDirectory = [directoryPaths objectAtIndex:0];
NSString *filePath = [NSString stringWithFormat:#"%#/%#", cachesDirectory, fileName];
return filePath;
}
+ (BOOL)addSkipBackupAttributeToFile:(NSString *) fileName
{
NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cachesDirectory = [directoryPaths objectAtIndex:0];
NSString *filePathStr = [NSString stringWithFormat:#"%#/%#", cachesDirectory, fileName];
const char* filePath = [filePathStr fileSystemRepresentation];
const char* attrName = "com.apple.MobileBackup";
u_int8_t attrValue = 1;
int result = setxattr(filePath, attrName, &attrValue, sizeof(attrValue), 0, 0);
return result == 0;
}
Remember to mark that text file we generated with the skip backup attribute like so after generating the file:
[MediaDirectory addSkipBackupAttributeToFile:#"fileStatus.txt"];
I also had a typo earlier, I said to write to file "firstLaunch.txt" when it should have been "fileStatus.txt" (bad copy and paste skills :P)
Original Text
You could try writing a value (such as "no") to a text file and storing in the application's sandbox Document folder when the user taps on "Don't ask again" button.
Next time the app loads, your view controller would read the value from this text file and if it's "no", then don't display the alert view.
Hope that helps.
Writing a one off UI alert view
OK, let me first explain the process we're going to go through:
1) Your application delegate class didFinishLaunchingWithOption is the method that gets called when an app is launched.
2) This method only gets called once when the app launches so this is the spot where we create a text file and we write our status of the alert view whether to "show again" or "don't show again" for the UIAlertView
3) We're going to bring up the alert view in your UIViewController's viewDidLoad method. However, we only display this UIAlertView IF the value in our text file IS NOT "don't show again". If the value in the text file is "show again", we show the UIAlertView, if it's "don't show again", we don't show the UIAlertView, makes sense?
(These values are only string, you can set any value you want, I'm only using these random values to demonstrate).
Right, now we got the general process, let's implement it.
The code
In your AppDelegate.h header file:
#interface MyAppDelegate : NSObject
{
...
bool isFirstLaunch;
}
Now in your AppDelegate.m implementation file:
// -------------------------------------------------------------------------------------
// I wrote a helper class called "MediaDirectory" that returns me the path to a file
// in the Documents directory of the application sandbox.
// Each application has a copy of the Documents directory so you can safely
// assume this directory will always exist.
//
// This class has been very useful for me, hope you find it useful too.
//
// DOWNLOAD THE FILES AT:
//
// http://www.chewedon.com/classes/MediaDirectory.h
// http://www.chewedon.com/classes/MediaDirectory.m
// -------------------------------------------------------------------------------------
#import <Foundation/NSFileManager.h>
#import <Foundation/NSFileHandle.h>
#import "MediaDirectory.h"
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
NSString *fileStatus = nil;
// Here, the NSFileManager defaultManager fileExistsAtPath method needs a path to
// the file in the Document directory, to get this path, use the static method from
// my MediaDirectory class and pass it the name of the file.
// I called it "fileStatus.txt"
//
// We're checking if the file exist in the Documents directory or not.
// If it does not exist then we create the text file and pass it the
// value "show again", otherwise we do nothing.
// (We don't want to overwrite the value everytime the app starts)
if([[NSFileManager defaultManager] fileExistsAtPath:[MediaDirectory mediaPathForFileName:#"fileStatus.txt"]] == NO)
{
// prepare fileStatus.txt by setting "show again" into our string
// we will write this string into the text file later
fileStatus = [[NSString alloc] initWithString:#"show again"];
// now we write the value "show again" so that the UIAlertView will show
// when it checks for the value, until the user clicks on no and we set
// this value to "don't show again" later in another piece of code
[fileStatus writeToFile:[MediaDirectory mediaPathForFileName:#"fileStatus.txt"]
atomically:YES
encoding:NSUTF8StringEncoding
error:nil];
}
[fileStatus release];
...
// rest of your didFinishLaunchingWithOptions method
[window addSubview:[viewController view]];
[self.window makeKeyAndVisible];
[self initGlobals];
return YES;
}
Now in your UIViewController class, we need to make this class conform to the UIAlertView protocol. We are going to use one of the delegate method that tells us when a button on the UIAlertView is clicked.
#interface MyViewController : UIViewController <UIAlertViewDelegate>
{
...
}
In our implementation file (MyViewController.m file) we check the value stored in the text file before showing the UIAlertView.
#import "MediaDirectory.h"
#import <Foundation/NSFileManager.h>
#import <Foundation/NSFileHandle.h>
-(void)viewDidLoad
{
...
BOOL shouldShowAlert = false;
NSString *fileStatus = nil;
// check if the file exists in the Documents directory and read its value
// if it does. If the value read from file is "show again",
// we bring up a UIAlertView
if([[NSFileManager defaultManager] fileExistsAtPath:[MediaDirectory mediaPathForFileName:#"fileStatus.txt"]] == YES)
{
fileStatus = [[NSMutableString alloc] initWithContentsOfFile:[MediaDirectory mediaPathForFileName:#"fileStatus.txt"]
encoding:NSUTF8StringEncoding
error:nil];
if([fileStatus isEqualToString:#"show again"] == YES)
{
shouldShowAlert = true;
}
else if([fileStatus isEqualToString:#"don't show again"] == YES)
{
shouldShowAlert = false;
}
}
if(shouldShowAlert == true)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"My Alert View Title"
message:#"my message"
delegate:self
cancelButtonTitle:#"OK"
otherButtonTitles:#"Don't Show Again", nil];
// we set a tag for this UIAlertView to distinguish between this UIAlertView
// and other UIAlertView in case there are any.
// I use a value of 10, you can use any value but make sure it is unique
[alert setTag:10];
[alert show];
[alert release];
}
}
Now we come to the last part where we handle which button the user tapped on for the UIAlertView.
Somewhere in your MyViewController.m file, write this delegate method:
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
// making sure its the UIAlertView we want
if([alertView tag] == 10)
{
// By default, the "cancel" button has an index of 0
// and the next button would have a index of 1
// In our case, we set the first button is "OK"
// and "Don't Show Again" as second button
if(buttonIndex == 1)
{
NSString *fileStatus = [[NSString alloc] initWithString:#"don't show again"];
[fileStatus writeToFile:[MediaDirectory mediaPathForFileName:#"fileStatus.txt"]
atomically:YES
encoding:NSUTF8StringEncoding
error:nil];
[fileStatus release];
}
}
}
Hopefully I didn't miss anything, let me know if it works or not.

Related

Objective-C Tested app on iPhone, updated data, data doesnt change

I just built my first app and now I started testing it on my phone. It looks great when I first launch the app after building it, the launch images appears and then my json data is loaded via NSURL and displays properly on the app. But when I close down the app, update the data via php and mysql and re open it the launch image does not appear and my app is not updated. Is it possible to have the app launch like it did when I first launched it, always have launch image and also get the new data?
Here is my code if it helps.
- (void)viewDidLoad
{
[super viewDidLoad];
[self loadJSON];
}
- (void)loadJSON
{
Reachability *networkReachability = [Reachability reachabilityForInternetConnection];
NetworkStatus networkStatus = [networkReachability currentReachabilityStatus];
if (networkStatus == NotReachable) {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"Error" message: #"Connection Failed" delegate: self cancelButtonTitle:#"Refresh" otherButtonTitles:nil]; [alert show]; [alert release];
});
} else {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL *url = [NSURL URLWithString:#"http://url.com/GetData.php"];
NSData *data = [NSData dataWithContentsOfURL:url options:0 error:nil];
NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSArray *firstItemArray = array[0];
NSString *yesNoString = firstItemArray[0];
NSString *dateString = firstItemArray[1];
NSString *timeString = firstItemArray[2];
NSString *homeString = firstItemArray[3];
NSString *awayString = firstItemArray[4];
NSString *lastUpdatedString = firstItemArray[5];
dispatch_async(dispatch_get_main_queue(), ^{
self.YesOrNo.text = yesNoString;
self.date.text = [#"For " stringByAppendingString:dateString];
self.time.text = timeString;
self.home.text = homeString;
self.away.text = awayString;
self.lastUpdated.text = lastUpdatedString;
self.lastUpdatedText.text = #"Last Updated";
self.vs.text = #"vs";
});
});
}
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
[self loadJSON];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)dealloc {
[_YesOrNo release];
[_date release];
[_time release];
[_vs release];
[_home release];
[_away release];
[_lastUpdatedText release];
[_lastUpdated release];
[super dealloc];
}
#end
If someone can point me in the right direction, that would be great and appreciated, thanks.
When you hit the round home button when running an app, it just puts it into the background, where it continues to run in a type of catatonic state. When you tap its icon again, it just wakes up, and doesn't re-launch. If you'd like to have your app completely quit when the user hits the home button, use the info.plist option "UIApplicationExitsOnSuspend" also known as "Application does not run in background." Set this to YES in your info.plist, and you'll get a fresh start every time. You can access this by clicking on your project in Xcode in the Project Navigator mode, and select the "info" tab at the top middle.
As it is your first app, you should try reading about the various devices and os versions.
Read about various application states, also the methods that are present in the AppDelegateClass, that get called when the app enters into various states,try reading about them.
So what has happened in your case is the device that you are using is Multitasking one. So when you press the home button or the sleep button the game goes to background, and is not killed. So next time when you tap on the application icon on your device, it brings it back to the foreground and does not relaunch it hence your Viewdidload method won't be called and your changes won't get reflected.
So, now to terminate your app, you can go through this link
http://www.gottabemobile.com/2013/02/10/how-to-close-apps-on-iphone/
Hope this helps.

ObjectiveFlickr Photo Upload Error

I'm working on using the ObjectiveFlickr library to upload photos to Flickr from my iPhone app. I am able to authorize the app and perform general requests, but I am getting an error while trying to upload a photo. The photo is meant to be uploaded is an image captured using AVFoundation. Here is the relevant code:
UIImage *image = [[UIImage alloc] initWithData:imageData];
if ([[AppDelegate sharedDelegate].apiContext.OAuthToken length]) {
NSData *uploadData = UIImageJPEGRepresentation(image, 1);
if (!flickrRequest) {
flickrRequest = [[OFFlickrAPIRequest alloc] initWithAPIContext:[AppDelegate sharedDelegate].apiContext];
flickrRequest.delegate = self;
flickrRequest.requestTimeoutInterval = 60.0;
}
[flickrRequest uploadImageStream:[NSInputStream inputStreamWithData:uploadData] suggestedFilename:#"Test" MIMEType:#"image/jpeg" arguments:[NSDictionary dictionaryWithObjectsAndKeys:#"0", #"is_public", nil]];
[UIApplication sharedApplication].idleTimerDisabled = YES;
NSLog(#"Can upload photo");
The flickrRequest object is defined and #property'd in the .h file pertaining to the code above.
The OFFlickrRequestDelegate methods are as follows:
- (void)flickrAPIRequest:(OFFlickrAPIRequest *)inRequest imageUploadSentBytes:(NSUInteger)inSentBytes totalBytes:(NSUInteger)inTotalBytes {
NSLog(#"Success");
}
- (void)flickrAPIRequest:(OFFlickrAPIRequest *)inRequest didCompleteWithResponse:(NSDictionary *)inResponseDictionary {
NSLog(#"%s %# %#", __PRETTY_FUNCTION__, inRequest.sessionInfo, inResponseDictionary);
}
- (void)flickrAPIRequest:(OFFlickrAPIRequest *)inRequest didFailWithError:(NSError *)inError {
NSLog(#"%s %# %#", __PRETTY_FUNCTION__, inRequest.sessionInfo, inError);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Error" message:[inError description] delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alert show];
}
When I run the project on the device the console shows:
Can upload photo
Success
Success
Success
Success
flickrAPIRequest:didFailWithError:(null) Error Domain=org.lukhnos.ObjectiveFlickr Code=2147418115 "The operation couldn’t be completed. (org.lukhnos.ObjectiveFlickr error 2147418115.)"
Searching through the API's documentation, "error 2147418115" is defined as "OFFlickrAPIRequestFaultyXMLResponseError". I'm not really sure what this means though. Also strange--"Success" appears four times when I'm only attempting to upload one photo.
The sample app utilizing ObjectiveFlickr, "Snap and Run" uploads photos fine on the same device that I am testing on. Comparing the code between my app and Snap and Run, I don't see any major differences that could cause the photo not to upload.
Any help would be greatly appreciated.
That is strange. OFFlickrAPIRequestFaultyXMLResponseError means the returned data cannot be parsed as XML. If the sample app SnapAndRun runs correctly and yours don't, I suggest add one line in ObjectiveFlickr.m, right after the line NSString *stat = [rsp objectForKey:#"stat"];:
NSString *dataString = [[[NSString alloc] initWithData:[request receivedData] encoding:NSUTF8StringEncoding] autorelease];
NSLog(#"received response: %#", dataString);
This may help you find out what goes wrong (e.g. if it's a server-side issue, or some response format that the library missed).
When I got this error I used Charles to see what was going on. Flickr was sending back an error saying that the signature was invalid. Looking at my request, I found that I had reversed the order of the key and values (i.e. there was whitespace in one of my keys, which probably caused a signature error).

Programmatically tap on HTML href in order to update app

I'm planning updates for an enterprise app with ad-hoc distribution.
For updates, Apple recommends having the user visit an HTML page and tap on a link:
href="itms-services://?action=download-manifest&url=http://example.com/
manifest.plist"
See http://help.apple.com/iosdeployment-apps/#app43ad871e
I don't want to do this. I want the app to programmatically check for updates on launch and alert the user with a UIAlertView that an update is available.
Here's what I have so far in application didFinishLaunching. The complicated plist parsing comes from the structure of an example plist found here: http://help.apple.com/iosdeployment-apps/#app43ad78b3
NSLog(#"checking for update");
NSData *plistData = [NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://example.com/MyApp.plist"]];
if (plistData) {
NSLog(#"finished checking for update");
NSError *error;
NSPropertyListFormat format;
NSDictionary *plist = [NSPropertyListSerialization propertyListWithData:plistData options:NSPropertyListImmutable format:&format error:&error];
if (plist) {
NSArray *items = [plist valueForKey:#"items"];
NSDictionary *dictionary;
if ([items count] > 0) {
dictionary = [items objectAtIndex:0];
}
NSDictionary *metaData = [dictionary objectForKey:#"metadata"];
float currentVersion = [[[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"] floatValue];
float newVersion = [[metaData objectForKey:#"bundle-version"] floatValue];
NSLog(#"newVersion: %f, currentVersion: %f", newVersion, currentVersion);
if (newVersion > currentVersion) {
NSLog(#"A new update is available");
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Update available" message:#"A new update is available." delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"UPDATE", nil];
[alert show];
}
}
}
Then I have my UIAlertView delegate method:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1) {
NSLog(#"downloading full update");
UIWebView *webView = [[UIWebView alloc] init];
[webView loadRequest:[[NSURLRequest alloc] initWithURL:[NSURL URLWithString:#"itms-services://?action=download-manifest&url=http://example.com/MyApp.plist"] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0]];
}
}
A few things:
I know [alert show] shouldn't be called in application didFinish, but I'll change it later.
I don't know how quickly plistData will be downloaded and how this download affects the app.
More importantly, my alert view delegate method doesn't work, and the update doesn't download. Even when I introduce webView with #property (nonatomic, strong) UIWebView *webView, the method doesn't do anything.
I think Dropbox has the MIME configured properly because I can download the .ipa through google Chrome.
So what I really need is a way using NSURLConnection (NSURLRequest etc.) to replicate the act of a user tapping on an HTML href. After that I think the full update will occur.
You can open a URL automatically using
[[UIApplication sharedApplication] openURL:...];
I don't know if it works for itms-services: urls, but it works for other bespoke URL schemes like tel:, fb: etc. so it should do unless Apple have specifically blocked it.

iPhone - tracking back button

I have a tableView which lists the contents of my document directory. I have some zip files in that. If I touch a file in the tableView, the corresponding zip file is unzipped and extracted in a temporary directory(newFilePath in my case). The contents unzipped is listed in the next tableView. When I touch the back button, the contents in the directory is listed again.
For example, consider that I have four zip files in my document directory.
songs.zip, videos.zip, files.zip, calculation.zip
When I run the application, all the four files are listed in the tableView. When I touch songs.zip, this file is extracted in the newFilePath and its contents are pushed to the next tableView. When I touch back, the previous tableView, i.e, the four files in the document directory are listed again. Everything works perfect.
The problem is, the extracted files in the newFilePath remains there itself. They occupy the memory unnecessarily. I want them to be removed from that path when I touch the back button, i.e, I want to make newFilePath empty when the back button is touched.
I tried for it. But, no use. I tried removeItemAtPath: method in viewWillAppear: and also in viewWillDisappear:. But it didnt work in both the cases.
Is there any other method to track the action of the back button? I want an event to take place when the back button is touched. So please help me by sharing your ideas. Here is my code for your verification.
This is my didSelectRowAtIndexPath:
NSString *filePath = //filePath
if([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSLog(#"File exists at path: %#", filePath);
} else {
NSLog(#"File does not exists at path: %#", filePath);
}
ZipArchive *zip = [[ZipArchive alloc] init];
NSString *newFilePath = //newFilePath
[[NSFileManager defaultManager] createDirectoryAtPath:newFilePath withIntermediateDirectories:NO attributes:nil error:nil];
BOOL result = NO;
if([zip UnzipOpenFile:filePath]) {
//zip file is there
if ([zip UnzipFileTo:newFilePath overWrite:YES]) {
//unzipped successfully
NSLog(#"Archive unzip Success");
result= YES;
} else {
NSLog(#"Failure To Extract Archive, maybe password?");
}
} else {
NSLog(#"Failure To Open Archive");
}
iDataTravellerAppDelegate *AppDelegate = (iDataTravellerAppDelegate *)[[UIApplication sharedApplication] delegate];
//Prepare to tableview.
MyFilesList *myFilesList = [[MyFilesList alloc] initWithNibName:#"MyFilesList" bundle:[NSBundle mainBundle]];
//Increment the Current View
myFilesList.CurrentLevel += 1;
viewPushed = YES;
//Push the new table view on the stack
myFilesList.directoryContent = [AppDelegate getTemporaryDirectoryItemList:newFilePath];
[myFilesList setTitle:detailedViewController.strName];
[self.navigationController pushViewController:myFilesList animated:YES];
[myFilesList release];
Thank you for your answers.
Oh ya, thats quite simple:
in LoadView,
self.navigationItem.leftBarButtonItem=[[UIBarButtonItem alloc]
initWithTitle:#"Back"
style:UIBarButtonItemStylePlain
target:self
action:#selector(backButtonHit)];
-(void)backButtonHit
{
// removeItemAtPath: newFilepath stuff here
[self.navigationController popViewControllerAnimated:YES];
}

UITextView hasText not working?

I am assigning the contents of the clipboard to the UITextView text property. However, when I check the hasText property, the condition is always false.
NSString paste_text = [[NSString alloc] init];
self.paste_text = [UIPasteboard generalPasteboard].string;
....
my_UITextView.text = self.paste_text;
//THIS CONDITION IS ALWAYS FALSE
if (my_UITextView hasText)
{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
#"Text ready to copy"
message:err_msg
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
}
You need to send your UITextView the hasText message by using brackets:
if ([my_UITextView hasText])
UPDATE:
Do you know that your UTTextView has text? You might want to check it on the console:
my_UITextView.text = self.paste_text;
NSLog(#"my_UITextView.text = %#",my_UITextView.text); // check for text
//THIS CONDITION IS ALWAYS FALSE
if ([my_UITextView hasText])
First off, your paste_text isn't used as intended since right after you alloc, it will immediately be discarded, possibly released, maybe not. You could actually just do away with the [[NSString alloc] init];.
Then add the following to test your code:
// delete:
// NSString paste_text = [[NSString alloc] init];
NSLog([UIPasteboard generalPasteboard].string);
self.paste_text = [UIPasteboard generalPasteboard].string;
NSLog(self.paste_text);
....
my_UITextView.text = self.paste_text;
NSLog(#"my_UITextView is %#, text contained: %#, my_UITextView , my_UITextView.text);
The first NSLog prints out the pasteboard string, the second the string once it is passed on to your paste_text, and the last will let you know if my_UITextView is non-nil, and what text it contains.
Also, if paste_text is a #property, what are its attributes? The text from [UIPasteboard generalPasteboard].string needs to be copied into it, otherwise when the pasteboard's string is changed, so is your paste_text.
How are you creating the my_UITextView property? If you did it in InterfaceBuilder, it's possible that you forgot to create an IBOutlet.