How I Optimize Swift and iOS Development Projects (2026 Workflow)

I optimize iOS projects by starting with the toolchain: Swift 6 language mode from day one, Swift Package Manager instead of CocoaPods, and Xcode's build timeline to find real bottlenecks. Then I fix concurrency and build settings before touching app code. Toolchain first, code second.
Part of the complete app development guide.
On this page
- Start with the toolchain, not the code
- The concurrency model is where most apps get slow
- Where people get this wrong
- Build times: measure before you optimize
- Binary size and startup
- Why this matters for the business side
- Testing without slowing yourself down
- FAQ
- Should I upgrade an existing project to Swift 6?
- Is Swift Package Manager ready for everything?
- How do I know if my app is actually slow?
Most “Swift optimization” advice online is either ten years old or written by someone who has never waited four minutes for a clean build on a MacBook Air. This is the workflow I actually use in 2026 for client iOS and macOS work, plus the specific things that make builds faster, binaries smaller, and code easier to hand off. If you are hiring this out, it is also a decent checklist for what you should expect from whoever you hire.
Start with the toolchain, not the code
Swift 6 shipped strict concurrency checking as the default language mode, and Xcode 16 and later pushed the whole ecosystem toward it. If you are still on Swift 5 language mode in a new project, you are borrowing pain from your future self. Migrating later means touching every type that crosses an actor boundary, which is a lot more work than starting clean.
The practical setup I use on a fresh project:
- Swift 6 language mode from day one, even if it means annotating a few types as
@MainActorearly. - Swift Package Manager for everything that is not a binary-only dependency. CocoaPods is maintenance mode at this point and I would not start a new project on it.
- Xcode’s build timeline turned on so I can see what is actually slow instead of guessing.
- A
Package.swiftwith explicit platform minimums, because implicit minimums cause weird “works on my machine” builds.
The concurrency model is where most apps get slow
Strict concurrency is not just a correctness thing. It forces you to be honest about which code runs where, and that usually exposes the real performance problems. A common one: doing JSON decoding on the main actor because it was convenient, then wondering why scrolling stutters.
Decoding is CPU work. Keep it off the main actor:
struct FeedResponse: Decodable, Sendable {
let items: [FeedItem]
}
actor FeedLoader {
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .shared) {
self.session = session
let d = JSONDecoder()
d.dateDecodingStrategy = .iso8601
self.decoder = d
}
func load(url: URL) async throws -> FeedResponse {
let (data, _) = try await session.data(from: url)
// Decoding happens off the main actor because we're inside an actor.
return try decoder.decode(FeedResponse.self, from: data)
}
}
@MainActor
final class FeedViewModel {
private let loader = FeedLoader()
var items: [FeedItem] = []
func refresh() async {
do {
let response = try await loader.load(
url: URL(string: "https://example.com/feed")!
)
items = response.items
} catch {
// Handle the error. Do not silently swallow it.
}
}
}
Note the Sendable conformance on the model type. That is not decoration. Without it, you get either a compile error under Swift 6 or a runtime data race you will spend a weekend chasing.
Where people get this wrong
- Marking an entire view model
@MainActorand then doing network and parsing inside it. The annotation is correct, the work placement is not. - Using
Task.detachedto “get off the main thread” without realizing it loses priority and task-local values. - Reaching for
@unchecked Sendableto silence the compiler. That is a promise you probably cannot keep.
Build times: measure before you optimize
Clean build times creep up quietly. A project that built in 40 seconds in 2023 can easily be at three minutes now, and nobody noticed because everyone just gets coffee. A few things that reliably help:
- Turn on whole module optimization for release builds only. It slows debug builds and helps release builds, so keep them separate.
- Check for type-checking blowups. One deeply nested generic expression or a big array literal with mixed types can add seconds to every file that touches it. Xcode will warn you with a compiler time flag.
- Split oversized files. Swift compiles per file for incremental builds, so a 3,000-line view controller is a 3,000-line recompile every time you touch it.
- Prune dependencies. Every package you pull in has its own build graph. I have seen apps drop a full minute of build time by removing two packages that were used for one helper function each.
Binary size and startup
App Store limits and download-over-cellular thresholds still matter, especially for anything with a large asset catalog. Two rules I follow:
- Use asset catalogs with vector PDFs or SVG for icons rather than shipping three PNG sizes, and let the catalog do the work.
- Strip unused code paths in release. Dead code stripping plus link-time optimization is a one-line build setting that often saves a few megabytes on a large app.
For startup, the biggest wins are almost always lazy initialization. If you are doing setup work in application(_:didFinishLaunchingWithOptions:) or in a global let that runs at first access, that cost shows up before the user sees anything. Move it behind the point where it is actually needed.
Why this matters for the business side
I do this work out of Commack, which means a lot of my projects are small teams and local businesses who need an app that ships and stays maintainable, not a research project. The difference between a codebase that is easy to hand off and one that is not usually comes down to the choices above, made early. If you are pairing an iOS app with a web presence, the same discipline applies on the web side, and I cover that under custom web development.
Testing without slowing yourself down
Swift Testing replaced XCTest for most new unit tests, and it is genuinely nicer. Tests are async by default, assertions read better, and parameterized tests remove a lot of copy-paste.
import Testing
@Test("Feed decodes ISO8601 dates")
func feedDecoding() throws {
let json = """
{"items":[{"id":"1","date":"2026-01-15T10:00:00Z"}]}
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let response = try decoder.decode(FeedResponse.self, from: json)
#expect(response.items.count == 1)
}
Keep unit tests fast and free of network calls. UI tests are slow, so I use them sparingly for the handful of flows that would cost real money to break.
FAQ
Should I upgrade an existing project to Swift 6?
Yes, but incrementally. Turn on strict concurrency per target, fix the errors target by target, and do not try to convert the whole app in one branch. Budget more time than you think, especially if the codebase uses a lot of callbacks.
Is Swift Package Manager ready for everything?
For most dependencies, yes. You still need a fallback for the rare binary-only SDK, but for typical networking, persistence, and utility libraries, SPM is the default choice and works well.
How do I know if my app is actually slow?
Profile it. Instruments still gives you the clearest picture of where time goes, and Xcode’s build timeline tells you whether the problem is your code or your build setup. Guessing at this almost always sends you in the wrong direction.


