I'm trying to add login form to white color view but textfield placeholder can't see well, however on preview it shows up.
I tried to change placeholder color SwiftUI. How to change the placeholder color of the TextField? but it still doesn't shows up well. Could you give me some tip how can I solve this problem?
struct CardView: View {
var body: some View {
ZStack {
Rectangle()
.fill(Color(UIColor.white))
.frame(height:300)
.cornerRadius(10)
.padding(16)
LoginForm()
}
}
}
struct LoginForm: View {
#State var username: String = ""
var body: some View {
VStack(alignment:.center) {
UsernameTextField(userNumber: $username)
.padding(50)
LoginButton()
}.padding()
}
}
struct UsernameTextField: View {
#Binding var userNumber: String
var body: some View {
TextField("Phone number", text: $userNumber)
.padding(50)
.onChange(of: userNumber, perform: { value in
userNumber = formatNumberTextField(pattern: "+X(XXX) XXX XX XX", phoneNumber: userNumber)
})
.frame(height: 48)
//.textFieldStyle(DefaultTextFieldStyle())
.cornerRadius(16)
.foregroundColor(.black)
.accentColor(.black)
.fixedSize(horizontal: true, vertical: false)
.padding([.leading, .trailing], 10)
.underlineTextField()
}
So I'd like my textfield to have a customizable placeholder text so I decided to put a Text() element in a ZStack in front of the text field. The only problem is, this Text() item blocks the selection of the textfield that is behind it (AKA when I click the placeholder I want the TextField to be clicked). Unfortunately, this Text() element blocks the click. I tried using the .allowsHitTesting() property as seen below but that also didn't work, and I'm not sure why.
struct ContentView: View {
#State var text: String = ""
var body: some View {
ZStack {
TextField("", text: self.$text)
.background(Color.red)
.foregroundColor(Color.white)
if text.isEmpty {
Text("Placeholder")
.allowsHitTesting(false)
}
}
}
}
It can be done with custom text field style.
Here is a demo of solution (or parameters can be tuned). Tested with Xcode 12 / iOS 14 (border is just for visibility)
struct PlaceholderStyle: TextFieldStyle {
let isActive: Bool
var placeholder = "Placeholder"
var color = Color.white
var backgrond = Color.red
func _body(configuration: TextField<_Label>) -> some View {
Text("\(isActive ? placeholder : "")")
.foregroundColor(isActive ? color : .clear)
.background(isActive ? backgrond : .clear)
.frame(maxWidth: .infinity, alignment: .leading)
.overlay(configuration)
}
}
struct DemoView: View {
#State private var text = ""
var body: some View {
TextField("", text: $text)
.border(Color.gray).padding(.horizontal)
.textFieldStyle(PlaceholderStyle(isActive: text.isEmpty))
}
}
See if this fits your needs:
struct ContentView: View {
#State var text = ""
var body: some View {
ZStack(alignment: .leading) {
if text.isEmpty { Text("Placeholder")
.foregroundColor(.red)
.background(Color.yellow)
}
TextField("", text: $text)
.background(text.isEmpty ? Color.clear : Color.yellow)
}
}
}
I've looked through the forums but I'm seeing mixed answers especially ones from an old Xcode version.
I only decided to add this after already typing up the code I have in this:
How could I go about doing that? I was wanting the 'Eyeball' toggle implemented on the password field.
You can simply use this view instead of SecureField. It has the eye icon inside, so for most cases you don't need to care about anything.
struct SecureInputView: View {
#Binding private var text: String
#State private var isSecured: Bool = true
private var title: String
init(_ title: String, text: Binding<String>) {
self.title = title
self._text = text
}
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecured {
SecureField(title, text: $text)
} else {
TextField(title, text: $text)
}
}.padding(.trailing, 32)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.accentColor(.gray)
}
}
}
}
Copy paste this view into your app, and instead of SecureField just use SecureInputView.
Example: SecureInputView("Password", text: $viewModel.password)
The possible approach is to show either TextField or SecureField joined to one storage, like in below demo:
Updated: Xcode 13.4 / iOS 15.5
with FocusState, now it is possible to change fields without having the keyboard disappear
Main part:
if showPassword {
TextField("Placeholer", text: $password)
.focused($inFocus, equals: .plain)
} else {
SecureField("Placeholder", text: $password)
.focused($inFocus, equals: .secure)
}
Button("toggle") {
self.showPassword.toggle()
inFocus = showPassword ? .plain : .secure
}
Test module in project is here
Old:
struct DemoShowPassword: View {
#State private var showPassword: Bool = false
#State private var password = "demo"
var body: some View {
VStack {
if showPassword {
TextField("Placeholer", text: $password)
} else {
SecureField("Placeholder", text: $password)
}
Button("toggle") {
self.showPassword.toggle()
}
}
}
}
For those still looking for a simple solution to this issue (requires iOS 15 for swiftUI 3):
With the new #FocusState introduced in swiftUI 3, it's possible to keep focus and keyboard open while changing State.
By using the opacity modifier instead of conditionally changing between SecureField and TextField, the focus can jump between the two without issues with the keyboard.
This allows you to toggle between revealing and hiding the password with the the eye button included in the ZStack.
import SwiftUI
struct SecureTextFieldWithReveal: View {
#FocusState var focus1: Bool
#FocusState var focus2: Bool
#State var showPassword: Bool = false
#State var text: String = ""
var body: some View {
HStack {
ZStack(alignment: .trailing) {
TextField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus1)
.opacity(showPassword ? 1 : 0)
SecureField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus2)
.opacity(showPassword ? 0 : 1)
Button(action: {
showPassword.toggle()
if showPassword { focus1 = true } else { focus2 = true }
}, label: {
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill").font(.system(size: 16, weight: .regular))
.padding()
})
}
}
}
}
Password field hidden
Password field revealed
This is the code in LoginModifier:
import SwiftUI
struct LoginModifier: ViewModifier {
var borderColor: Color = Color.gray
func body(content: Content) -> some View {
content
.disableAutocorrection(true)
.autocapitalization(.none)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(borderColor, lineWidth: 1))
}
}
The only issue I've had with this method is that on regaining focus SecureField will automatically clear any text already entered if you start typing. This seems to be a design choice by Apple.
I am using this approach for now in my current application. I would like to say that it works flawlessly.
#ViewBuilder
func secureField() -> some View {
if self.showPassword {
TextField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
} else {
SecureField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
}
}
Use:
HStack{
Image(systemName: "lock.fill")
.foregroundColor(passwordText.isEmpty ? .secondary : .primary)
.font(.system(size: 18, weight: .medium, design: .default))
.frame(width: 18, height: 18, alignment: .center)
secureField()
if !passwordText.isEmpty {
Button(action: {
self.showPassword.toggle()
}, label: {
ZStack(alignment: .trailing){
Color.clear
.frame(maxWidth: 29, maxHeight: 60, alignment: .center)
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill")
.font(.system(size: 18, weight: .medium))
.foregroundColor(Color.init(red: 160.0/255.0, green: 160.0/255.0, blue: 160.0/255.0))
}
})
}
}
.padding(.horizontal, 15)
.background(Color.primary.opacity(0.05).cornerRadius(10))
.padding(.horizontal, 15)
I am afraid most answers here fail to mention that switching from SecureField to TextField reduces security. SecureField is essentially, per Apple documentation, simply a TextField where user input is masked [1]. However, SecureField also does one other job - it prevents using third-party keyboards (keyboard extensions) and thus protects user's security and privacy.
Ideal solution would be to have input field that is both "secure" and has mask()/unmask() methods. Unfortunately, the only advice I found is when you want to implement unmasking as other answers suggested, at least block third-party keyboards from your application entirely [2]:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
return extensionPointIdentifier != UIApplication.ExtensionPointIdentifier.keyboard
}
}
#main
struct MyApplication: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Should also mention that UIApplicationDelegate is part of UIKit, not SwiftUI. There is no "native" SwiftUI for the same purpose as for now, although the above works fine for now.
https://developer.apple.com/documentation/swiftui/securefield
https://www.securing.pl/en/third-party-iphone-keyboards-vs-your-ios-application-security/
For those that do not want the keyboard disappearing while typing:
struct CustomSecureField: View {
#State var password: String = ""
#State var isShowingPassword: Bool = false
var body: some View {
VStack{
ZStack{
HStack{
SecureField(
isShowingPassword ? "" : "Password",
text: $password) {
}.opacity(isShowingPassword ? 0 : 1)
// show only one of these is not empty.
if(!password.isEmpty){
Image(systemName: isShowingPassword ? "eye.slash" : "eye")
.foregroundColor(.white)
.frame(width: 20, height: 20, alignment: .center)
.modifier(TouchDownUpEventModifier(changeState: { (buttonState) in
if buttonState == .pressed {
isShowingPassword = true
} else {
isShowingPassword = false
}
}))
}
}
if(isShowingPassword){
HStack{
Text(password)
.foregroundColor(.white)
.allowsHitTesting(false)
Spacer()
}
}
}
}.padding(10)
.background(Color.gray)
}
}
and the on tap and release modifier:
public enum ButtonState {
case pressed
case notPressed
}
/// ViewModifier allows us to get a view, then modify it and return it
public struct TouchDownUpEventModifier: ViewModifier {
/// Properties marked with `#GestureState` automatically resets when the gesture ends/is cancelled
/// for example, once the finger lifts up, this will reset to false
/// this functionality is handled inside the `.updating` modifier
#GestureState private var isPressed = false
/// this is the closure that will get passed around.
/// we will update the ButtonState every time your finger touches down or up.
let changeState: (ButtonState) -> Void
/// a required function for ViewModifier.
/// content is the body content of the caller view
public func body(content: Content) -> some View {
/// declare the drag gesture
let drag = DragGesture(minimumDistance: 0)
/// this is called whenever the gesture is happening
/// because we do this on a `DragGesture`, this is called when the finger is down
.updating($isPressed) { (value, gestureState, transaction) in
/// setting the gestureState will automatically set `$isPressed`
gestureState = true
}
return content
.gesture(drag) /// add the gesture
.onChange(of: isPressed, perform: { (pressed) in /// call `changeState` whenever the state changes
/// `onChange` is available in iOS 14 and higher.
if pressed {
self.changeState(.pressed)
} else {
self.changeState(.notPressed)
}
})
}
/// if you're on iPad Swift Playgrounds and you put all of this code in a seperate file,
/// you need to add a public init so that the compiler detects it.
public init(changeState: #escaping (ButtonState) -> Void) {
self.changeState = changeState
}
}
From what I have seen there is no easy way to keep the text showing unless you want to lose focus on your text.
Cheers!
#Derwrecked's answer really gave me some good inspirations: instead using two TextField, change SecureField opacity and show/hide a Text can avoid keyboard dismissing problem, but in his answer that long TouchDownUpEventModifier seems unnecessarily complicated, you can easily achieve the same effect using a Button with label.
So below is my approach, and the previews look like this
import SwiftUI
struct SecureInput: View {
let placeholder: String
#State private var showText: Bool = false
#State var text: String
var onCommit: (()->Void)?
var body: some View {
HStack {
ZStack {
SecureField(placeholder, text: $text, onCommit: {
onCommit?()
})
.opacity(showText ? 0 : 1)
if showText {
HStack {
Text(text)
.lineLimit(1)
Spacer()
}
}
}
Button(action: {
showText.toggle()
}, label: {
Image(systemName: showText ? "eye.slash.fill" : "eye.fill")
})
.accentColor(.secondary)
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary, lineWidth: 1)
.foregroundColor(.clear))
}
}
struct SecureInput_Previews: PreviewProvider {
static var previews: some View {
Group {
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.previewLayout(.fixed(width: 400, height: 100))
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.preferredColorScheme(.dark)
.previewLayout(.fixed(width: 400, height: 100))
}
}
}
An known issue for this approach: since when password is shown, SecureField has 0.0 opacity, so input cursor is not visible. But users can still keep typing without losing keyboard focus, so I find it acceptable, if anyone has a solution for this, please comment and share.
I've been looking for a nice solution for my use-case. I had to have an indicator which field is in focus. Successfully done that with onEditingChanged from TextField, but SecureField doesn't provide that closure. I tried stacking them both and disabling the SecureField so it only shows 'hidden' characters. That resulted in cursor sticking to the TextField text while SecureField text had different text width which made it seem buggy. Imagine a password with a lot of I's in it. The idea is to have a main binding with two side bindings that update the main one and sync each other.
struct CustomSecureField : View {
var label : String
#Binding var text : String
#State var isEditing = false
#State var isHidden = true
var body : some View {
let showPasswordBinding = Binding<String> {
self.text
} set: {
self.text = $0
}
let hidePasswordBinding = Binding<String> {
String.init(repeating: "●", count: self.text.count)
} set: { newValue in
if(newValue.count < self.text.count) {
self.text = ""
} else {
self.text.append(contentsOf: newValue.suffix(newValue.count - self.text.count) )
}
}
return ZStack(alignment: .trailing) {
TextField(
label,
text: isHidden ? hidePasswordBinding : showPasswordBinding,
onEditingChanged: { editingChanged in
isEditing = editingChanged
}
)
Image("eye").frame(width: 50, height: 50).onTapGesture {
isHidden.toggle()
}
}
}
}
}
Crazy (AKA don't use in production) and very breakable solution here (but working at the time of writing):
extension TextField {
public func secure(_ secure: Bool = true) -> TextField {
if secure {
var secureField = self
withUnsafeMutablePointer(to: &secureField) { pointer in
let offset = 32
let valuePointer = UnsafeMutableRawPointer(mutating: pointer)
.assumingMemoryBound(to: Bool.self)
.advanced(by: offset)
valuePointer.pointee = true
}
return secureField
} else {
return self
}
}
}
Usage
#State securing = true
...
TextField(...)
.secure(securing)
#Vahagn Gevorgyan's answer was almost correct but some people were struggling with maintaining state... this is because the field is using a binding which should ideally be held in a parent view. Therefore just update the bindings to state variables like this
struct SecureInputView: View {
let placeholder: String
#State var text: String
#State var isSecure: Bool = true
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecure {
SecureField(placeholder, text: $text)
} else {
TextField(placeholder, text: $text)
}
}.padding(.trailing, 32)
Button {
isSecure.toggle()
} label: {
Image(systemName: isSecure ? "lock.fill" : "lock.open")
}
}
}
}
#State private var isPasswordVisible = false
ZStack {
TextField("", text: $password)
.opacity(isPasswordVisible ? 1 : 0)
SecureField("", text: $password)
.opacity(isPasswordVisible ? 0 : 1)
}
It doesn't need #Focus from iOS 15
Keyboard will not disappear/appear on changing isPasswordVisible
Password will not cleared on changing from visible to invisible then typing
Good Luck
I made a custom text field that combine SecureField and TextField.
This is an example where I used my custom field for both email and pwd.
This is my solution:
struct CustomTextField: View {
let imageName: String
let placeholderText: String
var isSecureInput: Bool = false ///< define if this text field is secured and require eye button
#State private var isSecured: Bool
#Binding var text: String
init(image: String,
placeholder: String,
text: Binding<String>,
isSecureInput: Bool) {
imageName = image
placeholderText = placeholder
self._text = text
self.isSecureInput = isSecureInput
isSecured = isSecureInput
}
var body: some View {
VStack {
HStack {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
if isSecureInput {
Group {
if isSecured {
SecureField(placeholderText, text: $text)
}
else {
TextField(text, text: $text)
}
}
.disableAutocorrection(true)
.autocapitalization(.none)
.textContentType(.password)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
}
}
else {
TextField(placeholderText, text: $text)
}
}
Divider()
}
}
}
The question is as simple as in the title. I am trying to put a Picker which has the style of SegmentedPickerStyle to NavigationBar in SwiftUI. It is just like the native Phone application's history page. The image is below
I have looked for Google and Github for example projects, libraries or any tutorials and no luck. I think if nativa apps and WhatsApp for example has it, then it should be possible. Any help would be appreciated.
SwiftUI 2 + toolbar:
struct DemoView: View {
#State private var mode: Int = 0
var body: some View {
Text("Hello, World!")
.toolbar {
ToolbarItem(placement: .principal) {
Picker("Color", selection: $mode) {
Text("Light").tag(0)
Text("Dark").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
}
You can put a Picker directly into .navigationBarItems.
The only trouble I'm having is getting the Picker to be centered. (Just to show that a Picker can indeed be in the Navigation Bar I put together a kind of hacky solution with frame and Geometry Reader. You'll need to find a proper solution to centering.)
struct ContentView: View {
#State private var choices = ["All", "Missed"]
#State private var choice = 0
#State private var contacts = [("Anna Lisa Moreno", "9:40 AM"), ("Justin Shumaker", "9:35 AM")]
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
ForEach(self.contacts, id: \.self.0) { (contact, time) in
ContactView(name: contact, time: time)
}
.onDelete(perform: self.deleteItems)
}
.navigationBarTitle("Recents")
.navigationBarItems(
leading:
HStack {
Button("Clear") {
// do stuff
}
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.frame(width: 130)
.pickerStyle(SegmentedPickerStyle())
.padding(.leading, (geometry.size.width / 2.0) - 130)
},
trailing: EditButton())
}
}
}
func deleteItems(at offsets: IndexSet) {
contacts.remove(atOffsets: offsets)
}
}
struct ContactView: View {
var name: String
var time: String
var body: some View {
HStack {
VStack {
Image(systemName: "phone.fill.arrow.up.right")
.font(.headline)
.foregroundColor(.secondary)
Text("")
}
VStack(alignment: .leading) {
Text(self.name)
.font(.headline)
Text("iPhone")
.foregroundColor(.secondary)
}
Spacer()
Text(self.time)
.foregroundColor(.secondary)
}
}
}
For those who want to make it dead center, Just put two HStack to each side and made them width fixed and equal.
Add this method to View extension.
extension View {
func navigationBarItems<L, C, T>(leading: L, center: C, trailing: T) -> some View where L: View, C: View, T: View {
self.navigationBarItems(leading:
HStack{
HStack {
leading
}
.frame(width: 60, alignment: .leading)
Spacer()
HStack {
center
}
.frame(width: 300, alignment: .center)
Spacer()
HStack {
//Text("asdasd")
trailing
}
//.background(Color.blue)
.frame(width: 100, alignment: .trailing)
}
//.background(Color.yellow)
.frame(width: UIScreen.main.bounds.size.width-32)
)
}
}
Now you have a View modifier which has the same usage of navigationBatItems(:_). You can edit the code based on your needs.
Usage example:
.navigationBarItems(leading: EmptyView(), center:
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}, trailing: EmptyView())
UPDATE
There was the issue of leading and trailing items were violating UINavigationBarContentView's safeArea. While I was searching through, I came across another solution in this answer. It is little helper library called SwiftUIX. If you do not want install whole library -like me- I created a gist just for navigationBarItems. Just add the file to your project.
But do not forget this, It was stretching the Picker to cover all the free space and forcing StatusView to be narrower. So I had to set frames like this;
.navigationBarItems(center:
Picker(...) {
...
}
.frame(width: 150)
, trailing:
StatusView()
.frame(width: 70)
)
If you need segmentcontroll to be in center you need to use GeometryReader, below code will provide picker as title, and trailing (right) button.
You set up two view on the sides left and right with the same width, and the middle view will take the rest.
5 is the magic number depends how width you need segment to be.
You can experiment and see the best fit for you.
GeometryReader {
Text("TEST")
.navigationBarItems(leading:
HStack {
Spacer().frame(width: geometry.size.width / 5)
Spacer()
picker
Spacer()
Button().frame(width: geometry.size.width / 5)
}.frame(width: geometry.size.width)
}
But better solution is if you save picker size and then calculate other frame sizes, so picker will be same on ipad & iphone
#State var segmentControllerWidth: CGFloat = 0
var body: some View {
HStack {
Spacer()
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
.background(Color.red)
segmentController
.fixedSize()
.background(PreferenceViewSetter())
profileButton
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
}
.onPreferenceChange(PreferenceViewKey.self) { preferences in
segmentControllerWidth = preferences.width
}
}
struct PreferenceViewSetter: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: PreferenceViewKey.self,
value: PreferenceViewData(width: geometry.size.width))
}
}
}
struct PreferenceViewData: Equatable {
let width: CGFloat
}
struct PreferenceViewKey: PreferenceKey {
typealias Value = PreferenceViewData
static var defaultValue = PreferenceViewData(width: 0)
static func reduce(value: inout PreferenceViewData, nextValue: () -> PreferenceViewData) {
value = nextValue()
}
}
Simple answer how to center segment controller and hide one of the buttons.
#State var showLeadingButton = true
var body: some View {
HStack {
Button(action: {}, label: {"leading"})
.opacity(showLeadingButton ? true : false)
Spacer()
Picker(selection: $selectedStatus,
label: Text("SEGMENT") {
segmentValues
}
.id(UUID())
.pickerStyle(SegmentedPickerStyle())
.fixedSize()
Spacer()
Button(action: {}, label: {"trailing"})
}
.frame(width: UIScreen.main.bounds.width)
}
I have the following Cocoa form:
struct Canvas: PreviewProvider {
static var previews: some View {
VStack {
HStack(alignment: .firstTextBaseline) {
Text("Endpoint:")
TextField("https://localhost:8080/api", text: .constant(""))
}
Divider()
HStack(alignment: .firstTextBaseline) {
Text("Path:")
TextField("/todos", text: .constant(""))
}
Spacer()
}
.padding()
.previewLayout(.fixed(width: 280, height: 200))
}
}
This panel looks nice but I’d like to right-align “Endpoint:” and “Path:” labels:
So I apply a custom horizontal alignment:
struct Canvas: PreviewProvider {
static var previews: some View {
VStack(alignment: .label) {
HStack(alignment: .firstTextBaseline) {
Text("Endpoint:").alignmentGuide(.label) { $0[.trailing] }
TextField("https://localhost:8080/api", text: .constant(""))
}
Divider()
HStack(alignment: .firstTextBaseline) {
Text("Path:").alignmentGuide(.label) { $0[.trailing] }
TextField("/todos", text: .constant(""))
}
Spacer()
}
.padding()
.previewLayout(.fixed(width: 280, height: 200))
}
}
extension HorizontalAlignment {
private enum Label: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[.leading]
}
}
static let label: HorizontalAlignment = .init(Label.self)
}
Results are not what I need however:
There is no documentation, please help.
I don't believe alignment guides will work here in their current implementation. After playing with them a bit, it seems that they size their children based on the container's given size and then align each child based on the guide. This leads to the weird behavior you were seeing.
Below I show 3 different techniques that will allow you to get your desired results, in order of complexity. Each has its applications outside of this specific example.
The last (label3()) will be the most reliable for longer forms.
struct ContentView: View {
#State var sizes: [String:CGSize] = [:]
var body: some View {
VStack {
HStack(alignment: .firstTextBaseline) {
self.label3("Endpoint:")
TextField("https://localhost:8080/api", text: .constant(""))
}
Divider()
HStack(alignment: .firstTextBaseline) {
self.label3("Path:")
TextField("/todos", text: .constant(""))
}
}
.padding()
.onPreferenceChange(SizePreferenceKey.self) { preferences in
self.sizes = preferences
}
}
func label1(_ text: String) -> some View {
Text(text) // Use a minimum size based on your best guess. Look around and you'll see that many macOS apps actually lay forms out like this because it's simple to implement.
.frame(minWidth: 100, alignment: .trailing)
}
func label2(_ text: String, sizer: String = "Endpoint:") -> some View {
ZStack(alignment: .trailing) { // Use dummy content for sizing based on the largest expected item. This can be great when laying out icons and you know ahead of time which will be the biggest.
Text(sizer).opacity(0.0)
Text(text)
}
}
func label3(_ text: String) -> some View {
Text(text) // Use preferences and save the size of each label
.background(
GeometryReader { proxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: [text : proxy.size])
}
)
.frame(minWidth: self.sizes.values.map { $0.width }.max() ?? 0.0, alignment: .trailing)
}
}
struct SizePreferenceKey: PreferenceKey {
typealias Value = [String:CGSize]
static var defaultValue: Value = [:]
static func reduce(value: inout Value, nextValue: () -> Value) {
let next = nextValue()
for (k, v) in next {
value[k] = v
}
}
}
Here's a screenshot of the results with label2 or label3.
Using XCode 13.1 and targeting MacOS 12 you can achieve the desired result quite easily by adding a "Form" element:
struct Canvas: PreviewProvider {
static var previews: some View {
Form {
TextField("Endpoint:", text: .constant(""))
Divider()
TextField("Path:", text: .constant(""))
}
.previewLayout(.fixed(width: 280, height: 200))
}
}
The divider is not covering the area of the labels, but this is intended by Apple. Also, I haven't found out quickly how to add the placeholders to the text fields.