In SwiftUI, why will my buttons trigger if contained in a ZStack but not in an overlay on a VStack? - swift

Thanks in advance for any advice you can give. I have a custom dropdown menu that I have built in SwiftUI:
struct PhoneTypeDropdown: View {
let phoneTypes:[PhoneType] = [.Cell, .Work, .Landline]
#State var isExpanded: Bool
var body : some View {
VStack (spacing: 0) {
HStack {
Text("select type").font(.footnote)
Spacer()
Image(systemName: Constants.Design.Image.IconString.ChevronDown)
.resizable()
.frame(width: Constants.Design.Measurement.DropDownIconWidth,
height: Constants.Design.Measurement.DropDownIconHeight)
}
.padding(Constants.Design.Measurement.PadMin)
.background(Constants.Design.Colors.LightGrey)
.cornerRadius(Constants.Design.Measurement.CornerRadius)
.onTapGesture {
self.isExpanded.toggle()
}
if isExpanded {
VStack (spacing: 0){
ForEach(0 ..< phoneTypes.count) { i in
Button(self.phoneTypes[i].description, action: {
print("button tapped")
self.isExpanded.toggle()
})
.buttonStyle(DropDownButtonStyle())
if i != self.phoneTypes.count - 1 {
Divider()
.padding(.vertical, 0)
.padding(.horizontal, Constants.Design.Measurement.Pad)
.foregroundColor(Constants.Design.Colors.DarkGrey)
}
}
}.background(Constants.Design.Colors.LightGrey)
.cornerRadius(Constants.Design.Measurement.CornerRadiusMin)
.padding(.top, Constants.Design.Measurement.PadMin)
}
}.frame(width: Constants.Design.Measurement.PhoneTypeFieldWidth)
.cornerRadius(Constants.Design.Measurement.CornerRadius)
}
}
When I went to utilize this in my view, I had planned on using it as an overlay like this:
VStack(spacing: 0) {
ProfileField(text: phone1,
label: Constants.Content.PrimaryPhone,
position: .Justified)
.overlay(
PhoneTypeDropdown(isExpanded: expandDropdown1)
.padding(.top, 32) //FIXME: Fix this hard-coded value.
.padding(.trailing, Constants.Design.Measurement.PadMax),
alignment: .topTrailing
)
}
However, although the above code will trigger on clicking and expand the dropdown box, tapping any of the Button objects inside the dropdown box does nothing. I then tried to implement the dropdown box using a ZStack like this:
ZStack(alignment: .topTrailing) {
ProfileField(text: phone1,
label: Constants.Content.PrimaryPhone,
position: .Justified)
PhoneTypeDropdown(isExpanded: expandDropdown1)
.padding(.top, 32) //FIXME: Fix this hard-coded value.
.padding(.trailing, Constants.Design.Measurement.PadMax)
}
The dropdown box worked beautifully, expanding and collapsing as expected. However, now, when it expands, it pushes down the rest of the form instead of laying on top of the form as desired.
My question then is this: what would cause my button action to fire correctly when using the dropdown object in a ZStack as opposed to incorporating it in an overlay on an view in a VStack?

Related

SwiftUI: List, NavigationLink, and badges

I'm working on my first SwiftUI app, and in it would like to display a List of categories, with a badge indicating the number of items in that category. The title of the category would be on the left, and the badge would be right-aligned on the row. The list would consist of NavigationLinks so that tapping on one would drill further down into the view hierarchy. The code I've written to render the NavigationLinks looks like this:
List {
ForEach(myItems.indices) { categoryIndex in
let category = categories[categoryIndex]
let title = category.title
let fetchReq = FetchRequest<MyEntity>(entity: MyEntity(),
animation: .default)
NavigationLink(destination: MyItemView()) {
HStack(alignment: .center) {
Text(title)
Spacer()
ZStack {
Circle()
.foregroundColor(.gray)
Text("\(myItemsDict[category]?.count ?? 0)")
.foregroundColor(.white)
.font(Font.system(size: 12))
}
}
}
}
}
While it does render a functional NavigationLink, the badge is not displayed right-aligned, as I had hoped. Instead, it looks like this:
I know I'm getting hung up on something in my HStack, but am not sure what. How do I get it so that the category title Text takes up the majority of the row, with the badge right-aligned in the row?
SwiftUI doesn't know how big your Circle should be, so the Spacer doesn't do anything. You should set a fixed frame for it.
struct ContentView: View {
var body: some View {
List {
ForEach(0..<2) { categoryIndex in
let title = "Logins"
NavigationLink(destination: Text("Hi")) {
HStack(alignment: .center) {
Text(title)
Spacer()
ZStack {
Circle()
.foregroundColor(.gray)
.frame(width: 25, height: 25) // here!
Text("5")
.foregroundColor(.white)
.font(Font.system(size: 12))
}
}
}
}
}
}
}
Result:

SwiftUI: Overlay is on the whole VStack instead of the textfield after wrapping TextFields with VStack

Here's the simplified code:
var body: some View {
Section(header: Text("Personal Data").position(x:45, y: 17)) {
Form {
VStack {
TextField("Title", text: self.$title)
.disabled(true)
.overlay(
Button("", action: {
self.showTitles = true
}))
.popover(isPresented: self.$showTitles, attachmentAnchor: .point(.bottom)) { EmptyView() }
Divider()
TextField("Name", text: $firstName)
Divider()
TextField("Last Name", text: $lastName)
}
.padding()
.padding(.bottom, -7)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(Color.secondary, lineWidth: 1.0)
.opacity(0.3)
)
}
}
}
It was working as I wanted before adding the VStack although I need it to be able to place Dividers. I've tried to wrap the single TextField with a VStack and also used Group to see if I can only have button overlay on the the first TextField but nothing seems to work. Do I have to use GeometryReader for this purpose?
I'd appreciate it if anyone can provide some insights.
This is a little bit tricky:
Understanding the issue:
SwiftUI automatically detects the action of the default button and uses it as the cell action. So the popover will come as the popover of the entire list too (somehow 😁)
Solution
So you need to change the buttonStyle to something else than default
TextField("Title", text: self.$title)
.disabled(true)
.overlay(
Button(action: ( { showTitles.toggle() } ),
label: ( { Text("").frame(maxWidth: .infinity, maxHeight: .infinity) } )
).buttonStyle(BorderlessButtonStyle())
)
.popover(isPresented: self.$showTitles, attachmentAnchor: .point(.bottom)) { EmptyView() }
Note that how I scale the button to fill the entire space

SwiftUI: Two buttons with the same width/height

I have 2 buttons in an H/VStack. Both of them contain some text, in my example "Play" and "Pause". I would like to have that both buttons have the same width (and height) determined by the largest button. I have found some answers right here at SO but I can't get this code working unfortunately.
The following code illustrates the question:
import SwiftUI
struct ButtonsView: View {
var body: some View {
VStack {
Button(action: { print("PLAY tapped") }){
Text("Play")
}
Button(action: { print("PAUSE tapped") }) {
Text("Pause")
}
}
}
}
struct ButtonsView_Previews: PreviewProvider {
static var previews: some View {
ButtonsView()
}
}
The tvOS preview from Xcode shows the problem:
I would be thankful for an explanation for newbies 🙂
Here is run-time based approach without hard-coding. The idea is to detect max width of available buttons during drawing and apply it to other buttons on next update cycle (anyway it appears fluently and invisible for user).
Tested with Xcode 11.4 / tvOS 13.4
Required: Simulator or Device for testing, due to used run-time dispatched update
struct ButtonsView: View {
#State private var maxWidth: CGFloat = .zero
var body: some View {
VStack {
Button(action: { print("PLAY tapped") }){
Text("Play")
.background(rectReader($maxWidth))
.frame(minWidth: maxWidth)
}.id(maxWidth) // !! to rebuild button (tvOS specific)
Button(action: { print("PAUSE tapped") }) {
Text("Pause Long Demo")
.background(rectReader($maxWidth))
.frame(minWidth: maxWidth)
}.id(maxWidth) // !! to rebuild button (tvOS specific)
}
}
// helper reader of view intrinsic width
private func rectReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { gp -> Color in
DispatchQueue.main.async {
binding.wrappedValue = max(binding.wrappedValue, gp.frame(in: .local).width)
}
return Color.clear
}
}
}
You can implement the second custom layout example in the WWDC 2022 talk https://developer.apple.com/videos/play/wwdc2022/10056/ titled "Compose custom layouts with SwiftUI" which, if I understand the question, specifically solves it, for an arbitrary number of buttons/subviews. The example starts at the 7:50 mark.
after reading hit and trial implementing SO solns etc finally resolved this issue posting so that newbies as well as intermediate can benefit
paste it and obtain equal size(square) views
VStack(alignment: .center){
HStack(alignment:.center,spacing:0)
{
Button(action: {}, label: {
Text("Button one")
.padding(35)
.foregroundColor(.white)
.font(.system(size: 12))
.background(Color.green)
.frame(maxWidth:.infinity,maxHeight: .infinity)
.multilineTextAlignment(.center)
.cornerRadius(6)
}).background(Color.green)
.cornerRadius(6)
.padding()
Button(action: {}, label: {
Text("Button two")
.padding(35)
.foregroundColor(.white)
.font(.system(size: 12))
.frame(maxWidth:.infinity,maxHeight: .infinity)
.background(Color.green)
.multilineTextAlignment(.center)
}) .background(Color.green)
.buttonBorderShape(.roundedRectangle(radius: 8))
.cornerRadius(6)
.padding()
}.fixedSize(horizontal: false, vertical: true)
Add as many as buttons inside it. You can adjust it for VStack by adding only one button in hstack and add another button in another Hstack. I gave a general soln for both VStack and Hstack. You can also adjust padding of button as .padding(.leading,5) .padding(.top,5) .padding(.bottom,5) .padding(.trailing,5) to adjust the gaps between buttons
I think the best solution is to use GeometryReader, which resizes the width of the content of the Button. However, you need to check that you set a width of the Wrapper around the GeometryReader, because otherwise it would try to use the full screen width. (depends where you use that view, or if it is your primary view)
VStack
{
GeometryReader { geo in
VStack
{
Button(action: { print("PLAY tapped") }){
Text("Play")
.frame(width: geo.size.width)
}
.border(Color.blue)
Button(action: { print("Pause tapped") }){
Text("PAUSE")
.frame(width: geo.size.width)
}
.border(Color.blue)
}
}
}
.frame(width: 100)
.border(Color.yellow)
... which will look like that.
What happens if you put a Spacer() right after the Text("Play")? I think that might stretch out the 'Play' button.
Or maybe before and after Text("Play").

Modal picker not scrolling right SwiftUI

I created a modal but it seems to have a bug on the selection. When scrolling the left, it scrolls the right, I have to go to the very edge of the left to be able to scroll, this is how it looks:
import SwiftUI
struct ContentView: View {
#State var showingModal = false
#State var hours: Int = 0
#State var minutes: Int = 0
var body: some View {
ZStack {
VStack {
Button("Show me"){
self.showingModal = true
}
if $showingModal.wrappedValue {
VStack(alignment: .center) {
ZStack{
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.vertical)
// this one is it
VStack(spacing: 20) {
Text("Time between meals")
.bold().padding()
.frame(maxWidth: .infinity)
.background(Color.yellow)
.foregroundColor(Color.white)
HStack {
Spacer()
VStack {
Picker("", selection: $hours){
ForEach(0..<4, id: \.self) { i in
Text("\(i) hours").tag(i)
}
}
.frame(width: 150, height: 120)
.clipped()
}
VStack {
Picker("", selection: $minutes){
ForEach(0..<60, id: \.self) { i in
Text("\(i) min").tag(i)
}
}
.frame(width: 150, height: 120)
.clipped()
}
}
Spacer()
Button(action: {
self.showingModal = false
}){
Text("Close")
} .padding()
}
.frame(width:300, height: 300)
.background(Color.white)
.cornerRadius(20).shadow(radius: 20)
}
}
}
}
}
}
}
How can I fix that little bug? I tried playing around with the layout but no use... any help would be appreciated
What if I told you the reason your Picker not working was this line?
.cornerRadius(20).shadow(radius: 20)
Unfortunately, SwiftUI is still quite buggy and sometimes it doesn't do what it is supposed to do and especially Pickers are not that reliable. I guess we'll need to wait and see the next iteration of SwiftUI, but for now you can replace that line with the code below:
.mask(RoundedRectangle(cornerRadius: 20))
.shadow(radius: 20)
There are just modifiers which affect all view hierarchy (ie. all subviews) that can change resulting layout/presentation/behaviour. And .cornerRadius and .shadow are such type modifiers.
The solution is to apply (as intended) those modifiers only to entire constructed view, and here it is
.compositingGroup() // <<< fix !!
.cornerRadius(20).shadow(radius: 20)
where .compositionGroup is intended to make above view hierarchy flat rendered and all below modifiers applied to only to that flat view.

Why NavigationLink background is grey?

I'm following SwiftUI tutorial to create my first app with SwiftUI but on TVOs and I don't know why but the scroll view item are with background color in grey, and when they are focussed they become white.
I don't find the way to change the color...
Can you help me please ?
My code :
struct LigneCategorie: View {
var nomCategorie: String
var items: [Recette]
var body: some View {
VStack(alignment: .leading) {
Text(self.nomCategorie)
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top) {
ForEach(self.items) { recette in
NavigationLink(
destination: RecetteDetail(
recette: recette
)
) {
CategoryItem(recette: recette)
}
}
}
}
.frame(height: 185)
}
}
}
struct CategoryItem: View {
var recette: Recette
var body: some View {
VStack(alignment: .leading) {
recette.image
.renderingMode(.original)
.resizable()
.frame(width: 200, height: 150)
.cornerRadius(10)
Text(recette.name)
.foregroundColor(.primary)
.font(.caption)
}
.padding(.leading, 15)
}
}
Thank you
You can set your school view's background in a preview side. when you click your scroll view you would see a chosen scroll view in preview mode Font,color and similar options must to be appear. But if not, then try this it should be useful.
ScrollView(.horizontal, showsIndicators: false) {
}.color(.gray)