Async Sequence

How to iterate over a custom object given below with async sequence

enum FetchError: Error {
  case badImage
  case badRequest
  case invalidImageURL
  case noURL
  case failedToFetchImage
}
struct Photo: Codable {
  let albumId: Int
  let id: Int
  let title: String
  let urlPath: String
  let thumbnailUrl: String
}

What i have tried :-

func fetchAsyncImage(request:URLRequest) async throws -> [UIImage] {
    let (data, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badRequest }
    let photos = try JSONDecoder().decode([Photo].self, from: data)
    
    guard let imagePath = photos.first?.urlPath,
          let imageURL = URL.init(string: imagePath) else { throw FetchError.noURL }
    
    var imageArr:[UIImage] = []

// getting error in following 2 lines
// Error: For-in loop requires '[Photo]' to conform to 'AsyncSequence'
    for await photo in photos {
// Error: Type of expression is ambiguous without more context
        guard let imagePath = photo.urlPath,
              let imageURL = URL.init(string: imagePath) else { throw FetchError.noURL }
        do {
            let (imageData, imageResponse) = try await URLSession.shared.data(from: imageURL)
            guard (imageResponse as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.invalidImageURL }
            guard let image = UIImage(data: imageData) else { throw FetchError.badImage }
            imageArr.append(image)
        } catch {
            throw FetchError.failedToFetchImage
        }
    }
    return imageArr
}

I tried implementing AsyncSequenc and AsyncIteratorProtocol on Photo struct as follows:-

struct Photo: Codable, AsyncSequence {
  typealias Element = URL
   
  let albumId: Int
  let id: Int
  let title: String
  let urlPath: String
  let thumbnailUrl: String
   
  struct AsyncIterator: AsyncIteratorProtocol {
    let urlPath: String
    mutating func next() async throws -> URL? {
      do {
        guard let imageURL = URL.init(string: urlPath) else { throw FetchError.noURL }
        return imageURL
      } catch {
        throw FetchError.invalidImageURL
      }
    }
  }
   
  func makeAsyncIterator() -> AsyncIterator {
    AsyncIterator(urlPath: urlPath)
  }

}

As far as I read the proposal SE-0298Async/Await: Sequences, implementing AsyncSequence is far from intuitive.

You may need to write something like this:

func fetchAsyncImage(request: URLRequest) -> AsyncPhotos {

    return AsyncPhotos(request: request)
}

struct AsyncPhotos: AsyncSequence {
    typealias Element = UIImage
    
    let request: URLRequest
    
    struct AsyncIterator: AsyncIteratorProtocol {
        let request: URLRequest
        var photosIterator: Array<Photo>.Iterator?
        
        mutating func next() async throws -> UIImage? {
            if photosIterator == nil {
                let (data, response) = try await URLSession.shared.data(for: request)
                guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                    throw FetchError.badRequest
                }
                let photos = try JSONDecoder().decode([Photo].self, from: data)
                photosIterator = photos.makeIterator()
            }
            guard let photo = photosIterator?.next() else {
                return nil
            }
            do {
                guard let imageURL = URL(string: photo.urlPath) else {
                    throw FetchError.noURL
                }
                let (imageData, imageResponse) = try await URLSession.shared.data(from: imageURL)
                guard (imageResponse as? HTTPURLResponse)?.statusCode == 200 else {
                    throw FetchError.invalidImageURL
                }
                guard let image = UIImage(data: imageData) else {
                    throw FetchError.badImage
                }
                return image
            } catch {
                throw FetchError.invalidImageURL
            }
        }
    }
    
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(request: request)
    }
}

(Not tested. You may need to fix many parts.)

I'm not sure if AsyncStream/AsyncThrowingStream can improve the way implementing AsyncSequence.

Wow that is way more complicated than i thought. What i just want is to iterate over array of image urls i received after first call (array of objects having url). Cant i do it without making whole Photo object async ? Thanks @OOPer for the answer. i am still not feeling comfortable with AsyncIterator.

I am not able to understand the purpose of following:

  • typealias Element = UIImage,

  • mutating func next() async throws,

  • makeAsyncIterator()

These things are not intuitive and i cant find good resources explaining them.

These things are not intuitive and i cant find good resources explaining them.

Have you ever tried to make your custom type conform to Sequence? Making AsyncSequence is very similar to Sequence.

And as explained in the session video you referred, Swift compiler compiles for-(try)-await as:

    var iterator = quakes.makeAsyncIterator()
    while let quake = await iterator.next() {
        //...
    }

You need to implement what compiler demands. (typealias is a thing needed for making types conform to some protocol. Better read the Swift book carefully.)

Async Sequence
 
 
Q