Native iOS Architecture | Morton Software Group
Platforms — Morton Software Group

Native iOS
Architecture.

Structure is a product decision. The architecture choices made before the first screen is built determine whether a product can evolve — or whether it has to be rewritten.

Morton Software Group
MVVM
Primary Pattern
Model-View-ViewModel with strict separation of concerns. No business logic in views. No network calls in ViewControllers.
15 min
Cache TTL Standard
In-memory caching with 15-minute time-to-live on all external API responses — eliminating redundant calls without serving stale data.
Zero
Force Unwraps
Force unwrapping is prohibited in production codebases. Every optional is handled explicitly. Crashes from nil are not acceptable.
@MainActor
UI Thread Enforcement
All ViewModels are marked @MainActor — enforcing that UI updates happen on the main thread at compile time, not as a runtime assumption.
Not Generic MVVM. Our Specific Pattern.

MVVM is a broad pattern. How you implement it determines everything. Here is exactly how Morton Software Group structures the three layers across every product.

// Layer One
Model

Plain Swift structs representing data from external sources. Codable for automatic JSON decoding. No business logic, no formatting, no UI concerns. A model is data — nothing else.

Each external data source has its own typed model. AQI data, fire detection data, and weather data are separate structs — never combined into a generic dictionary.

struct AQIResponse: Codable
struct FireDetection: Codable
enum ConditionStatus
// Layer Two
ViewModel

@MainActor ObservableObject classes that own business logic, manage state, and coordinate with Services. @Published properties drive UI updates. Singleton shared instances prevent duplicate API calls across screens.

Cache TTL logic lives here — not in the View, not in the Service. The ViewModel decides when data is fresh enough to serve from memory versus when to refetch.

@MainActor final class ViewModel
@Published var status: Status
static let shared = ViewModel()
// Layer Three
View

SwiftUI Views are thin, declarative, and dumb. They observe ViewModel state via @StateObject or @EnvironmentObject and render accordingly. No network calls, no business logic, no direct model access.

A View should be replaceable without touching anything outside itself. If changing a View requires touching a ViewModel or Service, the View has too much responsibility.

@StateObject var vm: ViewModel
.onAppear { vm.onAppear() }
Text(vm.status.description)
// Service Layer Pattern
Protocol-Backed Service Architecture.

Every external data source in a Morton Software Group application is backed by a protocol — making the service swappable, testable, and isolated from the ViewModel that consumes it. This is a representative example of how the service layer is structured.

EnvironmentalService.swift
1234 5678 9101112 13141516 17181920 21222324 25262728 29303132 33343536 37383940 41424344
import Foundation import CoreLocation // MARK: - Protocol protocol EnvironmentalServiceProtocol { func fetchConditions( at coordinate: CLLocationCoordinate2D ) async throws -> ConditionResponse } // MARK: - Live Implementation final class EnvironmentalService: EnvironmentalServiceProtocol { static let shared = EnvironmentalService() private init() {} private let session: URLSession = .shared func fetchConditions( at coordinate: CLLocationCoordinate2D ) async throws -> ConditionResponse { guard let url = Endpoint.conditions(coordinate).url else { throw ServiceError.invalidURL } let (data, response) = try await session.data(from: url) guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { throw ServiceError.badResponse } return try JSONDecoder().decode(ConditionResponse.self, from: data) } } // MARK: - Error Types enum ServiceError: Error { case invalidURL case badResponse case decodingFailed }
Opinionated by Design.

Good architecture is as much about what you refuse to do as what you do. These are the patterns Morton Software Group explicitly avoids in production codebases — and what we use instead.

Avoided Pattern
MVC with Fat ViewControllers

The traditional Apple MVC pattern encourages ViewControllers to handle network calls, data formatting, navigation logic, and UI updates simultaneously. In practice this produces ViewControllers with hundreds of lines of mixed-responsibility code that is impossible to test and painful to modify.

We use MVVM — ViewControllers and Views own zero business logic
Avoided Pattern
Business Logic in Views

SwiftUI makes it easy to write conditional logic, data formatting, and decision trees directly in the view body. This is a trap — logic in views cannot be tested, cannot be reused, and couples the display layer to the data layer in ways that make both harder to change.

All logic lives in ViewModels — Views render state, never compute it
Avoided Pattern
Nested Completion Handlers

Completion-handler-based async code creates nested callback pyramids that are difficult to read, impossible to reason about, and painful to debug when errors need to propagate through multiple layers. Pre-Swift Concurrency codebases are full of this pattern.

Async/await throughout — flat, readable, structured error handling
Avoided Pattern
Force Unwrapping Optionals

The exclamation mark operator in Swift is a statement that a value will never be nil — a guarantee the compiler cannot verify. Every force unwrap is a deferred crash waiting for a production edge case that wasn't tested. One nil from an API response brings down the app.

Guard statements, optional binding, and nil-coalescing everywhere
Avoided Pattern
Uncached API Calls

Making a network request every time a view appears — including on tab switches, scroll events, and background-to-foreground transitions — creates unnecessary latency, drains battery, and hammers external APIs with redundant requests that return identical data.

In-memory caching with TTL in every ViewModel that touches external data
Avoided Pattern
Thread-Unsafe UI Updates

Updating UI from a background thread causes runtime warnings at best and unpredictable crashes at worst — often appearing only under specific timing conditions that don't reproduce in development. The bug is architectural, not incidental.

@MainActor on all ViewModels — thread safety enforced at compile time
These Patterns in Production.

Abstract architecture principles only matter when they are applied to real products. Here is how these specific patterns appear in Morton Software Group applications.

// Environmental Intelligence Application
Multi-Source Data Architecture

An environmental intelligence product integrating five federal data sources requires an architecture that can handle parallel async fetches, independent caching per data source, graceful degradation when one source fails, and a composite decision engine that synthesizes results into a single status.

The specific architectural decisions that make this work:

@MainActor on all ViewModels — UI always updates safely
singleton shared ViewModels — one fetch serves all screens
TTL cache per service — 15-minute freshness window
protocol per data source — swappable without touching consumers
async/await Task groups — parallel fetches, no blocking
enum composite status — single source of truth for UI state
// Product — Luxe Pulse
Game State Architecture

A precision timing game requires an architecture where game state transitions are immediate, score calculations are deterministic, and UI rendering never competes with game logic on the main thread. The architecture must guarantee that a tap at the wrong millisecond is the player's fault — not a threading issue.

The specific architectural decisions that make this work:

enum game state machine — explicit transitions, no ambiguity
@Published score — SwiftUI re-renders only what changed
@StateObject at root — single game VM for entire session
Codable leaderboard models — clean GameKit integration
defer cleanup — game resources released on session end
StoreKit isolated service — purchase logic never in game VM

Architecture Cost
Compounds.

The cost of bad architecture is not paid at launch. It is paid in every feature added after launch — in the extra time it takes to implement changes safely, in the bugs introduced by modifications to code that was never designed to be modified, and in the eventual decision to rewrite rather than refactor.

Morton Software Group builds architecture for the twelfth month of a product's life, not the first. The right structure at the start is the most cost-effective decision in iOS development — because every subsequent decision is made in the context of it.

Month 1 — Well-Architected
Higher Upfront Investment
Architecture design, protocol definitions, and service layer setup take more time than writing feature code directly. This cost is paid once.
Month 3 — First Features Added
New Features Are Isolated
Adding a new data source means adding a new Service conforming to an existing protocol. Existing ViewModels and Views are untouched. Risk is minimal.
Month 6 — Product Evolves
Refactors Are Surgical
Changing how data is fetched means changing one Service class. Changing how it is displayed means changing one View. The layers don't bleed into each other.
Month 12 — Scale Achieved
Codebase Still Readable
A developer who has never seen the codebase can locate any feature in minutes because the structure is consistent and predictable throughout. No rewrite required.