SwiftUI + Scenekit + Combine + GeometryReader ==> Scrollable List of 3D Models + Rotation of 3D Models By Scrolling the List - scenekit

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.

Related

How can I take a frame and divide its width by 2?

I am trying to set up a view that displays out a mathematical equation, with exponents being adjusted for size. I ran into an issue where after applying a scaleEffect to the item in the exponent, the frame of that view remained the same size, leaving empty space where there shouldn't be any.
My goal is very simple: I just need to take the proposed frame width and divide it by 2 in order to match the scaleEffect() applied to its contents. Anything like this works:
.frame(width: originalWidth / 2)
I quickly discovered that there is unexplainably not a single way to just read out the proposed, standard, ideal frame that would be applied to a view in absence of .frame().
That is, aside from Geometry Reader, which is a nightmare to deal with from how it resizes everything, how the data is only accessible within itself, and the fact code inside runs after a .frame() modifier is already applied to a view.
My starting point:
struct StaticEquationView: View {
//Initialization of the master controller
#StateObject var data = DataController()
#State var frameWidth: CGFloat = .zero
#State var frameApplied: Bool = false
var body: some View {
HStack(spacing: 0) {
ForEach($data.blockArray) { $block in
DisplayBlockView(data: data, block: $block)
}
}
}
}
struct DisplayBlockView: View {
#ObservedObject var data: DataController
#Binding var block: Block
var body: some View {
Text(block.label)
HStack(spacing: 0) {
ArrayView(data: data, array: $block.exp)
}
//HERE
.scaleEffect(0.5)
.offset(CGSize(width: 0, height: -10))
}
}
struct ArrayView: View {
#ObservedObject var data: DataController
#Binding var array: [Block]
var body: some View {
HStack {
ForEach($array) { $block in
DisplayBlockView(data: data, block: $block)
}
}
}
}
And here is my unsuccessful attempt at shrinking the frame. Somehow this code would uncontrollably expand the height of the view and shrink the subviews of each individual DisplayBlockView inside ArrayView, all while still keeping some spacing between the edges and the contents of ArrayView.
struct DisplayBlockView: View {
#ObservedObject var data: DataController
#Binding var block: Block
#State var frameWidth: CGFloat = .zero
#State var frameApplied: Bool = false
var body: some View {
Text(block.label)
HStack(spacing: 0) {
ArrayView(data: data, array: $block.exp)
.overlay(GeometryReader { geo in
Text("")
.onAppear() {
frameWidth = geo.size.width * 0.5
frameApplied = true
}
})
}
.frame(width: frameApplied ? frameWidth : nil, height: nil)
.scaleEffect(0.5)
.offset(CGSize(width: 0, height: -10))
.offset(block.cOffset)
.border(Color.gray)
}
}

Assigning NavigationLink to Images within Array in Swift UI

I have an array of images that I am passing through a LazyVgrid.
I am wondering how I assign a NavigationLink to the Image names stored within the array, clicking icon1 navigates to page1, ect for all 8.
------ Swift UI -------
import SwiftUI
struct Grid: View {
var images: [String] = ["icon1", "icon2", "icon3", "icon4",
"icon5", "icon6", "icon7","icon8"]
var columnGrid: [GridItem] = [GridItem(.flexible(), spacing: 25), GridItem(.flexible(), spacing: 25)]
var body: some View {
LazyVGrid(columns: columnGrid, spacing: 50) {
ForEach(images, id:\.self) { image in
Image(image)
.resizable()
.scaledToFill()
.frame(width: (UIScreen.main.bounds.width / 2.75 ) - 1,
height: (UIScreen.main.bounds.width / 2.75 ) - 1)
.clipped()
.cornerRadius(25)
}
}
}
}
For occasions like this I would prefer an enum that holds all related information. You also could do this with a struct the reasoning behind this stays the same.
enum ImageEnum: String, CaseIterable{ //Please find a better name ;)
case image1, image2
var imageName: String{ // get the assetname of the image
switch self{
case .image1:
return "test"
case .image2:
return "test2"
}
}
#ViewBuilder
var detailView: some View{ // create the view here, if you need to add
switch self{ // paramaters use a function or associated
case .image1: // values for your enum cases
TestView1()
case .image2:
TestView2()
}
}
}
struct TestView1: View{
var body: some View{
Text("test1")
}
}
struct TestView2: View{
var body: some View{
Text("test2")
}
}
And your Grid View:
struct Grid: View {
var columnGrid: [GridItem] = [GridItem(.flexible(), spacing: 25), GridItem(.flexible(), spacing: 25)]
var body: some View {
NavigationView{ // Add the NavigationView
LazyVGrid(columns: columnGrid, spacing: 50) {
ForEach(ImageEnum.allCases, id:\.self) { imageEnum in // Itterate over all enum cases
NavigationLink(destination: imageEnum.detailView){ // get detailview here
Image(imageEnum.imageName) // get image assset name here
.resizable()
.scaledToFill()
.clipped()
.cornerRadius(25)
}
}
}
}
}
}
Result:

SwiftUI - how to add a Scenekit Scene

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.

SceneKit and using a custom camera class

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)

Splitting an image into a grid in Swift

I am attempting to split an image into 10'000 smaller ones, by dividing the image into a grid of 100x100. When I run the code, I receive an Error_Bad_Instruction.
import Cocoa
import CoreImage
public class Image {
public var image: CGImage?
public func divide() -> [CGImage] {
var tiles: [CGImage] = []
for x in 0...100 {
for y in 0...100 {
let tile = image?.cropping(to: CGRect(x: (image?.width)!/x, y: (image?.height)!/y, width: (image?.width)! / 100, height: (image?.height)! / 100 ))
tiles.append(tile!)
}
}
return tiles
}
public init(image: CGImage) {
self.image = image
}
}
var img = Image(image: (NSImage(named: "aus-regions.jpg")?.cgImage(forProposedRect: nil, context: nil, hints: nil))!)
var a = img.divide()
There are some problems with the code that you posted:
forced wrapping the image
loops go including 100 - you just need 99. Or as a comment suggests: for x in 0..<100
you divide by 0 - in the first step
your algorithm, is not forming a grid.
I've updated to obtain a grid, using an image that I had on my computer, and with widths and heights just of 10, because it using 100 was too long.
import Cocoa
import CoreImage
let times = 10
public class Image {
public var image: CGImage?
public func divide() -> [CGImage] {
var tiles: [CGImage] = []
for x in 0..<times {
for y in 0..<times {
let tile = image?.cropping(to: CGRect(x: x * (image?.width)!/times, y: y * (image?.height)!/times, width: (image?.width)! / times, height: (image?.height)! / times ))
tiles.append(tile!)
}
}
return tiles
}
public init(image: CGImage) {
self.image = image
}
}
if let image = NSImage(named: "bg.jpg")?.cgImage(forProposedRect: nil, context: nil, hints: nil) {
var img = Image(image: image)
var a = img.divide()
}

Resources