SwiftUI - Placing two pickers side-by-side in HStack does not resize pickers - picker

My goal is to have two pickers placed side-by-side horizontally with each picker taking up half the width of the screen. Imagine a UIPickerView that fits the width of the screen and has two components of equal width - that's what I'm attempting to recreate in SwiftUI.
Since pickers in SwiftUI do not currently allow for multiple components, the obvious alternative to me was just to place two pickers inside an HStack.
Here's some example code from a test project:
struct ContentView: View {
#State var selection1: Int = 0
#State var selection2: Int = 0
#State var integers: [Int] = [0, 1, 2, 3, 4, 5]
var body: some View {
HStack {
Picker(selection: self.$selection1, label: Text("Numbers")) {
ForEach(self.integers) { integer in
Text("\(integer)")
}
}
Picker(selection: self.$selection2, label: Text("Numbers")) {
ForEach(self.integers) { integer in
Text("\(integer)")
}
}
}
}
}
And here is the canvas:
SwiftUI - Pickers in HStack
The pickers do not resize to be half the width of the screen like I would expect. They retain their size and instead stretch the width of the content view, distorting the widths of other UI elements in the process (as I found out when I tried to do this in my other project).
I know that I can use UIViewRepresentable to get the effect that I want, but SwiftUI would be much easier to use given the complexity of what I'm trying to use this for.
Is it a bug that placing two pickers inside an HStack does not properly resize them, or do pickers in SwiftUI just have a fixed width that cannot be changed?
Update
Using GeometryReader, I've managed to get closer to resizing the pickers how I want, but not all the way.
Side note: you can also achieve this same imperfect result without using GeometryReader by simply setting the frame on each picker to .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity).
Here's the example code:
struct ContentView: View {
#State var selection1: Int = 0
#State var selection2: Int = 0
#State var integers: [Int] = [0, 1, 2, 3, 4, 5]
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
Picker(selection: self.$selection1, label: Text("Numbers")) {
ForEach(self.integers) { integer in
Text("\(integer)")
}
}
.frame(maxWidth: geometry.size.width / 2)
Picker(selection: self.$selection2, label: Text("Numbers")) {
ForEach(self.integers) { integer in
Text("\(integer)")
}
}
.frame(maxWidth: geometry.size.width / 2)
}
}
}
}
And here is the canvas:
Pickers in HStack with GeometryReader
The pickers are now closer to having the appearance that I want, but the sizing is still slightly off, and they're now overlapping each other in the middle.

The overlapping in the middle you can fix by adding a clipped() modifier. As for the width, I see them both exactly the same:
struct ContentView: View {
#State var selection1: Int = 0
#State var selection2: Int = 0
#State var integers: [Int] = [0, 1, 2, 3, 4, 5]
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
Picker(selection: self.$selection1, label: Text("Numbers")) {
ForEach(self.integers) { integer in
Text("\(integer)")
}
}
.frame(maxWidth: geometry.size.width / 2)
.clipped()
.border(Color.red)
Picker(selection: self.$selection2, label: Text("Numbers")) {
ForEach(self.integers) { integer in
Text("\(integer)")
}
}
.frame(maxWidth: geometry.size.width / 2)
.clipped()
.border(Color.blue)
}
}
}
}

As of iOS 15.5 (tested on simulator), Xcode 13.4 additionally to adding .clipped() you also need to add the following extension to prevent the touch area overlap issue mentioned in the comments from the other answers:
extension UIPickerView {
open override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric , height: 150)
}
}
Just place it before the struct of the View where you're using the Picker.
Source: TommyL on the Apple forum:
https://developer.apple.com/forums/thread/687986?answerId=706782022#706782022

Related

How do I make all views the same height in a SwiftUI View with an HStack?

I want a simple graph with a colored rectangle of variable height for each data point.
The white space below the colored rectangle should expand so that the bottom numbers line up, the way the top row of numbers does.
This is my view:
So I would like an idiomatic solution to getting the bottom row of numbers to line up with the 59. Any advice that points me in the right direction is welcome. Thanks.
Here's what I have so far:
struct DemoView: View {
var dataSource = [1, 0, 34, 12, 59, 44]
/// Provide a Dynamic Height Based on the Tallest View in the Row
#State private var height: CGFloat = .zero // < calculable height
/// The main view is a row of variable height views
var body: some View {
HStack(alignment: .top) {
Spacer()
/// i want these to all be the same height
ForEach(0 ..< 6) { index in
VStack {
Text("\(index)")
Rectangle()
.fill(Color.orange)
.frame(width: 20, height: CGFloat(self.dataSource[index]))
Text("\(dataSource[index])")
}
}
Spacer()
}
.alignmentGuide(.top, computeValue: { d in
DispatchQueue.main.async {
self.height = max(d.height, self.height)
}
return d[.top]
})
}
}
struct Demo_Preview: PreviewProvider {
static var previews: some View {
DemoView()
}
}
Edit to show the final results:
I made the changes Asperi suggested, changed the .top alignments to .bottom and got a very nice simple chart:
Here is possible (seems simplest) approach. Tested with Xcode 12 / iOS 14
struct DemoView: View {
var dataSource = [1, 0, 34, 12, 59, 44]
var body: some View {
HStack(alignment: .top) {
Spacer()
/// i want these to all be the same height
ForEach(0 ..< 6) { index in
VStack {
Text("\(index)")
Color.clear
.frame(width: 20, height: CGFloat(dataSource.max() ?? 0))
.overlay(
Color.orange
.frame(height: CGFloat(self.dataSource[index]))
, alignment: .top)
Text("\(dataSource[index])")
}
}
Spacer()
}
}
}

How to know if element comes out the screen in swiftUI?

I try to know if an element comes out the screen in my application. I see that when my element it outside the screen, onDesappear is not trigger. I don't know if there is an other solution to trigger this?
I made an example to explain what I want. I have a circle with an offset to force it out of the screen to a certain degree:
struct ContentView: View {
#ObservedObject var location: LocationProvider = LocationProvider()
#State var heading: Double = 0
var body: some View {
ZStack {
Circle()
.frame(width: 30, height: 30)
.background(Color.red)
.clipShape(Circle())
.foregroundColor(Color.clear)
.offset(y: 300)
.border(Color.black)
.rotationEffect(.degrees(self.heading))
.onReceive(self.location.heading) { heading in
self.heading = heading
}
.onDisappear(perform: { print("Desappear") })
Text("\(self.heading)")
}
}
}
Maybe it's possible with geometryReader?

Dynamic row hight containing TextEditor inside a List in SwiftUI

I have a List containing a TextEditor
struct ContentView: View {
#State var text: String = "test"
var body: some View {
List((1...10), id: \.self) { _ in
TextEditor(text: $text)
}
}
}
But it's items are not growing on height change of the TextEditor. I have tried .fixedSize() modifier with no luck. What am I missing here?
You can use an invisible Text in a ZStack to make it dynamic.
struct ContentView: View {
#State var text: String = "test"
var body: some View {
List((1...10), id: \.self) { _ in
ZStack {
TextEditor(text: $text)
Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack
}
}
}
}
Note that you should consider changing font size and other properties to match the TextEditor
As far as I can see from view hierarchy TextEditor is just simple wrapper around UITextView and does not have more to add, so you can huck into that layer and find UIKit solution for what you need, or ...
here is a demo of possible approach to handle it at SwiftUI level (the idea is to use Text view as a reference for wrapping behaviour and adjust TextEditor exactly to it)
Tested with Xcode 12b / iOS 14 (red border is added for better visibility)
Modified your view:
struct ContentView: View {
#State var text: String = "test"
#State private var height: CGFloat = .zero
var body: some View {
List {
ForEach((1...10), id: \.self) { _ in
ZStack(alignment: .leading) {
Text(text).foregroundColor(.clear).padding(6)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
})
TextEditor(text: $text)
.frame(minHeight: height)
//.border(Color.red) // << for testing
}
.onPreferenceChange(ViewHeightKey.self) { height = $0 }
}
}
}
}
Note: ViewHeightKey is a preference key, used in my other solutions, so can be get from there
ForEach and GeometryReader: variable height for children?
How to make a SwiftUI List scroll automatically?
Automatically adjustable view height based on text height in SwiftUI

State Variables and ForEach Swift

I'm trying to build a simple view in swiftUI with a foreach loop that contains three buttons, and animates these buttons based on a state variable. The problem is, when one button is pressed, all of them animate. I've been working on it for a good few hours but I can't figure out a way to have only one of the buttons in the view animate when it is pressed without just not using a foreach loop and just making three different state variables for three different CardView views. Here is the code for reference:
struct CardView: View {
var flipped:Bool
var body: some View {
VStack {
Image(flipped ? "cardFront" : "cardBack").renderingMode(.original)
.resizable()
.scaledToFit()
.frame(height: 200)
.rotation3DEffect(Angle(degrees: flipped ? 0 : 180), axis: (x: 0, y: 1, z: 0))
.opacity(1)
Text("Flip").padding()
}
}
}
struct ContentView: View {
#State var isFlipped = false
var body: some View {
HStack {
ForEach(0..<3) {number in
Button(action: {
withAnimation(.spring()) {
self.isFlipped.toggle()
}
}) {
CardView(flipped: self.isFlipped)
}
}
}
}
}
Would be very grateful for any pointers as I'm pretty new to all of this. I'd really like to figure out how to make this work in a foreach loop without the aforementioned work around.
Put the #State variable and the Button in the CardView. Here is a basic example:
I needed to use a Rectangle since I didn't have the images you used.
struct CardView: View {
#State var isFlipped = false
var body: some View {
VStack {
Rectangle()
.foregroundColor(self.isFlipped ? Color.blue : Color.red)
.frame(width: 100, height: 200)
.rotation3DEffect(Angle(degrees: self.isFlipped ? 0 : 180), axis: (x: 0, y: 1, z: 0))
.opacity(1)
Button(action: {
withAnimation(.spring()) {
self.isFlipped.toggle()
}
}) {
Text("Flip")
}
}
}
}
struct ContentView: View {
var body: some View {
HStack {
ForEach(0..<3) {number in
CardView()
}
}
}
}
I hope this helps!

How can I make a bunch of vertical sliders in swiftUI

I am trying to make a group (10 in the test code, but 32 in reality) of vertical faders using SwiftUI on an iPad app.
When making sliders horizontally, they stretch across the screen properly. When rotating those same sliders vertical, they seem locked into their horizontal dimensions. Is there a simple way to get the sliders to be vertical?
Horizontal (stretches across screen):
import SwiftUI
struct ContentView: View {
#State private var sliderVal: Double = 0
#State var values: [Double] = Array.init(repeating: 0.0, count: 10)
var body: some View {
VStack() {
ForEach((0 ... 9), id: \.self) {i in
HStack {
Text("\(i): ")
Slider(value: self.$values[i], in: 0 ... 100, step: 1.0)
.colorScheme(.dark)
Text("\(Int(self.values[i]))")
}
}
}
}
}
Switching the stack views and rotating the sliders (does not work):
struct ContentView: View {
#State private var sliderVal: Double = 0
#State var values: [Double] = Array.init(repeating: 0.0, count: 10)
var body: some View {
HStack() {
ForEach((0 ... 9), id: \.self) {i in
VStack {
Text("\(i): ")
Slider(value: self.$values[i], in: 0 ... 100, step: 1.0)
.colorScheme(.dark)
.rotationEffect(.degrees(-90))
Text("\(Int(self.values[i]))")
}
}
}
}
}
You can make vertical sliders from horizontal ones in SwiftUI, the trick is frame(width:) and frame(height:) are swapped. Here is what I did to make some really nice vertical sliders using the built-in SwiftUI functions
import SwiftUI
struct VerticalSlider: View {
#EnvironmentObject var playData : PlayData
var channelNumber:Int
var sliderHeight:CGFloat
var body: some View {
Slider(
value: self.$playData.flickerDimmerValues[self.channelNumber],
in: 0...255,
step: 5.0
).rotationEffect(.degrees(-90.0), anchor: .topLeading)
.frame(width: sliderHeight)
.offset(y: sliderHeight)
}
}
Then pass the Slider frame(width: ) to the above code in the variable sliderHeight as in the following code, where sliderHeight is the layout dimension provided by SwiftUI when it is laying out the view. This is a slick use of GeometryReader to size the slider exactly right.
import SwiftUI
struct VerticalBar: View {
#EnvironmentObject var playData : PlayData
var channelNumber:Int
var body: some View {
VStack {
GeometryReader { geo in
VerticalSlider(
channelNumber: self.channelNumber,
sliderHeight: geo.size.height
)
}
Text("\(self.channelNumber + 1)")
.font(.headline)
.frame(height: 10.0)
.padding(.bottom)
}
}
}
I then put 8 of the above views within a view area using a HStack:
HStack {
Spacer(minLength: 5.0)
VerticalBar(channelNumber: 0)
VerticalBar(channelNumber: 1)
VerticalBar(channelNumber: 2)
VerticalBar(channelNumber: 3)
VerticalBar(channelNumber: 4)
VerticalBar(channelNumber: 5)
VerticalBar(channelNumber: 6)
VerticalBar(channelNumber: 7)
Spacer(minLength: 5.0)
}
When completed, the vertical sliders look like this:
I made a custom VSlider view (source on GitHub) to address this issue. It should be virtually identical in usage to a Slider, as shown in the comparison demo below (although it's not generic, so it has to be used with a Double).
There's this SwiftUI library I found: link to repo. The nice thing about it is that the sliders are the same across macOS and other platforms. Also, you can customize the slider very easily.