iOS 27 introduces a new role for TabView: .prominent. Apply it to a tab, and the system gives that tab extra visual emphasis in the tab bar — larger, more eye-catching, easier to find at a glance than the tabs around it.
Apps have always had a destination that matters more than the rest. A shopping app leans on the cart. A travel app leans on trips. A banking app leans on payments. Before iOS 27, giving that one destination extra weight meant building a custom tab bar from scratch. Now it’s a single role modifier on a standard Tab.
In this tutorial, you’ll build a small shopping app called QuickCart and give its Cart tab the .prominent treatment, complete with a live item-count badge. By the end, you'll know exactly how the role behaves, how to wire it into an @Observable MVVM structure, and how to carry the same pattern into any app where one destination deserves the spotlight.
What You’ll Build
QuickCart — a four-tab shopping app:
Home, Search, Cart, and Profile tabs in a standard
TabViewThe Cart tab styled with the new
.prominentroleA live badge on the Cart tab showing the current item count
A fully working
CartViewbacked by an@Observableview model, with quantity steppers and a running subtotal
The finished app runs on the iOS 27 simulator with zero third-party dependencies — everything here is stock SwiftUI.
Prerequisites
Xcode 27 or later
iOS 27 SDK (simulator or device)
Swift 6.2+
Familiarity with SwiftUI,
NavigationStack, and the@ObservablemacroNo Apple Developer account needed — this runs entirely in the simulator
Project Setup
Create a new project in Xcode: File → New → Project → iOS → App, interface set to SwiftUI, name it QuickCart.
Set up the following folder structure inside the project navigator:
QuickCart/
├── App/
│ └── QuickCartApp.swift
├── Core/
│ └── Models/
│ └── CartItem.swift
├── Features/
│ ├── Cart/
│ │ ├── CartViewModel.swift
│ │ └── CartView.swift
│ ├── Home/
│ │ └── HomeView.swift
│ ├── Search/
│ │ └── SearchView.swift
│ └── Profile/
│ └── ProfileView.swift
└── UI/
└── QuickCartTabView.swiftNothing here is .prominent-specific yet — it's just a clean shell to build into.
Step-by-Step Implementation
Step 1: Define the Cart Model
Start with the data. A cart is a list of items, each with a quantity.
// Core/Models/CartItem.swift
import Foundationstruct CartItem: Identifiable, Hashable {
let id: UUID
let name: String
let price: Decimal
var quantity: Int
}What just happened? A plain value type — no logic, no dependencies. Keeping the model dumb makes it trivial to test and safe to pass around by value.
Step 2: Build the Cart View Model
The view model owns cart state and exposes the two numbers the UI cares about: how many items, and what they cost.
// Features/Cart/CartViewModel.swift
import Foundation@Observable
final class CartViewModel {
private(set) var items: [CartItem] = [
CartItem(id: UUID(), name: "Trail Running Shoes", price: 128.00, quantity: 1),
CartItem(id: UUID(), name: "Insulated Water Bottle", price: 24.50, quantity: 2)
] var itemCount: Int {
items.reduce(0) { $0 + $1.quantity }
} var subtotal: Decimal {
items.reduce(Decimal.zero) { $0 + ($1.price * Decimal($1.quantity)) }
} func updateQuantity(for item: CartItem, to newQuantity: Int) {
guard let index = items.firstIndex(where: { $0.id == item.id }) else { return }
items[index].quantity = max(1, newQuantity)
} func remove(_ item: CartItem) {
items.removeAll { $0.id == item.id }
}
}What just happened? @Observable replaces ObservableObject here — no @Published needed on the stored properties. itemCount is a computed property, so it recalculates automatically any time items changes, which matters in a couple of steps when this number ends up on the tab badge.
Step 3: Build the Cart Destination View
This is the screen the Cart tab navigates to.
// Features/Cart/CartView.swift
import SwiftUI
struct CartView: View {
@State private var viewModel = CartViewModel()
var body: some View {
NavigationStack {
List {
ForEach(viewModel.items) { item in
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(item.name)
.font(.body.weight(.medium))
Text(item.price, format: .currency(code: "USD"))
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Stepper(
"\(item.quantity)",
value: Binding(
get: { item.quantity },
set: { viewModel.updateQuantity(for: item, to: $0) }
),
in: 1...10
)
.labelsHidden()
.fixedSize()
}
}
.onDelete { indexSet in
indexSet.map { viewModel.items[$0] }.forEach(viewModel.remove)
}
Section {
HStack {
Text("Subtotal")
.font(.headline)
Spacer()
Text(viewModel.subtotal, format: .currency(code: "USD"))
.font(.headline)
}
}
}
.navigationTitle("Cart")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Checkout") {
// Hook up your checkout flow here
}
.disabled(viewModel.items.isEmpty)
}
}
}
}
}What just happened? @State private var viewModel = CartViewModel() is the correct way to hold an @Observable instance in SwiftUI — not @StateObject, since @Observable types don't conform to ObservableObject. Everything else is a standard List with a Stepper bound through a computed Binding, which lets the row read and write quantity without owning any state itself.
Step 4: Build the Remaining Destinations
The other three tabs just need something to show. Keep them simple for now.
// Features/Home/HomeView.swift
import SwiftUI
struct HomeView: View {
var body: some View {
NavigationStack {
Text("Featured products")
.navigationTitle("Home")
}
}
}
// Features/Search/SearchView.swift
import SwiftUI
struct SearchView: View {
var body: some View {
NavigationStack {
Text("Search products")
.navigationTitle("Search")
}
}
}
// Features/Profile/ProfileView.swift
import SwiftUI
struct ProfileView: View {
var body: some View {
NavigationStack {
Text("Account details")
.navigationTitle("Profile")
}
}
}What just happened? Nothing exciting yet — that’s the point. These exist so the tab bar has four real destinations to compare against once Cart gets its .prominent styling.
Step 5: Assemble the Standard Tab Structure
Before touching .prominent, wire up a normal TabView with all four destinations.
// UI/QuickCartTabView.swift
import SwiftUI
struct QuickCartTabView: View {
var body: some View {
TabView {
Tab("Home", systemImage: "house") {
HomeView()
}
Tab("Search", systemImage: "magnifyingglass") {
SearchView()
}
Tab("Cart", systemImage: "cart.fill") {
CartView()
}
Tab("Profile", systemImage: "person") {
ProfileView()
}
}
}
}What just happened? This is the iOS 27 Tab builder syntax — each Tab takes a title, an SF Symbol, and a trailing closure for its destination. Build and run now, and all four tabs look identical in weight. That's what .prominent changes next.
Step 6: Apply the .prominent Role
Add the role to the Tab initializer for Cart, and switch to the label-closure form so you can control the icon and title directly.
Tab(role: .prominent) {
CartView()
} label: {
Label("Cart", systemImage: "cart.fill")
}Drop that in place of the plain Cart tab:
struct QuickCartTabView: View {
var body: some View {
TabView {
Tab("Home", systemImage: "house") {
HomeView()
}
Tab("Search", systemImage: "magnifyingglass") {
SearchView()
}
Tab(role: .prominent) {
CartView()
} label: {
Label("Cart", systemImage: "cart.fill")
}
Tab("Profile", systemImage: "person") {
ProfileView()
}
}
}
}What just happened? role: .prominent tells the system to render this tab with extra visual weight — a larger icon treatment relative to its neighbors in the bar. The destination and navigation behavior are unchanged; tapping it still pushes CartView() exactly like any other tab. Build and run, and Cart now visually stands out from Home, Search, and Profile.
Step 7: Add a Live Badge to the Prominent Tab
A prominent tab pairs naturally with a badge, since both are ways of surfacing “this matters right now.” Bring in the view model at the tab level and attach .badge().
struct QuickCartTabView: View {
@State private var cartViewModel = CartViewModel()
var body: some View {
TabView {
Tab("Home", systemImage: "house") {
HomeView()
}
Tab("Search", systemImage: "magnifyingglass") {
SearchView()
}
Tab(role: .prominent) {
CartView()
} label: {
Label("Cart", systemImage: "cart.fill")
}
.badge(cartViewModel.itemCount)
Tab("Profile", systemImage: "person") {
ProfileView()
}
}
}
}What just happened? .badge() accepts any Int, and since itemCount is a computed property on an @Observable view model, the badge updates automatically the moment items changes — no manual refresh logic required. Right now cartViewModel here and the one inside CartView are separate instances; in a real app you'd inject a single shared instance so both stay in sync, which the next section covers.
Step 8: Wire It Into the App Entry Point
Finish by pointing the app’s entry point at QuickCartTabView.
// App/QuickCartApp.swift
import SwiftUI
@main
struct QuickCartApp: App {
var body: some Scene {
WindowGroup {
QuickCartTabView()
}
}
}Sharing State Across the Badge and the Cart
To keep the badge and the cart contents in sync, share one view model instance instead of creating two.
struct QuickCartTabView: View {
@State private var cartViewModel = CartViewModel()
var body: some View {
TabView {
Tab("Home", systemImage: "house") {
HomeView()
}
Tab("Search", systemImage: "magnifyingglass") {
SearchView()
}
Tab(role: .prominent) {
CartView(viewModel: cartViewModel)
} label: {
Label("Cart", systemImage: "cart.fill")
}
.badge(cartViewModel.itemCount)
Tab("Profile", systemImage: "person") {
ProfileView()
}
}
}
}Update CartView to accept the shared instance instead of creating its own:
struct CartView: View {
var viewModel: CartViewModel
var body: some View {
// body unchanged from Step 3
}
}What just happened? CartView now takes viewModel as a plain property instead of owning it with @State. Because CartViewModel is a reference type marked @Observable, SwiftUI still tracks changes to it correctly — updating a quantity inside CartView immediately reflects in the tab badge, since both views are watching the same object.
Running the App
Build and run on an iOS 27 simulator. You’ll see:
Home, Search, and Profile rendered as standard tabs
Cart rendered with the
.prominentstyling — visually heavier than its neighborsA badge on the Cart tab showing
3— 1 pair of Trail Running Shoes plus 2 Water BottlesAdjusting a quantity in
CartViewupdates the badge in real time, since both views share the sameCartViewModel
Tapping the Cart tab behaves exactly like tapping any other tab — it navigates to CartView(). The only difference .prominent introduces is visual weight in the bar itself.
Key Takeaways
Tab(role: .prominent) { destination } label: { ... }is the full syntax — one role, one destination closure, one label.prominentchanges visual weight in the tab bar only; navigation behavior is identical to a standard tabPair it with
.badge()for a live count that draws even more attention to the destinationBack it with an
@Observableview model shared across the tab and its destination view so badges and content stay in sync automaticallyUse
@State— not@StateObject— to hold@Observableinstances in SwiftUI
Where to Take This Next
Swap
CartItemfor a SwiftData@Modeland persist the cart across launchesExplore
TabViewCustomizationin iOS 27 to let users reorder the non-prominent tabs while Cart stays pinned in placeApply the same pattern to other destinations — a Tickets tab in a ticketing app, a Trips tab in a travel app, or a Pay tab in a banking app all follow the identical
Tab(role: .prominent) { ... } label: { ... }structure shown here