You’re reading Ry’s Objective-C Tutorial → Data Types |
NSDictionary
Like an NSSet
, the
NSDictionary
class represents an unordered collection of objects;
however, they associate each value with a key, which acts like a label for the
value. This is useful for modeling relationships between pairs of objects.
NSDictionary
is immutable, but the NSMutableDictionary
data structure
lets you dynamically add and remove entries as necessary.
Creating Dictionaries
Immutable dictionaries can be defined using the literal @{}
syntax. But, like array literals, this was added relatively recently, so you
should also be aware of the dictionaryWithObjectsAndKeys:
and
dictionaryWithObjects:forKeys:
factory methods. All of these are
presented below.
// Literal syntax
NSDictionary
*
inventory
=
@{
@"Mercedes-Benz SLK250"
:
[
NSNumber
numberWithInt:
13
],
@"Mercedes-Benz E350"
:
[
NSNumber
numberWithInt:
22
],
@"BMW M3 Coupe"
:
[
NSNumber
numberWithInt:
19
],
@"BMW X6"
:
[
NSNumber
numberWithInt:
16
],
};
// Values and keys as arguments
inventory
=
[
NSDictionary
dictionaryWithObjectsAndKeys:
[
NSNumber
numberWithInt:
13
],
@"Mercedes-Benz SLK250"
,
[
NSNumber
numberWithInt:
22
],
@"Mercedes-Benz E350"
,
[
NSNumber
numberWithInt:
19
],
@"BMW M3 Coupe"
,
[
NSNumber
numberWithInt:
16
],
@"BMW X6"
,
nil
];
// Values and keys as arrays
NSArray
*
models
=
@[
@"Mercedes-Benz SLK250"
,
@"Mercedes-Benz E350"
,
@"BMW M3 Coupe"
,
@"BMW X6"
];
NSArray
*
stock
=
@[[
NSNumber
numberWithInt:
13
],
[
NSNumber
numberWithInt:
22
],
[
NSNumber
numberWithInt:
19
],
[
NSNumber
numberWithInt:
16
]];
inventory
=
[
NSDictionary
dictionaryWithObjects:
stock
forKeys:
models
];
NSLog
(
@"%@"
,
inventory
)
;
The dictionaryWithObjectsAndKeys:
method treats its argument
list as value-key pairs, so every two parameters define a single entry. This
ordering is somewhat counterintuitive, so make sure that the value always comes
before its associated key. The
dictionaryWithObjects:ForKeys:
method is a little bit more
straightforward, but you should be careful to ensure that the key array is the
same length as the value array.
For common programming tasks, you’ll probably never need to use
anything but an NSString
as a key, but it’s worth noting
that any object that adopts the NSCopying
protocol and implements
the hash
and the isEqual:
methods can be used as a
key. This is necessary because keys are copied when an entry is added to the
array. However, this is not true for values, which are strongly referenced
(just like set and array elements).
Accessing Values and Keys
You can use the same subscripting syntax as arrays
(someDict[key]
) to access the value for a particular key. The
objectForKey:
method is the other common way to access values.
NSDictionary
*
inventory
=
@{
@"Mercedes-Benz SLK250"
:
[
NSNumber
numberWithInt:
13
],
@"Mercedes-Benz E350"
:
[
NSNumber
numberWithInt:
22
],
@"BMW M3 Coupe"
:
[
NSNumber
numberWithInt:
19
],
@"BMW X6"
:
[
NSNumber
numberWithInt:
16
],
};
NSLog
(
@"There are %@ X6's in stock"
,
inventory
[
@"BMW X6"
])
;
NSLog
(
@"There are %@ E350's in stock"
,
[
inventory
objectForKey:
@"Mercedes-Benz E350"
]);
Typically, you’ll want to access values from keys, as shown above;
however, it’s possible to do a reverse lookup to get a value’s
key(s) with the allKeysForObject:
method. Note that this returns
an array because multiple keys can map to the same value (but not vice
versa). For example, the following extracts all of the keys with a value of
0
.
NSDictionary
*
inventory
=
@{
@"Mercedes-Benz SLK250"
:
[
NSNumber
numberWithInt:
0
],
@"Mercedes-Benz E350"
:
[
NSNumber
numberWithInt:
0
],
@"BMW M3 Coupe"
:
[
NSNumber
numberWithInt:
19
],
@"BMW X6"
:
[
NSNumber
numberWithInt:
16
],
};
NSArray
*
outOfStock
=
[
inventory
allKeysForObject:
[
NSNumber
numberWithInt:
0
]];
NSLog
(
@"The following cars are currently out of stock: %@"
,
[
outOfStock
componentsJoinedByString:
@", "
]);
Enumerating Dictionaries
As with sets and arrays, fast-enumeration is the most efficient way to
enumerate a dictionary, and it loops through the keys (not the
values). NSDictionary
also defines a count
method,
which returns the number of entries in the collection.
NSDictionary
*
inventory
=
@{
@"Mercedes-Benz SLK250"
:
[
NSNumber
numberWithInt:
13
],
@"Mercedes-Benz E350"
:
[
NSNumber
numberWithInt:
22
],
@"BMW M3 Coupe"
:
[
NSNumber
numberWithInt:
19
],
@"BMW X6"
:
[
NSNumber
numberWithInt:
16
],
};
NSLog
(
@"We currently have %ld models available"
,
[
inventory
count
])
;
for
(
id
key
in
inventory
)
{
NSLog
(
@"There are %@ %@'s in stock"
,
inventory
[
key
],
key
);
}
You can isolate a dictionary’s keys/values with the
allKeys
/allValues
methods, which return an
NSArray
of each key/value in the collection, respectively. Note
that there is no guarantee that these methods will return keys and values in
the same order.
NSLog
(
@"Models: %@"
,
[
inventory
allKeys
])
;
NSLog
(
@"Stock: %@"
,
[
inventory
allValues
])
;
For block-oriented developers,
enumerateKeysAndObjectsUsingBlock:
offers yet another way to
enumerate entries. The block is called for each entry, and both the key and the
value are passed as arguments:
[
inventory
enumerateKeysAndObjectsUsingBlock:
^
(
id
key
,
id
obj
,
BOOL
*
stop
)
{
NSLog
(
@"There are %@ %@'s in stock"
,
obj
,
key
);
}];
Comparing Dictionaries
Comparing dictionaries works the same as comparing arrays. The
isEqualToDictionary:
method returns YES
when both
dictionaries contain the same key-value pairs:
NSDictionary
*
warehouseInventory
=
@{
@"Mercedes-Benz SLK250"
:
[
NSNumber
numberWithInt:
13
],
@"Mercedes-Benz E350"
:
[
NSNumber
numberWithInt:
22
],
@"BMW M3 Coupe"
:
[
NSNumber
numberWithInt:
19
],
@"BMW X6"
:
[
NSNumber
numberWithInt:
16
],
};
NSDictionary
*
showroomInventory
=
@{
@"Mercedes-Benz SLK250"
:
[
NSNumber
numberWithInt:
13
],
@"Mercedes-Benz E350"
:
[
NSNumber
numberWithInt:
22
],
@"BMW M3 Coupe"
:
[
NSNumber
numberWithInt:
19
],
@"BMW X6"
:
[
NSNumber
numberWithInt:
16
],
};
if
([
warehouseInventory
isEqualToDictionary:
showroomInventory
])
{
NSLog
(
@"Why are we storing so many cars in our showroom?"
);
}
Sorting Dictionary Keys
Dictionaries can’t be directly sorted into a new
NSDictionary
instance, but it is possible to sort the
keys of the dictionary with
keysSortedByValueUsingComparator:
, which accepts a block that
should return one of the NSComparisonResult
enumerators described
in the NSArray module. The following
example sorts the models from most expensive to most affordable.
NSDictionary
*
prices
=
@{
@"Mercedes-Benz SLK250"
:
[
NSDecimalNumber
decimalNumberWithString:
@"42900.00"
],
@"Mercedes-Benz E350"
:
[
NSDecimalNumber
decimalNumberWithString:
@"51000.00"
],
@"BMW M3 Coupe"
:
[
NSDecimalNumber
decimalNumberWithString:
@"62000.00"
],
@"BMW X6"
:
[
NSDecimalNumber
decimalNumberWithString:
@"55015.00"
]
};
NSArray
*
sortedKeys
=
[
prices
keysSortedByValueUsingComparator:
^
NSComparisonResult
(
id
obj1
,
id
obj2
)
{
return
[
obj2
compare:
obj1
];
}];
NSLog
(
@"%@"
,
sortedKeys
)
;
It’s important to note that the dictionary’s values are
passed to the block—not the keys themselves. Alternatively, you can
always manually sort the results of allKeys
and
allValues
.
Filtering Dictionary Keys
Similarly, you can’t directly filter a dictionary, but the aptly named
keysOfEntriesPassingTest:
method lets you extract the keys passing
a particular test. The test is defined as a block that returns YES
if the current entry’s key should be included and NO
if it
shouldn’t. The resulting keys are returned as an NSSet
. For
example, the following snippet finds all of the cars that are under
$50,000.
NSDictionary
*
prices
=
@{
@"Mercedes-Benz SLK250"
:
[
NSDecimalNumber
decimalNumberWithString:
@"42900.00"
],
@"Mercedes-Benz E350"
:
[
NSDecimalNumber
decimalNumberWithString:
@"51000.00"
],
@"BMW M3 Coupe"
:
[
NSDecimalNumber
decimalNumberWithString:
@"62000.00"
],
@"BMW X6"
:
[
NSDecimalNumber
decimalNumberWithString:
@"55015.00"
]
};
NSDecimalNumber
*
maximumPrice
=
[
NSDecimalNumber
decimalNumberWithString:
@"50000.00"
];
NSSet
*
under50k
=
[
prices
keysOfEntriesPassingTest:
^
BOOL
(
id
key
,
id
obj
,
BOOL
*
stop
)
{
if
([
obj
compare:
maximumPrice
]
==
NSOrderedAscending
)
{
return
YES
;
}
else
{
return
NO
;
}
}];
NSLog
(
@"%@"
,
under50k
)
;
NSMutableDictionary
The NSMutableDictionary
class lets you add new key-value pairs
dynamically. Mutable dictionaries provide similar performance to mutable sets
when it comes to inserting and deleting entries, and remember that both of
these are a better choice than mutable arrays if you need to constantly alter
the collection.
Mutable collections in general lend themselves to representing system states, and mutable dictionaries are no different. A common use case is to map one set of objects to another set of objects. For example, an auto shop application might need to assign broken cars to specific mechanics. One way of modeling this is to treat cars as keys and mechanics as values. This arrangement allows a single mechanic to be responsible for multiple cars, but not vice versa.
Creating Mutable Dictionaries
Mutable dictionaries can be created by calling any of the factory methods
defined by NSDictionary
on the NSMutableDictionary
class. But, since many of these methods aren’t always the most intuitive
to work with, you might find it useful to convert a literal dictionary to a
mutable one using dictionaryWithDictionary
:
NSMutableDictionary
*
jobs
=
[
NSMutableDictionary
dictionaryWithDictionary:
@{
@"Audi TT"
:
@"John"
,
@"Audi Quattro (Black)"
:
@"Mary"
,
@"Audi Quattro (Silver)"
:
@"Bill"
,
@"Audi A7"
:
@"Bill"
}];
NSLog
(
@"%@"
,
jobs
)
;
Adding and Removing Entries
The setObject:forKey:
and removeObjectForKey:
methods are the significant additions contributed by
NSMutableDictionary
. The former can be used to either replace
existing keys or add new ones to the collection. As an alternative, you can
assign values to keys using the dictionary subscripting syntax, also shown
below.
NSMutableDictionary
*
jobs
=
[
NSMutableDictionary
dictionaryWithDictionary:
@{
@"Audi TT"
:
@"John"
,
@"Audi Quattro (Black)"
:
@"Mary"
,
@"Audi Quattro (Silver)"
:
@"Bill"
,
@"Audi A7"
:
@"Bill"
}];
// Transfer an existing job to Mary
[
jobs
setObject:
@"Mary"
forKey:
@"Audi TT"
];
// Finish a job
[
jobs
removeObjectForKey:
@"Audi A7"
];
// Add a new job
jobs
[
@"Audi R8 GT"
]
=
@"Jack"
;
Combining Dictionaries
Mutable dictionaries can be expanded by adding the contents of another
dictionary to its collection via the addEntriesFromDictionary:
method. This can be used, for example, to combine jobs from two auto shop
locations:
NSMutableDictionary
*
jobs
=
[
NSMutableDictionary
dictionaryWithDictionary:
@{
@"Audi TT"
:
@"John"
,
@"Audi Quattro (Black)"
:
@"Mary"
,
@"Audi Quattro (Silver)"
:
@"Bill"
,
@"Audi A7"
:
@"Bill"
}];
NSDictionary
*
bakerStreetJobs
=
@{
@"BMW 640i"
:
@"Dick"
,
@"BMW X5"
:
@"Brad"
};
[
jobs
addEntriesFromDictionary:
bakerStreetJobs
];
This method also presents another option for creating mutable dictionaries:
// Create an empty mutable dictionary
NSMutableDictionary
*
jobs
=
[
NSMutableDictionary
dictionary
];
// Populate it with initial key-value pairs
[
jobs
addEntriesFromDictionary:
@{
@"Audi TT"
:
@"John"
,
@"Audi Quattro (Black)"
:
@"Mary"
,
@"Audi Quattro (Silver)"
:
@"Bill"
,
@"Audi A7"
:
@"Bill"
}];
Enumeration Considerations
Dictionaries should not be mutated while you’re iterating over them. Please see the Enumeration Considerations section of the NSSet module for details.
Depending on how you need to alter the collection, you might be able to use
the allKeys
or allValues
methods to create snapshots
of just the keys or values. If those aren’t sufficient you should use the
dictionaryWithDictionary:
class method to create a shallow copy of
the entire dictionary.
Be sure to check out Ry’s Cocoa Tutorial. This brand new guide is a complete walkthrough of Mac App development, and it leverages all of the Objective-C skills that we just discussed. Learn more › |
Mailing List
Sign up for my low-volume mailing list to find out when new content is released. Next up is a comprehensive Swift tutorial planned for late January.
You’ll only receive emails when new tutorials are released, and your contact information will never be shared with third parties. Click here to unsubscribe.