Mobile 360° Photo Viewer Development
Our company: 7+ years in mobile development, 30+ 360° projects, 5+ years on the market.
What Are the Limitations of UIImageView for 360° Photos?
UIImageView displays a flat image. For spherical projection you need 3D rendering: a sphere with inverted normals (to look from inside), an equirectangular texture on it, a camera in the center, and rotation control via gyroscope or touch.
We develop custom solutions for viewing 360° photos turnkey. For a mobile 360° photo viewer, we implement custom 360 degree viewer iOS using SceneKit 360 panorama, and for Android 360 viewer we use Filament. This equirectangular projection requires tiled loading and XMP metadata parsing. Our team has over 7 years in mobile development and more than 30 implemented projects with panoramas. Certified engineers guarantee stable operation on a wide range of devices. Budget savings compared to in-house development can be up to 50% (typical cost savings: 40–60%). Starting cost of a basic solution — from $1,500. Contact us — we will evaluate your project and offer the optimal solution.
How to Implement Smooth Viewing Control?
Control via gyroscope — CMMotionManager → Euler angles → SCNNode.rotation. Via touch — UIPanGestureRecognizer with cumulative rotation angle. Inertia on swipe is implemented via SCNAction with easing (easeOut), not physics — SceneKit physics adds unpredictable damping. We also apply mipmapping and anisotropic filtering to maintain texture quality at oblique angles.
let sceneView = SCNView(frame: view.bounds) let scene = SCNScene() // Sphere with inverted normals let sphere = SCNSphere(radius: 10.0) sphere.segmentCount = 96 // more segments = less distortion at poles let material = SCNMaterial() material.isDoubleSided = true material.diffuse.contents = UIImage(named: "pano.jpg") sphere.materials = [material] let sphereNode = SCNNode(geometry: sphere) sphereNode.scale = SCNVector3(-1, 1, 1) // normal inversion scene.rootNode.addChildNode(sphereNode) let camera = SCNCamera() camera.fieldOfView = 90 let cameraNode = SCNNode() cameraNode.camera = camera scene.rootNode.addChildNode(cameraNode) sceneView.scene = scene Why Is Tiled Loading Important for Large Panorama Images?
JPEG 50 MPix after decoding to RGB takes about 150 MB in memory. On iPhone SE 2nd gen with 3 GB RAM that is already a third of available memory — with background processes you get SIGKILL due to memory pressure. The solution is tiled loading via CATiledLayer or downscaling before passing to SceneKit. This reduces memory consumption by over 10x compared to full loading. We also use texture atlasing to further reduce GPU overhead. Tiled loading is critical for stability and avoids costly crashes. A typical project pays for itself within 3–6 months due to increased user engagement, saving up to 50% compared to development from scratch. The starting price for a basic viewer is $1,500; full cross-platform implementation typically costs between $3,000 and $5,000.
// Progressive loading: first preview 2048px, then full resolution let thumbnail = image.preparingThumbnail(of: CGSize(width: 4096, height: 2048)) material.diffuse.contents = thumbnail // Asynchronously load full resolution Task.detached(priority: .background) { let full = await loadFullResImage(url: imageURL) await MainActor.run { material.diffuse.contents = full } } preparingThumbnail(of:) — async API (iOS 15+), decodes in background thread without blocking main run loop.
Processing Google Photo Sphere XMP Metadata
Panorama cameras and Google Street View write XMP metadata into JPEG with projection type and crop information (GPano:ProjectionType, GPano:CroppedAreaImageWidthPixels). Without reading them, an incomplete panorama (270° instead of 360°) will be displayed as full — with stretched edges. We read via CGImageSourceCopyPropertiesAtIndex → XMP metadata → parse GPano:* fields, adjust UV-mapping of the texture. This is 30–50 lines of code, but without it a third of real panoramas look incorrect.
Android Rendering: Filament vs OpenGL ES
On Android we use Filament (Google's physically-based renderer) or Panorama360View via GLSurfaceView. Filament requires more setup but provides correct tone mapping for HDR panoramas, and processes HDR 2x faster than OpenGL ES. It also supports GPU instancing for multiple spheres. OpenGL ES 3.0 is a lighter option suitable for older devices, though it requires manual shader permutations. For regular JPEG, the difference is negligible.
Technology comparison for iOS and Android
| Platform | Tool | Performance | Complexity | Recommendation |
|---|---|---|---|---|
| iOS | SceneKit | High (native support) | Low | Quick prototype, non-HDR |
| iOS | Metal | Maximum | High | HDR, custom shaders |
| Android | Filament | High (HDR, PBR) | Medium | HDR panoramas |
| Android | OpenGL ES | Medium | High | Compatibility with legacy devices |
Memory load method comparison
| Method | RAM consumption (50 MPix) | Time to first frame |
|---|---|---|
| Full load | ~150 MB | 0.5–1 s (decoding) |
| Tiled with preview | ~12 MB (preview) | <0.1 s |
Deliverables
- Architecture design and selection of optimal tool (SceneKit/Filament).
- Integration of gyroscope and touch control with inertia.
- Tiled loading of large images with progressive display.
- XMP metadata parsing and UV-mapping correction.
- Testing on 5+ real devices (including iPhone SE, Pixel 6, Samsung Galaxy).
- Code documentation, integration guide, repository access.
- 30 days free support after delivery.
- Knowledge transfer session for your team.
Timeline and cost
Basic viewer (iOS, SceneKit, gyroscope + touch): 1–1.5 weeks. Full implementation with tiled loading, XMP metadata, iOS + Android: 3–4 weeks. Starting at $1,500 for basic, $3,000–$5,000 for full. Our team: 7+ years of mobile development, 30+ 360° projects, 5+ years on the market. Request a consultation — we will prepare a commercial proposal within 1 business day.
Step-by-step integration of the 360° viewer
- Import the SceneKit framework (iOS) or connect Filament (Android).
- Create a 3D sphere with radius 10 and inverted normals (scale = -1).
- Set up the camera at the center of the scene with a field of view of 90°.
- Load the texture image using progressive loading (preview → full).
- Implement control: gyroscope (CMMotionManager) and touch (UIPanGestureRecognizer with inertia).
- Add XMP metadata parsing to correct UV-mapping for incomplete panoramas.
- Test on real devices with different screen resolutions and RAM sizes.







