pdfswiftuiimage-resizingvector-graphics

Why do PDFs resized in SwiftUI getting sharp edges?


I try to include a pdf in my SwiftUI enabled app using Xcode 11.4 and iOS 13.4. However, when I resize the pdf, it gets crips edges. I have included two versions of the pdf: One large pdf (icon.pdf) and one small pdf (icon_small.pdf). When I resize icon.pdf it gets start edges, while icon_small.pdf gets smooth edges. The issue applies to all other pdfs I have tried as well.

enter image description here

This is my code:

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer()
            Text("icon.pdf:")
            Image("icon")
                .resizable()
                .renderingMode(.template)
                .aspectRatio(contentMode: .fit)
                .frame(width: 27.0, height: 27.0)
            Spacer()
            Text("icon_small.pdf:")
            Image("icon_small")
            Spacer()
        }
    }
}

Both icon.pdf and icon_small.pdf have the following asset settings:

The pdfs are available here:


Solution

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

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

    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