My experience building an iOS App with the help of Chat GPT-4 (#1)

How I built a SplashScreen and basic navigation.

Andrzej Ryl
16 min readMay 8, 2023

Intro for newcomers

I’m an Android developer, never had any experience with iOS and yet I’m building a full iOS version of my own Android app called Uncover with the help of Chat GPT.

Here is the link to intro of this series — https://medium.com/@andrzej.ryl/my-experience-building-an-ios-app-with-the-help-of-chat-gpt-4-intro-dc5e402a6ea8

Ok! Let’s start. In this post I will try to show you how I started this journey and what I was able to achieve in a fairly short amount of time.

I started with a very simple message to Chat GPT

hello! I would like to write an iOS application from scratch. 
I would like it to be written in Swift programming language,
use SwiftUI for UI of the application. Can you help me do that?

And what happened? Chat actually treated me as a total beginner, which I am, and answered with this

Hello! I'd be happy to help you get started with writing an iOS application 
using Swift and SwiftUI.
Before we get started, make sure you have Xcode installed on your Mac.
Xcode is the integrated development environment (IDE) that Apple provides
for developing software for macOS, iOS, iPadOS, watchOS, and tvOS.
You can download Xcode for free from the Mac App Store.

Once you have Xcode installed, follow these steps to create a new iOS project
in Swift with SwiftUI:

* Open Xcode and select "Create a new Xcode project" from the main menu.
* In the "Choose a template for your new project" window,
select "App" under "iOS."
* Choose a name for your project, and make sure the "Language"
field is set to "Swift."
* Choose "SwiftUI" for the user interface.
* Click "Next" and choose a location to save your project.
* Click "Create" to create your project.

Congratulations! You have now created a new iOS project in Swift with SwiftUI.
You should see an empty app on the screen.

Easy, right? I actually went with those steps and yeah… he was right. I already had my first iOS app just sitting there and waiting for me to continue. Chat continued with some simple examples on how to change UI but I already knew that I wanted something different. I don’t want to learn iOS. I know this is a long and complicated process. I just want to write an app that will work as a good enough MVP that I can show to my potential investors and that can help me gain traction on iOS platform.

This is what I had right now


// UncoverApp.swift
import SwiftUI

@main
struct UncoverApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}


// ContentView.swift
import SwiftUI

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

I noticed that the main component was extending something called “ContentView”. I got a feeling that it may be close to Android activity so I just simply asked Chat to explain it to me like I’m an Android dev and damn…

ME:
what is the ContentView? Please be aware that I am an Android developer
(proficient with Kotlin and Android SDK)

CHAT:
Sure, let me explain. In an iOS app developed using SwiftUI,
the ContentView is the main view that is displayed when the app is launched.

The ContentView is similar to the "activity" or "fragment" concept
in Android development. It's the root view that defines the overall
layout and structure of the app's user interface.

In SwiftUI, the ContentView is defined as a struct conforming to the View
protocol, and it usually contains a body property that returns a view
hierarchy...

He then continued to explain to me how this works under the hood and what should I do to change the layout.

I decided to use that knowledge and started working on my first screen — a splash screen which makes a backend call downloading some initial information (like the flag specifying if backend is not down for maintenance), saves it in DB and then continues to the main screen. This is what I gave him

I would like to create a splash screen. Can you help me with that? 
It should just display a png picture, make a call to my backend server,
save some data in a database and then proceed to the main screen

What he gave me was quite interesting. I got a full view (displaying some generic image), a method which called backend url (he just used https://your-backend-server.com/api/data URL) at the startup of the screen and some generic file with Swift code actually creating Entity in Core database and saving values to it. I haven’t run it because I immediately knew that those things are not connected whatsoever but it was an interesting first try. Obviously he did explain everything to me so I immediately understood what was written in the code.

I did however notice that he was using some weird navigation logic which I’m not accustomed to on Android platform. We usually have separate Activities or Fragments and just navigate between them. Chat however generated quite complex ContentView which would be hard to maintain

struct ContentView: View {
@State private var showSplash = true

var body: some View {
ZStack {
if showSplash {
SplashScreenView()
.onAppear {
// I later learned from Chat that this was his way
// of imitating backend call. Just wait for 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.showSplash = false
}
}
} else {
MainView()
}
}
}
}

As you can see, he’s using showSplash variable and simply changes the views in if condition. That would be a nightmare if I were to create many more screens (views in his approach). So I tried to convince him to switch to more Android-like approach — multiple screens with a clear navigation between them. After couple of failed approaches I finally asked the right questions and got something close to what I was hoping for

ME:
let's say we have many screens in the app. How would you structure main
view to be able to navigate between those screens?

CHAT:
In SwiftUI, you can structure your app's main view hierarchy using
a NavigationView to enable navigation between multiple ContentViews.
Here's an example of how you can structure your main view to achieve this:
struct MainView: View {
var body: some View {
NavigationView {
VStack {
List {
NavigationLink(destination: FirstContentView()) {
Text("First ContentView")
}
NavigationLink(destination: SecondContentView()) {
Text("Second ContentView")
}
// Add more NavigationLinks for other ContentView(s) here
}
}
.navigationBarTitle("Main View")
}
}
}

Problem was that he needed buttons (in this case simple Text views) which after clicking would take you to destination view. I had to remind him of our original idea: splash screen shows up, makes a call to a backend, saves data in DB and then navigates to onboarding screen or main screen (based on data in DB). This is when he started to loose his mind a little bit. He constantly created only parts of the solution I wanted. Maybe it’s because I used trial version back then or it was too complex all-together for him (we will see next time when I’m using full Chat GPT Plus version) but overall I was able to get something like that after combining those 3 things.

struct ContentView: View {
@AppStorage("hasSeenOnboarding") var hasSeenOnboarding = false
@State private var isActive = false

var body: some View {
ZStack {
SplashScreenView()
if isActive {
NavigationLink(
destination: isActive ? MainScreen() : OnboardingScreen(),
isActive: $isActive,
label: { EmptyView() }
)
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.isActive = true
}
}
}
}

struct OnboardingScreen: View {
@AppStorage("hasSeenOnboarding") var hasSeenOnboarding = false

var body: some View {
VStack {
Text("Welcome to the Onboarding Screen!")
Button(action: {
hasSeenOnboarding = true
}) {
Text("Continue to Main Screen")
}
}
}
}

struct MainScreen: View {
var body: some View {
Text("Welcome to the Main Screen!")
}
}

This code basically makes a call to backend (simulated by 2 s wait), changes the flag called isActive and then navigates to Main or Onboarding screen based on some flag kept in DB. As you can see, he already created Onboarding and Main screen and even created (by himself. I haven’t suggested that) a button on Onboarding screen that would flip hasSeenOnboarding flag and navigate to Main screen.

Not bad I must say… BUT when I put everything together I started getting error in XCode

And let me tell you… errors in XCode are… hmm… weird 🤣

What’s more weird is the fact that Chat and XCode were 100% certain that it was me who messed up. I kept posting my exact solution and error I’m getting to Chat and kept getting the same response

ME:
I just got an error: "result values in '? :' expression
have mismatching types 'MainScreen' and 'OnboardingScreen"

CHAT:
This error can occur if the MainScreen and OnboardingScreen views
have different types, such as being different types of views
(e.g. View and SomeOtherView). To resolve this error, ensure that
the MainScreen and OnboardingScreen views have the same type.
For example, if they are both SwiftUI views, they should both
be of type View.

The problem was that MainScreen and OnboardingScreen both already had the same type. I checked it multiple times. I even asked Chat if the fact that I moved those 2 screens to separate files might have influenced that. And guess what? He said that it might but the only resolution was to make sure that they have the same type 🤣

So I spent about 40mins trying to make sure that they have correct types, imports etc but to no effect. And that’s when I remembered…

This mythical portal was once solving all of my problems so why not now?

And guess what… it did. It turned out that I was using newer version of XCode or Swift or iOS (I don’t remember now) which apparently Chat was not aware of. Simple google search helped me to change the code to use new approach and boom! It works


struct ContentView: View {
@AppStorage("hasSeenOnboarding") var hasSeenOnboarding = false
@State private var shouldNavigateToOnboarding = false
@State private var shouldNavigateToMain = false

var body: some View {
ZStack {
SplashScreenView()
.navigationDestination(isPresented:$shouldNavigateToOnboarding) {
OnboardingScreen()
}
.navigationDestination(isPresented:$shouldNavigateToMain) {
MainScreen()
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if (hasSeenOnboarding) {
self.shouldNavigateToMain = true
} else {
self.shouldNavigateToOnboarding = true
}
}
}
}
}

I had it! Working piece of code that would actually do some kind of real business logic.

But I thought to myself — this is ugly 🤮 I surely can do sth better with it. I remembered that iOS actually had an architecture that decouples views from logic. It’s called VIPER and we actually used it to some extent in our Android projects. So I just turned to Chat and asked him to rewrite this logic using VIPER architecture. I actually just used those words

can you change SplashScreenView to implement VIPER architecture pattern?

And this guy gave me this…

// SplashScreenView.swift

import SwiftUI

struct SplashScreenView: View {
@ObservedObject var presenter: SplashScreenPresenter

var body: some View {
Text("Splash Screen")
.onAppear {
presenter.viewDidLoad()
}
}
}



// SplashScreenPresenter.swift

import Foundation

class SplashScreenPresenter: ObservableObject {
var interactor: SplashScreenInteractor
var router: SplashScreenRouter

init(interactor: SplashScreenInteractor, router: SplashScreenRouter) {
self.interactor = interactor
self.router = router
}

func viewDidLoad() {
interactor.loadData { result in
switch result {
case .success(let shouldShowOnboarding):
if shouldShowOnboarding {
router.navigateToOnboarding()
} else {
router.navigateToMainScreen()
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}



// SplashScreenInteractor.swift

import Foundation

class SplashScreenInteractor {
func loadData(completion: @escaping (Result<Bool, Error>) -> Void) {
// Fetch data from backend server
// For example, you could use URLSession to make a network request
// Once you have retrieved the data, call the completion block with the result
let shouldShowOnboarding = true
completion(.success(shouldShowOnboarding))
}
}



// SplashScreenRouter.swift

import SwiftUI

class SplashScreenRouter {
var navigationController: UINavigationController?

func navigateToOnboarding() {
let onboardingView = OnboardingScreenView(presenter: OnboardingScreenPresenter())
let onboardingViewController = UIHostingController(rootView: onboardingView)
navigationController?.pushViewController(onboardingViewController, animated: true)
}

func navigateToMainScreen() {
let mainView = MainScreenView(presenter: MainScreenPresenter())
let mainViewController = UIHostingController(rootView: mainView)
navigationController?.pushViewController(mainViewController, animated: true)
}
}

As you can see he simplified a lot of things (like my splash screen layout and interactor logic) but he basically gave me exactly what I needed. Everything decoupled and some kind of skeleton of the logic I need. The only problem I immediately saw was the lack of dependency injection. So I just asked him what are DI tools in Swift. Among 5 different tools he mentioned, I saw Koin — the tool I’m using in my own projects. One more request later I got a full explanation on how to install a pod with Koin (including exact files I need to add lines to and terminal commands).

Sure! Here's how you can add Koin to your project and use it 
for the SplashscreenPresenter:

First, add Koin to your project by adding the following line to your Podfile:

pod 'Koin'

Run pod install in your terminal to install the Koin library.

Create a new Swift file named AppModule.swift in your project.

In the AppModule.swift file

import Koin
...


That's it! You've now added Koin to your project and used it to
inject the SplashscreenPresenter class into your SplashscreenView.

And that’s where things got weird. I started getting errors about missing Koin version and when I dug in a little bit deeper, it turned out that Cocoapods simply don’t have Koin and Koin has no tutorials whatsoever on how to use it in iOS projects… only multiplatform

Maybe I missed something but I think that Chat was simply just lying LOL 🤣 Let me tell you why. When I finally realised that Koin was not an option I asked him about any other DI tools. He mentioned couple of them and this time I did my research. I googled each of them and confirmed that they exist and do what I need them to do. I picked DIKit and asked Chat to rewrite my project to use DIKit. What I saw was faaaar away from the actual documentation that they provide…

  1. He picked a version 5.0.0 which simply doesn’t exist. DIKit has latest version of 1.6.0 and when I confronted Chat about he just simply said
I apologize for the confusion. You are correct that DIKit currently 
has a version of 1.6.1, not 5.0.0. The instructions I provided for
using DIKit were incorrect. Here are the corrected steps:

Add DIKit to your Podfile by adding the following line:

pod 'DIKit', '~> 1.6.1'

Then run pod install to install DIKit.

Well… almost right. Maybe DIKit has 1.6.1 version but it’s not available in Cocoapods so I had to manually find out that version 1.6.0 was the way to go.

2. He created pieces of code that were extremely similar to what other DI tool proposes (Swinject). Something along those lines

final class DIContainer {
static let shared = DIContainer { container in
container.register(SplashscreenServiceProtocol.self) { _ in
SplashscreenService()
}
container.register(SplashscreenPresenter.self) { resolver in
SplashscreenPresenter(service: resolver.resolve(SplashscreenServiceProtocol.self)!)
}
container.register(OnboardingScreenView.self) { _ in
OnboardingScreenView()
}
}
}

// SplashScreenView.swift
struct SplashscreenView: View {
@StateObject var presenter: SplashscreenPresenter = DIContainer.shared.resolve(SplashscreenPresenter.self)!

...
}

I knew however that DIKit had some cool annotations. That’s why I chose this lib instead of Swinject. So I had to tell him to change that code as well. After couple of exchanges we finally ended up with sth like that

// SplashContainer.swift
extension DependencyContainer {
static let splashContainer = DependencyContainer(parent: shared) { container in
container.register { SplashscreenService() as SplashscreenServiceProtocol }
container.register { SplashscreenPresenter(service: $0 ~> SplashscreenServiceProtocol) }
}

static let onboardingContainer = DependencyContainer(parent: shared) { container in
container.register { OnboardingScreenView() }
}
}

// SplashScreenView.swift
struct SplashscreenView: View {
@StateObject @LazyInject(from: .splashContainer) var presenter: SplashscreenPresenter

...
}

Obviously this was not ideal because I would always have to specify the exact container out of which I wanted to provide the dependency. What’s more, I already knew that this won’t work because DIKit docs specify that you have to initialise the library on the app startup. I confronted Chat about it and again… he apologised and actually did find how to initialise everything correctly. SUPER STRANGE…

He was still however using this “container. register” thingies which DIKit had no knowledge about. After I asked him about it, he just responded me with this and continued using the same syntax 🤣

You are correct, my previous example uses the syntax for registering 
dependencies in Swinject instead of DIKit. Here is an example using
DIKit's factory and single methods:

extension DependencyContainer {
static let splashContainer = DependencyContainer(parent: .mainContainer) { container in
// Register use cases
container.register...

Like… seriously? He was just bluntly lying to me telling me that the code he offers will work with DIKit and yet continuously giving me the code from Swinject… I just had to accept that this guy just doesn’t understand DIKit and I’m on my own here.

After many frustrations I just went with the most classical engineering approach ever — DO IT YOURSELF.

I removed DIKit from my project… (To all iOS developers out there: try to push Apple for a better library handling. Like seriously. Having to close XCode, running terminal commands, manually changing text files, removing folders etc. is inhumane 🤨 There is a better way. It should also be available for our iOS friends). I just have to say however, that Chat in situations like that is very helpful. He just scours the web for a perfect short tutorial and helps you step by step.

After removing DIKit I just added a simple Service Locator and decided to keep on moving. Let’s hope this will be enough to handle my app. Once I did that, I finally was able to build the project successfully and see the SplashScreen 🎉 Unfortunately that’s what I saw 😆

Obviously I had some problems with image scaling but thanks to Chat I was able to quickly resolve this issue and have my desired splash screen. I did however have a problem with centering the image and Chat actually helped me debug this. He proposed changing the background of the entire screen to red to be able to see the boundaries of my image, which I think was a very nice touch. This was the final result

struct SplashScreenView: View {
private var presenter: SplashScreenPresenter

init(presenter: SplashScreenPresenter) {
self.presenter = presenter
}

var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
HStack {
Spacer()
Image("ic_logo")
.resizable()
.scaledToFit()
.frame(width: geometry.size.width * 0.7)
Spacer()
}
Spacer()
}
}
.onAppear {
self.presenter.viewDidLoad()
}
}
}
Splash Screen

Additionally I asked him how can I manage this magic value of 0.7 and even suggested the Android solution for it so that he could understand me

ME:
is there something like CompanionObject from Kotlin in Swift?
I would like to move this "0.7" value there

CHAT:
In Swift, there is no direct equivalent to the companion object in Kotlin.
However, you can achieve a similar result by using a static variable or
constant within a struct or class. Here's an example of how you could
define a Constants struct with a static variable for the 0.7 value:

struct Constants {
static let imageWidthRatio: CGFloat = 0.7
}

struct SplashScreenView: View {
...
.frame(width: geometry.size.width * Constants.imageWidthRatio)
...

It was time for navigation. I set the bar low. I just wanted to show the splash screen, wait for 2 seconds and then navigate to Onboarding. I would increment on that later. And if I’m completely honest, Chat just failed tremendously. He started with a solution containing buttons on the splash screen, then totally rewrote everything to use automatic navigation but when navigation wasn’t working, he was pretty much useless. I had to again find a solution by myself. And trust me… I did go out looking …

After countless hours, stupid amount of different versions, many pointless talks with Chat GPT and couple of mental breakdowns I was finally able to do what I needed — a simple logic that shows Splash screen and after 2 seconds automatically navigates to onboarding screen. I can only say that Chat was actually working against me rather than helping. Every suggestion he made ended up being either a lie (totally not working with anything) or using some deprecated methods. I was constantly reminding him that we were targeting iOS 14 and he was constantly giving me some old solutions that simply didn’t work. XCode wasn’t helping either let me tell you… I particularly loved when it was simply showing me something that looked like SIGSEV and waited for me to figure out what was wrong.

Example of XCode “helping” me ❤

In cases like that however Chat was somewhat more helpful than XCode itself because he would explain to me what this could mean and would point me in the right direction.

Nevertheless this is what we were able to achieve…

Pretty amazing, right? 😆 Two days of work, constant battles, constant rewrites, hundreds of messages with Chat GPT and this is the result — simple screen with an image that after 2 seconds switches to a different screen. That was my initial thought. But after I cooled down, I thought to myself: during 2 days of work I was able to setup a project in a technology I knew nothing about, create a skeleton of the application, setup a custom DI, build something that looks like VIPER architecture, figure out messy iOS navigation and add simple yet extendable solution for transitions between screens. It might have taken me some time but next time I will be adding a screen with navigation to it, it will take me much less time.

Summary of #1

So what can I say after first week? I can say one thing for sure — Chat GPT is a pathological lier 😆 and the sooner you realise it, the sooner you start figuring out how to actually use it as a tool. It’s not a magical creature that will write the code for you. It’s not going to replace real iOS developers that know their craft. At least not soon. But it is a great sidekick if you realise that he doesn’t know everything and he likes to lie about that. You have to double check. And you have to do it fast so as not to loose many hours trying to implement things that simply don’t exist. You have to remember that StackOverflow is still your best friend when learning a new technology.

I know that after this experiment I won’t be able to call myself an iOS developer. I will be closer to Dr. Frankenstein. Question however remains: will my crazy creation be alive at the end of this process?

Let me know in the comments if you can see some obvious improvements I might have missed or if you want me to try out different ways of communicating with Chat.

Here is the third article in the series!
https://medium.com/@andrzej.ryl/my-experience-building-an-ios-app-with-the-help-of-chat-gpt-4-2-33cdf9044991

--

--

Andrzej Ryl

I've been an Android developer since 2014. Currently I'm an Android team lead in my day job and working on my own personal project after hours. Huge dog lover