Objective-C is not the language I would choose for a new iOS app in 2026.

For new Apple development, Swift is the normal answer. Swift is cleaner, easier to read, easier to hire for, and much better aligned with modern Apple APIs, SwiftUI, async/await, and current Xcode templates.

But Objective-C is still not dead.

It survives because a lot of real Apple software was written before Swift became the default. It also survives because Objective-C is not just syntax. It is a runtime model, a messaging system, and a compatibility layer that still appears in old apps, SDKs, crash logs, AppKit/UIKit patterns, and reverse engineering workflows.

So the practical answer is this:

Do not learn Objective-C first if your goal is modern iOS development. Learn Swift first.

But if you work with real production iOS/macOS apps, legacy code, QA automation, mobile security, SDKs, or reverse engineering, you should understand enough Objective-C to read it and debug it.

Not worship it.

Read it. Recognize it. Survive it.


What Objective-C Code Actually Looks Like

Objective-C usually has two files:

UserSession.h
UserSession.m

The .h file is the public interface:

// UserSession.h

#import <Foundation/Foundation.h>

@interface UserSession : NSObject

@property (nonatomic, copy) NSString *userId;
@property (nonatomic, assign, getter=isLoggedIn) BOOL loggedIn;

- (instancetype)initWithUserId:(NSString *)userId;
- (void)logout;

@end

The .m file is the implementation:

// UserSession.m

#import "UserSession.h"

@implementation UserSession

- (instancetype)initWithUserId:(NSString *)userId {
    self = [super init];

    if (self) {
        _userId = [userId copy];
        _loggedIn = YES;
    }

    return self;
}

- (void)logout {
    self.userId = nil;
    self.loggedIn = NO;
}

@end

Then you use it like this:

UserSession *session = [[UserSession alloc] initWithUserId:@"12345"];

if ([session isLoggedIn]) {
    NSLog(@"User is logged in: %@", session.userId);
}

[session logout];

The first thing that looks weird is the square bracket syntax:

[session logout];

That is a message send.

In many languages you would write:

session.logout()

In Objective-C, you send a message to an object:

[object doSomething];
[object doSomethingWithValue:value];
[object doSomethingWithFirstValue:first secondValue:second];

That style looks old, but it tells you something important about Objective-C: method calls are dynamic.


The Runtime Is the Real Reason Objective-C Still Matters

Objective-C has a runtime that lets the app inspect and modify behavior while it is running.

That is not just trivia. It matters for debugging, analytics SDKs, testing tools, old frameworks, and reverse engineering.

Example: checking whether an object supports a method.

SEL selector = @selector(logout);

if ([session respondsToSelector:selector]) {
    [session performSelector:selector];
}

This is very Objective-C.

The method is represented as a selector:

@selector(logout)

A selector is basically the name of a method known to the Objective-C runtime.

Another example:

Class cls = [session class];
NSLog(@"Class name: %@", NSStringFromClass(cls));

Or:

BOOL isUserSession = [session isKindOfClass:[UserSession class]];

Objective-C code can ask objects what they are and what they can do.

That is one reason it became useful for Cocoa, AppKit, UIKit, dynamic UI frameworks, testing, mocking, analytics, and runtime instrumentation.


A Realistic UIKit Example

A lot of old iOS code looks like this:

// LoginViewController.h

#import <UIKit/UIKit.h>

@interface LoginViewController : UIViewController

@end
// LoginViewController.m

#import "LoginViewController.h"

@interface LoginViewController ()

@property (nonatomic, strong) UITextField *emailField;
@property (nonatomic, strong) UIButton *loginButton;

@end

@implementation LoginViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor systemBackgroundColor];

    self.emailField = [[UITextField alloc] initWithFrame:CGRectMake(20, 100, 280, 44)];
    self.emailField.placeholder = @"Email";
    self.emailField.borderStyle = UITextBorderStyleRoundedRect;

    self.loginButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.loginButton.frame = CGRectMake(20, 160, 280, 44);
    [self.loginButton setTitle:@"Log In" forState:UIControlStateNormal];

    [self.loginButton addTarget:self
                         action:@selector(loginButtonTapped:)
               forControlEvents:UIControlEventTouchUpInside];

    [self.view addSubview:self.emailField];
    [self.view addSubview:self.loginButton];
}

- (void)loginButtonTapped:(UIButton *)sender {
    NSString *email = self.emailField.text;

    if (email.length == 0) {
        NSLog(@"Email is empty");
        return;
    }

    NSLog(@"Logging in with email: %@", email);
}

@end

This code shows several Objective-C patterns that still matter:

  • @interface and @implementation
  • properties
  • UIKit objects
  • target-action
  • selectors
  • UIViewController
  • private class extension
  • message sending
  • NSString

The important line is this:

[self.loginButton addTarget:self
                     action:@selector(loginButtonTapped:)
           forControlEvents:UIControlEventTouchUpInside];

The button does not call a closure here. It stores a target and a selector. When the event happens, UIKit sends the loginButtonTapped: message to the target.

This old pattern still influences Apple UI development.


Delegates: One of the Most Important Objective-C Patterns

If you have touched Apple development, you have seen delegates.

Objective-C made delegates everywhere.

A delegate is just another object that receives callbacks.

Example:

// DownloadManager.h

#import <Foundation/Foundation.h>

@class DownloadManager;

@protocol DownloadManagerDelegate <NSObject>

- (void)downloadManagerDidFinish:(DownloadManager *)manager;
- (void)downloadManager:(DownloadManager *)manager didFailWithError:(NSError *)error;

@end

@interface DownloadManager : NSObject

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

- (void)startDownload;

@end

Implementation:

// DownloadManager.m

#import "DownloadManager.h"

@implementation DownloadManager

- (void)startDownload {
    BOOL success = YES;

    if (success) {
        [self.delegate downloadManagerDidFinish:self];
    } else {
        NSError *error = [NSError errorWithDomain:@"DownloadError"
                                             code:1001
                                         userInfo:nil];

        [self.delegate downloadManager:self didFailWithError:error];
    }
}

@end

Using it:

@interface MyController () <DownloadManagerDelegate>

@property (nonatomic, strong) DownloadManager *downloadManager;

@end

@implementation MyController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.downloadManager = [[DownloadManager alloc] init];
    self.downloadManager.delegate = self;

    [self.downloadManager startDownload];
}

- (void)downloadManagerDidFinish:(DownloadManager *)manager {
    NSLog(@"Download finished");
}

- (void)downloadManager:(DownloadManager *)manager didFailWithError:(NSError *)error {
    NSLog(@"Download failed: %@", error.localizedDescription);
}

@end

This is not just old syntax. This is the root of a lot of Apple API design.

Even Swift code still uses delegate-style APIs:

tableView.delegate = self
tableView.dataSource = self

That pattern came from the Objective-C world.


Categories: Extending Classes Without Subclassing

Objective-C categories let you add methods to existing classes.

Example:

// NSString+Validation.h

#import <Foundation/Foundation.h>

@interface NSString (Validation)

- (BOOL)isValidEmail;

@end
// NSString+Validation.m

#import "NSString+Validation.h"

@implementation NSString (Validation)

- (BOOL)isValidEmail {
    return [self containsString:@"@"] && [self containsString:@"."];
}

@end

Usage:

NSString *email = @"test@example.com";

if ([email isValidEmail]) {
    NSLog(@"Valid email");
}

Categories are useful, but they can also be dangerous.

If two categories add the same method name to the same class, behavior can become unpredictable. This matters in old codebases where many teams and SDKs extended Apple classes.

You can see why Objective-C runtime behavior can become messy.

It gives you power.

It also gives you rope.


Blocks: Objective-C Closures

Objective-C also has blocks, which are similar to closures or lambdas.

Example:

void (^completion)(BOOL success) = ^(BOOL success) {
    if (success) {
        NSLog(@"Success");
    } else {
        NSLog(@"Failed");
    }
};

completion(YES);

A more realistic async-style API:

typedef void (^LoginCompletion)(BOOL success, NSError *error);

- (void)loginWithEmail:(NSString *)email
              password:(NSString *)password
            completion:(LoginCompletion)completion {
    BOOL success = email.length > 0 && password.length > 0;

    if (success) {
        completion(YES, nil);
    } else {
        NSError *error = [NSError errorWithDomain:@"LoginError"
                                             code:401
                                         userInfo:nil];

        completion(NO, error);
    }
}

Usage:

[authService loginWithEmail:@"test@example.com"
                   password:@"password123"
                 completion:^(BOOL success, NSError *error) {
    if (success) {
        NSLog(@"Logged in");
    } else {
        NSLog(@"Error: %@", error.localizedDescription);
    }
}];

This kind of code was very common before Swift async/await became the clean modern option.

If you maintain older apps or SDKs, you will still see it.


ARC and Memory: What You Need to Know

Old Objective-C used manual memory management:

[object retain];
[object release];
[object autorelease];

Modern Objective-C normally uses ARC, Automatic Reference Counting.

With ARC, you usually think in property attributes:

@property (nonatomic, strong) NSObject *ownerObject;
@property (nonatomic, weak) id delegate;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) BOOL enabled;

The practical version:

  • strong keeps an object alive
  • weak avoids retain cycles, commonly used for delegates
  • copy is common for strings and blocks
  • assign is for primitive values like BOOL, NSInteger, CGFloat

Classic retain cycle example:

@interface Parent : NSObject

@property (nonatomic, strong) Child *child;

@end

@interface Child : NSObject

@property (nonatomic, strong) Parent *parent;

@end

If both objects strongly reference each other, neither may be released.

Better:

@interface Child : NSObject

@property (nonatomic, weak) Parent *parent;

@end

This is still relevant when debugging memory leaks in older iOS/macOS apps.


nil Messaging: Weird but Useful

In Objective-C, sending a message to nil does not crash.

UserSession *session = nil;

[session logout]; // No crash

This surprises people coming from many other languages.

It can be convenient:

NSString *name = session.userId;
NSLog(@"User ID: %@", name);

If session is nil, name becomes nil.

But it can also hide bugs. Code silently does nothing instead of failing loudly.

Example:

[self.analytics trackEvent:@"Login"];

If self.analytics is nil, nothing happens. No crash. No event. Maybe nobody notices until production analytics are missing.

That is Objective-C in one sentence:

Sometimes forgiving. Sometimes too forgiving.


Method Swizzling: Powerful and Dangerous

Method swizzling means replacing one method implementation with another at runtime.

Example:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    Method original = class_getInstanceMethod(self, @selector(viewDidAppear:));
    Method swizzled = class_getInstanceMethod(self, @selector(tracked_viewDidAppear:));

    method_exchangeImplementations(original, swizzled);
}

- (void)tracked_viewDidAppear:(BOOL)animated {
    [self tracked_viewDidAppear:animated];

    NSLog(@"Screen appeared: %@", NSStringFromClass([self class]));
}

@end

This looks insane until you understand what happened.

After swizzling:

viewDidAppear:

and:

tracked_viewDidAppear:

have swapped implementations.

The call inside the method:

[self tracked_viewDidAppear:animated];

actually calls the original viewDidAppear: after the swap.

This pattern was used by analytics SDKs, debugging tools, testing helpers, and sometimes very questionable production code.

It is useful.

It is also exactly the kind of thing that makes bugs feel haunted.

If you are doing QA or debugging and screens are being tracked, modified, or intercepted magically, swizzling may be involved.


Swift and Objective-C Interop in Real Projects

Many real projects are mixed.

A Swift file can call Objective-C code through a bridging header.

Example Objective-C header:

// LegacyAnalytics.h

#import <Foundation/Foundation.h>

@interface LegacyAnalytics : NSObject

+ (void)trackEvent:(NSString *)eventName;
+ (void)trackEvent:(NSString *)eventName properties:(NSDictionary *)properties;

@end

Bridging header:

// MyApp-Bridging-Header.h

#import "LegacyAnalytics.h"

Swift usage:

LegacyAnalytics.trackEvent("Login")
LegacyAnalytics.trackEvent("Purchase", properties: ["plan": "pro"])

The other direction also exists. Objective-C can use Swift classes if they are exposed correctly.

Swift:

import Foundation

@objcMembers
class PaymentValidator: NSObject {
    func isValidAmount(_ amount: NSNumber) -> Bool {
        return amount.doubleValue > 0
    }
}

Objective-C:

#import "MyApp-Swift.h"

PaymentValidator *validator = [[PaymentValidator alloc] init];
BOOL valid = [validator isValidAmount:@49.99];

This is why Objective-C stays relevant even when teams mostly write Swift.

The migration is rarely clean. Companies mix both for years.


Objective-C in Crash Logs

Objective-C often appears in crash logs and stack traces.

You may see symbols like:

-[LoginViewController loginButtonTapped:]
+[AnalyticsManager trackEvent:]
-[UserSession logout]
objc_msgSend

The prefixes matter:

-  instance method
+  class method

So:

- (void)logout;

is called on an instance:

[session logout];

And:

+ (void)trackEvent:(NSString *)name;

is called on the class:

[AnalyticsManager trackEvent:@"Login"];

If you do QA, crash triage, or mobile debugging, this is practical knowledge. It helps you read crashes faster instead of treating the native stack like random noise.


Objective-C for Reverse Engineering

Objective-C is especially important for iOS/macOS reverse engineering.

Because the runtime keeps class names, method names, and selectors, tools can often inspect them.

With Frida, the mental model often looks like this:

if (ObjC.available) {
  const LoginManager = ObjC.classes.LoginManager;

  const method = LoginManager["- isUserLoggedIn"];

  Interceptor.attach(method.implementation, {
    onLeave(retval) {
      console.log("Original return:", retval);
      retval.replace(1);
    }
  });
}

Or listing methods:

const klass = ObjC.classes.LoginManager;

klass.$ownMethods.forEach(function(methodName) {
  console.log(methodName);
});

This is not Objective-C source code, but it uses Objective-C runtime concepts:

  • classes
  • instance methods
  • class methods
  • selectors
  • implementations
  • return values

You may hook a jailbreak/root detection method:

const SecurityChecker = ObjC.classes.SecurityChecker;

Interceptor.attach(SecurityChecker["- isJailbroken"].implementation, {
  onLeave(retval) {
    retval.replace(0);
  }
});

This is why security people still care.

Swift exists. Swift is modern. Swift is everywhere.

But Objective-C runtime metadata is still extremely useful when analyzing Apple apps.


Objective-C in SDKs and Frameworks

Another place Objective-C survives: SDKs.

If a company ships an iOS SDK, it may still care about Objective-C compatibility because customers may have old apps.

A clean SDK interface may still look like this:

// PaymentSDK.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^PaymentCompletion)(BOOL success, NSError *_Nullable error);

@interface PaymentSDK : NSObject

+ (instancetype)sharedInstance;

- (void)configureWithApiKey:(NSString *)apiKey;

- (void)startPaymentWithAmount:(NSNumber *)amount
                      currency:(NSString *)currency
                    completion:(PaymentCompletion)completion;

@end

NS_ASSUME_NONNULL_END

This API can be used from Objective-C apps and also imported into Swift.

That matters for SDK vendors because enterprise customers do not always move fast.

The bigger and older the customer, the more likely someone still has an Objective-C app in production.


What I Would Actually Learn in 2026

Do not learn Objective-C like you are trying to become a 2010 iOS developer.

Learn the useful parts.

Start with this:

// Class declaration
@interface MyClass : NSObject
@end

// Class implementation
@implementation MyClass
@end

// Instance method
- (void)doSomething;

// Class method
+ (instancetype)sharedInstance;

// Property
@property (nonatomic, strong) NSObject *object;

// Selector
@selector(doSomething)

// Protocol
@protocol MyDelegate <NSObject>
@end

// Category
@interface NSString (MyAdditions)
@end

// Block
void (^completion)(BOOL success);

// Runtime
NSClassFromString(@"SomeClass")
NSStringFromSelector(@selector(viewDidLoad))

You want recognition, not perfection.

The goal is to open old code and understand what is happening.


Should You Use Objective-C for a New App?

Usually no.

For a new app in 2026, use Swift.

Use Objective-C only if:

  • you are maintaining an old codebase
  • your team already has a large Objective-C app
  • you are building a compatibility layer
  • you are writing/maintaining an SDK
  • you need low-level runtime behavior
  • you have a very specific technical reason

Do not choose Objective-C because you want to be different.

That is not a strategy. That is self-harm with square brackets.


Where Objective-C Still Matters in 2026

Here is the realistic list:

  • old iOS apps
  • old macOS apps
  • AppKit projects
  • UIKit projects with long history
  • enterprise mobile codebases
  • banking/insurance/healthcare apps that move slowly
  • SDKs that support old customers
  • Swift migration projects
  • crash log analysis
  • XCTest helpers in older projects
  • analytics and instrumentation
  • method swizzling
  • Frida scripts
  • iOS/macOS reverse engineering
  • runtime debugging

That is enough relevance to not call it dead.

But it is not enough to call it the future.


Final Verdict

Objective-C is still relevant in 2026, but not as the main language for new Apple development.

Swift won the main road.

Objective-C still owns part of the basement.

If you are building new iOS apps, learn Swift first. If you are serious about Apple platforms, learn enough Objective-C to read old code, understand delegates/selectors, recognize runtime behavior, and debug mixed projects.

If you do QA, mobile security, or reverse engineering, Objective-C is even more useful because crash logs, runtime metadata, method names, selectors, and Frida hooks still expose a lot of Objective-C-shaped reality.

Objective-C refuses to die because Apple software has history.

And history does not disappear just because Xcode gives you a nicer template.