Drag degradation when SwiftUI macOS [Toolbar -> ToolbarItem -> Button -> Action] contains #State variable reference - macos

2021/10/28 Update:
Fixed when building with:
macOS 12.0.1 (21A559)
Xcode 13.1 (13A1030d)
2021/10/19 Update:
Bug has been submitted to Apple - FB9713387
Another user has tweeted about this.
Could anyone shed some light on why referencing a #State variable inside the following introduces drag degradation/choppiness, or a workaround?
I have tried the following:
Different variable types
Using the variable within the action and not using it
Running same code on iOS - no lag at all.
Xcode Running Xcode Version 13.0 (13A233)
Code
struct ContentView: View {
#State private var iAmAnUnusedBool: Bool = true
#State private var dragOffset = CGSize.zero
#State private var width: CGFloat = 200
var body: some View {
HStack(alignment: .top, spacing: 0) {
TextEditor(text: .constant("Left Panel"))
Rectangle()
.frame(width: 6)
.onHover { $0 ? NSCursor.resizeLeftRight.push() : NSCursor.pop() }
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .named("contentView"))
.onChanged { gesture in
dragOffset = gesture.translation
}
.onEnded { gesture in
let dragOffsetOriginal = dragOffset.width
dragOffset = .zero
width = width - dragOffsetOriginal
}
)
TextEditor(text: .constant("Right panel"))
.frame(width: width - dragOffset.width)
}
.toolbar {
ToolbarItem {
Button("Button") {
iAmAnUnusedBool // THIS INTRODUCES DEGRADATION
}
}
}
.coordinateSpace(name: "contentView")
}
}
No Lag
Given iAmAnUnusedBool is commented out
When I drag the Rectangle() back and forth
Then there is no lag
Lag
Given iAmAnUnusedBool is uncommented
When I drag the Rectangle() back and forth
Then there is lag (appears 1 minute after rapid dragging)

Related

SwiftUI transition with opacity not showing

I'm still new to SwiftUI. I'm trying to get each change of an image to start out at opacity 0.0 (fully transparent), then increase to opacity 1.0 (fully opaque)
I expected I could achieve this using the .opacity transition. .opacity is described as a "transition from transparent to opaque on insertion", so my assumption is that by stating "withAnimation" in my Button action, I'd trigger the Image to be re-rendered, and the transition would occur beginning from faded to transparent. Instead I see the same instant appear of the new shape & slow morphing to a new size, no apparent change in .opacity. Code and .gif showing current result, below. I've used UIKit & know I'd set alpha to zero, then UIView.animate to alpha 1.0 over a duration of 1.0, but am unsure how to get the same effect in SwiftUI. Thanks!
struct ContentView: View {
#State var imageName = ""
var imageNames = ["applelogo", "peacesign", "heart", "lightbulb"]
#State var currentImage = -1
var body: some View {
VStack {
Spacer()
Image(systemName: imageName)
.resizable()
.scaledToFit()
.padding()
.transition(.opacity)
Spacer()
Button("Press Me") {
currentImage = (currentImage == imageNames.count - 1 ? 0 : currentImage + 1)
withAnimation(.linear(duration: 1.0)) {
imageName = imageNames[currentImage]
}
}
}
}
}
The reason you are not getting the opacity transition is that you are keeping the same view. Even though it is drawing a different image each time, SwiftUI sees Image as the same. the fix is simple: add .id(). For example:
Image(systemName: imageName)
.resizable()
.scaledToFit()
.padding()
.transition(.opacity)
// Add the id here
.id(imageName)
Here is the correct approach for this kind of issue:
We should not forgot how transition works!!! Transition modifier simply transmit a view to nothing or nothing to a view (This should be written with golden ink)! in your code there is no transition happening instead update happing.
struct ContentView: View {
#State var imageName1: String? = nil
#State var imageName2: String? = nil
var imageNames: [String] = ["applelogo", "peacesign", "heart", "lightbulb"]
#State var currentImage = -1
var body: some View {
VStack {
Spacer()
ZStack {
if let unwrappedImageName: String = imageName1 {
Image(systemName: unwrappedImageName)
.resizable()
.transition(AnyTransition.opacity)
.animation(nil, value: unwrappedImageName)
.scaledToFit()
}
if let unwrappedImageName: String = imageName2 {
Image(systemName: unwrappedImageName)
.resizable()
.transition(AnyTransition.opacity)
.animation(nil, value: unwrappedImageName)
.scaledToFit()
}
}
.padding()
.animation(.linear, value: [imageName1, imageName2])
Spacer()
Button("Press Me") {
currentImage = (currentImage == imageNames.count - 1 ? 0 : currentImage + 1)
if imageName1 == nil {
imageName2 = nil; imageName1 = imageNames[currentImage]
}
else {
imageName1 = nil; imageName2 = imageNames[currentImage]
}
}
}
}
}

How to add animation to sheet dismissal

Presenting a modal sheet in SwiftUI MacOS has a nice slide in animation, but when dismissed it just disappear.
I would like to add a slide out animation to the dismissal of the sheet.
Adding the slideout on the content of the sheet obviously doesn’t work, as the content slides out, but the sheet frame remains until dismissed.
I might be missing something obvious here, but how can I add animation to the sheet itself?
struct ModalSheet: View {
#Binding var visible: Bool
#State var isShowing: Bool = true
var body: some View {
let s = "Click me to dismiss " + String(isShowing);
VStack {
Text(s)
}
.frame(width: 200, height: 100, alignment: .center)
.background(Color.green.opacity(0.5))
.onTapGesture {
isShowing.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
visible.toggle()
}
}
.offset(y: self.isShowing ? 0 : CGFloat(-110)) // <-- this should be on the sheet itself
.animation(self.isShowing ? .none : .default)
}
}
struct ModalTestAnimation: View {
#State var visible: Bool = false;
var body: some View {
VStack {
Text("Click me")
.onTapGesture {
visible = true
}
}
.frame(width: 250, height: 150)
.sheet(isPresented: $visible, content: {
ModalSheet(visible: $visible)
})
}
}
I did take a look at this, but it doesn’t work for me
SwiftUI sheet not animating dismissal on macOS Big Sur

Let a window still be resizable after a programatical resize in a SwiftUI MacOS AppKit App

I´m building a SwiftUI Mac App. It´s main window shall be resizable as normal in MacOS.
With a Button I want to resize the window programaticly.
That works fine with the code below.
After the programatic resizing the window cannot be resized anymore by the user(via mouse).
How can this be accomplished?
struct MainView1: View {
#State var width : CGFloat = .infinity
var body: some View {
SomeView(width: $width)
.background(Color.red)
.frame(width: width)
}
}
struct SomeView: View {
#Binding var width : CGFloat
var body: some View {
GeometryReader{geometry in
VStack{
Text("geometry width:\(geometry.size.width)")
Text("geometry hight:\(geometry.size.height)")
Button("Set width = 200"){width = 200}
Text("width:\(width)")
}//
.background(Color.yellow)
//.frame(width: geometry.size.height, height: geometry.size.height)
}
}
}

MacOS - SwiftUI, picker dismisses popover

I'm creating a popover for a MacOS app in SwiftUI, but the below picker dismisses the view once a selection is made:
#State private var showPopover: Bool = false
var strengths = ["Mild", "Medium", "Mature"]
#State private var selectedStrength = 0
var body: some View {
VStack{
Button("Show popover") {
self.showPopover = true
}.popover(
isPresented: self.$showPopover1,
arrowEdge: .bottom
) {
Picker(selection: self.$selectedStrength, label: Text("Strength")) {
ForEach(0 ..< self.strengths.count) {
Text(self.strengths[$0])
}
}.frame(width: 200, height: 100)
}
}
}
Once a selection in the picker is made, the popover dismisses. I need this to stay active, because I want to add further actions to the popover. Does anyone know how I can make sure the popover stays active?
Just to confirm, it's a MacOS app, perhaps MacOS handles it differently from iOS?
Thanks!

How can I align a progress indicator to the right of a label and keep the label centred in a VStack in SwiftUI?

I want to display a label in the centre of a view with a progress indicator to the right of the label. How can I do this in SwiftUI on macOS?
The code below aligns the HStack in the centre of the VStack but I want the text centered and the progress indicator aligned with the text's trailing edge. I guess I could replace the HStack with a ZStack but it's still not clear how one aligns two controls to each other or how one prevents the container from be centered by its container.
import SwiftUI
struct AlignmentTestView: View {
var body: some View {
VStack(alignment: .center, spacing: 4) {
HStack {
Text("Some text")
ActivityIndicator()
}
}.frame(width: 200, height: 200)
.background(Color.pink)
}
}
struct AlignmentTestView_Previews: PreviewProvider {
static var previews: some View {
AlignmentTestView()
}
}
Here is possible approach (tested replacing ActivityIndicator with just circle).
Used Xcode 11.4 / iOS 13.4 / macOS 10.15.4
var body: some View {
VStack {
HStack {
Text("Some text")
.alignmentGuide(.hAlignment) { $0.width / 2.0 }
ActivityIndicator()
}
}
.frame(width: 200, height: 200, alignment:
Alignment(horizontal: .hAlignment, vertical: VerticalAlignment.center))
.background(Color.pink)
}
extension HorizontalAlignment {
private enum HAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[HorizontalAlignment.center]
}
}
static let hAlignment = HorizontalAlignment(HAlignment.self)
}

Resources