SwiftUI: Two buttons with the same width/height - swift

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").

Related

VStack children to fill (not expand) its parent

i am trying to to make the button of an alert view fit the parent VStack. But I can only see two options:
button width as is, no frame modifier. that is not ideal as the button is not wide enough
set the frame modifier to .frame(maxWidth: .infinity). that is not ideal, because it not also fills its parent, but also makes it extend to the edges of the screen.
What I actually want is, that the VStack stays at its width and the button just fills up to the edges. No extending of the VStack. The size of the VStack is defined by the title and message, not by the button. Is this possible to achieve with SwiftUI?
Code:
Color.white
.overlay(
ZStack {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
VStack(spacing: 15) {
Text("Alert View")
.font(.headline)
Text("This is just a message in an alert")
Button("Okay", action: {})
.padding()
.frame(maxWidth: .infinity)
.background(Color.yellow)
}
.padding()
.background(Color.white)
}
)
As alluded to in the comments, if you want the width to be tied to the message size, you'll have to use a PreferenceKey to pass the value up the view hierarchy:
struct ContentView: View {
#State private var messageWidth: CGFloat = 0
var body: some View {
Color.white
.overlay(
ZStack {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
VStack(spacing: 15) {
Text("Alert View")
.font(.headline)
Text("This is just a message in an alert")
.background(GeometryReader {
Color.clear.preference(key: MessageWidthPreferenceKey.self,
value: $0.frame(in: .local).size.width)
})
Button("Okay", action: {})
.padding()
.frame(width: messageWidth)
.background(Color.yellow)
}
.padding()
.background(Color.white)
}
.onPreferenceChange(MessageWidthPreferenceKey.self) { pref in
self.messageWidth = pref
}
)
}
struct MessageWidthPreferenceKey : PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
}
I'd bet that there are scenarios where you would also want to set a minimum width (like if the alert message were one word long), so a real-world application of this would probably use max(minValue, messageWidth) or something like that to account for short messages.

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

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?

Problem with SwiftUI: Vertical ScrollView with Button

I'm quite new to programming so please excuse any dumb questions. I'm trying to make a ScrollView with the content being buttons. Although the button prints to console, when shown in the simulator the button displays as a large blue rectangle rather than displaying the image I would like it to.
Code Regarding ScrollView:
struct ContentView: View {
var body: some View {
[Simulator Display][1]
VStack {
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.leading, 50)
.padding(.trailing, 50)
.padding(.top, 20)
.padding(.bottom, -20)
Spacer()
ScrollView {
VStack(spacing: 20) {
Button(action: {
//ToDo
print("Executed")
}) {
Image("Logo")
}
}
}
}
}
}
Simulator Display:
Image(Placeholder for now) I want to be displayed:
So I tried it an yeah it was very weird. Anyway, here is an example of how you can include the image. Just take the portion of the Button and paste it
struct ContentView: View {
var body: some View {
ZStack {
Button(action: {
print("button pressed")
}) {
Image("image")
.renderingMode(.original)
}
}
}
}

SwiftUI List/Form/ScrollView being clipped when offset

I changed the y offset of a list, and now it's being clipped.
I am trying to make it so that when you scroll, you can partially see the text underneath the Title and buttons at the top of the view. In other words, I want the top section of the screen to be slightly transparent.
I added the offset to the list, so that it didn't overlap with the information at the top.
The image above is with the VStack in my code showing. I thought that the VStack might be getting in the way, so I commented it out and the image below was the result:
Here's my code:
var body: some View {
ZStack {
VStack {
HStack {
Button(action: {self.showAccountView.toggle()}) {
Image(systemName: "person.fill")
.renderingMode(.original)
.font(.system(size: 20, weight: .bold))
.frame(width: 44, height: 44)
.modifier(NavButtons())
}
.sheet(isPresented: $showAccountView) {
AccountView()
}
Spacer()
Button(action: { self.showHelpCenter.toggle()}) {
Image(systemName: "questionmark")
.renderingMode(.original)
.font(.system(size: 20, weight: .bold))
.modifier(NavButtons())
}
.sheet(isPresented: $showHelpCenter) {
HelpCenter()
}
}
.frame(maxWidth: .infinity)
.padding(.horizontal)
Spacer()
}
List {
ForEach (store.allLogs) { thing in
VStack (alignment: .leading) {
HStack {
Text("\(thing.date) , \(thing.time)")
}
Text(thing.notes)
.multilineTextAlignment(.leading)
}
}
}.offset(y: 50)
}
}
EDIT:
This is one possible solution:
struct MyList: UIViewRepresentable {
func makeUIView(context: Context) -> UITableView {
let view = UITableView()
view.clipsToBounds = false
return view
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
}
and then you would use makeUIView and updateUIView to update the cells. This is just messy and it's not really using SwiftUI at that point.
Second Edit
I've found this issue with a scrollView as well as a form:
The black is the background. Here's the code for all three:
Group {
List ((0 ... 10), id: \.self) {
Text("Row \($0)")
}
.offset(y: 200)
.border(Color.blue, width: 3)
.background(Color.black)
ScrollView {
Text("Text")
}
.foregroundColor(.white)
.offset(y: 200)
.border(Color.blue, width: 3)
.background(Color.black)
Form {
Text("Text")
}
.offset(y: 200)
.border(Color.blue, width: 3)
.background(Color.black)
}
Here are the wireframes of a List:
Here are the names of frames that are the same height as the List/Form/ScrollView:
List:
PlainList.BodyContent
ListCore.Container
ListRepresentable
View Host
TableWrapper
UpdateCoalescingTableView
Form:
GroupList.BodyContent
ListCore.Container
ListRepresentable
View Host
TableWrapper
UpdateCoalescingTableView
ScrollView:
ScrollViewBody
SystemScrollView
View Host
HostingScrollView
I guess that my question has changed from "how do I do this..." to "Why is this happening?"
I'm pretty confused about what exactly is going on.
UIScrollView.appearance().clipsToBounds = false
Stick this in the body of AppDelegate.swift -> applicationDidFinishLaunchingWithOptions(). It will make all scroll views across your application unclipped by default.
The problem is that a list has a table view (which is a scroll view) underneath. Scroll views by default are clipped. We just need to change that default.
Using LazyHStack instead of HStack solves the clipping problem.

How can I fit a shape in swift ui to accommodate the length and width of a text view

ZStack {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.red)
.scaledToFit() //.frame(width: 200, height: 25)
HStack {
Image(systemName: "tag.fill")
.foregroundColor(.white)
Text("Tickets Not Available")
.font(.headline)
.foregroundColor(.white)
.fixedSize(horizontal: true, vertical: false)
}
}
.scaledToFit()
As you can see my views are placed in a zstack so that the rounded rectangle can be the background of the text view. I've tried so many different things like where to put the .scaledtofit and it just gives me wack results each time.
is this what you're after (note the Image.resizable):
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 8).foregroundColor(.blue)
HStack{
Image(systemName: "tag.fill").resizable().padding(4).foregroundColor(.white).scaledToFit()
Text("Get Tickets").font(.headline).foregroundColor(.white)
}
}.fixedSize()
}
The question is a bit unclear but if you are trying to fit a shape inside the text view, and you are fine with getting rid of scaledToFit, then the code should be:
RoundedRectangle(cornerRadius: 8).foregroundColor(.red).frame(width: textView.width, height: textView.height)
Hope this helps, and hopefully you didn't need to use scaledToFit.
If you did tell me in comments.
A reusable ButtonStyle might be helpful here. Instead of a ZStack, using the .background modifier helps to keep the size of the Button contents:
struct RoundedButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
configuration.label
.font(.headline)
.padding()
.background(RoundedRectangle(cornerRadius: 8).foregroundColor(Color.blue))
}
}
}
Usage example:
Button(
action: {
print("Button Tapped")
},
label: {
HStack {
Image(systemName: "tag.fill")
Text("Tickets")
}
}
)
.buttonStyle(RoundedButtonStyle())