SwiftUI on macOS: Make subviews in .sheet use the available width - macos

I want to show a sheet with a title and action buttons upon clicking on a button in my macOS app. While this works, I do have an issue with the width of the containing buttons: they only use their intrinsically needed width instead of the width of their parent.
Out of the box, without providing any width values, the sheet looks like this:
I am currently hardcoding the width and applying it to the buttons – which works, but is not really sexy. The sheet then looks like this:
That's more like it. Here' my code:
struct ContentView: View {
#State private var showSheet = false
var body: some View {
VStack {
Text("My View")
Button {
showSheet = true
} label: {
Text("Show Sheet…")
}
}
.sheet(isPresented: $showSheet) {
SheetView(showSheet: $showSheet)
}
}
}
struct SheetView: View {
#Binding var showSheet: Bool
let width = CGFloat(200.0)
var body: some View {
VStack {
Text("FooBar")
Divider()
Button {
showSheet = false
} label: {
Label("Foo", systemImage: "paperclip")
.frame(width: width)
}
Button {
showSheet = false
} label: {
Label("FooBarBuzz", systemImage: "paperclip")
.frame(width: width)
}
Button {
showSheet = false
} label: {
Label("Cancel", systemImage: "xmark")
.frame(width: width)
}
}
.padding()
}
}
My goal though would be to set the width of all subviews in the sheet to the subview with the biggest width – how can I do that?

Related

SwiftUI: Why toolbar items added from any view appear only in the main toolbar in macOS?

In my macOS app (SwiftUI project multiplateform), I have a main toolbar that works well.
However, when I loaded a view, in a popover for example, the toolbar items set in the popover view, are added to the main toolbar.
In this image above, the X icon visible at top belongs to the popover view. It's added when loading the popover and hidden when leaving the popover.
The toolbar code in the popover view:
toolbar {
Button(action: {
selectedItemId = nil // set
},
label: { Image(systemName: "x.circle") })
}
Adding a searchable field will also be added in the main toolbar, and can break completly the toolbar layout.
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) {}
How macOS controls the different toolbar items from different views ? Each new view shoulb be a "window" to control a new toolbar attached to it ?
Thanks in advance.
.popover does not support Toolbars.
And in a Sheet the toolbar items are placed at the bottom.
But you can always include your own controls and buttons in a Popover. Just don't use .toolbar:
struct ContentView: View {
#State private var showSheet = false
#State private var showPopover = false
var body: some View {
VStack {
Button("Show sheet") { showSheet = true }
Button("Show popover") { showPopover = true}
}
.toolbar {
Button {
} label: {
Image(systemName: "house")
}
}
.sheet(isPresented: $showSheet) {
VStack {
Text("Dummy Sheet")
List(0..<20) { i in
Text("item \(i)")
}
.toolbar {
Button {
} label: {
Image(systemName: "x.circle")
}
}
}
.padding()
.frame(minWidth: 200, minHeight: 200)
}
.popover(isPresented: $showPopover) {
VStack {
Text("Dummy Popover")
List(0..<20) { i in
Text("item \(i)")
}
Divider()
Button {
} label: {
Image(systemName: "x.circle")
}
}
.frame(minWidth: 200, minHeight: 200)
.padding(.vertical)
}
}
}

SwiftUI: Animate resizing of .sheet (macOS)

Is it possible to animate a size change of a sheet? If I animate a size change of the sheet's contents, the sheet itself will immediately assume the new size without animation. See video (white background of sheet should never be visible):
Code:
struct ContentView: View {
#State var isPresented = false
#State var sheetContentHeight = 100.0
var body: some View {
VStack {
Button("Show Sheet") {
isPresented = true
}
}
.frame(width: 500, height: 300)
.sheet(isPresented: $isPresented) {
sheetContent
}
}
var sheetContent: some View {
ZStack {
Rectangle()
.frame(width: 300, height: sheetContentHeight)
.foregroundColor(Color.gray)
Button("Change Content Height") {
sheetContentHeight = 200
}
}
.animation(.easeOut(duration: 1), value: sheetContentHeight)
}
}

Animating changes to #FocusState SwiftUI

I am using the new #FocusState to control how my views react to the user deciding to start inputting information into text fields. My current need is to wrap an animation around my top view leaving the screen as the keyboard moves up. Usually this kind of thing can be accomplished by simply wrapping withAnimation() around a boolean toggle, but since Swift is toggling my focus state bool under the hood, I can't wrap an animation around it in this way. How else should I do it?
Here is a minimal reproducible example. Basically I want to animate the top (red) view leaving / coming back into view with changes to my focus state isFocused var.
struct ContentView: View {
#State var text: String = ""
#FocusState var isFocused: Bool
var body: some View {
ZStack {
VStack {
if !isFocused {
Text("How to Animate this?")
.frame(width: 300, height: 300)
.background(Color.red)
.animation(.easeInOut(duration: 5), value: isFocused)
}
Text("Middle Section")
.frame(width: 300, height: 300)
.background(Color.green)
Spacer()
TextField("placeholder", text: $text)
.focused($isFocused)
}
if isFocused {
Color.white.opacity(0.1)
.onTapGesture {
isFocused = false
}
}
}
}
}
I don't think the animation modifier that's currently on the top view is doing anything, but I imagine that that's where I'll put some animation code.
Here is something that works. I've done this before to make an animation happen upon an #FocusState property changing its value. Can't really tell you why though, it's just something I figured out with trial and error.
struct ContentView: View {
#State var text: String = ""
#FocusState var isFocused: Bool
#State private var showRedView = false
var body: some View {
ZStack {
VStack {
if !showRedView {
Text("How to Animate this?")
.frame(width: 300, height: 300)
.background(Color.red)
}
Text("Middle Section")
.frame(width: 300, height: 300)
.background(Color.green)
Spacer()
TextField("placeholder", text: $text)
.focused($isFocused)
}
.onChange(of: isFocused) { bool in
withAnimation(.easeInOut(duration: 5)) {
showRedView = bool
}
}
if isFocused {
Color.white.opacity(0.1)
.onTapGesture {
isFocused = false
}
}
}
}
}

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

How to auto-resize a SwiftUI sheet on macOS to fit its content?

Consider this code that presents a sheet on macOS:
struct ContentView: View {
#State var showSheet = false
var body: some View {
VStack {
Text("Hello, world!").padding()
Button("Show sheet") {
showSheet = true
}
}
.sheet(isPresented: $showSheet) {
SheetView()
}
}
}
struct SheetView: View {
#State var greenBiggerBox = false
var body: some View {
VStack {
Toggle("Show bigger box", isOn: $greenBiggerBox)
if greenBiggerBox {
Rectangle().frame(width: 640, height: 480).foregroundColor(.green)
} else {
Rectangle().frame(width: 320, height: 240).foregroundColor(.red)
}
}.padding()
}
}
The problem: initially, the sheet is presented, sized correctly to its contents (good). However, when something happens inside the sheet that resizes the content (like the toggle in the sheet), the sheet window is not resized, yielding a sheet size that is either too large or too small.
How do I make the sheet resize correctly and automatically if its the size of its content changes?

Resources