Why is ScollView content size not adjusting properly for this SwiftUI view? - swift

I am using Xcode beta7 and the following is the code.
This is for a MacOs app.
here is my code:
import SwiftUI
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
V1(isClicked: $isClicked)
}
}
}
struct V1: View {
#Binding var isClicked: Bool
var body: some View {
VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
Run this code and click on the Click me button.
You will see that the scrollView's contnent size does not update and stay's squished.
If i try to resize the frame of the application by using the mouse to resize the screen, then instantly, the ScrollView's content size snaps to the correct size.
Do i need to do something to get the ScrollView to do this automatically (instead of me having to manually inscrease the frame of the app with the mouse?

I got the same issue recently on iOS, watchOS. The only way, I solve it was to move the content of the scrollView as a func in your struct having the ScrollView. As shown below.
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
otherView()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
func otherView() -> some View{
return VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
It looks like a bug.

Related

How can I track scrolling with a ScrollView linked to a Custom PageControl - SwiftUI

I want to create a Carousel with SwiftUI(without using TabView)
with a matching/linked Page Control in SwiftUI
So far I have both views and can update the pageControl view with a
#State var pagecontrolTracker updated with a DragGesture() .onChanged but it doesn't update the PageControl if I scroll fast, or sometimes doesn't update at all 😭.
If I Scroll slow tho, the Page Control does update sometimes as expected.
Is there a better way to update this faster and smoother?
I saw .updating modifier for DragGesture() but this doesn't work either
Full View:
struct ContentView: View {
#State var pagecontrolTracker: Int = 0
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(0...3, id: \.self) { index in
PagingRow()
.gesture(DragGesture().onChanged({ _ in
pagecontrolTracker = index
}))
}
}
}
PagingControls(pagecontrolTracker: $pagecontrolTracker)
}
.padding()
}
}
Inside Custom SwiftUI Row View
struct PagingRow: View {
var body: some View {
VStack {
HStack {
Image(systemName: "globe")
Text("Test Title")
}
.padding()
Button {
print("Test action")
} label: {
Text("Tap Me")
}
.buttonStyle(.borderedProminent)
.padding()
}
.background(Color.orange)
.frame(width: 200)
.cornerRadius(8)
}
}
Custom PageControl in SwiftUI
struct PagingControls: View {
#Binding var pagecontrolTracker: Int
var body: some View {
HStack {
ForEach(0...3, id: \.self) { pagingIndex in
Circle()
.fill(pagecontrolTracker == pagingIndex ? .orange : .black)
.frame(width: 8, height: 8)
}
}
}
}
Note: I don't want to use TabView since I want to be able to show the next upcoming card in the scrollView
A TabView would only show one card per page

Show bottom sheet after button press swiftui

I'm trying to add to my app bottom sheet with responsive height which I can set programmatically. For this purpose I'm trying to use this video. Here is code of my view controller:
struct SecondView: View {
#State var cardShown = false
#State var cardDismissal = false
var body: some View {
Button {
cardShown.toggle()
cardDismissal.toggle()
} label: {
Text("Show card")
.bold()
.foregroundColor(Color.white)
.background(Color.red)
.frame(width: 200, height: 50)
}
BottomCard(cardShown: $cardShown, cardDismissal: $cardDismissal) {
CardContent()
}
}
}
struct CardContent:View{
var body: some View{
Text("some text")
}
}
struct BottomCard<Content:View>:View{
#Binding var cardShown:Bool
#Binding var cardDismissal:Bool
let content:Content
init(cardShown:Binding<Bool> , cardDismissal:Binding<Bool>, #ViewBuilder content: () -> Content){
_cardShown = cardShown
_cardDismissal = cardDismissal
self.content = content()
}
var body: some View{
ZStack{
//Dimmed
GeometryReader{ _ in
EmptyView()
}
.background(Color.red.opacity(0.2))
.opacity(cardShown ? 1 : 0)
.animation(.easeIn)
.onTapGesture {
cardShown.toggle()
}
// Card
VStack{
Spacer()
VStack{
content
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
but after pressing the button I don't see any pushed bottom menu. I checked and it seems that I have similar code to this video but on the video bottom sheet appears. Maybe I missed something important for menu showing. The main purpose is to show bottom menu with responsive height which will wrap elements and will be able to change menu height. I tried to use .sheet() but this element has stable height as I see. I know that from the ios 15+ we will have some solutions for this problem but I would like to create something more stable and convenient :)
iOS 16
We can have native SwiftUI resizable sheet (like UIKit). This is possible with the new .presentationDetents() modifier.
.sheet(isPresented: $showBudget) {
BudgetView()
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
Demo:
This is what I got when running your code
I got this after some adjustments to bottom card
struct BottomCard<Content:View>:View{
#Binding var cardShown:Bool
#Binding var cardDismissal:Bool
let content:Content
init(cardShown:Binding<Bool> , cardDismissal:Binding<Bool>, #ViewBuilder content: () -> Content){
_cardShown = cardShown
_cardDismissal = cardDismissal
self.content = content()
}
var body: some View{
ZStack{
//Dimmed
GeometryReader{ _ in
EmptyView()
}
.background(Color.red.opacity(0.2))
.animation(.easeIn)
.onTapGesture {
cardShown.toggle()
}
// Card
VStack{
Spacer()
VStack{
content
}
Spacer()
}
}.edgesIgnoringSafeArea(.all)
.opacity(cardShown ? 1 : 0)
}
}
So you just need to set the height!
what you want to do is to have a card that only exists when there is a certain standard met.
If you want to push up a card from the bottom then you can make a view of a card and put it at the bottom of a Zstack view using a geometry reader and then make a button that only allows for that card to exist when the button is pressed INSTEAD of trying to hire it by changing its opacity. Also, make sure you move the dismissal button to the inside of the cad you have.
Heres an example you can try :
struct SecondView: View {
#State var cardShown = false
var body: some View {
GeometryReader{
ZStack {
ZStack{
// I would also suggest getting used to physically making your
//button and then giving them functionality using a "Gesture"
Text("Show Button")
.background(Rectangle())
.onTapGesture{
let animation = Animation.spring()
withAnimation(animation){
self.cardShown.toggle
}
}
}
ZStack {
if cardShown == true{
BottomCard(cardShown: $cardShown) {
CardContent()
}
}
// here you can change how far up the card comes after the button
//is pushed by changing the "0"
.offset(cardShown == false ? geometry.size.height : 0)
}
}
}
}
}
Also, you don't need to have a variable for the card being shown and a variable for the card being dismissed. Just have one "cardShown" variable and make it so that when it is TRUE the card is shown and when it is FALSE (after hitting the button on the card or hitting the initial button again.) the card goes away.
iOS 16.0+
iPadOS 16.0+
macOS 13.0+
Mac Catalyst 16.0+
tvOS 16.0+
watchOS 9.0+
Use presentationDetents(_:)
struct ContentView: View {
#State private var isBottomSheetVisible = false
var body: some View {
Button("View Settings") {
isBottomSheetVisible = true
}
.sheet(isPresented: $isBottomSheetVisible) {
Text("Bottom Sheet")
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
}
}

SwiftUI sheet not animating dismissal on macOS Big Sur

I would like the sheet dismissal animated just like the appearance but reversed. I think this is also the standard behavior. You can see it in Xcode for example when you create a new file.
But as you can see it just disappears without animation
Here's my code:
struct ContentView: View {
#State var isAnotherViewPresented: Bool = false
var body: some View {
HStack {
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
}).sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
}
.frame(width: 500, height: 300, alignment: .center)
}
}
struct AnotherView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Close")
})
}.padding()
}
}
I'm on
Mac mini (M1, 2020)
macOS Big Sur 11.1 (20C69)
Xcode 12.3 (12C33)
But I can reproduce this on a
Mac mini (2018)
macOS Big Sur 11.0.1 (20B29)
Xcode 12.2 (12B45b)
I finally figured out how to do it, in my SwiftUI app it works if I do this while closing the sheet:
isSheetVisible = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
Example:
struct SheetView: View {
#Binding var isSheetVisible: Bool
var body: some View {
Button("Close") {
isSheetVisible = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
}
}
}
Disclaimer: I had/have the same problem where if I try to dismiss a sheet through a binding, it just disappears instead of having an animation. The below solution worked for me but I am unclear as to why its working.
Solution
Apparently the view you "attach" a modal to has an impact on how it transitions from being presented to not. For instance, in your code the sheet is attached to the button view:
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
// sheet is attached here
}).sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
When you call presentationMode.wrappedValue.dismiss() in the second view the modal jolts and disappears instead of sliding away. However, if you attach the sheet to the outer HStack view, then it works and it slides away as expected:
var body: some View {
HStack {
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
})
}
.frame(width: 500, height: 300, alignment: .center)
.sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
// sheet is now here
}
For me as long as the sheet wasn't attached to the button the animation worked. I don't know why this works but it did for me and hopefully it will for you as well.
I like this mix:
assuming you have:
#Environment(\.presentationMode) var presentationMode
then:
presentationMode.wrappedValue.dismiss() // this updates the binding from .sheet(isPresented: ...) to false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!) // this runs the animation

SwiftUI #State is updated only once when put inside NavigationView (macOS app)

Problem
I've put a simple button that increments a counter inside NavigationView, but the application appears to update #State value only once; later clicks are ignored. Switching between NavigationLinks will cause the view to update, but why it does not automatically after the counter did change?
Facts
otherView works fine after I put it outside the Navigation View.
Code
import SwiftUI
struct ContentView: View {
#State var counter = 0
var otherView: some View {
Button(action: {
counter += 1
}, label: {
Text(String(counter))
}).frame(maxWidth: .infinity, maxHeight: .infinity).padding()
}
var body: some View {
HStack {
NavigationView {
List {
NavigationLink("otherview", destination: otherView)
}.listStyle(SidebarListStyle())
}
}.frame(minWidth: 500, minHeight: 400)
}
}
Try to move the otherView into a seperate SwiftUI view like this:
struct ButtonView: View {
#State var counter = 0
var body: some View {
Button(action: {
counter += 1
}, label: {
Text(String(counter))
}).frame(maxWidth: .infinity, maxHeight: .infinity).padding()
}
}
Then in your ContentView call it like this:
struct ContentView: View {
var body: some View {
HStack {
NavigationView {
List {
NavigationLink("otherview", destination: ButtonView())
}.listStyle(SidebarListStyle())
}
}.frame(minWidth: 500, minHeight: 400)
}
}

SwiftUI if embedded View

So I have an embedded view in my MainView.Swift
VStack(alignment: .leading){
MediaPlayerView()
}.frame(height: 250)
What I would like to do is tell MediaPlayerView that if its frame height is 250 then show X content.
But if no frame height then show Z content.
What is the way to do this?
If full screen mode - I want the play pause button to show.
(Note images don't show play pause button as not yet coded.)
If not in full screen mode I want it to wrap the image and now playing info into a button which can call the MediaPlayerView - and hide the play pause button.
You can use GeometryReader to determine your view's height like this:
struct MediaPlayerView: View {
var body: some View {
GeometryReader { geo in
if geo.size.height == 250 {
Text("Height 250")
} else {
Text("Height something else")
}
}
}
}
although it would probably be better to pass a parameter to MediaPlayerView telling it what you'd like to show.
Something like this could work:
struct ContentView: View {
#State private var fullView = false
var body: some View {
VStack(alignment: .leading) {
MediaPlayerView(showFullView: self.fullView)
}
.frame(height: fullView ? 500 : 250)
.onTapGesture {
self.fullView.toggle()
}
}
}
struct MediaPlayerView: View {
let showFullView: Bool
var body: some View {
VStack {
if showFullView {
Text("Full view")
} else {
Text("logo only")
}
}
}
}