How can I make a bunch of vertical sliders in swiftUI - swift

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.

Related

How do I prevent SwiftUI View from taking up any space?

I’m pretty new to SwiftUI and am working on this little project. I want to place an image either on top of or between lines, depending on the position variable.
struct ContentView: View {
#State var position = 5
var body: some View {
VStack(spacing: 20){
ForEach(1...15, id: \.self){i in
ZStack{
if i%2 != 0{
Rectangle()
.frame(height: 4)
.foregroundColor(.white)
}
if i == position{
Circle()
.frame(height: 30)
.foregroundColor(.white)
}
}
}
}
}
}
This is the result ContentViewImage:-
If i is odd, we create a line. If i equals position, we create a circle on top of the line or if we didn’t create a line the circle will be drawn between the other lines.
My problem with this is that the lines don’t stay still when I change the value of position.* This is because the circle takes up space and pushes the lines away from it. The lines above and below the circle gets pushed away more when the circle is between two lines which causes the lines to kind of go back and forth as I change from between to on top of lines.
How would I go about fixing this?
There is two issues here: non-constant height of row (because row with circle and w/o circle have different heights) and conditional layout (absent rectangles gives different layout).
Here is a possible solution. Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
#State var position = 4
var body: some View {
VStack(spacing: 20){
ForEach(1...15, id: \.self){i in
ZStack {
Rectangle()
.frame(height: 4)
.foregroundColor(i%2 == 0 ? .clear : .white) // << here !!
if i == position{
Circle()
.foregroundColor(.white)
.frame(height: 30)
}
}.frame(height: 4) // << here !!
}
}
}
}

SwiftUI gestures in the toolbar area ignored

I'd like to implement a custom slider SwiftUI component and put it on the toolbar area of a SwiftUI Mac app. However the gesture of the control gets ignored as the system's window moving gesture takes priority. This problem does not occur for the system UI controls, like Slider or Button.
How to fix the code below so the slider works in the toolbar area as well, not just inside the window similar to the default SwiftUI components?
struct MySlider: View {
#State var offset: CGFloat = 0.0
var body: some View {
GeometryReader { gr in
let thumbSize = gr.size.height
let maxValue = (gr.size.width - thumbSize) / 2.0
let gesture = DragGesture(minimumDistance: 0).onChanged { v in
self.offset = max(min(v.translation.width, maxValue), -maxValue)
}
ZStack {
Capsule()
Circle()
.foregroundColor(Color.yellow)
.frame(width: thumbSize, height: thumbSize)
.offset(x: offset)
.highPriorityGesture(gesture)
}
}.frame(width: 100, height: 20)
}
}
struct ContentView: View {
#State var value = 0.5
var body: some View {
MySlider()
.toolbar {
MySlider()
Slider(value: $value).frame(width: 100, height: 20)
}.frame(width: 500, height: 100)
}
}
Looks like design limitation (or not implemented yet feature - Apple does not see such view as user interaction capable item).
A possible workaround is to wrap you active element into button style. The button as a container interpreted as user-interaction-able area but all drawing and handling is in your code.
Tested with Xcode 13.2 / macOS 12.2
Note: no changes in your slider logic
struct MySlider: View {
var body: some View {
Button("") {}.buttonStyle(SliderButtonStyle())
}
struct SliderButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
MySliderContent()
}
struct MySliderContent: View {
#State var offset: CGFloat = 0.0
var body: some View {
GeometryReader { gr in
let thumbSize = gr.size.height
let maxValue = (gr.size.width - thumbSize) / 2.0
let gesture = DragGesture(minimumDistance: 0).onChanged { v in
self.offset = max(min(v.translation.width, maxValue), -maxValue)
}
ZStack {
Capsule()
Circle()
.foregroundColor(Color.yellow)
.frame(width: thumbSize, height: thumbSize)
.offset(x: offset)
.highPriorityGesture(gesture)
}
}.frame(width: 100, height: 20)
}
}
}
}

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?

SwiftUI View struct without reloading

I would like to create a starry background view in SwiftUI that has its stars located randomly using Double.random(), but does not reinitialise them and move them when the parent view reloads its var body.
struct ContentView: View {
#State private var showButton = true
var body: some View {
ZStack {
BackgroundView()
if showButton {
Button("Tap me"){
self.showButton = false
}
}
}
}
}
I define my background view as such.
struct BackgroundView: View {
var body: some View {
ZStack {
GeometryReader { geometry in
Color.black
ForEach(0..<self.getStarAmount(using: geometry), id: \.self){ _ in
Star(using: geometry)
}
LinearGradient(gradient: Gradient(colors: [.purple, .clear]), startPoint: .bottom, endPoint: .top)
.opacity(0.7)
}
}
}
func getStarAmount(using geometry: GeometryProxy) -> Int {
return Int(geometry.size.width*geometry.size.height/100)
}
}
A Star is defined as
struct Star: View {
let pos: CGPoint
#State private var opacity = Double.random(in: 0.05..<0.4)
init(using geometry: GeometryProxy) {
self.pos = CGPoint(x: Double.random(in: 0..<Double(geometry.size.width)), y: Double.random(in: 0..<Double(geometry.size.height)))
}
var body: some View {
Circle()
.foregroundColor(.white)
.frame(width: 2, height: 2)
.scaleEffect(CGFloat(Double.random(in: 0.25...1)))
.position(pos)
.opacity(self.opacity)
.onAppear(){
withAnimation(Animation.linear(duration: 2).delay(Double.random(in: 0..<6)).repeatForever()){
self.opacity = self.opacity+0.5
}
}
}
}
As one can see, a Star heavily relies on random values, for both its animation (to create a 'random' twinkling effect) as well as its position. When the parent view of the BackgroundView, ContentView in this example, gets redrawn however, all Stars get reinitialised, their position values change and they move across the screen. How can this best be prevented?
I have tried several approaches to prevent the positions from being reinitialised. I can create a struct StarCollection as a static let of BackgroundView, but this is quite cumbersome. What is the best way to go about having a View dependent on random values (positions), only determine those positions once?
Furthermore, the rendering is quite slow. I have attempted to call .drawingGroup() on the ForEach, but this then seems to interfere with the animation's opacity interpolation. Is there any viable way to speed up the creation / re-rendering of a view with many Circle() elements?
The slowness coming out from the overcomplicated animations setting in onAppear, you only need the self.opacity state change to initiate the animation, so please move animation out and add to the shape directly.
Circle()
.foregroundColor(.white)
.frame(width: 2, height: 2)
.scaleEffect(CGFloat(Double.random(in: 0.25...1)))
.position(pos)
.opacity(self.opacity)
.animation(Animation.linear(duration: 0.2).delay(Double.random(in: 0..<6)).repeatForever())
.onAppear(){
// withAnimation{ //(Animation.linear(duration: 2).delay(Double.random(in: 0..<6)).repeatForever()){
self.opacity = self.opacity+0.5
// }
}

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

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