Categories: Blog

Augmented reality wayfinding

Augmented Reality (AR) is a technology that superimposes virtual objects on a user’s real-world view, creating an enhanced mixed reality experience. AR blends real and virtual objects into a single environment, allowing the user to interact with both real and virtual objects in a seamless manner.

AR technology uses a device’s camera to capture the user’s real-world view, and then processes this view using computer vision algorithms. This process involves identifying features in the real-world view, such as edges, textures, and patterns, and using these features to build a 3D model of the environment. The virtual objects are then rendered and overlaid onto the real-world view, creating an augmented experience.

AR experiences can be created using a variety of devices, including smartphones, tablets, and head-mounted displays. Some of the popular AR development platforms include Apple’s ARKit, Google’s ARCore, and Unity’s AR Foundation. These platforms provide developers with a set of tools and APIs to build AR applications, and they also handle many of the low-level tasks involved in AR development, such as camera tracking and feature detection.

AR Nodes, they are objects that represent virtual content in the AR environment. An AR Node can be thought of as a 3D object with a position and orientation in space, and can be used to represent anything from simple shapes to complex models. AR Nodes are typically used in AR development to build more complex AR scenes, and they allow for hierarchical relationships between objects. For example, an AR Node can be used to represent a car, and child AR Nodes can be used to represent the wheels, doors, and other parts of the car. This structure enables developers to manage complex AR scenes and control the behavior of individual objects within the scene.

For implementing AR into iOS. We need to import the ARKit package into the project.

Step 1:

Initialize pod for project using terminal to run below commands.

pod init
open Podfile

Add below 2 pods into the Podfile.

pod 'GoogleMaps'
pod 'GooglePlaces'

Save Podfile and run this command into terminal

pod install

Open your project .xcworkspace file.

Step 2:

Add below key into Info.Plist file

NSLocationWhenInUseUsageDescription

Step 3:

Add below swift package under Project target -> swift packages section

Step 4:

Create google api key and add it into the project globally.

Step 5:

Set Google api key when app launches. Add below code in appdelegate.swift file.


func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    GMSPlacesClient.provideAPIKey(GOOGLE_APIKEY)
    GMSServices.provideAPIKey(GOOGLE_APIKEY)
    return true
}

Step 6:

Add UIView and button to ViewController and assign GMSMapView class to UIView.

Step 7:

Create a MapViewController.swift file and assign it to the viewcontroller in the storyboard. We will setup google map related functionalities into this file.


import GoogleMaps
import GooglePlaces
 
class MapViewController: UIViewController {
    @IBOutlet weak var mapView: GMSMapView!
    
    //MARK: Go button action created
    @IBAction func navigateButton(_ sender: Any) {
        let vc = ViewController()
        self.navigationController?.pushViewController(vc, animated: true)
    }
    
    let locationManager = CLLocationManager()
    var currentLocation: CLLocation?
    var routeCoordinates = [CLLocationCoordinate2D]()
    
    var resultsViewController: GMSAutocompleteResultsViewController?
    var searchController: UISearchController?
    var resultView: UITextView?
    var selectedPin: GMSMarker?
}

Step 8:

Setup mapview functionalities when view controller loads.


override func viewDidLoad() {
    super.viewDidLoad()
    // Configuring location manager
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.requestWhenInUseAuthorization()
    locationManager.requestLocation()
    
    // Configure mapView
    mapView.delegate = self
    mapView.isUserInteractionEnabled = true
    
    // Zoom to user location
    let camera = GMSCameraPosition.camera(withLatitude: 0.01, longitude: 0.01, zoom: 15)
    mapView.camera = camera
    
    // Hide back button
    self.navigationItem.setHidesBackButton(true, animated: false)
    
    resultsViewController = GMSAutocompleteResultsViewController()
    resultsViewController?.delegate = self
    
    searchController = UISearchController(searchResultsController: resultsViewController)
    searchController?.searchResultsUpdater = resultsViewController
    
    // Put the search bar in the navigation bar.
    searchController?.searchBar.sizeToFit()
    navigationItem.titleView = searchController?.searchBar
    
    // When UISearchController presents the results view, present it in
    // this view controller, not one further up the chain.
    definesPresentationContext = true
    
    // Prevent the navigation bar from being hidden when searching.
    searchController?.hidesNavigationBarDuringPresentation = false
}

Step 9:

We will get location coordinates for source and destination. From that value we will draw a path in below method.


//MARK: Draw polyline route from source to destination.
func getPolylineRoute(from source: CLLocationCoordinate2D, to destination: CLLocationCoordinate2D){
    
    let config = URLSessionConfiguration.default
    let session = URLSession(configuration: config)
    
    let url = URL(string: "https://maps.googleapis.com/maps/api/directions/json?origin=\(source.latitude),\(source.longitude)&destination=\(destination.latitude),\(destination.longitude)&sensor=true&mode=driving&key=\(GOOGLE_APIKEY)")!
    
    let task = session.dataTask(with: url, completionHandler: {
        (data, response, error) in
        if error != nil {
            print(error!.localizedDescription)
            //self.activityIndicator.stopAnimating()
        }
        else {
            do {
                if let json : [String:Any] = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]{
                    
                    guard let routes = json["routes"] as? NSArray else {
                        DispatchQueue.main.async {
                            // self.activityIndicator.stopAnimating()
                        }
                        return
                    }
                    
                    if (routes.count > 0) {
                        let overview_polyline = routes[0] as? NSDictionary
                        let dictPolyline = overview_polyline?["overview_polyline"] as? NSDictionary
                        
                        let points = dictPolyline?.object(forKey: "points") as? String
                        if let legs = overview_polyline!["legs"] as? NSArray {
                            let legsValue = legs[0] as? NSDictionary
                            if let steps = legsValue!["steps"] as? NSArray {
                                for i in 1 ..< steps.count {
                                    let dic = steps[i] as? NSDictionary
                                    if i == 0 {
                                        let startLocation = dic!["start_location"] as? NSDictionary
                                        let location = CLLocationCoordinate2D(latitude: Double((startLocation!["lat"] as? NSNumber)!), longitude: Double((startLocation!["lng"] as? NSNumber)!))
                                        self.routeCoordinates.append(location)
                                    } else if i == steps.count - 1 {
                                        let startLocation = dic!["start_location"] as? NSDictionary
                                        let location = CLLocationCoordinate2D(latitude: Double((startLocation!["lat"] as? NSNumber)!), longitude: Double((startLocation!["lng"] as? NSNumber)!))
                                        self.routeCoordinates.append(location)
                                        let endLocation = dic!["end_location"] as? NSDictionary
                                        let secondLocation = CLLocationCoordinate2D(latitude: Double((endLocation!["lat"] as? NSNumber)!), longitude: Double((endLocation!["lng"] as? NSNumber)!))
                                        self.routeCoordinates.append(secondLocation)
                                        
                                    } else {
                                        let endLocation = dic!["end_location"] as? NSDictionary
                                        let location = CLLocationCoordinate2D(latitude: Double((endLocation!["lat"] as? NSNumber)!), longitude: Double((endLocation!["lng"] as? NSNumber)!))
                                        self.routeCoordinates.append(location)
                                    }
                                }
                            }
                            
                            //Path is available now, Let’s show path on map
                            self.showPath(polyStr: points!)
                            
                            DispatchQueue.main.async {
                                // self.activityIndicator.stopAnimating()
                                
                                let bounds = GMSCoordinateBounds(coordinate: source, coordinate: destination)
                                let update = GMSCameraUpdate.fit(bounds, with: UIEdgeInsets(top: 170, left: 30, bottom: 30, right: 30))
                                self.mapView!.moveCamera(update)
                            }
                        }
                        else {
                            DispatchQueue.main.async {
                                // self.activityIndicator.stopAnimating()
                            }
                        }
                    }
                }
            }
            catch {
                print("error in JSONSerialization")
                DispatchQueue.main.async {
                    // self.activityIndicator.stopAnimating()
                }
            }
        }
    })
    task.resume()
}


//MARK: Show path on map function
func showPath(polyStr :String){
    let path = GMSPath(fromEncodedPath: polyStr)
    let polyline = GMSPolyline(path: path)
    polyline.strokeWidth = 3.0
    polyline.strokeColor = UIColor.red
    polyline.map = mapView // Your map view
}

Step 10:

We will check if permission is granted or not. If permission is granted then we will request for location and update location.



//MARK: Configure CLLocationManagerDelegate method(s)
extension MapViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        if status == .authorizedWhenInUse {
            locationManager.requestLocation()
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        currentLocation = locations[0]
        self.mapView.clear()
        let marker = GMSMarker()
        marker.position = (currentLocation?.coordinate)!
        marker.icon = UIImage(named: "current")
        marker.map = mapView
        let camera = GMSCameraPosition.camera(withTarget: (currentLocation?.coordinate)!, zoom: 15)
        mapView.camera = camera
        
        print("Current location altitude: \(currentLocation?.altitude)")
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("Error finding location: \(error.localizedDescription)")
    }
}

Step 11:

ConfigGMSAutocompleteResultsViewControllerDelegate used to show place autocomplete predictions in a table view.


//MARK: ConfigGMSAutocompleteResultsViewControllerDelegate method(s)
extension MapViewController: GMSAutocompleteResultsViewControllerDelegate {

    func resultsController(_ resultsController: GMSAutocompleteResultsViewController,
                           didAutocompleteWith place: GMSPlace) {
        searchController?.isActive = false
        // Do something with the selected place.
        print("Place name: \(place.name)")
        print("Place address: \(place.formattedAddress)")
        print("Place attributions: \(place.attributions)")
        if (place.name?.contains("Let\'s Nurture"))!{
            DispatchQueue.main.asyncAfter(deadline:.now() + 2) {
                self.navigateButton(self)
            }
        }
    }

 func resultsController(_ resultsController: GMSAutocompleteResultsViewController,
                           didFailAutocompleteWithError error: Error){
        // TODO: handle the error.
        print("Error: ", error.localizedDescription)
    }

    // Turn the network activity indicator on and off again.
    func didRequestAutocompletePredictions(_ viewController: GMSAutocompleteViewController) {
        UIApplication.shared.isNetworkActivityIndicatorVisible = true
    }
    
    func didUpdateAutocompletePredictions(_ viewController: GMSAutocompleteViewController) {
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
    }
    func resultsController(_ resultsController: GMSAutocompleteResultsViewController, didSelect prediction: GMSAutocompletePrediction) -> Bool {
       
        return true
    }
}

Step 12:

GMSMapViewDelegate used to place marker on mapview


//MARK: GMSMapViewDelegate delegate method(s)
extension MapViewController: GMSMapViewDelegate {
    func mapView(_ mapView: GMSMapView, markerInfoWindow marker: GMSMarker) -> UIView? {
        return UIView()
        }
}

Step 13:

Create ViewController.swift file and below code into that file. We are using 2 packages into this file.

1. FocusNode – Focus node is used to add node on live scene
2. SmartHitTest – Used to estimate the position of the anchor, like looking for the best position based on what we know about our detected planes in the scene.


import ARKit
import FocusNode
import SmartHitTest
 
extension ARSCNView: ARSmartHitTest {}
 
class ViewController: UIViewController {
    
    var sceneView = ARSCNView(frame: .zero)
    
    let focusSquare = FocusSquare()
    
    var hitPoints = [SCNVector3]() {
        didSet {
            self.pathNode.path = self.hitPoints
        }
    }
    
    var pathNode = SCNPathNode(path: [])
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //Add button in navigation bar right side
        let btn = UIBarButtonItem(title: "Add/Clear", style: .plain, target: self, action: #selector(newRouteDraw))
        self.navigationItem.rightBarButtonItem = btn
        
        self.sceneView.frame = self.view.bounds
        self.sceneView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        
        self.view.addSubview(sceneView)
        
        // Set the view's delegate
        self.sceneView.delegate = self
        
        self.focusSquare.viewDelegate = self.sceneView
        self.sceneView.scene.rootNode.addChildNode(self.focusSquare)
        
        // the next chunk of lines are just things I've added to make the path look nicer
        let pathMat = SCNMaterial()
        self.pathNode.materials = [pathMat]
        self.pathNode.position.y += 0.05
        //        if Int.random(in: 0...1) == 0 {
        //            pathMat.diffuse.contents = UIImage(named: "path_seamless")
        //            self.pathNode.textureRepeats = true
        //        } else {
        pathMat.diffuse.contents = UIImage(named: "path_with_fade")
        //        }
        self.pathNode.width = 0.5
        
        self.sceneView.scene.rootNode.addChildNode(self.pathNode)
        
        if let data = loadData(){
            hitPoints = data
        } else {
            self.setupGestures()
        }
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal, .vertical]
        
        sceneView.session.run(configuration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        sceneView.session.pause()
    }

Step 14:

We will draw new route and save that into user defaults for future use and will remove points



//MARK: New route draw and save into user defaults.
@objc func newRouteDraw(){
    let defaults: UserDefaults = UserDefaults.standard
    defaults.removeObject(forKey: "kLetsNurtureRoute")
    defaults.synchronize()
    hitPoints.removeAll()
    self.setupGestures()
}

Step 15:

Will fetch existing routes from user defaults



//MARK: get data from user defaults
func loadData() -> [SCNVector3]?{
    let defaults: UserDefaults = UserDefaults.standard
    let data = defaults.object(forKey: "kLetsNurtureRoute") as? Data
    if data != nil {
        do {
            if let userinfo = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data! as Data) as? [SCNVector3]{
                return userinfo
            }
            else {
                return nil
            }
        }
    }
    return nil
}

Step 16:

Render scene and update time everytime



//MARK: Update AR view at specific time interval
extension ViewController: ARSCNViewDelegate {
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        DispatchQueue.main.async {
            self.focusSquare.updateFocusNode()
        }
    }
}

Step 17:

Add tap gesture on camera view and on tap add point to that position.



//MARK: Setup tap gesture on camera view
extension ViewController: UIGestureRecognizerDelegate {
    func setupGestures() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        tapGesture.delegate = self
        self.view.addGestureRecognizer(tapGesture)
    }
 
//Handle tap gesture functionality
    @IBAction func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
        guard gestureRecognizer.state == .ended else {
            return
        }
        if self.focusSquare.state != .initializing {
            self.hitPoints.append(self.focusSquare.position)
            
            let defaults: UserDefaults = UserDefaults.standard
            do{
                let data: Data = try NSKeyedArchiver.archivedData(withRootObject: self.hitPoints, requiringSecureCoding: false)
                defaults.set(data, forKey: "kLetsNurtureRoute")
            }
            catch{
                
            }
            defaults.synchronize()
        }
    }
}

Step 18:

Add/Update node when scene is rendering and set plane anchor according to it.



//MARK: Add/update node at the time of rendering
extension ViewController {
	func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
		if let planeAnchor = anchor as? ARPlaneAnchor, planeAnchor.alignment == .vertical, let geom = ARSCNPlaneGeometry(device: MTLCreateSystemDefaultDevice()!) {
			geom.update(from: planeAnchor.geometry)
			geom.firstMaterial?.colorBufferWriteMask = .alpha
			node.geometry = geom
		}
	}
 
	func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
		if let planeAnchor = anchor as? ARPlaneAnchor, planeAnchor.alignment == .vertical, let geom = node.geometry as? ARSCNPlaneGeometry {
			geom.update(from: planeAnchor.geometry)
		}
	}
}

Output

https://tinyurl.com/ydekktny

Lets Nurture

Share
Published by
Lets Nurture

Recent Posts

7 Powerful Psychological Triggers to Skyrocket Your Website Engagement

In the digital age, understanding the hidden forces driving user behavior is essential. By strategically…

9 months ago

What is haptics? How can we implement in in AR based mobile App? What are Haptics Use cases?

What is haptics?   Haptics refers to the use of touch feedback technology to simulate…

12 months ago

The Benefits of Using Virtual Reality in Business

In today's fast-paced and technologically driven world, businesses are constantly seeking innovative ways to stay…

1 year ago

A Closer Look at New Jersey’s Thriving Incubator Ecosystem

The Garden State, more popularly known as New Jersey, is not only known for its…

1 year ago

Why You Need a Mobile App for Your Business

In today's digital age, mobile apps have become an indispensable tool for businesses across all…

1 year ago

How to Optimize Your Website for Better User Experience

In today's digital era, a seamless and enjoyable user experience is crucial for the success…

1 year ago