CPListItem Image

Hello, I am build a CarPlay Audio Extension for our App. Users can listen to their Audio Playlist that they made in our App.

Loading remote images for CPListItem causes a problem. The images are always very small not filling out the full reserved space. If I put them as an asset in the app, the images are fully scaled. Anyone who ran into that problem and can help me out? Thank you. Cheers, Simon

Answered by fruitcoder_DE in 696594022

We have actually contacted Developer Technical Support for this and while the response wasn't exactly what we needed it pointed us in the right direction to come up with something that works for us. Note that there are still scaling issues when ALL items within a section have no detail text (but that's another issue). Here is what we use:

extension CPListItem {
	convenience init(text: String?,
                   detailText: String?,
                   remoteImageUrl: URL?,
                   placeholder: UIImage?,
                   accessoryImage: UIImage? = nil,
                   accessoryType: CPListItemAccessoryType = .none,
                   imageOperation: RemoteImageOperation? = nil) {
		self.init(text: text, detailText: detailText, image: placeholder, accessoryImage: accessoryImage, accessoryType: accessoryType)

		setImageUrl(remoteImageUrl)
	}

  func setImageUrl(_ url: URL?) {
    guard let imageUrl = url else { return }

    Current.downloadImage(imageUrl) { image in
      guard
        let cropped = image?.cpCropSquareImage,
        let resized = cropped.resized(to: CPListItem.maximumImageSize),
        let carPlayImage = resized.carPlayImage 
      else { return }
      DispatchQueue.main.async { [weak self] in
        self?.setImage(carPlayImage)
      }
    }
  }
}

extension UIImage {
  var carPlayImage: UIImage? {
    guard let traits = Current.interfaceController?.carTraitCollection else { return nil }
    
    let imageAsset = UIImageAsset()
    imageAsset.register(self, with: traits)
    return imageAsset.image(with: traits)
  }

  var cpCropSquareImage: UIImage { /* basic image cropping ... */ }
  func resized(to newSize: CGSize) -> UIImage? { /* basic image resizing ... */ }
}

The basic idea is to put the image in a UIImageAsset and take it out again which magically makes things work. Other than that we also crop it to a square and resize it to the maximum allowed image size. Our Current is like a dependency container where download is just a function to access our image cache/downloader and the interfaceController is the one we get when connecting the CarPlay scene (this is important for the native scale of the CarPlay display instead of the connected iOS device's display scale).

Accepted Answer

We have actually contacted Developer Technical Support for this and while the response wasn't exactly what we needed it pointed us in the right direction to come up with something that works for us. Note that there are still scaling issues when ALL items within a section have no detail text (but that's another issue). Here is what we use:

extension CPListItem {
	convenience init(text: String?,
                   detailText: String?,
                   remoteImageUrl: URL?,
                   placeholder: UIImage?,
                   accessoryImage: UIImage? = nil,
                   accessoryType: CPListItemAccessoryType = .none,
                   imageOperation: RemoteImageOperation? = nil) {
		self.init(text: text, detailText: detailText, image: placeholder, accessoryImage: accessoryImage, accessoryType: accessoryType)

		setImageUrl(remoteImageUrl)
	}

  func setImageUrl(_ url: URL?) {
    guard let imageUrl = url else { return }

    Current.downloadImage(imageUrl) { image in
      guard
        let cropped = image?.cpCropSquareImage,
        let resized = cropped.resized(to: CPListItem.maximumImageSize),
        let carPlayImage = resized.carPlayImage 
      else { return }
      DispatchQueue.main.async { [weak self] in
        self?.setImage(carPlayImage)
      }
    }
  }
}

extension UIImage {
  var carPlayImage: UIImage? {
    guard let traits = Current.interfaceController?.carTraitCollection else { return nil }
    
    let imageAsset = UIImageAsset()
    imageAsset.register(self, with: traits)
    return imageAsset.image(with: traits)
  }

  var cpCropSquareImage: UIImage { /* basic image cropping ... */ }
  func resized(to newSize: CGSize) -> UIImage? { /* basic image resizing ... */ }
}

The basic idea is to put the image in a UIImageAsset and take it out again which magically makes things work. Other than that we also crop it to a square and resize it to the maximum allowed image size. Our Current is like a dependency container where download is just a function to access our image cache/downloader and the interfaceController is the one we get when connecting the CarPlay scene (this is important for the native scale of the CarPlay display instead of the connected iOS device's display scale).

You should use +[CPListItem maximumImageSize] to fetch the maximum list image size permitted for your category of app. This is a resolution-independent value in points.

Then, you should access -[CPInterfaceController carTraitCollection], which provides the displayScale for the current car screen - this will be either 2x or 3x, depending on the vehicle. Multiplying the image size (in points) by the display scale will result in a pixel value that you should use for sizing your images.

I've tried the suggested solutions above but they either didn't work for me or deemed to be to overly complex to handle the matter. I've managed to solve it using fairly rudimentary scaling:

extension CarServicesItemDTO {
  func toListItem() -> CPListItem {
    let listItem = CPListItem(text: title, detailText: subtitle)

    DispatchQueue.global(qos: .background).async {
      let imgUrl = URL(string: imageUrl)
      if let imageUrl = imgUrl {
        let data = try? Data(contentsOf: imageUrl)
        if let imageData = data {
          let image = UIImage(data: imageData)?.scalePreservingAspectRatio(targetSize: CPListItem.maximumImageSize) ?? UIImage()
          listItem.setImage(image)
        }
      }
    }

    return listItem
  }
}

extension UIImage {
  func scalePreservingAspectRatio(targetSize: CGSize) -> UIImage {
      // Determine the scale factor that preserves aspect ratio
      let widthRatio = targetSize.width / size.width
      let heightRatio = targetSize.height / size.height

      let scaleFactor = min(widthRatio, heightRatio)

      // Compute the new image size that preserves aspect ratio
      let scaledImageSize = CGSize(
        width: size.width * scaleFactor,
        height: size.height * scaleFactor
      )

      // Draw and return the resized UIImage
      let renderer = UIGraphicsImageRenderer(
        size: scaledImageSize
      )

      let scaledImage = renderer.image { _ in
        self.draw(in: CGRect(
          origin: .zero,
          size: scaledImageSize
        ))
      }

      return scaledImage
    }
}

CarServiceItemDTO is a simple data type containing the data fields received from backend, and using toListItem() it will be converted into a CPListItem. The scaling happens in scalePreservingAspectRatio() and is an algorithm borrowed from this StackOverflow answer: https://stackoverflow.com/a/71078857

CPListItem Image
 
 
Q