SwiftUI ObservableObject CPU spikes - performance

I have a SwiftUI project and a View that binds to an EnvironmentObject. This object contains a #Published property.
import Foundation
class Global : ObservableObject{
#Published var check :Bool = false;
}
When I run the application and make changes to my property, I see that my view gets redrawn on every change.
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var global :Global
var body: some View {
VStack{
VStack{
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
}
VStack{
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
}
VStack{
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
}
VStack{
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
Toggle("Checked", isOn: $global.check)
}
}
}
}
CPU rates get near 15%, just to draw 20 checkboxes. Is there something I am doing wrong or are there other ways to improve performance? I don't want to use a debounce to resolve this.
Extra information
MacOS Catalina 10.15.2 running MacOS SwiftUI project in XCode 11.3
Attached is an Instrument profiling of me clicking and unclicking one of my checkboxes every second. In Instruments I even see CPU run op to 90%.

SwiftUI views get redrawn when #ObservedObject, #EnvironmentObject, #StateObject or #State change. If you use so many toggles that bind to the same property, then you would expect a higher CPU usage. You aren't doing anything wrong and there is no way to improve performance of it for your purpose other than wait for improvements to SwiftUI itself.

Related

Swiftui expanding menu expands whole HStack?

Is it possible to have an expanding menu that does not expand the HStack it is in?
I want a "menu bar" on top of my app and on the right is an expanding menu with all my objects in. But when I expand the menu all of the HStack expands which looks bad.
I tried putting a .fixedsize() on the HStack but it didn't help.
Do I have to move the menu or is it possible?
This is my code:
HStack{
Text("Input searchfunction here")
.foregroundColor(Color.white)
.padding()
Spacer()
Button(action: {
createViewIsActive = true
}, label: {
Text("Lägg till tecken.")
.foregroundColor(Color.white)
.onAppear {
if signs.count != 0 {
activeSign = signs[0]
}
}
Image(systemName: "plus")
.foregroundColor(Color.white)
})
.padding()
Button(action: {
deleteItems()
}, label: {
Text("Delete")
})
Spacer()
DisclosureGroup("Tecken", isExpanded: $isExpanded) {
ScrollView{
VStack{
ForEach(signs, id: \.self) { sign in
HStack{
Text(sign.name!)
.font(.title3)
.padding(.all)
.onTapGesture {
self.isExpanded.toggle()
self.activeSign = sign
}
}
}
}
}
}
.accentColor(.white) // Arrow color
.font(.title3)
.foregroundColor(.white) // Text color
.padding(.all)
.cornerRadius(8)
.frame(minWidth: 10, maxWidth: 300)
}
.background(Color.gray)
.padding()

How to get round shape images of the same size based on SF Symbols in SwiftUI?

In my application I want to get simple round buttons based on SF Symbols of the same size. However, the same approach results in different image sizes depending on the symbol.
For example, an image with a plus sign is larger than a minus sign.
To solve this problem, I use the ZStack trick in which I put a transparent plus under the minus. But I think this is not the best solution. Are there any better solutions?
HStack{
Image(systemName: "plus")
.padding()
.overlay(
Circle()
.stroke(Color.primary,
lineWidth:1))
Image(systemName: "minus")
.padding()
.overlay(
Circle()
.stroke(Color.primary,
lineWidth:1))
//my solution
ZStack {
Image(systemName: "plus")
.padding()
.opacity(0.0)
.overlay(
Circle()
.stroke(Color.primary,
lineWidth:1))
Image(systemName: "minus")
}
}
"minus" in the center has a smaller size than "plus", "minus" on the right - my solution:
You can use ViewModifier or if are buttons ButtonStyle
ViewModifier
#available(iOS 13.0, *)
struct fillButtonCircle: ViewModifier {
var foregroundColor: Color = .white
var backgroundColor: Color = .green
var dimension: CGFloat = 10
func body(content: Content) -> some View {
content
.foregroundColor(foregroundColor)
.padding(dimension)
.background(backgroundColor)
.clipShape(Circle())
.frame(width: dimension, height: dimension)
.overlay(
Circle()
.stroke(Color.primary,
lineWidth:1))
}
}
ButtonStyle
#available(iOS 13.0, *)
struct CircleScaleButton: ButtonStyle {
var color: Color = .blue
var maxHeight: CGFloat = 35
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: maxHeight, alignment: .center)
.foregroundColor(.black)
.background(RoundedRectangle(cornerRadius: 35/2.0).fill(self.color))
.compositingGroup()
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.primary,
lineWidth:1))
.opacity(configuration.isPressed ? 0.8 : 1.0)
.scaleEffect(configuration.isPressed ? 0.9 : 1.0)
}
}
Example
struct SwiftUIViewTest: View {
var body: some View {
VStack {
Text("Button")
HStack {
Button(action: {}, label: {
Image(systemName: "plus")
})
.buttonStyle(CircleScaleButton(color: .clear, maxHeight: 45))
Button(action: {}, label: {
Image(systemName: "minus")
})
.buttonStyle(CircleScaleButton(color: .clear, maxHeight: 45))
}
Spacer()
.frame(height: 50)
Text("Image")
HStack {
Image(systemName: "plus")
.modifier(fillButtonCircle(foregroundColor: .black, backgroundColor: .clear, dimension: 40))
Image(systemName: "minus")
.modifier(fillButtonCircle(foregroundColor: .black, backgroundColor: .clear, dimension: 40))
}
}
}
}
use .circle
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Image(systemName: "plus.circle")
.resizable()
.frame(width: 50, height: 50, alignment: .center)
Image(systemName: "minus.circle")
.resizable()
.frame(width: 50, height: 50, alignment: .center)
}
}
}

Adding Selectable ScrollView Brakes The View

I am working on a task manager app. Recently trying to edit an item inside the scroll view. While trying to do so my views just jammed and I can not see my scroll view on the view. Instead of scroll view my add NewTaskView appears.
You may see the whole project in https://github.com/m3rtkoksal/TaskManager
struct TaskListView: View {
#State private(set) var data = ""
#State var isSettings: Bool = false
#State var isSaved: Bool = false
#State var shown: Bool = false
#State var selectedTask = TaskElement(dateFrom: "", dateTo: "", title: "", text: "")
var body: some View {
NavigationView {
ZStack {
Color(#colorLiteral(red: 0.9333333333, green: 0.9450980392, blue: 0.9882352941, alpha: 1)).edgesIgnoringSafeArea(.all)
VStack {
TopBar()
HStack {
CustomTextField(data: $data, tFtext: "Find task", tFImage: "magnifyingglass")
Button(action: {
self.isSettings.toggle()
}, label: {
ZStack {
RoundedRectangle(cornerRadius: 15)
.frame(width: 50, height: 50, alignment: .center)
.foregroundColor(Color(#colorLiteral(red: 0.4274509804, green: 0.2196078431, blue: 1, alpha: 1)))
Image("buttonImage")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
}
.padding(.horizontal, 15)
})
}
CustomSegmentedView()
ZStack {
TaskFrameView()
VStack {
Spacer()
HStack {
Spacer()
Button( action: {
self.isSaved.toggle()
}, label: {
ZStack {
RoundedRectangle(cornerRadius: 25)
.foregroundColor(Color(#colorLiteral(red: 1, green: 0.7137254902, blue: 0.2196078431, alpha: 1)))
Text("+")
.foregroundColor(.white)
.font(.title)
.fontWeight(.bold)
}
.frame(width: 50, height: 50)
})
}
}
NavigationLink(
destination: NewTaskView(isShown: $shown, task: selectedTask),
isActive: $shown,
label: {
Text("")
})
}
}
}
.navigationBarHidden(true)
Spacer()
}
.navigationBarHidden(true)
}
}
The first image is my mixed-up view
Second is NewTaskView and the last one is proper TaskListView
While this code works like a charm without editable scroll view
struct TaskListView: View {
#State private(set) var data = ""
#State var isSettings: Bool = false
#State var isSaved: Bool = false
var body: some View {
NavigationView {
ZStack {
Color(#colorLiteral(red: 0.9333333333, green: 0.9450980392, blue: 0.9882352941, alpha: 1)).edgesIgnoringSafeArea(.all)
VStack {
TopBar()
HStack {
CustomTextField(data: $data, tFtext: "Find task", tFImage: "magnifyingglass")
Button(action: {
self.isSettings.toggle()
}, label: {
ZStack {
RoundedRectangle(cornerRadius: 15)
.frame(width: 50, height: 50, alignment: .center)
.foregroundColor(Color(#colorLiteral(red: 0.4274509804, green: 0.2196078431, blue: 1, alpha: 1)))
Image("buttonImage")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
}
.padding(.horizontal, 15)
})
}
CustomSegmentedView()
ZStack {
TaskFrameView()
VStack {
Spacer()
HStack {
Spacer()
Button( action: {
self.isSaved.toggle()
}, label: {
ZStack {
RoundedRectangle(cornerRadius: 25)
.foregroundColor(Color(#colorLiteral(red: 1, green: 0.7137254902, blue: 0.2196078431, alpha: 1)))
Text("+")
.foregroundColor(.white)
.font(.title)
.fontWeight(.bold)
}
.frame(width: 50, height: 50)
})
}
}
NavigationLink(
destination: NewTaskView(),
isActive: $isSaved,
label: {
Text("")
})
}
}
}
.navigationBarHidden(true)
Spacer()
}
.navigationBarHidden(true)
}
}
Because that is what you told it to show in your struct ScrollViewTask. You have a ZStack that contains a ScrollView that contains a VStack with a ForEach that is supposed to display your TaskElementViews(while not part of the answer, why not just use a List) as well as your NewTaskView. In essence, you would always have a NewTaskView displayed on top of any TaskElements you have. I suspect that your code is simply not actually displaying TaskElements, either due to a lack of data, or other incorrect coding. Regardless, your NewTaskView would always overlay it.
Below is a folded version of your code that makes it clear:
struct ScrollViewTask: View {
#ObservedObject private var obser = observer()
#State var selectedTask = TaskElement(dateFrom: "", dateTo: "", title: "", text: "")
#State var shown: Bool = false
var body: some View {
ZStack {
ScrollView(.vertical) {...}
.onAppear {...}
NewTaskView(isShown: $shown, task: selectedTask)
}
}
struct ScrollViewTask: View {
#ObservedObject private var obser = observer()
#State var selectedTask = TaskElement(dateFrom: "", dateTo: "", title: "", text: "")
#State var shown: Bool = false
var body: some View {
ScrollView(.vertical) {
VStack {
ForEach(self.obser.tasks) { task in
TaskElementView(task:task)
.onTapGesture {
self.selectedTask = task
self.shown.toggle()
}
}
}
}
.onAppear {
self.obser.fetchData()
}
.sheet(isPresented: $shown, content: {
NewTaskView(isShown: $shown, task: selectedTask)
})
}
}

SwiftUI - Button doesn't work at the top left of the view

I am new in SwiftUI and I am having an issue with my "hamburger" menu.
Basically I have created 2 views for the Home page: Home view and Home Side Menu view.
My main goal is to put the "hamburger" button at the top left of the view and open the side menu when it is clicked.
However when I use .edgesIgnoringSafeArea(.top) and put the "hamburger" button at the top left of the view, the button doesn't open the menu.
If, instead, I don't add .edgesIgnoringSafeArea(.top), the button is displayed in the middle of the view and works correctly opening the menu when it is clicked.
Could you please help me to see what I have done wrong.
Thanks in advance.
Home View:
import SwiftUI
struct HomeView: View {
var body: some View {
NavigationView{
ScrollView{
VStack(alignment: .center) {
Text("Home")
.font(.system(size: 22))
.fontWeight(.bold)
.foregroundColor(.white)
.padding(.top, 20)
.padding(.bottom, 20)
}
.frame(width: UIScreen.main.bounds.width, height: 80)
.background(Color.blue)
Image("image")
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.all)
}
.edgesIgnoringSafeArea(.top)
}
.navigationBarBackButtonHidden(true)
}
}
Home Side Menu view:
import SwiftUI
struct HomeSideMenuView : View {
#State var showMenu = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}
return NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
VStack {
HomeView()
.overlay(
Button (action:{
withAnimation {
self.showMenu = true
}
}){
Image(systemName: "line.horizontal.3")
.padding()
.imageScale(.large)
.font(Font.system(size: 20, weight: .bold))
}
.padding()
.foregroundColor(.white)
.shadow(color: Color.gray, radius: 0.2, x: 1, y: 1)
.frame(maxWidth: geometry.size.width, maxHeight: geometry.size.height, alignment: .topLeading)
.edgesIgnoringSafeArea(.top) // -> if I add this line, the button doesn't work
)
}
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.showMenu ? geometry.size.width/1.5 : 0)
.disabled(self.showMenu ? true : false)
if self.showMenu {
MenuView()
.frame(width: geometry.size.width/1.5, height: geometry.size.height)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
}
.navigationBarBackButtonHidden(true)
}
}
i am having the same issue, i guess the first top 100px are reserved to the navigationBar, just add
.navigationBarTitle("")
.navigationBarHidden(true)
to your views containers (first child of NavigationView).
Hope this helps

Presenting an alert

I running SwiftUI with Xcode 11 Beat 5, and I wish to add an Alert to show the user when a Button is Tapped.
I am having problems though.
HStack{
Text("Inverted V")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.black)
Spacer()
Button(action: {
}, label: {
Image(systemName: "questionmark.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(.red)
}).onTapGesture {
print("Hello Print")
}}
Can someone help with the code needed? Do I place it within the onTapGesture brackets?
I have a #State var showingAlert = false
Thank you in advance.
You would use something like the code below. If you are using onTapGesture you don't need a Button.
import SwiftUI
struct ContentView: View {
#State var showingAlert: Bool = false
var body: some View {
HStack{
Text("Inverted V")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.black)
Spacer()
Image(systemName: "questionmark.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(.red)
.onTapGesture {
print("Hello Print")
self.showingAlert = true
}
}.alert(isPresented: $showingAlert, content: {
Alert(title: Text("Error"), message: Text("Error Reason"), dismissButton: .default(Text("OK")))
})
}
}

Resources