Lab 068 – Adding an axis to custom layouts
Building on the custom layouts from Lab 067, we can add some 3D models and use SwiftUI modifiers to control rotation and position.
Overview
Yesterday I spent some time Exploring custom Layouts in SwiftUI. This lab builds on that work. I replaced the 2D circle view with a Model3D view. The model is a simple sphere with front face culling set on the material. That gives the impression of an inside out sphere. Then I used the new spatialOverlay feature to place a text view inside the sphere. This is sort of a lightweight alternative to RealityView with attachments.
fileprivate struct ModelViewEmoji: View {
@State var name: String = ""
let emoji: String
let bundle: Bundle
var body: some View {
Model3D(named: name, bundle: bundle)
{ phase in
if let model = phase.model {
model
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60)
.spatialOverlay(alignment: .center) {
Text(emoji)
.font(.system(size: 30))
}
} else if phase.error != nil {
Text(emoji)
} else {
ProgressView()
}
}
}
}I rotated the outer view 90 degrees on the X axis. To keep the content oriented correctly I applied the inverse rotation to the views inside this layout. For the HoneycombLayout, I applied that tot the entire layout. For the RadialLayout I applied it to the layout children, so each one will face the front of the view.
VStack {
if useHoneycomb {
HoneycombLayout {
ForEach(0..<nodes, id: \.self) { index in
ModelViewEmoji(name: "UISphere01", emoji: emoji[index], bundle: realityKitContentBundle)
}
}
.rotation3DLayout(Rotation3D(angle: .degrees(-90), axis: .x))
} else {
RadialLayout {
ForEach(0..<nodes, id: \.self) { index in
ModelViewEmoji(name: "UISphere01", emoji: emoji[index], bundle: realityKitContentBundle)
.rotation3DLayout(Rotation3D(angle: .degrees(-90), axis: .x))
.offset(z: 30 * CGFloat(index))
}
}
}
}
.rotation3DLayout(Rotation3D(angle: .degrees(90), axis: .x))
You can see I also applied an offset to the children in the RadialLayout.
Video Demo
Lab Code
For the custom layouts, see Lab 067.
struct Lab068: View {
@State var nodes: Int = 3
@State var previousNodes: Int = 3
@State var useHoneycomb: Bool = false
var emoji: [String] = ["🌸", "🐸", "❤️", "🔥", "💻", "🐶", "🥸", "📱", "🎉", "🚀", "🤔", "🤓", "🧲", "💰", "🤩", "🍪", "🦉", "💡", "😎"]
var body: some View {
VStack {
if useHoneycomb {
HoneycombLayout {
ForEach(0..<nodes, id: \.self) { index in
ModelViewEmoji(name: "UISphere01", emoji: emoji[index], bundle: realityKitContentBundle)
}
}
.rotation3DLayout(Rotation3D(angle: .degrees(-90), axis: .x))
} else {
RadialLayout {
ForEach(0..<nodes, id: \.self) { index in
ModelViewEmoji(name: "UISphere01", emoji: emoji[index], bundle: realityKitContentBundle)
.rotation3DLayout(Rotation3D(angle: .degrees(-90), axis: .x))
.offset(z: 30 * CGFloat(index))
}
}
}
}
.rotation3DLayout(Rotation3D(angle: .degrees(90), axis: .x))
.offset(y: 250)
.toolbar {
ToolbarItem(placement: .bottomOrnament, content: {
VStack(spacing: 16) {
// Layout toggle
HStack(spacing: 16) {
Button(action: {
withAnimation {
useHoneycomb = false
}
}, label: {
Text("Radial")
.padding()
.foregroundColor(useHoneycomb ? .gray : .white)
})
Button(action: {
withAnimation {
useHoneycomb = true
}
}, label: {
Text("Honeycomb")
.padding()
.foregroundColor(useHoneycomb ? .white : .gray)
})
}
// Node controls
HStack(spacing: 24) {
Button(action: {
withAnimation {
previousNodes = nodes
nodes -= 1
}
}, label: {
Image(systemName: "minus.circle.fill")
})
.disabled(nodes <= 3)
Text("\(nodes)")
.frame(width:60)
.contentTransition(.numericText(countsDown: nodes < previousNodes))
Button(action: {
withAnimation {
previousNodes = nodes
nodes += 1
}
}, label: {
Image(systemName: "plus.circle.fill")
})
.disabled(nodes >= 19)
}
}
})
}
}
}Download the Xcode project with this and many more labs from Step Into Vision.


Follow Step Into Vision