So my app begins on first run by parsing all the information in a json file (included in the project) and saves that into its core data model.
This runs fine via debugging deployment with XCode however when I install the app via Testflight, it crashes immediately.
I have identified it to be the parsing of the json file that is the problem as I have created a build that does not do this & instead grabs the data straight from a web api, the only problem with this is that from the web api it will take about an hour to download all the data, from the json file, it takes about a minute.
I have tried disabling BITCODE & ensuring that the scheme is set to Release, which seems to cover most of these start up problems via Testflight. Neither has worked.
My suspicion is that the json file has not correctly been packaged with the app when distributed via Testflight, but i have no idea how to remedy this. Does anyone have any suggestions?
I am including my import function below incase the error is contained within this and not the configuration.
The json file is roughly 26mb which is very large compared to most other files in the project.
My device is an iPhone X running iOS 11.2.6 and XCode 9.2,
the app also successfully runs on all simulators.
I am using Objective C
-(void)ImportInitialCards:(NSManagedObjectContext *) managedObjectContext
{
_privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
// Configure Managed Object Context
[_privateManagedObjectContext setParentContext:managedObjectContext];
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"cardDatabase1" ofType:#"json"];
NSInputStream *inputStream = [[NSInputStream alloc] initWithFileAtPath:filePath];
[inputStream open];
NSArray *cards = [NSJSONSerialization JSONObjectWithStream:inputStream options:kNilOptions error:nil];
float percentage = 1.0 / (float) cards.count;
for (NSDictionary *card in cards)
{
NSString *cardParticularsJson = [card valueForKey:#"cardContent"];
NSData *cardParticularsData = [cardParticularsJson dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *cardParticulars = [NSJSONSerialization JSONObjectWithData:cardParticularsData options:NSJSONReadingMutableContainers error:nil];
NSString *cardName = [cardParticulars valueForKey:#"name"];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Card"];
[request setPredicate:[NSPredicate predicateWithFormat:#"name == %#", cardName]];
NSError *error = nil;
NSUInteger count = [_privateManagedObjectContext countForFetchRequest:request error:&error];
if (count > 0)
{
NSLog(#"CardMatched: %#", cardName);
//card matched
_progress += percentage;
_currentProgressDescription = #"Scanning ...";
}
else
{
_currentProgressDescription = [NSString stringWithFormat:#"Importing: %#", cardName];
NSManagedObject *newCard = [NSEntityDescription insertNewObjectForEntityForName:#"Card" inManagedObjectContext:_privateManagedObjectContext];
[self SaveCardFrom:cardParticulars to:newCard saveThumb:NO inContext:_privateManagedObjectContext];
_progress += percentage;
dispatch_async(dispatch_get_main_queue(),^ {
NSError *error = nil;
[managedObjectContext save:&error];
} );
}
}
}
I've decided to post the solution to my problem in the hope that it helps someone else that struggled like me.
So while importing the JSON file I was updating a progress bar to indicate how long it would take, this was in a while loop that was maxing out the CPU, that along with the JSON import sent the CPU to approx 200% which caused the app to close.
The solution seems quite obvious really, don't make the cpu go above 100%
I now plan to only update the progress bar when the value is changed rather than constantly updating via a while loop.
Related
I have this strange issue that I am having trouble resolving. I am creating an App which allows music to be played back. When the screen is locked (and there is a currently playing song), the lock screen will populate with a bunch of data. One piece is the album art.
The problem is that after the phone is locked and I skip a few tracks (forwards or backwards), the UIImages are no longer being loaded. If I test out the functionality and quickly skip forward and backwards in my playback queue, the album art will appear for the first 4-5 songs. After that, the images stop appearing because I get a NSFileReadNoPermissionError from my code that grabs the image. I understand that I apparently do not have permission to access the png image files, but I do not understand why. My application created them, saved them on disk, and is now trying to load them from disk while my app is running in a background state.
The relevant code snippet:
+ (void)updateLockScreenInfoAndArtForSong:(Song *)song
{
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *artDirPath = [documentsPath stringByAppendingPathComponent:#"Album Art"];
NSString *path = artDirPath;
//-----> LIST ALL FILES for debugging <-----//
NSLog(#"LISTING ALL FILES FOUND");
int count;
NSArray *directoryContent = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path
error:NULL];
for (count = 0; count < (int)[directoryContent count]; count++)
{
NSLog(#"File %d: %#", (count + 1), [directoryContent objectAtIndex:count]);
}
//-----> END DEBUG CODE <-----//
Song *nowPlayingSong = [MusicPlaybackController nowPlayingSong];
Class playingInfoCenter = NSClassFromString(#"MPNowPlayingInfoCenter");
if (playingInfoCenter) {
NSMutableDictionary *songInfo = [[NSMutableDictionary alloc] init];
NSError *error;
NSData *data = [NSData dataWithContentsOfURL:
[AlbumArtUtilities albumArtFileNameToNSURL:nowPlayingSong.albumArtFileName] options:NSDataReadingUncached error:&error];
NSInteger code = error.code;
NSLog(#"Error code: %li", (long)code); //prints 257 sometimes, which is NSFileReadNoPermissionError
UIImage *albumArtImage = [UIImage imageWithData:data];
if(albumArtImage == nil){ //song has no album art, check if its album does
Album *songsAlbum = song.album;
if(songsAlbum){
albumArtImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:
[AlbumArtUtilities albumArtFileNameToNSURL:songsAlbum.albumArtFileName]]];
}
}
[songInfo setObject:nowPlayingSong.songName forKey:MPMediaItemPropertyTitle];
NSInteger duration = [nowPlayingSong.duration integerValue];
[songInfo setObject:[NSNumber numberWithInteger:duration]
forKey:MPMediaItemPropertyPlaybackDuration];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:songInfo];
}
}
Any help would be immensely appreciated! I have tried so many thing that I am at a loss for what to even try next. Note the above code snippet is called in
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
when event.subtype is UIEventSubtypeRemoteControlNextTrack or UIEventSubtypeRemoteControlPreviousTrack.
Figured it out after trying everything all day lol. Turns out that by default in iOS 8, much of the system is encrypted (files cannot be accessed after the phone is locked)...with a small delay of course. This is why a few album art images were loading but they stopped working after a few seconds. The delay between when the phone locked and the encryption enabled itself made it seem like my issue was "random".
Anyway for anyone reading this, my solution involved setting the file protection of all folders and subfolder and files leading to the album art directory.
Hint:
[attributes setValue:NSFileProtectionCompleteUntilFirstUserAuthentication forKey:NSFileProtectionKey];
Set that attribute on a file when it is being created (provide the information as an NSDictionary). If an existing directory or file is to be modified, gather the attributes of the thing using NSFileManager, and then set the values as shown above.
I am struck in a big problem. What I am trying to do is getting data from the server but the server in a single hit give all the data which is very large in quantity and I am doing all my process on the main thread, So there are around 400-500 images in the form of URL which I am saving in document directory in the form of NSData. So in the dubug navigator when the memory consumption reached around 80-90 mb then my application crashed and showing the following error:-
mach_vm_map(size=135168) failed (error code=3)
*** error: can't allocate region
*** set a breakpoint in malloc_error_break to debug
2015-01-23 17:10:03.946 ArchaioMobileViewer[853:148470] *** Terminating app due to uncaught exception 'NSMallocException', reason: 'Attempt to allocate 262144 bytes for NS/CFData failed'
I am Using ARC but still I am getting the memory problem. This is my code `-
(void)downloadDocumentsFromServer:(NSDictionary *)documentsList IsUpdate:(BOOL)isUpdate;
{
//Main Target(22)
BusinessLayer* bLL = [[BusinessLayer alloc]init];
FileManager* downloadImages = [FileManager alloc];
for(NSDictionary* inspDocumentResult in documentsList)
{
FloorDocument* floorDocument = [[FloorDocument alloc]init];
floorDocument.docID = [inspDocumentResult objectForKey:#"docID"];
floorDocument.buildingID = selectedBuildingID;
floorDocument.clientID = clientID;
NSDictionary* documentArray = [inspDocumentResult objectForKey:#"Document"];
floorDocument.docType = [[documentArray objectForKey:#"Type"] stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
floorDocument.docScale = [documentArray objectForKey:#"Scale"];
floorDocument.docDescription = [documentArray objectForKey:#"DocDesc"];
//floorDocument.floor = [bLL getFloorNameByDocIDAndBuildingID:selectedBuildingID DocID:floorDocument.docID];
floorDocument.floor = [inspDocumentResult objectForKey:#"Floor"];
NSLog(#"%#",[inspDocumentResult objectForKey:#"hiResImage"]);
[downloadImages downloadInspectionDocuments:[inspDocumentResult objectForKey:#"hiResImage"] ImageName:floorDocument.docID FileType:floorDocument.docType Folder:selectedBuildingID];
NSLog(#"Floor %# - High Res Image copied for %#",floorDocument.floor,floorDocument.docID);
//Download the Low Res Image
NSString* lowResImage = [inspDocumentResult objectForKey:#"lowResImage"];
[downloadImages downloadInspectionDocumentsLowRes:lowResImage ImageName:floorDocument.docID FileType:floorDocument.docType Folder:selectedBuildingID LowResName:#"lowResImage"];
//Copy the Quarter Size File
lowResImage = [lowResImage stringByReplacingOccurrencesOfString:#"LowRes" withString:#"LowRes4"];
[downloadImages downloadInspectionDocumentsLowRes:lowResImage ImageName:floorDocument.docID FileType:floorDocument.docType Folder:selectedBuildingID LowResName:#"lowResImage4"];
NSLog(#"Floor %# - Low Res Images copied for %#",floorDocument.floor,floorDocument.docID);
//Download the tiles
NSArray* tiles = [inspDocumentResult objectForKey:#"lsUrls"];
for(NSString* tile in tiles)
{
#autoreleasepool {
NSArray* tileNameArray = [tile componentsSeparatedByString:#"/"];
if(tileNameArray.count > 0)
{
NSString* destTile = [tileNameArray objectAtIndex:tileNameArray.count-1];
destTile = [destTile stringByReplacingOccurrencesOfString:[NSString stringWithFormat:#".%#",floorDocument.docType] withString:#""];
NSLog(#"TileName:%#",destTile);
[downloadImages downloadInspectionDocumentsTiles:tile ImageName:floorDocument.docID FileType:floorDocument.docType Folder:selectedBuildingID TileName:destTile];
}
}
}
NSLog(#"Floor %# - Tiles Image copied for %#",floorDocument.floor,floorDocument.docID);
NSLog(#"Downloading Documents Tiles For %# Completed at %#",floorDocument.docID,[bLL getCurrentDate]);
[bLL saveFloorDocuments:floorDocument IsUpdate:isUpdate];
// downloadImages=nil;
}
bLL = nil;
}
please help me out in this problem.`
This is the code which I am using inside the DownloadInspectionDocuments:-
-(void)downloadInspectionDocuments:(NSString *)url ImageName:(NSString *)imageName FileType:(NSString*)fileType Folder:(NSString*)folder
{
#autoreleasepool
{
NSString* source =[FileManager getInspectionDocumentsFolder];
//Lets get the destination folder
NSString *destination = [NSString stringWithFormat:#"%#/%#/%#",source,folder,imageName];
[self createFolder:destination CreateSubFolders:true];
NSString *filePath = [NSString stringWithFormat:#"%#/%#.%#",destination,imageName,fileType];
NSFileManager* fm = [[NSFileManager alloc]init];
if(![fm fileExistsAtPath:filePath])
{
NSData *data1 = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
[data1 writeToFile:filePath atomically:YES];
}
}
// return [NSString stringWithFormat:#"%#.%#",imageName,fileType];
}
ARC isn't garbage collection: it will insert the memory management code (retain/release) for you but you still need to make sure you are not using up too many resources (in the same way you would in non-ARC code).
You are running this large loop on the main thread, so any memory being consumed is not going to be freed until the next run loop.
You need to break this function down into smaller steps that can be carried out in stages.
For now, if there isn't too much memory consumed for a single iteration of the outer-loop of the function you can add an autorelease pool at that level (I see you have on on the inner loop)
for(NSDictionary* inspDocumentResult in documentsList)
{
#autoreleasepool {
.... remaining code goes here
}
}
and it will at least drain what it can each iteration.
Given that you are downloading a large number of files and will be relying on network connectivity I would recommend performing the downloads asynchronously though. If you haven't already, check out AFNetworking to simplify this. This will give you much more control over your resources than you are getting now with a resource-intensive blocking call on the main thread.
You can save yourself a lot of work by following davbryn's and Andrea's suggestions to use AFNetworking and stream the file. Basically, don't put the whole file in memory and then write it to disk, write to disk as you get the bytes from the network. This should reduce the pressure on memory. For example:
- (void)downloadFile:(NSString *)urlString {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
NSString *destinationPath = [NSDocumentDirectory() stringByAppendingPathComponent:#"some-file-name"];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setOutputStream:[NSOutputStream outputStreamToFileAtPath:destinationPath append:NO]];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Super duper awesome!");
// Maybe start another download here?
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error downloading file: %#", error);
}];
[operation start];
}
So then all you need to do is generate the list of things to download and in your success block start downloading another file.
I've got a quiz app on the Apple app store. The questions are stored in a Plist file. What I'm looking to do is find a way to update the Plist file via downloading a new version and not having to submit an update every time I have new questions to add
Does anyone know of a decent tutorial which may help me?
Many thanks.
I'm not sure about tutorials, but the steps to achieve what you describe are pretty simple:
Create a url request to your remote data
Parse the returned data
Write the parsed data to a new local plist
Eg:
// Create a NSURL to your data
NSString *dataPath = #"www.mydata.com/path";
NSURL *dataURL = [NSURL URLWithString:dataPath];
// Create an asycnhronous request along with a block to be executed when complete
[NSURLConnection sendAsynchronousRequest:[[NSURLRequest alloc] initWithURL:dataURL]
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (error) {
NSLog(#"%#", error.localizedDescription);
// Handle the request error
}
else {
// We have some data, now we need to serialize it so that it's usable
NSError *serializationError;
NSDictionary *serializedDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&serializationError];
if (serializationError) {
NSLog(#"%#", serializationError.localizedDescription);
// Handle the serialization error
}
else {
// We have a serialized NSDictionary, now we just want to write it
// First we create the path to write it to, eg. uers documents directory
NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
// Then write it
BOOL success = [serializedDictionary writeToFile:documentsDirectory.path atomically:YES];
if (!success) {
NSLog(#"Error writing file");
}
}
}
}];
Note: You may want to think about storing your data in a remote database like Parse. This way you can query for new questions and only download those so you're not using unnecessary bandwidth. You may also want to consider using Core Data to maintain your local data rather than writing it to a plist. The main advantage being that you don't have to serialise the entire plist into memory to use it - you can just query for the particular questions you need.
Hope this helps.
I use Core Data for most of my projects that require data persistence from launch to launch, but what if I just want to pull information down from an API such as Twitter's and present it to the user, and I don't require it to be persistent from launch to launch?
Is the typical solution here to simply use NSMutableArray and store all the objects therein, and create a class for what's stored in it, similar to Core Data's managed objects?
I've had great success using Mantle.
Yes, an NSMutableArray of custom objects will work. An NSMutableArray of NSDictionary's is also a common solution, and works well for simple data sets.
https://developer.apple.com/library/ios/referencelibrary/GettingStarted/RoadMapiOS/ThirdTutorial.html#//apple_ref/doc/uid/TP40011343-CH10-SW1
Assuming that you can request a JSON response from the server, the code to pull information from the server would look something like this. The jsonData object produced by this code consists of nested NSArrays and NSDictionarys. The NSLog at the end will dump the data set returned by the server.
- (void)retrieveJSONdataFromServer:(NSString *)param1
{
NSError *error;
NSString *str = [NSString stringWithFormat:#"http://www.example.com/sometopic/api/json?param1=%#", param1 ];
NSURL *url = [NSURL URLWithString:str];
NSData *data = [NSData dataWithContentsOfURL:url options:0 error:&error];
if ( !data )
{
NSLog( #"%#", error );
return;
}
id jsonData = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if ( !jsonData )
{
NSLog( #"%#", error );
return;
}
// do something with the JSON data here
// for this sample code, we just dump the data to the debug console
NSLog( #"%#", jsonData );
}
I am very new to iOS development, but I would like to make an app that has two table view controllers (columns): both are a row of images that act as links. The first would be a column of YouTube videos and the second a column of websites. I would like to have all these listed in a file file.txt listed like so: V, http://youtube.com/example W, http://example.com
There would be a long list of those, the V meaning its a video (for the video column) and W for the websites. Now, I understand how to being the single file in, but what happens afterwards is my concern. Can I read each line into some sort of queue and then fire the NSURL request for each one consecutively? How can that be done with NSURL? Is there perhaps a better approach?
There are two questions for me:
Is a text file really the best format?
I might suggest a plist or archive (if the file is only going to exist only in your app's bundle and/or documents folder) or JSON (if it's going to live on a server before delivering it to the user) instead of a text file. It will make it easier to parse this file than a text file. For example, consider the following dictionary:
NSDictionary *dictionary = #{#"videos" : #[#"http://youtube.com/abc", #"http://vimeo.com/xyz"],
#"websites": #[#"http://apple.com", #"http://microsoft.com"]};
You can save that to a plist with:
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *plistPath = [documentsPath stringByAppendingPathComponent:#"files.plist"];
[dictionary writeToFile:plistPath atomically:YES];
You can add that file to your bundle or whatever, and then read it at a future date with:
dictionary = [NSDictionary dictionaryWithContentsOfFile:plistPath];
You can, alternatively, write that to a JSON file with:
NSError *error = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:&error];
NSString *jsonPath = [documentsPath stringByAppendingPathComponent:#"files.json"];
[data writeToFile:jsonPath atomically:YES];
You can read that JSON file with:
data = [NSData dataWithContentsOfFile:jsonPath];
dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
Either way, you can get the list of videos or web sites like so:
NSArray *videos = dictionary[#"videos"];
NSArray *websites = dictionary[#"websites"];
Now that you have your arrays of videos and websites, the question then is how you then use those URLs.
You could do something like:
for (NSString *urlString in videos) {
NSURL *url = [NSURL URLWithString: urlString];
// now do something with the URL
}
The big question is what is the "do something" logic. Because you're dealing with a lot of URLs, you would want to use a NSOperation based solution, not a GCD solution, because NSOperationQueue lets you control the degree of concurrency. I'd suggest a NSOperation-based networking library like AFNetworking. For example, to download the HTML for your websites:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
for (NSString *urlString in websites)
{
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// convert the `NSData` responseObject to a string, if you want
NSString *string = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
// now do something with it, like saving it in a cache or persistent storage
// I'll just log it
NSLog(#"responseObject string = %#", string);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"error = %#", error);
}];
[queue addOperation:operation];
}
Having said that, I'm not sure it makes sense to kick off a ton of network requests. Wouldn't you really prefer to wait until the user taps on one of those cells before retrieving it (and for example, then just open that URL in a UIWebView)? You don't want an app that unnecessarily chews up the user's data plan and battery retrieving stuff that they might not want to retrieve. (Apple has rejected apps that request too much data from a cellular connection.) Or, at the very least, if you want to retrieve stuff up front, only retrieve stuff as you need it (e.g. in cellForRowAtIndexPath), which will retrieve the visible rows, rather than the hundreds of rows that might be in your text/plist/json file.
Frankly, we need a clearer articulation of what you're trying to do, and we might be able to help you with more concise counsel.