Interaction of SVG Images in Your iOS app.
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.xcworkspace
and 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!