Trying to figure out why NSMutableString:rangeOfString: is returning really weird results. NSLog is showing me a result like this:
location=9223372036854775807 length=0:This is a test
My test string does not contain "###", so I'd expect location=0 length=0. The weird location keeps coming up until the string actually contains "###" then location and length are correct. What am I missing in the below code snippet?
ServerPacketMotd.h
typedef struct _serverPacketMotdStruct
{
int8_t type; /* SP_MOTD */
int8_t pad1;
int8_t pad2;
int8_t pad3;
int8_t line[80];
} serverPacketMotdStruct;
ServerPacketMotd.m
#import "ServerPacketMotd.h"
#interface ServerPacketMotd()
{
NSMutableString *buffer;
}
#end
#implementation ServerPacketMotd
- (id)init
{
if( !( self = [super init] ) )
return nil;
buffer = [[NSMutableString alloc] init];
return self;
}
- (NSMutableData *)handlePacket:(NSData *)data withTag:(long)tag
{
serverPacketMotdStruct gamePacket;
uint16_t size = sizeof(serverPacketMotdStruct);
NSRange packetWindow = NSMakeRange(0, size);
NSRange atAtAt = NSMakeRange(0,0);
while (expression)
{
[data getBytes:&gamePacket range:packetWindow];
[buffer appendFormat:#"%s\n", gamePacket.line];
atAtAt = [buffer rangeOfString:#"###"];
NSLog(#"XXX location=%lu length=%lu:%#", atAtAt.location, atAtAt.length, buffer);
}
Check if atAtAt.location == NSNotFound. A location of 0 means the string was found at location 0, it doesn't mean it wan't found.
I have just recently found Objective Zip Ihave been reading through the instructions to get it set up in my project. However I am not really sure how to use it to decompress some NSData I have that I am wanting to decompress.
I have looked at the example solution and it seems to be performing the unzip on a zip file the code looks roughly like this
ZipFile *unzipFile= [[ZipFile alloc] initWithFileName:filePath mode:ZipFileModeUnzip];
[unzipFile goToFirstFileInZip];
ZipReadStream *read1= [unzipFile readCurrentFileInZip];
give or take some other instructions this is how they show you to use it, their sample code is here
I would like to know how to do the same thing but using NSData? or would I have to convert the NSData into a zipFile? if so how is that performed properly?
The NSData I am trying to unzip if zlib compressed... any example code would be helpful
here it is https://stackoverflow.com/a/6466832/751885
I use the following two methods process NSData
and call
saveToFile
method write on disk.
[[self compressData:uncompressedData] writeToFile:#"fileName.zip" atomically:YES];
Compress:
-(NSData*) compressData:(NSData* )uncompressedData {
if ([uncompressedData length] == 0) return uncompressedData;
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.total_out = 0;
strm.next_in=(Bytef *)[uncompressedData bytes];
strm.avail_in = (unsigned int)[uncompressedData length];
// Compresssion Levels:
// Z_NO_COMPRESSION
// Z_BEST_SPEED
// Z_BEST_COMPRESSION
// Z_DEFAULT_COMPRESSION
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) return nil;
NSMutableData *compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion
do {
if (strm.total_out >= [compressed length])
[compressed increaseLengthBy: 16384];
strm.next_out = [compressed mutableBytes] + strm.total_out;
strm.avail_out = (unsigned int)([compressed length] - strm.total_out);
deflate(&strm, Z_FINISH);
} while (strm.avail_out == 0);
deflateEnd(&strm);
[compressed setLength: strm.total_out];
return [NSData dataWithData:compressed];
}
Uncompress:
-(NSData*) uncompressGZip:(NSData*) compressedData {
if ([compressedData length] == 0) return compressedData;
NSUInteger full_length = [compressedData length];
NSUInteger half_length = [compressedData length] / 2;
NSMutableData *decompressed = [NSMutableData dataWithLength: full_length + half_length];
BOOL done = NO;
int status;
z_stream strm;
strm.next_in = (Bytef *)[compressedData bytes];
strm.avail_in = (unsigned int)[compressedData length];
strm.total_out = 0;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
if (inflateInit2(&strm, (15+32)) != Z_OK) return nil;
while (!done) {
// Make sure we have enough room and reset the lengths.
if (strm.total_out >= [decompressed length]) {
[decompressed increaseLengthBy: half_length];
}
strm.next_out = [decompressed mutableBytes] + strm.total_out;
strm.avail_out = (unsigned int)([decompressed length] - strm.total_out);
// Inflate another chunk.
status = inflate (&strm, Z_SYNC_FLUSH);
if (status == Z_STREAM_END) {
done = YES;
} else if (status != Z_OK) {
break;
}
}
if (inflateEnd (&strm) != Z_OK) return nil;
// Set real length.
if (done) {
[decompressed setLength: strm.total_out];
return [NSData dataWithData: decompressed];
} else {
return nil;
}
}
So, I would like to get a sound file and convert it in packets, and send it to another computer. I would like that the other computer be able to play the packets as they arrive.
I am using AVAudioPlayer to try to play this packets, but I couldn't find a proper way to serialize the data on the peer1 that the peer2 can play.
The scenario is, peer1 has a audio file, split the audio file in many small packets, put them on a NSData and send them to peer2. Peer 2 receive the packets and play one by one, as they arrive.
Does anyone have know how to do this? or even if it is possible?
EDIT:
Here it is some piece of code to illustrate what I would like to achieve.
// This code is part of the peer1, the one who sends the data
- (void)sendData
{
int packetId = 0;
NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:#"myAudioFile" ofType:#"wav"];
NSData *soundData = [[NSData alloc] initWithContentsOfFile:soundFilePath];
NSMutableArray *arraySoundData = [[NSMutableArray alloc] init];
// Spliting the audio in 2 pieces
// This is only an illustration
// The idea is to split the data into multiple pieces
// dependin on the size of the file to be sent
NSRange soundRange;
soundRange.length = [soundData length]/2;
soundRange.location = 0;
[arraySoundData addObject:[soundData subdataWithRange:soundRange]];
soundRange.length = [soundData length]/2;
soundRange.location = [soundData length]/2;
[arraySoundData addObject:[soundData subdataWithRange:soundRange]];
for (int i=0; i<[arraySoundData count]; i++)
{
NSData *soundPacket = [arraySoundData objectAtIndex:i];
if(soundPacket == nil)
{
NSLog(#"soundData is nil");
return;
}
NSMutableData* message = [[NSMutableData alloc] init];
NSKeyedArchiver* archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:message];
[archiver encodeInt:packetId++ forKey:PACKET_ID];
[archiver encodeObject:soundPacket forKey:PACKET_SOUND_DATA];
[archiver finishEncoding];
NSError* error = nil;
[connectionManager sendMessage:message error:&error];
if (error) NSLog (#"send greeting failed: %#" , [error localizedDescription]);
[message release];
[archiver release];
}
[soundData release];
[arraySoundData release];
}
// This is the code on peer2 that would receive and play the piece of audio on each packet
- (void) receiveData:(NSData *)data
{
NSKeyedUnarchiver* unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
if ([unarchiver containsValueForKey:PACKET_ID])
NSLog(#"DECODED PACKET_ID: %i", [unarchiver decodeIntForKey:PACKET_ID]);
if ([unarchiver containsValueForKey:PACKET_SOUND_DATA])
{
NSLog(#"DECODED sound");
NSData *sound = (NSData *)[unarchiver decodeObjectForKey:PACKET_SOUND_DATA];
if (sound == nil)
{
NSLog(#"sound is nil!");
}
else
{
NSLog(#"sound is not nil!");
AVAudioPlayer *audioPlayer = [AVAudioPlayer alloc];
if ([audioPlayer initWithData:sound error:nil])
{
[audioPlayer prepareToPlay];
[audioPlayer play];
} else {
[audioPlayer release];
NSLog(#"Player couldn't load data");
}
}
}
[unarchiver release];
}
So, here is what I am trying to achieve...so, what I really need to know is how to create the packets, so peer2 can play the audio.
It would be a kind of streaming. Yes, for now I am not worried about the order that the packet are received or played...I only need to get the sound sliced and them be able to play each piece, each slice, without need to wait for the whole file be received by peer2.
Thanks!
Here is simplest class to play files with AQ
Note that you can play it from any point (just set currentPacketNumber)
#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>
#interface AudioFile : NSObject {
AudioFileID fileID; // the identifier for the audio file to play
AudioStreamBasicDescription format;
UInt64 packetsCount;
UInt32 maxPacketSize;
}
#property (readwrite) AudioFileID fileID;
#property (readwrite) UInt64 packetsCount;
#property (readwrite) UInt32 maxPacketSize;
- (id) initWithURL: (CFURLRef) url;
- (AudioStreamBasicDescription *)audioFormatRef;
#end
// AudioFile.m
#import "AudioFile.h"
#implementation AudioFile
#synthesize fileID;
#synthesize format;
#synthesize maxPacketSize;
#synthesize packetsCount;
- (id)initWithURL:(CFURLRef)url{
if (self = [super init]){
AudioFileOpenURL(
url,
0x01, //fsRdPerm, read only
0, //no hint
&fileID
);
UInt32 sizeOfPlaybackFormatASBDStruct = sizeof format;
AudioFileGetProperty (
fileID,
kAudioFilePropertyDataFormat,
&sizeOfPlaybackFormatASBDStruct,
&format
);
UInt32 propertySize = sizeof (maxPacketSize);
AudioFileGetProperty (
fileID,
kAudioFilePropertyMaximumPacketSize,
&propertySize,
&maxPacketSize
);
propertySize = sizeof(packetsCount);
AudioFileGetProperty(fileID, kAudioFilePropertyAudioDataPacketCount, &propertySize, &packetsCount);
}
return self;
}
-(AudioStreamBasicDescription *)audioFormatRef{
return &format;
}
- (void) dealloc {
AudioFileClose(fileID);
[super dealloc];
}
// AQPlayer.h
#import <Foundation/Foundation.h>
#import "AudioFile.h"
#define AUDIOBUFFERS_NUMBER 3
#define MAX_PACKET_COUNT 4096
#interface AQPlayer : NSObject {
#public
AudioQueueRef queue;
AudioQueueBufferRef buffers[AUDIOBUFFERS_NUMBER];
NSInteger bufferByteSize;
AudioStreamPacketDescription packetDescriptions[MAX_PACKET_COUNT];
AudioFile * audioFile;
SInt64 currentPacketNumber;
UInt32 numPacketsToRead;
}
#property (nonatomic) SInt64 currentPacketNumber;
#property (nonatomic, retain) AudioFile * audioFile;
-(id)initWithFile:(NSString *)file;
-(NSInteger)fillBuffer:(AudioQueueBufferRef)buffer;
-(void)play;
#end
// AQPlayer.m
#import "AQPlayer.h"
static void AQOutputCallback(void * inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
AQPlayer * aqp = (AQPlayer *)inUserData;
[aqp fillBuffer:(AudioQueueBufferRef)inBuffer];
}
#implementation AQPlayer
#synthesize currentPacketNumber;
#synthesize audioFile;
-(id)initWithFile:(NSString *)file{
if ([self init]){
audioFile = [[AudioFile alloc] initWithURL:[NSURL fileURLWithPath:file]];
currentPacketNumber = 0;
AudioQueueNewOutput ([audioFile audioFormatRef], AQOutputCallback, self, CFRunLoopGetCurrent (), kCFRunLoopCommonModes, 0, &queue);
bufferByteSize = 4096;
if (bufferByteSize < audioFile.maxPacketSize) bufferByteSize = audioFile.maxPacketSize;
numPacketsToRead = bufferByteSize/audioFile.maxPacketSize;
for(int i=0; i<AUDIOBUFFERS_NUMBER; i++){
AudioQueueAllocateBuffer (queue, bufferByteSize, &buffers[i]);
}
}
return self;
}
-(void) dealloc{
[audioFile release];
if (queue){
AudioQueueDispose(queue, YES);
queue = nil;
}
[super dealloc];
}
- (void)play{
for (int bufferIndex = 0; bufferIndex < AUDIOBUFFERS_NUMBER; ++bufferIndex){
[self fillBuffer:buffers[bufferIndex]];
}
AudioQueueStart (queue, NULL);
}
-(NSInteger)fillBuffer:(AudioQueueBufferRef)buffer{
UInt32 numBytes;
UInt32 numPackets = numPacketsToRead;
BOOL isVBR = [audioFile audioFormatRef]->mBytesPerPacket == 0 ? YES : NO;
AudioFileReadPackets(
audioFile.fileID,
NO,
&numBytes,
isVBR ? packetDescriptions : 0,
currentPacketNumber,
&numPackets,
buffer->mAudioData
);
if (numPackets > 0) {
buffer->mAudioDataByteSize = numBytes;
AudioQueueEnqueueBuffer (
queue,
buffer,
isVBR ? numPackets : 0,
isVBR ? packetDescriptions : 0
);
}
else{
// end of present data, check if all packets are played
// if yes, stop play and dispose queue
// if no, pause queue till new data arrive then start it again
}
return numPackets;
}
It seems you are solving wrong task, because AVAudioPlayer capable play only whole audiofile. You should use Audio Queue Service from AudioToolbox framework instead, to play audio on packet-by-packet basis. In fact you need not divide audiofile into real sound packets, you can use any data block like in your own example above, but then you should read received data chuncks using Audiofile Service or Audio File Stream Services functions (from AudioToolbox) and feed them to audioqueue buffers.
If you nevertheless want to divide audiofile into sound packets, you can easily do it with Audiofile Service functions. Audiofile consist of header where its properties like number of packets, samplerate, number of channels etc. are stored, and raw sound data.
Use AudioFileOpenURL to open audiofile and take all its properties with AudioFileGetProperty function. Basicaly you need only kAudioFilePropertyDataFormat and kAudioFilePropertyAudioDataPacketCount properties:
AudioFileID fileID; // the identifier for the audio file
CFURLRef fileURL = ...; // file URL
AudioStreamBasicDescription format; // structure containing audio header info
UInt64 packetsCount;
AudioFileOpenURL(fileURL,
0x01, //fsRdPerm, // read only
0, //no hint
&fileID
);
UInt32 sizeOfPlaybackFormatASBDStruct = sizeof format;
AudioFileGetProperty (
fileID,
kAudioFilePropertyDataFormat,
&sizeOfPlaybackFormatASBDStruct,
&format
);
propertySize = sizeof(packetsCount);
AudioFileGetProperty(fileID, kAudioFilePropertyAudioDataPacketCount, &propertySize, &packetsCount);
Then you can take any range of audiopackets data with:
OSStatus AudioFileReadPackets (
AudioFileID inAudioFile,
Boolean inUseCache,
UInt32 *outNumBytes,
AudioStreamPacketDescription *outPacketDescriptions,
SInt64 inStartingPacket,
UInt32 *ioNumPackets,
void *outBuffer
);
Apple already has written something that can do this: AUNetSend and AUNetReceive. AUNetSend is an effect AudioUnit that sends audio to an AUNetReceive AudioUnit on another computer.
Unfortunately these AUs are not available on the iPhone, though.
What is the appropriate way of dealing with large text files in Objective-C? Let's say I need to read each line separately and want to treat each line as an NSString. What is the most efficient way of doing this?
One solution is using the NSString method:
+ (id)stringWithContentsOfFile:(NSString *)path
encoding:(NSStringEncoding)enc
error:(NSError **)error
and then split the lines with a newline separator, and then iterate over the elements in the array. However, this seems fairly inefficient. Is there no easy way to treat the file as a stream, enumerating over each line, instead of just reading it all in at once? Kinda like Java's java.io.BufferedReader.
This will work for general reading a String from Text.
If you would like to read longer text (large size of text), then use the method that other people here were mentioned such as buffered (reserve the size of the text in memory space).
Say you read a Text File.
NSString* filePath = #""//file path...
NSString* fileRoot = [[NSBundle mainBundle]
pathForResource:filePath ofType:#"txt"];
You want to get rid of new line.
// read everything from text
NSString* fileContents =
[NSString stringWithContentsOfFile:fileRoot
encoding:NSUTF8StringEncoding error:nil];
// first, separate by new line
NSArray* allLinedStrings =
[fileContents componentsSeparatedByCharactersInSet:
[NSCharacterSet newlineCharacterSet]];
// then break down even further
NSString* strsInOneLine =
[allLinedStrings objectAtIndex:0];
// choose whatever input identity you have decided. in this case ;
NSArray* singleStrs =
[currentPointString componentsSeparatedByCharactersInSet:
[NSCharacterSet characterSetWithCharactersInString:#";"]];
There you have it.
That's a great question. I think #Diederik has a good answer, although it's unfortunate that Cocoa doesn't have a mechanism for exactly what you want to do.
NSInputStream allows you to read chunks of N bytes (very similar to java.io.BufferedReader), but you have to convert it to an NSString on your own, then scan for newlines (or whatever other delimiter) and save any remaining characters for the next read, or read more characters if a newline hasn't been read yet. (NSFileHandle lets you read an NSData which you can then convert to an NSString, but it's essentially the same process.)
Apple has a Stream Programming Guide that can help fill in the details, and this SO question may help as well if you're going to be dealing with uint8_t* buffers.
If you're going to be reading strings like this frequently (especially in different parts of your program) it would be a good idea to encapsulate this behavior in a class that can handle the details for you, or even subclassing NSInputStream (it's designed to be subclassed) and adding methods that allow you to read exactly what you want.
For the record, I think this would be a nice feature to add, and I'll be filing an enhancement request for something that makes this possible. :-)
Edit: Turns out this request already exists. There's a Radar dating from 2006 for this (rdar://4742914 for Apple-internal people).
This should do the trick:
#include <stdio.h>
NSString *readLineAsNSString(FILE *file)
{
char buffer[4096];
// tune this capacity to your liking -- larger buffer sizes will be faster, but
// use more memory
NSMutableString *result = [NSMutableString stringWithCapacity:256];
// Read up to 4095 non-newline characters, then read and discard the newline
int charsRead;
do
{
if(fscanf(file, "%4095[^\n]%n%*c", buffer, &charsRead) == 1)
[result appendFormat:#"%s", buffer];
else
break;
} while(charsRead == 4095);
return result;
}
Use as follows:
FILE *file = fopen("myfile", "r");
// check for NULL
while(!feof(file))
{
NSString *line = readLineAsNSString(file);
// do stuff with line; line is autoreleased, so you should NOT release it (unless you also retain it beforehand)
}
fclose(file);
This code reads non-newline characters from the file, up to 4095 at a time. If you have a line that is longer than 4095 characters, it keeps reading until it hits a newline or end-of-file.
Note: I have not tested this code. Please test it before using it.
Mac OS X is Unix, Objective-C is C superset, so you can just use old-school fopen and fgets from <stdio.h>. It's guaranteed to work.
[NSString stringWithUTF8String:buf] will convert C string to NSString. There are also methods for creating strings in other encodings and creating without copying.
You can use NSInputStream which has a basic implementation for file streams. You can read bytes into a buffer (read:maxLength: method). You have to scan the buffer for newlines yourself.
The appropriate way to read text files in Cocoa/Objective-C is documented in Apple's String programming guide. The section for reading and writing files should be just what you're after. PS: What's a "line"? Two sections of a string separated by "\n"? Or "\r"? Or "\r\n"? Or maybe you're actually after paragraphs? The previously mentioned guide also includes a section on splitting a string into lines or paragraphs. (This section is called "Paragraphs and Line Breaks", and is linked to in the left-hand-side menu of the page I pointed to above. Unfortunately this site doesn't allow me to post more than one URL as I'm not a trustworthy user yet.)
To paraphrase Knuth: premature optimisation is the root of all evil. Don't simply assume that "reading the whole file into memory" is slow. Have you benchmarked it? Do you know that it actually reads the whole file into memory? Maybe it simply returns a proxy object and keeps reading behind the scenes as you consume the string? (Disclaimer: I have no idea if NSString actually does this. It conceivably could.) The point is: first go with the documented way of doing things. Then, if benchmarks show that this doesn't have the performance you desire, optimise.
A lot of these answers are long chunks of code or they read in the entire file. I like to use the c methods for this very task.
FILE* file = fopen("path to my file", "r");
size_t length;
char *cLine = fgetln(file,&length);
while (length>0) {
char str[length+1];
strncpy(str, cLine, length);
str[length] = '\0';
NSString *line = [NSString stringWithFormat:#"%s",str];
% Do what you want here.
cLine = fgetln(file,&length);
}
Note that fgetln will not keep your newline character. Also, We +1 the length of the str because we want to make space for the NULL termination.
Just like #porneL said, the C api is very handy.
NSString* fileRoot = [[NSBundle mainBundle] pathForResource:#"record" ofType:#"txt"];
FILE *file = fopen([fileRoot UTF8String], "r");
char buffer[256];
while (fgets(buffer, 256, file) != NULL){
NSString* result = [NSString stringWithUTF8String:buffer];
NSLog(#"%#",result);
}
To read a file line by line (also for extreme big files) can be done by the following functions:
DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile];
NSString * line = nil;
while ((line = [reader readLine])) {
NSLog(#"read line: %#", line);
}
[reader release];
Or:
DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile];
[reader enumerateLinesUsingBlock:^(NSString * line, BOOL * stop) {
NSLog(#"read line: %#", line);
}];
[reader release];
The class DDFileReader that enables this is the following:
Interface File (.h):
#interface DDFileReader : NSObject {
NSString * filePath;
NSFileHandle * fileHandle;
unsigned long long currentOffset;
unsigned long long totalFileLength;
NSString * lineDelimiter;
NSUInteger chunkSize;
}
#property (nonatomic, copy) NSString * lineDelimiter;
#property (nonatomic) NSUInteger chunkSize;
- (id) initWithFilePath:(NSString *)aPath;
- (NSString *) readLine;
- (NSString *) readTrimmedLine;
#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL *))block;
#endif
#end
Implementation (.m)
#import "DDFileReader.h"
#interface NSData (DDAdditions)
- (NSRange) rangeOfData_dd:(NSData *)dataToFind;
#end
#implementation NSData (DDAdditions)
- (NSRange) rangeOfData_dd:(NSData *)dataToFind {
const void * bytes = [self bytes];
NSUInteger length = [self length];
const void * searchBytes = [dataToFind bytes];
NSUInteger searchLength = [dataToFind length];
NSUInteger searchIndex = 0;
NSRange foundRange = {NSNotFound, searchLength};
for (NSUInteger index = 0; index < length; index++) {
if (((char *)bytes)[index] == ((char *)searchBytes)[searchIndex]) {
//the current character matches
if (foundRange.location == NSNotFound) {
foundRange.location = index;
}
searchIndex++;
if (searchIndex >= searchLength) { return foundRange; }
} else {
searchIndex = 0;
foundRange.location = NSNotFound;
}
}
return foundRange;
}
#end
#implementation DDFileReader
#synthesize lineDelimiter, chunkSize;
- (id) initWithFilePath:(NSString *)aPath {
if (self = [super init]) {
fileHandle = [NSFileHandle fileHandleForReadingAtPath:aPath];
if (fileHandle == nil) {
[self release]; return nil;
}
lineDelimiter = [[NSString alloc] initWithString:#"\n"];
[fileHandle retain];
filePath = [aPath retain];
currentOffset = 0ULL;
chunkSize = 10;
[fileHandle seekToEndOfFile];
totalFileLength = [fileHandle offsetInFile];
//we don't need to seek back, since readLine will do that.
}
return self;
}
- (void) dealloc {
[fileHandle closeFile];
[fileHandle release], fileHandle = nil;
[filePath release], filePath = nil;
[lineDelimiter release], lineDelimiter = nil;
currentOffset = 0ULL;
[super dealloc];
}
- (NSString *) readLine {
if (currentOffset >= totalFileLength) { return nil; }
NSData * newLineData = [lineDelimiter dataUsingEncoding:NSUTF8StringEncoding];
[fileHandle seekToFileOffset:currentOffset];
NSMutableData * currentData = [[NSMutableData alloc] init];
BOOL shouldReadMore = YES;
NSAutoreleasePool * readPool = [[NSAutoreleasePool alloc] init];
while (shouldReadMore) {
if (currentOffset >= totalFileLength) { break; }
NSData * chunk = [fileHandle readDataOfLength:chunkSize];
NSRange newLineRange = [chunk rangeOfData_dd:newLineData];
if (newLineRange.location != NSNotFound) {
//include the length so we can include the delimiter in the string
chunk = [chunk subdataWithRange:NSMakeRange(0, newLineRange.location+[newLineData length])];
shouldReadMore = NO;
}
[currentData appendData:chunk];
currentOffset += [chunk length];
}
[readPool release];
NSString * line = [[NSString alloc] initWithData:currentData encoding:NSUTF8StringEncoding];
[currentData release];
return [line autorelease];
}
- (NSString *) readTrimmedLine {
return [[self readLine] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL*))block {
NSString * line = nil;
BOOL stop = NO;
while (stop == NO && (line = [self readLine])) {
block(line, &stop);
}
}
#endif
#end
The class was done by Dave DeLong
As others have answered both NSInputStream and NSFileHandle are fine options, but it can also be done in a fairly compact way with NSData and memory mapping:
BRLineReader.h
#import <Foundation/Foundation.h>
#interface BRLineReader : NSObject
#property (readonly, nonatomic) NSData *data;
#property (readonly, nonatomic) NSUInteger linesRead;
#property (strong, nonatomic) NSCharacterSet *lineTrimCharacters;
#property (readonly, nonatomic) NSStringEncoding stringEncoding;
- (instancetype)initWithFile:(NSString *)filePath encoding:(NSStringEncoding)encoding;
- (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding;
- (NSString *)readLine;
- (NSString *)readTrimmedLine;
- (void)setLineSearchPosition:(NSUInteger)position;
#end
BRLineReader.m
#import "BRLineReader.h"
static unsigned char const BRLineReaderDelimiter = '\n';
#implementation BRLineReader
{
NSRange _lastRange;
}
- (instancetype)initWithFile:(NSString *)filePath encoding:(NSStringEncoding)encoding
{
self = [super init];
if (self) {
NSError *error = nil;
_data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedAlways error:&error];
if (!_data) {
NSLog(#"%#", [error localizedDescription]);
}
_stringEncoding = encoding;
_lineTrimCharacters = [NSCharacterSet whitespaceAndNewlineCharacterSet];
}
return self;
}
- (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding
{
self = [super init];
if (self) {
_data = data;
_stringEncoding = encoding;
_lineTrimCharacters = [NSCharacterSet whitespaceAndNewlineCharacterSet];
}
return self;
}
- (NSString *)readLine
{
NSUInteger dataLength = [_data length];
NSUInteger beginPos = _lastRange.location + _lastRange.length;
NSUInteger endPos = 0;
if (beginPos == dataLength) {
// End of file
return nil;
}
unsigned char *buffer = (unsigned char *)[_data bytes];
for (NSUInteger i = beginPos; i < dataLength; i++) {
endPos = i;
if (buffer[i] == BRLineReaderDelimiter) break;
}
// End of line found
_lastRange = NSMakeRange(beginPos, endPos - beginPos + 1);
NSData *lineData = [_data subdataWithRange:_lastRange];
NSString *line = [[NSString alloc] initWithData:lineData encoding:_stringEncoding];
_linesRead++;
return line;
}
- (NSString *)readTrimmedLine
{
return [[self readLine] stringByTrimmingCharactersInSet:_lineTrimCharacters];
}
- (void)setLineSearchPosition:(NSUInteger)position
{
_lastRange = NSMakeRange(position, 0);
_linesRead = 0;
}
#end
This answer is NOT ObjC but C.
Since ObjC is 'C' based, why not use fgets?
And yes, I'm sure ObjC has it's own method - I'm just not proficient enough yet to know what it is :)
from #Adam Rosenfield's answer, the formatting string of fscanf would be changed like below:
"%4095[^\r\n]%n%*[\n\r]"
it will work in osx, linux, windows line endings.
Using category or extension to make our life a bit easier.
extension String {
func lines() -> [String] {
var lines = [String]()
self.enumerateLines { (line, stop) -> () in
lines.append(line)
}
return lines
}
}
// then
for line in string.lines() {
// do the right thing
}
I found response by #lukaswelte and code from Dave DeLong very helpful. I was looking for a solution to this problem but needed to parse large files by \r\n not just \n.
The code as written contains a bug if parsing by more than one character. I've changed the code as below.
.h file:
#import <Foundation/Foundation.h>
#interface FileChunkReader : NSObject {
NSString * filePath;
NSFileHandle * fileHandle;
unsigned long long currentOffset;
unsigned long long totalFileLength;
NSString * lineDelimiter;
NSUInteger chunkSize;
}
#property (nonatomic, copy) NSString * lineDelimiter;
#property (nonatomic) NSUInteger chunkSize;
- (id) initWithFilePath:(NSString *)aPath;
- (NSString *) readLine;
- (NSString *) readTrimmedLine;
#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL *))block;
#endif
#end
.m file:
#import "FileChunkReader.h"
#interface NSData (DDAdditions)
- (NSRange) rangeOfData_dd:(NSData *)dataToFind;
#end
#implementation NSData (DDAdditions)
- (NSRange) rangeOfData_dd:(NSData *)dataToFind {
const void * bytes = [self bytes];
NSUInteger length = [self length];
const void * searchBytes = [dataToFind bytes];
NSUInteger searchLength = [dataToFind length];
NSUInteger searchIndex = 0;
NSRange foundRange = {NSNotFound, searchLength};
for (NSUInteger index = 0; index < length; index++) {
if (((char *)bytes)[index] == ((char *)searchBytes)[searchIndex]) {
//the current character matches
if (foundRange.location == NSNotFound) {
foundRange.location = index;
}
searchIndex++;
if (searchIndex >= searchLength)
{
return foundRange;
}
} else {
searchIndex = 0;
foundRange.location = NSNotFound;
}
}
if (foundRange.location != NSNotFound
&& length < foundRange.location + foundRange.length )
{
// if the dataToFind is partially found at the end of [self bytes],
// then the loop above would end, and indicate the dataToFind is found
// when it only partially was.
foundRange.location = NSNotFound;
}
return foundRange;
}
#end
#implementation FileChunkReader
#synthesize lineDelimiter, chunkSize;
- (id) initWithFilePath:(NSString *)aPath {
if (self = [super init]) {
fileHandle = [NSFileHandle fileHandleForReadingAtPath:aPath];
if (fileHandle == nil) {
return nil;
}
lineDelimiter = #"\n";
currentOffset = 0ULL; // ???
chunkSize = 128;
[fileHandle seekToEndOfFile];
totalFileLength = [fileHandle offsetInFile];
//we don't need to seek back, since readLine will do that.
}
return self;
}
- (void) dealloc {
[fileHandle closeFile];
currentOffset = 0ULL;
}
- (NSString *) readLine {
if (currentOffset >= totalFileLength)
{
return nil;
}
#autoreleasepool {
NSData * newLineData = [lineDelimiter dataUsingEncoding:NSUTF8StringEncoding];
[fileHandle seekToFileOffset:currentOffset];
unsigned long long originalOffset = currentOffset;
NSMutableData *currentData = [[NSMutableData alloc] init];
NSData *currentLine = [[NSData alloc] init];
BOOL shouldReadMore = YES;
while (shouldReadMore) {
if (currentOffset >= totalFileLength)
{
break;
}
NSData * chunk = [fileHandle readDataOfLength:chunkSize];
[currentData appendData:chunk];
NSRange newLineRange = [currentData rangeOfData_dd:newLineData];
if (newLineRange.location != NSNotFound) {
currentOffset = originalOffset + newLineRange.location + newLineData.length;
currentLine = [currentData subdataWithRange:NSMakeRange(0, newLineRange.location)];
shouldReadMore = NO;
}else{
currentOffset += [chunk length];
}
}
if (currentLine.length == 0 && currentData.length > 0)
{
currentLine = currentData;
}
return [[NSString alloc] initWithData:currentLine encoding:NSUTF8StringEncoding];
}
}
- (NSString *) readTrimmedLine {
return [[self readLine] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL*))block {
NSString * line = nil;
BOOL stop = NO;
while (stop == NO && (line = [self readLine])) {
block(line, &stop);
}
}
#endif
#end
I am adding this because all other answers I tried fell short one way or another. The following method can handle large files, arbitrary long lines, as well as empty lines. It has been tested with actual content and will strip out newline character from the output.
- (NSString*)readLineFromFile:(FILE *)file
{
char buffer[4096];
NSMutableString *result = [NSMutableString stringWithCapacity:1000];
int charsRead;
do {
if(fscanf(file, "%4095[^\r\n]%n%*[\n\r]", buffer, &charsRead) == 1) {
[result appendFormat:#"%s", buffer];
}
else {
break;
}
} while(charsRead == 4095);
return result.length ? result : nil;
}
Credit goes to #Adam Rosenfield and #sooop
I see a lot of these answers rely on reading the whole text file into memory instead of taking it one chunk at a time. Here's my solution in nice modern Swift, using FileHandle to keep memory impact low:
enum MyError {
case invalidTextFormat
}
extension FileHandle {
func readLine(maxLength: Int) throws -> String {
// Read in a string of up to the maximum length
let offset = offsetInFile
let data = readData(ofLength: maxLength)
guard let string = String(data: data, encoding: .utf8) else {
throw MyError.invalidTextFormat
}
// Check for carriage returns; if none, this is the whole string
let substring: String
if let subindex = string.firstIndex(of: "\n") {
substring = String(string[string.startIndex ... subindex])
} else {
substring = string
}
// Wind back to the correct offset so that we don't miss any lines
guard let dataCount = substring.data(using: .utf8, allowLossyConversion: false)?.count else {
throw MyError.invalidTextFormat
}
try seek(toOffset: offset + UInt64(dataCount))
return substring
}
}
Note that this preserves the carriage return at the end of the line, so depending on your needs you may want to adjust the code to remove it.
Usage: simply open a file handle to your target text file and call readLine with a suitable maximum length - 1024 is standard for plain text, but I left it open in case you know it will be shorter. Note that the command will not overflow the end of the file, so you may have to check manually that you've not reached it if you intend to parse the entire thing. Here's some sample code that shows how to open a file at myFileURL and read it line-by-line until the end.
do {
let handle = try FileHandle(forReadingFrom: myFileURL)
try handle.seekToEndOfFile()
let eof = handle.offsetInFile
try handle.seek(toFileOffset: 0)
while handle.offsetInFile < eof {
let line = try handle.readLine(maxLength: 1024)
// Do something with the string here
}
try handle.close()
catch let error {
print("Error reading file: \(error.localizedDescription)"
}
Here's a nice simple solution i use for smaller files:
NSString *path = [[NSBundle mainBundle] pathForResource:#"Terrain1" ofType:#"txt"];
NSString *contents = [NSString stringWithContentsOfFile:path encoding:NSASCIIStringEncoding error:nil];
NSArray *lines = [contents componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:#"\r\n"]];
for (NSString* line in lines) {
if (line.length) {
NSLog(#"line: %#", line);
}
}
Use this script, it works great:
NSString *path = #"/Users/xxx/Desktop/names.txt";
NSError *error;
NSString *stringFromFileAtPath = [NSString stringWithContentsOfFile: path
encoding: NSUTF8StringEncoding
error: &error];
if (stringFromFileAtPath == nil) {
NSLog(#"Error reading file at %#\n%#", path, [error localizedFailureReason]);
}
NSLog(#"Contents:%#", stringFromFileAtPath);