Module 80 Projects — Part 2: Notes App, Core Data App, Swift Mixed App, Legacy Modernization

Walk through four more complete, realistic Objective-C project builds — a Notes application, a Core Data-backed application, an Objective-C/Swift mixed application, and a legacy-code modernization exercise — combining model design, persistence, search, notifications, delegation, interoperability, and modernization techniques from across the whole course into working, production-shaped code.

This lesson is the second half of Module 80's project walkthroughs, picking up where Projects 1 through 4 left off. Where the first four projects focused on foundational app shapes — data models, table-driven UIs, networking, and image/media handling — this second set pushes into four different but equally important real-world shapes: an app built around a persistent, searchable model (Notes); an app whose entire data layer is Core Data (Core Data Application); an app that deliberately mixes Objective-C and Swift in one codebase (the Mixed Application); and, finally, a piece of intentionally bad, old Objective-C code that you modernize step by step into current, safe, idiomatic Objective-C (Legacy Modernization). Each project is presented the way you'd actually approach it on the job: what the app needs to do, how you'd structure it, and then real code for the pieces that matter most — not a restated feature checklist. By the end of this lesson you will have walked through the architecture and core code of eight total projects across both lessons in Module 80, which is deliberately meant to feel like a small portfolio of real Objective-C work.

Project 5 — Notes Application: Model, Controller, Persistence, Search, and Notifications

A notes app looks simple from the outside — a list of text, an editor, a way to search — but it is a genuinely good vehicle for practicing the everyday shape of an Objective-C app: a plain model object, a controller that owns a collection of them, a persistence layer that survives relaunches, and cross-object communication that doesn't require every object to know about every other object directly. We'll build this the way you would in a real codebase: Note as a small, immutable-leaning model object; NotesStore as the controller that owns the array of notes, handles persistence, and performs search; and NSNotificationCenter plus a delegate protocol as two different, complementary ways for other parts of the app to react to changes.

The model, Note, is intentionally plain — a value-ish object with a stable identifier, a title, body text, and timestamps. Giving every note a NSUUID-backed identifier at creation time (rather than relying on array index or title) is what makes search, editing, and deletion safe: you always look a note up — or refer to it in a notification's userInfo — by its noteID, never by position, because positions shift the moment the underlying array is sorted, filtered, or edited elsewhere.

NotesStore is the single owner of the notes array and the single place persistence happens, following the same 'one owner per collection' discipline used in the model design lessons earlier in the course. It exposes read-only access to its notes, mutation methods (addNote:, updateNote:, deleteNote:) that are the *only* sanctioned way to change the collection, and it is responsible for loading from and saving to disk. Persistence here uses NSKeyedArchiver/NSKeyedUnarchiver with NSSecureCoding, which the course covered in the persistence and serialization lessons — Note adopts NSSecureCoding and implements encodeWithCoder:/initWithCoder:, and NotesStore archives the whole array to a file inside the app's Application Support directory on every mutation (debounced in a real app; done synchronously here for clarity).

Search is implemented as a simple, case-insensitive predicate-driven filter over title and body, using NSPredicate with format strings the way the Core Data and Foundation collections lessons introduced — this keeps the search logic declarative and easy to extend (for example, later adding a date range or tag filter is just another predicate ANDed in) rather than hand-rolling nested loops.

Two different notification mechanisms are used on purpose, to contrast them directly: NSNotificationCenter posts a broadcast NotesStoreDidChangeNotification that any number of interested objects can observe without NotesStore needing to know they exist — good for loosely coupled observers like a badge counter or an analytics logger. The NotesStoreDelegate protocol, by contrast, is a single, strongly-typed relationship for the one object (typically the list view controller) that needs synchronous, specific callbacks such as being told *which* note was deleted so it can animate a specific row out. This delegate-versus-notification split mirrors exactly what the delegation and NSNotificationCenter lessons taught: delegate for a focused one-to-one contract, notification center for one-to-many broadcast.

• Note: plain model object, NSUUID identifier, adopts NSSecureCoding for archiving. • NotesStore: single owner of the notes array; only place addNote:/updateNote:/deleteNote: and persistence happen. • Persistence via NSKeyedArchiver/NSKeyedUnarchiver into Application Support, matching the earlier persistence lessons. • Search via NSPredicate format strings over title/body — declarative and extensible, not hand-rolled loops. • NSNotificationCenter for broadcast, loosely-coupled observers; a delegate protocol for the one strongly-typed, synchronous relationship. • Accessibility: every note row exposes an accessibilityLabel combining title and a relative date, and the empty-state view is announced via UIAccessibilityPostNotification. • Testing: NotesStore is initialized with an injectable file URL and an injectable NSFileManager, so unit tests never touch the real filesystem — direct application of the Mocking and Dependency Injection lesson.

Project 6 — Core Data Application: Entities, Relationships, Fetching, and Background Contexts

This project is the one place in Module 80 where the entire persistence layer is Core Data rather than hand-rolled archiving, and it deliberately exercises the parts of Core Data that a real app leans on hardest: a two-entity model with a relationship, predicate-driven fetching, a background context for writes so the UI never stalls, and a short note on migration and performance. We assume the Core Data Fundamentals and Core Data Advanced lessons already covered the stack setup (NSPersistentContainer, the managed object model, contexts, and saving) in depth, so this walkthrough focuses on how those pieces come together in one concrete app: a 'Library' app tracking Book and Author entities.

The Core Data model defines two entities. Author has a name attribute and a to-many relationship books to Book. Book has title, pageCount, and dateAdded attributes, plus an inverse to-one relationship author back to Author. Setting the inverse relationship in the model editor (or programmatically) is what lets Core Data maintain referential integrity automatically — deleting an Author with a cascade delete rule removes its Books, and appending a Book to an Author's books set automatically sets that book's author on save.

Both entities are backed by generated NSManagedObject subclasses (Book+CoreDataClass/Book+CoreDataProperties, and the same for Author), which is the pattern the Core Data Fundamentals lesson recommended over accessing attributes with key-value coding strings — it gives you compile-time checking and autocomplete on book.title instead of [book valueForKey:@"title"].

Fetching uses NSFetchRequest built from the generated +fetchRequest class method, with NSPredicate and NSSortDescriptor layered on top — for example, fetching every book whose title contains a search string, added within the last year, sorted by dateAdded descending. Building predicates with format strings and argument substitution (titleCONTAINS[cd] %@) avoids the classic mistake of hand-building predicate strings by concatenating user input, which the Core Data Advanced lesson flagged as both a correctness and injection-style risk.

Writes happen on a background context created with newBackgroundContext from the persistent container, and every operation on that context is wrapped in performBlock: (or performBlockAndWait: when the caller genuinely needs to block) — never touched directly from another thread, per Core Data's strict 'one context, one thread/queue' rule covered in Core Data Advanced. After a background save, the main context is refreshed via NSManagedObjectContextDidSaveNotification merging (mergeChangesFromContextDidSaveNotification:) so the UI's fetched results stay in sync without a manual refetch.

On migration and performance: the project ships a .xcdatamodeld with two model versions to demonstrate a lightweight migration (adding the pageCount attribute with a default value), enabling NSMigratePersistentStoresAutomaticallyOption and NSInferMappingModelAutomaticallyOption so Core Data infers the mapping itself, which is sufficient for additive, non-destructive schema changes. For performance, the book list uses an NSFetchedResultsController with a small fetchBatchSize rather than fetching every row into memory, and relationship faulting is left as Core Data's default (faults resolve lazily) rather than forcing relationshipKeyPathsForPrefetching unless profiling (see the Instruments lesson) actually shows a N+1-style fetch problem.

• Two entities, Author (to-many books) and Book (to-one author), with an inverse relationship for referential integrity. • Generated NSManagedObject subclasses for compile-time-checked property access instead of raw KVC strings. • NSFetchRequest + NSPredicate(format:) + NSSortDescriptor for safe, declarative, parameterized fetching. • Background NSManagedObjectContext for writes, always inside performBlock:, merged into the main context on save. • Lightweight migration across two model versions using automatic migration and mapping inference. • NSFetchedResultsController with a bounded fetchBatchSize for list performance instead of fetching everything eagerly.

Project 7 — Objective-C + Swift Mixed Application: Interoperability and API Annotations

Very few real Objective-C codebases stay pure Objective-C forever — the far more common situation, and the one this project simulates, is a mature Objective-C service and model layer with newer feature work happening in Swift on top of it. The architecture is intentionally layered top to bottom: a Swift UI/feature layer calls into an Objective-C service layer, which calls into an Objective-C model layer, which sits on Foundation, which talks to the network. This is exactly the direction of dependency the Swift Interoperability lesson recommends — Swift depends on Objective-C, not the other way around, so the older, larger Objective-C codebase never needs to know Swift exists.

The Objective-C service class, PXUserProfileService, is written specifically to read well from Swift, which means every public API is annotated. Nullability is marked exhaustively with NS_ASSUME_NONNULL_BEGIN/END bracketing the interface plus explicit nullable on the one value (an optional cached profile) that really can be absent — the Nullability lesson's rule of 'assume nonnull, opt out explicitly' rather than the reverse. The completion block's error parameter is NSError * _Nullable, which Swift interprets as Error?, and because the method follows Cocoa's completion:(void (^)(PXUserProfile * _Nullable, NSError * _Nullable))completion shape with a void-returning block and NSError-last, the Swift compiler automatically imports it as a throws-style completion or lets Swift callers pattern-match it as a normal optional/error pair.

NS_SWIFT_NAME is used on both the class-level factory method and a couple of parameter labels to make the Swift call site read naturally — for instance annotating fetchProfileForUserID:completion: with NS_SWIFT_NAME(fetchProfile(userID:completion:)) so Swift code calls service.fetchProfile(userID: id) { ... } instead of the more Objective-C-shaped service.fetchProfile(forUserID: id, completion: ...). This is the same annotation technique the Swift Interoperability lesson covered, just applied here to a full class rather than a single method in isolation.

On the build-setup side (described here rather than shown as code, since it's project configuration, not a class): the Swift target imports the Objective-C service and model classes through a bridging header (ProjectName-Bridging-Header.h) that #imports PXUserProfileService.h and PXUserProfile.h; going the other direction, if any Objective-C code needed to call back into Swift, it would #import "ProjectName-Swift.h", the compiler-generated umbrella header — but in this architecture that direction is deliberately avoided, keeping the dependency arrow pointing one way, Swift down to Objective-C, never back up.

The Objective-C model object, PXUserProfile, uses lightweight generics on its one collection property (NSArray<NSString *> *interests) so that Swift sees [String] rather than the far less useful [Any] it would infer from an untyped NSArray * — this is the same technique the Lightweight Generics lesson demonstrated, and it is one of the highest-value, lowest-effort annotations you can add when preparing an Objective-C model layer for Swift consumers.

• Layered dependency direction: Swift feature layer → Objective-C service → Objective-C model → Foundation → network. • NS_ASSUME_NONNULL_BEGIN/END plus explicit nullable only where a value can truly be absent. • NS_SWIFT_NAME reshapes Objective-C method/parameter names into idiomatic Swift call sites. • Bridging header imports Objective-C headers into Swift; the generated -Swift.h header (unused here on purpose) would go the other way. • Lightweight generics on collection properties (NSArray<NSString *> *) so Swift infers real element types instead of Any.

Project 8 — Legacy Modernization: From MRC and Unsafe APIs to ARC, Nullability, and DI

The last project inverts the usual approach: instead of writing a new app, you're handed a small, deliberately old-fashioned Objective-C class — the kind of code a real team might inherit from a five- or ten-year-old codebase — and you modernize it in a series of concrete, reviewable steps. The starting point, LegacyImageCache, is manually reference counted (MRC), uses an unsafe C-string-keyed cache, has no nullability annotations, no generics, no tests, no dependency injection (it reaches for a singleton and touches the filesystem directly), and it has compiler warnings that were simply left unaddressed.

The modernization proceeds in the order a real migration would: first ARC (mechanical but must be done first, since MRC and ARC cannot be mixed within one file), then safer APIs (replacing the raw NSMutableDictionary keyed by char * with a proper NSString-keyed, strongly-typed cache), then nullability annotations (so callers — including any Swift caller — know exactly which return values can be nil), then lightweight generics on the cache's collection type, then dependency injection (so the class can be unit tested without hitting the real filesystem), and finally a pass to eliminate every remaining compiler warning and tighten a couple of latent performance and security issues (a predictable cache-file naming scheme that could collide or be guessed, fixed with a hashed, sandboxed filename).

This project deliberately reuses techniques from several earlier lessons rather than re-teaching them: the MRC-to-ARC mechanics are exactly what the ARC and MRC lessons covered (dropping manual retain/release/autorelease, converting dealloc to only release non-object resources), the nullability annotations follow the same pattern as Project 7 above, the generics follow the Lightweight Generics lesson, and injecting an NSFileManager and a cache-root NSURL through the initializer — instead of hardcoding NSFileManager.defaultManager and a fixed path — is precisely the seam-creation technique from the Testing and Mocking and Dependency Injection lessons, which is what finally makes LegacyImageCache unit-testable at all.

• Order of operations: ARC conversion first (MRC/ARC cannot mix in one file), then safer APIs, then nullability, then generics, then DI, then warnings/perf/security. • Unsafe char*-keyed dictionary replaced with a strongly-typed NSString-keyed, NSCache-backed store. • Hardcoded singleton + direct filesystem access replaced with injected NSFileManager and cache-root NSURL for testability. • Predictable cache filenames replaced with a hashed name inside the app's caches directory to remove a guessable-path risk.

Code Examples

Note is a small NSSecureCoding-conformant model with a stable NSUUID-based identifier. NotesStore owns the array exclusively, persists with NSKeyedArchiver/NSKeyedUnarchiver, exposes both a delegate for the one strongly-typed observer and NSNotificationCenter for any number of loosely-coupled observers, and implements search with a declarative NSPredicate rather than manual string scanning. NotesStore's designated initializer takes an NSFileManager and a file NSURL rather than reaching for NSFileManager.defaultManager and a hardcoded path internally — this is what lets a unit test construct a NotesStore pointed at a temporary directory instead of the app's real Application Support folder, exactly the dependency-injection seam the Testing and Mocking and Dependency Injection lessons recommend.

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title CONTAINS[cd] %@ OR body CONTAINS[cd] %@", searchText, searchText];

The fetch method composes two NSPredicates with NSCompoundPredicate and applies an NSSortDescriptor plus a bounded fetchBatchSize, all parameterized rather than string-concatenated. The write method demonstrates the mandatory pattern for Core Data background work: create a background context, do everything to it inside performBlock:, save it, then merge the save notification back into the main/view context on the main queue before touching the UI.

[backgroundContext performBlock:^{ ... NSError *saveError = nil; BOOL saved = [backgroundContext save:&saveError]; ... }];

Common mistake: Calling insertNewObjectForEntityForName: or save: on a background NSManagedObjectContext directly from an arbitrary thread, outside a performBlock:/performBlockAndWait: block, violates Core Data's threading contract and produces intermittent, hard-to-reproduce corruption or crashes.

PXUserProfileService is written specifically for Swift interoperability: NS_ASSUME_NONNULL_BEGIN/END plus a targeted nullable on the one value that can genuinely be absent, NS_SWIFT_NAME reshaping both the class and the fetch method into idiomatic Swift, and NSArray<NSString *> generics on the model so Swift sees [String] instead of [Any]. The commented Swift call site shows how this reads from the other side of the bridging header without needing a Swift compiler in this pipeline.

• (void)fetchProfileForUserID:(NSString *)userID completion:(...)completion NS_SWIFT_NAME(fetchProfile(userID:completion:));

The BEFORE version is manually reference counted, keys its cache with an unsafe char *, builds a predictable unsandboxed file path by string formatting, never checks whether contentsAtPath: returned nil before handing the data to UIImage, and reaches for a hardcoded singleton with no way to substitute a fake filesystem in a test. The AFTER version drops all manual retain/release/autorelease (ARC), uses a strongly-typed NSCache<NSString *, UIImage *> with lightweight generics, takes its NSFileManager and cache-directory NSURL through the initializer for testability, checks every optional value before use, marks the lookup return type nullable, and replaces the guessable file path with a hashed filename.

@property (nonatomic, strong) NSCache<NSString *, UIImage *> *cache;

Common mistake: The BEFORE code's [[img retain] autorelease] and manual [cache release] in dealloc are exactly the kind of manual reference-counting bookkeeping ARC eliminates entirely — under ARC this code wouldn't even compile as written, which is itself a useful signal during modernization: if converting a file to ARC produces a pile of 'ARC forbids explicit message send of release' errors, that's ARC pointing at precisely the lines that needed to change.

// Note.h
NS_ASSUME_NONNULL_BEGIN

@interface Note : NSObject <NSSecureCoding>

@property (nonatomic, readonly, copy) NSString *noteID;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;
@property (nonatomic, readonly, strong) NSDate *createdAt;
@property (nonatomic, strong) NSDate *modifiedAt;

- (instancetype)initWithTitle:(NSString *)title body:(NSString *)body;

@end

NS_ASSUME_NONNULL_END

// Note.m
@implementation Note

+ (BOOL)supportsSecureCoding { return YES; }

- (instancetype)initWithTitle:(NSString *)title body:(NSString *)body {
    self = [super init];
    if (self) {
        _noteID = [[NSUUID UUID] UUIDString];
        _title = [title copy];
        _body = [body copy];
        _createdAt = [NSDate date];
        _modifiedAt = [NSDate date];
    }
    return self;
}

- (nullable instancetype)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        _noteID = [coder decodeObjectOfClass:[NSString class] forKey:@"noteID"];
        _title = [coder decodeObjectOfClass:[NSString class] forKey:@"title"];
        _body = [coder decodeObjectOfClass:[NSString class] forKey:@"body"];
        _createdAt = [coder deco
…
// Book+CoreDataClass.h (generated subclass, simplified)
@class Author;

NS_ASSUME_NONNULL_BEGIN

@interface Book : NSManagedObject

@property (nonatomic, copy) NSString *title;
@property (nonatomic) int32_t pageCount;
@property (nonatomic, strong) NSDate *dateAdded;
@property (nonatomic, strong, nullable) Author *author;

@end

NS_ASSUME_NONNULL_END

// LibraryController.m — fetching with predicate + sort descriptor
- (NSArray<Book *> *)booksAddedInLastYearMatching:(NSString *)searchText
                                         inContext:(NSManagedObjectContext *)context {
    NSFetchRequest<Book *> *request = [Book fetchRequest];

    NSDate *oneYearAgo = [[NSCalendar currentCalendar] dateByAddingUnit:NSCalendarUnitYear
                                                                    value:-1
                                                                   toDate:[NSDate date]
                                                                  options:0];

    NSPredicate *recentPredicate = [NSPredicate predicateWithFormat:@"dateAdded >= %@", oneYearAgo];
    NSPredicate *textPredicate = nil;
    if (searchText.length > 0) {
        textPredicate = [NSPredicate predicateWithFo
…
// PXUserProfile.h
NS_ASSUME_NONNULL_BEGIN

@interface PXUserProfile : NSObject

@property (nonatomic, copy, readonly) NSString *userID;
@property (nonatomic, copy, readonly) NSString *displayName;
@property (nonatomic, copy, readonly) NSArray<NSString *> *interests;

- (instancetype)initWithUserID:(NSString *)userID
                    displayName:(NSString *)displayName
                      interests:(NSArray<NSString *> *)interests NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;

@end

NS_ASSUME_NONNULL_END

// PXUserProfileService.h
NS_ASSUME_NONNULL_BEGIN

NS_SWIFT_NAME(UserProfileService)
@interface PXUserProfileService : NSObject

@property (nonatomic, strong, readonly, nullable) PXUserProfile *cachedProfile;

- (void)fetchProfileForUserID:(NSString *)userID
                    completion:(void (^)(PXUserProfile * _Nullable profile,
                                          NSError * _Nullable error))completion
    NS_SWIFT_NAME(fetchProfile(userID:completion:));

@end

NS_ASSUME_NONNULL_END

// PXUserProfileService.m
@implementation PXUserProfileService {
    NSURLSession *_session;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        
…
// BEFORE — LegacyImageCache.m (MRC, unsafe, untestable, no nullability)
@implementation LegacyImageCache

+ (LegacyImageCache *)sharedCache {
    static LegacyImageCache *instance = nil;
    if (instance == nil) {
        instance = [[LegacyImageCache alloc] init];
    }
    return instance;
}

- (id)init {
    self = [super init];
    if (self) {
        cache = [[NSMutableDictionary alloc] init];   // ivar, MRC-retained
    }
    return self;
}

- (void)dealloc {
    [cache release];
    [super dealloc];
}

- (UIImage *)imageForKey:(const char *)key {
    NSString *nsKey = [NSString stringWithCString:key encoding:NSASCIIStringEncoding];
    UIImage *img = [cache objectForKey:nsKey];
    if (img) {
        return [[img retain] autorelease];
    }
    NSString *path = [NSString stringWithFormat:@"/tmp/%s.png", key];   // predictable, unsandboxed path
    NSData *data = [[NSFileManager defaultManager] contentsAtPath:path];
    UIImage *loaded = [[UIImage alloc] initWithData:data];   // no nil check on data
    [cache setObject:loaded forKey:nsKey];
    return [loaded autorelease];
}

@end

// AFTER — LegacyImageCache.m (ARC, safe, nullability-annotated, generics, DI, testable)
NS_A
…

Notes App Object Relationships and Notification Flow

The delegate is a focused, strongly-typed one-to-one contract with the list view controller; NSNotificationCenter is a broadcast fan-out any number of loosely-coupled observers (like a badge counter) can subscribe to without NotesStore knowing they exist.

┌───────────────────┐
        │  NotesListVC       │◄───────────── delegate callbacks ─────────┐
        │  (delegate)        │                                            │
        └─────────┬──────────┘                                            │
                   │ observes                                             │
                   ▼                                                      │
   ┌────────────────────────────┐   addNote: / updateNote: /  ┌───────────┴──────────┐
   │ NSNotificationCenter        │◄──────────────────────────  │     NotesStore        │
   │ (broadcast: DidChange)      │   deleteNoteWithID:          │  (owns Note array)    │
   └──────────────┬──────────────┘                              └───────────┬──────────┘
                   │ notified                                                │ persists
                   ▼                                                         ▼
        ┌───────────────────┐                                    ┌───────────────────┐
        │ BadgeCounterView   │                                    │  Disk (Application │
        │ (loosely coupled)  │                                    │  Support / .archive)│
        └──
…

Why These Four Shapes Show Up Constantly in Real Objective-C Codebases

Almost every long-lived Objective-C app eventually needs at least one of these four shapes. A searchable, persisted list of user-authored content (Notes) is the shape behind todo apps, journaling apps, and most 'my stuff' screens. A relational, queryable local database (Core Data App) is the shape behind anything with real data modeling needs — catalogs, offline-first apps, anything with more than one connected entity. A mixed Objective-C/Swift codebase (Mixed App) is simply the reality of most apps older than a few years, where new features get written in Swift on top of a stable Objective-C core that nobody wants to rewrite wholesale. And legacy modernization (Legacy Modernization) is, frankly, one of the single most common tasks an Objective-C engineer is actually hired to do in 2026 — far more teams are maintaining and incrementally modernizing existing Objective-C than starting brand-new Objective-C projects from scratch, which is exactly why this course treats it as a first-class project rather than an afterthought.

Referring to notes by array index (e.g. self.notes[indexPath.row]) when posting delete notifications or delegate callbacks.

Always identify a note by its stable noteID (NSUUID-backed) in notifications, delegate callbacks, and lookups.

Array indices shift whenever the collection is sorted, filtered, or mutated elsewhere; a stable identifier is the only safe way to refer to 'this specific note' across asynchronous or delayed callbacks.

Building an NSPredicate by concatenating raw user search text directly into a format string, e.g. stringWithFormat:@"title CONTAINS '%@'", searchText.

Use NSPredicate predicateWithFormat: with %@ substitution, letting NSPredicate handle argument insertion safely: [NSPredicate predicateWithFormat:@"title CONTAINS[cd] %@", searchText].

Manually splicing user input into a predicate string re-introduces the exact class of bug parameterized queries exist to prevent, and it silently breaks on any search text containing a quote character.

Saving or inserting objects on a Core Data background NSManagedObjectContext directly from a GCD block without wrapping the work in performBlock:.

Do every operation on a given NSManagedObjectContext inside that context's own performBlock: (or performBlockAndWait:), even on a background context.

Each NSManagedObjectContext has its own confinement queue; touching it from any other queue violates Core Data's threading contract and produces intermittent corruption that is very hard to reproduce later.

In the Swift-mixed project, importing the Swift-generated -Swift.h header back into the Objective-C service or model layer 'just in case it's needed later.'

Keep the dependency direction one-way: Swift imports Objective-C via the bridging header; Objective-C service/model code never imports the generated Swift header.

Letting the arrow point both ways couples the stable Objective-C core to whatever Swift feature code happens to exist, defeating the whole point of keeping the older layer independent and reusable.

During legacy modernization, converting MRC to ARC and adding nullability/generics/DI all in one enormous, unreviewable commit.

Modernize in the staged order this project used: ARC conversion first, then safer APIs, then nullability, then generics, then dependency injection, then a warnings/performance/security pass — each as its own reviewable step.

A single giant rewrite commit is nearly impossible to review safely and makes it hard to bisect a regression back to the specific change that caused it; staged commits each do one clearly-scoped thing.

In the Notes application, why does NotesStore identify notes by a NSUUID-based noteID rather than by their position in the array? A. NSUUID strings sort alphabetically, which speeds up search B. Array positions shift whenever the collection is sorted, filtered, or mutated, so an index is not a stable reference to a specific note C. NSSecureCoding requires every archived object to have a UUID property D. UIKit table views cannot use array indices as identifiers under any circumstances

B. Array positions shift whenever the collection is sorted, filtered, or mutated, so an index is not a stable reference to a specific note — A stable identifier is required because notifications, delegate callbacks, and lookups can happen after the array has already changed shape; only an identifier tied to the object itself remains valid.

Why does the Notes project use both a delegate protocol and NSNotificationCenter, rather than picking just one? A. Apple requires every model class to support both mechanisms B. NSNotificationCenter is deprecated so the delegate is kept as a fallback C. They serve different purposes: the delegate is a focused, strongly-typed one-to-one contract, while NSNotificationCenter is a loosely-coupled broadcast to any number of observers D. Delegates cannot be used with NSMutableArray-backed models

C. They serve different purposes: the delegate is a focused, strongly-typed one-to-one contract, while NSNotificationCenter is a loosely-coupled broadcast to any number of observers — The delegate gives the one interested object (the list view controller) precise, synchronous callbacks, while notifications let any number of unrelated objects (like a badge counter) react without NotesStore needing to know about them.

In the Core Data project's addBookWithTitle:pageCount:author:completion: method, why is all the Core Data work wrapped inside [backgroundContext performBlock:^{ ... }]? A. performBlock: is only needed for fetch requests, not inserts or saves B. Each NSManagedObjectContext is confined to its own queue, and every operation on it must run inside that context's performBlock:/performBlockAndWait: to respect that confinement C. performBlock: automatically retries the save if it fails D. It is purely a style convention with no functional effect

B. Each NSManagedObjectContext is confined to its own queue, and every operation on it must run inside that context's performBlock:/performBlockAndWait: to respect that confinement — Core Data enforces a strict one-context-per-queue rule; performBlock: dispatches the block onto the context's own private queue, which is required for any interaction with that context.

In the Swift Mixed Application project, what does NS_SWIFT_NAME(fetchProfile(userID:completion:)) accomplish on the Objective-C method fetchProfileForUserID:completion:? A. It changes the method's actual Objective-C selector to fetchProfile B. It renames the method only for Swift callers, so Swift code reads service.fetchProfile(userID:completion:) while the Objective-C selector is unchanged C. It makes the method unavailable from Objective-C entirely D. It converts the completion block into a Swift async function automatically

B. It renames the method only for Swift callers, so Swift code reads service.fetchProfile(userID:completion:) while the Objective-C selector is unchanged — NS_SWIFT_NAME retargets how a symbol is imported into Swift without altering its Objective-C selector, letting the same method read idiomatically from both languages.

In the Legacy Modernization project, why must the MRC-to-ARC conversion happen before adding nullability annotations, generics, and dependency injection? A. Nullability annotations are only valid in files that have never used MRC, even historically B. ARC and MRC memory-management calls (retain/release/autorelease) cannot coexist correctly within the same file, so the file must be converted to ARC before other modernization steps proceed on stable ground C. The order doesn't actually matter; it was chosen arbitrarily for the lesson D. Generics require ARC to be enabled project-wide before Xcode will allow angle-bracket syntax

B. ARC and MRC memory-management calls (retain/release/autorelease) cannot coexist correctly within the same file, so the file must be converted to ARC before other modernization steps proceed on stable ground — A single file must be consistently MRC or ARC — mixing manual retain/release calls with ARC's automatic insertion in the same file is not supported, so ARC conversion is the necessary first, foundational step.

What specific problem does replacing LegacyImageCache's raw NSFileManager.defaultManager usage with an injected NSFileManager parameter solve? A. It makes file reads faster by caching NSFileManager instances B. It allows the class to be unit tested by substituting a fake or sandboxed filesystem instead of touching the real one C. It is required for NSCache to function correctly D. It automatically encrypts any data written to disk

B. It allows the class to be unit tested by substituting a fake or sandboxed filesystem instead of touching the real one — Injecting NSFileManager (and the cache directory URL) creates a seam that tests can use to substitute controlled, isolated filesystem behavior, which is exactly the dependency-injection technique the Testing and Mocking and Dependency Injection lessons teach.

Extend the Notes application from Project 5 with tagging: add a tags property (an NSArray<NSString *> *) to Note, update NSSecureCoding encode/decode to include it, add a notesMatchingTag: method to NotesStore built with NSPredicate (using the ANY collection operator against each note's tags array), and post a distinct NotesStoreDidChangeNotification userInfo entry (@{@"reason": @"tagged"}) so an observer can distinguish a tagging change from a content edit without needing a new notification name.

Note gains @property (nonatomic, copy) NSArray<NSString *> *tags;, encoded/decoded the same way title and body are, defaulting to @[] if absent on decode. NotesStore adds - (NSArray<Note *> *)notesMatchingTag:(NSString *)tagName { NSPredicate *p = [NSPredicate predicateWithFormat:@"ANY tags ==[cd] %@", tagName]; return [self.mutableNotes filteredArrayUsingPredicate:p]; }. updateNote: (or a new updateTagsForNote:) posts the notification with a userInfo dictionary carrying the reason key, and any observer that cares about tagging specifically checks notification.userInfo[@"reason"] before reacting, while observers that don't care can ignore userInfo entirely and keep working exactly as before.

For the ANY predicate, the format looks like [NSPredicate predicateWithFormat:@"ANY tags ==[cd] %@", tagName] — ANY applies the comparison to each element of the collection property and matches if any element satisfies it.