SwiftUI SegmentedPickerStyle width proportional to Text length - ios

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()

Related

Disable keyboard avoidance in a bottom sheet

TLDR: The view modifier .ignoresSafeArea(.keyboard) does not appear to work when used inside a bottom sheet. Is there a workaround?
In a SwiftUI View, tapping a TextField invokes the keyboard and the Textfield then moves upwards to avoid the keyboard.
struct ContentView: View {
#State var mytext: String = "Some text"
var body: some View {
VStack {
Spacer()
TextField("abc", text: $mytext)
Spacer()
}
}
}
This keyboard avoidance behaviour can be disabled by adding the .ignoresSafeArea modifier
struct ContentView: View {
#State var mytext: String = "Some text"
var body: some View {
VStack {
Spacer()
TextField("abc", text: $mytext)
Spacer()
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
and the TextField no longer moves upwards.
If this technique is applied inside to a view in a bottom sheet it no longer works and the entire sheet is pushed up by the keyboard.
struct ContentView: View {
#State var mytext: String = "Some text"
#State var isPresented: Bool = true
var body: some View {
Color.mint
.sheet(isPresented: $isPresented) {
VStack {
Spacer()
TextField("abc", text: $mytext)
Spacer()
}
.presentationDetents( [.fraction(0.33)] )
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
}
I've tried applying .ignoresSafeArea(.keyboard, edges: .bottom) to every view thats exposed in the code with no success.
I suspect that the bug is due to the bottom sheet implementation using a UIHostingController internally. This can been seen using Xcode's Debug View Hierarchy tool.
Others have described how UIHostingController does not respect the .ignoresSafeArea(.keyboard, edges: .bottom) modifier and have developed workarounds but these are not applicable here because the UIHostingController is created internally, not explicitly in my code.
Is there any way to get the view inside the sheet to ignore the keyboard and stay put?
I'm open to any and all suggestions. Thanks!

SwiftUI - TabView automatically changes selection when size changes

I have a paging style TabView in a HStack with another view. I'm adding a selection binding to the TabView to programmatically control selection.
When a tab is tapped the second view in the HStack disappears and the TabView resizes to fill the space.
However, if this is done after scrolling through my TabView's contents what happens is the selection changes many times as the size changes, causing the TabView to erratically scroll through many pages, rather than stay on the same selection and resize.
To reproduce:
Create a new project
Replace ContentView with the following code
Scroll through ~20 pages
Tap on the rectangle (blue or red)
The selection will change by itself.
Tested on iOS 14.5
struct ContentView: View {
#State var showSidebar = true
#State var selection = 0
var body: some View {
HStack {
Rectangle()
.frame(width: showSidebar ? 100 : 0)
.frame(maxHeight: .infinity)
TabView(selection: $selection) {
ForEach(0..<100, id: \.self) { i in
Rectangle()
.foregroundColor(i % 2 == 0 ? .red : .blue)
.padding()
.onTapGesture {
withAnimation {
showSidebar.toggle()
}
}
}
}.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
}

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

Change UIView background color in SwiftUI

Hello everyone. I'm creating a simple iOS app with SwiftUI, and I'd like to change my view's background color to a custom one I have.
This is something extremely easy to do but it seems that it's impossible to achieve in SwiftUI without using ZStacks or workarounds like that, which if you use a List, for example, don't work.
I want to change the color of the view, not use a ZStack with a custom color and then put the rest of the views on top of it. I tried using UIView.appearance().backgroundColor = color when initializing my view, but then all the view is hidden and the screen is filled with the color chosen.
As I'm not good at explaining, here you have some images describing the problem:
Without color change
With color change
My code
import SwiftUI
struct TabController: View {
#State private var selection = 0
init() {
UIView.appearance().backgroundColor = UIColor(named: "backgroundColor")
}
var body: some View {
TabView(selection: $selection) {
HomePageView()
.tabItem {
Image(systemName: "house.fill")
.font(.title)
}
.tag(0)
Text("Second View")
.font(.title)
.tabItem {
Image(systemName: "bell.fill")
.font(.title)
}
.tag(1)
}.edgesIgnoringSafeArea(.top)
}
}
Hope this will help to understand:
var body: some View {
Color.purple
.overlay(
VStack(spacing: 20) {
Text("Overlay").font(.largeTitle)
Text("Example").font(.title).foregroundColor(.white)
})
.edgesIgnoringSafeArea(.vertical)
}
Another If you use the views in Group
var body: some View {
Group {
Text("Hello SwiftUI!")
}
.background(Color.black)
}
For changing the background color, I think the current method most people, and myself, are using is using a ZStack. I haven't seen many problems with putting a UIViewRepresentable on top of it.
var body: some View {
ZStack {
Color.blue
.edgesIgnoringSafeArea(.all)
VStack {
Text("Hello World!")
}
}
}

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.

Resources