SwiftUI CPU High Usage on Real-Time ForEach View Updating (macOS) - swift

So I have a view and I am computing the amplitude of an audio file and then passing it to a view to graph it and it changes in real-time. The problem is that the graph gets so laggy after few seconds and CPU usage gets maximized and literally everything kind of freezes.
Is there any way to render the view using GPU instead of the CPU in SwiftUI macOS?
I am updating the values for amplitudes in a DispatchQueue.main.async and sending an NSNotification.
This is the file I am calling when I want to show the graphs
struct TimeDomain: View {
#Binding var manager: AVManager
#State var pub = DSPNotification().publisherPath()
#State var amps = [Double]()
var body: some View {
AmplitudeVisualizer(amplitudes: $amps).frame(height: 300)
.onReceive(pub) { (output) in
self.amps = manager.amplitudes
}
.onAppear {
self.amps = manager.amplitudes
}
.drawingGroup()
}
}
and the file for AmplitudeVisualizer.swift
struct AmplitudeVisualizer: View {
#Binding var amplitudes: [Double]
var body: some View {
HStack(spacing: 0.0){
ForEach(0..<self.amplitudes.count, id: \.self) { number in
VerticalBar(amplitude: self.$amplitudes[number])
}
.drawingGroup()
}
}
}
and VerticalBar.swift
/// Single bar of Amplitude Visualizer
struct VerticalBar: View {
#Binding var amplitude: Double
var body: some View {
GeometryReader
{ geometry in
ZStack(alignment: .bottom){
// Colored rectangle in back of ZStack
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: [.red, .yellow, .green]), startPoint: .top, endPoint: .center))
// blue/purple bar style - try switching this out with the .fill statement above
//.fill(LinearGradient(gradient: Gradient(colors: [Color.init(red: 0.0, green: 1.0, blue: 1.0), .blue, .purple]), startPoint: .top, endPoint: .bottom))
// Dynamic black mask padded from bottom in relation to the amplitude
Rectangle()
.fill(Color.black)
.mask(Rectangle().padding(.bottom, geometry.size.height * CGFloat(self.amplitude)))
.animation(.easeOut(duration: 0.05))
// White bar with slower animation for floating effect
Rectangle()
.fill(Color.white)
.frame(height: geometry.size.height * 0.005)
.offset(x: 0.0, y: -geometry.size.height * CGFloat(self.amplitude) - geometry.size.height * 0.02)
.animation(.easeOut(duration: 0.6))
}
.padding(geometry.size.width * 0.1)
.border(Color.black, width: geometry.size.width * 0.1)
}.drawingGroup()
}
}

Related

Can a navigation link to a view fill more than half a VStack? [duplicate]

I have encountered this problem during the Swift programming. I have this long frame that I created, here it is.
The main idea that this frame will be used in a horizontal Scroll View in different view, like this. It will be opening different view on tap.
Here's the catch. If we want to transition to different view, we need NavigationLink. In order to work NavigationLink needs NavigationView. When we add our LongFrame in NavigationView, this happens
If we tap on it, it will display View, but in small frame
And If we, for example, add our LongFrameScrollView somewhere, It won't even show up sometimes
I will provide the code here. My guess that should be connected to .frame, but without this line of code I can't create this frame(.
// FRAME ITSELF
import SwiftUI
struct LongFrameView: View {
var body: some View {
NavigationView {
NavigationLink {
PlayerView()
} label: {
ZStack {
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: [Color(red: 0.268, green: 0.376, blue: 0.587), Color(red: 0.139, green: 0.267, blue: 0.517)]),
startPoint: .leading,
endPoint: .trailing))
.frame(width: 310, height: 62)
.cornerRadius(8)
HStack {
Image("mountains")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 70, height: 62)
.cornerRadius(8, corners: [.topLeft, .bottomLeft])
VStack(alignment: .leading) {
Text("Sense of anxiety")
.font(.custom("Manrope-Bold", size: 14))
.foregroundColor(.white)
Text("11 MIN")
.font(.custom("Manrope-Medium", size: 12))
.foregroundColor(.white)
}
Spacer()
}
}
.frame(width: 310, height: 62)
}
}
}
}
struct LongFrameView_Previews: PreviewProvider {
static var previews: some View {
LongFrameView()
}
}
// MARK: - WITH THIS CODE WE CAN DEFINE WHERE CORNER RADIUS WILL BE CHANGED OR NOT. DO NOT MODIFY
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
// SCROLL VIEW WITH FRAMES
import SwiftUI
struct LongFrameScrollView: View {
let rows = Array(repeating: GridItem(.fixed(60), spacing: 10, alignment: .leading), count: 2)
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 10) {
// PLACEHOLDER UNTIL API IS READY
LongFrameView()
LongFrameView()
LongFrameView()
LongFrameView()
}
}
.padding([.horizontal, .bottom], 10)
}
}
struct LongFrameScrollView_Previews: PreviewProvider {
static var previews: some View {
LongFrameScrollView()
}
}
NavigationView should be added as the first/top view. So embed your ScrollView with NavigationView inside LongFrameScrollView and removed it from LongFrameView. Inside LongFrameView you just need NavigationLink.
NavigationView {
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 10) {
// PLACEHOLDER UNTIL API IS READY
LongFrameView()
LongFrameView()
LongFrameView()
LongFrameView()
}
}
.padding([.horizontal, .bottom], 10)
}

SwiftUI gestures in the toolbar area ignored

I'd like to implement a custom slider SwiftUI component and put it on the toolbar area of a SwiftUI Mac app. However the gesture of the control gets ignored as the system's window moving gesture takes priority. This problem does not occur for the system UI controls, like Slider or Button.
How to fix the code below so the slider works in the toolbar area as well, not just inside the window similar to the default SwiftUI components?
struct MySlider: View {
#State var offset: CGFloat = 0.0
var body: some View {
GeometryReader { gr in
let thumbSize = gr.size.height
let maxValue = (gr.size.width - thumbSize) / 2.0
let gesture = DragGesture(minimumDistance: 0).onChanged { v in
self.offset = max(min(v.translation.width, maxValue), -maxValue)
}
ZStack {
Capsule()
Circle()
.foregroundColor(Color.yellow)
.frame(width: thumbSize, height: thumbSize)
.offset(x: offset)
.highPriorityGesture(gesture)
}
}.frame(width: 100, height: 20)
}
}
struct ContentView: View {
#State var value = 0.5
var body: some View {
MySlider()
.toolbar {
MySlider()
Slider(value: $value).frame(width: 100, height: 20)
}.frame(width: 500, height: 100)
}
}
Looks like design limitation (or not implemented yet feature - Apple does not see such view as user interaction capable item).
A possible workaround is to wrap you active element into button style. The button as a container interpreted as user-interaction-able area but all drawing and handling is in your code.
Tested with Xcode 13.2 / macOS 12.2
Note: no changes in your slider logic
struct MySlider: View {
var body: some View {
Button("") {}.buttonStyle(SliderButtonStyle())
}
struct SliderButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
MySliderContent()
}
struct MySliderContent: View {
#State var offset: CGFloat = 0.0
var body: some View {
GeometryReader { gr in
let thumbSize = gr.size.height
let maxValue = (gr.size.width - thumbSize) / 2.0
let gesture = DragGesture(minimumDistance: 0).onChanged { v in
self.offset = max(min(v.translation.width, maxValue), -maxValue)
}
ZStack {
Capsule()
Circle()
.foregroundColor(Color.yellow)
.frame(width: thumbSize, height: thumbSize)
.offset(x: offset)
.highPriorityGesture(gesture)
}
}.frame(width: 100, height: 20)
}
}
}
}

Custom Slider Taking Too Much Space

I used a code from here to create my own variation of a custom slider. However, the slider has too much extra space at the bottom, and I would like to remove that. Is there any way to do so?
Here is the code of the slider:
struct CustomSlider: View {
#Binding var value: CGFloat
#State var lastOffset: CGFloat = 0
var range: ClosedRange<CGFloat>
var leadingOffset: CGFloat = 5
var trailingOffset: CGFloat = 5
var knobSize: CGSize = CGSize(width: 25, height: 25)
let trackGradient = LinearGradient(gradient: Gradient(colors: [.pink, .yellow]), startPoint: .leading, endPoint: .trailing)
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
ZStack(alignment: .leading) {
Capsule()
.fill(Color(hue: 0.172, saturation: 0.275, brightness: 1.0))
.frame(height: 30)
Capsule()
.fill(Color(hue: 0.55, saturation: 0.326, brightness: 1.0))
.frame(width: self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset))+30, height: 30)
}
HStack {
RoundedRectangle(cornerRadius: 50)
.frame(width: self.knobSize.width, height: self.knobSize.height)
.foregroundColor(.white)
.offset(x: self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset)))
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
if abs(value.translation.width) < 0.1 {
self.lastOffset = self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset))
}
let sliderPos = max(0 + self.leadingOffset, min(self.lastOffset + value.translation.width, geometry.size.width - self.knobSize.width - self.trailingOffset))
let sliderVal = sliderPos.map(from: self.leadingOffset...(geometry.size.width - self.knobSize.width - self.trailingOffset), to: self.range)
self.value = sliderVal
}
)
}
}
}
}
}
I have noticed that when I select the empty space, the code cased in geometry reader is automatically selected. (I couldn't embed the image in this post, so here's the link)
When you use GeometryReader, it automatically takes up all available space. If you'd like to constrain it, use a frame(height: ) modifier to give it a certain height.
struct CustomSlider: View {
//all of the properties
var body: some View {
GeometryReader { geometry in
// slider code
}.frame(height: 60) //<-- Here
}
}

SwiftUI Create a Animating Circle

I'd like to create some content like this, a blinking green circle, and it works in the single preview mode
But when I put the View inside a List, the Green circle start moving left and right
struct DotView: View {
#State var delay: Double = 0 // 1.
#State var scale: CGFloat = 0.5
var body: some View {
Circle()
.frame(width: 6, height: 6)
.foregroundColor(Color.green)
.scaleEffect(scale)
.animation(Animation.easeInOut(duration: 0.6).repeatForever().delay(delay)) // 2.
.onAppear {
withAnimation {
self.scale = 1
}
}
}
}
Using inside a navigation view
List {
VStack {
HStack {
Text(server.name)
.fontWeight(.bold)
.foregroundColor(Color.primary)
.fontWeight(.light)
.foregroundColor(.gray)
Spacer()
DotView()
}
}
}
As I wrote in comments it is not reproducible for me, but try the following...
struct DotView: View {
var delay: Double = 0 // 1. << don't use state for injectable property
#State private var scale: CGFloat = 0.5
var body: some View {
Circle()
.frame(width: 6, height: 6)
.foregroundColor(Color.green)
.scaleEffect(scale)
.animation(
Animation.easeInOut(duration: 0.6)
.repeatForever().delay(delay), value: scale // 2. << link to value
)
.onAppear {
self.scale = 1 // 3. << withAnimation no needed now
}
}
}

How to change anchor of rotation angle to center with CGAffineTransform?

For some raisons, in my app in SwiftUI, I need to use transformEffect modifier with CGAffineTransform and rotationAngle property. But the result is la this:
How can I set the anchor of rotation angle to center of my arrow image? I see in the document that can we use CGPoint?
My code:
import SwiftUI
import CoreLocation
struct Arrow: View {
var locationManager = CLLocationManager()
#ObservedObject var heading: LocationManager = LocationManager()
private var animationArrow: Animation {
Animation
.easeInOut(duration: 0.2)
}
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.overlay(RoundedRectangle(cornerRadius: 0)
.stroke(Color.white, lineWidth: 2))
.animation(animationArrow)
.transformEffect(
CGAffineTransform(rotationAngle: CGFloat(-heading.heading.degreesToRadians))
.translatedBy(x: 0, y: 0)
)
Text(String(heading.heading.degreesToRadians))
.font(.system(size: 30))
.fontWeight(.light)
}
}
}
If you want to do it with a transform matrix, you'll need to translate, rotate, translate. But you can perform a rotationEffect with an anchor point. Note that the default anchor point is center. I am just including it explicity, so it works if you want to rotate anchoring somewhere else.
The anchor point is specified in UnitPoint, which means coordinates are 0 to 1. With 0.5 meaning center.
struct ContentView: View {
#State private var angle: Double = 0
var body: some View {
VStack {
Rectangle().frame(width: 100, height: 100)
.rotationEffect(Angle(degrees: angle), anchor: UnitPoint(x: 0.5, y: 0.5))
Slider(value: $angle, in: 0...360)
}
}
}
The same effect, using matrices, is uglier, but also works:
struct ContentView: View {
#State private var angle: Double = 0
var body: some View {
VStack {
Rectangle().frame(width: 100, height: 100)
.transformEffect(
CGAffineTransform(translationX: -50, y: -50)
.concatenating(CGAffineTransform(rotationAngle: CGFloat(angle * .pi / 180)))
.concatenating(CGAffineTransform(translationX: 50, y: 50)))
Slider(value: $angle, in: 0...360)
}
}
}