SwiftUIPanoramaViewer
is a high-performance library that uses SceneKit to display complete spherical or cylindrical panoramas with touch, Gamepad or motion based controls that can be used in SwiftUI projects.
SwiftUIPanoramaViewer
is based off of the source code from scihant's CTPanoramaView open source library, which has been heavily modified by me to support modern OSes, SwiftUI, reading Pitch & Yaw, support for Gamepads and many more features.
Why Make a New Library? I released
SwiftUIPanoramaViewer
as a new library/package instead of making a pull request onCTPanoramaView
for several reasons, but the main two are as follows: First, it looks like theCTPanoramaView
is no longer being supported. Second, the SwiftUI changes that I've made takeCTPanoramaView
away from its original design of supportingUIKit
and Objective-C.
As a result, you'll find both my MIT License included in the package, as well as, scihant's original MIT License from the CTPanoramaView
open source library.
If you find SwiftUIPanoramaViewer
useful and would like to help support its continued development and maintenance, please consider making a small donation, especially if you are using it in a commercial product:
It's through the support of contributors like yourself, I can continue to build, release and maintain high-quality, well documented Swift Packages like SwiftUIPanoramaViewer
for free.
Swift Package Manager using Xcode:
- In Xcode, select the File > Add Package Dependency… menu item.
- Paste
https://github.com/Appracatappra/SwiftUIPanoramaViewer.git
in the dialog box. - Follow the Xcode's instruction to complete the installation.
Why not CocoaPods, or Carthage, or etc?
Supporting multiple dependency managers makes maintaining a library exponentially more complicated and time consuming.
Since, the Swift Package Manager is integrated with Xcode, it's the easiest choice to support going further.
SwiftUIPanoramaViewer
provides a simply way to add an interactive spherical or cylindrical panorama viewer to any SwiftUI View
. Let's look at the following example:
@State var rotationIndicator:Float = 0.0
...
ZStack {
// Display the panorama
PanoramaViewer(image: SwiftUIPanoramaViewer.bindImage("PanoramaImageName")) {key in }
cameraMoved: { pitch, yaw, roll in
rotationIndicator = yaw
}
VStack {
Spacer()
CompassView()
.frame(width: 50.0, height: 50.0)
.rotationEffect(Angle(degrees: Double(rotationIndicator)))
}
}
// If using `SwiftUIGamepad` package, allow the gamepad to rotate the view.
.onGamepadLeftThumbstick(viewID: viewID) { xAxis, yAxis in
PanoramaManager.moveCamera(xAxis: xAxis, yAxis: yAxis)
}
Let's break down the key points:
rotationIndicator
- We created a State variable to track changes in the panorama's rotation.PanoramaViewer
- This displays the panorama with the default user interaction of touch on mobile devices.SwiftUIPanoramaViewer.bindImage("PanoramaImageName")
- The helper functionSwiftUIPanoramaViewer.bindImage
returns a boundUIImage
of the given name. This image should either be a spherical or cylindrical panorama held in the app's asset catalog or downloaded via On Demand Resources.cameraMoved
- ThecameraMoved
event is called when the user interacts with the panorama moving the view. Here we are storing the yaw (or X Axis movement) in therotationIndicator
variable to update the compass.CompassView()
- Displays a compass overlay at the bottom of the panorama indicating the direct the user is looking.rotationEffect
- We are usingrotationEffect
on theCompassView
along with therotationIndicator
variable to point the compass in the direction that the user is "looking" in the panorama.
If you want to store your panorama images in On Demand Resources, please see our ODRManager Package to help make working with ODR easier.
In its current implementation, only one
PanoramaViewer
can be active in your app at one time. You can use thePanoramaManager
singleton class to communicate with the currently active viewer.
By including our SwiftUIGamepad Package in your SwiftUI app, it's easy to add gamepad support to the PanoramaViewer
. The following lines of code allow the left thumbstick of an attached gamepad to move the panorama view:
// If using `SwiftUIGamepad` package, allow the gamepad to rotate the view.
.onGamepadLeftThumbstick(viewID: viewID) { xAxis, yAxis in
PanoramaManager.moveCamera(xAxis: xAxis, yAxis: yAxis)
}
Use PanoramaManager.moveCamera
to communicate the amount of change in the xAxis
(or yaw) and yAxis
(or pitch) in the panorama's view. The cameraMoved
event will be raised on the PanoramaViewer
is response to this change.
There is more setup required to use
SwiftUIGamepad
in a SwiftUIView
than is shown here. Only theSwiftUIPanoramaViewer
specific parts are shown for brevity. Please see theSwiftUIGamepad
DocC documentation for the full setup required.
By creating your own storage class and noting specific pitch
and yaw
points in a given panorama image, you can create Indicators or Interaction Points that can be displayed in response to cameraMoved
events.
If the pitch
and yaw
points returned to the cameraMoved
events match specific points in your structure, simply display an Icon or Button overlaying the PanoramaViewer
using a ZStack
.
The PanoramaManager
singleton class provides several helper function to assist in this task:
leadingTarget
- Calculates the Top Left corner of a given hit target (yaw, pitch).trailingTarget
- Calculates the Bottom Right corner of a given hit target (yaw, pitch).targetHit
- Given the currentpitch
andyaw
from thePanoramaViewer
check to see if it is within the given target point.inRange
- Tests the given rotational point to see if it is inside of the specified range.
Because of all the different ways that the
PanoramaViewer
can be used in a SwiftUI app, providing all of the data structures to support interaction and node navigation was out of the scope of this library. Any "default" implementation could either be too limiting or too complex for any given apps needs.As a result, I've decided to provide the tools to easily implement this functionality, while leaving the specifics up the the consuming app's code.
If you need to change the panorama image that is being displayed in the PanoramaViewer
due to user interaction (or any other in app event), there are a few extra steps that will need to be taken. See the following code snipit:
@State var panoImage:String = "MyImage"
...
PanoramaViewer(image: SwiftUIPanoramaViewer.bindImage(panoImage))
...
// Change the displayed image
PanoramaManager.shouldUpdateImage = true
PanoramaManager.shouldResetCameraAngle = false
panoImage = "newPanoImage"
In this code we are using State variable panoImage
to hold the current panorama image being displayed. Here are the key points:
PanoramaManager.shouldUpdateImage
- By setting this property totrue
you are informing thePanoramaViewer
that it needs to look for an upcoming image change. This allows the viewer to do the necessary housekeeping to insure the image changes smoothly without unwanted flickering in the UI.PanoramaManager.shouldResetCameraAngle
- By setting this property tofalse
we are telling thePanoramaViewer
to maintain the currentpitch
andyaw
when the image changes. If this property istrue
bothpitch
andyaw
will be reset to zero (0) when the image changes.panoImage
- When we specify a new image, thePanoramaViewer
will make the change and reset thePanoramaManager.shouldUpdateImage
property tofalse
.
The request to change the panorama image displayed in the
PanoramaViewer
should be executed on the main thread for best results.
The Package includes full DocC Documentation for all of its features.