Instead of implementing `consume()` for each integer type separately,
you can implement a *single generic* method:

    mutating func consume<T: FixedWidthInteger>() throws -> T {
        let size = MemoryLayout<T>.size
        guard idx + size <= bytes.count else {
            throw Error.notEnoughBytes
        }
        defer { idx += size }
        return bytes[idx..<idx + size].enumerated().reduce(0) {
            $0 + T($1.element) << (8 * $1.offset)
        }
    }

which can be used for all signed and unsigned integer types `(U)Int`, `(U)Int8`, ..., `(U)Int64`.


Another option is to copy the bytes into a value of the desired type
instead of bit shifting and adding:

    mutating func consume<T: FixedWidthInteger>() throws -> T {
        let size = MemoryLayout<T>.size
        guard idx + size <= bytes.count else {
            throw Error.notEnoughBytes
        }
        var value: T = 0
        bytes.withUnsafeBytes { _ = memcpy(&value, $0.baseAddress! + idx, size) }
        idx += size
        return T(littleEndian: value)
    }

----------

Instead of letting the compiler infer the return type from the context,
one can alternatively pass it as a parameter:

    mutating func consume<T: FixedWidthInteger>(_: T.Type) throws -> T { ... }

which is then – for example – called as

    let crc32 = try buffer.consume(UInt32.self)

----------

I would probably call the method `get()` or `read()` instead of `consume()`.

----------

Defining a local `enum Error` type which conforms to the (global)
`Error` protocol is possible, but might be confusing to the reader.
I would use a different name for the concrete error type, for example:

    enum ReadError: Error {
        case notEnoughBytes
    }

----------

Now let's have a look how an error would be reported.
The caller does not know the actual error type, so a typical calling
sequence is:

    do {
        let someData = Data(bytes: [1])
        var buffer = ConsumableByteArray(data: someData)
        let crc32: UInt32 = try buffer.consume()
        print(crc32)
    } catch {
        print(error.localizedDescription)
    }

This produces the output:

<pre>
The operation couldn’t be completed. (MyProg.ConsumableByteArray.ReadError error 0.)
</pre>

This can be improved by adopting the `LocalizedError` protocol
(see for example [How to provide a localized description with an Error type in Swift?](https://stackoverflow.com/q/39176196/1187415) on
Stack Overflow):

    enum ReadError: Error, LocalizedError {
        case notEnoughBytes
        
        public var errorDescription: String? {
            switch self {
            case .notEnoughBytes:
                return "Not enough bytes in buffer"
            }
        }
    }

Now the error output of the above program becomes

<pre>
Not enough bytes in buffer
</pre>

You can even store additional information about the error in associated
values:

    enum ReadError: Error, LocalizedError {
        case notEnoughBytes(available: Int, needed: Int)
        
        public var errorDescription: String? {
            switch self {
            case .notEnoughBytes(let available, let needed):
                return "Not enough bytes in buffer (available: \(available), needed: \(needed))"
            }
        }
    }

Then by throwing

     throw ReadError.notEnoughBytes(available: bytes.count - idx, needed: size)

an error message like

<pre>
Not enough bytes in buffer (available: 1, needed: 4)
</pre>

is produced.

----------

Finally note that there is a [`ByteBuffer`](https://apple.github.io/swift-nio/docs/current/NIO/Structs/ByteBuffer.html) type as part of the 
[SwiftNIO](https://github.com/apple/swift-nio) framework,
which can do all this and more. Even if you decide not to use it,
having a look at its documentation and interface might be instructive.