SwiftUI Login Page Layout - ios

I am exploring SwiftUI as I am trying to build a login view and now I am facing a problem
This is what I am trying to achieve:
As you can see I already reached this point but I don't like my implementation
struct ContentView : View {
#State var username: String = ""
var body: some View {
VStack(alignment: .leading) {
Text("Login")
.font(.title)
.multilineTextAlignment(.center)
.lineLimit(nil)
Text("Please")
.font(.subheadline)
HStack {
VStack (alignment: .leading, spacing: 20) {
Text("Username: ")
Text("Password: ")
}
VStack {
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
}
}
}.padding()
}
}
Because in order to make the username and password text aligned exactly in the middle of the textfield, I had to put literal spacing value of 20 in the VStack which I don't like because most probably It won't work on different device sizes.
Anyone sees a better way to achieve the same result?
Thanks

We're going to implement two new View modifier methods so that we can write this:
struct ContentView: View {
#State var labelWidth: CGFloat? = nil
#State var username = ""
#State var password = ""
var body: some View {
VStack {
HStack {
Text("User:")
.equalSizedLabel(width: labelWidth, alignment: .trailing)
TextField("User", text: $username)
}
HStack {
Text("Password:")
.equalSizedLabel(width: labelWidth, alignment: .trailing)
SecureField("Password", text: $password)
}
}
.padding()
.textFieldStyle(.roundedBorder)
.storeMaxLabelWidth(in: $labelWidth)
}
}
The two new modifiers are equalSizedLabel(width:alignment:) and storeMaxLabelWidth(in:).
The equalSizedLabel(width:alignment) modifier does two things:
It applies the width and alignment to its content (the Text(“User:”) and Text(“Password:”) views).
It measures the width of its content and passes that up to any ancestor view that wants it.
The storeMaxLabelWidth(in:) modifier receives those widths measured by equalSizedLabel and stores the maximum width in the $labelWidth binding we pass to it.
So, how do we implement these modifiers? How do we pass a value from a descendant view up to an ancestor? In SwiftUI, we do this using the (currently undocumented) “preference” system.
To define a new preference, we define a type conforming to PreferenceKey. To conform to PreferenceKey, we have to define the default value for our preference, and we have to define how to combine the preferences of multiple subviews. We want our preference to be the maximum width of all the labels, so the default value is zero and we combine preferences by taking the maximum. Here's the PreferenceKey we'll use:
struct MaxLabelWidth: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = max(value, nextValue())
}
}
The preference modifier function sets a preference, so we can say .preference(key: MaxLabelWidth.self, value: width) to set our preference, but we have to know what width to set. We need to use a GeometryReader to get the width, and it's a little tricky to do properly, so we'll wrap it up in a ViewModifier like this:
extension MaxLabelWidth: ViewModifier {
func body(content: Content) -> some View {
return content
.background(GeometryReader { proxy in
Color.clear
.preference(key: Self.self, value: proxy.size.width)
})
}
}
What's happening above is we attach a background View to the content, because a background is always the same size as the content it's attached to. The background View is a GeometryReader, which (via the proxy) provides access to its own size. We have to give the GeometryReader its own content. Since we don't actually want to show a background behind the original content, we use Color.clear as the GeometryReader's content. Finally, we use the preference modifier to store the width as the MaxLabelWidth preference.
Now have can define the equalSizedLabel(width:alignment:) and storeMaxLabelWidth(in:) modifier methods:
extension View {
func equalSizedLabel(width: CGFloat?, alignment: Alignment) -> some View {
return self
.modifier(MaxLabelWidth())
.frame(width: width, alignment: alignment)
}
}
extension View {
func storeMaxLabelWidth(in binding: Binding<CGFloat?>) -> some View {
return self.onPreferenceChange(MaxLabelWidth.self) {
binding.value = $0
}
}
}
Here's the result:

You can use Spacers alongside with fixedSize modifier for height. You should set set heights of any row's object in order to achieve exact table style view:
struct ContentView : View {
private let height: Length = 32
#State var username: String = ""
var body: some View {
VStack(alignment: .leading) {
Text("Login")
.font(.title)
.multilineTextAlignment(.center)
.lineLimit(nil)
Text("Please")
.font(.subheadline)
HStack {
VStack (alignment: .leading) {
Text("Username: ") .frame(height: height)
Spacer()
Text("Password: ") .frame(height: height)
}
VStack {
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
.frame(height: height)
Spacer()
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
.frame(height: height)
}
}
.fixedSize(horizontal: false, vertical: true)
}
.padding()
}
}
Note that setting height on TextField does not effect it's height directly, but it will just set the height of it's content text's height.

If you are looking to do something similar with Buttons on macOS, be advised that as of Xcode 11.3 you'll end up with the following at run time:
instead of:
I've written up my solution in this blog post. It is quite similar to #rob-mayoff 's answer and works for both labels and buttons.

Related

Duration picker view(multicomponent picker) in swiftUI

I have a custom swiftUI picker that takes a duration in the form of the picture above. The issue is, I have to use hard coded frame in my code to make it show and appear. I will explain further below.
var body: some View {
let hours = [Int](0...maxHours)
let minutes = [Int](0...maxMinutes)
HStack(spacing: 0) {
Picker(selection: self.selection.hours, label: Text("")) {
ForEach(0..<maxHours, id: \.self) { index in
Text("\(hours[index]) hr")
.foregroundColor(Color(Asset.Color.V2.white.color))
}
}
.labelsHidden()
.pickerStyle(.wheel)
Picker(selection: self.selection.minutes, label: Text("")) {
ForEach(0..<maxMinutes, id: \.self) { index in
Text("\(minutes[index]) min")
.foregroundColor(Color(Asset.Color.V2.white.color))
}
.frame(maxWidth: .infinity, alignment: .center)
}
.labelsHidden()
.pickerStyle(.wheel)
}
}
The issue is I am combining two pickers and it is not a native approach. So it'll end up looking like this:
The frame becomes very small and off when it is part of a larger component.
If I remove the HStack and have one picker, the frame and sizing will fix themselves.
note: Ignore the colors, my only concern is the frame sizes that get messed up when I have two pickers.
problem 1: This picker is part of another large component. Here is how the structure is set up, and apologies in advance as I cannot share all of the code as it will be be more than 5000 lines of code.
We have this:
VStack {
element1
element2
...
element4
.onTap{
showCustomPickerView = true
}
.frame(width: 200, height: 200)
if showCustomPickerView {
CustomPickerView()
}
element5
}
So when we click on element4 which is essentially an HStack, we want our custom picker view to appear underneath it. The issue is I do not want hard coded frame values, but when I remove the frame, the CustomPickerView collapses and becomes like the picture I posted. If my CustomPickerView has only one picker in it, it shows just fine without the frame. But since I have two and I they are in an HStack, it does not show their default size, and I am guessing it shows the HStack size instead.
update 1: I added
extension UIPickerView {
override open var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: super.intrinsicContentSize.height)
}
}
at my file, as without it, the right picker would get mixed with the first one, but the framing issue still persists.
I've slightly modified your solution, and it works fine for me:
#main
struct Test: App {
#State var hourSelection = 0
#State var minuteSelection = 0
var body: some Scene {
WindowGroup {
CustomDatePicker(hourSelection: $hourSelection, minuteSelection: $minuteSelection)
}
}
}
struct CustomDatePicker: View {
#Binding var hourSelection: Int
#Binding var minuteSelection: Int
static private let maxHours = 24
static private let maxMinutes = 60
private let hours = [Int](0...Self.maxHours)
private let minutes = [Int](0...Self.maxMinutes)
var body: some View {
GeometryReader { geometry in
HStack(spacing: .zero) {
Picker(selection: $hourSelection, label: Text("")) {
ForEach(hours, id: \.self) { value in
Text("\(value) hr")
.tag(value)
}
}
.pickerStyle(.wheel)
.frame(width: geometry.size.width / 2, alignment: .center)
Picker(selection: $minuteSelection, label: Text("")) {
ForEach(minutes, id: \.self) { value in
Text("\(value) min")
.tag(value)
}
.frame(maxWidth: .infinity, alignment: .center)
}
.pickerStyle(.wheel)
.frame(width: geometry.size.width / 2, alignment: .center)
}
}
}
}
The main idea here is that:
You do not specify the height of the picker, so the GeometryReader adjusts its size to correspond to the default Picker height.
Change the width from geometry.size.width / 3 to geometry.size.width / 2 (as there are only two pickers).
Remove the unnecessary modifiers (.compositeGroup(), .clipped(), .etc).
Move the picker into a separate struct for ease of use.
Alternatively, you can manually specify a fixed size for the custom component using the .frame(height:) modifier.
Let me know if it still collapses

How to get Text width with SwiftUI?

I would like to underline a title with a rectangle that should have the same width as the Text.
First I create an underlined text as below:
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}
}
}
So I get the following result:
Now I want to get this result:
So I would like to know if it's possible to bind Text width and apply it to Rectangle by writing something like :
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(width: Text.width, height: (5.0))
}
}
}
By doing so, I could change text and it will be dynamically underlined with correct width.
I tried many options but I can't find how to do it. I also checked this question but it's seems to not be the same issue.
Just specify that container has fixed size and it will tight to content, like
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}.fixedSize() // << here !!
}

SwiftUI - How can a TextField increase dynamically its height based on its content?

I am trying to build a "chat" view using SwiftUI and I would like to know how can I do in order to increase the height dynamically of a TextField where the users should write their messages.
I have defined a minHeight expecting that the TextField could increase its height based on its intrinsic content.
My current view code:
struct MessageSenderView: View {
#Binding var userTextInput: String
var body: some View {
VStack {
HStack(alignment: .center, spacing: 17) {
senderPlusImage()
ZStack {
Capsule()
.fill(Color("messagesBankDetailColor"))
.frame(minHeight: 34, alignment: .bottom)
HStack(spacing: 15){
Spacer()
ZStack(alignment: .leading) {
if userTextInput.isEmpty { Text(Constants.Login.Text.userPlaceHolder).foregroundColor(Color.white) }
TextField(" ", text: $userTextInput)
.multilineTextAlignment(.leading)
.frame(minHeight: CGFloat(34))
.foregroundColor(Color.white)
.background(Color("messagesBankDetailColor"))
.onAppear { self.userTextInput = "" }
}
arrowImage()
}
.frame(minHeight: CGFloat(34))
.padding(.trailing, 16)
.layoutPriority(100)
}
}
.padding(16)
}
.background(Color("mainBackgroundColor"))
}
}
And here is how it looks like:
Thank you!!!!
For this purpose, you should use UITextfield with the UIViewRepresentable protocol.
Maybe this tutorial can help you : Dynamic TextField SwiftUI
To support multiline text, you should use TextEditor instead of TextField.
If you want it to grow as you type, embed it with a label like below:
ZStack {
TextEditor(text: $text)
Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack
}
Demo

Text inside a VStack truncates when it's not supposed to in SwiftUI

I'm trying to create a simple stack of Text inside a VStack, and no matter what I do, the text will truncate instead of wrap, even if I explicitly set lineLimit(nil) (although I know this is the default now).
I've tried setting layoutPriority(1) on the first element in the VStack, and I have also tried setting frame(idealHeight: .greatestFiniteMagnitude) as some other posts have suggested, but nothing seems to fix the issue.
Here is a video of the issue in action:
Here is some code that reproduces the issue:
import SwiftUI
struct BugRepro: View {
#State var length: Double = 1.0
var body: some View {
VStack {
ForEach(0..<3) { i in
BugReproElement(index: i)
}
.background(Color.gray3)
.frame(width: UIScreen.main.bounds.width * CGFloat(length))
Slider(value: $length, in: 0.0...1.0)
}
}
}
struct BugRepro_Previews: PreviewProvider {
static var previews: some View {
BugRepro()
}
}
struct BugReproElement: View {
var index: Int
var body: some View {
Text("iaush isuh siudh siudh isudh isudhdsiu sdiuh sdihs")
.foregroundColor(.gray7)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
}
Could this just be a bug in Xcode? I'm running Beta 7
I figured out how to get the text to render properly.
Applying the answer here works: Views compressed by other views in SwiftUI VStack and List
The key is to ensure that the .fixedSize() gets added before .frame()
No Spacer() needed!
I had to add .fixedSize(horizontal: false, vertical: true) AND .padding() before all my text would stop truncating (show in view properly) I also have a frame tag for a separate vstack (I have 2 separate vstacks in the view)
Unfortunately It is a bug.
There is a work around you can use to force it to recalculate elements but it's just a workaround and you must wait for the release and see if it fixed.
Workaround
Add a pair of spacer with the height of zero above and below the contents of ForEach.
struct BugRepro: View {
#State var length: Double = 1.0
var body: some View {
VStack {
ForEach(0..<3) { i in
Spacer().frame(height: 0)
BugReproElement(index: i)
Spacer().frame(height: 0)
}.frame(width: UIScreen.main.bounds.width * CGFloat(length))
Slider(value: self.$length, in: 0.0...1.0)
}
}
}
.fixedSized had to be added before .font() for it to work for me.
Text("Some Text")
.lineLimit(1)
.fixedSize()
will display always in 1 line that disaplayed fully.

ForEach inside ScrollView doesn't take whole width

I'm trying to re-create UI of my current app using SwiftUI. And it is way more difficult than I initially though.
I wanted to achieve card-like cells with some background behind them. I found that List doesn't support that, at least yet. List is so limited - it doesn't allow you to remove cell separator.
So I moved to ForEach inside ScrollView. I guess that isn't something which should be used in production for long tables but that should work for now. The problem I have is that ForeEach view doesn't take all the width ScrollView provides. I can set .frame(...) modifier but that will require hardcoding width which I definitely don't want to do.
Any ideas how to force VStack take full width of the ScrollView? I tried to use ForeEach without VStack and it has the same issue. It seems like ScrollView (parent view) "tells" its child view (VStack) that its frame is less that actual ScrollView's frame. And based on that information child views build their layout and sizes.
Here is my current result:
And here is the code:
struct LandmarkList : View {
var body: some View {
NavigationView {
ScrollView() {
VStack {
Spacer().frame(height: 160)
ForEach(landmarkData) { landmark in
LandmarkRow(landmark: landmark).padding([.leading, .trailing], 16)
}
}.scaledToFill()
.background(Color.pink)
}
.background(Color.yellow)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkRow : View {
var landmark: Landmark
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(landmark.name).font(.title)
Text("Subtitle")
.font(.callout)
.color(.gray)
}
Spacer()
Text("5 mi")
.font(.largeTitle)
}.frame(height: 80)
.padding()
.background(Color.white)
.cornerRadius(16)
.clipped()
.shadow(radius: 2)
}
}
I've got the same issue, the only way I have found so far is to fix the ScrollView and the content view width, so that every subview you add inside the content view will be centered.
I created a simple wrapper that take the width as init parameter
struct CenteredList<Data: RandomAccessCollection, Content: View>: View where Data.Element: Identifiable {
public private(set) var width: Length
private var data: Data
private var contentBuilder: (Data.Element.IdentifiedValue) -> Content
init(
width: Length = UIScreen.main.bounds.width,
data: Data,
#ViewBuilder content: #escaping (Data.Element.IdentifiedValue) -> Content)
{
self.width = width
self.data = data
self.contentBuilder = content
}
var body: some View {
ScrollView {
VStack {
ForEach(data) { item in
return self.contentBuilder(item)
}.frame(width: width)
}
.frame(width: width)
}
.frame(width: width)
}
}
By default it takes the screen width (UIScreen.main.bounds.width).
It works just like a List view:
var body: some View {
TileList(data: 0...3) { index in
HStack {
Text("Hello world")
Text("#\(index)")
}
}
}
Its possible that the answer to this might just be wrapping your scrollView inside of a GeometryReader
Like done in the answer here -> How do I stretch a View to its parent frame with SwiftUI?

Resources