I am trying to make a chat app, but having difficulties with the HStack. The border I have is going over the edge of the view so the full rounded rectangle isn't shown.
The code I have for the TextField and HStack is this.
var body: some View {
HStack {
CustomTextField(placeholder: Text("Enter your message"), text: $message)
Button {
messagesManager.sendMessage(message: message)
message = ""
} label: {
Image(systemName: "arrow.up.circle.fill")
.foregroundColor(.blue)
.padding(10)
.scaledToFit()
}
}
.padding(.horizontal)
.padding(.vertical, 0)
.border(.gray)
.cornerRadius(20)
}
struct CustomTextField: View {
var placeholder: Text
#Binding var text: String
var editingChanged: (Bool) -> () = {_ in}
var commit: () -> () = {}
var body: some View {
ZStack(alignment: .leading) {
if text.isEmpty {
placeholder
.opacity(0.5)
}
TextField("", text: $text, onEditingChanged: editingChanged, onCommit: commit)
}
}
}
I can't find out how to reduce the length of the HStack so the whole of the rounded rectangle will show, as well as increasing the size of the button to fill up more space within the shape.
You can use overlay with RoundedRectangle for the border with a corner radius. I have added leading padding to your CustomTextField so that it will look proper and horizontal padding to your HStack after adding overlay.
var body: some View {
HStack {
CustomTextField(placeholder: Text("Enter your message"), text: $message)
.padding(.leading, 10)
Button {
//messagesManager.sendMessage(message: message)
message = ""
} label: {
Image(systemName: "arrow.up.circle.fill")
.foregroundColor(.blue)
.padding(10)
.scaledToFit()
}
}
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(.gray)
)
.padding(.horizontal)
}
Preview
Related
I'm struggling with a view where I want to have multiple pickers embedded in
other views. When I wrap the pickers in a Form, I get the desired behavior for the
picker but there is a lot of extra space around the pickers that I can't seem to
automatically adjust.
This is an example - the space in the red outline seems to be determined by the other
view elements not the size of the picker.
I can, of course, hard-code a frame height for the Form but that is trial and error
and would only be specific to the device and orientation. I have tried multiple
versions of Stacks inside Stacks with padding, GeometryReader etc, but I have not come up with any
solution. As an aside, I DO want the picker labels, otherwise I could just remove
the Form.
I also tried setting UITableView.appearance().tableFooterView in an init() but that did not work either.
Here is a simplified version:
struct ContentView4: View {
#State var selectedNumber1: Int = 1
#State var selectedNumber2: Int = 2
#State var selectedNumber3: Int = 3
var body: some View {
NavigationView {
VStack(alignment: .leading) {
HStack {
Spacer()
Text("Compare up to 3")
.font(.caption)
Spacer()
}//h
Form {//for pickers
Picker(selection: $selectedNumber1, label: Text("A")) {
ForEach(0..<10) {
Text("\($0)")
}
}//picker
Picker(selection: $selectedNumber2, label: Text("B")) {
ForEach(0..<10) {
Text("\($0)")
}
}//picker
Picker(selection: $selectedNumber3, label: Text("C")) {
ForEach(0..<10) {
Text("\($0)")
}
}//picker
}//form for pickers
.padding(.horizontal, 10)
//.frame(height: 200) //don't want to hard code this
VStack(alignment: .leading) {
HStack {
Text("A")
.frame(width: 100)
Text("B")
.frame(width: 100)
Text("C")
.frame(width: 100)
}
.padding(.horizontal, 10)
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading){
Text("A title line")
.font(.headline)
.padding(.vertical, 5)
HStack {
Text("Number")
.frame(width: 100)
Text("Number")
.frame(width: 100)
Text("Number")
.frame(width: 100)
}
Text("Another title line")
.font(.headline)
.padding(.vertical, 5)
HStack {
Text("Something")
.frame(width: 100)
Text("Something")
.frame(width: 100)
Text("Something")
.frame(width: 100)
}
Text("A Third title line")
.font(.headline)
.padding(.vertical, 5)
HStack {
Text("More")
.frame(width: 100)
Text("More")
.frame(width: 100)
Text("More")
.frame(width: 100)
}
}
}//scroll
.padding(.horizontal, 10)
}
.navigationBarTitle("Compare Three", displayMode: .inline)
}
}//nav
}//body
}//struct
Interestingly, I am able to get a solution by removing the form and wrapping each
picker in a menu, like this:
Menu {
Picker(selection: $selectedNumber2, label: EmptyView()) {
ForEach(0..<10) {
Text("\($0)")
}
}//picker
} label: {
HStack {
Text("B")
Spacer()
Image(systemName: "chevron.right")
.resizable()
.frame(width: 14, height: 14)
}//h
}//menu label
However, I still like the look of the Form better if I could automatically configure
the space around the Form items.
Any guidance would be appreciated. Xcode 13.4, iOS 15.5
Form (and List) is not meant to be stacked inside other views like this, which is why it has such strange behavior.
Thankfully, it's fairly simple to recreate the stuff you do want using NavigationLink. Here’s a quick example of a couple custom views that do just that:
// drop-in NavigationLink replacement for Picker
struct NavigationButton<Content: View, SelectionValue: Hashable> : View {
#Binding var selection: SelectionValue
#ViewBuilder let content: () -> Content
#ViewBuilder let label: () -> Text
var body: some View {
NavigationLink {
PickerView(selection: $selection, content: content, label: label)
} label: {
HStack {
label()
Spacer()
Text(String(describing: selection))
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
}
.buttonStyle(NavigationLinkButtonStyle())
}
}
// subview for the Picker page, which lets us use `dismiss()`
// to pop the subview when the user selects an option
struct PickerView<Content: View, SelectionValue: Hashable> : View {
#Binding var selection: SelectionValue
#ViewBuilder let content: () -> Content
#ViewBuilder let label: () -> Text
#Environment(\.dismiss) private var dismiss
var body: some View {
Form {
Picker(selection: $selection, content: content, label: label)
.pickerStyle(.inline)
.labelsHidden()
.onChange(of: selection) { _ in
dismiss()
}
}
.navigationTitle(label())
}
}
// recreate the appearance of a List row
struct NavigationLinkButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.label
.frame(maxWidth: .infinity)
Image(systemName: "chevron.right")
.font(.footnote.bold())
.foregroundColor(Color(UIColor.tertiaryLabel))
}
.padding()
.background(
Rectangle()
.fill(configuration.isPressed ? Color(UIColor.quaternaryLabel) : Color(UIColor.systemBackground))
)
}
}
If you like the .insetGrouped style you got using Form, we can replicate that by putting NavigationButton inside a clipped VStack:
VStack(spacing: 0) {
NavigationButton(selection: $selectedNumber1) {
ForEach(0..<10) {
Text("\($0)")
}
} label: {
Text("A")
}
Divider()
NavigationButton(selection: $selectedNumber2) {
ForEach(0..<10) {
Text("\($0)")
}
} label: {
Text("B")
}
}
.clipShape(RoundedRectangle(cornerRadius: 11))
.padding()
.background(Color(UIColor.systemGroupedBackground))
And here’s a screenshot showing my custom views above your original Form.
(And if you like Picker as a popup menu, you could use Menu instead of NavigationLink)
I have a header that is fixed in place using an offset relative to the scroll position. Strangely enough though, when the contents of the scroll view has a dynamic opacity to the buttons, the offset is very jumpy:
This is the scroll view code, the HeaderView is "fixed" in place by pinning the offset to the scroll view's offset. The opacity seems to be causing the performance issue is on the MyButtonStyle style on the last line of code:
struct ContentView: View {
#State private var isPresented = false
#State private var offsetY: CGFloat = 0
#State private var headerHeight: CGFloat = 200
var body: some View {
GeometryReader { screenGeometry in
ZStack {
Color(.label)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 0.0) {
Color.clear
.frame(height: headerHeight)
.overlay(
HeaderView(isPresented: $isPresented)
.offset(y: offsetY != 0 ? headerHeight + screenGeometry.safeAreaInsets.top - offsetY : 0)
)
VStack(spacing: 16) {
VStack(alignment: .leading) {
ForEach(0...10, id: \.self) { index in
Button("Button \(index)") {}
.buttonStyle(MyButtonStyle(icon: Image(systemName: "alarm")))
}
}
Spacer()
}
.frame(maxWidth: .infinity, minHeight: screenGeometry.size.height)
.padding()
.background(
GeometryReader { geometry in
Color.white
.cornerRadius(32)
.onChange(of: geometry.frame(in: .global).minY) { newValue in
offsetY = newValue
}
}
)
}
}
}
.alert(isPresented: $isPresented) { Alert(title: Text("Button tapped")) }
}
}
}
struct HeaderView: View {
#Binding var isPresented: Bool
var body: some View {
VStack {
Image(systemName: "bell")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(Color(.systemBackground))
Button(action: { isPresented = false }) {
Text("Press")
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(16)
}
}
.padding()
}
}
struct MyButtonStyle: ButtonStyle {
let icon: Image
func makeBody(configuration: Configuration) -> some View {
Content(
configuration: configuration,
icon: icon
)
}
struct Content: View {
let configuration: Configuration
let icon: Image
var body: some View {
HStack(spacing: 18) {
Label(
title: { configuration.label },
icon: { icon.padding(.trailing, 8) }
)
Spacer()
Image(systemName: "chevron.right")
.accessibilityHidden(true)
}
.padding(18)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(8)
.opacity(configuration.isPressed ? 0.5 : 1) // <-- Comment this out and jumpiness goes away!!
}
}
}
Is there a performance improvement that can be done to use the opacity on the button press and make the jumpiness go away? Or a different way to approach this sticky offset because not sure if this is actually the source of the issue and I use opacity in a lot of places in my app (not just button presses)? The purpose of doing it this way is so the button can be tapped instead of putting it in the background of the scroll view. Thanks for any help or insight!
I'm new to SwiftUI and have a question regarding how to setup a VStack relative to the safe area of the screen.
I'm currently writing an app that will have a single sign-in/sign-up button in the log-in screen. The idea is that if the email address entered in the login screen does not exist, a few more children views will be shown on the screen and the screen will now appear like a registration screen.
I was able to achieve what I wanted using this code...
import SwiftUI
import Combine
struct ContentView: View {
#State var userLoggedIn = false
#State var registerUser = false
#State var saveLoginInfo = false
#ObservedObject var user = User()
var body: some View {
// *** What modifier can I use for this VStack to clip subviews that exceed the screen's safe area?
VStack(alignment: .leading) {
// Show header image and title
HeaderView()
// Show views common to both log-in and registration screens
CommonViews(registerUser: $registerUser, email: $user.email, password: $user.password)
if !registerUser {
// Initially show only interfaces needed for log-in
LoginViewGroup(saveLoginInfo: $saveLoginInfo, registerUser: $registerUser, message: $user.message, signInAllowed: $user.isValid)
} else {
// Show user registration fields if user is new
RegistrationScreenView(registerUser: $registerUser, message: $user.message)
}
Spacer()
// Show footer message
FooterMessage(message: $user.message)
}
.padding(.horizontal)
// Background modifier will check the area occupied by ContentView in the screen
.background(Color.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class User: ObservableObject {
#Published var email: String = ""
#Published var password: String = ""
#Published var message: String = ""
#Published var isValid: Bool = false
private var disposables: Set<AnyCancellable> = []
var isEmailPasswordValid: AnyPublisher<Bool, Never> {
Publishers.CombineLatest($email, $password)
.dropFirst()
.map { (email, pass) in
if !self.isValidEmail(email) {
self.message = "Invalid email address"
return false
}
if self.password.isEmpty {
self.message = "Password should not be blank"
return false
}
self.message = ""
return true
}
.eraseToAnyPublisher()
}
init() {
isEmailPasswordValid
.receive(on: RunLoop.main)
.assign(to: \.isValid, on: self)
.store(in: &disposables)
}
private func isValidEmail(_ email: String) -> Bool {
let emailRegEx = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPred = NSPredicate(format:"SELF MATCHES %#", emailRegEx)
return emailPred.evaluate(with: email)
}
}
struct HeaderView: View {
var body: some View {
VStack {
HStack {
Spacer()
Image(systemName: "a.book.closed")
.resizable()
.scaledToFit()
.frame(height: UIScreen.main.bounds.height * 0.125)
.padding(.vertical)
Spacer()
}
HStack {
Spacer()
//Text("Live Fit Mealkit Ordering App")
Text("Some text underneath an image")
.font(.title3)
Spacer()
}
}
}
}
struct RegistrationScreenView: View {
#State var phone: String = ""
#State var confirmPassword: String = ""
#Binding var registerUser: Bool
#Binding var message: String
var body: some View {
VStack(alignment: .leading) {
Text("Confirm Password")
.font(.headline)
.padding(.top, 5)
SecureField("Re-enter password", text: $confirmPassword)
.padding(.all, 10)
.background(Color(.secondarySystemBackground))
Text("Phone number")
.font(.headline)
.padding(.top, 10)
TextField("e.g. +1-416-555-6789", text: $phone)
.textContentType(.emailAddress)
.autocapitalization(.none)
.padding(.all, 10)
.background(Color(.secondarySystemBackground))
Text("Profile photo")
.font(.headline)
.padding(.top, 10)
HStack {
Image(systemName: "camera")
.resizable()
.scaledToFit()
.frame(width: UIScreen.main.bounds.size.width * 0.35, height: UIScreen.main.bounds.size.width * 0.35)
.padding(.leading, 15)
Spacer()
VStack {
Button("Use Camera", action: {print("Launch Camera app")})
.padding(.all)
.frame(width: UIScreen.main.bounds.size.width * 0.4)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
Button("Choose Photo", action: {print("Launch Photos app")})
.padding(.all)
.frame(width: UIScreen.main.bounds.size.width * 0.4)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}.padding(.trailing, 15)
}
HStack {
Button("Register", action: {})
.padding(.all)
.frame(width: UIScreen.main.bounds.size.width * 0.5)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
Spacer()
Button("Cancel", action: {
registerUser.toggle()
message = ""
})
.padding(.all)
.frame(width: UIScreen.main.bounds.size.width * 0.3)
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(10)
}.padding(.horizontal)
}
}
}
struct CommonViews: View {
#Binding var registerUser: Bool
#Binding var email: String
#Binding var password: String
var body: some View {
VStack {
Text("Email")
.font(.headline)
.padding(.top, registerUser ? 10 : 20)
TextField("Enter email address", text: $email)
.textContentType(.emailAddress)
.autocapitalization(.none)
.padding(.all, 10)
.background(Color(.secondarySystemBackground))
Text("Password")
.font(.headline)
.padding(.top, registerUser ? 5 : 10)
SecureField("Enter password", text: $password)
.padding(.all, 10)
.background(Color(.secondarySystemBackground))
}
}
}
struct LoginViewGroup: View {
#Binding var saveLoginInfo: Bool
#Binding var registerUser: Bool
#Binding var message: String
#Binding var signInAllowed: Bool
var body: some View {
VStack {
HStack {
Spacer()
Button("Sign-in / Sign-up") {
signInButtonPressed()
}
.font(.title2)
.padding(15)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.opacity(signInAllowed ? 1.0: 0.5)
.disabled(!signInAllowed)
Spacer()
}
.padding(.top, 30)
Toggle("Save username and password?", isOn: $saveLoginInfo)
.padding()
}
}
private func signInButtonPressed() {
// **Note**:
// The code inside this function will check a database to see whether the user's email address exists and will decide whether to login the user (if corresponding password is correct) or display the registration view.
// To simplify things, the database checking code was removed and the button action will simply just show the additional user registration fields regardless of the email input
registerUser = true
message = "New user registration"
}
}
struct FooterMessage: View {
#Binding var message: String
var body: some View {
HStack {
Spacer()
Text(message)
.foregroundColor(.red)
.padding(.bottom)
Spacer()
}
}
}
The children views within the top most VStack in ContentView appear to be contained within the safe view area of the screen (which is what I expected) if the total height of views inside the VStack is smaller compared to the safe view area height. This can be verified when I added a gray background modifier to the top most VStack to see how much of the view is covered relative to the phone screen.
VStack falls within safe area (Click to see image)
However, I noticed that when the children views within VStack exceeds the height of the safe area (such as when the additional registration fields are shown), the children views are not clipped but spills outside of the safe area.
VStack spills out of safe area (Click to see image)
Is there a modifier that I can use for the top most VStack that will allow me to clip the top and bottom edges of the children views that will spill out of the safe area?
I want to use this as a visual indicator when running my app using different phone previews so that it will be easier for me to see how much height resizing I will have to perform if the children views of the VStack spills out of the safe area for a specific iphone screen size.
I tried looking for this info but all I see are the opposite of what I want. :)
Also, is there a better way to implement auto-resizing of children views within a VStack to make it fit within the safe area height of the screen other than using:
.frame(minHeight, idealHeight, maxHeight)
Appreciate any suggestions that can be provided.
Thanks.
I am very new at IOS dev, this is my day one. so I have been playing with SF symbols and tried to add it into a button, but somehow it doesn't appear at the button
The Bug
The color should be black and it should've appear on the button.
Perhaps I made a beginner mistake, but still don't know ho two fix it. so what did I do wrong?
struct ButtonBig: View {
var ButtonText:String
var ButtonIcon:String
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 25.0)
.frame(width:117, height: 112.36, alignment:.center)
.foregroundColor(.blue)
VStack{
Image(systemName: ButtonIcon).padding()
Text(ButtonText).foregroundColor(.white)
}
}
}
}
}
Button(action: ) {
ButtonBig(ButtonText: "Button3",ButtonIcon: "calendar")
}
Add button style to PlainButtonStyle()
Button(action: ) {
ButtonBig(ButtonText: "Button3",ButtonIcon: "calendar")
}.buttonStyle(PlainButtonStyle()) //<-- Here
Or you can set renderingMode mode to original.
// Other code
VStack{
Image(systemName: ButtonIcon).renderingMode(.original).padding() //<--- Here
Text(ButtonText).foregroundColor(.white)
}
// Other code
You can do it by overlaying your button image and text on the RoundedRectangle shape view like below :
struct ButtonBig: View {
var ButtonText:String
var ButtonIcon:String
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25.0)
.frame(width:117, height: 112.36, alignment:.center)
.foregroundColor(.blue)
.overlay(
VStack{
Image(systemName: ButtonIcon).renderingMode(.original).padding()
Text(ButtonText).foregroundColor(.white)
}
)
}
}
}
Or you can also do it by just setting the button style directly to HStack of the button.
HStack {
Button(action: {
}, label: {
ButtonBig(ButtonText: "Button1",ButtonIcon: "calendar")
})
Button(action: {
}, label: {
ButtonBig(ButtonText: "Button2",ButtonIcon: "calendar")
})
Button(action: {
}, label: {
ButtonBig(ButtonText: "Button3",ButtonIcon: "calendar")
})
}.buttonStyle(PlainButtonStyle()) //Set the button style to HStack instead of each button
I have a simple login screen with two textfield and a button. It should look like this. The two textfields closer together and the button a little ways down.
Here is my code.
struct ContentView: View {
var body: some View {
VStack {
Spacer()
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer()
ActionButton(title: "Login", action: {})
Spacer()
}
}
}
struct InputTextField: View {
let title: String
#Binding var text: String
var body: some View {
VStack(alignment: .leading) {
Text(title)
.foregroundColor(.primary)
.fontWeight(.medium)
.font(.system(size: 18))
HStack {
TextField("", text: $text)
.frame(height: 54)
.textFieldStyle(PlainTextFieldStyle())
.cornerRadius(10)
}
.padding([.leading, .trailing], 10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 0.6))
}
.padding()
}
}
struct ActionButton: View {
let title: String
var action: () -> Void
var body: some View {
Button(title) {
action()
}
.frame(minWidth: 100, idealWidth: 100, maxWidth: .infinity, minHeight: 60, idealHeight: 60)
.font(.system(size: 24, weight: .bold))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
.padding([.leading, .trailing])
.shadow(color: Color.gray, radius: 2, x: 0, y: 2)
}
}
I wanted to embed this inside a ScrollView so that user can scroll up and down when the keyboard comes up.
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Spacer()
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer()
ActionButton(title: "Login", action: {})
Spacer()
}
}
}
}
Here is where I'm coming across this issue. When I add the VStack inside a ScrollView, all the content kind of shrinks and shows clumped together. Seems like the Spacers have no effect when inside a ScrollView.
How can I fix this?
Demo project
Here, You need to make the content stretch to fill the whole scroll view by giving minimum height as below
struct ContentView: View {
var body: some View {
GeometryReader { gr in
ScrollView {
VStack {
Spacer()
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer()
ActionButton(title: "Login", action: {})
Spacer()
}
.frame(minHeight: gr.size.height)
}
}
}
}
Here is output:
As you have found, Spacers behave differently when they are in a ScrollView or not, or put differently, when the axis they can expand on is infinite or finite.
If what you want is for your content to be centered vertically when it fits and scroll when it's larger than the screen, I would do something like this:
struct ContentView: View {
var body: some View {
VStack { // This new stack would vertically center the content
// (I haven't actually tried it though)
ScrollView {
VStack {
Spacer().size(height: MARGIN) // The minimum margin you want
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer().size(height: SPACING)
ActionButton(title: "Login", action: {})
Spacer().size(height: MARGIN)
}
}
}
}
}