Developing a SwiftUI algorithm based on screen size - swift

so I have a slider and I have a little VStack frame with some text in it that I'd like to move when you touch the slider (I'd like the VStack to stay directly above where the slider is all the time). I wasn't sure if there was a better way to do this so I tried just adjusting the leading padding of the VStack by making it dependent on the value of the slider. Here's my code:
struct ViewName: View {
#State private var sliderValue: Double = 0.0
var body: some View {
VStack {
HStack {
VStack {
Text("t")
}.frame(height: 30).background(Color.white)
.padding(.leading, CGFloat(CGFloat(DayRangeValue) * (UIScreen.main.bounds.width * 0.7)/24) + ((UIScreen.main.bounds.width * 0.15) + 10))
//The above padding is the algorithm: the addition of (UIScreen.main.bounds.width * 0.15 + 10) essentially works; it tells the VStack the starting position, and works on diff device simulators.
Spacer()
}
Slider(value: $sliderValue, in: 0...23).frame(width: UIScreen.main.bounds.width * 0.7)
//Note: the 0...23 cannot be changed, and the width of the slider is a percentage of screen size because I figured it can't be some static value, as that would only work on one device
}
}
}
Long story short, I'd like the "t" to be above the slider the wherever it is; on whatever device. The above algorithm for padding currently works perfectly on one of the iPad simulators, but it doesn't work properly on the iPhone 8 simulator (for example). If someone could help me figure out a padding algorithm based on screen size that works across all devices, that would be amazing.

If I correctly understood your intention, here is possible solution (geometry-independent). Tested with Xcode 11.4 / iOS 13.4
let kKnobWidth = CGFloat(24)
struct DemoLabelAboveSlider: View {
var range: ClosedRange<Double> = 0...23
#State private var sliderValue: Double = 0.0
var body: some View {
VStack(alignment: .leading) {
Slider(value: self.$sliderValue, in: self.range)
.padding(.vertical)
.overlay(GeometryReader { gp in
Text("t")
.position(x: CGFloat(self.sliderValue) * (gp.size.width - kKnobWidth) / CGFloat(self.range.upperBound) + kKnobWidth / 2, y: 0)
})
}
.padding(.horizontal) // << this one is only for better demo
}
}

Related

How can fix the size of view in mac in SwiftUI?

I am experiencing a new change with Xcode Version 14.1 about view size in macOS, in older version if I gave a frame size or use fixedSize modifier, the view would stay in that size, but with 14.1 I see that view could get expended even if I use frame or fixedSize modifier, which is not what I expect. For example I made an example to show the issue, when I update the size to 200.0, the view/window stay in bigger size, which I expect it shrinks itself to smaller size, so how can I solve the issue?
struct ContentView: View {
#State private var sizeOfWindow: CGFloat = 400.0
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
Button("update", action: {
if (sizeOfWindow == 200.0) { sizeOfWindow = 400.0 }
else { sizeOfWindow = 200.0 }
print(sizeOfWindow)
})
}
.frame(width: sizeOfWindow, height: sizeOfWindow)
.fixedSize()
}
}
Credit:
Credit to https://developer.apple.com/forums/thread/708177?answerId=717217022#717217022
Approach:
Use .windowResizability(.contentSize)
Along with windowResizability use one of the following:
if you want set size based on the content size use fixedSize()
If you want to hardcode the frame then directly set .frame(width:height:)
Code
#main
struct DemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.frame(width: 200, height: 200) //or use fixed size and rely on content
}
.windowResizability(.contentSize)
}
}

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

How can you make Text in SwiftUI appear sideways while also rotating the frame?

The goal is to have something that looks like this:
I am aware of .rotationEffect(), and the solution provided here.
However the problem with this solution is that it does not rotate the frame, it only rotates the text itself.
The image below is from the Canvas preview in Xcode and the code underneath it.
HStack(spacing: 10) {
Text("Distance")
.rotationEffect(.degrees(270))
.foregroundColor(.black)
.minimumScaleFactor(0.01)
Rectangle()
.foregroundColor(.black)
.frame(width: 3)
.padding([.top, .bottom])
Spacer()
}
As you can see, the text goes outside the frame and the frame keeps its original width, making it difficult to have the text hug the side of the view and have the vertical line hug the side of the text.
The only way I have been able to figure out how to get the desired result is to use the .offset(x:) modifier. But this feels messy and could lead to bugs down the road I think.
Is there any way to be able to rotate the frame along with the text?
If we talk about dynamic detection, then we need to measure text frame before rotation and apply to frame size of changed width/height
Note: of course in simplified variant frame width can be just hardcoded
Here is main part:
#State private var size = CGSize.zero
var body: some View {
HStack(spacing: 10) {
Text("Distance")
.fixedSize() // << important !!
.background(GeometryReader {
Color.clear
.preference(key: ViewSizeKey.self, value: $0.frame(in: .local).size)
})
.onPreferenceChange(ViewSizeKey.self) {
self.size = $0 // << here !!
}
.rotationEffect(.degrees(270))
.frame(width: size.height, height: size.width) // << here !!
Used preference key:
public struct ViewSizeKey: PreferenceKey {
public typealias Value = CGSize
public static var defaultValue = CGSize.zero
public static func reduce(value: inout Value, nextValue: () -> Value) {
}
}
Test module/dependecies on GitHub

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.

How to scale text to fit parent view with SwiftUI?

I'd like to create a text view inside a circle view. The font size should be automatically set to fit the size of the circle. How can this be done in SwiftUI? I tried scaledToFill and scaledToFit modifiers, but they have no effect on the Text view:
struct ContentView : View {
var body: some View {
ZStack {
Circle().strokeBorder(Color.red, lineWidth: 30)
Text("Text").scaledToFill()
}
}
}
One possible "hack" is to use a big font size and a small scale factor so it will shrink itself:
ZStack {
Circle().strokeBorder(Color.red, lineWidth: 30)
Text("Text")
.padding(40)
.font(.system(size: 500))
.minimumScaleFactor(0.01)
}
}
You want to allow your text to:
shrink up to a certain limit
on 1 (or several) line(s)
You choose this scale factor limit to suit your need. Typically you don't shrink beyond readable or beyond the limit that will make your design look bad
struct ContentView : View {
var body: some View {
ZStack {
Circle().strokeBorder(Color.red, lineWidth: 30)
Text("Text")
.scaledToFill()
.minimumScaleFactor(0.5)
.lineLimit(1)
}
}
}
One can use GeometryReader in order to make it also work in landscape mode.
It first checks if the width or the height is smaller and then adjusts the font size according to the smaller of these.
GeometryReader{g in
ZStack {
Circle().strokeBorder(Color.red, lineWidth: 30)
Text("Text")
.font(.system(size: g.size.height > g.size.width ? g.size.width * 0.4: g.size.height * 0.4))
}
}
Here's a solution that hides the text resizing code in a custom modifier which can be applied to any View, not just a Circle, and takes a parameter specifying the fraction of the View that the text should occupy.
(I have to agree that while #szemian's solution is still not ideal, her method seems to be the best we can do with the current SwiftUI implementation because of issues inherent in the others. #simibac's answer requires fiddling to find a new magic number to replace 0.4 any time the text or its attributes--font, weight, etc.--are changed, and #giuseppe-sapienza's doesn't allow the size of the circle to be specified, only the font size of the text.)
struct FitToWidth: ViewModifier {
var fraction: CGFloat = 1.0
func body(content: Content) -> some View {
GeometryReader { g in
content
.font(.system(size: 1000))
.minimumScaleFactor(0.005)
.lineLimit(1)
.frame(width: g.size.width*self.fraction)
}
}
}
Using the modifier, the code becomes just this:
var body: some View {
Circle().strokeBorder(Color.red, lineWidth: 30)
.aspectRatio(contentMode: .fit)
.overlay(Text("Text")
.modifier(FitToWidth(fraction: fraction)))
}
Also, when a future version of Xcode offers SwiftUI improvements that obviate the .minimumScaleFactor hack, you can just update the modifier code to use it. :)
If you want to see how the fraction parameter works, here's code to let you adjust it interactively with a slider:
struct ContentView: View {
#State var fraction: CGFloat = 0.5
var body: some View {
VStack {
Spacer()
Circle().strokeBorder(Color.red, lineWidth: 30)
.aspectRatio(contentMode: .fit)
.overlay(Text("Text")
.modifier(FitToWidth(fraction: fraction)))
Slider(value: $fraction, in:0.1...0.9, step: 0.1).padding()
Text("Fraction: \(fraction, specifier: "%.1f")")
Spacer()
}
}
}
and here's what it looks like:
I had fixed size button and this worked for me to autoshrink long text.
Text("This is a long label that will be scaled to fit:")
.lineLimit(1)
.minimumScaleFactor(0.5)
Source: Apple
I did a mix of #Simibac's and #Anton's answers, only to be broken by iOS 14.0, so here's what I did to fix it. Should work on SwiftUI 1.0 as well.
struct FitSystemFont: ViewModifier {
var lineLimit: Int
var minimumScaleFactor: CGFloat
var percentage: CGFloat
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.font(.system(size: min(geometry.size.width, geometry.size.height) * percentage))
.lineLimit(self.lineLimit)
.minimumScaleFactor(self.minimumScaleFactor)
.position(x: geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY)
}
}
}
As you can see I used the geometry proxy's frame(in:) method to get the local coordinate space, and then use .midX and .midY to center it properly, since proper centering is what broke for me on iOS 14.
Then I set up an extension on View:
extension View {
func fitSystemFont(lineLimit: Int = 1, minimumScaleFactor: CGFloat = 0.01, percentage: CGFloat = 1) -> ModifiedContent<Self, FitSystemFont> {
return modifier(FitSystemFont(lineLimit: lineLimit, minimumScaleFactor: minimumScaleFactor, percentage: percentage))
}
}
So usage is like this:
Text("Your text")
.fitSystemFont()
To achieve this you don't need the ZStack. You can add a background to the Text:
Text("Text text text?")
.padding()
.background(
Circle()
.strokeBorder(Color.red, lineWidth: 10)
.scaledToFill()
.foregroundColor(Color.white)
)
The result is this:
Building on #JaimeeAz answer. Added an option to specify the minimum font.
import SwiftUI
public struct FitSystemFont: ViewModifier {
public var lineLimit: Int?
public var fontSize: CGFloat?
public var minimumScaleFactor: CGFloat
public var percentage: CGFloat
public func body(content: Content) -> some View {
GeometryReader { geometry in
content
.font(.system(size: min(min(geometry.size.width, geometry.size.height) * percentage, fontSize ?? CGFloat.greatestFiniteMagnitude)))
.lineLimit(self.lineLimit)
.minimumScaleFactor(self.minimumScaleFactor)
.position(x: geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY)
}
}
}
public extension View {
func fitSystemFont(lineLimit: Int? = nil, fontSize: CGFloat? = nil, minimumScaleFactor: CGFloat = 0.01, percentage: CGFloat = 1) -> ModifiedContent<Self, FitSystemFont> {
return modifier(FitSystemFont(lineLimit: lineLimit, fontSize: fontSize, minimumScaleFactor: minimumScaleFactor, percentage: percentage))
}
}
I had the same problem for a Timer. Unfortunately a timer changes the text once a second, so the text was jumping around and scaling up and down all the time.
My approach was to figure, what was the longest possible timer that could be displayed - in my case "-44:44:44" - and with 50pt size that would result in a 227.7pt big frame. 227 divided by 50 (point size I used before) a width of 4.5 (rounded down) per point size.
Careful: with 1pt size it gave me a 5.3 point big frame - so the bigger the font the closer to the actual text size without the frame it gets.
As I was using a GeometryReader anyway I could simple set a fixed text size, using
Text("-44:44:44")
.font(.system(size: geometry.size.width / 4.5))
Works perfectly well, if there is no '-' or no hours shown I have some space to the left and right, but the text doesn't jump around.
this could be refined with different scale-factors, depending on the amount of digits shown there - so another scale factor for "-mm:ss".
This would lead to a "jump" when the hours are shown or hidden - but that happens rarely for my need.