Introduction to Google Cardboard for iOS
Dive into the world of virtual reality with Google Cardboard VR and learn how to use the iOS SDK to embark on a worldwide 360 vacation! By Lyndsey Scott.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Introduction to Google Cardboard for iOS
25 mins
Making the VR Views Interactive
In Vacation 360, the user can click through images while viewing imageVRView
and can play and pause video while viewing videoVRView
. The backing classes for each of these inherit from GVRWidgetView
, which implements the GVRWidgetViewDelegate
protocol. This protocol lets GVRWidgetView
notify its delegate of various state changes and interactions, which will come in handy while customizing the VR view behaviors.
Directly underneath the closing bracket of the VacationViewController
class, add the following:
extension VacationViewController: GVRWidgetViewDelegate {
func widgetView(_ widgetView: GVRWidgetView!, didLoadContent content: Any!) {
}
func widgetView(_ widgetView: GVRWidgetView!, didFailToLoadContent content: Any!,
withErrorMessage errorMessage: String!) {
}
func widgetView(_ widgetView: GVRWidgetView!, didChange displayMode: GVRWidgetDisplayMode) {
}
func widgetViewDidTap(_ widgetView: GVRWidgetView!) {
}
}
You’ve implemented the GVRWidgetViewDelegate
and all of its methods in an extension. It’s time to fill in each method one by one.
widgetView(_:didLoadContent:)
is called when a VR view has loaded its content successfully. What’s that you say? How about using this method to only reveal elements of the view once they have loaded? Sounds like a great idea! :]
First, head back to viewDidLoad()
and add the following just below super.viewDidLoad()
:
imageLabel.isHidden = true
imageVRView.isHidden = true
videoLabel.isHidden = true
videoVRView.isHidden = true
This will hide all of the labels and VR views initially, so that they won’t appear until the content loads.
Now, grouped with the other imageVRView
code in viewDidLoad()
, add the following:
imageVRView.delegate = self
Similarly, add the following with the videoVRView
code:
videoVRView.delegate = self
This sets both view’s GVRWidgetViewDelegate
s to the VacationViewController
.
You can now unhide the labels and views once their corresponding content has loaded. Back in widgetView(_:didLoadContent:)
, add the following:
if content is UIImage {
imageVRView.isHidden = false
imageLabel.isHidden = false
} else if content is NSURL {
videoVRView.isHidden = false
videoLabel.isHidden = false
}
widgetView(_:didLoadContent:)
is passed the loaded content
object – in this case a UIImage
for GVRPanoramaView
s and an NSURL
for GVRVideoView
s. For the UIImage
, unhide the imageVRView
and its corresponding label. For an NSURL
, unhide the videoVRView
and its corresponding label.
Build and run; you’ll see that each VR view and label now only appears when its corresponding content has loaded:
Add the following to widgetView(_:didFailToLoadContent:withErrorMessage:)
:
print(errorMessage)
This is called when there’s an issue loading the content. In that case, you simply print the passed errorMessage
.
Next you’ll use widgetView(_:didChange:)
to store the current display mode and selected view, so that tapping on the widgets will trigger the appropriate actions only when a VR view is in fullscreen or VR mode. This method is called each time you switch between embedded, fullscreen, or VR mode and passes the involved widgetView
.
First, in the main VacationViewController
class below the enum Media
declaration block, add the following:
var currentView: UIView?
var currentDisplayMode = GVRWidgetDisplayMode.embedded
currentView
will maintain a reference to the view currently displayed in fullscreen or VR mode. currentDisplayMode
is used to hold the current display mode. It is initialized to GVRWidgetDisplayMode.embedded
which represents the initial display mode when the VR views are embedded in the VacationViewController
.
Then in widgetView(_:didChange:)
, add the following:
currentView = widgetView
currentDisplayMode = displayMode
widgetView(_:didChange:)
passes the new displayMode
as well as the widgetView
where the mode was changed. These are stored in currentDisplayMode
and currentView
, respectively, for later reference.
Knowing the display mode and displayed view, you can finally start to implement the interactive behaviors while in fullscreen or VR mode. Tapping the magnetic widget button on Cardboard or directly tapping the screen should cycle through the photos in Media.photoArray
when the imageVRView
is in fullscreen or VR mode. To implement this behavior, add the following to widgetViewDidTap(_:)
:
// 1
guard currentDisplayMode != GVRWidgetDisplayMode.embedded else {return}
// 2
if currentView == imageVRView {
Media.photoArray.append(Media.photoArray.removeFirst())
imageVRView?.load(UIImage(named: Media.photoArray.first!),
of: GVRPanoramaImageType.mono)
}
- Since you only want to handle taps while in fullscreen or VR mode, a
currentDisplayMode
of.embedded
triggers an early return. - If
currentView
isimageVRView
, advance theMedia.photoArray
queue by removing the first element of the array and appending it to the end. SinceMedia.photoArray.removeFirst()
returns the removed element, you can remove the first element and append it to the end in a single line of code. Then you loadimageVRView
‘sUIImage
using the file now in the first index of the photo array.
Build and run your project; open the image VR view into either fullscreen or VR fullscreen, then tap either the device screen or the widget button respectively. You should be able to click through the images.
You may notice one minor issue: the other elements of the view show through between each click. To prevent that, hide the elements whenever you’re not viewing the embedded display mode by adding the following to the end of widgetView(_:didChange:)
:
if currentView == imageVRView && currentDisplayMode != GVRWidgetDisplayMode.embedded {
view.isHidden = true
} else {
view.isHidden = false
}
If the currentView
is the imageVRView
and the currentDisplayMode
isn’t embedded, this means you’re viewing the image view in fullscreen or VR mode. In that case, hide the view controller’s root view; otherwise, unhide it.
Build and run; click through the images again, and the transition between images should be almost seamless:
Note: Even after setting view.isHidden
to true, the GVRPanoramaView
stays unhidden because it and the GVRVideoView
are not subviews of the VacationViewController
‘s view when in fullscreen or VR mode.
Congrats on successfully creating a 360 image slideshow!
But don’t get too lost in your vacation yet. The interactions are only halfway done!
Video View Interaction
You still have to implement the video view interaction which will let you pause and play the “Living with Elephants” video by Photos of Africa so that each moment of your vacation can last even longer.
Back in the main VacationViewController
class, add the following new variable underneath the var currentDisplayMode
declaration:
var isPaused = true
There is no property inherent to the GVRVideoView
that indicates whether the video is paused or playing, so the variable you’ve just created can serve as a state marker.
Also create a method to set the initial play state behavior dependent on the display mode. Underneath viewDidLoad()
add:
func refreshVideoPlayStatus() {
// 1
if currentView == videoVRView && currentDisplayMode != GVRWidgetDisplayMode.embedded {
videoVRView?.resume()
isPaused = false
}
// 2
else {
videoVRView?.pause()
isPaused = true
}
}
- If
videoVRView
is in fullscreen or VR mode, calling this method plays the video and appropriately setsisPaused
tofalse
. - Otherwise, if the
videoVRView
‘s display mode is in its embedded state or theimageVRView
is in fullscreen, calling this method pauses the video and appropriately setsisPaused
totrue
.
Then within widgetView(_:didLoadContent:)
‘s else if
block, add:
refreshVideoPlayStatus()
This pauses the video as soon as it loads.
Now in widgetView(_:didChange:)
, just before the if
block and after setting currentDisplayMode
, insert the following:
refreshVideoPlayStatus()
By calling refreshVideoPlayStatus()
whenever the display mode changes, the video will play when entering fullscreen or VR mode, and pause otherwise.
Now that you’ve set videoVRView
‘s initial display mode behaviors, you can implement a play/pause toggle triggered by user interaction. In widgetViewDidTap(_:)
, after the if block, add:
else {
if isPaused {
videoVRView?.resume()
} else {
videoVRView?.pause()
}
isPaused = !isPaused
}
You hit this else
clause when you tap a videoVRView
. If the video is paused, resume playing; if the video is playing, pause it. Then you update isPaused
to reflect the new play state.
Build and run your app, open the video into fullscreen or VR mode, then tap the screen or widget button and the video should toggle between play and pause states:
You don’t want your vacation to stop after a few minutes though, do you? Of course not! So therefore you’ll loop the video to the beginning once it reaches the end.
Add a GVRVideoViewDelegate
extension below the final bracket of the GVRWidgetViewDelegate
extension to implement the looping behavior:
extension VacationViewController: GVRVideoViewDelegate {
func videoView(_ videoView: GVRVideoView!, didUpdatePosition position: TimeInterval) {
if position >= videoView.duration() {
videoView.seekTo(0)
videoView.resume()
}
}
}
videoView(_:didUpdatePosition:)
is called at approximately one-second intervals as the video plays. Once the NSTimeInterval
position
of the video is greater than or equal to the duration
of the video, the video has reached its end. videoView.seekTo(0)
then sets the position back to the beginning before resuming the video.
Note: There is no need to set VacationViewController
as the GVRVideoViewDelegate
because…it’s already set! GVRVideoViewDelegate
inherits from GVRWidgetViewDelegate
, which you’ve already adopted and set up in viewDidLoad()
of VacationViewController
.
There you have it! Now you can build, run, sit back and enjoy the worldwide vacation you’ve just created with your bare hands. Isn’t iOS development spectacular?