Why do PDFs resized in SwiftUI getting sharp edges?

PDF vectors needs to be programmatically resized via UIGraphicsBeginImageContextWithOptions so that they are not shown blurred when you scale them up (or down). There is no need to have multiple PDFs with different resolution to accomplish this.

Unfortunately this is not done automatically by UIKit or SwiftUI. Here is an example where a 24x24 PDF vector is tinted and resized to 200x200.

Image(uiImage: UIImage(named: "heart")!.tinted(withColor: .blue,
                                               biggerSize: CGSize(width: 200, height: 200)))
      .resizable()
      .frame(width: 200, height: 200,
             alignment: .center)
extension UIImage {

    /// Uses compositor blending to apply color to an image. When an image is too small it will be shown
    /// blurred. So you have to provide a size property to get a good resolution image
    public func tinted(withColor: UIColor?, biggerSize: CGSize = .zero) -> UIImage {
        guard let withColor = withColor else { return self }
        
        let size = biggerSize == .zero ? self.size : biggerSize
        let img2 = UIImage.createWithColor(size, color: withColor)
        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        let renderer = UIGraphicsImageRenderer(size: size)
        let result = renderer.image { _ in
            img2.draw(in: rect, blendMode: .normal, alpha: 1)
            draw(in: rect, blendMode: .destinationIn, alpha: 1)
        }
        return result
    }

    public static func createWithColor(_ size: CGSize, color: UIColor) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        let context = UIGraphicsGetCurrentContext()
        let rect = CGRect(size: size)
        color.setFill()
        context!.fill(rect)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image!
    }
}

I did a side by side comparison for both vector images using the ones you provided:

  • http://simensolbakken.com/public/stackoverflow/icon.pdf
  • http://simensolbakken.com/public/stackoverflow/icon_small.pdf

At first, I used SwiftUI's inbuilt Image and as mentioned, both performed badly at their extreme ends:

  • Large image got sharp edges when it scaled down
  • Small image got blurred as it scaled up

At first I thought it might be your pdf vectors so I used ones that I know have worked well in my previous projects, but I got the same issues.
Thinking it to be a UIImage issue, I used SwiftUIs Image(uiImage:) but same problem.

Last guess was the image container, and knowing that UIImageView has handled vector images well, getting UIViewRepresentable to wrap the UIImageView seems to solve this issue. And for now it looks like a possible workaround.

Workaround Solution:

struct MyImageView: UIViewRepresentable {
  var name: String
  var contentMode: UIView.ContentMode = .scaleAspectFit
  var tintColor: UIColor = .black

  func makeUIView(context: Context) -> UIImageView {
    let imageView = UIImageView()
    imageView.setContentCompressionResistancePriority(.fittingSizeLevel, 
                                                      for: .vertical)
    return imageView
  }

  func updateUIView(_ uiView: UIImageView, context: Context) {
    uiView.contentMode = contentMode
    uiView.tintColor = tintColor
    if let image = UIImage(named: name) {
      uiView.image = image
    }
  }
}

This loses some SwiftUI Image modifiers (you still have normal View modifiers) but you can always pass in some parameters such as contentMode and tintColor as shown above. Add more if needed and handle accordingly.


Usage Example:

struct ContentView: View {
  var body: some View {
    VStack {
      MyImageView(name: "icon", //REQUIRED
                  contentMode: .scaleAspectFit, //OPTIONAL
                  tintColor: .black /*OPTIONAL*/)
        .frame(width: 27, height: 27)
      MyImageView(name: "icon_small", //REQUIRED
                  contentMode: .scaleAspectFit, //OPTIONAL
                  tintColor: .black /*OPTIONAL*/)
        .frame(width: 27, height: 27)
    }
  }
}

Now this is all speculation but it looks as though SwiftUI treats vector images as a PNG.

The following example is a simple side by side comparison of the small and large vector images rendered in UIKit's UIImageView and SwiftUI's Image.

Comparison:

struct ContentView: View {
  let (largeImage, smallImage) = ("icon", "icon_small")
  let range = stride(from: 20, to: 320, by: 40).map { CGFloat($0) }

  var body: some View {
    List(range, id: \.self) { (side) in
      ScrollView(.horizontal) {
        VStack(alignment: .leading) {
          Text(String(format: "%gx%g", side, side))
          HStack {
            VStack {
              Text("UIKit")
              MyImageView(name: self.smallImage)
                .frame(width: side, height: side)
              MyImageView(name: self.largeImage)
                .frame(width: side, height: side)
            }
            VStack {
              Text("SwiftUI")
              Image(self.smallImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: side)
              Image(self.largeImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: side)
            }
          }
        }
      }
    }
  }
}

Results:

  1. Top row; Left : Small Image in UIImageView
  2. Top row; Right : Small Image in SwiftUI Image
  3. Bottom row; Left : Large Image in UIImageView
  4. Bottom row; Right : Large Image in SwiftUI Image

UIKit's UIImageView has consistent performace while SwiftUI's Image is having trouble.

20x20


60x60


100x100


180x180