Fix: Cannot Assign Value of Type ‘Int’ to Type ‘CGFloat’ in SwiftUI

Fix: Cannot Assign Value of Type 'Int' to Type 'CGFloat' in SwiftUI

Written by

in

Quick answer: Swift infers @State private var velX = 5 as an Int. CGPoint.x is a CGFloat, and Swift never converts Int to CGFloat for you, so the assignment fails. Declare the velocity as CGFloat (@State private var velX: CGFloat = 5), change ball.x = velX to ball.x += velX, and call that move function from a repeating timer so the ball actually moves.

A common first step in a SwiftUI game, such as a Pong clone built without SpriteKit, is to store the ball position in a CGPoint and update it from a velocity value. Then the build stops with:

Cannot assign value of type 'Int' to type 'CGFloat'

The error points at lines like these:

@State private var ball = CGPoint(x: 50, y: 10)
@State private var velX = 5
@State private var velY = 8

func moveBall() {
    ball.x = velX   // error
    ball.y = velY   // error
}

Fixing the type error is quick. The code has a few other problems that stop the ball from moving even after it compiles, so this guide covers those as well.

Why this happens

Swift infers a type from the literal you assign. 5 and 8 are integer literals. With no annotation, velX and velY become Int.

The x and y properties of CGPoint are CGFloat. Swift does not convert between numeric types implicitly. There is one exception: since Swift 5.5, Double and CGFloat convert to each other automatically. Int is not covered by that rule, so assigning an Int to a CGFloat property is a compile error.

Two more issues are in the same function:

  • ball.x = velX replaces the position with the velocity instead of adding to it. The ball would jump to (5, 8) and stay there.
  • velX += -velX is the same as velX = velX - velX, which is always 0. The ball would stop when it hits a wall instead of bouncing.

Also, nothing calls moveBall() repeatedly. A game needs a loop that updates state many times per second.

How to fix it

1. Declare the velocity as CGFloat

This fixes the compile error. You have three options.

Add an explicit type annotation. This is the clearest choice:

@State private var velX: CGFloat = 5
@State private var velY: CGFloat = 8

Or initialize with CGFloat directly:

@State private var velX = CGFloat(5)
@State private var velY = CGFloat(8)

Or convert at the call site, if you have a real reason to keep the velocity as Int:

ball.x += CGFloat(velX)

For a game, use floating-point velocity. You will want fractional speeds and angles later, and the values will feed straight into CGPoint math with no casts.

A cleaner option is to store both components in one CGVector:

@State private var velocity = CGVector(dx: 5, dy: 8)

2. Fix the movement and bounce math

Add the velocity to the position each frame. To bounce, flip the sign of the velocity:

ball.x += velocity.dx
ball.y += velocity.dy

if ball.y <= 5 || ball.y >= size.height - 5 {
    velocity.dy = -velocity.dy
}

velocity.dy = -velocity.dy reverses direction. velocity.dy += -velocity.dy sets it to zero.

Also clamp the ball back inside the bounds after a bounce. If the ball overshoots the wall by a few points and you only flip the sign, it can still be outside the wall on the next frame. The sign flips again and the ball sticks to the wall.

3. Drive the ball with a timer

SwiftUI redraws when @State changes, so you only need something that calls moveBall() about 60 times per second. A Combine timer with onReceive is the simplest way. The example below also uses GeometryReader, so the walls match the real screen size instead of hard-coded numbers like 400 and 550:

import SwiftUI
import Combine

struct PongView: View {
    @State private var ball = CGPoint(x: 200, y: 300)
    @State private var velocity = CGVector(dx: 5, dy: 8)
    @State private var score1 = 0
    @State private var score2 = 0

    private let tick = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common)
        .autoconnect()
    private let ballSize: CGFloat = 10

    var body: some View {
        GeometryReader { geo in
            ZStack {
                Color.black
                Rectangle()
                    .fill(.white)
                    .frame(width: ballSize, height: ballSize)
                    .position(ball)
            }
            .onReceive(tick) { _ in
                moveBall(in: geo.size)
            }
        }
        .ignoresSafeArea()
    }

    private func moveBall(in size: CGSize) {
        let r = ballSize / 2
        ball.x += velocity.dx
        ball.y += velocity.dy

        // Bounce off top and bottom
        if ball.y <= r || ball.y >= size.height - r {
            velocity.dy = -velocity.dy
            ball.y = min(max(ball.y, r), size.height - r)
        }

        // Left or right edge: score and reset
        if ball.x < 0 {
            score2 += 1
            resetBall(in: size)
        } else if ball.x > size.width {
            score1 += 1
            resetBall(in: size)
        }
    }

    private func resetBall(in size: CGSize) {
        ball = CGPoint(x: size.width / 2, y: size.height / 2)
        velocity.dx = -velocity.dx
    }
}

The scoring branch replaces a separate checkScore() function. Scoring happens at the moment the ball leaves the screen, and the ball resets in the same step.

To pause the game, for example when someone reaches 10 points, check a gameOver flag at the top of moveBall and return early.

4. Alternative: an async loop with .task

If you would rather not use Combine, run the loop inside .task. SwiftUI cancels the task when the view disappears:

.task {
    while !Task.isCancelled {
        moveBall(in: currentSize)
        try? await Task.sleep(for: .milliseconds(16))
    }
}

Task.sleep(for:) requires iOS 16 or later. You also need to store the size somewhere, for example in a @State variable set from GeometryReader. The timer version above passes geo.size directly, so it is simpler for a first game.

5. Check the paddle drag coordinate space

If your paddles use DragGesture(coordinateSpace: .named("game")), a container view must declare that name. Otherwise the drag locations are not measured where you expect. Add the declaration to the ZStack:

ZStack { /* paddles, ball */ }
    .coordinateSpace(name: "game")

On iOS 17 and later you can write .coordinateSpace(.named("game")) instead.

Paddle collision uses the same idea as wall bouncing. When the ball’s x reaches the paddle’s x and its y is within half the paddle height of the paddle center, flip velocity.dx.

How to prevent it

  • Annotate numeric @State properties that feed geometry: var speed: CGFloat = 5.
  • Group related values into CGPoint, CGVector or CGSize instead of separate loose numbers.
  • Use += to move something and = -value to reverse it. Read x += -x as “set to zero.”
  • Get bounds from GeometryReader instead of hard-coding screen sizes. Hard-coded sizes break on other devices.
  • Clamp positions after a collision so objects cannot get stuck inside walls.

FAQ

Why does ball.x = 5.0 compile but ball.x = velX doesn’t?

A literal written directly in the assignment takes the type of its destination, so 5 or 5.0 becomes a CGFloat there. A stored Int variable already has a fixed type, and Swift will not convert it to CGFloat implicitly.

Should I use withAnimation to move the ball instead of a timer?

Not for gameplay. withAnimation interpolates toward a final value, but a game has to check collisions every frame. A timer or async loop that updates position in small steps gives you that control.

Is a 60 fps Timer accurate enough for a simple game?

For a classroom Pong game, yes. Timers can fire slightly late under load. For smoother motion, compute the time since the last tick and multiply the velocity by it, so speed does not depend on frame rate.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.