Toolmingo
Guides19 min read

iOS Developer Roadmap 2025 (Step-by-Step Guide)

The complete iOS developer roadmap for 2025 — Swift, SwiftUI, UIKit, Xcode, architecture patterns, testing, and publishing to the App Store. Know exactly what to learn and in what order.

iOS powers over 1.5 billion active devices worldwide — iPhones, iPads, Apple Watches, and Macs (via Catalyst). iOS developers command some of the highest salaries in mobile and enjoy a famously engaged, high-monetisation user base. This roadmap shows you exactly what to learn, in what order, with realistic timelines to go from complete beginner to job-ready iOS developer in 2025.

At a glance

Phase Topics Time estimate
1 Swift fundamentals 4–6 weeks
2 Xcode & iOS app basics 3–4 weeks
3 SwiftUI (modern UI) 4–6 weeks
4 UIKit (legacy & advanced UI) 3–5 weeks
5 Architecture patterns 3–4 weeks
6 Data, networking & persistence 4–6 weeks
7 Testing 2–3 weeks
8 App Store & deployment 1–2 weeks
9 Advanced topics ongoing
10 Portfolio & job search 4–6 weeks

Total: 12–18 months to job-ready.


Phase 1 — Swift fundamentals

Swift is the primary language for iOS (and all Apple platform) development. Introduced in 2014, it replaced Objective-C for new codebases and is now mandatory knowledge for any iOS role.

Core Swift syntax

// Variables and constants
let name: String = "Alice"   // immutable (prefer this)
var age: Int = 30            // mutable

// Type inference
let pi = 3.14159             // Double inferred
var isLoggedIn = false       // Bool inferred

// Optionals — Swift's key safety feature
var email: String? = nil
let length = email?.count ?? 0   // optional chaining + nil coalescing

// Functions
func greet(name: String, greeting: String = "Hello") -> String {
    return "\(greeting), \(name)!"
}
print(greet(name: "Bob"))         // Hello, Bob!
print(greet(name: "Bob", greeting: "Hi"))  // Hi, Bob!

Collections

// Array
var fruits = ["apple", "banana", "cherry"]
fruits.append("date")
let first = fruits.first ?? "empty"

// Dictionary
var scores: [String: Int] = ["Alice": 95, "Bob": 82]
scores["Charlie"] = 78
let aliceScore = scores["Alice"] ?? 0

// Set
let uniqueNumbers: Set = [1, 2, 3, 2, 1]  // {1, 2, 3}

Control flow & closures

// If / guard
func processAge(_ age: Int?) {
    guard let age = age, age >= 0 else {
        print("Invalid age")
        return
    }
    print("Age: \(age)")
}

// Switch (exhaustive, no fallthrough)
let status = 404
switch status {
case 200:      print("OK")
case 400..<500: print("Client error")
default:       print("Other")
}

// Closures
let doubled = [1, 2, 3].map { $0 * 2 }     // [2, 4, 6]
let evens = [1, 2, 3, 4].filter { $0 % 2 == 0 }  // [2, 4]
let sum = [1, 2, 3, 4].reduce(0, +)         // 10

Structs, classes, and enums

Feature Struct Class
Memory Stack (value type) Heap (reference type)
Copying Creates new copy Shares reference
Inheritance
Preferred for Models, data ViewControllers, singletons
Default ✓ (Swift prefer)
// Struct (value type — preferred for models)
struct User {
    let id: UUID
    var name: String
    var email: String

    mutating func updateEmail(to email: String) {
        self.email = email
    }
}

// Enum with associated values
enum NetworkError: Error {
    case notFound(url: String)
    case serverError(code: Int)
    case decodingFailed(reason: String)
}

// Class (reference type)
class ViewModel: ObservableObject {
    @Published var items: [User] = []
}

Concurrency (Swift Concurrency — async/await)

// Async function
func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// Call from async context
Task {
    do {
        let user = try await fetchUser(id: 42)
        print(user.name)
    } catch {
        print("Error: \(error)")
    }
}

// Actor — protects shared state from data races
actor UserCache {
    private var cache: [Int: User] = [:]

    func user(for id: Int) -> User? { cache[id] }
    func store(_ user: User, for id: Int) { cache[id] = user }
}

What to learn in Phase 1:

  • Variables, types, optionals, control flow
  • Functions, closures, higher-order functions
  • Structs vs classes, protocols, extensions
  • Enums (especially with associated values)
  • Error handling (throw/try/catch)
  • Swift Concurrency (async/await, actors)
  • Generics

Phase 2 — Xcode & iOS app basics

Xcode is Apple's IDE — you cannot build or submit iOS apps without it.

Xcode essentials

Tool Purpose
Interface Builder / Storyboard Visual UI builder (legacy, still used)
SwiftUI Canvas Live preview for SwiftUI views
Simulator Test on virtual devices
Instruments Profile CPU, memory, network
Organizer Manage archives and crash logs
Debugger (LLDB) Breakpoints, memory inspection

iOS app structure

MyApp/
├── MyApp.swift          # @main entry point
├── ContentView.swift    # Root SwiftUI view
├── Assets.xcassets/     # Images, colors, icons
├── Info.plist           # App metadata & permissions
├── Preview Content/     # Preview assets (dev only)
├── Models/              # Data models
├── Views/               # SwiftUI views
├── ViewModels/          # MVVM logic
├── Services/            # Networking, storage
└── Resources/           # Localisation, fonts

App lifecycle

@main
struct MyApp: App {
    @StateObject private var appState = AppState()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appState)
        }
    }
}
Lifecycle state When triggered
onAppear View appears on screen
onDisappear View leaves screen
scenePhase .active App in foreground
scenePhase .background App backgrounded
scenePhase .inactive Transitioning

Info.plist permissions

Always declare why you need each permission (Apple will reject apps without NSUsageDescription strings):

<key>NSCameraUsageDescription</key>
<string>Used to scan QR codes and capture profile photos.</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>Used to show nearby stores on the map.</string>

<key>NSPhotoLibraryUsageDescription</key>
<string>Used to let you choose a profile picture.</string>

Phase 3 — SwiftUI (modern UI framework)

SwiftUI is Apple's declarative UI framework introduced in 2019. All new Apple-platform apps should use SwiftUI. Start here — learn UIKit later for legacy code and advanced customisation.

SwiftUI fundamentals

import SwiftUI

struct ProfileView: View {
    let user: User
    @State private var isExpanded = false

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            AsyncImage(url: user.avatarURL) { image in
                image.resizable().scaledToFill()
            } placeholder: {
                ProgressView()
            }
            .frame(width: 80, height: 80)
            .clipShape(Circle())

            Text(user.name)
                .font(.title2)
                .fontWeight(.bold)

            Text(user.email)
                .foregroundStyle(.secondary)

            if isExpanded {
                Text(user.bio)
                    .transition(.opacity)
            }

            Button(isExpanded ? "Show less" : "Show more") {
                withAnimation { isExpanded.toggle() }
            }
        }
        .padding()
    }
}

Core layout building blocks

Container Purpose
VStack Vertical stack
HStack Horizontal stack
ZStack Overlapping layers
LazyVStack / LazyHStack Lazy-loaded stacks (large lists)
Grid 2D grid layout (iOS 16+)
ScrollView Scrollable container
List Scrollable list with built-in accessory views
NavigationStack Push/pop navigation (iOS 16+)
TabView Tab bar navigation
GeometryReader Read parent geometry

State management in SwiftUI

Property wrapper Used for
@State Local view state (value types)
@Binding Two-way binding from parent
@StateObject Owned observable object
@ObservedObject Injected observable object
@EnvironmentObject Dependency passed through view tree
@Environment System values (colorScheme, locale, dismiss)
@AppStorage UserDefaults persistence
// ObservableObject (iOS 13–16 compatible)
class CartViewModel: ObservableObject {
    @Published var items: [CartItem] = []
    @Published var isLoading = false

    var total: Double { items.reduce(0) { $0 + $1.price } }

    func addItem(_ item: CartItem) {
        items.append(item)
    }
}

// Observable macro (iOS 17+, Swift 5.9)
@Observable
class CartViewModel {
    var items: [CartItem] = []
    var isLoading = false

    var total: Double { items.reduce(0) { $0 + $1.price } }
}

Navigation (NavigationStack — iOS 16+)

struct RootView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            ProductListView(path: $path)
                .navigationDestination(for: Product.self) { product in
                    ProductDetailView(product: product)
                }
                .navigationDestination(for: CartItem.self) { item in
                    ItemDetailView(item: item)
                }
        }
    }
}

Phase 4 — UIKit (legacy & advanced UI)

UIKit has powered iOS apps since 2008 and remains necessary for:

  • Maintaining existing codebases
  • Custom UI components not yet in SwiftUI
  • Complex gestures and animations
  • Interoperability (use UIKit inside SwiftUI via UIViewRepresentable)

UIViewController lifecycle

class ProductViewController: UIViewController {
    // 1. View loaded from nib/storyboard or programmatic init
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        bindViewModel()
    }

    // 2. About to appear on screen
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        viewModel.refresh()
    }

    // 3. Fully visible
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Start animations, track analytics
    }

    // 4. About to disappear
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Pause video, cancel timers
    }

    // 5. Gone from screen
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
    }
}

UITableView vs UICollectionView

UITableView UICollectionView
Layout Single column only Any layout (grid, waterfall, carousel)
Use case Settings, feeds, lists Photos, cards, complex layouts
Diffable data source
iOS 16+ replacement List (SwiftUI) LazyVGrid (SwiftUI)

Integrating UIKit in SwiftUI

struct MapViewRepresentable: UIViewRepresentable {
    @Binding var region: MKCoordinateRegion

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_ uiView: MKMapView, context: Context) {
        uiView.setRegion(region, animated: true)
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapViewRepresentable
        init(_ parent: MapViewRepresentable) { self.parent = parent }
    }
}

Phase 5 — Architecture patterns

iOS apps need clear architecture to stay maintainable as they grow.

Architecture patterns comparison

Pattern Separation Testability Complexity Common in
MVC Low Hard Low Legacy UIKit apps
MVVM Good Good Medium SwiftUI, modern UIKit
MVP Good Excellent Medium UIKit TDD teams
VIPER High Excellent High Large enterprise apps
TCA Very high Excellent High Functional SwiftUI apps

MVVM with SwiftUI (recommended starting point)

// Model
struct Article: Identifiable, Codable {
    let id: UUID
    let title: String
    let body: String
    let publishedAt: Date
}

// ViewModel
@Observable
class ArticleListViewModel {
    var articles: [Article] = []
    var isLoading = false
    var errorMessage: String?

    private let service: ArticleServiceProtocol

    init(service: ArticleServiceProtocol = ArticleService()) {
        self.service = service
    }

    func loadArticles() async {
        isLoading = true
        defer { isLoading = false }
        do {
            articles = try await service.fetchArticles()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

// View
struct ArticleListView: View {
    @State private var viewModel = ArticleListViewModel()

    var body: some View {
        Group {
            if viewModel.isLoading {
                ProgressView()
            } else if let error = viewModel.errorMessage {
                Text(error).foregroundStyle(.red)
            } else {
                List(viewModel.articles) { article in
                    ArticleRow(article: article)
                }
            }
        }
        .task { await viewModel.loadArticles() }
        .navigationTitle("Articles")
    }
}

Dependency injection

// Protocol for testability
protocol ArticleServiceProtocol {
    func fetchArticles() async throws -> [Article]
}

// Production implementation
struct ArticleService: ArticleServiceProtocol {
    func fetchArticles() async throws -> [Article] {
        let url = URL(string: "https://api.example.com/articles")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([Article].self, from: data)
    }
}

// Test double
struct MockArticleService: ArticleServiceProtocol {
    var result: Result<[Article], Error> = .success([])
    func fetchArticles() async throws -> [Article] {
        try result.get()
    }
}

Phase 6 — Data, networking & persistence

Networking with URLSession

struct NetworkService {
    private let session: URLSession
    private let decoder: JSONDecoder

    init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        self.session = URLSession(configuration: config)
        self.decoder = JSONDecoder()
        self.decoder.keyDecodingStrategy = .convertFromSnakeCase
        self.decoder.dateDecodingStrategy = .iso8601
    }

    func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
        let (data, response) = try await session.data(from: url)

        guard let http = response as? HTTPURLResponse,
              (200..<300).contains(http.statusCode) else {
            throw NetworkError.serverError(code: (response as? HTTPURLResponse)?.statusCode ?? 0)
        }

        return try decoder.decode(T.self, from: data)
    }
}

Persistence options

Storage Use case Apple framework
UserDefaults Small preferences (theme, language, login state) Foundation
Keychain Sensitive data (tokens, passwords) Security.framework
FileManager Documents, images, large files Foundation
Core Data Complex relational data, offline-first apps CoreData
SwiftData Modern Core Data replacement (iOS 17+) SwiftData
SQLite / GRDB Direct SQL access Third-party
CloudKit iCloud sync CloudKit

SwiftData (iOS 17+ — modern persistence)

import SwiftData

// Define model
@Model
class Task {
    var title: String
    var isCompleted: Bool
    var createdAt: Date

    init(title: String) {
        self.title = title
        self.isCompleted = false
        self.createdAt = .now
    }
}

// App setup
@main
struct TaskApp: App {
    var body: some Scene {
        WindowGroup {
            TaskListView()
        }
        .modelContainer(for: Task.self)
    }
}

// View with query
struct TaskListView: View {
    @Query(sort: \Task.createdAt, order: .reverse) var tasks: [Task]
    @Environment(\.modelContext) private var context

    var body: some View {
        List(tasks) { task in
            TaskRow(task: task)
        }
        .toolbar {
            Button("Add") {
                context.insert(Task(title: "New task"))
            }
        }
    }
}

Keychain (secure storage)

import Security

struct KeychainService {
    static func save(key: String, data: Data) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data
        ]
        SecItemDelete(query as CFDictionary)
        SecItemAdd(query as CFDictionary, nil)
    }

    static func load(key: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var dataTypeRef: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
        return status == errSecSuccess ? dataTypeRef as? Data : nil
    }
}

Phase 7 — Testing

Apple's testing stack covers unit tests, UI tests, and performance tests without needing third-party libraries.

Testing pyramid

Level Framework Runs Speed Confidence
Unit XCTest In-process Fast High
Integration XCTest + URLProtocol In-process Medium High
Snapshot swift-snapshot-testing In-process Fast Medium
UI / E2E XCUITest On device Slow Very high

Unit testing with XCTest

import XCTest
@testable import MyApp

final class ArticleListViewModelTests: XCTestCase {
    var sut: ArticleListViewModel!
    var mockService: MockArticleService!

    override func setUp() {
        super.setUp()
        mockService = MockArticleService()
        sut = ArticleListViewModel(service: mockService)
    }

    override func tearDown() {
        sut = nil
        mockService = nil
        super.tearDown()
    }

    func test_loadArticles_success_populatesArticles() async {
        // Arrange
        let expected = [Article(id: UUID(), title: "Test", body: "Body", publishedAt: .now)]
        mockService.result = .success(expected)

        // Act
        await sut.loadArticles()

        // Assert
        XCTAssertEqual(sut.articles.count, 1)
        XCTAssertEqual(sut.articles.first?.title, "Test")
        XCTAssertFalse(sut.isLoading)
        XCTAssertNil(sut.errorMessage)
    }

    func test_loadArticles_failure_setsErrorMessage() async {
        // Arrange
        mockService.result = .failure(NetworkError.serverError(code: 500))

        // Act
        await sut.loadArticles()

        // Assert
        XCTAssertTrue(sut.articles.isEmpty)
        XCTAssertNotNil(sut.errorMessage)
    }
}

SwiftUI View testing (ViewInspector or XCUITest)

// XCUITest — black-box UI test
final class OnboardingUITests: XCTestCase {
    var app: XCUIApplication!

    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launchArguments = ["--uitesting", "--reset-state"]
        app.launch()
    }

    func test_onboarding_completionShowsHomeScreen() {
        // Tap through onboarding
        app.buttons["Get started"].tap()
        app.buttons["Continue"].tap()
        app.buttons["Allow Notifications"].tap()

        // Assert home screen visible
        XCTAssertTrue(app.navigationBars["Home"].exists)
    }
}

Phase 8 — App Store & deployment

Build configuration

Configuration Purpose
Debug Development, with symbols & sanitisers
Staging QA, TestFlight, with staging backend
Release Production, optimised, stripped symbols

Code signing & provisioning

  1. Apple Developer Account ($99/year) — mandatory for device testing and distribution
  2. Certificates — identify you as the developer (Development + Distribution)
  3. Bundle ID — unique reverse-domain identifier (com.company.appname)
  4. Provisioning profile — ties your app, certificate, and device list together
  5. Xcode Automatic Signing — handles most of this automatically

GitHub Actions CI/CD

name: iOS CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: macos-14

    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode
        run: sudo xcode-select -s /Applications/Xcode_16.0.app

      - name: Install dependencies
        run: |
          gem install bundler
          bundle install
          bundle exec pod install  # if using CocoaPods

      - name: Run tests
        run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0' \
            -resultBundlePath TestResults.xcresult \
            CODE_SIGNING_ALLOWED=NO

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: TestResults.xcresult

App Store submission checklist

  • App icon (1024×1024 PNG, no alpha) in Assets.xcassets
  • Launch screen (LaunchScreen.storyboard or SwiftUI)
  • Screenshot sets for required device sizes (iPhone 6.7", 6.5", iPad Pro)
  • App Privacy declaration (what data you collect and why)
  • Age rating completed
  • Review notes (explain any permissions or non-obvious features)
  • TestFlight beta tested on real devices
  • Version number bumped (CFBundleShortVersionString + CFBundleVersion)

Phase 9 — Advanced topics

Swift Concurrency deep dive

// Structured concurrency — async let
func loadDashboard() async throws -> Dashboard {
    async let user = fetchUser()
    async let feed = fetchFeed()
    async let notifications = fetchNotifications()

    // All three run in parallel; wait for all
    return Dashboard(
        user: try await user,
        feed: try await feed,
        notifications: try await notifications
    )
}

// TaskGroup — dynamic parallelism
func loadImages(urls: [URL]) async throws -> [UIImage] {
    try await withThrowingTaskGroup(of: UIImage.self) { group in
        for url in urls {
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                guard let image = UIImage(data: data) else {
                    throw ImageError.invalidData
                }
                return image
            }
        }
        return try await group.reduce(into: []) { $0.append($1) }
    }
}

Performance optimisation

Problem Tool Fix
Dropped frames Instruments → Core Animation Reduce body recomputation, use Equatable
Memory leaks Instruments → Leaks Use [weak self] in closures
CPU spikes Instruments → Time Profiler Move work off main thread
Large images Instruments → Memory Downsample with ImageRenderer or vImage
Slow lists Xcode Canvas profiler Use LazyVStack, diffable data sources

Accessibility (non-negotiable)

Image("chart")
    .accessibilityLabel("Revenue chart")
    .accessibilityValue("$45,000 in July, up 12% from June")

Button("Delete") { delete() }
    .accessibilityHint("Removes this item permanently")

// Group related elements
HStack {
    Image(systemName: "star.fill")
    Text("4.8 stars")
}
.accessibilityElement(children: .combine)

Push notifications (APNs)

// Request permission
UNUserNotificationCenter.current().requestAuthorization(
    options: [.alert, .badge, .sound]
) { granted, error in
    if granted {
        DispatchQueue.main.async {
            UIApplication.shared.registerForRemoteNotifications()
        }
    }
}

// Handle token in AppDelegate
func application(_ app: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
    let tokenString = token.map { String(format: "%02.2hhx", $0) }.joined()
    // Send tokenString to your server
}

Widgets (WidgetKit)

struct ArticleWidget: Widget {
    let kind = "ArticleWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: ArticleProvider()) { entry in
            ArticleWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Latest Article")
        .description("Shows the latest article from your feed.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

Full technology map

iOS Developer Stack
│
├── Language
│   ├── Swift (mandatory)
│   └── Objective-C (legacy reading/interop)
│
├── UI Frameworks
│   ├── SwiftUI (new apps — start here)
│   └── UIKit (legacy, custom components, interop)
│
├── Architecture
│   ├── MVVM (most common with SwiftUI)
│   ├── MVC (legacy UIKit)
│   └── TCA / VIPER (enterprise)
│
├── Data
│   ├── SwiftData (iOS 17+)
│   ├── Core Data (iOS 13+, proven)
│   ├── UserDefaults (preferences)
│   └── Keychain (secrets)
│
├── Networking
│   ├── URLSession (built-in)
│   └── Alamofire (third-party, optional)
│
├── Async
│   ├── Swift Concurrency (async/await)
│   └── Combine (reactive, still used)
│
├── Testing
│   ├── XCTest (unit + integration)
│   └── XCUITest (UI/E2E)
│
├── Package Management
│   ├── Swift Package Manager (preferred)
│   └── CocoaPods / Carthage (legacy)
│
├── DevOps
│   ├── Xcode Cloud (Apple CI/CD)
│   ├── GitHub Actions + xcodebuild
│   └── Fastlane (automation)
│
└── Distribution
    ├── TestFlight (beta)
    └── App Store Connect (production)

Realistic 14-month timeline

Month Focus Milestone
1–2 Swift fundamentals + Xcode Build 3 command-line tools
3–4 SwiftUI basics Build a to-do app
5–6 SwiftUI advanced + networking Build a news reader with API
7–8 UIKit fundamentals Port SwiftUI app to UIKit
9–10 Architecture + persistence Add Core Data / SwiftData to news app
11 Testing Reach 80% unit test coverage
12 Advanced topics (widgets, notifications) Ship to TestFlight
13 App Store submission First app live on the App Store
14 Job search Portfolio = 3 shipped apps

Portfolio project ideas

Project Skills demonstrated Difficulty
To-Do App SwiftUI, MVVM, SwiftData Beginner
Weather App Networking, async/await, JSON decoding Beginner
Crypto Tracker Charts, WebSockets, Combine Intermediate
Social Feed Pagination, image caching, UITableView Intermediate
Habit Tracker with Widgets WidgetKit, SwiftData, notifications Intermediate
Podcast App AVFoundation, background audio, CarPlay Advanced

iOS developer roles and salaries (2025)

Role Experience Salary (US) Salary (EU)
Junior iOS Developer 0–2 years $70k–$100k €35k–€55k
Mid-level iOS Developer 2–5 years $100k–$150k €55k–€80k
Senior iOS Developer 5+ years $150k–$200k €80k–€120k
Staff / Principal iOS Engineer 8+ years $200k–$300k+ €120k–€180k
iOS Tech Lead 7+ years $180k–$250k €100k–€150k
Mobile Architect 10+ years $200k–$350k+ €130k–€200k

Salaries vary significantly by company (FAANG vs startup vs agency) and location.


Common mistakes

Mistake Problem Fix
Ignoring optionals Runtime crashes on nil Use if let, guard let, nil coalescing
Skipping UIKit Can't maintain legacy code Learn UIKit basics after SwiftUI
Forgetting @MainActor UI updates from background thread Mark ViewModels with @MainActor
Strong reference cycles Memory leaks in closures Use [weak self] / [unowned self]
No accessibility labels App fails App Store review Add accessibilityLabel to images/icons
Hardcoding values Brittle code Use Constants.swift or environment config
Skipping tests Regressions ship to users Test ViewModels; 80%+ unit coverage
One big ViewController Impossible to maintain Extract services, ViewModels, child VCs

iOS vs Android vs Cross-platform

iOS (Swift/SwiftUI) Android (Kotlin/Compose) React Native Flutter
Language Swift Kotlin JavaScript/TypeScript Dart
UI framework SwiftUI / UIKit Jetpack Compose / XML React Flutter widgets
Performance Native Native Near-native Near-native
Code sharing None (native) None (native) Cross-platform Cross-platform
Platform access Full Full Limited Good
App Store approval Strict Lenient Strict Strict
Market Higher revenue/user Larger user base Both Both
Best for High-revenue iOS apps Android-first products Web teams going mobile Single codebase priority

Frequently asked questions

Do I need a Mac to develop iOS apps?
Yes. Xcode runs only on macOS. You need a Mac (any modern one), or a cloud Mac service like MacStadium or GitHub Actions macOS runners for CI.

Should I learn UIKit or SwiftUI first?
Start with SwiftUI. It is Apple's strategic direction and most new iOS jobs use it. Learn UIKit basics after — you will need it for legacy codebases and advanced custom UI components.

Is Objective-C still required?
Rarely. New jobs almost never require Objective-C, but you should be able to read it when maintaining old code or calling legacy APIs.

Do I need a paid Apple Developer account?
To test on a real device (free tier limited to 7-day signing) and to submit to the App Store ($99/year), yes. For Simulator-only development, a free Apple ID is enough.

What package manager should I use?
Swift Package Manager (SPM) — it is built into Xcode and the Apple-endorsed solution. Use CocoaPods only for libraries that haven't migrated to SPM yet.

How long does App Store review take?
Typically 24–48 hours for standard reviews. Rejection is common the first time — read the App Store Review Guidelines carefully before submitting.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools