Objective C - Change all attributes in NSAttributedString? - iphone

[attributedString enumerateAttributesInRange:range options:NSAttributedStringEnumerationReverse usingBlock:
^(NSDictionary *attributes, NSRange range, BOOL *stop) {
NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes];
[mutableAttributes setObject:[NSNumber numberWithInt:1] forKey:#"NSUnderline"];
attributes = mutableAttributes;
}];
I am trying to loop through all attributed and add NSUnderline to them. when debugging it seems like NSUnderline is added to the dictionary, but when i loop for the second time they are removed.
Am I doing anything wrong while updating NSDictionaries?

Jonathan's answer does a good job of explaining why it doesn't work. To make it work, you need to tell the attributed string to use these new attributes.
[attributedString enumerateAttributesInRange:range options:NSAttributedStringEnumerationReverse usingBlock:
^(NSDictionary *attributes, NSRange range, BOOL *stop) {
NSMutableDictionary *mutableAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes];
[mutableAttributes setObject:[NSNumber numberWithInt:1] forKey:#"NSUnderline"];
[attributedString setAttributes:mutableAttributes range:range];
}];
Changing the attributes of an attributed string requires that it is a NSMutableAttributedString.
There is also an easier way to do this. NSMutableAttributedString defines the addAttribute:value:range: method, which sets the value of a specific attribute over the specified range, without changing other attributes. You can replace your code with a simple call to this method (still requiring a mutable string).
[attributedString addAttribute:#"NSUnderline" value:[NSNumber numberWithInt:1] range:(NSRange){0,[attributedString length]}];

You're modifying a local copy of the dictionary; the attributed string does not have any way to see the change.
Pointers in C are passed by value (and thus what they point to is passed by reference.) So when you assign a new value to attributes, the code that called the block has no idea you changed it. The change does not propagate outside of the block's scope.

Related

Filter Through Array

How would I filter through an array and return values that contain a certain part of a string? I have a text box where, for the sake of this example, a user puts in 25, and then hits a "Done" button.
Example:
Original Array {25-1002, 25-1005, 12-1003, 1000-0942, 1-1, 234-25}
I want it to return (after sorting through it and pulling the values I want):
New Array {25-1002, 25-1005}
Please note that in the original array, the last value of 234-25 has a 25 in it as well but is not pulled through. It needs to be the number on the first part of the hyphen.
Thanks in advance!
Use the -filteredArrayUsingPredicate: method, like this:
NSString *searchText = [someField.text stringByAppendingString:#"-"];
newArray = [originalArray filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^(NSString *value, NSDictionary *bindings){
return ([value rangeOfString:searchText].location != NSNotFound)
}]];
Note that blocks (the ^{} thing) aren’t available pre-iOS 4, so you’ll have to use another of NSPredicate’s constructors if you’re targeting 3.x devices as well.
as an easy to understand answer (not using NSPredicate, which can be intimidating (but is really the correct way to do it)):
NSMutableArray *myNewArray = [[NSMutableArray alloc] init];
for (NSString *string in myArray) {
if([[string substringToIndex:3] isEqualToString #"25-"])
{
[myNewArray addObject:string];
}
}

iPhone Objective C - How to remove URLs from an NSString

I am looking for an efficient way to replace URLs in an NSString (or NSMutableString) with the replacement word 'link', for example ...
#"This is a sample with **http://bitbanter.com** within the string and heres another **http://spikyorange.co.uk** for luck"
I would like this to become...
#"This is a sample with **'link'** within the string and heres another **'link'** for luck"
Ideally, I would like this to be some sort of method that accepts regular expressions, but, this needs to work on the iPhone, preferably without any libraries, or, I could be persuaded if the library was tiny.
Other features that would be handy, replace #"OMG" with #"Oh my God", but not when it's part of a word, i.e. #"DOOMGAME" shouldn't be touched.
Any suggestions appreciated.
Regards,
Rob.
This was actually quite a bit of fun to play with and hopefully the solution is somehow what you were looking for. This is flexible enough to cater not only for links, but also other patterns where you may want to replace a word for another using certain conditions:
I have commented most of the code so it should be pretty self explanatory. If not, feel free to leave a comment and I will try my best to help:
- (NSString*)replacePattern:(NSString*)pattern withReplacement:(NSString*)replacement forString:(NSString*)string usingCharacterSet:(NSCharacterSet*)characterSetOrNil
{
// Check if a NSCharacterSet has been provided, otherwise use our "default" one
if (!characterSetOrNil)
characterSetOrNil = [NSCharacterSet characterSetWithCharactersInString:#" !?,()]"];
// Create a mutable copy of the string supplied, setup all the default variables we'll need to use
NSMutableString *mutableString = [[[NSMutableString alloc] initWithString:string] autorelease];
NSString *beforePatternString = nil;
NSRange outputrange = NSMakeRange(0, 0);
// Check if the string contains the "pattern" you're looking for, otherwise simply return it.
NSRange containsPattern = [mutableString rangeOfString:pattern];
while (containsPattern.location != NSNotFound)
// Found the pattern, let's run with the changes
{
// Firstly, we grab the full string range
NSRange stringrange = NSMakeRange(0, [mutableString length]);
NSScanner *scanner = [[NSScanner alloc] initWithString:mutableString];
// Now we use NSScanner to scan UP TO the pattern provided
[scanner scanUpToString:pattern intoString:&beforePatternString];
// Check for nil here otherwise you will crash - you will get nil if the pattern is at the very beginning of the string
// outputrange represents the range of the string right BEFORE your pattern
// We need this to know where to start searching for our characterset (i.e. end of output range = beginning of our pattern)
if (beforePatternString != nil)
outputrange = [mutableString rangeOfString:beforePatternString];
// Search for any of the character sets supplied to know where to stop.
// i.e. for a URL you'd be looking at non-URL friendly characters, including spaces (this may need a bit more research for an exhaustive list)
NSRange characterAfterPatternRange = [mutableString rangeOfCharacterFromSet:characterSetOrNil options:NSLiteralSearch range:NSMakeRange(outputrange.length, stringrange.length-outputrange.length)];
// Check if the link is not at the very end of the string, in which case there will be no characters AFTER it so set the NSRage location to the end of the string (== it's length)
if (characterAfterPatternRange.location == NSNotFound)
characterAfterPatternRange.location = [mutableString length];
// Assign the pattern's start position and length, and then replace it with the pattern
NSInteger patternStartPosition = outputrange.length;
NSInteger patternLength = characterAfterPatternRange.location - outputrange.length;
[mutableString replaceCharactersInRange:NSMakeRange(patternStartPosition, patternLength) withString:replacement];
[scanner release];
// Reset containsPattern for new mutablestring and let the loop continue
containsPattern = [mutableString rangeOfString:pattern];
}
return [[mutableString copy] autorelease];
}
And to use your question as an example, here's how you could call it:
NSString *firstString = #"OMG!!!! this is the best convenience method ever, seriously! It even works with URLs like http://www.stackoverflow.com";
NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:#" !?,()]"];
NSString *returnedFirstString = [self replacePattern:#"OMG" withReplacement:#"Oh my God" forString:firstString usingCharacterSet:characterSet];
NSString *returnedSecondString = [self replacePattern:#"http://" withReplacement:#"LINK" forString:returnedFirstString usingCharacterSet:characterSet];
NSLog (#"Original string = %#\nFirst returned string = %#\nSecond returned string = %#", firstString, returnedFirstString, returnedSecondString);
I hope it helps!
Cheers,
Rog
As of iOS 4, NSRegularExpression is available. Amongst other things, you can enumerate all matches within a string via a block, allowing you to do whatever you want to each, or have the regular expression perform some kinds of substitution directly for you.
Direct string substitutions (like 'OMG' -> 'Oh my God') can be performed directly by an NSString, using -stringByReplacingOccurencesOfString:withString:, or replaceOccurrencesOfString:withString:options:range: if your string is mutable.

How can I use an integer value as 'key' to set value in NSMutableDictionary?

How can I use an integer value as 'key' to set a float value in NSMutableDictionary ?
As NSDictionarys are only designed to deal with objects, a simple way to do this is to wrap the integer and float in a NSNumber object. For example:
NSMutableDictionary *testDictionary = [[NSMutableDictionary alloc] init];
[testDictionary setObject:[NSNumber numberWithFloat:1.23f]
forKey:[NSNumber numberWithInt:1]];
NSLog(#"Test dictionary: %#", testDictionary);
[testDictionary release];
To extract the relevant value, simply use the appropriate intValue, floatValue, etc. method from the NSNumber class.
You can use NSMapTable as it supports integer keys and/or values directly. No need to box/unbox through NSNumber, but it is also slightly more difficult to set up and use.
It needs to be an object, so use [NSNumber numberWithInt:myInteger] instead.
Then, retrieve it with -integerValue

Why should I use KVC rather than the simple dot syntax when accessing object properties?

There's the option to go the long way, if an receiver class conforms to the NSKeyValueProtocol:
[myInstance setValue:[NSNumber numberWithInt:2] forKey:#"integerProperty"];
or the short way:
myInstance.integerProperty = 2;
what's the point of this KVC method? When is this useful?
First, those aren't the same, the second should be:
myInstance.integerProperty = [NSNumber numbwerWithInt:2];
if integerProperty is an NSNumber.
In general you use the second form when you are doing the most things. You use setValue:forKey: and valueForKey: when you want to dynamically choose the property to store things in. For instance, think about how valueForKeyPath: against an NSArray works (for reference, if you call -valueForKey: against an NSArray it will return an array where each object is the result of asking the corresponding object in that NSArray for that value:
- (NSArray *) valueForKey:(id)key {
NSMutableArray *retval = [NSMutableArray array];
for (NSObject *object in self) {
[retval addObject:[object valueForKey:key]];
}
return retval;
}
In the above case we were able to use valueForKey: to implement our function even though we do not know what the key is beforehand, since it is passed in as an argument.

Why am I having trouble with a deep copy in Objective C?

I'm assuming my understanding of how to perform a deep copy isn't just there yet. The same with some sub-optimal memory handling that I'm performing down below. This code below probably depicts a shallow copy, and I believe that's where my problem might be. I have some cookie-cutter code for an example that looks like the following:
NSArray *user = [[xmlParser createArrayWithDictionaries:dataAsXML
withXPath:kUserXPath] retain];
if([user count] > 0) {
self.name = [[user valueForKey:#"name"] copy];
}
// Crash happens if I leave the next line un-commented.
// But then we have a memory leak.
[user release];
[xmlParser release];
Unfortunately when I comment out [user release], the code works, but we have an obvious memory leak. The method createArrayWithDictionaries:withXPath: was refactored last night when the SO community helped me understand better memory management. Here's what it looks like:
- (NSArray *)createArrayWithDictionaries:(NSString *)xmlDocument
withXPath:(NSString *)XPathStr {
NSError *theError = nil;
NSMutableArray *dictionaries = [NSMutableArray array];
CXMLDocument *theXMLDocument = [CXMLDocument alloc];
theXMLDocument = [theXMLDocument initWithXMLString:xmlDocument
options:0
error:&theError];
NSArray *nodes = [theXMLDocument nodesForXPath:XPathStr error:&theError];
for (CXMLElement *xmlElement in nodes) {
NSArray *attributes = [xmlElement attributes];
NSMutableDictionary *attributeDictionary;
attributeDictionary = [NSMutableDictionary dictionary];
for (CXMLNode *attribute in attributes) {
[attributeDictionary setObject:[attribute stringValue]
forKey:[attribute name]];
}
[dictionaries addObject:attributeDictionary];
}
[theXMLDocument release];
return dictionaries;
}
I'm guessing there's a couple of issues that might be going on here:
Auto release on my dictionaries array is happening, thus my app crashing.
I'm not performing a deep copy, only a shallow copy. Thus when the user array is released, self.name is done for.
With NSZombieEnabled, I see the following:
*** -[CFString respondsToSelector:]:
message sent to deallocated instance 0x1ae9a0
Also, the final call where the backtrace shows this is crashing contains the following code in a separate module from the other two methods:
User *u = self.user;
NSString *uri = [NSString stringWithFormat:#"%#/user/%#/%#",
[self groupName], u.userId, kLocationsUri];
Between all the auto releasing/copies/retain happening between the client code and createArrayWithDictionaries:withXPath, I'm a bit confused as to the real problem here. Thanks again for helping me understand.
OK, you don't need to retain the return value from createArrayWithDictionaries: since you're not keeping it around. The return value is autoreleased. I'd strongly recommend reading up on how autoreleasing works. You only retain things that you intend to keep around in your object.
Also, user is an NSArray. If you call [user valueForKey:#"name"], you'll get another NSArray of values representing the values of the name key for each of the objects in users. Furthermore, how is the name property on your object defined? If you declared it as copy or retain (I believe retain is the default if you don't specify it yourself), you don't need to copy or retain the value. Indeed, the accessor should always be responsible for doing the memory management, not the caller. If you wrote your own accessor (i.e. you didn't use the #synthesize keyword), you need to make sure you do the memory management there.
I'm guessing what you meant to write was something more like this:
NSArray *user = [xmlParser createArrayWithDictionaries:dataAsXML withXPath:kUserXPath];
if ([user count] > 0)
self.name = [[user objectAtIndex:0] objectForKey:#"name"];
[xmlParser release];
I think your troubles are stemming from a misunderstanding of how memory management works in Objective-C.
Hope this helps.
Auto release on my dictionaries array is happening, thus my app crashing.
If the caller intends to keep the array around somewhere, it needs to retain it. Otherwise, it will crash when it tries to access the (now-deceased) object.
If the caller is going to store it in a property, it must use the self.dictionaries = […] syntax, not dictionaries = […]. The former is a property access, which calls the setter method; the latter is a direct instance variable assignment.
Coming back to your actual question, that of a deep copy: You need to get the sub-elements of every element and put them in each element's dictionary.
Basically, you need a recursive method (or a queue, but that's harder—file under premature optimization until you've proven you need it) that takes an element and returns a dictionary, and then you need to call this method on each of your element's child elements, and collect the results into an array and put that into the dictionary you're creating.
I would recommend making this recursive method an instance method of the element. Something like:
- (NSDictionary *) dictionaryRepresentation {
NSMutableDictionary *attributeDictionary = [NSMutableDictionary dictionary];
for (CXMLNode *attribute in attributes) {
[attributeDictionary setObject:[attribute stringValue] forKey:[attribute name]];
}
NSArray *childElements = [self childElements];
return [NSDictionary dictionaryWithObjectsAndKeys:
attributeDictionary, #"attributes",
[childElements valueForKey:#"dictionaryRepresentation"], #"childElements",
nil];
}
Then you replace the loop in createArrayWithDictionaries:withXPath: with a similar valueForKey: message. I'll leave you to fill it in.
valueForKey: is Key-Value Coding's principal method. In both places, we're making use of NSArray's handy implementation of it.
(If the use of valueForKey: still doesn't make sense to you, you should read the KVC Programming Guide. KVC is vitally important in modern Cocoa, so you do need to read this sooner or later.)