Reputation: 120
I want to display a mathematical fraction. To do so I have a VStack that contains the numerator view (called ContainerView), a rectangle that is used as a fraction line and a denominator view (the same type of the numerator). Since the fraction is editable, the size of numerator and denominator can change and the fraction line has to adapt consequently. My first attempt was to put the fraction line in the background and use a geometry reader to get the frame of the fraction based only on the size of numerator and denominator. In this way the fraction line is in the centre of the background with the size I want. Here is the code:
struct FractionView: View {
@EnvironmentObject var buffer: Buffer
var numeratorBufferIndex: Int
var denominatorBufferIndex: Int
var body: some View {
VStack(alignment: .center, spacing: 0) {
ContainerView(activeBufferIndex: numeratorBufferIndex)
.environmentObject(self.buffer)
ContainerView(activeBufferIndex: denominatorBufferIndex)
.environmentObject(self.buffer)
}
.background(GeometryReader { geometry in
Rectangle()
.frame(width: geometry.frame(in: .local).width, height: 2)
.foregroundColor(Color(.systemGray))
})
}
}
The three properties are used to renderer numerator and denominator.The result is this. However since it is in the background it doesn't know the height of the numerator and the denominator. This gives problems when they have different sizes, like in this case. In the image you can see that the numerator has another fraction in it, so its view's height is bigger. This causes a problem because the first fraction line passes through part of the numerator.
To avoid this I thought the best way was to put the fraction line directly in the main VStack, so it would divide numerator and denominator without caring about their heights. However the rectangle shape takes all the space available and I don't know how to limit its width to the parent's width. This is the new code and what it looks like:
struct FractionView: View {
@EnvironmentObject var buffer: Buffer
var numeratorBufferIndex: Int
var denominatorBufferIndex: Int
var body: some View {
VStack(alignment: .center, spacing: 0) {
ContainerView(activeBufferIndex: numeratorBufferIndex)
.environmentObject(self.buffer)
Rectangle()
.frame(height: 2)
.foregroundColor(Color(.systemGray))
ContainerView(activeBufferIndex: denominatorBufferIndex)
.environmentObject(self.buffer)
}
}
}
To summarize I have to find a way to limit the rectangle's width to the width that the VStack would have without it. I have tried using preferences following the example of this article.
This is what I've tried:
struct FractionView: View {
@EnvironmentObject var buffer: Buffer
@State var fractionFrame: CGFloat = 0
var numeratorBufferIndex: Int
var denominatorBufferIndex: Int
var body: some View {
VStack(alignment: .center, spacing: 0) {
ContainerView(activeBufferIndex: numeratorBufferIndex)
.environmentObject(self.buffer)
Rectangle()
.frame(width: fractionFrame, height: 2)
.foregroundColor(Color(.systemGray))
ContainerView(activeBufferIndex: denominatorBufferIndex)
.environmentObject(self.buffer)
}
.coordinateSpace(name: "VStack")
.background(GeometryObteiner())
.onPreferenceChange(MyFractionPreferenceKey.self) { (value) in
print("Il valore della larghezza è \(value)")
self.fractionFrame = value
}
}
}
struct GeometryObteiner: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.frame(width: geometry.frame(in: .global).width, height: geometry.frame(in: .global).height)
.foregroundColor(.clear)
.preference(key: MyFractionPreferenceKey.self, value: geometry.frame(in: .named("VStack")).width)
}
}
}
struct MyFractionPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat = 10
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
However it seems like the onPreferenceChange is never called since the result is always that the fraction line has the width of the default Value (10).
Maybe the solution is much easier than what I think but I can't figure it out. I know that what I wrote may be a little confusing, if you need any clarification ask me in the comments.
Upvotes: 4
Views: 3272
Reputation: 257503
Here is applied same idea as commented to your scenario. Of course it is only sketch, but IMO it can be tuned as far as needed and made generic (replacing currently used Text
with, say, View
or FullFraction
itself to construct more complex fractions).
Demo (as DispatchQueue is used to avoid modification during view draw warning you should run it to see result):
Code (of screenshot demo):
struct FullFraction: View {
var whole: String = ""
var num: String
var denom: String
@State private var numWidth: CGFloat = 12.0 // just initial
@State private var denomWidth: CGFloat = 12.0 // just initial
var body: some View {
HStack {
Text(whole)
VStack {
numerator
divider
denominator
}
}
}
var numerator: some View {
Text(num)
.offset(x: 0, y: 8)
.alignmentGuide(HorizontalAlignment.center, computeValue: { d in
DispatchQueue.main.async {
self.numWidth = d.width
}
return d[HorizontalAlignment.center]
})
}
var denominator: some View {
Text(denom)
.offset(x: 0, y: -4)
.alignmentGuide(HorizontalAlignment.center, computeValue: { d in
DispatchQueue.main.async {
self.denomWidth = d.width
}
return d[HorizontalAlignment.center]
})
}
var divider: some View {
Rectangle().fill(Color.black).frame(width:max(self.numWidth, self.denomWidth), height: 2.0)
}
}
struct DemoFraction: View {
var body: some View {
VStack {
FullFraction(whole: "F", num: "(a + b)", denom: "c")
Divider()
FullFraction(whole: "ab12", num: "13", denom: "761")
Divider()
FullFraction(num: "1111", denom: "3")
}
}
}
Upvotes: 5
Reputation: 677
You can achieve the same effect while still keeping things declarative/supporting preview by wrapping in a ZStack and using layoutPriority modifier to keep the Shape within the bounds of the text.
Ends up being much simpler:
struct FullFraction: View {
var whole: String = ""
var num: String
var denom: String
var body: some View {
HStack {
Text(whole)
ZStack{
VStack {
numerator
denominator
}
divider
.layoutPriority(-1)
}
}
}
var numerator: some View {
Text(num)
.padding(2)
}
var denominator: some View {
Text(denom)
.padding(2)
}
var divider: some View {
Rectangle()
.fill(Color.black)
.frame(height:2.0)
}
}
Running in preview:
Upvotes: 1