Interaction of SVG Images in Your iOS app.

Ankur Lahiry
6 min readDec 6, 2019

--

In our day to day lives, we like to do things according to our personal preference. If User rides on a bus, he/she may prefer window seats to other seats, may prefer the middle side of a theatre hall for watching a movie. What if we, the developers give an option to the users to select their preferences for any kind of reservation like bus seat, train seat or hotel room?

That is the idea what we are going to implement right now. In case of developers side, we may break down the above scene in a short story. We provide an image to the user and give them interact with the image, more precisely on some specific part of an image. Seems like hard, but actually not.

Forget the above examples right now. Let us play a game. We will load a world map and select our own county. Let’s start.

Why SVG?

Scalable Vector Graphics (SVG) is the description of an image as an application of the Extensible Markup Language (XML). If any web browser recognises XML, then the web browser can show the data as an image in the web browser. SVG is more interactive than any other image format like .jpg, .jpeg, .png etc.

Start The Project

In this project we will use Macaw, a powerful tool for rendering the SVG.

Open a new project named SVGInteraction in XCode.

open the terminal , navigate to the project directory and run the following command.

pod init

open the podfile, add the following line and save the file.

pod 'Macaw'

run the following command in terminal

pod install

open the SVGInteraction.xcworkspaceand create the storyboard as per below

As you can see, we have 4 additional files named SVGDetailsViewControllerViewController.swift, SVGLayoutViewController.swift,

SVGScrollView.swift, SVGMacawView.swift.

In SVGDetailsViewController, we will show the selected component in a label.

In SVGLayoutViewController.swift, we will render the SVGScrollView. SVGScrollView will be needed if we use zoom in and zoom out and other scrolling mechanism.

This scroll view will hold the original and our desired view SVGMacawView.swift

add your svg file in your bundle. In our case the SVG file is world.svg

Render SVG in app

So far we have done the preliminary work, now time is to show the SVG in our application.

in SVGLayoutViewController.swift add the following lines

var svgScrollView: SVGScrollView!override func viewDidLoad() {     super.viewDidLoad()     self.svgScrollView = SVGScrollView(template: "world", frame: self.view.frame)     self.view.addSubview(self.svgScrollView)     self.svgScrollView.backgroundColor = .white}

In SVGScrollView.swift

let maxScale: CGFloat = 15.0let minScale: CGFloat = 1.0let maxWidth: CGFloat = 4000.0var svgView : SVGMacawView!public init(template: String, frame : CGRect) {      super.init(frame: frame)      svgView = SVGMacawView(template: template, frame: CGRect.init(x: 0, y: 0, width: frame.size.width, height: frame.size.height))      addSubview(svgView)      contentSize = svgView.bounds.size      minimumZoomScale = minScale      maximumZoomScale = maxScale      decelerationRate = UIScrollView.DecelerationRate.normal}required init?(coder: NSCoder) {      fatalError("init(coder:) has not been implemented")}

In SVGMacawView.swift

class SVGMacawView: MacawView {init(template: String, frame : CGRect) {    super.init(frame : frame)    self.backgroundColor = .white    if let node = try? SVGParser.parse(resource: "stage", ofType: "svg", inDirectory: nil, fromBundle: Bundle.main) {       if let group = node as? Group {          let rect = Rect.init(x: 1, y: 1, w: 4000, h: 4000)          let backgroundShape = Shape(form: rect, fill: Color.clear, tag: ["back"])          var contents = group.contents          contents.insert(backgroundShape, at: 0)          group.contents = contents          self.node = group} else {         self.node = node}// layout         self.contentMode = .center}}@objc required convenience init?(coder aDecoder: NSCoder) {     fatalError("init(coder:) has not been implemented")}}

Run the code. Congratulations, you will find the SVG in your app!

Pretty simple. Isn’t it?

You may ask question, the .svg is stored in my bundle and I have loaded from the bundle. What if I have to download the SVG from my backend?

I will describe this state latter in the article. Just keep away your headache.

Select The component

We have to select the path from the svg. But how to distinguish one path from another?

Open the SVG in Sublime Text, you will find a xml text instead of an image. Search for id , you will see every path has an id. The id is the everything of all the process what we will do the next.

Forget all other classes except SVGMacawView

import SWXMLHash in SVGMacawView.

add an array for getting the ids in the xml.

private var pathArray = [String]()

Then add the following code after self.node = group

if let url = Bundle.main.url(forResource: template, withExtension: "svg") {      if let xmlString = try? String(contentsOf: url) {            let xml = SWXMLHash.parse(xmlString)            enumerate(indexer: xml, level: 0)            for case let element in pathArray {                  self.registerForSelection(nodeTag: element)             }      }}

In this case, we start parsing the xml file and get the xml string. As we want to select all the countries, we have to search all the ids having in the xml. enumerate does the same.

private func enumerate(indexer: XMLIndexer, level: Int) {for child in indexer.children {     if let element = child.element {          if let idAttribute = element.attribute(by: "id") {          let text = idAttribute.text          pathArray.append(text)        }     }    enumerate(indexer: child, level: level + 1)   }}

now, the component selection

private func registerForSelection(nodeTag : String) {     self.node.nodeBy(tag: nodeTag)?.onTouchPressed({ (touch) in     let nodeShape = self.node.nodeBy(tag: nodeTag) as! Shape     nodeShape.fill = Color.blue  })}

Here we are, we can now select any country

Zoom In and Zoom Out

You may find the image is needed to be zoomed. Yes! we can zoom in and can select your country.

Get back from SVGMacawView . We have to add zoom mechanism in SVGScrollView

Add import Macaw in SVGScrollView

add the following code in public init(template : String, frame : CGRect)

panGestureRecognizer.delegate = selfpinchGestureRecognizer?.delegate = selfpanGestureRecognizer.isEnabled = true

implements the delegate functions

extension SVGScrollView : UIScrollViewDelegate {public func viewForZooming(in scrollView: UIScrollView) -> UIView? {return svgView}public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {self.svgView.contentScaleFactor = scale + 5self.svgView.layoutIfNeeded()}}extension SVGScrollView: UIGestureRecognizerDelegate {public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {       if let recognizer = gestureRecognizer as? UIPinchGestureRecognizer {        let location = recognizer.location(in: self)        let scale = Double(recognizer.scale)        let anchor = Point(x: Double(location.x), y: Double(location.y))         let node = self.svgView.node         node.place = Transform.move(dx: anchor.x * (1.0 - scale), dy: anchor.y * (1.0 - scale)).scale(sx: scale, sy: scale)}       return true}public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {     return true}}

Run the code. You can zoom in to your country and select that country. Let me introduce my country, Bangladesh!

Show the country name

While selecting the path from the SVGMacaw view, pass the path id to SVGDetailsViewController . Create a dictionary where id is the key and countryName is the value. Get the name from the dictionary and show it on the label.

Load SVG From Server

So far, I have shown an example that loads a SVG from locally. But when we want to load the image from the server, we have to make a little bit different approach. Load image from server in SVGLayoutViewController , then pass the xml string directly in SVGScrollView(template : frame) function as template.(Scroll up the article and you will find that we passed the svg file name here).

In case of Parsing in SVGMacawVIew , parse the xml directly using

if let node = try? SVGParser.parse(text: template)

instead of

if let node = try? SVGParser.parse(resource: "stage", ofType: "svg", inDirectory: nil, fromBundle: Bundle.main)

Replace the lines

if let url = Bundle.main.url(forResource: template, withExtension: "svg") {       if let xmlString = try? String(contentsOf: url) {       let xml = SWXMLHash.parse(xmlString)       enumerate(indexer: xml, level: 0)       for case let element in pathArray {              self.registerForSelection(nodeTag: element)        }    }}

with

let xml = SWXMLHash.parse(template)enumerate(indexer: xml, level: 0)for case let element in pathArray {      self.registerForSelection(nodeTag: element)}

others are the same.

Cheers

Checkout the full project here.

Thanks for reading!

--

--

Ankur Lahiry
Ankur Lahiry

Written by Ankur Lahiry

CS PhD, Ex-Software Engineer, iOS Developer

Responses (2)