Meet my new Junior Developer: ChatGPT


I’ve been playing around with ChatGPT for a few days now, that I was able to create an account. Of course I had to have it generate some limericks, some poems and responses to emails. But the most burning question on my mind is this: will AI take my job?

I found that ChatGPT has a lot of syntax and conceptual knowledge about Swift and SwiftUI. So I wanted to put it to the test to see how smart it would be in fulfilling my requests. I was taking on the role of the mentor and ChatGPT was basically a student that took my original request and refined it over several iterations until I ended up with a beautiful simple solution.

For a current project I needed a view that displays a circle but I didn’t want to draw it but instead use a CAShapeLayer and a property that would update the color of the shape. So I had in my mind already an idea how I would do it, now the question was, how would I transfer this mental picture from my mind to the AI’s “mind”.

Me: Create a UIKit view that uses CAShapeLayer to display a filled circle

ChatGPT: Here is an example of how to create a UIKit view with a filled circle displayed using a CAShapeLayer in Swift:

import UIKit

class CircleView: UIView {

    override func draw(_ rect: CGRect) {
        let circleLayer = CAShapeLayer()
        let circlePath = UIBezierPath(arcCenter: CGPoint(x: rect.size.width / 2, y: rect.size.height / 2), radius: rect.size.width / 2, startAngle: 0.0, endAngle: CGFloat.pi * 2, clockwise: true)
        circleLayer.path = circlePath.cgPath
        circleLayer.fillColor = UIColor.red.cgColor
        layer.addSublayer(circleLayer)
    }
}

I immediately noticed several problems here.

At this point the result was in quality less than most responses I would see on Stack Overflow. The game was on!

Me: This is nonsense because it would add a new sublayer every time that the draw function is being called!

ChatGPT: I apologize for that mistake. Here is an updated example that only adds the CAShapeLayer once:

import UIKit

class CircleView: UIView {
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupCircle()
    }
  
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupCircle()
    }
  
    private func setupCircle() {
        let circleLayer = CAShapeLayer()
        let circlePath = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2), radius: bounds.size.width / 2, startAngle: 0.0, endAngle: CGFloat.pi * 2, clockwise: true)
        circleLayer.path = circlePath.cgPath
        circleLayer.fillColor = UIColor.red.cgColor
        layer.addSublayer(circleLayer)
    }
}

It moved the creation of the shape layer into a setup function which was called from both inits. The one with frame is used from code, the one with decoder is used when loading the view from a XIB or storyboard. This was an unexpected side-benefit.

I noticed though that this wouldn’t update the path when the bounds of the view would change.

Me: It also needs to update the circle path when the bounds of the view change.

ChatGPT: Here’s an updated version of the code that updates the circle path when the bounds of the view change:

import UIKit

class CircleView: UIView {
  
    private let circleLayer = CAShapeLayer()
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupCircle()
    }
  
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupCircle()
    }
  
    override func layoutSubviews() {
        super.layoutSubviews()
        updateCirclePath()
    }
  
    private func setupCircle() {
        layer.addSublayer(circleLayer)
        updateCirclePath()
    }
  
    private func updateCirclePath() {
        let circlePath = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2), radius: bounds.size.width / 2, startAngle: 0.0, endAngle: CGFloat.pi * 2, clockwise: true)
        circleLayer.path = circlePath.cgPath
        circleLayer.fillColor = UIColor.red.cgColor
    }
}

Interesting! It also knows that layoutSubviews is a great place to do something when layout changes. Next to the setup function it also added an function that updates the shape.

These successful refinements emboldened me. How about making the fill color be configurable from outside the view via a property? Also, while is technically correct to draw a circle via an arc bezier path, that only gives me an outline.

Me: Replace the arc bezier path with one for an oval that also gets filled with the UIColor set in the fillColor property.

ChatGPT: Here’s an updated version of the code that draws an oval shape instead of a circle and fills it with the color set in the fillColor property:

import UIKit

class OvalView: UIView {
  
    private let ovalLayer = CAShapeLayer()
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupOval()
    }
  
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupOval()
    }
  
    override func layoutSubviews() {
        super.layoutSubviews()
        updateOvalPath()
    }
  
    private func setupOval() {
        layer.addSublayer(ovalLayer)
        updateOvalPath()
    }
  
    private func updateOvalPath() {
        let ovalPath = UIBezierPath(ovalIn: bounds)
        ovalLayer.path = ovalPath.cgPath
        ovalLayer.fillColor = UIColor.red.cgColor
    }
}

Hm, it says that it did it, namely adding a fillColor property, but that’s nowhere to be found. So I tried to say it in a better way.

Me: Don’t hard code the fillColor, but make it a property of the view that updates the overLayer when set.

ChatGPT: Here’s an updated version of the code where the fill color is not hardcoded and can be set as a property of the view:

import UIKit

class OvalView: UIView {
  
    private let ovalLayer = CAShapeLayer()
  
    var fillColor: UIColor = UIColor.red {
        didSet {
            updateOvalPath()
        }
    }
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupOval()
    }
  
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupOval()
    }
  
    override func layoutSubviews() {
        super.layoutSubviews()
        updateOvalPath()
    }
  
    private func setupOval() {
        layer.addSublayer(ovalLayer)
        updateOvalPath()
    }
  
    private func updateOvalPath() {
        let ovalPath = UIBezierPath(ovalIn: bounds)
        ovalLayer.path = ovalPath.cgPath
        ovalLayer.fillColor = fillColor.cgColor
    }
}

I had mistyped “overLayer”, but ChatGPT still understood my request and executed it flawlessly. This was already quite good, but an expert programmer wouldn’t be satisfied with this and keep polishing the result until it shines.

Me: Separate the update of the fill color from updating the shape path. The path should only be updated when the bounds change and the color only when the property changes.

ChatGPT: Here’s an updated version of the code where the fill color and the shape path are separated, and the shape path is only updated when the bounds change:

import UIKit

class OvalView: UIView {
  
    private let ovalLayer = CAShapeLayer()
  
    var fillColor: UIColor = UIColor.red {
        didSet {
            ovalLayer.fillColor = fillColor.cgColor
        }
    }
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupOval()
    }
  
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupOval()
    }
  
    override func layoutSubviews() {
        super.layoutSubviews()
        updateOvalPath()
    }
  
    private func setupOval() {
        layer.addSublayer(ovalLayer)
        updateOvalPath()
    }
  
    private func updateOvalPath() {
        let ovalPath = UIBezierPath(ovalIn: bounds)
        ovalLayer.path = ovalPath.cgPath
    }
}

Ha! I spotted another mistake! This way the fill color wouldn’t remain the default and only be updated if the property is changed.

Me: This is missing the initial setting of the fill color.

ChatGPT: Here’s an updated version of the code that sets the initial value of the fillColor property:

import UIKit

class OvalView: UIView {
  
    private let ovalLayer = CAShapeLayer()
  
    var fillColor: UIColor = UIColor.red {
        didSet {
            ovalLayer.fillColor = fillColor.cgColor
        }
    }
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupOval()
    }
  
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupOval()
    }
  
    override func layoutSubviews() {
        super.layoutSubviews()
        updateOvalPath()
    }
  
    private func setupOval() {
        layer.addSublayer(ovalLayer)
        updateOvalPath()
        ovalLayer.fillColor = fillColor.cgColor
    }
  
    private func updateOvalPath() {
        let ovalPath = UIBezierPath(ovalIn: bounds)
        ovalLayer.path = ovalPath.cgPath
    }
}

That’s correct. Now we are were at a point where I was no longer so certain because I don’t create CAShapeLayers every day. We need to set the shape layers frame somehow, don’t we?

Me: I believe that this code is missing an update for the frame of the sublayer.

ChatGPT: Here’s an updated version of the code that sets the frame of the ovalLayer sublayer:

import UIKit

class OvalView: UIView {
  
    private let ovalLayer = CAShapeLayer()
  
    var fillColor: UIColor = UIColor.red {
        didSet {
            ovalLayer.fillColor = fillColor.cgColor
        }
    }
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupOval()
    }
  
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupOval()
    }
  
    override func layoutSubviews() {
        super.layoutSubviews()
        updateOvalPath()
    }
  
    private func setupOval() {
        layer.addSublayer(ovalLayer)
        updateOvalPath()
        ovalLayer.fillColor = fillColor.cgColor
    }
  
    private func updateOvalPath() {
        ovalLayer.frame = bounds
        let ovalPath = UIBezierPath(ovalIn: bounds)
        ovalLayer.path = ovalPath.cgPath
    }
}

Yeah that looks better, if we set the frame every time layoutSubviews is called then that should work. At this point I had a thought that I wanted to Senior-Programmer’splain to the poor and beaten AI…

Me: layoutSubviews is probably the wrong place to update the layer frame and oval path because that belongs to the view layout flow, but there is a separate layout flow for layers. put it there.

ChatGPT, unflinchingly: Here’s an updated version of the code where the frame of the ovalLayer and the oval path are updated in the layoutSublayers function instead of layoutSubviews:

import UIKit

class OvalView: UIView {
  
    private let ovalLayer = CAShapeLayer()
  
    var fillColor: UIColor = UIColor.red {
        didSet {
            ovalLayer.fillColor = fillColor.cgColor
        }
    }
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupOval()
    }
  
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupOval()
    }
  
    override func layoutSublayers(of layer: CALayer) {
        super.layoutSublayers(of: layer)
        updateOvalPath()
    }
  
    private func setupOval() {
        layer.addSublayer(ovalLayer)
        ovalLayer.fillColor = fillColor.cgColor
    }
  
    private func updateOvalPath() {
        ovalLayer.frame = bounds
        let ovalPath = UIBezierPath(ovalIn: bounds)
        ovalLayer.path = ovalPath.cgPath
    }
}

Impressed, again! It knows about layoutSublayers.

Then I thought some more. I was worried about the circle not being properly updated in UIView animations. If you set a layer frame in layoutSubviews then the contents jumps to the new location and size without animation, because UIView properties are implicitly animated, but layer properties are not.

And why do we need the basic CALayer at all, when we can make the shape layer be the view’s main layer? Ha! Now I knew how to stump the AI!

Me: It would also be better to make the shape layer the main layer of the view instead of setting up a separate sublayer for it.

ChatGPT: Here’s an updated version of the code where the CAShapeLayer is set as the main layer of the OvalView instead of a separate sublayer:

import UIKit

class OvalView: UIView {
  
    var fillColor: UIColor = UIColor.red {
        didSet {
            layer.fillColor = fillColor.cgColor
        }
    }
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupOval()
    }
  
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupOval()
    }
  
    override func layoutSublayers(of layer: CALayer) {
        super.layoutSublayers(of: layer)
        updateOvalPath()
    }
  
    private func setupOval() {
        layer.fillColor = fillColor.cgColor
        layer.masksToBounds = true
    }
  
    private func updateOvalPath() {
        layer.frame = bounds
        layer.cornerRadius = bounds.width / 2
    }
}

Gotcha! Gone is the shape layer only to be replaced by setting the layer’s cornerRadius to half of the width of the view. And an expertly inserted masksToBounds makes this work.

Nice try, but let me tell you what’s wrong with this:

Me: That doesn’t work when the view is not square. I want you to create the layer for the view via returning CAShapeLayer as the layerClass of the view.

ChatGPT blushing: Internal server error

Was it something I said? No, probably not. ChatGPT’s servers get overloaded frequently. When I pressed the “Regenerate Response” button I got a proper response:

ChatGPT: Here’s an updated version of the code where CAShapeLayer is returned as the layer class for the OvalView:

import UIKit

class OvalView: UIView {
  
    var fillColor: UIColor = UIColor.red {
        didSet {
            layer.fillColor = fillColor.cgColor
        }
    }
  
    override class var layerClass: AnyClass {
        return CAShapeLayer.self
    }
  
    override func layoutSubviews() {
        super.layoutSubviews()
        updateOvalPath()
    }
  
    private func updateOvalPath() {
        let ovalPath = UIBezierPath(ovalIn: bounds)
        (layer as! CAShapeLayer).path = ovalPath.cgPath
        layer.fillColor = fillColor.cgColor
    }
}

Nice! We’re back in business. I pasted this code into Xcode only to find a syntax error I had missed from only glancing at it.

Me: That will produce a syntax error because the layer in the fillColor setter is not known to be a CAShapeLayer to the compiler. You need to cast it to one.

ChatGPT: Here’s an updated version of the code that casts the layer property to a CAShapeLayer in the fill color setter:

import UIKit

class OvalView: UIView {
  
    var fillColor: UIColor = UIColor.red {
        didSet {
            (layer as! CAShapeLayer).fillColor = fillColor.cgColor
        }
    }
  
    override class var layerClass: AnyClass {
        return CAShapeLayer.self
    }
  
    override func layoutSubviews() {
        super.layoutSubviews()
        updateOvalPath()
    }
  
    private func updateOvalPath() {
        let ovalPath = UIBezierPath(ovalIn: bounds)
        (layer as! CAShapeLayer).path = ovalPath.cgPath
        (layer as! CAShapeLayer).fillColor = fillColor.cgColor
    }
}

Technically correct now, but still in need of polish.

Me: Refactor those `(layer as! CAShapeLayer)` into a property

ChatGPT: Here’s an updated version of the code that uses a computed property to avoid casting layer to CAShapeLayer multiple times:

import UIKit

class OvalView: UIView {
  
    var fillColor: UIColor = UIColor.red {
        didSet {
            shapeLayer.fillColor = fillColor.cgColor
        }
    }
  
    private var shapeLayer: CAShapeLayer {
        return layer as! CAShapeLayer
    }
  
    override class var layerClass: AnyClass {
        return CAShapeLayer.self
    }
  
    override func layoutSubviews() {
        super.layoutSubviews()
        updateOvalPath()
    }
  
    private func updateOvalPath() {
        let ovalPath = UIBezierPath(ovalIn: bounds)
        shapeLayer.path = ovalPath.cgPath
        shapeLayer.fillColor = fillColor.cgColor
    }
}

Ah, now nice… we have a new shapeLayer property that force casts the layer property into a CAShapeLayer. I put the code into my project, it compiled without issue and displayed the red circles I lusted for as requested.

Me: Thanks, that’s the best answer for my initial question.

ChatGPT: You’re welcome! I’m glad I could help. If you have any more questions, feel free to ask!

Evaluation

Similar to stories where ChatGPT sat university exams with bad but passing grades it also performs around the junior level for software developers. It is able to produce the correct result if an experienced developer has the patience and skills to guided it over multiple iterations.

It does require a lot of hand-holding. Inputting your requirements via chat requires that you know how to express all those concepts in a way that the AI can parse. I do have a lot of experience training Junior Engineers and a decade of programming UIKit taught me how to put things that they can be understood.

I would have spent a fraction of the time if I had just coded this view like I envisioned it from the start. But the point of this exchange is not to prove that I am better at programming than ChatGPT. It is to prove that Chat GPT is more capable than finding sample code on the Internet. It’s not even web search on sterioids. It is clearly much more than that because it can understand concepts and synthesize a result from that including making refinements that an expert user is guiding it to do.

This is a first data point. There was none before it because public access to ChatGPT was only activated a few days ago. So it is amazing that something is possible today that wasn’t possible before.

The Future

In the least I would like ChatGPT built into Xcode: as a way to write new code. And I don’t mean dictating code, but speaking in concepts like shown above. How about having this in Xcode? “Hi Xcode, I need a new view that does this and that”. If that were more streamlined and faster that would immediately be useful. Especially because it also improves your own learning curve. Having to communicate concepts makes you yourself understand them better. What you teach, you learn!

I can see a future where ChatGPT becomes more and more productive to the eventual degree of a Junior Developer. With this I mean somebody who can conceptional execute the itty gritty which you, as a Senior Developer, give it direction in a general vision you maintain. Like the captain of a ship who sees the ice bergs and knows where the ship should be going, and ChatGPT is the entire crew.

Then the step after that – in the far future – is to combine ChatGPT with something that can understand sketches and mockups and functional descriptions of apps. This is where the Senior Developer brains are and will still be needed for a long time. To translate the client’s wishes and designs into software concepts.

We haven’t touched on the question what will happen if one day all Juniors can be replaced by AI. If there are no new Juniors then there will be nobody to mature into Seniors at some point. Maybe our training will have to change to focus on mastering conceptionalizing and high level concept synthesis and leaving the mundane creation of code to AI instead.



Also published on Medium.


Categories: Tools

Leave a Reply

Your email address will not be published. Required fields are marked *