Objective-C standards
Criteria verified as of August 2026. Re-verify on the web before committing to anything (§8).
1. Scope and triggers
Applies to the Objective-C and Objective-C++ language: .m/.mm files and headers with @interface/@property, ARC and property qualifiers, nullability annotations, lightweight generics, the dynamic runtime (objc_msgSend, categories, swizzling, KVO/KVC), designing headers for consumption from Swift, GCD from Objective-C, tooling (xcodebuild, the clang analyzer, clang-format, sanitizers) and the target's dependency management.
The real case is maintenance and interoperability, not new code. For new code on Apple platforms the answer is Swift; writing a new module in Objective-C requires written justification (§7). This skill exists for two concrete reasons: (a) there is an enormous amount of Objective-C in production that has to be touched without breaking it, and (b) the Swift↔Objective-C boundary is where things break — badly annotated nullability, id without generics, retain cycles Swift cannot see, NSError** that Swift translates into throws, and the dynamic runtime the Swift compiler cannot verify. The axis of this document is: make the existing Objective-C safe to consume from Swift and safe to delete little by little.
Not applicable: see mobile-standards (the app is theirs: architecture, navigation, app lifecycle, permissions and Info.plist, signing, App Store/TestFlight distribution, accessibility, secure storage as policy, MASVS, crash reporting; and Swift is theirs — here only interoperability from the Objective-C side: how the header is annotated and designed so Swift can consume it without exploding), c-standards and cpp-standards (Objective-C is a superset of C and .mm mixes in C++: the C and C++ inside a .m/.mm remain subject to their skills — manual memory management, undefined behaviour, integers, warning flags, sanitizers, RAII and the STL; what is specific to the Objective-C runtime, ARC and the bridge to Swift belongs here), assembly-standards (a .mm can contain inline asm: the criteria for when and how assembly is written are theirs), cryptography-pki-standards (algorithm choice, mode and key lifecycle; here only their use through the platform APIs), identity-access-management-standards (OAuth/OIDC flows on the IdP side), appsec-standards (threat modelling and process), cicd-standards (pipeline design; here only which tool is run), secrets-management-standards and vulnerability-management-standards (patching SLAs and triage of dependency CVEs), smalltalk-standards (a direct lineage: Objective-C's message runtime comes from there; living Smalltalk — Pharo, Squeak, GemStone/S, VAST — is theirs).
2. Default decisions and toolchain
Verify the latest version on the web before pinning it in a real project (§8). What follows is the status verified as of August 2026.
| Decision |
Default |
Justifiable alternative |
Reason |
| Language for new code |
Swift |
Objective-C only if the module must be consumed from existing C/C++, if the dynamic runtime is a genuine requirement, or if the team maintains a public ObjC framework with a stable contract |
Verified: Apple publishes no language changes to Objective-C; what changes is the module system and the build (e.g. Explicitly Built Modules in Xcode 16 for C/ObjC). No deprecation announced, but no evolution either: it is a stable and static language |
| Toolchain |
Xcode 26.x (current line; 26.6 RC with Swift 6.3 as of Aug 2026) |
The minimum version App Store Connect demands |
Verified: since 28 Apr 2026 every upload to App Store Connect requires a build with the iOS 26 SDK or later (Xcode 26+), with no grace period |
| ARC |
Mandatory (-fobjc-arc) in every file |
MRR (-fno-objc-arc) only per file and with a written reason (interoperability with very old Core Foundation, generated code) |
Manual MRR is a source of leaks and over-releases that no tool compensates for |
| Dependencies |
Swift Package Manager |
A vendored XCFramework with a checksum when the vendor does not publish to SPM |
Verified: the CocoaPods trunk goes read-only on 2 Dec 2026 — "no new versions or pods will be added to trunk", though "this will keep all existing builds working" as long as GitHub and jsDelivr exist. That is: current builds keep working, but there will be no updates and no security patches through trunk. Migrating is planned work, not a same-day emergency |
| Carthage |
Do not adopt |
Keep existing usage until migrated |
Verified through the Atom feed: latest release 0.40.0 (2024-09-09). No releases since: not a base for a new project |
| Mixing Swift/ObjC in SPM |
Separate targets (FooObjC clang + Foo swift) |
Binary target / XCFramework |
Verified: SE-0403 (mixed language targets) is "Returned for Revision", not accepted. In SPM a target is either Swift or C-based, not both. In an Xcode target they do coexist (bridging header + generated header) |
| Tests |
XCTest for Objective-C code |
Swift Testing for new tests written in Swift |
Verified: Swift Testing supports Swift only; XCTest remains the only path for tests written in Objective-C, UI automation (XCUITest) and performance tests (XCTMetric). XCTest is not deprecated. A single test target accepts both worlds, but each test lives in exactly one |
| Formatting |
clang-format with a versioned .clang-format |
— |
clang-format understands Objective-C; without a versioned file the style gets re-argued in every PR |
Toolchain rules:
- The project declares its minimum deployment version and compiles with
-Werror in CI. The C warning set applies in full (see c-standards), plus the Objective-C-specific ones: -Wobjc-missing-property-synthesis, -Wdirect-ivar-access, -Wnullable-to-nonnull-conversion, -Wstrict-selector-match, -Wundeclared-selector, -Wdeprecated-implementations, -Wobjc-interface-ivars.
-Wnullable-to-nonnull-conversion is non-negotiable: it is the warning that stops Swift receiving a nil in a let x: String and aborting with a crash that has no useful trace.
- A reproducible build through
xcodebuild from CI with explicit -scheme/-destination; no "it builds in my Xcode". Configuration in versioned .xcconfig files, not in the project UI.
3. The language: lifecycle, the Swift boundary and the runtime
ARC and lifecycle
- Qualifiers:
strong (the default for objects), weak for back references and delegates, copy mandatory for NSString/NSArray/NSDictionary/blocks in public properties (the caller can pass the mutable subclass and mutate it behind your back), assign only for scalars. unsafe_unretained is vetoed except for interoperability with an API that requires it, and with a comment: it is not nilled out on release, so it produces a use-after-free instead of nil.
- Retain cycles in blocks: a block captures
self as strong. The mandatory pattern when the block is retained by the object itself (or by something it owns):
__weak __typeof(self) weakSelf = self;
self.completion = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf; // once only, on entry
if (!strongSelf) { return; } // mandatory early exit
[strongSelf doWork];
};
Without the strongSelf, every access to weakSelf can become nil halfway through the block and the behaviour turns non-deterministic. Check for nil and return, do not carry on "just in case".
delegate is always weak (or a documented unsafe_unretained if the protocol is not object-based). A strong delegate is a guaranteed cycle.
@autoreleasepool is mandatory inside loops that create temporary objects in volume (parsing, images, conversions): without it the memory peak grows until the end of the run-loop cycle and the system kills the process.
- Core Foundation: explicit ownership transfer with
__bridge/__bridge_transfer/__bridge_retained or the CFBridgingRetain/CFBridgingRelease macros. A bare __bridge where __bridge_transfer was needed is a leak the analyzer does detect: it is not silenced.
dealloc: deregistration only (KVO/NSNotificationCenter observers if the lifecycle requires it) and timer invalidation. No business logic and no calls to overridable methods; the object is already half destroyed.
nil as a valid receiver — the structural trap
Sending a message to nil is legal and returns zero/nil/a zeroed struct. Consequence: an initialisation bug does not fail, it propagates silently to a distant point where the symptom bears no relation to the cause.
- Do not use a method's return value as proof that the receiver existed. If a
nil at that point is a programming error, there are NSParameterAssert/NSAssert (which disappear with NS_BLOCK_ASSERTIONS, so they are no good for validating external input) or an explicit check.
- Be careful with non-object return types: for a
nil receiver, returned floating-point values and structs are defined as zero in Apple's current ABIs, but depending on that is fragile; check first.
Properties and atomic
nonatomic by default. atomic does not give thread safety: it guarantees that an individual get/set does not return a half-written value, nothing more. An array.count followed by an indexed access is still a race. Real synchronisation is designed (a serial queue, a lock, immutability), not obtained by writing atomic.
- Accessing ivars: through the property, not directly (
-Wdirect-ivar-access), except in init and dealloc, where direct access is the correct thing (setters can have effects and the object is not in a valid state).
- Every public property has explicitly declared semantics (
nonatomic, copy, readonly…). No @property NSString *name; without qualifiers.
Nullability and lightweight generics — a requirement for consumption from Swift
This is not cosmetic: it decides whether Swift sees String or String!. An unannotated id arrives in Swift as Any!, and an implicitly-unwrapped optional that turns out to be nil aborts the process.
- Every public header is wrapped in
NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END, explicitly marking what can be nil with nullable (and null_resettable where applicable). An unannotated header is debt paid by the consumer.
- Lightweight generics are mandatory on exposed collections:
NSArray<NSString *> *, NSDictionary<NSString *, NSNumber *> *. Without them Swift receives [Any] and all the typing on the Swift side degrades into casts.
instancetype in constructors, never id. NS_DESIGNATED_INITIALIZER on the designated initialiser and NS_UNAVAILABLE on those that must not be used: without that, Swift inherits initialisers that leave the object invalid.
- Enumerations with
NS_ENUM/NS_OPTIONS, typed strings with NS_STRING_ENUM/NS_TYPED_ENUM. Never a bare C enum in a public API.
- The annotation is a promise the compiler only partially verifies: keep
-Wnullable-to-nonnull-conversion on, and at boundaries receiving external data (JSON, disk, network) check at runtime before returning anything declared nonnull.
Designing the header with the Swift consumer in mind
NS_SWIFT_NAME to give the idiomatic Swift name when the automatic translation produces something unreadable.
NS_REFINED_FOR_SWIFT when the Objective-C API cannot be idiomatic (out-params, pointers, boxed NSNumber): the symbol is imported with a __ prefix and wrapped in a Swift extension. That is the correct route, not changing the ObjC API to please Swift.
NS_NOESCAPE on non-escaping blocks, NS_SWIFT_SENDABLE on types safe to cross isolation and NS_SWIFT_UI_ACTOR / NS_SWIFT_NONISOLATED where isolation matters — verify availability and exact semantics in the SDK of the version being used (§8): these annotations arrived across versions and their effect depends on the target's Swift concurrency mode.
NS_ERROR_ENUM for error domains, so Swift imports them as a typed Error.
NS_SWIFT_UNAVAILABLE for what must not cross; API_AVAILABLE/API_DEPRECATED on every API with a version condition.
- The module is exposed through
module.modulemap, not a bridging header, when it is a consumable framework. The bridging header is for the app's code, and it does not exist in the other direction: Swift is seen from ObjC through the generated header (<Product>-Swift.h), and only what is marked @objc/@objcMembers and inherits from NSObject.
Categories
- They exist to add methods to a class you do not control. Every category method on someone else's class carries a project prefix (
px_doThing): the runtime has no namespaces and two categories declaring the same selector collide silently, with the last loaded winning — an order that is non-deterministic between libraries.
- FORBIDDEN to override an existing method from a category. The behaviour is undefined and it breaks the base class, other categories and the subclasses. If behaviour needs changing: subclass, composition or delegation.
- Categories with properties: they require associated objects (
objc_setAssociatedObject), which are hidden state with no dealloc of their own. Use them sparingly and document them.
- Class extensions (
@interface Foo () in the .m) are the correct mechanism for private members, including redeclaring a public readonly property as readwrite.
Dynamic runtime
objc_msgSend is the dispatch mechanism: every send is dynamic, there is no devirtualisation. Practical implication: the compiler can warn you about almost nothing regarding the receiver, so the static typing of the headers is your only net.
- FORBIDDEN: method swizzling in application code except in a justified, documented case (typically third-party instrumentation, and even then an alternative is preferred). It is a classic source of irreproducible failures: it depends on load order, breaks with every SDK update, and two libraries swizzling the same selector corrupt each other. If it is done anyway: only in
+load or with dispatch_once, on your own selectors, always invoking the original IMP, and with a written owner and review date.
respondsToSelector: / performSelector: are escape hatches, not design. performSelector: with more than two arguments or a non-object return is incorrect (ARC does not know the signature and -Warc-performSelector-leaks says so). Default replacement: a typed block or a protocol with @optional.
NSInvocation and hand-written objc_msgSend: only in infrastructure code with its own tests; on arm64 they require casting objc_msgSend to the exact signature or the result is garbage.
- KVO/KVC are fragile: keys as strings with no compile-time checking,
observeValueForKeyPath: untyped, and removing the observer at exactly the right moment (an observer alive after the observed object's dealloc is a guaranteed crash). Criterion: use the token-based API (-addObserverForKeyPath:…, which returns NSKeyValueObservation in Swift, or store the context and deregister in dealloc), prefer notifications or explicit callbacks, and never KVO on objects whose lifecycle you do not control. valueForKey: with a constructed string is, additionally, attack surface.
Errors
- Canonical pattern:
- (BOOL)doThing:(NSError **)error or an object return with nil on failure. The failure indicator is the return value, not error: check the return first and only then read *error. Writing to *error without checking the pointer is not NULL is a crash.
- Swift translates this pattern into
throws only if the signature follows the convention (last parameter NSError **, NS_SWIFT_NOTHROW to exclude it). Changing the signature breaks the Swift side even though the ObjC compiles.
- Objective-C exceptions are not control flow: they represent programming errors (index out of range, unrecognised selector) and are not safe with ARC — unwinding the stack with ARC leaks memory except with
-fobjc-arc-exceptions, which has a cost. @try/@catch only to wrap system APIs documented as throwing (some of NSFileHandle, KVC) and at the C++ boundary in a .mm. FORBIDDEN: @throw for expected errors.
Concurrency (GCD from Objective-C)
- Default model: one serial queue per unit of mutable state. A serial queue is a lock that also orders; it is cheaper to reason about than
NSLock scattered around.
- Concurrent queues only with an explicit reader/writer pattern (
dispatch_barrier_async for writes) and a concurrency test under TSan.
- FORBIDDEN:
dispatch_sync onto the main queue from the main queue: an immediate deadlock. By extension, dispatch_sync onto any queue from that same queue. Operational rule: dispatch_sync only towards a queue you know for certain is not you, and never while holding another lock.
- All UI work on the main queue; all I/O off it.
dispatch_after is not a synchronisation mechanism.
- Interop with Swift's
async/await: an ObjC API with a completion block as the last parameter is imported as async — the signature is a contract. The block must be called exactly once on every path, including the error and cancellation ones: zero calls hang the Swift task forever and two calls are a runtime failure.
volatile does not synchronise (see c-standards). For counters, C11 atomic_* or os_unfair_lock; OSSpinLock is deprecated because of priority inversion.
Objective-C++ (.mm)
- Used only to bridge to an existing C++ library, and it is isolated: the C++ does not leak into public Objective-C headers (a header with
std:: forces every consumer to be .mm). Pattern: a clean ObjC header, a .mm implementation with a hidden C++ implementation struct (pImpl).
- ARC and C++ coexist: an ObjC object as a member of a C++ class needs an explicit
__strong/__weak and the class stops being trivially copyable. id inside a union or in POD types requires care.
- Exceptions: a C++ exception crossing into an Objective-C frame with ARC leaks resources; catch it at the bridge's edge.
- The C++ in a
.mm is subject to cpp-standards (RAII, the STL, UB, flags). Verified: there is a known interaction between C++ standard modules and Objective-C++ depending on the C++ language mode — check the release notes of the specific Xcode version before enabling C++ modules in a .mm target (§8).
4. Quality: analysis, tests and CI gates
- Clang Static Analyzer (
xcodebuild analyze or Analyze in Xcode) is the main gate and it is specifically good at Objective-C: ARC/CF leaks, over-release, nil receivers, use of uninitialised values. Zero findings in your own tree; a suppression requires a // NOLINT-equivalent with a reason, not deleting the warning.
clang-tidy over compile_commands.json for the C/C++ inside the .m/.mm (bugprone-*, cert-*).
clang-format --dry-run --Werror in CI, with a versioned .clang-format. Mass reformatting in a separate commit, recorded in .git-blame-ignore-revs.
- Tests: XCTest for the Objective-C code; the boundary test is mandatory — for every public API consumed from Swift, a test written in Swift that exercises it. It is the only way to check that nullability, generics and the
NSError** translation are what you think they are.
- Edge coverage:
nil in every parameter annotated nullable, an empty collection, an empty string, boundary values, the completion block invoked on every path, and the error path of every NSError**.
- Sanitizers in Xcode, in separate schemes: Address Sanitizer (+ Detect use of stack after return), Thread Sanitizer and Undefined Behavior Sanitizer. ASan and TSan are not combined. Also Zombie Objects and Malloc Scribble to diagnose over-release in inherited MRR code. Never enabled in a distribution build.
- Instruments: Leaks and Allocations over the critical flow before releasing a change that touches lifecycle; a retain cycle does not show up in unit tests.
- Minimum CI gate (everything breaks the build): formatting → build with
-Werror (the warnings in §2) → xcodebuild analyze with no findings → ObjC unit tests + Swift boundary tests → tests under ASan+UBSan → dependency SCA/SBOM.
5. Stack security
NSKeyedUnarchiver without an allowed class list is code execution. FORBIDDEN: +unarchiveObjectWithData: and the whole unsafe family: deserialising an arbitrary archive instantiates whichever classes the attacker names. The only replacement: NSSecureCoding with +unarchivedObjectOfClass:fromData:error: / +unarchivedObjectOfClasses:fromData:error:, with requiresSecureCoding = YES and the allowed class list as narrow as possible. Every serialisable class implements NSSecureCoding (not just NSCoding) and its +supportsSecureCoding genuinely returns YES, not by copy-paste.
- The same criterion applies to any deserialisation:
NSJSONSerialization with type validation of every field before use (an NSDictionary arriving with an NSNull where you expected an NSString is an unrecognized selector in production). Never trust the shape of the JSON.
- Format strings:
-Wformat=2 -Werror=format-security on. FORBIDDEN to pass user input as a format string to +stringWithFormat:, NSLog, -appendFormat: or predicates. It is arbitrary memory read/write. The same goes for NSPredicate predicateWithFormat: built by concatenation: direct injection; use substitution with %@/%K.
- On-device storage: Keychain for credentials, tokens and keys, with the most restrictive protection class the case allows (
…ThisDeviceOnly when it must not sync) and with biometric SecAccessControl where applicable. NSUserDefaults is not secure storage: it is a plist in the clear, it is read from a backup and in some scenarios it survives uninstallation. Files with NSFileProtectionComplete and isExcludedFromBackup where appropriate. The policy of what is stored and with what classification belongs to mobile-standards; here, the correct API.
- A client binary does not keep secrets. Every embedded string — API keys, tokens, OAuth client secrets, internal URLs, signing credentials — is in the user's hands:
strings over the .app finds it in seconds. Obfuscation is not protection. If an operation requires a secret, it runs on the server. What is protected is what the device generates (a key pair in the Secure Enclave, an ephemeral token in the Keychain).
- Crypto: platform APIs (CryptoKit from Swift,
SecKey/CommonCrypto from ObjC). FORBIDDEN to implement your own cryptography; compare secrets in constant time, never memcmp or isEqualToString:.
- Logging:
os_log with a format string and redaction of dynamic values by default (%{private}@ for everything not explicitly public). NSLog with domain objects in a release build is a PII leak into the device's log system.
- Dependencies: exact pinning (version + checksum in
Package.resolved, a mandatory checksum on binaryTarget), an SBOM of the artifact and SCA. Any pod that stops receiving updates after Dec 2026 becomes security debt with a date, not a stable dependency.
6. Performance and operability
- Dynamic dispatch has a cost, but it is almost never the bottleneck: measure with Instruments (Time Profiler) before touching anything. Optimising messages by hand (a cached
IMP) requires a measured number and a test.
- Memory peaks:
@autoreleasepool in loops (§3) and NSCache instead of NSMutableDictionary for caches (it responds to memory pressure). System memory warnings are acted upon.
- Crashes: dSYM archived per build with its UUID; a crash without symbols is wasted time. Unrecognised selectors,
NSInvalidArgumentException from nil, and retain cycles are the three dominant families in Objective-C codebases — they get instrumented and counted.
- Start-up:
+load runs before main and delays the whole app's launch; prefer +initialize (lazy) or explicit initialisation. A +load in a third-party library is a legitimate reason to question the dependency.
7. Sustainability and prohibitions
Strategy, not just style. The Objective-C in a living codebase is treated as debt with an amortisation plan: it gets annotated (nullability and generics) before being touched, covered with tests from Swift, and replaced by Swift from the inside out — the internal implementation first, keeping the ObjC header as a facade — so consumers do not break. Migrating a class without a boundary test is trading a known bug for an unknown one.
Cadence: move up Xcode at least with every major line and before App Store Connect demands it (verified: the Apr 2026 cut-off had no grace period). Every SDK bump is tested in CI against the previous one: framework behaviour changes, not language changes, are what break things.
Dependency migration: CocoaPods' exit has a known date (2 Dec 2026 for trunk). Plan the migration to SPM/XCFramework before that date, not after; whatever remains in pods after it stops receiving new versions through trunk.
Conscious debt: every shortcut leaves // TODO(user): reason — issue #N. Every warning or analyzer suppression carries a reason and a review date.
FORBIDDEN (an exception requires written justification and approval):
- ❌ Writing a new module in Objective-C when Swift is viable.
- ❌ Method swizzling in application code; swizzling other people's selectors, UIKit/Foundation methods, or doing it without invoking the original
IMP.
- ❌ Overriding an existing method from a category; category methods on other people's classes without a project prefix.
- ❌ A public header without
NS_ASSUME_NONNULL_BEGIN/END, without lightweight generics on collections, or with id where a type would fit.
- ❌
unsafe_unretained without justification; a delegate declared strong; an object property with no explicit semantics; a public NSString/collection without copy.
- ❌ A block capturing
self as strong while retained by self; weakSelf used without promoting to strongSelf and without a nil check.
- ❌ A loop creating temporary objects in volume without
@autoreleasepool.
- ❌
atomic presented as synchronisation; shared mutable state with no serial queue and no lock.
- ❌
dispatch_sync onto the current queue (deadlock); UI work off the main queue; network or disk I/O on the main queue.
- ❌ A completion block that is not invoked exactly once on every path.
- ❌
@throw/@try as control flow; ObjC exceptions for expected errors; writing to *error without checking the pointer; reading *error without checking the return value first.
- ❌
+unarchiveObjectWithData: and any unarchiving without NSSecureCoding and a class list; NSCoding without NSSecureCoding in new types.
- ❌ User input as a format string (
stringWithFormat:, NSLog, concatenated predicateWithFormat:).
- ❌ Secrets in the binary (API keys, tokens, client secrets); obfuscation presented as protection; credentials in
NSUserDefaults.
- ❌ Home-grown cryptography; comparing secrets with
isEqualToString:/memcmp.
- ❌
os_log/NSLog with domain data and no %{private}.
- ❌
performSelector: with a non-trivial signature when a typed block or a protocol would do; KVO on objects whose lifecycle you do not control.
- ❌ MRR (
-fno-objc-arc) in new code; OSSpinLock.
- ❌ C++ (
std::) leaking into public Objective-C headers.
- ❌
+load for initialisation that could be lazy.
- ❌ Adopting Carthage in a new project; adding new dependencies only through CocoaPods in full knowledge of the trunk cut-off.
- ❌ Mass reformatting alongside functional changes.
8. Mandatory web verification
Before pinning versions or dates, or asserting the state of the ecosystem, verify on the web (never from memory):
- The state of Objective-C in the toolchain: the release notes for the specific Xcode and Clang version (https://developer.apple.com/documentation/xcode-release-notes). Verified as of Aug 2026: no language evolution, no deprecation announced; what changes is build/modules. If Apple announces something, the announcement wins.
- CocoaPods: https://blog.cocoapods.org/CocoaPods-Specs-Repo/ — verified: trunk read-only on 2 Dec 2026, existing builds keep working. Confirm the date has not moved and which vendors have already stopped publishing there.
- Carthage:
https://github.com/Carthage/Carthage/releases.atom — verified: latest 0.40.0 (2024-09-09).
- SPM and mixed targets: the status of SE-0403 at https://github.com/swiftlang/swift-evolution (verified: Returned for Revision). If it were accepted, the package structure criterion in §2 changes.
- Swift Testing versus XCTest: https://developer.apple.com/documentation/testing and the
swiftlang/swift-testing repository — verified: Swift only; XCTest remains for ObjC, UI and performance, and is not deprecated.
- Interoperability annotations (
NS_SWIFT_SENDABLE, NS_SWIFT_UI_ACTOR, NS_REFINED_FOR_SWIFT, NS_SWIFT_NOTHROW): availability and exact semantics in the SDK of the version used — they arrived across versions.
- App Store Connect requirements (minimum SDK and Xcode, cut-off dates): https://developer.apple.com/news/ — the 28 Apr 2026 cut-off was verified (iOS 26 SDK, no grace period); these dates recur every year.
- Deprecated Foundation/Security APIs before citing them as a default: Apple's documentation marks the deprecation by OS version.
Declared gaps (unverified as of Aug 2026, verify before using as a rule):
- The verbatim text of the
+unarchiveObjectWithData: deprecation in Apple's documentation: not verified (the page returned no body). The criterion — NSSecureCoding with a class list — stands on its own, but the exact deprecation version is not confirmed here.
- The exact Xcode version published as stable as of Aug 2026 and its Swift mapping: partially verified (26.6 RC with Swift 6.3, and the 26.5/Swift 6.3.2 line in May 2026, per secondary sources; not checked against developer.apple.com). Secondary sources disagree on the Xcode↔Swift mapping: confirm at https://developer.apple.com/xcode/system-requirements.
- The exact interaction between C++ standard modules and Objective-C++ per Xcode version and
-std=c++ mode: not verified per version.
- The defined return behaviour of methods with a
nil receiver for struct and floating-point types in Apple's current ABIs: not verified against the specification; the criterion (do not depend on it) does not change.
- Verbatim licences of the tools cited (
swift-testing, clang-format): not verified verbatim.
If the web contradicts this document, the web wins — flag the discrepancy.
1---2name: objective-c-standards3description: Objective-C and Objective-C++ engineering standards for legacy maintenance and Swift interoperability. Trigger on .m/.mm/.h files with @interface/@implementation/@property, ARC and __weak/__strong/__unsafe_unretained/__bridge, NS_ASSUME_NONNULL_BEGIN and nullable/nonnull annotations, lightweight generics, NS_SWIFT_NAME/NS_REFINED_FOR_SWIFT/NS_SWIFT_SENDABLE, bridging headers and module.modulemap, objc_msgSend, method swizzling, categories, KVO/KVC, NSError** out-parameters, GCD dispatch_queue/dispatch_sync, NSSecureCoding and NSKeyedUnarchiver, Podfile/Podfile.lock, Cartfile, xcodebuild on Objective-C targets, or XCTest suites written in Objective-C.4---56# Objective-C standards78Criteria verified as of **August 2026**. Re-verify on the web before committing to anything (§8).910## 1. Scope and triggers1112Applies to the Objective-C and Objective-C++ **language**: `.m`/`.mm` files and headers with `@interface`/`@property`, ARC and property qualifiers, nullability annotations, lightweight generics, the dynamic runtime (`objc_msgSend`, categories, swizzling, KVO/KVC), designing headers **for consumption from Swift**, GCD from Objective-C, tooling (`xcodebuild`, the `clang` analyzer, `clang-format`, sanitizers) and the target's dependency management.1314**The real case is maintenance and interoperability, not new code.** For new code on Apple platforms the answer is **Swift**; writing a new module in Objective-C requires written justification (§7). This skill exists for two concrete reasons: (a) there is an enormous amount of Objective-C in production that has to be touched without breaking it, and (b) **the Swift↔Objective-C boundary is where things break** — badly annotated nullability, `id` without generics, retain cycles Swift cannot see, `NSError**` that Swift translates into `throws`, and the dynamic runtime the Swift compiler cannot verify. The axis of this document is: **make the existing Objective-C safe to consume from Swift and safe to delete little by little**.1516**Not applicable**: see `mobile-standards` (**the app is theirs**: architecture, navigation, app lifecycle, permissions and `Info.plist`, signing, App Store/TestFlight distribution, accessibility, secure storage as policy, MASVS, crash reporting; and **Swift is theirs** — here only interoperability **from the Objective-C side**: how the header is annotated and designed so Swift can consume it without exploding), `c-standards` and `cpp-standards` (Objective-C is a superset of C and `.mm` mixes in C++: **the C and C++ inside a `.m`/`.mm` remain subject to their skills** — manual memory management, undefined behaviour, integers, warning flags, sanitizers, RAII and the STL; **what is specific to the Objective-C runtime, ARC and the bridge to Swift belongs here**), `assembly-standards` (a `.mm` can contain inline `asm`: the criteria for when and how assembly is written are theirs), `cryptography-pki-standards` (algorithm choice, mode and key lifecycle; here only their use through the platform APIs), `identity-access-management-standards` (OAuth/OIDC flows on the IdP side), `appsec-standards` (threat modelling and process), `cicd-standards` (pipeline design; here only which tool is run), `secrets-management-standards` and `vulnerability-management-standards` (patching SLAs and triage of dependency CVEs), `smalltalk-standards` (**a direct lineage**: Objective-C's message runtime comes from there; living Smalltalk — Pharo, Squeak, GemStone/S, VAST — is theirs).1718## 2. Default decisions and toolchain1920> **Verify the latest version on the web before pinning it in a real project** (§8). What follows is the status verified as of **August 2026**.2122| Decision | Default | Justifiable alternative | Reason |23|---|---|---|---|24| Language for new code | **Swift** | Objective-C only if the module must be consumed from existing C/C++, if the dynamic runtime is a genuine requirement, or if the team maintains a public ObjC framework with a stable contract | Verified: Apple **publishes no language changes** to Objective-C; what changes is the module system and the build (e.g. *Explicitly Built Modules* in Xcode 16 for C/ObjC). **No deprecation announced**, but no evolution either: it is a **stable and static** language |25| Toolchain | **Xcode 26.x** (current line; 26.6 RC with Swift 6.3 as of Aug 2026) | The minimum version App Store Connect demands | Verified: since **28 Apr 2026** every upload to App Store Connect requires a build with the iOS 26 SDK or later (Xcode 26+), **with no grace period** |26| ARC | **Mandatory** (`-fobjc-arc`) in every file | MRR (`-fno-objc-arc`) **only** per file and with a written reason (interoperability with very old Core Foundation, generated code) | Manual MRR is a source of leaks and over-releases that no tool compensates for |27| Dependencies | **Swift Package Manager** | A vendored XCFramework with a checksum when the vendor does not publish to SPM | Verified: **the CocoaPods trunk goes read-only on 2 Dec 2026** — *"no new versions or pods will be added to trunk"*, though *"this will keep all existing builds working"* as long as GitHub and jsDelivr exist. That is: current builds keep working, **but there will be no updates and no security patches through trunk**. Migrating is planned work, not a same-day emergency |28| Carthage | **Do not adopt** | Keep existing usage until migrated | Verified through the Atom feed: latest release **0.40.0 (2024-09-09)**. No releases since: not a base for a new project |29| Mixing Swift/ObjC in SPM | **Separate targets** (`FooObjC` clang + `Foo` swift) | Binary target / XCFramework | Verified: **SE-0403 (mixed language targets) is "Returned for Revision"**, not accepted. In SPM **a target is either Swift or C-based, not both**. In an Xcode target they do coexist (bridging header + generated header) |30| Tests | **XCTest** for Objective-C code | **Swift Testing** for new tests written in Swift | Verified: **Swift Testing supports Swift only**; XCTest remains the only path for tests written in Objective-C, UI automation (XCUITest) and performance tests (XCTMetric). XCTest **is not deprecated**. A single test target accepts both worlds, but **each test lives in exactly one** |31| Formatting | `clang-format` with a versioned `.clang-format` | — | `clang-format` understands Objective-C; without a versioned file the style gets re-argued in every PR |3233Toolchain rules:34- The project declares its **minimum deployment version** and **compiles with `-Werror` in CI**. The C warning set applies in full (see `c-standards`), plus the Objective-C-specific ones: `-Wobjc-missing-property-synthesis`, `-Wdirect-ivar-access`, `-Wnullable-to-nonnull-conversion`, `-Wstrict-selector-match`, `-Wundeclared-selector`, `-Wdeprecated-implementations`, `-Wobjc-interface-ivars`.35- **`-Wnullable-to-nonnull-conversion` is non-negotiable**: it is the warning that stops Swift receiving a `nil` in a `let x: String` and aborting with a crash that has no useful trace.36- A reproducible build through `xcodebuild` from CI with explicit `-scheme`/`-destination`; no "it builds in my Xcode". Configuration in versioned `.xcconfig` files, not in the project UI.3738## 3. The language: lifecycle, the Swift boundary and the runtime3940### ARC and lifecycle41- Qualifiers: `strong` (the default for objects), `weak` for back references and delegates, `copy` **mandatory** for `NSString`/`NSArray`/`NSDictionary`/blocks in public properties (the caller can pass the mutable subclass and mutate it behind your back), `assign` only for scalars. **`unsafe_unretained` is vetoed** except for interoperability with an API that requires it, and with a comment: it is not nilled out on release, so it produces a *use-after-free* instead of `nil`.42- **Retain cycles in blocks**: a block captures `self` as `strong`. The mandatory pattern when the block is retained by the object itself (or by something it owns):4344```objc45__weak __typeof(self) weakSelf = self;46self.completion = ^{47 __strong __typeof(weakSelf) strongSelf = weakSelf; // once only, on entry48 if (!strongSelf) { return; } // mandatory early exit49 [strongSelf doWork];50};51```52 Without the `strongSelf`, every access to `weakSelf` can become `nil` halfway through the block and the behaviour turns non-deterministic. **Check for `nil` and return**, do not carry on "just in case".53- `delegate` is **always `weak`** (or a documented `unsafe_unretained` if the protocol is not object-based). A `strong` delegate is a guaranteed cycle.54- `@autoreleasepool` is **mandatory** inside loops that create temporary objects in volume (parsing, images, conversions): without it the memory peak grows until the end of the run-loop cycle and the system kills the process.55- Core Foundation: **explicit** ownership transfer with `__bridge`/`__bridge_transfer`/`__bridge_retained` or the `CFBridgingRetain`/`CFBridgingRelease` macros. A bare `__bridge` where `__bridge_transfer` was needed is a leak the analyzer **does** detect: it is not silenced.56- `dealloc`: deregistration only (KVO/`NSNotificationCenter` observers if the lifecycle requires it) and timer invalidation. **No business logic and no calls to overridable methods**; the object is already half destroyed.5758### `nil` as a valid receiver — the structural trap59Sending a message to `nil` is legal and returns zero/`nil`/a zeroed struct. Consequence: **an initialisation bug does not fail, it propagates silently** to a distant point where the symptom bears no relation to the cause.60- Do not use a method's return value as proof that the receiver existed. If a `nil` at that point is a programming error, there are `NSParameterAssert`/`NSAssert` (which **disappear with `NS_BLOCK_ASSERTIONS`**, so they are no good for validating external input) or an explicit check.61- Be careful with non-object return types: for a `nil` receiver, returned floating-point values and structs are defined as zero **in Apple's current ABIs**, but depending on that is fragile; check first.6263### Properties and `atomic`64- `nonatomic` by default. **`atomic` does not give thread safety**: it guarantees that an individual `get`/`set` does not return a half-written value, nothing more. An `array.count` followed by an indexed access is still a race. Real synchronisation is designed (a serial queue, a lock, immutability), not obtained by writing `atomic`.65- Accessing ivars: **through the property**, not directly (`-Wdirect-ivar-access`), except in `init` and `dealloc`, where direct access is the correct thing (setters can have effects and the object is not in a valid state).66- Every public property has explicitly declared semantics (`nonatomic, copy, readonly`…). No `@property NSString *name;` without qualifiers.6768### Nullability and lightweight generics — a requirement for consumption from Swift69This is **not cosmetic**: it decides whether Swift sees `String` or `String!`. An unannotated `id` arrives in Swift as `Any!`, and an implicitly-unwrapped optional that turns out to be `nil` **aborts the process**.70- **Every public header is wrapped** in `NS_ASSUME_NONNULL_BEGIN` / `NS_ASSUME_NONNULL_END`, explicitly marking what can be `nil` with `nullable` (and `null_resettable` where applicable). An unannotated header is debt paid by the consumer.71- **Lightweight generics are mandatory** on exposed collections: `NSArray<NSString *> *`, `NSDictionary<NSString *, NSNumber *> *`. Without them Swift receives `[Any]` and all the typing on the Swift side degrades into casts.72- `instancetype` in constructors, never `id`. `NS_DESIGNATED_INITIALIZER` on the designated initialiser and `NS_UNAVAILABLE` on those that must not be used: without that, Swift inherits initialisers that leave the object invalid.73- Enumerations with `NS_ENUM`/`NS_OPTIONS`, typed strings with `NS_STRING_ENUM`/`NS_TYPED_ENUM`. Never a bare C `enum` in a public API.74- **The annotation is a promise the compiler only partially verifies**: keep `-Wnullable-to-nonnull-conversion` on, and at boundaries receiving external data (JSON, disk, network) **check at runtime** before returning anything declared `nonnull`.7576### Designing the header with the Swift consumer in mind77- `NS_SWIFT_NAME` to give the idiomatic Swift name when the automatic translation produces something unreadable.78- `NS_REFINED_FOR_SWIFT` when the Objective-C API cannot be idiomatic (out-params, pointers, boxed `NSNumber`): the symbol is imported with a `__` prefix and wrapped in a Swift extension. **That is the correct route**, not changing the ObjC API to please Swift.79- `NS_NOESCAPE` on non-escaping blocks, `NS_SWIFT_SENDABLE` on types safe to cross isolation and `NS_SWIFT_UI_ACTOR` / `NS_SWIFT_NONISOLATED` where isolation matters — **verify availability and exact semantics in the SDK of the version being used** (§8): these annotations arrived across versions and their effect depends on the target's Swift concurrency mode.80- `NS_ERROR_ENUM` for error domains, so Swift imports them as a typed `Error`.81- `NS_SWIFT_UNAVAILABLE` for what must not cross; `API_AVAILABLE`/`API_DEPRECATED` on every API with a version condition.82- **The module is exposed through `module.modulemap`**, not a bridging header, when it is a consumable framework. The bridging header is for the app's code, and it **does not** exist in the other direction: Swift is seen from ObjC through the generated header (`<Product>-Swift.h`), and **only what is marked `@objc`/`@objcMembers` and inherits from `NSObject`**.8384### Categories85- They exist to **add** methods to a class you do not control. Every category method on someone else's class carries a **project prefix** (`px_doThing`): the runtime has no namespaces and two categories declaring the same selector collide silently, with the last loaded winning — an order that is **non-deterministic** between libraries.86- **FORBIDDEN to override an existing method from a category.** The behaviour is undefined and it breaks the base class, other categories and the subclasses. If behaviour needs changing: subclass, composition or delegation.87- Categories with properties: they require *associated objects* (`objc_setAssociatedObject`), which are hidden state with no `dealloc` of their own. Use them sparingly and document them.88- Class extensions (`@interface Foo ()` in the `.m`) are the correct mechanism for private members, including redeclaring a public `readonly` property as `readwrite`.8990### Dynamic runtime91- `objc_msgSend` is the dispatch mechanism: **every send is dynamic**, there is no devirtualisation. Practical implication: the compiler can warn you about almost nothing regarding the receiver, so the static typing of the headers is your only net.92- **FORBIDDEN: *method swizzling* in application code** except in a justified, documented case (typically third-party instrumentation, and even then an alternative is preferred). It is a classic source of irreproducible failures: it depends on load order, breaks with every SDK update, and two libraries swizzling the same selector corrupt each other. If it is done anyway: only in `+load` or with `dispatch_once`, on your own selectors, always invoking the original `IMP`, and with a written owner and review date.93- `respondsToSelector:` / `performSelector:` are *escape hatches*, not design. `performSelector:` with more than two arguments or a non-object return is incorrect (ARC does not know the signature and `-Warc-performSelector-leaks` says so). **Default replacement: a typed block or a protocol with `@optional`.**94- `NSInvocation` and hand-written `objc_msgSend`: only in infrastructure code with its own tests; on `arm64` they require casting `objc_msgSend` to the exact signature or the result is garbage.95- **KVO/KVC are fragile**: keys as strings with no compile-time checking, `observeValueForKeyPath:` untyped, and **removing the observer at exactly the right moment** (an observer alive after the observed object's `dealloc` is a guaranteed crash). Criterion: use the token-based API (`-addObserverForKeyPath:…`, which returns `NSKeyValueObservation` in Swift, or store the context and deregister in `dealloc`), prefer notifications or explicit callbacks, and **never KVO on objects whose lifecycle you do not control**. `valueForKey:` with a constructed string is, additionally, attack surface.9697### Errors98- Canonical pattern: `- (BOOL)doThing:(NSError **)error` or an object return with `nil` on failure. **The failure indicator is the return value, not `error`**: check the return first and only then read `*error`. Writing to `*error` without checking the pointer is not `NULL` is a crash.99- Swift translates this pattern into `throws` **only if the signature follows the convention** (last parameter `NSError **`, `NS_SWIFT_NOTHROW` to exclude it). Changing the signature breaks the Swift side even though the ObjC compiles.100- **Objective-C exceptions are not control flow**: they represent programming errors (index out of range, unrecognised selector) and **are not safe with ARC** — unwinding the stack with ARC leaks memory except with `-fobjc-arc-exceptions`, which has a cost. `@try/@catch` only to wrap system APIs documented as throwing (some of `NSFileHandle`, KVC) and at the C++ boundary in a `.mm`. **FORBIDDEN**: `@throw` for expected errors.101102### Concurrency (GCD from Objective-C)103- Default model: **one serial queue per unit of mutable state**. A serial queue is a lock that also orders; it is cheaper to reason about than `NSLock` scattered around.104- Concurrent queues only with an explicit reader/writer pattern (`dispatch_barrier_async` for writes) and a concurrency test under TSan.105- **FORBIDDEN: `dispatch_sync` onto the main queue from the main queue**: an immediate deadlock. By extension, `dispatch_sync` onto any queue from that same queue. Operational rule: `dispatch_sync` only towards a queue you know for certain is not you, and never while holding another lock.106- All UI work on the main queue; all I/O off it. `dispatch_after` is not a synchronisation mechanism.107- Interop with Swift's `async/await`: an ObjC API with a completion block as the **last** parameter is imported as `async` — the signature is a contract. The block must be called **exactly once** on every path, including the error and cancellation ones: zero calls hang the Swift task forever and two calls are a runtime failure.108- `volatile` does not synchronise (see `c-standards`). For counters, C11 `atomic_*` or `os_unfair_lock`; **`OSSpinLock` is deprecated** because of priority inversion.109110### Objective-C++ (`.mm`)111- Used **only** to bridge to an existing C++ library, and it is isolated: the C++ does not leak into public Objective-C headers (a header with `std::` forces every consumer to be `.mm`). Pattern: a clean ObjC header, a `.mm` implementation with a hidden C++ implementation `struct` (pImpl).112- ARC and C++ coexist: an ObjC object as a member of a C++ class needs an explicit `__strong`/`__weak` and the class stops being trivially copyable. `id` inside a `union` or in POD types requires care.113- Exceptions: a C++ exception crossing into an Objective-C frame with ARC leaks resources; catch it at the bridge's edge.114- **The C++ in a `.mm` is subject to `cpp-standards`** (RAII, the STL, UB, flags). Verified: there is a known interaction between C++ standard modules and Objective-C++ depending on the C++ language mode — check the release notes of the specific Xcode version before enabling C++ modules in a `.mm` target (§8).115116## 4. Quality: analysis, tests and CI gates117118- **Clang Static Analyzer** (`xcodebuild analyze` or *Analyze* in Xcode) is the main gate and it is specifically good at Objective-C: ARC/CF leaks, over-release, `nil` receivers, use of uninitialised values. **Zero findings** in your own tree; a suppression requires a `// NOLINT`-equivalent with a reason, not deleting the warning.119- `clang-tidy` over `compile_commands.json` for the C/C++ inside the `.m`/`.mm` (`bugprone-*`, `cert-*`).120- `clang-format --dry-run --Werror` in CI, with a versioned `.clang-format`. Mass reformatting in a separate commit, recorded in `.git-blame-ignore-revs`.121- Tests: XCTest for the Objective-C code; **the boundary test is mandatory** — for every public API consumed from Swift, a test **written in Swift** that exercises it. It is the only way to check that nullability, generics and the `NSError**` translation are what you think they are.122- Edge coverage: `nil` in every parameter annotated `nullable`, an empty collection, an empty string, boundary values, the completion block invoked on every path, and the error path of every `NSError**`.123- **Sanitizers in Xcode**, in separate schemes: Address Sanitizer (+ *Detect use of stack after return*), Thread Sanitizer and Undefined Behavior Sanitizer. ASan and TSan **are not combined**. Also *Zombie Objects* and *Malloc Scribble* to diagnose over-release in inherited MRR code. Never enabled in a distribution build.124- Instruments: **Leaks** and **Allocations** over the critical flow before releasing a change that touches lifecycle; a retain cycle does not show up in unit tests.125- Minimum CI gate (everything breaks the build): formatting → build with `-Werror` (the warnings in §2) → `xcodebuild analyze` with no findings → ObjC unit tests + Swift boundary tests → tests under ASan+UBSan → dependency SCA/SBOM.126127## 5. Stack security128129- **`NSKeyedUnarchiver` without an allowed class list is code execution.** **FORBIDDEN: `+unarchiveObjectWithData:`** and the whole unsafe family: deserialising an arbitrary archive instantiates whichever classes the attacker names. The only replacement: `NSSecureCoding` with `+unarchivedObjectOfClass:fromData:error:` / `+unarchivedObjectOfClasses:fromData:error:`, with `requiresSecureCoding = YES` and the **allowed class list as narrow as possible**. Every serialisable class implements `NSSecureCoding` (not just `NSCoding`) and its `+supportsSecureCoding` genuinely returns `YES`, not by copy-paste.130- The same criterion applies to any deserialisation: `NSJSONSerialization` with type validation of every field before use (an `NSDictionary` arriving with an `NSNull` where you expected an `NSString` is an `unrecognized selector` in production). **Never** trust the shape of the JSON.131- **Format strings**: `-Wformat=2 -Werror=format-security` on. **FORBIDDEN** to pass user input as a format string to `+stringWithFormat:`, `NSLog`, `-appendFormat:` or predicates. It is arbitrary memory read/write. The same goes for `NSPredicate predicateWithFormat:` built by concatenation: direct injection; use substitution with `%@`/`%K`.132- On-device storage: **Keychain** for credentials, tokens and keys, with the most restrictive protection class the case allows (`…ThisDeviceOnly` when it must not sync) and with biometric `SecAccessControl` where applicable. **`NSUserDefaults` is not secure storage**: it is a plist in the clear, it is read from a backup and in some scenarios it survives uninstallation. Files with `NSFileProtectionComplete` and `isExcludedFromBackup` where appropriate. The policy of what is stored and with what classification belongs to `mobile-standards`; here, the correct API.133- **A client binary does not keep secrets.** Every embedded string — API keys, tokens, OAuth client secrets, internal URLs, signing credentials — is in the user's hands: `strings` over the `.app` finds it in seconds. Obfuscation is not protection. If an operation requires a secret, it runs on the server. What is protected is what the device **generates** (a key pair in the Secure Enclave, an ephemeral token in the Keychain).134- Crypto: platform APIs (CryptoKit from Swift, `SecKey`/`CommonCrypto` from ObjC). **FORBIDDEN to implement your own cryptography**; compare secrets in constant time, never `memcmp` or `isEqualToString:`.135- Logging: `os_log` with a format string and **redaction of dynamic values by default** (`%{private}@` for everything not explicitly public). `NSLog` with domain objects in a release build is a PII leak into the device's log system.136- Dependencies: exact pinning (version + checksum in `Package.resolved`, a mandatory checksum on `binaryTarget`), an SBOM of the artifact and SCA. **Any pod that stops receiving updates after Dec 2026 becomes security debt with a date**, not a stable dependency.137138## 6. Performance and operability139140- Dynamic dispatch has a cost, but **it is almost never the bottleneck**: measure with Instruments (Time Profiler) before touching anything. Optimising messages by hand (a cached `IMP`) requires a measured number and a test.141- Memory peaks: `@autoreleasepool` in loops (§3) and `NSCache` instead of `NSMutableDictionary` for caches (it responds to memory pressure). System memory warnings are acted upon.142- Crashes: dSYM archived per build with its UUID; a crash without symbols is wasted time. Unrecognised selectors, `NSInvalidArgumentException` from `nil`, and retain cycles are the three dominant families in Objective-C codebases — they get instrumented and counted.143- Start-up: `+load` runs before `main` and **delays the whole app's launch**; prefer `+initialize` (lazy) or explicit initialisation. A `+load` in a third-party library is a legitimate reason to question the dependency.144145## 7. Sustainability and prohibitions146147**Strategy, not just style.** The Objective-C in a living codebase is treated as **debt with an amortisation plan**: it gets annotated (nullability and generics) before being touched, covered with tests from Swift, and replaced by Swift **from the inside out** — the internal implementation first, keeping the ObjC header as a facade — so consumers do not break. Migrating a class without a boundary test is trading a known bug for an unknown one.148149**Cadence**: move up Xcode at least with every major line and **before** App Store Connect demands it (verified: the Apr 2026 cut-off had no grace period). Every SDK bump is tested in CI against the previous one: framework behaviour changes, not language changes, are what break things.150151**Dependency migration**: CocoaPods' exit has a known date (2 Dec 2026 for trunk). Plan the migration to SPM/XCFramework **before** that date, not after; whatever remains in pods after it stops receiving new versions through trunk.152153**Conscious debt**: every shortcut leaves `// TODO(user): reason — issue #N`. Every warning or analyzer suppression carries a reason and a review date.154155**FORBIDDEN** (an exception requires written justification and approval):156- ❌ Writing a **new module** in Objective-C when Swift is viable.157- ❌ **Method swizzling** in application code; swizzling other people's selectors, UIKit/Foundation methods, or doing it without invoking the original `IMP`.158- ❌ **Overriding an existing method from a category**; category methods on other people's classes **without a project prefix**.159- ❌ A public header **without `NS_ASSUME_NONNULL_BEGIN`/`END`**, without lightweight generics on collections, or with `id` where a type would fit.160- ❌ `unsafe_unretained` without justification; a `delegate` declared `strong`; an object property with no explicit semantics; a public `NSString`/collection without `copy`.161- ❌ A block capturing `self` as `strong` while retained by `self`; `weakSelf` used without promoting to `strongSelf` and without a `nil` check.162- ❌ A loop creating temporary objects in volume **without `@autoreleasepool`**.163- ❌ `atomic` presented as synchronisation; shared mutable state with no serial queue and no lock.164- ❌ **`dispatch_sync` onto the current queue** (deadlock); UI work off the main queue; network or disk I/O on the main queue.165- ❌ A completion block that is **not** invoked exactly once on every path.166- ❌ `@throw`/`@try` as control flow; ObjC exceptions for expected errors; writing to `*error` without checking the pointer; reading `*error` without checking the return value first.167- ❌ **`+unarchiveObjectWithData:`** and any unarchiving without `NSSecureCoding` and a class list; `NSCoding` without `NSSecureCoding` in new types.168- ❌ User input as a **format string** (`stringWithFormat:`, `NSLog`, concatenated `predicateWithFormat:`).169- ❌ **Secrets in the binary** (API keys, tokens, client secrets); obfuscation presented as protection; credentials in `NSUserDefaults`.170- ❌ Home-grown cryptography; comparing secrets with `isEqualToString:`/`memcmp`.171- ❌ `os_log`/`NSLog` with domain data and no `%{private}`.172- ❌ `performSelector:` with a non-trivial signature when a typed block or a protocol would do; KVO on objects whose lifecycle you do not control.173- ❌ MRR (`-fno-objc-arc`) in new code; `OSSpinLock`.174- ❌ C++ (`std::`) leaking into public Objective-C headers.175- ❌ `+load` for initialisation that could be lazy.176- ❌ Adopting **Carthage** in a new project; adding new dependencies **only** through CocoaPods in full knowledge of the trunk cut-off.177- ❌ Mass reformatting alongside functional changes.178179## 8. Mandatory web verification180181Before pinning versions or dates, or asserting the state of the ecosystem, **verify on the web** (never from memory):1821. **The state of Objective-C in the toolchain**: the release notes for the specific Xcode and Clang version (https://developer.apple.com/documentation/xcode-release-notes). Verified as of Aug 2026: **no language evolution, no deprecation announced**; what changes is build/modules. If Apple announces something, the announcement wins.1832. **CocoaPods**: https://blog.cocoapods.org/CocoaPods-Specs-Repo/ — verified: trunk read-only on **2 Dec 2026**, existing builds keep working. Confirm the date has not moved and which vendors have already stopped publishing there.1843. **Carthage**: `https://github.com/Carthage/Carthage/releases.atom` — verified: latest **0.40.0 (2024-09-09)**.1854. **SPM and mixed targets**: the status of **SE-0403** at https://github.com/swiftlang/swift-evolution (verified: *Returned for Revision*). If it were accepted, the package structure criterion in §2 changes.1865. **Swift Testing versus XCTest**: https://developer.apple.com/documentation/testing and the `swiftlang/swift-testing` repository — verified: Swift only; XCTest remains for ObjC, UI and performance, and **is not deprecated**.1876. **Interoperability annotations** (`NS_SWIFT_SENDABLE`, `NS_SWIFT_UI_ACTOR`, `NS_REFINED_FOR_SWIFT`, `NS_SWIFT_NOTHROW`): availability and exact semantics in the SDK of the version used — they arrived across versions.1887. **App Store Connect requirements** (minimum SDK and Xcode, cut-off dates): https://developer.apple.com/news/ — the **28 Apr 2026** cut-off was verified (iOS 26 SDK, no grace period); these dates recur every year.1898. **Deprecated Foundation/Security APIs** before citing them as a default: Apple's documentation marks the deprecation by OS version.190191**Declared gaps (unverified as of Aug 2026, verify before using as a rule)**:192- The verbatim text of the `+unarchiveObjectWithData:` deprecation in Apple's documentation: **not verified** (the page returned no body). The criterion — `NSSecureCoding` with a class list — stands on its own, but the exact deprecation version is not confirmed here.193- The exact Xcode version published as stable as of Aug 2026 and its Swift mapping: **partially verified** (26.6 RC with Swift 6.3, and the 26.5/Swift 6.3.2 line in May 2026, per secondary sources; **not checked against developer.apple.com**). Secondary sources disagree on the Xcode↔Swift mapping: confirm at https://developer.apple.com/xcode/system-requirements.194- The exact interaction between C++ standard modules and Objective-C++ per Xcode version and `-std=c++` mode: **not verified per version**.195- The defined return behaviour of methods with a `nil` receiver for struct and floating-point types in Apple's current ABIs: **not verified against the specification**; the criterion (do not depend on it) does not change.196- Verbatim licences of the tools cited (`swift-testing`, `clang-format`): **not verified verbatim**.197198If the web contradicts this document, **the web wins** — flag the discrepancy.