NSPredicate and NSExpression
Learn to build and use NSPredicate to filter and query collections and Core Data fetch requests -- predicate format strings, comparison operators, compound predicates, filtering NSArray/NSMutableArray, NSExpression for computed and aggregate values, and how to avoid predicate injection by using format-string argument substitution instead of naive string concatenation.
NSPredicate is Foundation's way of expressing a logical condition -- 'age is at least 18,' 'name begins with A,' 'all of these are true at once' -- as a reusable object rather than as scattered if-statements. The same predicate object filters an in-memory NSArray, drives a Core Data NSFetchRequest, or (less commonly today) filters a Spotlight query, because it's a description of a condition, not code tied to any one collection type. This lesson builds up from the simplest possible predicate through compound logic, Core Data usage (cross-referencing the sibling Core Data Fundamentals and Core Data Advanced lessons directly), NSExpression for computed and aggregate values, and closes with a security-minded section on predicate injection -- the SQL-injection-shaped risk of building a predicate's format string via naive concatenation with untrusted input, and the safe alternative every example in this lesson otherwise already uses.
The Simplest Predicate: predicateWithFormat:
The canonical way to build an NSPredicate is +predicateWithFormat:, which takes a small, SQL-like format-string language and parses it into a predicate object at runtime. The simplest possible example states a single comparison directly:
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age >= 18"];
This predicate doesn't do anything by itself yet -- it's an inert object describing a condition ('the value of a key named age is greater than or equal to 18') until it's evaluated against something, either by asking it directly with -evaluateWithObject:, or by handing it to a collection's filtering method (covered next) or a Core Data fetch request (covered later in this lesson). The key name in the format string (age here) refers to a property or key-value-coding-compliant accessor on whatever object the predicate is eventually evaluated against -- there's no schema or type declared anywhere in the predicate itself; it's resolved dynamically, the same way ordinary KVC key paths are, when the predicate actually runs.
• predicateWithFormat: parses a small SQL-like format-string language into an NSPredicate object, at runtime. • [NSPredicate predicateWithFormat:@"age >= 18"] describes the condition 'age is at least 18' -- it does nothing until evaluated. • -evaluateWithObject: checks a single object against a predicate directly, returning a BOOL. • Key names in the format string (like age) are resolved via key-value coding against whatever object the predicate is evaluated on -- there's no separate schema declaration.
Comparison Operators
Beyond the numeric comparisons (==, !=, <, <=, >, >= -- the >= 18 from the opening example being one of these), predicate format strings support several operators specifically useful for strings and collections. CONTAINS tests whether a string contains a given substring (or, on a collection-valued key, whether the collection contains a given element); BEGINSWITH and ENDSWITH test string prefixes and suffixes respectively; and IN tests membership against a list, written as a parenthesized, comma-separated set.
String-comparison operators accept an optional [c] modifier (and [d] for diacritic-insensitive comparison, or both combined as [cd]) directly after the operator, making the comparison case-insensitive -- name BEGINSWITH[c] 'a' matches 'Alice' as well as 'alice', where the unmodified version would only match the latter.
IN is worth calling out specifically because it reads naturally in both directions depending on which side is the collection: status IN {'active', 'pending'} checks whether a scalar value is a member of a fixed set, while, on the Core Data side covered later in this lesson, it's equally natural to check the reverse -- whether a to-many relationship contains a particular value.
• ==, !=, <, <=, >, >= -- ordinary comparison operators, usable on numbers, dates, and other comparable types. • CONTAINS, BEGINSWITH, ENDSWITH -- string (and, for CONTAINS, collection) matching operators. • IN tests membership against a parenthesized/braced list: status IN {'active', 'pending'}. • String operators accept a [c] (case-insensitive) and/or [d] (diacritic-insensitive) modifier directly after the operator, e.g. BEGINSWITH[c].
Compound Predicates: AND, OR, NOT
Format strings combine multiple conditions directly using the keywords AND, OR, and NOT (their symbolic equivalents && , ||, and ! are also accepted), exactly the way a boolean expression in ordinary Objective-C code would read: @"age >= 18 AND country == 'US'" is a single predicate requiring both conditions to hold. Parentheses group sub-conditions exactly as they would in code, and are frequently necessary once AND and OR are mixed in the same expression, since operator precedence between them can otherwise produce a different grouping than intended.
Beyond format strings, NSCompoundPredicate offers a programmatic way to build the same logic out of an array of existing NSPredicate objects -- +andPredicateWithSubpredicates:, +orPredicateWithSubpredicates:, and +notPredicateWithSubpredicate: -- which is the natural choice when the set of conditions to combine is itself built up dynamically at runtime (for example, one optional filter predicate per active UI control), rather than known and fixed as a single literal string ahead of time.
Both approaches produce the exact same kind of NSPredicate object underneath and can be mixed freely -- a predicate built from a format string can be passed as one of the subpredicates to NSCompoundPredicate's array-based constructors, and vice versa; the choice is purely about which is more convenient for a given piece of code to construct.
• AND, OR, NOT (or &&, ||, !) combine conditions directly inside a format string, with parentheses for explicit grouping. • NSCompoundPredicate builds the same logic programmatically from an array of NSPredicate objects: andPredicateWithSubpredicates:, orPredicateWithSubpredicates:, notPredicateWithSubpredicate:. • NSCompoundPredicate is the natural choice when the set of conditions to combine is itself assembled dynamically at runtime, rather than known as one fixed literal string. • Format-string and NSCompoundPredicate-built predicates are the same underlying object type and can be freely mixed.
Filtering NSArray and NSMutableArray
NSArray's -filteredArrayUsingPredicate: is the most common consumer of a predicate outside of Core Data: it evaluates the predicate against every element in the array (via -evaluateWithObject: under the hood, one call per element) and returns a new NSArray containing only the elements that satisfied it, leaving the original array unmodified. NSMutableArray offers the in-place counterpart, -filterUsingPredicate:, which removes every element that does NOT satisfy the predicate directly from the array it's called on, rather than returning a new one.
Both methods rely entirely on key-value coding to resolve the keys used in the predicate against each element, which means filtering works uniformly whether the array holds plain NSDictionary entries (using dictionary-key lookup) or custom model objects with declared @property accessors (using ordinary property access) -- the predicate doesn't need to know or care which kind of object it's being evaluated against, as long as the keys it references resolve via KVC.
Because -filteredArrayUsingPredicate: does a full linear pass over the array, evaluating the predicate once per element, filtering a very large array repeatedly with the same predicate is a reasonable candidate for caching the result (or switching to Core Data's fetch-request-level filtering, covered next, which can push the work down to the persistent store instead of evaluating every object in memory).
• -filteredArrayUsingPredicate: (NSArray) returns a NEW array of only the elements satisfying the predicate; the original array is unchanged. • -filterUsingPredicate: (NSMutableArray) removes non-matching elements IN PLACE from the array it's called on. • Both use key-value coding to resolve predicate keys, working uniformly against NSDictionary entries or custom @property-based model objects. • Filtering is a full linear pass, evaluating the predicate once per element -- for very large collections, Core Data's fetch-level filtering can be more efficient.
Predicates and Core Data Fetch Requests
The sibling Core Data Fundamentals lesson covers NSFetchRequest as the object describing what entity to fetch, and this is exactly where NSPredicate becomes central to Core Data rather than merely useful alongside it: setting a fetch request's -predicate property is the standard way to filter which managed objects a fetch actually returns, using the identical format-string language and operators already covered in this lesson -- [NSPredicate predicateWithFormat:@"age >= 18 AND country == %@", countryCode] works against a Core Data entity's attributes exactly the way it works against an in-memory model object's properties.
The crucial performance difference from -filteredArrayUsingPredicate: is where the filtering actually happens: a fetch request's predicate is translated by Core Data into the underlying store's own native query language (SQL, for the default SQLite store covered in Core Data Fundamentals), so the filtering runs at the store level, only bringing matching managed objects into memory, rather than fetching everything and filtering it afterward in Objective-C code. This is a large part of why predicates matter so directly for Core Data performance, a theme the sibling Core Data Advanced lesson develops further alongside faulting and batching.
Predicates on relationships follow naturally from the same key-path syntax KVC already supports: department.name == 'Engineering' walks a to-one relationship, while employees.@count > 5 (using the collection operator @count, discussed further in the NSExpression section below) reasons about a to-many relationship's size -- both are ordinary predicate expressions, evaluated identically whether run against in-memory objects or pushed down into a Core Data fetch.
• Setting NSFetchRequest's -predicate property filters which managed objects a fetch returns, using the same format-string language as in-memory filtering. • A fetch request's predicate is translated into the store's native query language (SQL, for the default SQLite store) -- filtering happens at the store level, not in memory afterward, which is a real performance difference from -filteredArrayUsingPredicate:. • This store-level filtering is a key theme the sibling Core Data Advanced lesson builds on alongside faulting and batch operations. • Relationship key paths work naturally in predicates: department.name == 'Engineering' (to-one) or employees.@count > 5 (to-many, via a collection operator).
NSExpression: Computed and Aggregate Values
NSExpression represents a value to be computed, rather than a condition to be tested -- it's the building block predicates are made of internally (a comparison predicate like age >= 18 is, underneath, two NSExpressions -- one for the key path age, one for the constant 18 -- combined by a comparison operator), but it's also directly useful on its own for expressing aggregate calculations over a collection, most commonly via Core Data's NSExpressionDescription for fetch requests that need to compute a value rather than fetch whole objects.
Collection operators, written with an @ prefix inside a key path, are where NSExpression's aggregate side becomes visible in ordinary code: @sum.salary computes the sum of the salary key across a collection, @avg.salary computes the average, @count counts elements (as used in the employees.@count > 5 predicate above), and @max/@min find the largest and smallest values. These can appear directly inside predicate format strings for comparisons (employees.@count > 5), or be built as standalone NSExpression objects and evaluated with -expressionValueWithObject:context: to get the computed value back directly, rather than a true/false predicate result.
In a Core Data context specifically, NSExpressionDescription lets a fetch request return computed aggregate values (a department's total salary, an average rating) directly from the store, again pushing the computation down to the SQL level the sibling Core Data lessons describe, rather than fetching every matching managed object into memory and summing them in Objective-C afterward -- the same store-level-versus-in-memory performance distinction the predicate section above makes for filtering applies equally to aggregation.
• NSExpression represents a computed value; a comparison predicate is built from two NSExpressions and a comparison operator underneath. • @-prefixed collection operators inside a key path compute aggregates: @sum, @avg, @count, @max, @min -- usable inside predicate comparisons or as a standalone expression. • -expressionValueWithObject:context: evaluates a standalone NSExpression directly, returning the computed value rather than a predicate's true/false result. • Core Data's NSExpressionDescription lets a fetch request return computed aggregates directly from the store -- the same store-level-vs-in-memory performance distinction applies here as it does to predicate-based filtering.
Predicate Injection: Why Naive String Concatenation With User Input Is Dangerous
Every example so far in this lesson has built predicate format strings safely, but it's worth stating the danger directly, because the unsafe version looks completely reasonable at first glance: [NSString stringWithFormat:@"name == '%@'", userInput] hand-builds a predicate format string by directly splicing untrusted input into the middle of it. This is structurally identical to the classic SQL-injection mistake of concatenating untrusted input directly into a SQL string -- if userInput contains a single quote, or predicate-format-language syntax of its own, the resulting string is no longer the simple equality check it was meant to be; it can become a different, attacker-controlled predicate expression entirely.
Concretely, if userInput is the string "x' OR 'a'=='a", naive concatenation produces the format string name == 'x' OR 'a'=='a' -- an entirely different predicate than intended, one that's now true for every single element regardless of its actual name, because 'a'=='a' is always true. Depending on what the predicate is protecting (visibility of records, an access check, which rows a query returns), an attacker deliberately crafting input in this shape can bypass the filtering the predicate was supposed to enforce, or, more mundanely, simply crash the app by supplying input that produces a syntactically invalid predicate format string that +predicateWithFormat: cannot parse at all.
The safe alternative is the same mechanism +predicateWithFormat: already supports and every earlier example in this lesson already used without calling it out explicitly: pass untrusted values as separate arguments using %@ (or the appropriate format specifier for the value's type) directly in the format string, rather than splicing them into the string yourself first. [NSPredicate predicateWithFormat:@"name == %@", userInput] treats userInput strictly as a VALUE to compare against, never as text that gets parsed as part of the predicate's own syntax -- exactly the same distinction as using a parameterized SQL query instead of concatenating a SQL string, and it completely closes off the injection risk because the untrusted content is never interpreted as predicate-language syntax in the first place.
• [NSString stringWithFormat:@"name == '%@'", userInput] splices untrusted input directly into predicate syntax -- structurally the same mistake as SQL injection. • Crafted input (e.g. containing a stray quote or its own predicate syntax) can turn a simple equality check into an entirely different, attacker-controlled predicate, or crash the app with an unparseable format string. • The safe fix: [NSPredicate predicateWithFormat:@"name == %@", userInput] -- passing the value as a substitution ARGUMENT, never as concatenated text. • Argument substitution treats untrusted content strictly as a value to compare, never as predicate-language syntax -- the same principle as a parameterized SQL query versus a concatenated one.
Building and Evaluating a Predicate
predicateWithFormat: parses a SQL-like condition into a reusable NSPredicate object -- it evaluates nothing by itself. The key (age) is resolved via key-value coding against whatever object the predicate is later evaluated on. Always prefer %@ argument substitution over string concatenation when any part of the format string comes from untrusted input.
NSPredicate *predicate =
[NSPredicate predicateWithFormat:@"age >= 18"];Code Examples
The predicate is built once and can be reused against any KVC-compliant object -- here, an NSDictionary, where 'age' resolves via dictionary key lookup. -evaluateWithObject: is the most direct way to test a single object without going through a collection's filtering method at all.
BOOL isAdult = [predicate evaluateWithObject:person];
filteredArrayUsingPredicate: returns a new array without touching the original; the second predicate shows argument substitution (%@) used with a literal 'a', plus the [c] modifier making BEGINSWITH case-insensitive so it matches both 'Alice' and 'Amanda' despite the lowercase 'a' comparison value.
NSArray *adultsOnly = [people filteredArrayUsingPredicate:adults];
The two forms produce equivalent underlying predicate objects; the NSCompoundPredicate version is preferable exactly when, as here, whether the country condition applies at all depends on runtime state (requireUSOnly), so the set of subpredicates being ANDed together isn't known as one fixed literal string ahead of time.
NSPredicate *dynamicPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates];
Setting -predicate on the fetch request, described fully in the sibling Core Data Fundamentals lesson, pushes this exact same filtering logic down into the store's own SQL query rather than fetching every Employee and filtering afterward in Objective-C -- the identical predicate syntax from earlier examples, now doing store-level work instead of in-memory work.
request.predicate = [NSPredicate predicateWithFormat:@"age >= %@ AND country == %@", @18, @"US"];
@sum and @avg are collection operators evaluated via ordinary key-value coding (-valueForKeyPath:), the same underlying mechanism NSExpression uses internally -- they compute an aggregate over the salary key across every dictionary in the array. The final predicate shows the identical @count operator used earlier for Core Data relationships, now filtering on a to-many relationship's size directly inside a predicate comparison.
NSNumber *totalSalary = [employees valueForKeyPath:@"@sum.salary"];
The unsafe version builds the format string BEFORE handing it to predicateWithFormat:, so the untrusted content is already part of the syntax being parsed by the time predicateWithFormat: sees it. The safe version hands predicateWithFormat: the untrusted value as a %@ argument instead, so it's substituted in as a plain value after parsing, never interpreted as predicate-language syntax -- structurally identical to the difference between a parameterized SQL query and a concatenated one.
NSPredicate *safePredicate = [NSPredicate predicateWithFormat:@"name == %@", userInput];
Common mistake: Building a predicate's format string via [NSString stringWithFormat:...] with untrusted input spliced directly in, instead of passing that input as a %@ substitution argument to predicateWithFormat: itself.
NSPredicate *predicate =
[NSPredicate predicateWithFormat:@"age >= 18"];
NSDictionary *person = @{ @"name": @"Alice", @"age": @25 };
BOOL isAdult = [predicate evaluateWithObject:person];
NSLog(@"%d", isAdult); // 1 (YES)NSArray<NSDictionary *> *people = @[
@{ @"name": @"Alice", @"age": @25 },
@{ @"name": @"Bob", @"age": @17 },
@{ @"name": @"Amanda",@"age": @30 },
];
NSPredicate *adults =
[NSPredicate predicateWithFormat:@"age >= 18"];
NSArray *adultsOnly = [people filteredArrayUsingPredicate:adults];
// -> Alice, Amanda
NSPredicate *startsWithA =
[NSPredicate predicateWithFormat:@"name BEGINSWITH[c] %@", @"a"];
NSArray *aNames = [people filteredArrayUsingPredicate:startsWithA];
// -> Alice, Amanda (case-insensitive match)// Format-string form:
NSPredicate *eligible =
[NSPredicate predicateWithFormat:@"age >= 18 AND country == %@", @"US"];
// Programmatic NSCompoundPredicate form, built from an array of
// existing predicates -- useful when the set of conditions is
// assembled dynamically:
NSMutableArray<NSPredicate *> *subpredicates = [NSMutableArray array];
[subpredicates addObject:[NSPredicate predicateWithFormat:@"age >= 18"]];
if (requireUSOnly) {
[subpredicates addObject:
[NSPredicate predicateWithFormat:@"country == %@", @"US"]];
}
NSPredicate *dynamicPredicate =
[NSCompoundPredicate andPredicateWithSubpredicates:subpredicates];NSFetchRequest<Employee *> *request =
[Employee fetchRequest];
request.predicate =
[NSPredicate predicateWithFormat:@"age >= %@ AND country == %@",
@18, @"US"];
request.sortDescriptors =
@[[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]];
NSError *error = nil;
NSArray<Employee *> *results =
[managedObjectContext executeFetchRequest:request error:&error];NSArray<NSDictionary *> *employees = @[
@{ @"name": @"Alice", @"salary": @85000 },
@{ @"name": @"Bob", @"salary": @72000 },
@{ @"name": @"Amanda",@"salary": @91000 },
];
NSNumber *totalSalary =
[employees valueForKeyPath:@"@sum.salary"];
NSNumber *averageSalary =
[employees valueForKeyPath:@"@avg.salary"];
NSLog(@"total: %@ average: %@", totalSalary, averageSalary);
// Same @count operator used inside a predicate on a to-many relationship:
NSPredicate *bigTeams =
[NSPredicate predicateWithFormat:@"employees.@count > %@", @5];// UNSAFE: untrusted userInput is spliced directly into predicate syntax.
NSString *userInput = @"x' OR 'a'=='a";
NSString *unsafeFormat =
[NSString stringWithFormat:@"name == '%@'", userInput];
NSPredicate *unsafePredicate =
[NSPredicate predicateWithFormat:unsafeFormat];
// unsafeFormat is now: name == 'x' OR 'a'=='a'
// -- a predicate that matches EVERY element, not just ones named "x".
// SAFE: userInput is passed as a substitution ARGUMENT, never as text
// spliced into the format string itself.
NSPredicate *safePredicate =
[NSPredicate predicateWithFormat:@"name == %@", userInput];
// safePredicate only ever compares 'name' against the literal VALUE of
// userInput -- the string "x' OR 'a'=='a" is treated purely as data.Where Predicates and Expressions Show Up in Real Apps
Predicates are the standard way search bars, filter controls, and settings toggles narrow down what a list screen displays -- typically built as an NSCompoundPredicate assembled dynamically from whichever filters are currently active, exactly the pattern in this lesson's compound-predicate example. In Core Data-backed apps specifically, predicates on NSFetchRequest are how virtually every filtered list or detail lookup is implemented, from the sibling Core Data Fundamentals and Core Data Advanced lessons, and NSExpressionDescription-based aggregates are the standard way to show a computed total or average without loading every underlying record into memory. Predicate injection becomes a real concern the moment any part of a predicate's format string is built from user-supplied text -- a search field, a URL parameter, imported data -- rather than from values entirely under the app's own control, which is exactly the boundary the safe %@ substitution pattern exists to protect.
NSString *unsafe = [NSString stringWithFormat:@"name == '%@'", userInput]; NSPredicate *p = [NSPredicate predicateWithFormat:unsafe];
NSPredicate *p = [NSPredicate predicateWithFormat:@"name == %@", userInput];
Splicing untrusted input into a format string before parsing lets crafted input (containing a stray quote or predicate syntax of its own) change the predicate's actual logic -- an injection risk structurally identical to concatenated SQL. Passing the value as a %@ argument treats it strictly as data, never as syntax.
NSArray *filtered = [people filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"salary"]]; // missing comparison entirely
NSArray *filtered = [people filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"salary >= %@", @50000]];
A predicate format string needs an actual comparison (or boolean expression) to evaluate to true/false -- a bare key path with no operator isn't a valid comparison predicate and either fails to parse or behaves unexpectedly depending on context; always pair a key with an explicit operator and value.
What does [NSPredicate predicateWithFormat:@"age >= 18"] actually do by itself, before being evaluated against anything? A. It immediately filters every object currently in memory B. Nothing yet -- it builds a reusable predicate object describing the condition, which only takes effect once evaluated against an object or handed to a filtering/fetch method C. It throws an exception because no object was specified D. It queries a Core Data store directly, even with no managed object context involved
B. Nothing yet -- it builds a reusable predicate object describing the condition, which only takes effect once evaluated against an object or handed to a filtering/fetch method — predicateWithFormat: only parses the format string into a reusable NSPredicate object -- it performs no evaluation on its own. The condition takes effect only when the predicate is evaluated, via -evaluateWithObject:, a filtering method, or a fetch request's -predicate property.
What's the difference between NSArray's -filteredArrayUsingPredicate: and NSMutableArray's -filterUsingPredicate:? A. They are identical in every respect B. -filteredArrayUsingPredicate: returns a new array of matching elements, leaving the original unchanged; -filterUsingPredicate: removes non-matching elements in place from the array it's called on C. -filterUsingPredicate: only works with Core Data managed objects D. -filteredArrayUsingPredicate: mutates the original array while -filterUsingPredicate: does not
B. -filteredArrayUsingPredicate: returns a new array of matching elements, leaving the original unchanged; -filterUsingPredicate: removes non-matching elements in place from the array it's called on — -filteredArrayUsingPredicate: is non-mutating and returns a new NSArray; -filterUsingPredicate:, available on NSMutableArray, removes elements that don't satisfy the predicate directly from the receiver.
Why does setting a predicate on a Core Data NSFetchRequest tend to be more efficient than fetching everything and filtering with -filteredArrayUsingPredicate: afterward? A. There is no efficiency difference between the two approaches B. A fetch request's predicate is translated into the store's native query language (SQL for the default store), so filtering happens at the store level, bringing only matching objects into memory -- rather than loading everything and filtering in Objective-C afterward C. -filteredArrayUsingPredicate: is always faster because it runs on a background thread automatically D. NSFetchRequest ignores predicates entirely and always returns every object of the given entity
B. A fetch request's predicate is translated into the store's native query language (SQL for the default store), so filtering happens at the store level, bringing only matching objects into memory -- rather than loading everything and filtering in Objective-C afterward — Core Data translates a fetch request's predicate into the store's own query language, so the filtering happens where the data lives rather than requiring every object to be fetched into memory first and filtered afterward -- a key theme the sibling Core Data Advanced lesson develops further.
What is 'predicate injection,' and why is [NSString stringWithFormat:@"name == '%@'", userInput] vulnerable to it? A. It's a performance issue only, never a correctness or security issue B. It's a SQL-injection-shaped risk: splicing untrusted input directly into a predicate format string before parsing lets crafted input change the predicate's actual logic, rather than being treated as a plain value to compare against C. It only affects predicates used with NSMutableArray, never NSArray or Core Data D. NSPredicate automatically sanitizes any string passed to predicateWithFormat:, so this pattern is actually safe
B. It's a SQL-injection-shaped risk: splicing untrusted input directly into a predicate format string before parsing lets crafted input change the predicate's actual logic, rather than being treated as a plain value to compare against — Building the format string via concatenation means untrusted content becomes part of the syntax the predicate parser interprets, letting crafted input (a stray quote, an injected OR clause) change the predicate's meaning entirely -- structurally identical to classic SQL injection.
What is the safe alternative to string-concatenating untrusted input into a predicate format string? A. Manually escaping every single quote in the untrusted string before concatenating it B. Passing the untrusted value as a %@ (or appropriate specifier) substitution argument to predicateWithFormat: itself, rather than splicing it into the string beforehand C. Avoiding NSPredicate entirely and writing manual if-statements instead D. Only ever using NSCompoundPredicate, never predicateWithFormat:, for any predicate involving user input
B. Passing the untrusted value as a %@ (or appropriate specifier) substitution argument to predicateWithFormat: itself, rather than splicing it into the string beforehand — predicateWithFormat: already supports argument substitution (%@ and other specifiers) exactly for this purpose -- values passed this way are treated strictly as data to compare against, never parsed as predicate-language syntax, closing off the injection risk entirely.
Given an NSArray of NSDictionary objects representing products, each with 'category' (NSString) and 'price' (NSNumber) keys, write a predicate that matches products where category is either 'Books' or 'Electronics' AND price is less than 50, using safe %@ argument substitution throughout. Then write the unsafe, string-concatenation version of the same predicate assuming the category list came from user input, and explain specifically what a malicious category value could do to it.
Safe: NSPredicate *p = [NSPredicate predicateWithFormat:@"(category IN %@) AND price < %@", @[@"Books", @"Electronics"], @50];\n\nUnsafe: NSString *unsafeFormat = [NSString stringWithFormat:@"(category IN {%@}) AND price < 50", userSuppliedCategoryList]; NSPredicate *p = [NSPredicate predicateWithFormat:unsafeFormat];\n\nIf userSuppliedCategoryList were something like \"'Books'} OR 1==1 //\", the resulting format string's logic changes entirely -- the injected OR 1==1 can make the whole compound condition true for every product regardless of category or price, exactly the injection risk the safe %@-argument version avoids by never treating the untrusted text as predicate syntax in the first place.
IN works well for the category-membership half of the condition, and AND combines it with the price comparison; wrap the IN clause in parentheses if you combine it with AND to avoid ambiguous grouping. For the unsafe version, think about what happens if the 'user input' contains a closing brace or its own OR clause.