Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Square Text using aspectRatio in SwiftUI

Tags:

swiftui

I'm trying to achieve a following layout using Swift UI…

enter image description here

struct ContentView : View {
    var body: some View {
        List(1...5) { index  in

            HStack {
                HStack {
                    Text("Item number \(index)")
                    Spacer()
                    }.padding([.leading, .top, .bottom])
                    .background(Color.blue)

                Text("i")
                    .font(.title)
                    .italic()
                    .padding()
                    .aspectRatio(1, contentMode: .fill)
                    .background(Color.pink)

                }.background(Color.yellow)
        }
    }
}

I'd like the Text("i") to be square, but setting the .aspectRatio(1, contentMode: .fill) doesn't seem to do anything…

enter image description here

I could set the frame width and height of the text so it's square, but it seems that setting the aspect ratio should achieve what I want in a more dynamic way.

What am I missing?

like image 881
Ashley Mills Avatar asked Jun 26 '19 12:06

Ashley Mills


People also ask

What is aspectRatio in Swift?

aspectRatio. A size that specifies the ratio of width to height to use for the resulting view.

What is aspect ratio SwiftUI?

aspectRatio(100/50, contentMode: . fill) . frame(width: 320, height: 480)

What is resizable SwiftUI?

resizable(capInsets:resizingMode:)Sets the mode by which SwiftUI resizes an image to fit its space.


4 Answers

I think this is what you're looking for:

List(1..<6) { index  in
                HStack {
                    HStack {
                        Text("Item number \(index)")
                        
                        Spacer()
                    }
                    .padding([.leading, .top, .bottom])
                    .background(Color.blue)
                    
                    Text("i")
                        .font(.title)
                        .italic()
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                        .aspectRatio(1, contentMode: .fill)
                        .background(Color.pink)
                        .fixedSize(horizontal: true, vertical: false)
                        .padding(.leading, 6)
                }
                .padding(6)
                .background(Color.yellow)
            }

The answer being said, i don't recommend giving SwiftUI too much freedom to decide the sizings. one of the biggest SwiftUI problems right now is the way it decides how to fit the views into each other. if something goes not-so-good on SwiftUI's side, it can result in too many calls to the UIKit's sizeToFit method which can slowdown the app, or even crash it.

but, if you tried this solution in a few different situations and it worked, you can assume that in your case, giving SwiftUI the choice of deciding the sizings is not problematic.

like image 119
Mahdi BM Avatar answered Nov 09 '22 15:11

Mahdi BM


The issue is due to used different fonts for left/right sides, so paddings generate different resulting area.

Here is possible solution. The idea is to give right side rect based on default view size of left side text (this gives ability to track dynamic fonts sizes as well, automatically).

Tested with Xcode 12 / iOS 14

demo

struct ContentView: View {
    @State private var height = CGFloat.zero
    var body: some View {
        List(1...5, id: \.self) { index  in

            HStack(spacing: 8) {
                HStack {
                    Text("Item number \(index)")
                    Spacer()
                    }
                    .padding([.leading, .top, .bottom])
                    .background(GeometryReader {
                        Color.blue.preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
                    })


                Text("i")
                    .italic()
                    .font(.title)
                    .frame(width: height, height: height)
                    .background(Color.pink)

                }
                .padding(8)
                .background(Color.yellow)
                .onPreferenceChange(ViewHeightKey.self) {
                    self.height = $0
                }
        }
    }
}

struct ViewHeightKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}
like image 44
Asperi Avatar answered Nov 09 '22 13:11

Asperi


I managed to recreate the view in your first screenshot in SwiftUI. I wasn't sure on how much padding you wanted so I defined a private immutable variable for this value

The blue view is the one that will have the text content and could change in size so by using a GeometryReader you can get the size of the blue view and then use the height value from the size to set the width and height of the pink view. This means that whatever the height of the blue view is, the pink view will follow keeping an equal aspect ratio

The SizeGetter view below is used to get any views size using a GeometryReader and then binds that value back to a @State variable in the ContentView. Because the @State and @Binding property wrappers are being used, whenever the blueViewSize is updated SwiftUI will automatically refresh the view.

The SizeGetter view can be used for any view and is implemented using the .background() modifier as shown below

struct SizeGetter: View {

    @Binding var size: CGSize;

    var body: some View {

        // Get the size of the view using a GeometryReader
        GeometryReader { geometry in
            Group { () -> AnyView in

                // Get the size from the geometry
                let size = geometry.frame(in: .global).size;

                // If the size has changed, update the size on the main thread
                // Checking if the size has changed stops an infinite layout loop
                if (size != self.size) {
                    DispatchQueue.main.async {
                        self.size = size;
                    }
                }

                // Return an empty view
                return AnyView(EmptyView());
            }
        }
    }
}

struct ContentView: View {

    private let padding: Length = 10;
    @State private var blueViewSize: CGSize = .zero;

    var body: some View {

        List(1...5) { index  in

            // The yellow view
            HStack(spacing: self.padding) {

                // The blue view
                HStack(spacing: 0) {
                    VStack(spacing: 0) {
                        Text("Item number \(index)")
                            .padding(self.padding);
                    }
                    Spacer();
                }
                .background(SizeGetter(size: self.$blueViewSize))
                .background(Color.blue);

                // The pink view
                VStack(spacing: 0) {
                    Text("i")
                        .font(.title)
                        .italic();
                }
                .frame(
                    width: self.blueViewSize.height,
                    height: self.blueViewSize.height
                )
                .background(Color.pink);
            }
            .padding(self.padding)
            .background(Color.yellow);
        }
    }
}

In my opinion it is better to set the background colour of a VStack or HStack instead of the Text view directly because you can then add more text and other views to the stack and not have to set the background colour for each one

like image 45
Liam Avatar answered Nov 09 '22 15:11

Liam


I was searching very similar topic "Square Text in SwiftUI", came across your question and I think I've found quite simple approach to achieve your desired layout, using GeometryProxy to set width and heigh of the square view from offered geometry.size.

Checkout the code below, an example of TableCellView which can be used within List View context:

import SwiftUI

struct TableCellView: View {
    var index: Int
    
    var body: some View {
        HStack {
            HStack {
                Text("Item number \(index)")
                    .padding([.top, .leading, .bottom])
                Spacer()
            }
            .background(Color(.systemBlue))
            .layoutPriority(1)
            
            GeometryReader { geometry in
                self.squareView(geometry: geometry)
            }
            .padding(.trailing)
        }
        .background(Color(.systemYellow))
        .padding(.trailing)
    }
    
    func squareView(geometry: GeometryProxy) -> some View {
        Text("i")
            .frame(width: geometry.size.height, height: geometry.size.height)
            .background(Color(.systemPink))
    }
}

enter image description here

like image 21
Manoli Avatar answered Nov 09 '22 14:11

Manoli