How can I add a Scenekit Scene to a SwiftUI view?
I tried the following Hello World, using the standard Ship Scene example...
import SwiftUI
import SceneKit
struct SwiftUIView : View {
var body: some View {
ship()
Text("hello World")
}
But it didn't work:
Rawbee's answer does the job if you are creating a SwiftUIView inside a Game project (the default game project that Xcode will create for you)
But if you are in a Single View App project, you can create the same SceneView like this:
First
drag the art.scnassets folder (which contains 2 files: ship.scn and texture.png) from your game project to your Single View App project.
Second
In your Single View App project create a new SwiftUI file — I called it: ScenekitView. This is a structure which conforms to the UIViewRepresentable protocol
Third
Copy and paste the code below to this file and turn on live preview mode
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
let scene = SCNScene(named: "art.scnassets/ship.scn")!
func makeUIView(context: Context) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// retrieve the ship node
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
// animate the 3d object
ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
// retrieve the SCNView
let scnView = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.black
}
}
#if DEBUG
struct ScenekitView_Previews : PreviewProvider {
static var previews: some View {
ScenekitView()
}
}
#endif
I'm not a pro but this code worked for me (Xcode 11 beta 4)
You don't need use UIViewRepresentable anymore. Here's an update code for SwiftUI
import SwiftUI
import SceneKit
struct ContentView: View {
var scene: SCNScene? {
SCNScene(named: "Models.scnassets/Avatar.scn")
}
var cameraNode: SCNNode? {
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 2)
return cameraNode
}
var body: some View {
SceneView(
scene: scene,
pointOfView: cameraNode,
options: [
.allowsCameraControl,
.autoenablesDefaultLighting,
.temporalAntialiasingEnabled
]
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In order for this to work, your SwiftUI View must conform to UIViewRepresentable. There's more info about that in Apple's tutorial: Interfacing with UIKit.
import SwiftUI
struct SwiftUIView : UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
return UIStoryboard(name: "Main", bundle: Bundle.main).instantiateInitialViewController()!.view
}
func updateUIView(_ view: UIView, context: Context) {
}
}
#if DEBUG
struct SwiftUIView_Previews : PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
#endif
Note that you'll have to turn on live preview to see it working.
Related
I have been trying to do this for some time, but have not been able to achieve my goal. I would like to show images stored at Firebase in an Image Slider or carousel in SwiftUI. I am able to fetch and show a single image (that's a different thing). However, unable to load images from firebase to slider. The first part is to get url of images which I am not getting how to do it.
Update
This loads images but cant auto scroll or auto iterate, images load one after another than stops:
func downImage(){
let imageRef = Storage.storage()
let loc = imageRef.reference().child("Images")
loc.listAll{(result,error) in
if error != nil{
print("Ooops : ", error)
return
}
for item in result!.items{
let count = result?.items.count ?? 5
//provides actual count ie 3
print("number of Images: ", self.count)
print("images url : " , item)
item.getData(maxSize: 15 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
guard let data = data else { return }
DispatchQueue.main.async {
self.count = count
// self.actualImage.append(self.image)
self.image = UIImage(data: data as Data)!
}}}}}}
In Slider, images are shown but stops after loading third image:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
/* private let timer = Timer.publish(every: 3, on: .main, in: .common)
.autoconnect() */
var body: some View {
TabView(){
/* ForEach(imageList.actualImage, id: \.self) { item in
if foreach loop is used with actualImage appending Images no
image is shown or Index out of range error */
Image(uiImage: imageList.image)
.resizable()
.scaledToFill()
}
.tabViewStyle(PageTabViewStyle())
.animation(.easeInOut(duration: 3))
.onAppear(){
imageList.downImage()
}
}
If I apply the timer (commented out) with other settings it keeps updating selected image 0, selected image 1 but image never changes. Need images to auto scroll in this slider.
As I understand your code, you are downloading the string url from Firebase into
#Published var imageURL: [String] = []. This does not download the image data, just the url string.
In your loadImageURL, try this, to store all the url strings into your imageURL array:
DispatchQueue.main.async {
if let linq = link {
self.imageURL.append(linq)
print("url link : ", self.imageURL)
}
In your ImageSlider, use something like this, instead of your Image(item):
AsyncImage(url: URL(string: item))
also, change #ObservedObject var imageList = FireImageModel() to #StateObject var imageList = FireImageModel()
and, do not use !, anywhere in your code, it is a recipe for disaster.
EDIT-1:
Given your new code, try this simple example code, to try to find where your problem is:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
var body: some View {
ScrollView {
ForEach(imageList.actualImage, id: \.self) { item in
Image(uiImage: item)
.resizable()
.frame(width: 222, height: 222)
}
.onAppear {
imageList.downImage()
}
}
}
}
Also, do not use ! anywhere in your code.
If that works, try this:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
var body: some View {
VStack {
if imageList.actualImage.count > 0 {
TabView {
ForEach(imageList.actualImage, id: \.self) { item in
Image(uiImage: item)
.resizable()
.scaledToFill()
}
}
.tabViewStyle(PageTabViewStyle())
} else {
ProgressView()
}
}
.onAppear {
imageList.downImage()
}
}
}
You may also want to clean your FireImageModel, such as:
class FireImageModel: ObservableObject {
#Published var actualImage: [UIImage] = []
func downImage() {
let imageRef = Storage.storage()
let loc = imageRef.reference().child("Images")
loc.listAll{(result,error) in
if error != nil{
print("Ooops : ", error)
return
}
if let images = result {
print("number of Images: ", images.items.count)
for item in images.items {
print("images url : " , item)
item.getData(maxSize: 15 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
DispatchQueue.main.async {
if let theData = data, let image = UIImage(data: theData) {
self.actualImage.append(image)
}
}
}
}
}
}
}
}
I was able to display images in slider final prob was auto iteration. ForEach was throwing index out of range error(known prob) when directly using an array having image data so ans by #workingdogUkraine helped to resolve the error by putting check before entering the foreach. Auto iteration is done. Look into updated clean FireImageModel model by WorkingDog, below is the slider struct:
struct ImageSlider: View {
#StateObject var imageList = FireImageModel()
#State private var currentIndex = 0
#State var zoomed = false
private let timer = Timer.publish(every: 3, on: .main, in: .common)
.autoconnect()
var body: some View {
// 2
VStack {
if imageList.actualImage.count > 0 {
GeometryReader{ proxy in
TabView(selection: $currentIndex){
ForEach(imageList.actualImage.indices, id: \.self) {
item in
let sot = imageList.actualImage[item]
Image(uiImage: sot)
.resizable()
.scaledToFill()
.onTapGesture {
self.zoomed.toggle()
}
.scaleEffect(self.zoomed ? 1.73 : 1)
}
.onReceive(timer, perform: {_ in
withAnimation(.spring(response: 1,
dampingFraction: 1, blendDuration: 1)){
currentIndex = currentIndex <
imageList.actualImage.count ? currentIndex + 1 : 0
}
})
}
.tabViewStyle(PageTabViewStyle())
.frame(width: proxy.size.width, height: proxy.size.height/3)
}
}else {
ProgressView()
}
}.background(.ultraThinMaterial)
.onAppear {
imageList.downImage()
}
}}
Images appear with non noticeable animation :), will like to add some cool animation, after every 3 seconds image changes
I have a scroll view that has an image view embedded into it. The user clicks the "Take Photo" button, takes a picture, and then those photos are stored in an array and then displayed on the scrollable imageView. My issue is that after the didFinishSelectingMedia is called and the camera closes, the imageView always shows the first image in the array. I want the imageView to show the most recent image added to the array and scroll backward through the images until the user comes to the first image in the array. How do I make this happen?
Btw: Idk if this is even plausible, but I tried reversing the for loop and that didn't work.
I'm a new programmer, so please explain your answers.
Here's my code:
import UIKit
class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#IBOutlet weak var scrollImageView: UIScrollView!
var imageTaken: [UIImage] = []
var imagePicker = UIImagePickerController()
override func viewDidLoad() {
super.viewDidLoad()
scrollImageView.frame = scrollImageView.frame
}
#IBAction func takePhotoButton(_ sender: Any) {
imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .camera
present(imagePicker, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
imageTaken.append((info[.originalImage] as? UIImage)!)
imagePicker.dismiss(animated: true, completion: nil)
for i in 0..<imageTaken.count {
let imageView = UIImageView()
imageView.image = imageTaken[i]
imageView.contentMode = .scaleAspectFit
let xPosition = self.scrollImageView.frame.width * CGFloat(i)
imageView.frame = CGRect(x: xPosition, y: 0, width: self.scrollImageView.frame.width, height: self.scrollImageView.frame.height)
scrollImageView.contentSize.width = scrollImageView.frame.width * CGFloat(i + 1)
scrollImageView.addSubview(imageView)
}
}
}
Two options I guess you can take which ever one is seems easier.
OOP — Create an array of objects example:
struct TakePhotos {
var image : UIImage
var timestamp : Date
}
Then sort the array by the timestamp.
Use PHAsset
import Photos
Using PHAsset you should be able to retrieve the images taken from the user's album. Set a count when the user takes a new image.
Then you can run a query and sort the images in reverse order, they will already have the timestamp.
let photoOptions = PHFetchOptions()
photoOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
This is what I did in my application.
// FIXME: - Query only happens once this needs to be refactored so it happens when the album is updated on the device.
// Query only happens one time.
if imageArray.isEmpty {
let photoOptions = PHFetchOptions()
photoOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let allPhotos = PHAsset.fetchAssets(with: photoOptions)
let fetchOptions = PHImageRequestOptions()
fetchOptions.deliveryMode = .highQualityFormat
fetchOptions.resizeMode = .exact
fetchOptions.normalizedCropRect = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width)
// Must be a synchronous call otherwise the view loads before images are retrieved (result is an empty view).
fetchOptions.isSynchronous = true
let imageSize : CGSize = CGSize(width: view.frame.width, height: view.frame.height)
//Retrieves Images using assets.
allPhotos.enumerateObjects { (assets, index, pointer) in
DispatchQueue.global(qos: .background).sync {
PHImageManager.default().requestImage(for: assets, targetSize: imageSize, contentMode: .aspectFit, options: fetchOptions) { (image, hashDictionary) in
guard let image = image else {return}
self.imageArray.append(image)
self.albumImages.append(ImageAlbum(images: image))
}
}
}
selectedImage.image = imageArray.first
selectedImage.contentMode = .scaleAspectFill
}
I want to create a scrollable list of 3D models(here: default ship model of Xcode which you can find in a Game project of Xcode).
For that I've created 3 View structures:
one for viewing the 3D model (called ScenekitView)
one for a CardView (called MyCardView)
one for the scrollable list of 3D models(called MyCardsListView)
Here are the Structures:
ScenekitView
(btw I added the art.scnassets folder of Xcode's default Game project to my Single View App project)
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
#Binding var yRotation : Float
let scene = SCNScene(named: "art.scnassets/ship.scn")!
func makeUIView(context: Context) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// retrieve the ship node
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
// animate the 3d object
ship.eulerAngles = SCNVector3(x: 0, y: self.yRotation * Float((Double.pi)/180.0) , z: 0)
// retrieve the SCNView
let scnView = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.black
}
}
#if DEBUG
struct ScenekitView_Previews : PreviewProvider {
static var previews: some View {
ScenekitView(yRotation: .constant(0))
}
}
#endif
MyCardView
import SwiftUI
struct MyCardView : View {
#Binding var yRotation: Float
var body: some View {
VStack(spacing: 0) {
ZStack {
Rectangle()
.foregroundColor(.black)
.opacity(0.8)
.frame(width:250, height: 70)
Text( "Ship \( self.yRotation )" )
.foregroundColor(.white)
.font(.title)
.fontWeight(.bold)
}
ScenekitView(yRotation: self.$yRotation )
.frame(width: 250, height: 250)
}
.cornerRadius(35)
.frame(width: 250,height: 320)
}
}
#if DEBUG
struct MyCardView_Previews : PreviewProvider {
static var previews: some View {
MyCardView(yRotation: .constant(90))
}
}
#endif
MyCardsListView
import SwiftUI
struct MyCardsListView: View {
#State var yRotation: Float = 0
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(0...2) { number in
GeometryReader { geometry in
MyCardView(yRotation: self.$yRotation )
.rotation3DEffect(Angle(degrees: Double(geometry.frame(in: .global).minX) / -20), axis: (x: 0, y: 10, z: 0))
}
.padding()
.frame(width: 250, height: 350)
}
}
}
}
}
#if DEBUG
struct Product3DView_Previews: PreviewProvider {
static var previews: some View {
MyCardsListView()
}
}
#endif
In MyCardsListView I'm getting each Card's minimum X position using GeometryReader
My goal is:
while I'm scrolling the Cards in MyCardsListView, every ship model should rotate with respect to its Card's minimum X position (all of them shouldn't rotate with same rotation angle at the same time)
(I know I might made lots of mistakes in using Bindings, States and etc, Sorry about that)
There are many problems in your code:
You never assign a value to yRotation.
You only have one yRotation variable in MyCardsListView, but because you want to have different rotation values, depending on the view's position, you need one yRotation variable for each view.
The following line:
ship.eulerAngles = SCNVector3(x: 0, y: self.yRotation * Float((Double.pi)/180.0) , z: 0)
is inside the makeUIView() method, but that method is only executed once (when your view is created). You want the value to update continously, so you should assign the value in updateUIView() instead.
You should definitely watch WWDC sessions 226: Data Flow in SwiftUI and 231: Integrating SwiftUI.
I am trying to create a custom Camera class to reuse across all levels in SceneKit.
I have defined the cameraNode
Set the sceneView to use the cameraNode pointOfView
Define class:
class GameCamera: SCNCamera {
let cameraNodeHorizontal: SCNNode!
override init() {
cameraNodeHorizontal = SCNScene(named: "/GameAssets.scnassets/Camera.scn")?.rootNode.childNode(withName: "GameCamera", recursively: true)
super.init()
}
func setup(scnView: SCNView) {
scnView.scene?.rootNode.addChildNode(cameraNodeHorizontal)
scnView.pointOfView = cameraNodeHorizontal
}
}
Inside ViewController:
private var camera = GameCamera()
private func loadCamera() {
camera.setup(scnView: self.scnView)
}
The scene renders from a default pointOfView other than the one I defined.
Wondering if anyone can help?
I don't use .scn - but just a basic class, something like this:
var cameraEye = SCNNode()
var cameraFocus = SCNNode()
init()
{
cameraEye.name = "Camera Eye"
cameraFocus.name = "Camera Focus"
cameraFocus.isHidden = true
cameraFocus.position = SCNVector3(x: 0, y: 0, z: 0)
cameraEye.camera = SCNCamera()
cameraEye.constraints = []
cameraEye.position = SCNVector3(x: 0, y: 15, z: 0.1)
let vConstraint = SCNLookAtConstraint(target: cameraFocus)
vConstraint.isGimbalLockEnabled = true
cameraEye.constraints = [vConstraint]
}
// Add camera and focus nodes to your Scenekit nodes
gameNodes.addChildNode(camera.cameraEye)
gameNodes.addChildNode(camera.cameraFocus)
Suppose I have a collada file which has a box in it and I import the dae file into the scene. Now after importing in the scene I know the dae object is a box. How can I get the dimensions of the box in scenekit after adding it to the scene
If I import the node as a SCNBox i get errors saying that SNCBox is not a subtype of SCNNode.
floorNode = scene.rootNode.childNodeWithName("floor", recursively: false) as SCNBox
floorNode?.physicsBody = SCNPhysicsBody.staticBody()
floorNode?.physicsBody?.categoryBitMask = 2
let floorGeo: SCNBox = floorNode.geometry! as SCNBox
How do I get the dimensions if SCNNode is the only way to import nodes?
SCNBox is just helper subclass of SCNGeometry for box geometry creation. When you import Collada into a scene, you get scene graph of SCNNodes with SCNGeometries, not SCNBox'es or SCNCones etc doesn't matter how they look. If you want to get dimensions of any node you should use SCNBoundingVolume protocol, which is implemented by both SCNNode and SCNGeometry classes:
func getBoundingBoxMin(_ min: UnsafeMutablePointer,
max max: UnsafeMutablePointer) -> Bool
With this method you will get bounding box corners.
For box-shaped geometry, dimensions will match bounding box.
Example:
var v1 = SCNVector3(x:0, y:0, z:0)
var v2 = SCNVector3(x:0, y:0, z:0)
node.getBoundingBoxMin(&v1, max:&v2)
Where node is node you want to get bounding box of.
Results will be in v1 and v2.
Swift 3
Using Swift 3 you can simply use node.boundingBox.min and node.boundingBox.max respectively.
Example Swift code on how to use boundingBox:
var min = shipNode.boundingBox.min
var max = shipNode.boundingBox.max
let w = CGFloat(max.x - min.x)
let h = CGFloat(max.y - min.y)
let l = CGFloat(max.z - min.z)
let boxShape = SCNBox (width: w , height: h , length: l, chamferRadius: 0.0)
let shape = SCNPhysicsShape(geometry: boxShape, options: nil)
shipNode.physicsBody!.physicsShape = SCNPhysicsShape(geometry: boxShape, options: nil)
shipNode.physicsBody = SCNPhysicsBody.dynamic()
Swift 5.4.
import ARKit
extension SCNNode {
var height: CGFloat { CGFloat(self.boundingBox.max.y - self.boundingBox.min.y) }
var width: CGFloat { CGFloat(self.boundingBox.max.x - self.boundingBox.min.x) }
var length: CGFloat { CGFloat(self.boundingBox.max.z - self.boundingBox.min.z) }
var halfCGHeight: CGFloat { height / 2.0 }
var halfHeight: Float { Float(height / 2.0) }
var halfScaledHeight: Float { halfHeight * self.scale.y }
}
// Usage:
let node = SCNNode()
node.height
node.width
node.lenght