Module 80 Projects — Part 1: Calculator, Contact Manager, Network Client, Download Manager

Begin the capstone project sequence by building four progressively harder Objective-C programs — a calculator, a contact manager, a network client, and a download manager — that each combine several techniques from Modules 1-79 into a single working architecture, rather than practicing any one feature in isolation.

Every module up to this point taught one idea at a time: pointers, classes, protocols, categories, blocks, memory management, NSURLSession, delegation, Grand Central Dispatch, and dozens more. Real software never arrives in single-idea pieces, and the only way to find out whether those ideas actually fit together in your hands is to build something that needs several of them at once. This lesson is the first of the course's project walkthroughs. It takes four small but realistic Objective-C programs — a calculator, a contact manager, a network client, and a download manager — and builds each one from its architecture down to its method bodies, explaining the design decision behind every class boundary. None of the individual techniques here are new; what is new is seeing them composed under the pressure of a real feature list, with real error handling and real edge cases. Treat this lesson as a set of blueprints you should type out and run yourself, not just read.

Project 1 — C + Objective-C Calculator

The calculator is the simplest of the four projects, and that is exactly its point: it is small enough that you can see the whole architecture in one sitting, but it still forces you to decide how operators should be represented, how invalid input should be reported, and where the boundary between plain C and Objective-C should sit.

Architecturally, the calculator teaches the 'strategy' shape of object-oriented design: instead of one giant switch statement that branches on an operator character, each operator becomes its own small class conforming to a shared protocol. Recall the Protocols lesson's discussion of protocols as contracts rather than inheritance — a CalculatorOperation protocol lets AdditionOperation, SubtractionOperation, and DivisionOperation all be treated uniformly by an engine that never needs to know which concrete class it is holding. This is the same pattern you will meet again, at larger scale, in the delegate-based Download Manager later in this lesson.

The second architectural lesson is disciplined error handling. Objective-C's NSError-out-parameter convention, covered in the earlier Error Handling with NSError lesson, is the idiomatic way to report a recoverable failure — division by zero, malformed input, an unrecognized operator — without resorting to exceptions, which in Objective-C are reserved for programmer error, not user input. A calculator that crashes on '5 / 0' is a calculator with a bug in its error-handling design, not in its arithmetic.

• Command-line input loop reading raw text and handing it to a parser • One class per operator behind a shared CalculatorOperation protocol • A CalculatorEngine that owns a symbol-to-operation lookup table • NSError-based validation for malformed expressions and unknown operators • Division-by-zero and overflow treated as recoverable errors, not crashes

Project 2 — Contact Manager

The contact manager moves from arithmetic to data management, and it is the first project in the sequence that has to survive being closed and reopened. That single requirement — persistence — drives most of its design.

The Person class is a plain Foundation value object: it stores its fields with copy properties (recall the Properties and Memory Management lessons' rule that NSString properties should always be declared copy, never strong, so a caller's mutable string cannot silently change your object's state after the fact), and it adopts NSCoding so the object graph can be archived to disk. NSMutableArray is the storage backbone — no database, no Core Data yet, just an in-memory collection that the ContactManager class owns privately and exposes only through a read-only copy, so nothing outside the class can mutate the list behind its back.

Search and sort are where Foundation's declarative collection APIs pay off. NSPredicate turns 'find every contact whose name contains this substring' into a single expression instead of a hand-written loop, and NSSortDescriptor composes multiple sort keys (last name, then first name) without writing a custom comparator block. Persistence uses NSKeyedArchiver/NSKeyedUnarchiver with secure coding, which is the modern replacement for the older, insecure archiving APIs — always prefer the *_error: and requiringSecureCoding: variants covered in the Serialization and File Handling lesson.

• Person value class with copy properties and NSCoding/NSCopying conformance • NSMutableArray-backed store wrapped by a ContactManager façade • NSPredicate-based substring search across first and last name • NSSortDescriptor-based multi-key sorting • Secure NSKeyedArchiver/NSKeyedUnarchiver persistence to a file on disk

Project 3 — Network Client

The network client is where the course's asynchronous-programming lessons finally meet a real, unreliable network. Everything about this project's design exists to answer one question: what happens when the network doesn't behave?

As covered in the earlier NSURLSession and Networking lesson, NSURLSessionDataTask runs its completion handler on a background queue, so any UI update or shared-state mutation triggered from inside it must be dispatched back to the main queue — this project's completion handler does exactly that with dispatch_async(dispatch_get_main_queue(), ...). JSON parsing goes through NSJSONSerialization and is immediately converted into a strongly typed model object (RepositoryModel) rather than passed around as loose dictionaries, which is the same 'parse at the boundary' discipline from the JSON and Model Objects lesson.

The two ideas that are new at this scale are loading state and retry. A LoadingState enum (idle, loading, success, failed) gives any UI layer a single source of truth for what to show instead of inferring state from whether an error object happens to be nil. Retry logic re-issues the request a bounded number of times on a transient failure (a 5xx response or a transport error), with a short delay between attempts, so a single dropped packet doesn't surface as a hard failure to the user.

• NSURLSessionDataTask-based GET request against a JSON REST endpoint • NSJSONSerialization parsing into a dedicated RepositoryModel class • LoadingState enum (idle / loading / success / failed) as the single source of truth • Bounded retry with a delay for transient (5xx or transport) failures • Main-queue dispatch of the completion block so callers can safely touch UI

Project 4 — Download Manager

The download manager is the most demanding of the four projects because it has to track long-running work that can be cancelled, that reports incremental progress, and that Apple expects you to be able to continue even after your app has been suspended.

NSURLSessionDownloadTask, driven through an NSURLSessionDownloadDelegate, is the right tool here rather than a simple completion-handler task, because progress reporting (URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) and completion (URLSession:downloadTask:didFinishDownloadingToURL:) are delegate callbacks, not block arguments. Recall the Retain Cycles lesson's warning about delegate references: a DownloadManager that holds itself as the session delegate must be careful that nothing on the other side holds a strong reference back to it in a way that outlives the session, and any block captured inside a delegate callback should still follow the weak-self/strong-self dance from the Blocks and Closures lesson if it is long-lived.

Background session configuration (NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:) lets a download continue even if the app is suspended by the system, at the cost of losing block-based completion handlers — background sessions are delegate-only. This project therefore mixes both styles deliberately: a background, delegate-driven session for large, resumable transfers, and a lighter, block-based foreground session for short, one-shot downloads, so you can see both patterns side by side and understand why one has to be delegate-based and the other doesn't.

• NSURLSessionDownloadTask driven by an NSURLSessionDownloadDelegate • Per-download progress via didWriteData:totalBytesWritten:totalBytesExpectedToWrite: • Cancellation by task identifier without leaking the cancelled task's entry • Background NSURLSessionConfiguration for transfers that must survive app suspension • A parallel block-based API for short, foreground-only downloads • Moving the finished temp file to a permanent location before the delegate callback returns

The Strategy-Protocol Shape Used Across All Four Projects

A protocol describes what a type can do, not what it inherits from — several unrelated classes can conform to the same protocol. An engine that stores id<SomeStrategy> instead of a concrete class can add new behavior (new operators, new operations) without ever being recompiled. This exact shape reappears as CalculatorOperation in Project 1 and, in spirit, as the delegate protocol in Project 4.

@protocol SomeStrategy <NSObject>
@property (nonatomic, readonly) NSString *identifier;
- (id)performWithInput:(id)input error:(NSError **)error;
@end

@interface ConcreteStrategyA : NSObject <SomeStrategy>
@end

@interface StrategyDrivenEngine : NSObject
- (void)registerStrategy:(id<SomeStrategy>)strategy;
- (id)runWithInput:(id)input usingIdentifier:(NSString *)identifier error:(NSError **)error;
@end

Code Examples

Each operator is a self-contained class implementing the same three-member protocol. Nothing about AdditionOperation or DivisionOperation knows about the other — the engine that will hold them is what makes them interchangeable. Note the out-parameter pattern: the method still returns a double (0.0) on failure, but the caller is expected to check the NSError pointer before trusting that return value — this is the standard Objective-C convention, not a special case for this project.

if (rhs == 0.0) { ... *error = [NSError errorWithDomain: ... ]; }

The engine never switches on the operator string itself — it looks the symbol up in a dictionary of registered strategies and forwards the call. Adding a new operator means adding a new class and one registerOperation: call, never touching evaluateExpression: again.

Common mistake: A common first draft skips the tokens.count != 3 check and lets [tokens[1] doubleValue] silently return 0 for garbage input, turning a user typo into a wrong answer instead of a reported error.

Person is a pure value object: every string property is copy so external mutation cannot corrupt a stored Person, and it conforms to NSCoding so the archiving layer can persist it without any bespoke serialization code. decodeObjectOfClass:forKey: (rather than the older, unchecked decodeObjectForKey:) is what lets the class support requiringSecureCoding:YES — it refuses to decode anything that isn't the declared class, closing off a class of deserialization attacks.

decodeObjectOfClass:[NSString class] forKey:@"firstName" The public surface never exposes the mutable backing array directly — allContacts returns an immutable copy, so callers cannot accidentally reach in and mutate the manager's internal state without going through addContact: or removeContact:. Expected result: Search text 'ann' against contacts named Anna Brooks, Lee Chan, and Ann Osei returns Anna Brooks and Ann Osei, sorted by last name: Brooks, Osei.

[NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:data error:error]

The retry path and the success/failure path share one recursive method, performRequestWithURL:remainingRetries:completion:, so the retry count decrements naturally with each recursive call instead of needing a separate loop or counter variable.

BOOL shouldRetry = (error != nil || httpResponse.statusCode >= 500) && remainingRetries > 0;

Common mistake: Retrying on every error, including a 404 for a username that genuinely doesn't exist, wastes three round trips reproducing a failure that will never succeed — only transport errors and 5xx server errors are worth retrying.

The delegate property is declared weak, exactly as the Retain Cycles lesson requires for any delegate reference, so the object that owns the DownloadManager (say, a view controller) doesn't get kept alive forever just because the download manager holds a pointer back to it. URLSession:downloadTask:didFinishDownloadingToURL: hands you a temporary file that is deleted the moment the delegate method returns — moveItemAtURL:toURL:error: must run synchronously, inside that callback, before you return, which is why the move happens before the dispatch_async that reports success back to the delegate.

@property (nonatomic, weak) id<DownloadManagerDelegate> delegate;

This is the same file-move logic as the delegate version, but reached through a block-based completion handler on a separate, ordinary (non-background) session — proof that the two APIs are interchangeable in spirit, and the choice between them comes down to whether the transfer needs to survive app suspension.

Common mistake: Capturing self strongly inside completionHandler here (writing self instead of weakSelf/strongSelf) would keep the DownloadManager alive for as long as the task lives, and if the task never completes — say, the user backgrounds the app indefinitely — that reference never releases.

typedef NS_ENUM(NSInteger, CalculatorErrorCode) {
    CalculatorErrorCodeDivisionByZero  = 1,
    CalculatorErrorCodeInvalidInput    = 2,
    CalculatorErrorCodeUnknownOperator = 3
};

NSString * const CalculatorErrorDomain = @"CalculatorErrorDomain";

@protocol CalculatorOperation <NSObject>
@property (nonatomic, readonly) NSString *symbol;
- (double)applyToLHS:(double)lhs rhs:(double)rhs error:(NSError **)error;
@end

@interface AdditionOperation : NSObject <CalculatorOperation>
@end

@implementation AdditionOperation
- (NSString *)symbol { return @"+"; }
- (double)applyToLHS:(double)lhs rhs:(double)rhs error:(NSError **)error {
    return lhs + rhs;
}
@end

@interface DivisionOperation : NSObject <CalculatorOperation>
@end

@implementation DivisionOperation
- (NSString *)symbol { return @"/"; }
- (double)applyToLHS:(double)lhs rhs:(double)rhs error:(NSError **)error {
    if (rhs == 0.0) {
        if (error) {
            *error = [NSError errorWithDomain:CalculatorErrorDomain
                                          code:CalculatorErrorCodeDivisionByZero
                                      userInfo:@{ NSLocalizedDescriptionKey: @"Cannot divide by zero." }];
        }
       
…
@interface CalculatorEngine : NSObject
- (void)registerOperation:(id<CalculatorOperation>)operation;
- (double)evaluateExpression:(NSString *)expression error:(NSError **)error;
@end

@implementation CalculatorEngine {
    NSMutableDictionary<NSString *, id<CalculatorOperation>> *_operationsBySymbol;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _operationsBySymbol = [NSMutableDictionary dictionary];
        [self registerOperation:[[AdditionOperation alloc] init]];
        [self registerOperation:[[DivisionOperation alloc] init]];
    }
    return self;
}

- (void)registerOperation:(id<CalculatorOperation>)operation {
    _operationsBySymbol[operation.symbol] = operation;
}

- (double)evaluateExpression:(NSString *)expression error:(NSError **)error {
    NSArray<NSString *> *tokens = [expression componentsSeparatedByString:@" "];
    if (tokens.count != 3) {
        if (error) {
            *error = [NSError errorWithDomain:CalculatorErrorDomain
                                          code:CalculatorErrorCodeInvalidInput
                                      userInfo:@{ NSLocalizedDescriptionKey: @"Expected format: <lhs> <op> <rhs>" }];
        }
  
…
@interface Person : NSObject <NSCoding, NSCopying>
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *phone;

- (instancetype)initWithFirstName:(NSString *)firstName
                          lastName:(NSString *)lastName
                             email:(NSString *)email
                             phone:(NSString *)phone;
- (NSString *)fullName;
@end

@implementation Person

- (instancetype)initWithFirstName:(NSString *)firstName
                          lastName:(NSString *)lastName
                             email:(NSString *)email
                             phone:(NSString *)phone {
    self = [super init];
    if (self) {
        _firstName = [firstName copy];
        _lastName = [lastName copy];
        _email = [email copy];
        _phone = [phone copy];
    }
    return self;
}

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

#pragma mark - NSCoding

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.firstName forKey:@"firstName"];
    [coder encodeObject:se
…
@interface ContactManager : NSObject
@property (nonatomic, readonly) NSArray<Person *> *allContacts;
- (void)addContact:(Person *)person;
- (void)removeContact:(Person *)person;
- (NSArray<Person *> *)contactsMatchingSearchText:(NSString *)text;
- (NSArray<Person *> *)contactsSortedByLastName;
- (BOOL)saveToFile:(NSString *)path error:(NSError **)error;
- (BOOL)loadFromFile:(NSString *)path error:(NSError **)error;
@end

@implementation ContactManager {
    NSMutableArray<Person *> *_contacts;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _contacts = [NSMutableArray array];
    }
    return self;
}

- (NSArray<Person *> *)allContacts {
    return [_contacts copy];
}

- (void)addContact:(Person *)person {
    [_contacts addObject:person];
}

- (void)removeContact:(Person *)person {
    [_contacts removeObject:person];
}

- (NSArray<Person *> *)contactsMatchingSearchText:(NSString *)text {
    NSPredicate *predicate = [NSPredicate predicateWithFormat:
        @"firstName CONTAINS[cd] %@ OR lastName CONTAINS[cd] %@", text, text];
    return [_contacts filteredArrayUsingPredicate:predicate];
}

- (NSArray<Person *> *)contactsSortedByLastName {
    NSSortDes
…
@interface RepositoryModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *ownerLogin;
@property (nonatomic, assign) NSInteger stargazersCount;
+ (instancetype)modelFromJSONDictionary:(NSDictionary *)json;
@end

@implementation RepositoryModel
+ (instancetype)modelFromJSONDictionary:(NSDictionary *)json {
    RepositoryModel *model = [[RepositoryModel alloc] init];
    model.name = json[@"name"];
    model.ownerLogin = json[@"owner"][@"login"];
    model.stargazersCount = [json[@"stargazers_count"] integerValue];
    return model;
}
@end

typedef NS_ENUM(NSInteger, LoadingState) {
    LoadingStateIdle,
    LoadingStateLoading,
    LoadingStateSuccess,
    LoadingStateFailed
};

typedef void (^RepositoryFetchCompletion)(NSArray<RepositoryModel *> *models, NSError * _Nullable error);

@interface RepositoryClient : NSObject
@property (nonatomic, assign, readonly) LoadingState state;
- (void)fetchRepositoriesForUser:(NSString *)username
                      maxRetries:(NSInteger)maxRetries
                      completion:(RepositoryFetchCompletion)completion;
@end

@implementation RepositoryClient {
    NSURLSession *_session;
    LoadingS
…
@protocol DownloadManagerDelegate <NSObject>
- (void)downloadManager:(id)manager didUpdateProgress:(float)progress forIdentifier:(NSString *)identifier;
- (void)downloadManager:(id)manager didFinishDownloadingToURL:(NSURL *)location forIdentifier:(NSString *)identifier;
- (void)downloadManager:(id)manager didFailWithError:(NSError *)error forIdentifier:(NSString *)identifier;
@end

@interface DownloadManager : NSObject <NSURLSessionDownloadDelegate>
@property (nonatomic, weak) id<DownloadManagerDelegate> delegate;
- (NSString *)startDownloadFromURL:(NSURL *)url;
- (void)cancelDownloadWithIdentifier:(NSString *)identifier;
@end

@implementation DownloadManager {
    NSURLSession *_backgroundSession;
    NSMutableDictionary<NSString *, NSURLSessionDownloadTask *> *_tasksByIdentifier;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        NSURLSessionConfiguration *config =
            [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.example.downloadmanager"];
        config.sessionSendsLaunchEvents = YES;
        _backgroundSession = [NSURLSession sessionWithConfiguration:config
                                                         
…
- (void)downloadFileAtURL:(NSURL *)url
                completion:(void (^)(NSURL * _Nullable fileURL, NSError * _Nullable error))completion {
    // Background sessions cannot use completion-handler tasks, so short,
    // foreground-only downloads use a plain default-configuration session instead.
    __weak typeof(self) weakSelf = self;
    NSURLSessionDownloadTask *task =
        [_foregroundSession downloadTaskWithURL:url
                               completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        __strong typeof(self) strongSelf = weakSelf;
        if (!strongSelf || error) {
            completion(nil, error);
            return;
        }

        NSURL *destination = [strongSelf permanentLocationForIdentifier:[[NSUUID UUID] UUIDString]];
        NSError *moveError = nil;
        [[NSFileManager defaultManager] moveItemAtURL:location toURL:destination error:&moveError];
        completion(moveError ? nil : destination, moveError);
    }];
    [task resume];
}

The Shared Architecture Behind All Four Projects

All four projects share one shape: something outside the class triggers work, a small model type carries the data, and a single controller-like class (engine, manager, or client) owns the logic and reports back through a return value, an NSError, or a delegate callback — never through global state.

Input / Trigger              Model Layer                 Service / Controller           Output
  ----------------              -----------                 ---------------------           ------
  "5 / 0" (typed)   ---->   AdditionOp, DivisionOp  ---->  CalculatorEngine          ---->  result or NSError
  addContact: call  ---->   Person (NSCoding)       ---->  ContactManager            ---->  NSArray<Person *>
                                                             |  saveToFile: / loadFromFile:
                                                             v
                                                       archived .plist on disk

  fetch call        ---->   RepositoryModel          ---->  RepositoryClient          ---->  models[] or NSError
                                                       (NSURLSession + retry)          (LoadingState tracks progress)

  startDownload:    ---->   raw bytes on the wire     ---->  DownloadManager           ---->  delegate callbacks
                                                       (NSURLSessionDownloadTask)       (progress / finish / fail)

Why These Four Projects, In This Order

The four projects are ordered by which failure modes they force you to confront. The calculator has no I/O at all — its only failure mode is bad input, so it is the cleanest place to practice NSError discipline. The contact manager adds state that must survive a restart, so it forces you to think about ownership (who is allowed to mutate the array) and serialization. The network client adds a failure mode you cannot fully control — the network itself — so it forces you to design around latency, partial failure, and retry policy. The download manager combines everything: long-lived state, an unreliable network, and a requirement (cancellation and backgrounding) that only shows up once a transfer takes long enough for a user to want to interrupt it or for the OS to suspend your app mid-transfer. Production apps rarely need a calculator, but nearly every production app needs a network client and a download manager built exactly this way.

Making the DownloadManager's delegate property @property (nonatomic, strong) id<DownloadManagerDelegate> delegate;

Declare delegate references weak: @property (nonatomic, weak) id<DownloadManagerDelegate> delegate;

If the delegate holds a strong reference to the DownloadManager (which it usually does, to call startDownloadFromURL:), and the DownloadManager holds a strong reference back, neither object can ever be deallocated — the exact retain cycle the earlier Retain Cycles lesson warned about.

Calling [task cancel] but leaving the cancelled task in _tasksByIdentifier.

Remove the entry from _tasksByIdentifier in the same method that cancels the task, as cancelDownloadWithIdentifier: does above.

A stale dictionary entry pointing at a cancelled task means a later cancelDownloadWithIdentifier: call for the same identifier will silently do nothing, and the dictionary grows without bound if downloads are started and cancelled repeatedly.

Parsing JSON and updating UI directly inside the NSURLSessionDataTask completion handler.

Dispatch back to the main queue before touching anything the UI reads, exactly as fetchRepositoriesForUser:maxRetries:completion: does with dispatch_async(dispatch_get_main_queue(), ...).

NSURLSession completion handlers run on a background queue by default; touching UIKit or reading/writing shared mutable state from that queue is undefined behavior and a classic source of intermittent crashes.

Writing the Contact Manager's persistence with plain NSKeyedArchiver archiveRootObject:toFile: and NSKeyedUnarchiver unarchiveObjectWithFile:.

Use the error-returning, secure-coding variants: archivedDataWithRootObject:requiringSecureCoding:error: and unarchivedObjectOfClasses:fromData:error:.

The older APIs have no error reporting (a failed load just silently returns nil with no explanation) and no secure-coding class check, which the Serialization lesson flagged as a real security gap when loading untrusted files.

[why] In Project 1, why does each operator get its own class conforming to CalculatorOperation instead of one method with a switch statement on the operator character? A. Because Objective-C does not support switch statements on characters B. So new operators can be added by writing a new class and registering it, without ever modifying or recompiling CalculatorEngine's evaluation logic C. Because NSError cannot be used inside a switch statement D. It has no real benefit — it is purely a style preference with no architectural effect

B. So new operators can be added by writing a new class and registering it, without ever modifying or recompiling CalculatorEngine's evaluation logic — This is the strategy pattern: CalculatorEngine depends only on the CalculatorOperation protocol, so extending the calculator with new operators never requires touching evaluateExpression: — a direct payoff of protocol-based design over a hardcoded switch.

[why] Why does ContactManager's allContacts property return [_contacts copy] instead of returning _contacts directly? A. copy is required by NSMutableArray and returning it directly would fail to compile B. Returning the mutable array directly would let any caller add, remove, or reorder contacts without going through addContact:/removeContact:, breaking the class's control over its own state C. copy makes the array thread-safe automatically D. It has no functional difference — copy is only used for style consistency

B. Returning the mutable array directly would let any caller add, remove, or reorder contacts without going through addContact:/removeContact:, breaking the class's control over its own state — Exposing the internal NSMutableArray would let any caller mutate the manager's private state directly, bypassing whatever invariants addContact:/removeContact: are meant to enforce. Returning an immutable copy is the standard way to expose a mutable internal collection safely.

[why] In Project 3's RepositoryClient, why does shouldRetry check httpResponse.statusCode >= 500 but not check for a 404 status code? A. 404 responses cannot be represented as NSHTTPURLResponse objects B. A 404 means the requested resource genuinely does not exist and retrying will reproduce the same failure every time, while a 5xx status usually indicates a transient server-side problem worth retrying C. Retrying on 404 would cause an infinite loop regardless of maxRetries D. NSURLSession automatically retries 404 responses internally, so the app does not need to

B. A 404 means the requested resource genuinely does not exist and retrying will reproduce the same failure every time, while a 5xx status usually indicates a transient server-side problem worth retrying — Retry logic should only re-attempt requests whose failure might be transient. A 404 is a deterministic client error — the same URL will return 404 again immediately — so retrying it wastes network calls without any chance of success, unlike a 5xx or transport-level error.

[why] Why does the DownloadManager in Project 4 use an NSURLSessionDownloadDelegate instead of a block-based completion handler for its background transfers? A. Block-based completion handlers are slower than delegate callbacks B. Delegates are required for any download over 10 megabytes C. Background NSURLSessionConfiguration sessions do not support completion-handler-based tasks; only delegate callbacks are available for them, and delegates are also the only way to receive incremental progress updates D. NSURLSessionDownloadTask does not exist as a block-based API in any configuration

C. Background NSURLSessionConfiguration sessions do not support completion-handler-based tasks; only delegate callbacks are available for them, and delegates are also the only way to receive incremental progress updates — A background session's whole purpose is to keep working after your app is suspended, and iOS achieves that by handing results back through delegate callbacks (possibly after relaunching your app), not through a block closure that could only exist in the process that created it. Progress reporting is also delegate-only.

[why] Why is the DownloadManager's delegate property declared weak rather than strong? A. weak properties are faster to read than strong properties B. To avoid a retain cycle: the object that owns the DownloadManager typically also holds a strong reference to it, so a strong delegate reference back would mean neither object could ever be deallocated C. NSURLSessionDownloadDelegate requires all conforming properties to be weak D. weak is required because id<DownloadManagerDelegate> is a protocol type, not a class type

B. To avoid a retain cycle: the object that owns the DownloadManager typically also holds a strong reference to it, so a strong delegate reference back would mean neither object could ever be deallocated — This mirrors the standard Cocoa delegate pattern: the delegating object (DownloadManager) does not own its delegate. A strong reference in both directions would create the classic ownership cycle the Retain Cycles lesson covered — weak breaks the cycle without changing behavior in the common case where the delegate outlives the manager anyway.

Extend the Contact Manager from this lesson with a 'favorites' feature: add a BOOL isFavorite property to Person, add a contactsMarkedAsFavorite method to ContactManager that returns only favorited contacts sorted by last name (reuse contactsSortedByLastName's NSSortDescriptor approach, filtered first with an NSPredicate on isFavorite), and make sure toggling isFavorite on a Person that is already stored in _contacts is reflected the next time allContacts or contactsMarkedAsFavorite is called — without needing to re-add the object to the array.

Add @property (nonatomic, assign) BOOL isFavorite; to Person (and encode/decode it as a BOOL in NSCoding). Then implement: - (NSArray<Person *> *)contactsMarkedAsFavorite { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"isFavorite == YES"]; NSArray<Person *> *favorites = [_contacts filteredArrayUsingPredicate:predicate]; NSSortDescriptor *byLastName = [NSSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES]; return [favorites sortedArrayUsingDescriptors:@[byLastName]]; }

Because NSMutableArray stores references, not copies, mutating a Person object you already have a pointer to (from a previous addContact: call) is visible through every other pointer to that same object — you do not need to remove and re-add it. Combine an NSPredicate on isFavorite == YES with the same NSSortDescriptor array used in contactsSortedByLastName.