DEV Community

Cover image for XCUITest Page Object Model: Build Maintainable iOS UI Tests with Swift
QAPulse by SK
QAPulse by SK

Posted on Originally published at skakarh.com

XCUITest Page Object Model: Build Maintainable iOS UI Tests with Swift

XCUITest Page Object Model is a practical design approach for organizing iOS UI automation into reusable screen-level components instead of placing every locator, action, wait, and assertion directly inside test cases. For growing XCUITest suites, this separation improves maintainability, readability, reuse, debugging, and team scalability.

What is XCUITest Page Object Model?

XCUITest Page Object Model is an automation design pattern where each important iOS screen or reusable UI component is represented by a Swift class containing its locators and user actions.

Instead of writing this directly inside every test:

let email = app.textFields["login.email"]
email.tap()
email.typeText("qa@example.com")

let password = app.secureTextFields["login.password"]
password.tap()
password.typeText("Password123")

app.buttons["login.button"].tap()
Enter fullscreen mode Exit fullscreen mode

A Page Object encapsulates the implementation:

let loginPage = LoginPage(app: app)

loginPage.login(
    email: "qa@example.com",
    password: "Password123"
)
Enter fullscreen mode Exit fullscreen mode

The test focuses on business behavior, while the Page Object manages UI implementation details.

Definition

XCUITest Page Object Model is a Swift-based test architecture that encapsulates iOS screen locators, interactions, synchronization, and reusable UI behavior inside dedicated Page Object classes.

Key Points

  • Create one Page Object for each important screen.
  • Keep locators inside Page Objects.
  • Keep reusable actions inside Page Objects.
  • Keep business scenarios inside test classes.
  • Prefer accessibility identifiers.
  • Centralize synchronization.
  • Avoid duplicated UI queries.
  • Return Page Objects when navigation changes screens.
  • Keep assertions close to the state they validate.
  • Avoid putting test-specific business logic into Page Objects.
  • Use component objects for reusable UI sections.
  • Keep Page Objects small and focused.
  • Use dependency injection for XCUIApplication.
  • Design Page Objects around user behavior, not implementation details.
  • Refactor repeated workflows into reusable methods.

Why Use the Page Object Pattern in XCUITest?

A small UI test suite can survive without an architectural layer.

For example:

func testLogin() {
    let app = XCUIApplication()
    app.launch()

    app.textFields["login.email"]
        .tap()

    app.textFields["login.email"]
        .typeText("qa@example.com")

    app.secureTextFields["login.password"]
        .tap()

    app.secureTextFields["login.password"]
        .typeText("Password123")

    app.buttons["login.button"]
        .tap()
}
Enter fullscreen mode Exit fullscreen mode

The problem appears when dozens of tests repeat the same locators.

Imagine 40 tests containing:

app.textFields["login.email"]
Enter fullscreen mode Exit fullscreen mode

If the accessibility identifier changes, every test potentially needs modification.

With a Page Object:

final class LoginPage {

    private let app: XCUIApplication

    init(app: XCUIApplication) {
        self.app = app
    }

    private var emailField:
        XCUIElement {
        app.textFields["login.email"]
    }

    private var passwordField:
        XCUIElement {
        app.secureTextFields["login.password"]
    }

    private var loginButton:
        XCUIElement {
        app.buttons["login.button"]
    }
}
Enter fullscreen mode Exit fullscreen mode

The locator exists in one place.

That is the central architectural advantage.

Page Object Model Architecture

A scalable XCUITest project can follow this structure:

XCUITest Target
│
├── Tests
│   ├── LoginTests.swift
│   ├── CheckoutTests.swift
│   └── SearchTests.swift
│
├── Pages
│   ├── LoginPage.swift
│   ├── HomePage.swift
│   ├── SearchPage.swift
│   └── CheckoutPage.swift
│
├── Components
│   ├── NavigationBar.swift
│   ├── ProductCard.swift
│   └── AlertComponent.swift
│
├── Helpers
│   ├── WaitHelper.swift
│   └── TestData.swift
│
└── Base
    └── BaseTest.swift
Enter fullscreen mode Exit fullscreen mode

This separates responsibilities.

Tests
  ↓
Pages
  ↓
Components
  ↓
XCUITest API
  ↓
iOS Application
Enter fullscreen mode Exit fullscreen mode

Page Objects vs Test Cases

A test case should describe what the user is trying to achieve.

A Page Object should describe how the user interacts with a screen.

Test

func testSuccessfulLogin() {

    let loginPage =
        LoginPage(app: app)

    let homePage =
        loginPage.login(
            email: "qa@example.com",
            password: "Password123"
        )

    XCTAssertTrue(
        homePage.isDisplayed
    )
}
Enter fullscreen mode Exit fullscreen mode

Page Object

final class LoginPage {

    private let app: XCUIApplication

    init(app: XCUIApplication) {
        self.app = app
    }

    func login(
        email: String,
        password: String
    ) -> HomePage {

        emailField.tap()
        emailField.typeText(email)

        passwordField.tap()
        passwordField.typeText(password)

        loginButton.tap()

        return HomePage(app: app)
    }
}
Enter fullscreen mode Exit fullscreen mode

The test is easier to understand because implementation details are hidden.

Building Your First Page Object

Start with the screen’s elements.

For a login screen:

Login Screen
├── Email
├── Password
├── Login
├── Forgot Password
└── Sign Up
Enter fullscreen mode Exit fullscreen mode

Create:

final class LoginPage {

    private let app: XCUIApplication

    init(app: XCUIApplication) {
        self.app = app
    }
}
Enter fullscreen mode Exit fullscreen mode

👉 Continue reading the full article on skakarh.com →

Originally published at skakarh.com/xcuitest-page-object-model.
Subscribe to QA Pulse by SK
weekly signal for QA, Test Automation and AI in Software Engineering.

Top comments (0)