I would like to underline a title with a rectangle that should have the same width as the Text.
First I create an underlined text as below:
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}
}
}
So I get the following result:
Now I want to get this result:
So I would like to know if it's possible to bind Text width and apply it to Rectangle by writing something like :
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(width: Text.width, height: (5.0))
}
}
}
By doing so, I could change text and it will be dynamically underlined with correct width.
I tried many options but I can't find how to do it. I also checked this question but it's seems to not be the same issue.
Just specify that container has fixed size and it will tight to content, like
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}.fixedSize() // << here !!
}
Related
I'm using a ForEach within a horizontal ScrollView to display a list of movies from my movies array. I want to make an interaction like Netflix where the background updates with the an image, movie description, etc. based on the item currently in focus.
This is the best solution I've managed to come up with so far but its very far off.
My MainMenuView:
struct MainMenuView: View {
#ObservedObject var vm = MainMenuViewModel()
var body: some View {
VStack {
Text("Movies")
.font(.title3.weight(.semibold))
.frame(maxWidth: .infinity, alignment: .leading)
ScrollView(.horizontal) {
LazyHStack {
ForEach(vm.selections) { selection in
Button {
print("Hello, World!")
} label: {
ZStack {
SelectionPreview(selection: selection)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .bottom)
Image(selection.thumbnail)
.resizable()
.scaledToFill()
.frame(width: 500, height: 281)
}
}
.cornerRadius(32)
.buttonStyle(.plain)
}
}
}
}
}
}
My SelectionPreview:
struct SelectionPreview: View {
#State var selection: Movie
#Environment(\.isFocused) var isFocused: Bool
var body: some View {
Image(isFocused ? selection.backdrop : "")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
The above code does manage to update a background based on the current item in focus but all the other items in the list get pushed to the side since the View is now the size of the background image.
I also tried to set a background on the entire VStack instead but this didn't work. I think because the Vstack on its own is not focusable:
Something like this:
struct MainMenuView: View {
var body: some View {
VStack {
ScrollView(.horizontal) {
[...]
}
}
.background(
SelectionPreview()
)
}
}
My goal is to achieve something similar to this:
Any help would be appreciated!
Is it possible to make paged TabView that wraps its content? I don't know the height of the content (it is an Image resized to fit the width of the screen) so I can't use frame modifier.
My code looks like this:
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Color.red
.frame(width: 20, height: 20)
}
The problem is that the TabView is as big as the screen and PageIndicator is placed in the top right corner of the screen instead of top right corner of the image. Tanks for any suggestions.
EDIT:
I've added code that is reproducible. PageIndicator was replaced by red rectangle.
struct Test: View {
struct Entry: Identifiable {
let image: Image
let description: String
var id: String { description }
}
let data = [
Entry(image: Image(systemName: "scribble"), description: "image 1"),
Entry(image: Image(systemName: "trash"), description: "image 2")
]
var body: some View {
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Color.red
.frame(width: 20, height: 20)
}
}
}
Your PageIndicator is not placed on the image, because you didn't place it there. You are placing it in a layer on top of a VStack that happens to contain text and an image that can be shorter than the screen. If you want the PageIndicator on the image, then you need to do that specifically. You didn't provide a Minimal, Reproducible Example, but does something like this work:
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.overlay(
PageIndicator()
)
.frame(maxWidth: .infinity)
.overlay(
PageIndicator()
)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
The .overlay() needs to go before the .frame() so it stays on the image, instead of overlaying the .frame().
Edit:
Based off of the MRE and the comment, here is alternate solution that aligns the PageIndicator to the top of the image, but does not scroll with the image. Please note that this is not a perfect MRE as the image heights are different, but this solution actually accounts for that as well. Lastly, I added a yellow background on the image to show that things are aligned properly.
struct Test: View {
struct Entry: Identifiable {
let image: Image
let description: String
var id: String { description }
}
let data = [
Entry(image: Image(systemName: "scribble"), description: "image 1"),
Entry(image: Image(systemName: "trash"), description: "image 2")
]
#State private var imageTop: CGFloat = 50
var body: some View {
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.background(Color.yellow)
.background(
GeometryReader { imageProxy in
Color.clear.preference(
key: ImageTopInGlobal.self,
// This returns the top of the image relative to the TabView()
value: imageProxy.frame(in: .named("TabView")).minY)
}
)
Text(entry.description)
}
}
}
// This gives a reference to another container for comparison
.coordinateSpace(name: "TabView")
.tabViewStyle(.page(indexDisplayMode: .never))
VStack {
Spacer()
.frame(height: imageTop)
Color.red
.frame(width: 20, height: 20)
}
}
// You either have to ignore the safe area or account for it with regard to the TabView(). This was simpler.
.edgesIgnoringSafeArea(.top)
.onPreferenceChange(ImageTopInGlobal.self) {
imageTop = $0
}
}
}
private extension Test {
struct ImageTopInGlobal: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = nextValue()
}
}
}
Final edit:
In response to the last comment, my answer is a qualified no. I don't think there is any way to make a TabView hug its contents. (I take the original question using the term wrap to mean hug, as the TabView always "wraps" its contents.) If you try to use a preference key, the TabView collapses. There would have to be a minHeight or height set to prevent this which defeats the purpose of the hugging attempt.
Edit: Substitute your "system name:" of choice. "pencil.circle" works fine. "edit" is not a valid SF Symbol.
(I've simplified my code so you can cut and paste. That's why you see .frame, resizable, etc. where much simpler code might your first instinct.)
I have created a view which is a vertical list of row items (table view).
Each row item has a horizontal view with two images inside it.
The images take up too much space and do not fit correctly on the screen:
import SwiftUI
#main
struct StackOverflowDemoApp: App {
var body: some Scene {
WindowGroup {
TandemView()
}
}
}
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
Spacer()
}
}
struct TandemView: View {
var body: some View {
HStack {
Spacer()
Image(systemName: "pencil")
.resizable()
.background(Color.orange)
.frame(height: 80)
.aspectRatio(1, contentMode: .fill)
PaddedImageView()
.frame(width: 200, height: 80)
}
.padding()
.fixedSize()
}
}
struct TandemView_Previews: PreviewProvider {
static var previews: some View {
TandemView()
}
}
The above is the closest I can get to the desired layout (it just needs to fit horizontally). I experimented with GeometryReader but that did not produce desired results.
Here are some things I tried:
The code as provided
NoConstraintsOnPencilOrHStack
NoConstraintsOnTandemView
NoConstraintsOnImageInPaddedViewButWithFrameConstraint
I am trying to get a row view which consists of two Images (my actual source consists of UIImage objects) that fits within the width of the screen.
Edit:
After Accepting cedricbahirwe's spot-on response, I was able to simplify the code further. New results:
I added at the top level
TandemView()
.padding(.horizontal)
I removed:
// Spacer()
at the end of PaddedImageView
updated TandemView -- changed both frames and removed 3 lines:
struct TandemView: View {
var body: some View {
HStack {
Spacer()
Image(systemName: "pencil")
.resizable()
.background(Color.orange)
.frame(width: 80, height: 80)
// .aspectRatio(1, contentMode: .fill)
PaddedImageView()
.frame(height: 80)
}
// .padding()
// .fixedSize()
}
}
This is happening because of the layout of PaddedImageView View, you can actually remove the Spacer since it is not needed there.
So change
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
Spacer()
}
}
to
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
}
}
Note:
SwiftUI Engine infers the layout of your view from the implementation of the body property. It's recommended to have one Parent View inside the body property.
Xcode 11 Beta 4 deprecated .relativeSize as well as .relativeWidth and .relativeHeight (see this related post).
So what is the alternative?
I want to create an overlay that has a width relative to it's parent.
Let's say I have the following main view
struct MainView: View {
var body: some View {
ZStack(alignment: .topLeading) {
BackgroundView()
SideBarView()
.frame(idealWidth: 200)
.fixedSize(horizontal: true, vertical: false)
}
}
}
With a simple BackgroundView and SideBarView as well those work as expected.
struct SideBarView: View {
var body: some View {
Rectangle()
.foregroundColor(.green)
}
}
struct BackgroundView: View {
var body: some View {
Rectangle()
.foregroundColor(.red)
}
}
This was suggested in the release notes and this answer.
How can I avoid to hardcode those values as I could before by using .relativeWidth(0.3) instead of .frame(idealWidth:)?1
1Note: Using .relativeWidth never actually worked, e.g. using 0.3 as a relative value never resulted in a view that was 30 % of the width of the parent, but you could get close to your desired result through trial-and-error.
There are multiple ways to achieve it, one way is using .overlay instead of ZStack. The view you use in the overlay, will get the size of the BackgroundView offered by the parent. Then you simply use GeometryReader to get the width and multiply it by 0.7:
struct ContentView: View {
var body: some View {
BackgroundView().overlay(SideBarView())
}
}
struct SideBarView: View {
var body: some View {
GeometryReader { proxy in
HStack {
Spacer()
Rectangle()
.frame(width: proxy.size.width * 0.7)
.fixedSize(horizontal: true, vertical: false)
.foregroundColor(.green)
}
}
}
}
struct BackgroundView: View {
var body: some View {
Rectangle()
.foregroundColor(.red)
}
}
I'm trying to achieve a following layout using Swift UI…
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…
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?
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.
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
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()
}
}
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
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))
}
}