How to get Text width with SwiftUI? - ios

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 !!
}

Related

In SwiftUI how can I have a View exceed its parents frame/boundary in a List?

Consider the following code:
import SwiftUI
struct ContentView: View {
private var listContent: [String] = ["This", "is", "a", "placeholder", "list"]
var body: some View {
List {
ForEach(listContent, id: \.self) { entry in
listElement(text: entry)
}
}
}
}
struct listElement: View {
#State var text: String
var body: some View {
Section() {
HStack {
Rectangle()
.fill(.green)
.frame(width: 30, height: nil)
Text(text)
}
.listRowInsets(EdgeInsets())
}
}
}
which generates this
I would like to add additional markers to the left of each List element and/or section, like this (mockup):
I have the data that describes the width of the green bars leading into the list elements on the child and parent element. Coming from web development I expect to do something like a child element having negative margin and is thus exceeding its parents container. However I could not get it to work with SwiftUI's padding. Is there a way to do this?
For example I have tried the following modification to the previously mentioned listElement:
var body: some View {
Section() {
HStack {
Rectangle()
.fill(.red)
.frame(width: 30, height: 10)
.padding(.leading, -20)
.zIndex(9999)
Rectangle()
.fill(.green)
.frame(width: 30, height: nil)
Text(text)
}
.listRowInsets(EdgeInsets())
}
}
which results in:
The red rectangle overflow is hidden. I've tried pushing it to the top with the dreaded zIndex hack but it doesn't work. Even setting the Lists .scrollContentBackground to .hidden and the .background to .clear doesn't show the red Rectangle. I need the overflow to be visible.

SwiftUI SegmentedPickerStyle width proportional to Text length

I am trying to build a Segmented Control with width proportional to the length of the text. My current code:
import SwiftUI
struct ContentView: View {
#State private var selectedElement = ""
var body: some View {
VStack {
HStack {
Text("Some choice").foregroundColor(Color.black)
Spacer()
Picker("aaa", selection: $selectedElement, content: {
Text("choice 1").foregroundColor(Color.black)
Text("another choice").foregroundColor(Color.black)
})
}.pickerStyle(SegmentedPickerStyle()).padding(16)
Spacer()
}.background(Color.white)
}
}
Building this code results in:
code result
What I would like to see is:
expected result where red rectangles show how much space each part should take. How can I achieve this ? Also, why does the .foregroundColor(Color.black) has no effect on the Text color within the Segmented Control ?
The solution is simple, add .fixedSize() after your picker:
Picker("aaa", selection: $selectedElement, content: {
Text("choice 1").foregroundColor(Color.black)
Text("another choice").foregroundColor(Color.black)
}).fixedSize()

Adding shapes in List row of SwiftUI

I am trying to create a List View where rows looks like this:
However, I am unable to align the Circle on the leading side. Tried using Spacer(), HStack within VStack, it just doesn't work. Here's my code and its output.
struct PeopleView: View {
let people = ["Adam", "James"]
var body: some View {
NavigationView {
List {
ForEach(people, id: \.self) { person in
HStack {
Circle()
VStack {
Text("\(person)")
}
}
}
}
.navigationBarTitle("People", displayMode: .inline)
}
}
}
Actually you don't need shape itself in this case, but only as a mask to visually present text in circle.
So the solution can be like following
HStack {
Text(person.prefix(2).uppercased()).bold()
.foregroundColor(.white)
.padding()
.background(Color.red)
.mask(Circle()) // << shaping text !!
Spacer()
VStack {
Text("\(person)")
}
}
Some views in SwiftUI fill all available space. Such views are shapes, colors, spacers, dividers, and GeometryReader.
Your Circle is a shape and it behaves similarly like a Spacer (in terms of filling space).
If you replace Circle with an image of a circle it will work:
ForEach(people, id: \.self) { person in
HStack {
Image(systemName: "circle.fill")
.imageScale(.large)
Spacer()
VStack {
Text("\(person)")
}
}
}
That is happening because you did not give a fixed (or relative) frame to the Circle Shape, so the Circle is taking up the maximum available width.
If you add a frame(width:height:), everything should work correctly:
struct PeopleView: View {
let people = ["Adam", "James"]
var body: some View {
NavigationView {
List {
ForEach(people, id: \.self) { person in
HStack {
Circle()
.frame(width: 50, height: 50)
VStack {
Text("\(person)")
}
}
}
}
.navigationBarTitle("People", displayMode: .inline)
}
}
}

SwiftUI Login Page Layout

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.

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