iosswiftsvguiimageviewsdwebimage

Download SVG Image in iOS Swift


I'm working on a project which I'm getting data from API and in that API I'm getting url for image. but these images are not in PNG,JPEG or JPG formats. They are SVG(Scalable Vector Graphic) images. So as I know there is no direct method, function or maybe iOS project Framework library exist which is used to render SVG images. So we have bunch of Pods which is actually use for rendering these image like(SDWebImage,SDWebImageSVGCoder,SDWebImageSVGKitPlugin,SVGKit). I used them all but all these Pods didn't solve my problem. So let discuss what problem that I'm facing right now.

Here's my ViewController class code

import SDWebImageSVGCoder

class ViewController: UIViewController {
    
    var banks: [Bank] = [Bank(logo:"https://identity.moneyhub.co.uk/bank-icons/default",name:"Dummy"),
        Bank(logo: "https://identity.moneyhub.co.uk/bank-icons/accord", name: "Accord Mortgages"),
                           Bank(logo: "https://identity.moneyhub.co.uk/bank-icons/ajBell", name: "AJ Bell"),
                           Bank(logo: "https://identity.moneyhub.co.uk/bank-icons/aldermore", name: "Aldermore"),
                           Bank(logo: "https://identity.moneyhub.co.uk/bank-icons/amex", name: "American Express"),
                           Bank(logo: "https://identity.moneyhub.co.uk/bank-icons/brewinDolphin", name: "Brewin Dolphin"),
                           Bank(logo: "https://identity.moneyhub.co.uk/bank-icons/caxton", name: "Caxton"),]
    
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }
    
    func setupTableView() {
        self.tableView.delegate = self
        self.tableView.dataSource = self
    }

}

//MARK: - TABLEVIEW DELEGATE DATASOURCE
extension ViewController: UITableViewDelegate, UITableViewDataSource {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return banks.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: BankTableViewCell.identifier, for: indexPath) as! BankTableViewCell
        cell.configure(self.banks[indexPath.row])
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 62
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
    }
    
}

My TableViewCell class

class BankTableViewCell: UITableViewCell {
    
    @IBOutlet weak var bankNameLabel: UILabel!
    @IBOutlet weak var bankLogoImageView: UIImageView!
    
    class var identifier: String {
        return "BankTableViewCell"
    }
    
    func configure(_ bank: Bank) {
        self.bankNameLabel.text = bank.name
        guard let url = URL(string: bank.logo) else {return}
        self.bankLogoImageView.sd_setImage(fromURL: url)
    }
    
}

But when I run this code only 3 images rendered first one is dummy or default case, but rest of three are not rendered. Then I try to locate the issue and then I found that not all images are true SVG images actually some of them are Raster image. here's I'm sharing data of both image type of images.

Those Images who's rendered successfully has data like this

<svg viewBox="-25 -25 200 200" xmlns="http://www.w3.org/2000/svg">
    <path d="M138.299 130.707H10.644a6.386 6.386 0 00-6.384 6.391 6.384 6.384 0 006.384 6.384h127.652a6.384 6.384 0 006.384-6.384c-.002-3.532-2.86-6.391-6.381-6.391zM18.621 114.101c-3.526 0-6.384 2.859-6.384 6.388s2.858 6.391 6.384 6.391h111.697c3.526 0 6.384-2.861 6.384-6.391s-2.858-6.388-6.384-6.388h-1.593V56.625h1.593a3.19 3.19 0 000-6.38H18.621a3.19 3.19 0 000 6.38h1.597v57.472h-1.597v.004zm97.336-57.476v57.472H96.81V56.625h19.147zm-31.917 0v57.472H64.893V56.625H84.04zm-51.06 0h19.147v57.472H32.98V56.625zM10.644 44.503H138.34a6.387 6.387 0 006.402-6.388 6.392 6.392 0 00-4.312-6.044L77.094 3.558a6.4 6.4 0 00-5.237 0L8.026 32.293a6.385 6.385 0 00-3.622 7.165 6.38 6.38 0 006.24 5.045z"/>
</svg>

But those image who's not rendered but display white or null images has this type of data

<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="20" y="135" width="460" height="230" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0" transform="scale(0.005 0.01)"/>
</pattern>
<image id="image0" width="200" height="100" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAACBRJREF......UwT2kAAAAASUVORK5CYII="/>
</defs>
</svg>

So my Screen look like this after running the project.

List of banks

I found solution for this myself to just put condition and first check every image response. If image is pure written in SVG then I'm calling Pod function otherwise I'm using my own custom code to extract base64Encoded string and then render image. But it creating some performance issue. As you know library also working in Async calling and I'm also using that to images are sometime load and sometime not show correct image.

my custom code is here

class BankTableViewCell: UITableViewCell {
    
    @IBOutlet weak var bankNameLabel: UILabel!
    @IBOutlet weak var bankLogoImageView: SVGImageView!
    
    class var identifier: String {
        return "BankTableViewCell"
    }
    
    func configure(_ bank: Bank) {
        self.bankNameLabel.text = bank.name
        guard let url = URL(string: bank.logo) else {return}
        self.bankLogoImageView.loadImage(fromURL: url, placeHolderImage: "")
    }
    
}

class SVGImageView: UIImageView {
    
    private let imageCache = NSCache<AnyObject, UIImage>()
    
    func loadImage(fromURL imageURL: URL, placeHolderImage: String)
    {
        self.image = UIImage(named: placeHolderImage)

        if let cachedImage = self.imageCache.object(forKey: imageURL as AnyObject)
        {
            debugPrint("image loaded from cache for =\(imageURL)")
            self.image = cachedImage
            return
        }

        DispatchQueue.global().async {
            [weak self] in
            
            if let imageData = try? Data(contentsOf: imageURL) {
                guard let xmlStr = String(data: imageData, encoding: .utf8) else {
                    DispatchQueue.main.async {
                        self?.sd_setImage(with: URL(string: "https://identity.moneyhub.co.uk/bank-icons/default")!)
                    }
                    return
                }
                print(xmlStr)
                if let range = xmlStr.range(of: "data:image/png;base64,") {
                    let base64StringWithoutfilter = xmlStr.suffix(from: range.upperBound)
                    let base64StringWithoutfilterRange = base64StringWithoutfilter.range(of: "/>")
                    
                    let lowerRange = range.upperBound
                    if let upperRange = base64StringWithoutfilterRange?.upperBound {
                        let base64ImageString = xmlStr[lowerRange..<upperRange].dropLast(3)
                        if let imageData = NSData(base64Encoded: String(describing:base64ImageString)) {
                            if let image = UIImage(data: imageData as Data) {
                                DispatchQueue.main.async {
                                    self?.image = image
                                    return
                                }
                            }
                        }
                    }
                } else {
                    self?.sd_setImage(with: imageURL,completed: { image, error,cache, url in
                        if let image = image {
                            DispatchQueue.main.async {
                                self?.image = image
                                return
                            }
                        }
                    })
                }
            }
        }
    }
}

I just want to solve my issue to put my custom code into library because I don't know about Objective C and SDWebImageSVGCoder pod is written in Objective C. If this is not possible then kindly give me a suggestion, or any other library pod anything which I use and solve my problem and display my list look like this.

enter image description here


Solution

  • I'd use a custom decoder. Once done, you can call it with:

    self.bankLogoImageView.sd_setImage(with: url,
                                       placeholderImage: nil,
                                       context: [.imageCoder: CustomSVGDecoder(fallbackDecoder: SDImageSVGCoder.shared)])
    

    I named it CustomSVGDecoder, but since it's a PNG/JPG decoding, it might have a better name. I didn't check, but I guess, that the custom SVG you get use some SVG feature, and incorporate a non-vector image in it, but you currently just use the non-vector image.

    I gave it the fallback decoder of the default SVGDecoder from SDWebImageSVGDecoder, in order to avoid checks.

    class CustomSVGDecoder: NSObject, SDImageCoder {
    
        let fallbackDecoder: SDImageCoder?
    
        init(fallbackDecoder: SDImageCoder?) {
            self.fallbackDecoder =  fallbackDecoder
        }
    
        static var regex: NSRegularExpression = {
            let pattern = "<image.*xlink:href=\"data:image\\/(png|jpg);base64,(.*)\"\\/>"
            let regex = try! NSRegularExpression(pattern: pattern, options: [])
            return regex
        }()
    
        func canDecode(from data: Data?) -> Bool {
            guard let data = data, let string = String(data: data, encoding: .utf8) else { return false }
            guard Self.regex.firstMatch(in: string, range: NSRange(location: 0, length: string.utf16.count)) == nil else {
                print("CustomSVGDecoder: We can decode")
                return true //It self should decode
            }
            guard let fallbackDecoder = fallbackDecoder else {
                print("CustomSVGDecoder: Can't decode and there is no fallBack decoder")
                return false
            }
            print("CustomSVGDecoder: Will rely on fallback decoder to decode")
            return fallbackDecoder.canDecode(from: data)
        }
    
        func decodedImage(with data: Data?, options: [SDImageCoderOption : Any]? = nil) -> UIImage? {
            guard let data = data,
                    let string = String(data: data, encoding: .utf8) else { return nil }
            guard let match = Self.regex.firstMatch(in: string, range: NSRange(location: 0, length: string.utf16.count)) else {
                print("CustomSVGDecoder: Will rely on fallback decoder to decode because of no match")
                return fallbackDecoder?.decodedImage(with: data, options: options)
            }
            guard let rawBase64DataRange = Range(match.range(at: 2), in: string) else {
                print("CustomSVGDecoder: Will rely on fallback decoder to decode because we didn't fiund the base64 part")
                return fallbackDecoder?.decodedImage(with: data, options: options)
            }
            let rawBase64Data = String(string[rawBase64DataRange])
            guard let imageData = Data(base64Encoded: Data(rawBase64Data.utf8), options: .ignoreUnknownCharacters) else {
                print("CustomSVGDecoder: Will rely on fallback decoder to decode because of invalid base64")
                return fallbackDecoder?.decodedImage(with: data, options: options)
            }
            return UIImage(data: imageData)
        }
    
        //You might need to implement these methods, I didn't check their meaning yet
        func canEncode(to format: SDImageFormat) -> Bool {
            print("CustomSVGDecoder: canEncode(to:)")
            return true
        }
    
        func encodedData(with image: UIImage?, format: SDImageFormat, options: [SDImageCoderOption : Any]? = nil) -> Data? {
            print("CustomSVGDecoder: encodedData(with:format:options:)")
            return nil
        }
    }
    

    I used a regex to find the Base64 content, and it might need some modifications. You can use your own version with range(of:).