How to add other views to UIViewRepresentable? - ios

As a beginner in Swift, I wanted to create a painting app where we can choose a photo to frame into a 3d frame.
I'm using SwiftUI and here is my code for the 3d scene.
import SwiftUI
import SceneKit
struct SceneView: UIViewRepresentable {
let scene = SCNScene()
func makeUIView(context: Context) -> SCNView {
// create a box
scene.rootNode.addChildNode(createBox())
scene.rootNode.addChildNode(createFrameY(ypos: 30))
scene.rootNode.addChildNode(createFrameY(ypos: -30))
scene.rootNode.addChildNode(createFrameX(xpos: 20))
scene.rootNode.addChildNode(createFrameX(xpos: -20))
// code for creating the camera and setting up lights is omitted for simplicity
// …
// retrieve the SCNView
let scnView = SCNView()
scnView.scene = scene
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// configure the view
scnView.backgroundColor = UIColor.gray
// show statistics such as fps and timing information
scnView.showsStatistics = true
}
func createBox() -> SCNNode {
let boxGeometry = SCNBox(width: 1, height: 60, length: 40, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
let material = SCNMaterial()
material.locksAmbientWithDiffuse = true;
material.isDoubleSided = false
material.diffuse.contents = "Mona"
boxGeometry.materials = [material]
box.localRotate(by: SCNQuaternion(x: 0, y: 0.7071, z: 0, w: 0.7071))
return box
}
func createFrameY(ypos: Float) -> SCNNode {
let boxGeometry = SCNBox(width: 2, height: 2, length: 42, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
box.position.y = ypos
boxGeometry.firstMaterial?.diffuse.contents = UIColor.black
box.localRotate(by: SCNQuaternion(x: 0, y: 0.7071, z: 0, w: 0.7071))
return box
}
func createFrameX(xpos: Float) -> SCNNode {
let boxGeometry = SCNBox(width: 2, height: 62, length: 2, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
box.position.x = xpos
boxGeometry.firstMaterial?.diffuse.contents = UIColor.black
return box
}
func removeBox() {
// check if box exists and remove it from the scene
guard let box = scene.rootNode.childNode(withName: "box", recursively: true) else { return }
box.removeFromParentNode()
}
func addBox() {
// check if box is already present, no need to add one
if scene.rootNode.childNode(withName: "box", recursively: true) != nil {
return
}
scene.rootNode.addChildNode(createBox())
}
}
#if DEBUG
struct ScenekitView_Previews : PreviewProvider {
static var previews: some View {
SceneView()
.previewDevice("iPhone 13 Pro")
}
}
#endif
As I understood, when working with UIViewRepresentable, you cannot have other elements to your screen. So I tried to use this code to have both buttons and the Scene View
import SwiftUI
import SceneKit
struct ContentVie: View {
var body: some View {
Home()
}
}
struct ContentVie_Previews: PreviewProvider {
static var previews: some View {
ContentVie()
}
}
// Home View...
struct Home : View {
#State var models = [
Model(id: 0, name: "Earth", modelName: "Earth.usdz", details: "Earth is the third planet from the Sun and the only astronomical object known to harbor life. According to radiometric dating estimation and other evidence, Earth formed over 4.5 billion years ago. Earth's gravity interacts with other objects in space, especially the Sun and the Moon, which is Earth's only natural satellite. Earth orbits around the Sun in 365.256 solar days.")
]
#State var index = 0
var scene = SCNScene(named: "Earth.usdz")
var body: some View{
VStack{
// Going to use SceneKit Scene View....
// default is first object ie: Earth...
// Scene View Has a default Camera View...
// if you nedd custom means add there...
SceneView(scene: scene, options: [.autoenablesDefaultLighting,.allowsCameraControl])
// for user action...
// setting custom frame...
.frame(width: UIScreen.main.bounds.width , height: UIScreen.main.bounds.height / 2)
ZStack{
// Forward and backward buttons...
HStack{
Button(action: {
withAnimation{
if index > 0{
index -= 1
}
}
}, label: {
Image(systemName: "chevron.left")
.font(.system(size: 35, weight: .bold))
.opacity(index == 0 ? 0.3 : 1)
})
.disabled(index == 0 ? true : false)
Spacer(minLength: 0)
Button(action: {
withAnimation{
if index < models.count{
index += 1
}
}
}, label: {
Image(systemName: "chevron.right")
.font(.system(size: 35, weight: .bold))
// disabling button when no other data ....
.opacity(index == models.count - 1 ? 0.3 : 1)
})
.disabled(index == models.count - 1 ? true : false)
}
Text(models[index].name)
.font(.system(size: 45, weight: .bold))
}
.foregroundColor(.black)
.padding(.horizontal)
.padding(.vertical,30)
// Details....
VStack(alignment: .leading, spacing: 15, content: {
Text("About")
.font(.title2)
.fontWeight(.bold)
Text(models[index].details)
})
.padding(.horizontal)
Spacer(minLength: 0)
}
}
}
// Data Model...
struct Model : Identifiable {
var id : Int
var name : String
var modelName : String
var details : String
}
But I cannot add SCNNodes to the scene. Do you know how I could combine both to make it work?

Related

How to make the line smoother with Path in SwiftUI?

When the user touches the screen, it will record the gesture as CGPoint and then display them with Path. But now the line is not smooth at the turning point. What should I do?
This is my code:
struct LineView: View {
#State var removeLine = false
#State var singleLineData = [CGPoint]()
var body: some View {
ZStack {
Rectangle()
.cornerRadius(20)
.opacity(0.1)
.shadow(color: .gray, radius: 4, x: 0, y: 2)
Path { path in
path.addLines(singleLineData)
}
.stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
}
.gesture(
DragGesture()
.onChanged { state in
if removeLine {
singleLineData.removeAll()
removeLine = false
}
singleLineData.append(state.location)
}
.onEnded { _ in
removeLine = true
}
)
.frame(width: 370, height: 500)
}
}
struct LineView_Previews: PreviewProvider {
static var previews: some View {
LineView()
}
}
I'm back and I know a solution to make line smoother.
First I created a class to return Path 「PS: I learned this from Mrs. Karin Prater's youtube course("How to make a drawing app with SwiftUI 3"), Shoutout to her」
class DrawingEngine {
func createPath(for points: [CGPoint]) -> Path {
var path = Path()
if let firstPoint = points.first {
path.move(to: firstPoint)
}
for index in 1..<points.count {
let mid = calculateMidPoint(points[index-1], points[index])
path.addQuadCurve(to: mid, control: points[index - 1])
}
if let last = points.last {
path.addLine(to: last)
}
return path
}
func calculateMidPoint(_ p1: CGPoint, _ p2: CGPoint) -> CGPoint {
let newMidPoint = CGPoint(x: (p1.x+p2.x)/2, y: (p1.y+p2.y)/2)
return newMidPoint
}
}
Then I only record new state points whose linear distance from the previous point exceeds a certain value(10 or 20...)
.gesture(DragGesture()
.onChanged { state in
if removeLine {
removeLine = false
singleLineData = [CGPoint]()
}
var exceedsMinimumDistance: Bool {
return sqrt(pow((singleLineData[singleLineData.count-1].x - state.location.x), 2) + pow((singleLineData[singleLineData.count-1].y - state.location.y), 2)) > 20
}
if singleLineData.count == 0 {
singleLineData.append(state.location)
} else if exceedsMinimumDistance {
singleLineData.append(state.location)
}
}
.onEnded { _ in
removeLine = true
})
This is full code of LineView:
struct LineView: View {
#State var removeLine = false
#State var singleLineData = [CGPoint]()
let engine = DrawingEngine()
let minimumDistance: CGFloat = 20
var body: some View {
ZStack {
Rectangle()
.cornerRadius(20)
.opacity(0.1)
.shadow(color: .gray, radius: 4, x: 0, y: 2)
if singleLineData.count != 0 {
Path { path in
path = engine.createPath(for: singleLineData)
}
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
}
}
.gesture(DragGesture()
.onChanged { state in
if removeLine {
removeLine = false
singleLineData = [CGPoint]()
}
var exceedsMinimumDistance: Bool {
return sqrt(pow((singleLineData[singleLineData.count-1].x - state.location.x), 2) + pow((singleLineData[singleLineData.count-1].y - state.location.y), 2)) > minimumDistance
}
if singleLineData.count == 0 {
singleLineData.append(state.location)
} else if exceedsMinimumDistance {
singleLineData.append(state.location)
}
}
.onEnded { _ in
removeLine = true
})
}
}
My English is not good, hope this solution could help you ;)

Rotating a gradient based off device pitch, roll, and yaw - creating a hologram / security sticker effect

I'm trying to implement a little fun addition into an app I'm working on, and can't seem to figure out how to get the animation to work completely / correctly.
At the moment I have something that looks like this: https://i.imgur.com/95O1Wul.mp4
What I'm trying to achieve
I'm trying to create a hologram like you would see on some shiny game cards or in some bank notes or on those VOID stickers.
I'm using the x position of the device to determine whether to shift the start and end point of the gradient. I'm trying to figure out in my head how to use these values to make the gradient's start and end points shift based on the values. And if I incorporated the other axes how it would move around if the device is rotated.
As for code, at the moment I have:
View Model
final class ContentViewModel: ObservableObject {
#Published var gyroRotation = CMRotationRate()
private let manager = CMMotionManager()
private var timer: Timer?
func startGyroscope() {
if(manager.isGyroAvailable) {
manager.gyroUpdateInterval = 1.0
manager.startGyroUpdates()
self.timer = Timer(fire: Date(), interval: (1.0/60.0), repeats: true, block: { timer in
if let data = self.manager.gyroData {
self.gyroRotation = data.rotationRate
}
})
RunLoop.current.add(self.timer!, forMode: .default)
}
}
func stopGyroscope() {
if(self.timer != nil) {
self.timer?.invalidate()
self.timer = nil
self.manager.stopGyroUpdates()
}
}
}
View
struct ContentView: View {
#StateObject var cm = ContentViewModel()
var rotation: CMRotationRate { cm.gyroRotation }
let gridItems = [
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90))
]
var body: some View {
LazyVGrid(columns: gridItems, spacing: 30) {
ForEach(0..<60) { _ in
LinearGradient(
colors: [.red, .orange, .yellow, .blue, .purple, .green],
startPoint: rotation.x > 0 ? .leading : .trailing,
endPoint: rotation.x > 0 ? .trailing : .leading
)
.animation(.linear, value: rotation.x)
.frame(width: 50, height: 50)
.mask(
Image("logo")
.resizable()
.scaledToFit()
.opacity(0.3)
)
}
}
.padding(20)
.onAppear { cm.startGyroscope() }
}
}
Continuing on this to make it work, and I realised I didn't need the LinearGradient to animate but the Color itself:
View
struct CoreMotion: View {
// -- where the data lives
#ObservedObject var coreMotionVM = CoreMotionViewModel()
let gridItems = [
GridItem(), GridItem(), GridItem(), GridItem()
]
// -- the view
var body: some View {
LazyVGrid(columns: gridItems, spacing: 5) {
ForEach( 0 ..< 60 ) { _ in
coreMotionVM.tintColour
.animation(
.easeInOut,
value: coreMotionVM.roll
)
.frame(width: 60, height: 60)
.padding(.vertical)
.mask(
Image("logo")
.resizable()
.scaledToFit()
.opacity(0.2)
)
}
}
.padding(.horizontal)
.padding(.top)
}
}
ViewModel
final class CoreMotionViewModel: ObservableObject {
// -- what we share
#Published var roll: Double = 0.0
// -- core motion manager
private var manager: CMMotionManager
// -- initialisation
init() {
// -- create the instance
self.manager = CMMotionManager()
// -- how often to get the data
self.manager.deviceMotionUpdateInterval = 1/60
// -- start it on the main thread
self.manager.startDeviceMotionUpdates(to: .main) { motionData, error in
// -- error lets get out of here
guard error == nil else {
print(error!)
return
}
// -- update the data
if let motionData = motionData {
self.roll = motionData.attitude.roll
}
}
}
// cycle to the next point
var tintColour: Color {
switch roll {
case ..<(-1.5):
return .red
case -1.5 ..< -1.0 :
return .orange
case -1.0 ..< -0.5 :
return .yellow
case -0.5 ..< 0.5 :
return .green
case 0.5 ..< 1.0 :
return .blue
case 1.0... :
return .purple
default:
return .pink
}
}
}

SwiftUI: Using LongPressGesture to display a Pinterest like context menu

I am trying create a context menu similar to Pinterest's context menu in their iOS app. Long pressing a post reveals a four button view, which while the user continues the longpress, is then able to drag and select the other buttons. Letting go of the long press will either select whichever button you are currently selecting or dismiss the menu altogether if you don't have anything selected. Please see an example of this below:
So far, I've tried something similar to Apple's documentation here:
https://developer.apple.com/documentation/swiftui/longpressgesture
But it seems that the gesture finishes as soon as it hits the minimumDuration defined in the gesture. I'd like the gesture to continue for as long as the user is holding, and end as soon as they let go.
Additionally, I am in the weeds when it comes to the dragging and selecting the other buttons. Here is my approach so far:
struct Example: View {
#GestureState var isDetectingLongPress = false
#State var completedLongPress = false
var longPress: some Gesture {
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
self.completedLongPress = finished
}
}
var body: some View {
HStack {
Spacer()
ZStack {
// Three button array to fan out when main button is being held
Button(action: {
// ToDo
}) {
Image(systemName: "circle.fill")
.frame(width: 70, height: 70)
.foregroundColor(.red)
}
.offset(x: self.isDetectingLongPress ? -90 : 0, y: self.isDetectingLongPress ? -90 : 0)
Button(action: {
// ToDo
}) {
Image(systemName: "circle.fill")
.frame(width: 70, height: 70)
.foregroundColor(.green)
}
.offset(x: 0, y: self.isDetectingLongPress ? -120 : 0)
Button(action: {
// ToDo
}) {
Image(systemName: "circle.fill")
.frame(width: 70, height: 70)
.foregroundColor(.blue)
}
.offset(x: self.isDetectingLongPress ? 90 : 0, y: self.isDetectingLongPress ? -90 : 0)
// Main button
Image(systemName: "largecircle.fill.circle")
.gesture(longPress)
}
Spacer()
}
}
The closest equivalent I can find is a context menu
Src: AppleDeveloper
if you press and hold, you get a similar effect.
Context Menu - Apple Devloper Documentation
Update: I found a solution that works, but it's using UIKit as I don't believe SwiftUI provides a way to do this natively. I followed the guidance from the answer to a similar question found here: https://stackoverflow.com/a/31591162/862561
It was however written in ObjC, so I translated roughly to Swift. For those who are curious, here is a simplified version of the process:
class UIControlsView: UIView {
let createButton = CreateButtonView()
let secondButton = ButtonView(color: .red)
var currentDraggedButton: ButtonView!
required init?(coder: NSCoder) {
fatalError("-")
}
init() {
super.init(frame: .zero)
self.backgroundColor = .green
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(self.longPress))
createButton.addGestureRecognizer(longPress)
createButton.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
createButton.center = CGPoint(x: UIScreen.main.bounds.size.width / 2, y: 40)
secondButton.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
secondButton.center = CGPoint(x: UIScreen.main.bounds.size.width / 2, y: 40)
self.addSubview(secondButton)
self.addSubview(createButton)
}
#objc func longPress(sender: UILongPressGestureRecognizer) {
if sender.state == .began {
print("Started Long Press")
secondButton.center.x = secondButton.center.x + 90
}
if sender.state == .changed {
let location = sender.location(in: self)
guard let superViewLocation = self.superview?.convert(location, from: self) else {
return
}
guard let view = self.superview?.hitTest(superViewLocation, with: nil) else {
return
}
if view.isKind(of: ButtonView.self) {
let touchedButton = view as! ButtonView
if self.currentDraggedButton != touchedButton {
if self.currentDraggedButton != nil {
self.currentDraggedButton.untouchedUp()
}
}
self.currentDraggedButton = touchedButton
touchedButton.isTouchedUp()
} else {
if self.currentDraggedButton != nil {
print("Unsetting currentDraggedButton")
self.currentDraggedButton.untouchedUp()
}
}
}
if sender.state == .ended {
print("Long Press Ended")
let location = sender.location(in: self)
guard let superViewLocation = self.superview?.convert(location, from: self) else {
return
}
guard let view = self.superview?.hitTest(superViewLocation, with: nil) else {
return
}
if view.isKind(of: ButtonView.self) {
let touchedButton = view as! ButtonView
touchedButton.untouchedUp()
touchedButton.tap()
self.currentDraggedButton = nil
}
secondButton.center.x = secondButton.center.x - 90
}
}

How can I keep a SceneKit scene from rerendering poorly when State changes?

My parent view has a child view which contains a SceneKit scene, but when any state changes happen in the parent view, it resets the SceneKit animation, alters the model's texture, and makes the model larger.
Is there a way to keep the SceneKit scene from being affected in this way by state changes?
Image of the model before and after the state changes
Here's the code for the parent view:
struct ContentView: View {
#State private var color: Color = .sBlue
var body: some View {
VStack {
Button(action: { self.color = .sOrange }) {
self.color
}
.frame(height: 240)
ModelView()
}
}
}
And here's the code for the SceneKit view:
struct ModelView: UIViewRepresentable {
let model = SCNScene(named: "art.scnassets/3D Models/yBotIdle.scn")!
func makeUIView(context: Context) -> SCNView {
model.rootNode.childNode(withName: "yBot", recursively: true)?
.scale = SCNVector3(x: 0.03, y: 0.03, z: 0.03)
let cameraNode = SCNNode()
let camera = SCNCamera()
camera.focalLength = 120
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0, y: 2.8, z: 35)
model.rootNode.addChildNode(cameraNode)
let directionalLightNode = SCNNode()
directionalLightNode.light = SCNLight()
directionalLightNode.light?.type = SCNLight.LightType.directional
directionalLightNode.light?.intensity = 1500
directionalLightNode.position = SCNVector3(x: 0, y: 6, z: 10)
directionalLightNode.eulerAngles = SCNVector3(x: -0.4, y: 0, z: 0)
model.rootNode.addChildNode(directionalLightNode)
let modelView = SCNView()
modelView.antialiasingMode = SCNAntialiasingMode.multisampling4X
modelView.backgroundColor = UIColor(ciColor: .clear)
return modelView
}
func updateUIView(_ modelView: SCNView, context: Context) {
modelView.scene = model
}
}
Thanks!
Figured it out! Creating an instance of the SceneKit view to use in the parent view instead of using the SceneKit view directly gets rid of the issues. I'm not sure why that's the case, if anyone can explain I'd love to hear it.
struct ContentView: View {
#State private var color: Color = .sBlue
let modelView = ModelView()
var body: some View {
VStack {
Button(action: { self.color = .sOrange }) {
self.color
}
.frame(height: 240)
modelView
}
}
}

How to animate images in SwiftUI, to play a frame animation

I want to animate images in SwiftUI's Image view
First, I tried creating some variables and a function to toggle the Image("imageVariable"). It changes but there is no animation even tried the withAnimation { } method
Secondly, I tried to use a UIKit view. Here, the animation works but I can't apply the resizable() modifier or a set a fixed frame
var images: [UIImage]! = [UIImage(named: "pushup001")!, UIImage(named: "pushup002")!]
let animatedImage = UIImage.animatedImage(with: images, duration: 0.5)
struct workoutAnimation: UIViewRepresentable {
func makeUIView(context: Self.Context) -> UIImageView {
return UIImageView(image: animatedImage)
}
func updateUIView(_ uiView: UIImageView, context: UIViewRepresentableContext<workoutAnimation>) {
}
}
struct WorkoutView: View {
var body: some View {
VStack {
workoutAnimation().aspectRatio(contentMode: .fit)
}
}
}
In method 1 I can change the image but not animate, while, in method 2 I can animate but not control it's size
I solved this using UIViewRepresentable protocol. Here I returned a UIView with the ImageView as it's subview. This gave me more control over the child's size, etc.
import SwiftUI
var images : [UIImage]! = [UIImage(named: "pushup001")!, UIImage(named: "pushup002")!]
let animatedImage = UIImage.animatedImage(with: images, duration: 0.5)
struct workoutAnimation: UIViewRepresentable {
func makeUIView(context: Self.Context) -> UIView {
let someView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
let someImage = UIImageView(frame: CGRect(x: 20, y: 100, width: 360, height: 180))
someImage.clipsToBounds = true
someImage.layer.cornerRadius = 20
someImage.autoresizesSubviews = true
someImage.contentMode = UIView.ContentMode.scaleAspectFill
someImage.image = animatedImage
someView.addSubview(someImage)
return someView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<workoutAnimation>) {
}
}
struct WorkoutView: View {
var body: some View {
VStack (alignment: HorizontalAlignment.center, spacing: 10) {
workoutAnimation()
Text("zzzz")
}
}
}
If you want a robust and cross-platform SwiftUI implementation for animated images, like GIF/APNG/WebP, I recommend using SDWebImageSwiftUI. This framework is based on exist success image loading framework SDWebImage and provides a SwiftUI binding.
To play the animation, use AnimatedImage view.
var body: some View {
Group {
// Network
AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"))
.onFailure(perform: { (error) in
// Error
})
}
}
in model :
var publisher : Timer?
#Published var index = 0
func startTimer() {
index = 0
publisher = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: {_ in
if self.index < count/*count of frames*/{
self.index += 1
}
else if let timer = self.publisher {
timer.invalidate()
self.publisher = nil
}
})
}
}
in view :
struct MyAnimationView : View {
let width : CGFloat
let images = (0...60).map { UIImage(named: "tile\($0)")! }
#StateObject var viewmodel : MyViewModel
var body: some View {
Image(uiImage: images[viewmodel.index])
.resizable()
.frame(width: width, height: width, alignment: .center)
}
}
I have created an image animation class that can be easily reused
import SwiftUI
struct ImageAnimated: UIViewRepresentable {
let imageSize: CGSize
let imageNames: [String]
let duration: Double = 0.5
func makeUIView(context: Self.Context) -> UIView {
let containerView = UIView(frame: CGRect(x: 0, y: 0
, width: imageSize.width, height: imageSize.height))
let animationImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height))
animationImageView.clipsToBounds = true
animationImageView.layer.cornerRadius = 5
animationImageView.autoresizesSubviews = true
animationImageView.contentMode = UIView.ContentMode.scaleAspectFill
var images = [UIImage]()
imageNames.forEach { imageName in
if let img = UIImage(named: imageName) {
images.append(img)
}
}
animationImageView.image = UIImage.animatedImage(with: images, duration: duration)
containerView.addSubview(animationImageView)
return containerView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<ImageAnimated>) {
}
}
The way to use it:
ImageAnimated(imageSize: CGSize(width: size, height: size), imageNames: ["loading1","loading2","loading3","loading4"], duration: 0.3)
.frame(width: size, height: size, alignment: .center)

Resources