In my project, I have a SCNView encapsulated in UIViewRepresentable, that should resize in height according to the DragGesture of another view.
The issue is that when dragging, or after drag gesture ended, the memory usage is abnormally high. Checking for memory leaks in Instruments yielded no leaks. It also only occurs in a physical device, not in Simulator.
When using the Simulator,
The memory use is stable at around 19MB, regardless of resizing or not.
When using a physical device,
The memory use spiked badly upon each resize, and doesn't fall after end. (Worst I've seen was around 1.9GB of memory usage, before crash.)
Any experts here know the reason why it happens, and how to fix it? Thanks in advance.
UPDATE: I noticed that after DragGesture ended, if I were to move the camera of the SCNView, the memory will drop back to normal values.
Heres the code of the main ContentView.
Basically its a split view layout where if I were to adjust the RoundedRectangle (which acts as a handle), the top and bottom views will resize. It is when resizing that will cause the memory spike issue, which, at worst, will cause a crash, and Message from debugger: Terminated due to memory issue.
VStack {
SceneView()
.frame(height: (UIScreen.main.bounds.height / 2) + self.gestureTranslation.height)
RoundedRectangle(cornerRadius: 5)
.frame(width: 40, height: 6)
.foregroundColor(Color.gray)
.padding(2)
.gesture(DragGesture(coordinateSpace: .global)
.onChanged({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
})
.onEnded({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
self.prevTranslation = self.gestureTranslation
})
)
Rectangle()
.fill(Color.green)
.frame(height: (UIScreen.main.bounds.height / 2) - self.gestureTranslation.height)
}
}
Heres the code for the UIViewRepresentable wrapper for the SCNView.
struct SceneView: UIViewRepresentable {
typealias UIViewType = SCNView
func makeUIView(context: Context) -> SCNView {
let scene = SCNScene(named: "SceneAssets.scnassets/Cube.scn")
let view = SCNView()
view.scene = scene
view.allowsCameraControl = true
view.defaultCameraController.interactionMode = .orbitTurntable
view.defaultCameraController.inertiaEnabled = true
return view
}
func updateUIView(_ uiView: SCNView, context: Context) {
}
}
Here's the full test project that I've made to describe the issue: https://github.com/vincentneo/swiftui-scnview-memory-issue
Related
Here is a breakdown.
I have a zstack that contains 2 vstacks.
first vstack has a spacer and an image
second has a text and button.
ZStack {
VStack {
Spacer()
Image("some image")
}
VStack {
Text("press the button")
Button("ok") {
print("you pressed the button")
}
}
}
Now this setup would easily give me an image on the bottom of a zstack, and a centered title and button.
However if for example the device had a small screen or an ipad rotates to landscape. depending on the image size (which is dynamic). The title and button will overlap the image. instead of the button being "pushed" up.
In UIKit this is as simple as centering the button to superview with a high priority and having greaterThanOrEqualTo image.topAnchor with a required priority.
button would be centered in screen but if the top of the image was too big the center constraint would give priority to the image top anchor required constraint and push the button up.
I have looked into custom alignments and can easily get always above image or always center but am missing some insight in having it both depending on layout. Image size is dynamic so no hardcoded sizes.
What am i missing here? how would you solve this simple yet tricky task.
There might be an easier way using .alignmentGuide but I tried to practice on Layout for this answer.
I created a custom ImageAndButtonLayout that should do what you want: it takes two views assuming the first is the image and the second is the button (or anything else).
They are put into subviews just for clarity, you can also put them directly into ImageAndButtonLayout. For testing you can change the height of the image via slider.
The Layout always uses the available full height and pushes the first view (image) to the bottom - so you don't need an extra Spacer() with the image. The position of the second view (button) is calculated based on the height of the first view and the available height.
struct ContentView: View {
#State private var imageHeight = 200.0 // for testing
var body: some View {
VStack {
ImageAndButtonLayout {
imageView
buttonView
}
// changing "image" height for testing
Slider(value: $imageHeight, in: 50...1000)
.padding()
}
}
var imageView: some View {
Color.teal // Image placeholder
.frame(height: imageHeight)
}
var buttonView: some View {
VStack {
Text("press the button")
Button("ok") {
print("you pressed the button")
}
}
}
}
struct ImageAndButtonLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxsizes = subviews.map { $0.sizeThatFits(.infinity) }
var totalWidth = maxsizes.max {$0.width < $1.width}?.width ?? 0
totalWidth = min(totalWidth, proposal.width ?? .infinity )
let totalHeight = proposal.height ?? .infinity // always return maximum height
return CGSize(width: totalWidth, height: totalHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let heightImage = subviews.first?.sizeThatFits(.unspecified).height ?? 0
let heightButton = subviews.last?.sizeThatFits(.unspecified).height ?? 0
let maxHeightContent = bounds.height
// place image at bottom, growing upwards
let ptBottom = CGPoint(x: bounds.midX, y: bounds.maxY) // bottom of screen
if let first = subviews.first {
var totalWidth = first.sizeThatFits(.infinity).width
totalWidth = min(totalWidth, proposal.width ?? .infinity )
first.place(at: ptBottom, anchor: .bottom, proposal: .init(width: totalWidth, height: maxHeightContent))
}
// place button at center – or above image
var centerY = bounds.midY
if heightImage > maxHeightContent / 2 - heightButton {
centerY = maxHeightContent - heightImage
centerY = max ( heightButton * 2 , centerY ) // stop at top of screen
}
let ptCenter = CGPoint(x: bounds.midX, y: centerY)
if let last = subviews.last {
last.place(at: ptCenter, anchor: .center, proposal: .unspecified)
}
}
}
I copied this period of code to make a clipped shape of rounded corner. But the "rect" variable make me puzzled. It isn't an input variable.How can I use this struct without passing a value or initialize it.
import SwiftUI
struct RoundedShape:Shape{
var corners:UIRectCorner
func path(in rect:CGRect) -> Path {
let path=UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: 80, height: 80))
return Path(path.cgPath)
}
}
⬆️ This is what I got from Internet
Rectangle()
.clipShape( RoundedShape(corners: [.bottomRight]))
}
And I use this struct here and got this
image
Nothing wrong with the result,I just don't understand why this works without any passing or initializing to the "rect".
If you can help me, I appreciate it a lot.
A shape takes all available space in a place where it is created. So rect is injected by SwiftUI layout engine and in your case it is all space consumed by parent Rectangle:
Rectangle() // << bounds of this view
.clipShape( RoundedShape(corners: [.bottomRight]))
Here I have this question when I try to give a View an initial position, then user can use drag gesture to change the location of the View to anywhere. Although I already solved the issue by only using .position(x:y:) on a View, at the beginning I was thinking using .position(x:y:) to give initial position and .offset(offset:) to make the View move with gesture, simultaneously. Now, I really just want to know in more detail, what exactly happens when I use both of them the same time (the code below), so I can explain what happens in the View below.
What I cannot explain in the View below is that: when I simply drag gesture on the VStack box, it works as expected and the VStack moves with finger gesture, however, once the gesture ends and try to start a new drag gesture on the VStack, the VStack box goes back to the original position suddenly (like jumping to the original position when the code is loaded), then start moving with the gesture. Note that the gesture is moving as regular gesture, but the VStack already jumped to a different position so it starts moving from a different position. And this causes that the finger tip is no long on top of the VStack box, but off for some distance, although the VStack moves with the same trajectory as drag gesture does.
My question is: why the .position(x:y:) modifier seems only take effect at the very beginning of each new drag gesture detected, but during the drag gesture action on it seems .offset(offset:) dominates the main movement and the VStack stops at where it was dragged to. But once new drag gesture is on, the VStack jumps suddenly to the original position. I just could not wrap my head around how this behavior happens through timeline. Can somebody provide some insights?
Note that I already solved the issue to achieve what I need, right now it's just to understand what is exactly going on when .position(x:y:) and .offset(offset:) are used the same time, so please avoid some advice like. not use them simultaneously, thank you. The code bellow suppose to be runnable after copy and paste, if not pardon me for making mistake as I delete few lines to make it cleaner to reproduce the issue.
import SwiftUI
struct ContentView: View {
var body: some View {
ButtonsViewOffset()
}
}
struct ButtonsViewOffset: View {
let location: CGPoint = CGPoint(x: 50, y: 50)
#State private var offset = CGSize.zero
#State private var color = Color.purple
var dragGesture: some Gesture {
DragGesture()
.onChanged{ value in
self.offset = value.translation
print("offset onChange: \(offset)")
}
.onEnded{ _ in
if self.color == Color.purple{
self.color = Color.blue
}
else{
self.color = Color.purple
}
}
}
var body: some View {
VStack {
Text("Watch 3-1")
Text("x: \(self.location.x), y: \(self.location.y)")
}
.background(Color.gray)
.foregroundColor(self.color)
.offset(self.offset)
.position(x: self.location.x, y: self.location.y)
.gesture(dragGesture)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
Your issue has nothing to do with the use of position and offset. They actually both work simultaneously. Position sets the absolute position of the view, where as offset moves it relative to the absolute position. Therefore, you will notice that your view starts at position (50, 50) on the screen, and then you can drag it all around. Once you let go, it stops wherever it was. So far, so good. You then want to move it around again, and it pops back to the original position. The reason it does that is the way you set up location as a let constant. It needs to be state.
The problem stems from the fact that you are adding, without realizing it, the values of offset to position. When you finish your drag, offset retains the last values. However, when you start your next drag, those values start at (0,0) again, therefore the offset is reset to (0,0) and the view moves back to the original position. The key is that you need to use just the position or update the the offset in .onEnded. Don't use both. Here you have a set position, and are not saving the offset. How you handle it depends upon the purpose for which you are moving the view.
First, just use .position():
struct OffsetAndPositionView: View {
#State private var position = CGPoint(x: 50, y: 50)
#State private var color = Color.purple
var dragGesture: some Gesture {
DragGesture()
.onChanged{ value in
position = value.location
print("position onChange: \(position)")
}
.onEnded{ value in
if color == Color.purple{
color = Color.blue
}
else{
color = Color.purple
}
}
}
var body: some View {
Rectangle()
.fill(color)
.frame(width: 30, height: 30)
.position(position)
.gesture(dragGesture)
}
}
Second, just use .offset():
struct ButtonsViewOffset: View {
#State private var savedOffset = CGSize.zero
#State private var dragValue = CGSize.zero
#State private var color = Color.purple
var offset: CGSize {
savedOffset + dragValue
}
var dragGesture: some Gesture {
DragGesture()
.onChanged{ value in
dragValue = value.translation
print("dragValue onChange: \(dragValue)")
}
.onEnded{ value in
savedOffset = savedOffset + value.translation
dragValue = CGSize.zero
if color == Color.purple{
color = Color.blue
}
else{
color = Color.purple
}
}
}
var body: some View {
Rectangle()
.fill(color)
.frame(width: 30, height: 30)
.offset(offset)
.gesture(dragGesture)
}
}
// Convenience operator overload
func + (lhs: CGSize, rhs: CGSize) -> CGSize {
return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height)
}
Im currently making an app usig PencilKit and SwiftUI. If the screen is rotated, the canvas (and the drawing) should be rescaled acordingly. I do this with the Geometry reader and calculating the size.
NavigationView {
GeometryReader { g in
HStack {
CanvasView(canvasView: $canvasView)
.frame(width: g.size.width/1.5, height: (g.size.width/1.5)/1.5)
placeholder(placeholder: scaleDrawing(canvasHeight: (g.size.width/1.5)/1.5))
}
}
}
func scaleDrawing(canvasHeight : CGFloat) -> Bool {
let factor = canvasHeight / lastCanvasHeight
let transform = CGAffineTransform(scaleX: factor, y: factor)
canvasView.drawing = canvasView.drawing.transformed(using: transform)
lastCanvasHeight = canvasHeight
return true
}
The drawing however gets not scaled.
My solution was to create a placeholder view which has a boolean as a parameter. Also a method that scales the drawing with the height of the Canvas as an input.
This works but i think its pretty hacky and not the best solution, but this was the only solution that worked.
Is there a better way to do this? What am i missing?
On occasion, whatever default animation exists in the still limited ScrollView in Swift UI, leads to a "compounding" animation effect.
ScrollView {
Text("Hello")
.animation(
Animation.interpolatingSpring(stiffness: 200, damping: 3)
)
}
This can, sometimes, lead to a much stronger spring effect (with respect to the offset) on initial load as the ScrollView gets rendered.
Is there a way to isolate the spring effect to the offset relative to itself rather than its absolute offset.
As interesting note is that if I turn the ScrollView into VStack this doesn't happen. It seemScrollView has some animation on initial render.
Alright here's a hack for anyone desperate:
struct Hack: View {
#State var show = false
public var body: some View {
ScrollView {
Text("Hello")
.animation(
Animation.interpolatingSpring(stiffness: 200, damping: 3)
)
.opacity(show ? 1 : 0) // this is the hack
.onAppear { self.show.toggle() }
}
}
}
This will disable the egregious (at least in my case) animation on the initial load. This really isn't a good answer nor an insight into whats happening with ScrollView.