This article is part of the ‘New Beings’ series of articles from Zenitech and Stevens & Bolton, examining the practicalities, issues and possibilities of using AI in development. 

Today, we explore the practicalities of iOS development using AI.

Introduction

In this article, we delve into the coding phase, focusing on how AI tools, specifically ChatGPT and Copilot X, can accelerate iOS development. Having previously looked at the legal issues relating to AI, we can now explore how to harness the capabilities of these AI tools in building iOS applications.

Tools

We’ll focus on two AI tools in this article. 

  1. ChatGPT (GPT-4): A state-of-the-art Large Language Model (LLM) by OpenAI, ChatGPT is built on the GPT-4 architecture, specialising in generating human-like text and synthesising code.
  2. Copilot X: An AI code assistant based on a custom LLM, Copilot X provides context-aware code suggestions, optimising coding workflows. (Currently in private beta.)

The legal issues around the use of these tools are the subject of a dedicated article in this series, New Beings, Legal Issues and AI.

Overview of the task

For this test of AI tools, our iOS developers at Zenitech worked on an application for a fictional airport. The task was to create a feature-rich iOS app that integrates multiple screens, network communication to fetch real-time data, and local persistence for user-specific information. 

Through the course of development, we employed ChatGPT and Copilot X to expedite the coding process by creating Data Transfer Objects (DTOs), fetching flight data, and generating relevant UI code.

The fictional ‘client’: SZIA

The South-Zubony International Airport (SZIA), an imaginary airport located in Hungary, needs an application to improve the travel experience of passengers. The core purpose is to provide users with an intuitive and efficient way to access real-time flight information, airport news, maps, and a platform for submitting complaints. The scope of the application includes network communication for real-time data retrieval, local data persistence, multiple views for different features, and user interactions.

Functionalities: Arrivals, Departures, Complaints, Favourites

The application requires the following primary functionalities:

  • Arrivals: This screen showcases a list of arriving flights with the option for users to refresh the data.
  • Departures: Similar to arrivals, this screen displays a list of departing flights.
  • Complaints: Users can submit complaints through a form, which can include a text description and an optional image attachment.
  • Favourites: Users have the option to mark specific flights as favourites, which are then saved locally on the device for easy access.

These functionalities aim to provide a comprehensive travel tool to passengers, helping them stay informed and engaged throughout their journey.

Backend Integration

The backend system for the SZIA application is a REST API that provides endpoints for retrieving real-time flight information, news, and submitting complaints. The backend is accessible at https://szia-backend.autsoft.hu/api/, and is crucial in supplying the data needed for the application’s features. The endpoints include
- GET /Airlines,
- POST /Complaints,
- GET /Flights, and
- GET /News.

Introduction to the OpenAPI descriptor

An OpenAPI descriptor is provided for the backend, which is a specification document written in YAML or JSON (interchangeable). This descriptor provides a standardised description of the API’s endpoints, requests, responses, and other components. 

By using this OpenAPI descriptor, developers can understand the API’s capabilities and generate client libraries, server stubs, API documentation, and other code artefacts. This proves to be particularly beneficial when integrating ChatGPT and Copilot X, as it allows these AI tools to have a better understanding of the API structure and generate more accurate code.

Setting Up Development Environments

ChatGPT 4

Integrating ChatGPT into your development workflow is a relatively straightforward process. ChatGPT doesn’t require any special software installation; all you need is a functional web browser.

  • ChatGPT is accessible through web browsers and doesn’t necessitate a specific development environment.
  • Create an account on OpenAI’s platform if you don’t have one already.
  • Subscribe to ChatGPT Plus, which costs $20 per month. ChatGPT Plus is a subscription plan that offers a host of benefits including general access to ChatGPT even during peak times, faster response times, and priority access to new features and improvements (i.e. GPT-4).

Once you have subscribed to ChatGPT Plus, you can start using it (through your browser) to assist in generating code snippets, brainstorming ideas, or answering queries related to (iOS) development.

Copilot X

You need to fulfil certain prerequisites to harness the capabilities of Copilot X within Visual Studio Code – Insiders:

  • Ensure you have a GitHub account. (If not, you can create one here.)
  • Subscribe to GitHub Copilot, which is priced at $100 per year. You can subscribe to GitHub Copilot here.
  • Request access to the Copilot X private beta by visiting this page. It’s important to note that access to Copilot X is granted on an invitation basis, and you may have to wait for approval.
  • Once you have access to Copilot X, download and install Visual Studio Code – Insiders edition from this link.
  • After installing Visual Studio Code – Insiders, launch the application and go to the Extensions view by clicking on the Extensions icon in the Activity Bar on the side of the window.
  • Search for “GitHub Copilot Nightly” and install the extension. This extension is the nightly build of GitHub Copilot and includes the most recent features.
  • Additionally, search for “GitHub Copilot Chat” (Preview) and install this extension as well. This extension allows you to chat with Copilot X and get real-time coding assistance.
  • Activate these extensions by login into your GitHub account.
  • After installing the extensions and successfully activating them, you will notice a new icon appearing in the sidebar. This is where you can engage in a chat with Copilot X, ask questions, and receive code suggestions.

With Copilot X integrated into Visual Studio Code – Insiders, you are now equipped with a powerful AI assistant that can offer intelligent code suggestions and accelerate your (iOS) development process.

Copilot for Xcode – A Worthy Mention

While our experimentation primarily revolves around the integration of ChatGPT and Copilot X in Visual Studio Code – Insiders, there is another tool worth mentioning for the Xcode environment. Copilot for Xcode is an innovative extension that integrates the functionalities of ChatGPT and GitHub Copilot directly into Xcode.

This open-source extension operates by making API calls to offer real-time, AI-driven code suggestions. Kudos to the developers for actively contributing to this project!

However, it’s important to note that Copilot for Xcode uses API keys for its functionality. In the context of this article, integrating it wasn’t feasible as I didn’t have access to GPT-4 via API, and Copilot X also employs a different language model compared to Copilot, which would have made the comparison less equitable.

But Copilot for Xcode is definitely worth exploring. Even if just for experimentation, integrating it into Xcode could offer valuable insights into how AI-driven code suggestions can augment the development process. 

While Copilot X offers excellent integration within Visual Studio Code, it is being developed by Microsoft and lacks the capability to compile Xcode projects.

Copilot for Xcode presents an exciting opportunity for those eager to see AI in action within the Xcode environment.

Generating Networking Layer with AI Tools

First, let’s dive into creating the networking layer for our app. The network layer is what allows us to get and update data for our app. We’ll be using ChatGPT and Copilot X to speed up this task.

Please note that I’ve omitted some of the entities for the sake of brevity where I haven’t explicitly stated otherwise.

"Airline": {
    "properties": {
        "imageUrl": {
            "type": "string"
        },
        "name": {
            "type": "string"
        },
        "airlineCode": {
            "type": "string"
        },
        "id": {
            "type": "number",
            "format": "double"
        }
    },
    "additionalProperties": false
},
"Flight": {
    "properties": {
        "flightNumber": {
            "type": "string"
        },
        "departure": {
            "type": "string"
        },
        "arrival": {
            "type": "string"
        },
        "departureCity": {
            "type": "string"
        },
        "departureCode": {
            "type": "string"
        },
        "arrivalCity": {
            "type": "string"
        },
        "arrivalCode": {
            "type": "string"
        },
        "departureTime": {
            "type": "string",
            "format": "date-time"
        },
        "arrivalTime": {
            "type": "string",
            "format": "date-time"
        },
        "status": {
            "type": "string"
        },
        "checkinDeskNumber": {
            "type": "number",
            "format": "double"
        },
        "gateNumber": {
            "type": "number",
            "format": "double"
        },
        "delay": {
            "type": "number",
            "format": "double"
        },
        "comment": {
            "type": "string"
        },
        "id": {
            "type": "number",
            "format": "double"
        },
        "airlineId": {
            "type": "number",
            "format": "double"
        }
    },
    "required": [
        "flightNumber",
        "departure",
        "arrival",
        "departureTime",
        "arrivalTime",
        "status",
        "checkinDeskNumber",
        "gateNumber",
        "delay"
    ],
    "additionalProperties": false
}

Feeding the OpenAPI JSON to ChatGPT – Models

I gave a snippet of the OpenAPI definition to ChatGPT which describes the following models: Airline, Flight and asked it to generate Swift network models for my iOS application. Using the information from the OpenAPI definition, here is the first version of the generated Swift models for these entities.

Airline.swift

struct Airline: Codable {
    let imageUrl: String?
    let name: String?
    let airlineCode: String?
    let id: Double?
}

Flight.swift

struct Flight: Codable {
    let flightNumber: String
    let departure: String
    let arrival: String
    let departureCity: String?
    let departureCode: String?
    let arrivalCity: String?
    let arrivalCode: String?
    let departureTime: String
    let arrivalTime: String
    let status: String
    let checkinDeskNumber: Double
    let gateNumber: Double
    let delay: Double
    let comment: String?
    let id: Double?
    let airlineId: Double?
}
  • Codable: Each struct is marked with the Codable protocol, which permits both encoding to and decoding from a serialised format like JSON. ✅
  • Optional vs. Non-Optional Properties: Properties that are not explicitly required are declared as optional in Swift (appending a ? to the type). ✅
  • Type Consistency: The consistency with types are maintained (except for dates) as described in the OpenAPI document. For instance, properties defined as "type": "number", "format": "double" were declared as Double in Swift. 🟡
  • Date Handling: Properties such as departureTime and arrivalTime are currently strings, which is not ideal for date manipulation. This should be improved by using a more specialised type: Date. ❌
  • Equatable: The structs are not marked as Equatable, which is a protocol that allows us to compare two instances of the same type for equality. This is a useful feature to have when writing unit tests. 🟡
"departureTime": {
    "type": "string",
    "format": "date-time"
},

Telling ChatGPT to generate the models again taking into consideration the date-time format and also adding Equatable protocol conformance, I got the following updated versions of the models.

struct Airline: Codable, Equatable {
    let imageUrl: String?
    let name: String?
    let airlineCode: String?
    let id: Double?
}
struct Flight: Codable, Equatable {
    let flightNumber: String
    let departure: String
    let arrival: String
    let departureCity: String?
    let departureCode: String?
    let arrivalCity: String?
    let arrivalCode: String?
    let departureTime: Date
    let arrivalTime: Date
    let status: String
    let checkinDeskNumber: Double
    let gateNumber: Double
    let delay: Double
    let comment: String?
    let id: Double?
    let airlineId: Double?
}

I’m extremely satisfied with the results – the models are finely crafted, it’s as if I had written them by hand or employed a finely tuned custom OpenAPI generator.

Feeding the OpenAPI JSON to Copilot X – Models

Having swagger.json open in Visual Studio Code – Insiders edition, I invoked Copilot X to generate the models of the networking layer. Copilot X generated the following code.

struct SziaUserRequest: Codable {
    let username: String
    let email: String
    let id: Double
    let sziaUserId: Double
}

struct SziaUser: Codable {
    let username: String
    let email: String
    let id: Double
}
  • Codable: Each struct is marked with the Codable protocol, which permits both encoding to and decoding from a serialised format like JSON. ✅
  • Hallucinogenic entity: The SziaUserRequest struct is not even present in the OpenAPI definition. ❌
  • Optional vs. Non-Optional Properties: Properties that are not explicitly required are NOT declared as optional. ❌
  • Missing entities: Copilot X did not generate models for the other entities such as Airline, Flight, etc. ❌

Having swagger.json open in Visual Studio Code – Insiders edition, I explicitly selected the definition for the entities I wanted to generate, I invoked Copilot X to generate the models of the networking layer with specific instructions:

“Can you please generate Codable, Equatable Swift struct from the selected OpenAPI model definitions from swagger.json? Take into consideration that some properties are optional and that format: date-time should be a Date property.”

struct Airline: Codable, Equatable {
    let id: Double?
    let name: String?
    let airlineCode: String?
    let imageUrl: String?
}

struct Flight: Codable, Equatable {
    let flightNumber: String
    let departure: String
    let arrival: String
    let departureCity: String?
    let departureCode: String?
    let arrivalCity: String?
    let arrivalCode: String?
    let departureTime: Date
    let arrivalTime: Date
    let status: String
    let checkinDeskNumber: Double
    let gateNumber: Double
    let delay: Double
    let comment: String?
    let id: Double?
    let airlineId: Double?
}

This time I’ve got similar output to ChatGPT and Copilot X has generated the models for all the entities. ✅

Feeding the OpenAPI JSON to ChatGPT – Request/Response

I’ve told ChatGPT to generate the networking code for the GET `Flights“ endpoint without any other specific instructions, I’ve just given it the OpenAPI specification of that endpoint. Here is the result.

import Foundation

struct Flight: Decodable {
    // Assuming there are properties defined under '#/definitions/Flight'
}

class FlightAPI {
    let baseURL = "https://api.imaginaryairport.com"
    
    func getFlights(filter: String?, completion: @escaping (Result<[Flight], Error>) -> Void) {
        var urlString = "\(baseURL)/Flights"
        
        if let filter = filter {
            urlString.append("?filter=\(filter.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "")")
        }
        
        guard let url = URL(string: urlString) else {
            completion(.failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let data = data else {
                completion(.failure(NSError(domain: "", code: -2, userInfo: [NSLocalizedDescriptionKey: "No data received"])))
                return
            }
            
            do {
                let flights = try 
JSONDecoder().decode([Flight].self, from: data)
                completion(.success(flights))
            } catch {
                completion(.failure(error))
            }
        }.resume()
    }
}
  • Base URL: The generated code has a base URL property, which is a good practice, I just had to add it by hand pointing to the SZIA server. ✅
  • Error Handling: The generated code has a decent error handling. It checks for invalid URL, absence of data, and decoding errors, which are essential for stability. It doesn’t account for specific HTTP status codes or domain-specific errors, but these aren’t included in the OpenAPI specification either. We don’t usually use NSErrors, because we have Swift’s Error protocol to conform to and create meaningful errors. 🟡
  • Multithreading: The network request is made asynchronously using `URLSession.shared.dataTask(with:completion:)“. However, the completion handler is not explicitly dispatched to any queue which means it will run in the background thread. In most cases, you would want the completion handler to be executed on the main thread if it involves UI updates. 🟡
  • Working code?: The generated code is not working code. It results in a DecodingError for departureTime. ❌ (This is because this assignment doesn’t respect ISO8601 standard for date-time format on purpose as a gotcha for candidates.)

Giving the Flight object and the error message to ChatGPT, it generated a working fix and also dispatched the result to the main thread (Except in case of failure 😈). ✅

...
      do {
          let decoder = JSONDecoder()
          let dateFormatter = DateFormatter()
          dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
          decoder.dateDecodingStrategy = .formatted(dateFormatter)
          let flights = try decoder.decode([Flight].self, from: data)
          DispatchQueue.main.async {
              completion(.success(flights))
          }
      } catch {
          completion(.failure(error))
      }
...

I’m quite pleased with how the AI-generated code performed in handling the network request. But, I’m curious to explore if we can further refine and modernise this implementation by leveraging newer APIs such as Swift’s Combine framework or async/await, which can provide more elegant and streamlined solutions compared to the traditional completion handlers.

import Foundation

enum FlightAPIError: Error {
    case invalidURL
    case failedRequest
    case decodingError
}

struct FlightAPI {

    let baseURL: String

    func fetchFlights(filter: String? = nil) async throws -> [Flight] {
        var urlComponents = URLComponents(string: baseURL + "/Flights")
        if let filter = filter {
            urlComponents?.queryItems = [URLQueryItem(name: "filter", value: filter)]
        }

        guard let url = urlComponents?.url else {
            throw FlightAPIError.invalidURL
        }

        let (data, response) = try await URLSession.shared.data(from: url)

        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw FlightAPIError.failedRequest
        }

        do {
            let flights = try JSONDecoder().decode([Flight].self, from: data)
            return flights
        } catch {
            throw FlightAPIError.decodingError
        }
    }
}
  • Modern Swift Concurrency: The generated code is using the new async/await APIs and it’s also handling errors as I’ve requested. ✅
  • Working code?: The generated code is not working code. It results in the same DecodingError for departureTime. ChatGPT didn’t remember the fix it generated for the previous code. ❌

However, by giving the underlying decoding error to ChatGPT again, it generated a working fix.

...
    do {
      let decoder = JSONDecoder()
      let dateFormatter = DateFormatter()
      dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
      dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
      decoder.dateDecodingStrategy = .formatted(dateFormatter)
      let flights = try decoder.decode([Flight].self, from: data)
      return flights
    } catch {
      print("Decoding error: \(error)")
      throw FlightAPIError.decodingError
    }
...

ChatGPT handled the generation of network requests commendably, demonstrating an impressive capacity to adapt to requirements such as custom date formatting.

Feeding the OpenAPI JSON to Copilot X – Request/Response

I’ve also tried to feed the OpenAPI JSON to Copilot X to see how it would perform. I’ve used the same OpenAPI JSON file as before without giving specific instructions about how I want the networking code generated. Copilot X gave me the following code:

guard let url = URL(string: "https://example.com/api/Flight") else {
    fatalError("Invalid URL")
}

var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    guard let data = data, error == nil else {
        print("Error: \(error?.localizedDescription ?? "Unknown error")")
        return
    }

    do {
        let flights = try JSONDecoder().decode([Flight].self, from: data)
        print("Flights: \(flights)")
    } catch {
        print("Error decoding response: \(error.localizedDescription)")
    }
}

task.resume()
  • Base URL: The generated code doesn’t use a base URL property. It’s not a big deal here as the URL is hardcoded. However, it’s a good practice to have a base URL property in case of dynamic URLs. 🟡
  • Error Handling: The generated code does an ok job for error handling. In case of an invalid URL it crashes the app. This is fine here as this would be a programming error. Other times with dynamic urls, crashing the app is too extreme. 🟡
  • Multithreading: The network request is made asynchronously using URLSession.shared.dataTask(with:completion:). However, the completion handler is not explicitly dispatched to any queue which means it will run in the background thread. In most cases, you would want the completion handler to be executed on the main thread if it involves UI updates. 🟡
  • Filtering: The generated code doesn’t support filtering. ❌
  • Working code?: The generated code is not working code. It results in a DecodingError for departureTime. ❌

Giving the Flight object and the error message to Copilot X, it also generated a working fix. It is worth noting that I had to explicitly say that the date format is not ISO8601 to get the fix. ✅

let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
decoder.dateDecodingStrategy = .formatted(dateFormatter)

do {
    let flights = try decoder.decode([Flight].self, from: data)
    print("Flights: \(flights)")
} catch {
    print("Error decoding response: \(error.localizedDescription)")
}

I’ve also tried to feed Copilot X more specific instructions about how I want the networking code generated. I’ve asked it to use the new async/await APIs and handle errors. Copilot X gave me the following code:

enum FlightError: Error {
    case invalidURL
    case decodingError
}

func fetchFlights() async throws -> [Flight] {
    guard let url = URL(string: "https://example.com/api/Flight") else {
        throw FlightError.invalidURL
    }

    var request = URLRequest(url: url)
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")

    let (data, response) = try await URLSession.shared.data(for: request)

    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw FlightError.invalidURL
    }

    do {
        let flights = try JSONDecoder().decode([Flight].self, from: data)
        return flights
    } catch {
        throw FlightError.decodingError
    }
}
  • Modern Swift Concurrency: Pretty good! The generated code is using the new async/await APIs and it’s also handling errors like I’ve requested. ✅
  • Working code?: The generated code is not working code. It results in a DecodingError for departureTime. It didn’t remember the date formatter from the previous example. ❌

Giving the underlying decoding error to Copilot X, it generated a working fix. ✅

I’m still overall impressed with Copilot X; when provided with specific instructions, it delivers targeted and quality results.

Developing User Interface with AI Tools

Next, we’ll walk through the process of using AI tools, to develop the user interface of our application. Our focus will be on creating a SwiftUI List that displays flights, using the previously created Flight struct and the related network fetching code.

For our SwiftUI List, the requirements are:

  • A simple and visually appealing list.
  • Each list item should display data about the specific flight.
  • The list should be scrollable and populate dynamically based on the data fetched from the network call.
  • Appropriate formatting for the date and time.

ChatGPT

I’ve told ChatGPT about my Flight struct and the (`async throws“) network fetching code. I’ve also told it that I want to use SwiftUI to display a list of these Flights. Here’s what it generated:

import SwiftUI

struct FlightsListView: View {
    @State private var flights: [Flight] = []
    @State private var errorMessage: String? = nil
    
    private static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .short
        return formatter
    }()
    
    var body: some View {
        NavigationView {
            VStack {
                if let errorMessage = errorMessage {
                    Text(errorMessage)
                        .foregroundColor(.red)
                }
                
                List(flights) { flight in
                    VStack(alignment: .leading) {
                        Text(flight.flightNumber)
                            .font(.headline)
                        Text("\(flight.departure) - \(flight.arrival)")
                            .font(.subheadline)
                        HStack {
                            Text("Departure:")
                            Text(Self.dateFormatter.string(from: flight.departureTime))
                        }
                        HStack {
                            Text("Arrival:")
                            Text(Self.dateFormatter.string(from: flight.arrivalTime))
                        }
                        Text("Status: \(flight.status)")
                        if let delay = flight.delay, delay > 0 {
                            Text("Delayed by \(delay) minutes")
                                .foregroundColor(.red)
                        }
                        if let comment = flight.comment {
                            Text(comment)
                                .font(.caption)
                        }
                    }
                }
            }
            .navigationTitle("Flights")
        }
        .task {
            do {
                flights = try await fetchFlights()
            } catch {
                errorMessage = "Failed to fetch flights: \(error.localizedDescription)"
            }
        }
    }
}

The generated code is pretty good, but it’s not perfect and doesn’t even compile. Let’s go through the issues one by one.
Flight doesn’t conform to Identifiable protocol, so the list doesn’t know how to identify each item. 🟡 The fix is pretty easy, either make Flight Identifiable or make it Hashable and pass the \.self“ key path to the list’s init for the id parameter. I’ve chosen the latter not wanting to mess with the Flight` struct.

List(flights, id: \.self) { flight in
    ...
}
  • flight.delay is not optional, so the if let check is unnecessary. 🟡 Similarly it’s easy to remedy, just remove the unwrapping.
if flight.delay > 0 {
    Text("Delayed by \(flight.delay) minutes")
    .foregroundColor(.red)
}
  • fetchFlights is defined in FlightAPI. I consider it my fault as I haven’t told ChatGPT about it. ✅ The fix is easy, just instantiate and call FlightAPI.fetchFlights().

Apart from architectural improvements, I wanted to make two changes to the generated code:
– Don’t create a DateFormatter, just use Text’s date formatter initialiser.
– Add some images from SF Symbols to make it more visually appealing.

struct FlightsListView: View {
  @State private var flights: [Flight] = []
  @State private var errorMessage: String? = nil

  var body: some View {
    NavigationView {
      VStack {
        if let errorMessage = errorMessage {
          Text(errorMessage)
            .foregroundColor(.red)
            .padding()
        }

        List(flights, id: \.self) { flight in
          HStack(alignment: .top) {
            VStack(alignment: .leading) {
              HStack {
                Image(systemName: "airplane")
                  .foregroundColor(.blue)
                Text(flight.flightNumber)
                  .font(.headline)
              }
              Text("\(flight.departure) - \(flight.arrival)")
                .font(.subheadline)
              HStack {
                Image(systemName: "clock.arrow.circlepath")
                  .foregroundColor(.gray)
                Text("Departure:")
                Text(flight.departureTime, format: .dateTime)
              }
              HStack {
                Image(systemName: "clock.arrow.2.circlepath")
                  .foregroundColor(.gray)
                Text("Arrival:")
                Text(flight.arrivalTime, format: .dateTime)
              }
              HStack {
                Image(systemName: "info.circle")
                  .foregroundColor(.gray)
                Text("Status: \(flight.status)")
              }
              if flight.delay > 0 {
                HStack {
                  Image(systemName: "exclamationmark.triangle")
                    .foregroundColor(.red)
                  Text("Delayed by \(flight.delay) minutes")
                }
              }
              if let comment = flight.comment {
                HStack {
                  Image(systemName: "bubble.right")
                    .foregroundColor(.gray)
                  Text(comment)
                    .font(.caption)
                }
              }
            }
          }
          .padding(.vertical)
        }
      }
      .navigationTitle("Flights")
    }
    .task {
      do {
        flights = try await FlightAPI().fetchFlights()
      } catch {
        errorMessage = "Failed to fetch flights: \(error.localizedDescription)"
      }
    }
  }
}

The only minor issue is the incorrect usage of the Text initialiser, but it was only a syntax error.

Finally to add a cherry on top, I asked ChatGPT to generate a FlightDetailView with all the parameters it can display. It provided me with a nice view with the minor issues of optional handling and date formatting that we’ve already seen.

import SwiftUI

struct FlightDetailView: View {
  let flight: Flight

  private static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .short
    return formatter
  }()

  var body: some View {
    List {
      Section(header: Text("Flight Information")) {
        HStack {
          Image(systemName: "airplane")
          Text(flight.flightNumber)
            .font(.headline)
        }
        HStack {
          Image(systemName: "location")
          Text("\(flight.departure) - \(flight.arrival)")
        }
        HStack {
          Image(systemName: "clock.arrow.circlepath")
          Text("Departure: \(flight.departureTime, format: .dateTime)")
        }
        HStack {
          Image(systemName: "clock.arrow.2.circlepath")
          Text("Arrival: \(flight.arrivalTime, format: .dateTime)")
        }
      }

      Section(header: Text("Additional Information")) {
        HStack {
          Image(systemName: "info.circle")
          Text("Status: \(flight.status)")
        }
        if flight.delay > 0 {
          HStack {
            Image(systemName: "exclamationmark.triangle")
            Text("Delayed by \(flight.delay) minutes")
          }
        }
        if let comment = flight.comment {
          HStack {
            Image(systemName: "bubble.right")
            Text(comment)
          }
        }
        HStack {
          Image(systemName: "doc.text.magnifyingglass")
          Text("Check-in Desk: \(flight.checkinDeskNumber)")
        }

        HStack {
          Image(systemName: "arrow.turn.down.right")
          Text("Gate Number: \(flight.gateNumber)")
        }

        if let departureCity = flight.departureCity {
          HStack {
            Image(systemName: "building.columns")
            Text("Departure City: \(departureCity)")
          }
        }
        if let arrivalCity = flight.arrivalCity {
          HStack {
            Image(systemName: "building.columns.fill")
            Text("Arrival City: \(arrivalCity)")
          }
        }
        if let departureCode = flight.departureCode {
          HStack {
            Image(systemName: "barcode")
            Text("Departure Code: \(departureCode)")
          }
        }
        if let arrivalCode = flight.arrivalCode {
          HStack {
            Image(systemName: "barcode.viewfinder")
            Text("Arrival Code: \(arrivalCode)")
          }
        }
      }
    }
    .listStyle(.insetGrouped)
    .navigationTitle("Flight Details")
  }
}

After hooking it up to the navigation link, we have a simple, but functional app!

NavigationLink(destination: FlightDetailView(flight: flight)) {
...
}

Copilot X

Learning from my mistake from the model generation when trying out Copilot X, I’ve decided to be specific immediately:

“I have an array of Flight objects: “struct Flight: Codable, Equatable { let flightNumber: String let departure: String let arrival: String let departureCity: String? let departureCode: String? let arrivalCity: String? let arrivalCode: String? let departureTime: Date let arrivalTime: Date let status: String let checkinDeskNumber: Double let gateNumber: Double let delay: Double let comment: String? let id: Double? let airlineId: Double? }” I would like you to create a visually pleasing SwiftUI List. The Flight object doesn’t conform to Identifiable yet and I also want you to use the modern .task API as my fetchFlights API is async.”

import SwiftUI

struct FlightsListView: View {
    @State private var flights: [Flight] = []
    
    var body: some View {
        List(flights) { flight in
            VStack(alignment: .leading) {
                Text(flight.flightNumber)
                    .font(.headline)
                Text("\(flight.departure) - \(flight.arrival)")
                    .font(.subheadline)
                Text(flight.status)
                    .font(.subheadline)
            }
        }
        .task {
            do {
                let fetchedFlights = try await fetchFlights()
                self.flights = fetchedFlights
            } catch {
                print("Error fetching flights: \(error.localizedDescription)")
            }
        }
    }
    
    func fetchFlights() async throws -> [Flight] {
        // Your async fetchFlights implementation here
    }
}

struct FlightListView_Previews: PreviewProvider {
    static var previews: some View {
        FlightsListView()
    }
}

The generated code is mostly fine, but has pretty much the same issues as ChatGPT’s code. I had to modify List’s init to provide an id and also call the fetchFlights correctly.

Playing around with Copilot X, I wanted to achieve a similar result as with ChatGPT and show the departure and arrival times, format them using Text’s date formatter, add contextually relevant SF Symbol images to the Texts and create the detail view as well.

import SwiftUI

struct FlightsListView: View {
    @State private var flights: [Flight] = []
    
    var body: some View {
      NavigationView {
        List(flights, id: \.self) { flight in
            NavigationLink {
              FlightDetailView(flight: flight)
            } label: {
              HStack {
                  Image(systemName: "airplane")
                      .foregroundColor(.blue)
                  VStack(alignment: .leading) {
                      HStack {
                          Image(systemName: "number")
                              .foregroundColor(.gray)
                          Text(flight.flightNumber)
                              .font(.headline)
                      }
                      HStack {
                          Image(systemName: "arrow.up.right")
                              .foregroundColor(.gray)
                          Text("\(flight.departure) - \(flight.arrival)")
                              .font(.subheadline)
                      }
                      HStack {
                          Image(systemName: "clock")
                              .foregroundColor(.gray)
                          Text("Departure: \(Text(flight.departureTime, style: .time))")
                              .font(.subheadline)
                      }
                      HStack {
                          Image(systemName: "clock.fill")
                              .foregroundColor(.gray)
                          Text("Arrival: \(Text(flight.arrivalTime, style: .time))")
                              .font(.subheadline)
                      }
                      HStack {
                          Image(systemName: "info.circle")
                              .foregroundColor(.gray)
                          Text(flight.status)
                              .font(.subheadline)
                      }
                  }
              }
            }
          }
          .task {
              do {
                  let fetchedFlights = try await FlightAPI().fetchFlights()
                  self.flights = fetchedFlights
              } catch {
                  print("Error fetching flights: \(error.localizedDescription)")
              }
        }
      }
    }
}

struct FlightListView_Previews: PreviewProvider {
    static var previews: some View {
        FlightsListView()
    }
}

Copilot X seems to be performing slightly worse than ChatGPT, but it’s still pretty impressive.

Before Giving Too Many Points to ChatGPT

Also, before declaring ChatGPT the winner, I wanted to try out a few more things. I wanted to see how it would perform with more complex instructions.

“Can you add a little blue shadow to the top trailing corner of a list item?”

“Nearly but you are now not supporting dark mode, it’s not readable.”

“Change the list style to plain and only add shadow to the whole cell, not to every text”

“Can you please add pull-to-refresh functionality?”

As of my last knowledge update in September 2021, SwiftUI did not have a built-in pull-to-refresh feature. However, you can use a `UIRefreshControl` with a `UIScrollView` by wrapping it in a `UIViewRepresentable`.

Let's create a custom SwiftUI view named `RefreshableScrollView` which will integrate the UIKit's `UIRefreshControl` into a SwiftUI view:
...

SwiftUI does have a refreshable modifier on List from iOS 15, but still kudos to ChatGPT by solving the problem in a different way.

“I would like to use MVVM, can you please create a ViewModel for my FlightsListView?”

import Foundation

class FlightsViewModel: ObservableObject {
    @Published var flights: [Flight] = []
    @Published var errorMessage: String? = nil

    func fetchFlights() async {
        do {
            flights = try await FlightAPI().fetchFlights()
        } catch {
            errorMessage = "Failed to fetch flights: \(error.localizedDescription)"
        }
    }
}
struct FlightsListView: View {
    @StateObject private var viewModel = FlightsViewModel()

    var body: some View {
        NavigationView {
            VStack {
                if let errorMessage = viewModel.errorMessage {
                    Text(errorMessage)
                        .foregroundColor(.red)
                        .padding()
                }

                RefreshableScrollView(onRefresh: { await viewModel.fetchFlights() }) {
                    ForEach(viewModel.flights, id: \.self) { flight in
                        NavigationLink(destination: FlightDetailView(flight: flight)) {
                            FlightCell(flight: flight)
                        }
                    }
                }
                .padding(.horizontal)
            }
            .navigationTitle("Flights")
        }
        .task {
            await viewModel.fetchFlights()
        }
    }
}

“Where is FlightCell?”

import SwiftUI

struct FlightCell: View {
    var flight: Flight
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Image(systemName: "airplane")
                    .foregroundColor(.blue)
                Text(flight.flightNumber)
                    .font(.headline)
            }
            ...
        }
        .padding()
        .background(Color.secondarySystemBackground)
        .clipShape(RoundedRectangle(cornerRadius: 10))
        .shadow(color: .blue, radius: 5, x: -5, y: -5)
    }
}

Conclusion

Both ChatGPT 4 and Copilot X are formidable allies for iOS developers in their quest to augment the development process. 

ChatGPT 4 is the victor in generating higher-quality results across an array of tasks, albeit at a higher cost and with some usage constraints. 

Copilot X, though trailing slightly in performance, compensates with a seamless IDE integration, making it more convenient for the day-to-day workflow. 

Since both have their own set of advantages, it’s difficult to crown an indisputable champion. The decision hinges on individual priorities – whether one values quality and sophistication or integration and cost-effectiveness.

Context is Limited

When using ChatGPT or Copilot X, we need to recognise the limitations in their memory and input capacities. ChatGPT can handle 4096 tokens, while Copilot X is confined to 2048. (I’ve asked them to provide this data.) This necessitates conciseness and precision when dealing with extensive documentation or large-scale enterprise projects. 

However, the rapidly advancing AI landscape instills confidence that these token constraints will likely experience significant expansions in the near future.

Prompt Engineering and Expertise Matters

ChatGPT and Copilot X’s output quality is substantially enhanced through meticulous, prompt engineering and contextualisation. 

While they can generate code incorporating modern Swift Concurrency and Swift errors, these aren’t their defaults due to the abundance of completion handler solutions in their training data. 

This highlights the importance of domain expertise to steer the AI tools towards generating state-of-the-art code. These tools can be instrumental in accelerating the development process when wielded by skilled hands.

Generating Network Layer

We can clearly see the abilities of both ChatGPT and Copilot X in sculpting the network layer’s models, with ChatGPT taking an early lead. 

The models generated by Copilot X caught up once given more specific directives. The learning point here is not to presume Copilot X can rely solely on open files; it requires some guidance. 

Though traditional OpenAPI generators remain unchallenged in speed and consistency, ChatGPT’s ability to offer custom solutions and troubleshoot issues offers a versatile dynamism that is valuable. Both AI tools prove adept at refining .mustache files for OpenAPI generators.

ChatGPT 4 also demonstrated finesse in generating networking calls but needed some fine-tuning. Copilot X, though requiring more context and iterations, capitalised on its IDE integration for generating networking calls. 

In summary, while ChatGPT 4 had a slight edge in code accuracy, Copilot X was more convenient and responsive. Both significantly accelerated the development process, albeit with the need for some manual refinement.

UI is Still a Mostly Manual Process

ChatGPT 4 edges ahead of Copilot X in generating UI code, but is contingent on the provision of unambiguous directives. 

Both tools exhibited occasional syntactical slips and a limited acquaintance with cutting-edge SwiftUI features. 

Copilot X was noted for its imaginative tendencies (hallucination), conjuring non-existent UI elements. Though helpful in expediting the code-writing process, neither tool could replicate the finesse of UI designed by human experts. 

AI systems have yet to master the nuances of interpreting and executing complex designs. Thus, ChatGPT and Copilot X are best used as supplementary resources that complement human expertise rather than supplant it.

What Will Apple Do?

With Microsoft at the forefront with OpenAI-backed tools, Google close on its heels with Bard, and Meta also delving into the domain, my eyes are now on Apple. 

So far, Apple has remained tight-lipped, but the market dynamics strongly suggest that Apple might not be far behind in unveiling its own offering. 

While the recent WWDC was mostly bereft of announcements on this front, the tech community remains on tenterhooks for the next edition. Apple, known for its innovation, might just bring something groundbreaking to the table.

Bringing it All Together

In the realm of iOS development, AI tools like ChatGPT and Copilot X are carving out powerful accelerants that can streamline various aspects of the development lifecycle. 

While ChatGPT 4 emerges as the forerunner in generating sophisticated code, Copilot X’s integration with IDEs makes it a practical tool for daily use (if you don’t mind switching back and forth between Visual Studio Code and Xcode).

However, it is essential to recognise and navigate the limitations of these AI systems, especially in terms of token capacity and the necessity for precise prompts. 

As of now, when it comes to UI design, human creativity and expertise hold sway, as AI-generated code still lacks the finesse required for intricate designs. But, for generating network layers and other backend functionalities, AI tools are proving to be valuable.

It is essential for developers to remain abreast of the evolving AI landscape. The token limitations that are pertinent today may not be significant constraints tomorrow, and AI-generated code may soon rival human-generated code in sophistication.

Finally, in this highly competitive tech ecosystem, Apple’s entry into the AI tool space is eagerly anticipated. While ChatGPT and Copilot X are excellent tools in their own right, Apple’s innovation could redefine the standards and expectations.

In conclusion, integrating AI tools into (iOS) development is not just a fad, but an evolution that is here to stay. 

However, the use of AI does not obviate the need for human expertise. Rather, it’s the synergy of human intelligence and AI capabilities that will drive the future of (iOS) development. As developers, continually adapting and learning to harness these tools effectively is the key to staying relevant and successful in this ever-evolving landscape.

If you’re interested in learning more about our exploration of AI for development, see our previous articles in the ‘New Beings’ series.