I am making a swiftUI calendar and met a weird truncation problem of SwiftUI Text View.
struct test : View {
var body: some View {
GeometryReader { geometry in
HStack(alignment: .center, spacing: 0) {
ForEach(0..<7) { _ in
Text("Tue").frame(width: geometry.size.width / 7).border(Color.red, width: 2)
}
}
}
}
}
In the beginning, I thought it might be because the Text View size is not big enough. But when I decrease the Text View Width, the truncation is gone. I also tried to set a smaller font and it didn't work too. Thanks for any hint!
struct test : View {
var body: some View {
GeometryReader { geometry in
HStack(alignment: .center, spacing: 0) {
ForEach(0..<7) { _ in
Text("Tue").frame(width: geometry.size.width / 8).border(Color.red, width: 2)
}
}
}
}
}
Beta 5
In beta 5 it seems the problem has been resolved.
Beta 4 and Previous
It is definitely a bug that only shows in the iPhone XS, but not with the iPhone XR. Note that the XS is 375 points wide, while the XR is 414 points. However, that has nothing to do with it. 375 is more than enough to fit the 7 labels.
I think you should submit a bug report, and in the meantime, use the XR for development. If anyone has an actual device, it would be nice to know if the bug is also present there.
I created this small example that shows how erratically it works on the Xs
And here is the Xr, which works as it should:
Here's the code of the example:
struct ContentView: View {
#State private var slider: Float = 100.0
var body: some View {
VStack {
GeometryReader { geometry in
HStack(alignment: .center, spacing: 0) {
ForEach(0..<7) { _ in
Text("Tue").frame(width: geometry.size.width / 7, height: 30).border(Color.blue)
}
}
}.frame(width: Length(slider), height: 40)
Text("\(slider)")
Slider(value: self.$slider, from: 100.0, through: 375.0, by: 1.0)
Spacer()
}
}
}
Related
Edit This is a regression in iOS 15 beta. The code works as expected on iOS 14.5:
I have submitted a bug to Apple.
I have a dashboard-style screen in my SwiftUI app, where I am using a LazyVGrid with a single .adaptative column to layout my dashboard widgets, where widgets are laid out in wrapping rows.
It works as I want it to.
However, if a widget happens to be taller than others, I would like other widgets in the same row to grow vertically, so they end up having the same height as the tallest of the row.
This small bit of code illustrates my problem:
struct ContentView: View {
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
VStack {
Spacer()
Text("Hello")
}
.border(.red)
Text("Lorem ipsum")
.border(.blue)
}
.border(.green)
.padding(.horizontal, 100)
}
}
The result is:
I would like the red box (VStack containing Spacer + Hello) to be as tall as the blue box (lorem ipsum).
How could I accomplish that?
Please don't suggest using an HStack, as the above example is only to illustrate my problem with LazyVGrid. I do need to use the grid because I have quite a few children to layout, and the grid works great between phone and iPad form factors (adjusting the number of columns dynamically, exactly as I want it).
It looks like Apple begins (for unknown reason) to apply fixedSize for views in grid to make layout based on known intrinsic content sizes. The Spacer, Shape, Color, etc. do not have intrinsic size so we observe... that what's observed.
A possible approach to resolve this is perform calculations by ourselves (to find dynamically max height and apply it to all cells/views).
Here is a demo (with simple helper wrapper for cell). Tested with Xcode 13.2 / iOS 15.2
struct ContentView: View {
#State private var viewHeight = CGFloat.zero
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
GridCell(height: $viewHeight) {
VStack {
Spacer()
Text("Hello")
}
}.border(.red)
GridCell(height: $viewHeight) {
Text("Lorem ipsum asdfd")
}.border(.blue)
}
.onPreferenceChange(ViewHeightKey.self) {
self.viewHeight = max($0, viewHeight)
}
.border(.green)
.padding(.horizontal, 100)
}
}
struct GridCell<Content: View>: View {
#Binding var height: CGFloat
#ViewBuilder let content: () -> Content
var body: some View {
content()
.frame(minHeight: height)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
I had exactly same problem. My LazyVGrid looked great on iOS 14, but now its items have different heights.
I found a dirty workaround to force the items have same height:
In my case I have only several items in each LazyVGrid (So it won't cause too much performance drop), and it is easy for me to know which item has the largest height. So I made a ZStack and put a transparent highest item behind the actual item.
struct ContentView: View {
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
ZStack {
Text("Lorem ipsum") // This is the highest item.
.opacity(0) // Make it transparent.
Text("Hello")
}
.border(.red)
Text("Lorem ipsum")
.border(.blue)
}
.border(.green)
.padding(.horizontal, 100)
}
}
This workaround works in my case, but I don't recommend using it widely in your app especially when you have a lot of items in the grid.
I'm looking for a similar way https://github.com/stokatyan/ScrollCounter in SwiftUI
This is a very rudimentary build of the same thing. I'm not sure if you're wanting to do it on a per-digit basis, however this should give you a solid foundation to work off of. The way that I'm handling it is by using a geometry reader. You should be able to easily implement this view by utilizing an HStack for extra digits/decimals. The next thing I would do would be to create an extension that handles returning the views based on the string representation of your numeric value. Then that string is passed as an array and views created for each index in the array, returning a digit flipping view. You'd then have properties that are having their state observed, and change as needed. You can also attach an .opacity(...) modifier to give it that faded in/out look, then multiply the opacity * n where n is the animation duration.
In this example you can simply tie your Digit value to the previewedNumber and it should take over from there.
struct TestView: View {
#State var previewedNumber = 0;
var body: some View {
ZStack(alignment:.bottomTrailing) {
GeometryReader { reader in
VStack {
ForEach((0...9).reversed(), id: \.self) { i in
Text("\(i)")
.font(.system(size: 100))
.fontWeight(.bold)
.frame(width: reader.size.width, height: reader.size.height)
.foregroundColor(Color.white)
.offset(y: reader.size.height * CGFloat(previewedNumber))
.animation(.linear(duration: 0.2))
}
}.frame(width: reader.size.width, height: reader.size.height, alignment: .bottom)
}
.background(Color.black)
Button(action: {
withAnimation {
previewedNumber += 1
if (previewedNumber > 9) {
previewedNumber = 0
}
}
}, label: {
Text("Go To Next")
}).padding()
}
}
}
We have a custom view, that we use in forms (its main feature is to be able to show a nice border on if there is a validation error on the form):
struct ButtonNavigationLink: View {
var label: String
var markingType: MarkingType = .none
var action: () -> Void
var body: some View {
GeometryReader { geometry in
HStack {
Text(label)
.offset(x: 5, y: 0)
Spacer()
Image("PfeilRechts")
}
.frame(width: geometry.size.width + 10, height: geometry.size.height, alignment: .leading)
.padding(.trailing, 5)
.border(markingType.getMarkingColor())
.offset(x: -5, y: 0)
// to allow click on the whole width
.contentShape(Rectangle())
.onTapGesture {
print("in ButtonNavigationLink")
self.action()
}
}
}
}
The onTapGesture works well, until it doesn't work anymore.
It happens if we open other sheets and then eventually come to this one.
The funny thing is, that - when is not working anymore - if you scroll, even so lightly, the form containing it, it starts working again. The form is not disabled
In my app, I drag a View horizontally to set a position of a model object. I can also drag it downward and release it to delete the model object. When it has been dragged downward far enough, I indicate the potential for deletion by changing its appearance. The problem is that this change interrupts the DragGesture. I don't understand why this happens.
The example code below demonstrates the problem. You can drag the light blue box side to side. If you pull down, it and it turns to the "rays" system image, but the drag dies.
The DragGesture is applied to the ZStack with a size of 50x50. The ZStack should continue to exist across that state change, no? Why is the drag gesture dying?
struct ContentView: View {
var body: some View {
ZStack {
DraggableThing()
}.frame(width: 300, height: 300)
.border(Color.black, width: 1)
}
}
struct DraggableThing: View {
#State private var willDeleteIfDropped = false
#State private var xPosition: CGFloat = 150
var body: some View {
//Rectangle()
// .fill(willDeleteIfDropped ? Color.red : Color.blue.opacity(0.3))
ZStack {
if willDeleteIfDropped {
Image(systemName: "rays")
} else {
Rectangle().fill(Color.blue.opacity(0.3))
}
}
.frame(width: 50, height: 50)
.position(x: xPosition, y: 150)
.gesture(DragGesture()
.onChanged { val in
print("drag changed \(val.translation)")
self.xPosition = 150 + val.translation.width
self.willDeleteIfDropped = (val.translation.height > 25)
}
.onEnded { val in
print("drag ended")
self.xPosition = 150
}
)
}
}
You need to keep content, which originally captured gesture. So your goal can be achieved with the following changes:
ZStack {
Rectangle().fill(Color.blue.opacity(willDeleteIfDropped ? 0.0 : 0.3))
if willDeleteIfDropped {
Image(systemName: "rays")
}
}
I'm trying to achieve a layout like this:
For this simple example the base would be something like this:
HStack {
VStack {
Text("Foo")
Text("W")
Text("X")
}
VStack {
Text("Bar")
Text("Y")
Text("Z")
}
}
Now that relativeSize(...) is deprecated, the only remaining option I see is GeometryReader, but the issue with it is that once it's itself nested in another stack, it will attempt to fill all available space, in other terms it cannot determine the size it's containing stack would have had if it wasn't present in it and I end up with an overly sized stack.
I wonder if I'm missing something or if this is just how stacks work, or maybe a beta bug?
Thank you for your help
EDIT:
I did this:
VStack {
GeometryReader { /* #kontiki code */ }
Text("Other")
Spacer().layoutPriority(1)
}
But unfortunately this is the result I get, do you think this is a SwiftUI bug?
Second Attempt
I think this does exactly what you need. It uses Preferences. If you need to learn more about how to use SwiftUI preferences, check this post I wrote. They are fully explained there, but it is too long of a subject to post it here.
import SwiftUI
struct MyPref: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct SetWidthPreference: View {
var body: some View {
GeometryReader { proxy in
Rectangle().fill(Color.clear).preference(key: MyPref.self, value: proxy.size.width)
}
}
}
struct ContentView : View {
#State private var width: CGFloat = 0
var body: some View {
VStack {
ScrollView {
HStack(spacing: 0) {
VStack {
Text("Foo")
Text("Bar")
}.frame(width: width * 0.7, alignment: .leading).fixedSize().border(Color.red)
VStack {
Text("W")
Text("Y")
}.frame(width: width * 0.15).fixedSize().border(Color.red)
VStack {
Text("X")
Text("Z")
}.frame(width: width * 0.15).fixedSize().border(Color.red)
}
Text("Text below table")
}
.border(Color.green, width: 3)
HStack { Spacer() }.background(SetWidthPreference())
}
.onPreferenceChange(MyPref.self) { w in
print("\(w)")
DispatchQueue.main.async {
self.width = w
}
}
}
}
Previous Attempt (I keep it here, so comments make sense)
This example will draw 3 columns with 0.7, 0.15 and 0.15 of the parent's width. It's a starting point that you can fine tune. Note that the borders are there so that you can see what you are doing, of course you can remove them.
If GeometryReader is expanding too much, explain exactly what is that you want to accomplish, providing more context on the surroundings of the table (i.e., GeometryReader).
struct ContentView : View {
var body: some View {
GeometryReader { proxy in
HStack(spacing: 0) {
VStack {
Text("Foo")
Text("Bar")
}.frame(width: proxy.size.width * 0.7, alignment: .leading).fixedSize().border(Color.red)
VStack {
Text("W")
Text("Y")
}.frame(width: proxy.size.width * 0.15).fixedSize().border(Color.red)
VStack {
Text("X")
Text("Z")
}.frame(width: proxy.size.width * 0.15).fixedSize().border(Color.red)
}
}.padding(20)
}
}