SwiftUI Size Struct by .frame modifier - swift

Hello I got a Custom Button Struct which has an Image and a Text and also a Style for that Button, which is added at the end of the Struct. My Question is: Is there any way I can use the .frame(width: , height: ) and .frame(minWidth: maxWidth) modifier, without passing a variable to the struct which then passes it to the button style?
CustomButton:
struct CustomButtonWithImage: View {
let title: String
let image: String
let action: () -> Void
var body: some View {
Button(action: action) {
HStack {
Image(image)
.resizable()
.renderingMode(.template)
.frame(maxWidth: imageSize.width, maxHeight: imageSize.height)
.aspectRatio(contentMode: .fit)
Text(title)
.font(.system(size: fontSize))
.bold()
.minimumScaleFactor(0.8)
.lineLimit(1)
}
.padding(.horizontal, horizontalPadding)
}
.buttonStyle(CustomButtonStyle(size: buttonSize)) // <- Button Style
.accentColor(Color.white)
}
}
struct CustomButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.contentShape(Rectangle())
.foregroundColor(Color.white)
.accentColor(Color.white)
.background(Color("CustomButtonColor"))
}
}
Basically I just want to Do this:
var body: some View {
VStack {
CustomButton(text: "hello": image("someImage.png") { print("hello") }
// Is there a way to set this without passing the struct a var or let?
// I can set this but this wont make the button bigger, because the label in the button style wont change size with this
.frame(width: 40, height: 50)
.frame(maxWidth: ..., maxHeight: ...)
}
}
}

Remove hardcoded frame and add max allowed, so it will consume available space shrank only by other views layout. Also you can use only one, either maxWidth or maxHeight restriction if needed.
CustomButton(text: "hello": image("someImage.png") { print("hello") }
// .frame(width: 40, height: 50) // remove hardcode
.frame(maxWidth: .infinity, maxHeight: .infinity) // << max allowed

Related

How do I make an HStack of SwiftUI buttons have height?

I have a simple SwiftUI row of buttons to mimic a keyboard layout. Just can't get the buttons to fill the height of the HStack. I can't tell where I need to give something height so it will look like a keyboard. No matter where I try to add height, the buttons still are small.
struct KeyboardView: View {
let rows = [String.keyboardFKeyRow, String.keyboardNumKeyRow, String.alpha1KeyRow, String.alpha2KeyRow, String.alpha3KeyRow, String.arrowKeyRow]
var body: some View {
VStack() {
KeyRow(labels: String.keyboardFKeyRow).frame(height: 100)
KeyRow(labels: String.keyboardNumKeyRow).frame(height: 100)
KeyRow(labels: String.alpha1KeyRow).frame(height: 100)
KeyRow(labels: String.alpha2KeyRow).frame(height: 100)
KeyRow(labels: String.alpha3KeyRow).frame(height: 100)
KeyRow(labels: String.arrowKeyRow).frame(height: 100)
}
}
}
struct KeyRow: View {
var labels: [String]
var body: some View {
HStack() {
Spacer()
ForEach(labels, id: \.self) { label in
Button {
print("Key Clicked")
} label: {
Text(label.capitalized)
.font(.body)
.frame(minWidth: 60, maxWidth: 80, minHeight: 80, maxHeight: .infinity)
}
}
Spacer()
}
}
}
The .bordered button style on macOS has a hard-coded size and refuses to stretch vertically. If you need a different button height, either use the .borderless style or create your own ButtonStyle or PrimitiveButtonStyle, and (either way) draw the background yourself. For example, I got this result:
from this playground:
import PlaygroundSupport
import SwiftUI
PlaygroundPage.current.setLiveView(HStack {
// Default button style, Text label.
Button("a", action: {})
.background { Rectangle().stroke(.mint.opacity(0.5), lineWidth: 1).clipped() }
.frame(maxHeight: .infinity)
// Default button style, vertically greedy Text label.
Button(action: {}, label: {
Text("b")
.frame(maxHeight: .infinity)
})
.background { Rectangle().stroke(.mint.opacity(0.5), lineWidth: 1).clipped() }
.frame(maxHeight: .infinity)
Button(action: {}, label: {
Text("c")
.fixedSize()
.padding()
.frame(maxHeight: .infinity)
.background {
RoundedRectangle(cornerRadius: 10)
.fill(Color(nsColor: .controlColor))
}
}).buttonStyle(.borderless)
.background { Rectangle().stroke(.mint.opacity(0.5), lineWidth: 1).clipped() }
.frame(maxHeight: .infinity)
}
.background { Rectangle().stroke(.red.opacity(0.5), lineWidth: 1).clipped() }
.frame(height: 100)
)

How to render avoiding safeareaview?

I want the status bar and at the bottom to be white (Same as root background color), but no idea, do i need to get status bar height and add margin top and bottom?
Here is my code and the preview below
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(
alignment: .leading,
spacing: 10
) {
Text("Title")
.font(
.system(size: 32)
.weight(.heavy)
)
Text("Content")
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(
EdgeInsets(
top: 0,
leading: 20,
bottom: 0,
trailing: 20
)
)
.background(Color.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
If it were me tackling this kind of UI, I would use some other nice Views that SwiftUI provides for us (like ZStack).
The ZStack places objects one on top of another from the bottom up. So you would want your color first, then the VStack after. It would look something like this:
struct ContentView: View {
var body: some View {
ZStack {
Color.gray
VStack(alignment: .leading, spacing: 10) {
Text("Title")
.font(.system(size: 32).weight(.heavy))
Text("Content")
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
add a vertical padding of 1:
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Title")
.font(.system(size: 32).weight(.heavy))
Text("Content")
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(.horizontal, 20)
.background(Color.gray)
.padding(.vertical, 1) // here
}
}
Let me add another answer that goes with a different approach, without using .frame(). Instead, it uses the full width and height of HStack and VStack to fill the screen. For the status bar and the bottom area, this approach uses a .layoutPriority() modifier to the gray color but not allowing it to overlap the safe area.
While the other answers work quite fine, my purpose with this example is to open the range of possibilities.
struct Example: View {
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 10) {
Text("Title")
.font(
.system(size: 32)
.weight(.heavy)
)
Text("Content")
Spacer() // This spacer will extend the VStack to full height
}
Spacer() // This spacer will extend the HStack to full width
}
.padding()
.background {
VStack {
// Status bar
Color.clear
.ignoresSafeArea()
// Rest of the view: gray has the priority but can't overlap
// the status bar
Color.gray
.layoutPriority(1)
}
}
}
}

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.

SwiftUI: How do I overlay an image in the center of the parent view?

I have an image that I'm placing over an existing view. It is flush left/leading but I want it centered on the screen (or the salmon colored area).
This is my code:
struct LeaderBoard: View {
#State var shouldDisplayOverLay = true
var body: some View {
ZStack {
VStack(alignment: .center) {
Text("Daily Dashboard")
.font(.subheadline)
Color.darkSalmon
Spacer()
HStack {
Text("Exercise")
Text("Sleep (in hours):")
Text("Weight") // red if lower more than a pound, yellow if less than pound, green if same or higher
}
.font(.caption)
Spacer()
}
Image("Leader-1024")
.resizable()
.scaledToFit()
.modifier(InsetViewModifier())
}
}
}
struct InsetViewModifier: ViewModifier {
func body(content: Content) -> some View {
GeometryReader { proxy in
content
.frame(width: proxy.size.width * 0.8, height: proxy.size.height * 0.8, alignment: .center)
}
}
}
GeometyReader does not have own alignment, so the solution can be to embed some stack in modifier (because all of them have .center alignment by default):
struct InsetViewModifier: ViewModifier {
func body(content: Content) -> some View {
GeometryReader { proxy in
VStack {
content
.frame(width: proxy.size.width * 0.8, height: proxy.size.height * 0.8, alignment: .center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
and some replicated demo

How to show or hide an overlay at a Text or Button

I do have a few items that I need to hide ( not disable ) or show depending on a value.
As for a Text() or BUtton() sample, I need to have a overlay or no overlay.
Button("how secret?", action: {
self.secretOverlay = true
})
.overlay( TopSecretOverlayView()
....
})
I did try something like
struct TopSecretOverlayView: View {
var body: some View {
HStack {
if secretOverlay {
Text("Top Secret")
.bold()
.font(.system(size: 64))
.frame(width: 350, height: 80, alignment: .center)
} else {
....
}
}
}
}
.presentation is deprecated. Not sure if that was the way.
But how should I switch a overlay between hidden and visable?
Where should an if statement look like?
As always, thank you!
Have you tried this:
Button("how secret?") {
showSecretOverlay = true
}
.overlay(TopSecretOverlayView().opacity(showSecretOverlay ? 1 : 0))
Another way of handling it is with a view builder:
struct SecretView: View {
#State private var showSecretOverlay = false
var body: some View {
Button("show password?") {
showSecretOverlay.toggle()
}
.overlay(secretOverlay)
}
#ViewBuilder private var secretOverlay: some View {
if showSecretOverlay {
TopSecretOverlayView()
}
}
}
struct TopSecretOverlayView: View {
var body: some View {
Text("password")
.foregroundColor(.white)
.background(.black)
}
}
struct SecretView_Previews: PreviewProvider {
static var previews: some View {
SecretView()
}
}
If the button tap will always have the same action, then how about setting the opacity to 0 if you want it invisible and a 1 if you want it visible?
I know this may be a little old but it seems like you could probably do what you want using an ObservedObject passed into the view.
struct TopSecretOverlayView: View {
#ObservedObject var secretVM: SecretViewModel
var body: some View {
HStack {
switch secretVM.accessType {
case .top:
Text("Top Secret")
.bold()
.font(.system(size: 64))
.frame(width: 350, height: 80, alignment: .center)
case .middle:
Text("Secret")
.bold()
.font(.system(size: 64))
.frame(width: 350, height: 80, alignment: .center)
case .low:
Text("Common Knowledge")
.bold()
.font(.system(size: 64))
.frame(width: 350, height: 80, alignment: .center)
default:
Text("Access Denied")
.bold()
.font(.system(size: 64))
.frame(width: 350, height: 80, alignment: .center)
}
}
}
}
Then where ever you change the accessType property it should cause it to update the overlay.
Button("how secret?", action: {
self.secretVM.accessType = .top
})
.overlay( TopSecretOverlayView(secretVM:self.secretVM)
....
})