SwiftUI - Fill image of background cell keeping aspect ratio - ios

In the wonderful world of SwiftUI, I have a View that I use as a cell. I intend to reproduce the Layout of a previous application of mine, with Autolayout, not SwiftUI, in which the background image filled the entire cell, adjusting to the width and losing pieces of image above and below.
In my new app, the code in SwiftUI is the following:
struct CharacterRow2: View {
var character: Character
var body: some View {
Text(character.name)
.font(Font.custom(avengeanceHeroicAvengerNormal, size: 30))
.foregroundColor(.white)
.baselineOffset(-10)
.shadow(color: .black, radius: 1, x: -1, y: 1)
.frame(width: UIScreen.main.bounds.width, height: 140)
.background {
WebImage(url: extractImage(data: character.thumbnail))
.resizable()
.frame(width: UIScreen.main.bounds.width, height: 150)
}
}
}
With this code, my app looks like this:
I tried to add scaledToFill():
WebImage(url: extractImage(data: character.thumbnail))
.resizable()
.scaledToFill()
.frame(width: UIScreen.main.bounds.width, height: 150)
But this is the result:
I'm stuck...
Thank you in advance!

In this case, you are simply using too many frames. And using them incorrectly. You should avoid using UIScreen.main.bounds in SwiftUI, especially in something like a view cell. By doing this, the cell will not behave properly with other views, and can cause UI issues that would be difficult to trace.
The simplest way to get the behavior you want is this:
Text(character.name)
.font(.largeTitle)
.foregroundColor(.white)
.baselineOffset(-10)
.shadow(color: .black, radius: 1, x: -1, y: 1)
.frame(height: 140)
.frame(maxWidth: .infinity) // use .infinity for max width to make it
// as large as the space offered by the
// parent view
.background {
WebImage(url: extractImage(data: character.thumbnail))
.resizable()
// .frame(width: UIScreen.main.bounds.width, height: 150) <-- Remove this frame altogether
.scaledToFill()
}
.clipped // This keeps the image from being larger than the frame
This will size to be as wide as the parent view allows it to be. Leaving the UIScreen.main.bounds.width could cause it to be larger than the parent view and cause partial eclipsing.

In your last example the images are overlaying each other. This is due to calling scaleToFill(). The images are now ignoring their frame boundaries regarding the height. Adding .clipped solves the problem.
struct CharacterRow2: View {
var character: String
var body: some View {
Text(character)
.font(.largeTitle)
.foregroundColor(.white)
.baselineOffset(-10)
.shadow(color: .black, radius: 1, x: -1, y: 1)
.frame(width: UIScreen.main.bounds.width, height: 140)
.background {
Image(character)
.resizable()
.scaledToFill()
.frame(width: UIScreen.main.bounds.width, height: 150)
.clipped() // <-- Add this
}
}
}
It seems this will work only with a ForEach inside a ScrollView. Using a List seems to breack the vertical frame boundary.
struct ContentView: View{
let content = ["1.jpg", "2.jpg", "3.jpg" ]
var body: some View{
//These look really weird
// List(content, id: \.self){ name in
// CharacterRow2(character: name)
// }
// List{
// VStack{
// ForEach(content, id: \.self){ name in
// CharacterRow2(character: name)
// }
// }
// }
//working
ScrollView{
VStack{
ForEach(content, id: \.self){ name in
CharacterRow2(character: name)
.padding()
}
}
}
}
}
Result:

Related

SwiftUI ZStack alignment works strangely

I created very simple views with SwiftUI including ZStack.
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Text("aaa")
.frame(width: 50, height: 50)
.font(.system(size: 20))
}
.frame(width: 142.0, height: 142.0)
.background(.pink)
}
}
I expected that the Text("aaa") would appear in the top leading of pink square.
But the result was this.
More strangely, it works well if I add 'Color.clear' to ZStack like this.
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Color.clear
Text("aaa")
.frame(width: 50, height: 50)
.font(.system(size: 20))
}
.frame(width: 142.0, height: 142.0)
.background(.pink)
}
}
And the result it this.
I cannot understand this situation. SwiftUI is totally crazy.
Does anybody know about this?
The elements of the ZStack are laid out and aligned based on the size of the largest child, not the frame of the ZStack itself.
You should think of the "content area" of the stack being seperate to the frame of the stack.
When you add a Color.clear, its default frame is unbounded (the height and width are infinity), so when it is added to the ZStack, it will grow the "content area" to the maximum possible size.
This makes the content area of the stack the same size as the ZStacks frame.
You can achieve the same result (in a clearer way) by using a Spacer() with explicit infinite bounds.
This will ensure the children always fill the same available to them.
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Spacer()
.frame(maxWidth: .infinity, maxHeight: .infinity)
Text("aaa")
.frame(width: 50, height: 50)
.font(.system(size: 20))
}
.frame(width: 142.0, height: 142.0)
.background(.pink)
}
}

how to disable ScrollView clipping content in SwiftUI

I need that red rectangle should be visible until reach to the leading or trailing edge of the device.
So the problem is scrollview cliping red boxes (hiding) as move beyond the scrollview container size.
code:
struct test: View {
var body: some View {
ScrollView(.horizontal,showsIndicators:false) {
LazyHStack{
ForEach(0...4,id:\.self){i in
Rectangle()
.fill(Color.red)
.frame(width: 60, height: 50, alignment: .center)
}
}.padding(.horizontal, 4)
}.background(Color.yellow)
.frame(width: 68, height: 60, alignment: .trailing)
}
}
Results:
Expected:(I also produce this result by setting ScrollView full width and adding padding (.padding(.horizontal, UIScreen.main.bounds.width/2 )) to LazyHStack)
But it is a hack by adding space at start and end, and problem remain unresolved, that is clipping of ScrollView content
I'm not exactly sure what the problem is, but you probably want the padding to be on the Text and not the ScrollView. Correct me if this isn't what you are looking for.
struct test: View {
var body: some View {
ScrollView(.horizontal) {
Text("Hello, World!")
.frame(width: UIScreen.main.bounds.width, alignment: .leading)
.padding(.horizontal, 40)
}
.background(Color.yellow)
}
}
Result:
It seems to be working for me when I remove the line: .frame(width: UIScreen.main.bounds.width, alignment: .leading).
On an iPhone XR, I'm seeing a horizontal ScrollView with a yellow background starting at 40 offset from leading and ending at 40 offset from trailing. Is this what you're trying to achieve?
Also, I'm pretty sure UIScreen.main.bounds.width is going to return the width of the device, which will be a problem if you want your text to take up 80 pixels less than that value (since your ScrollView has 40 padding either side).
if I understand correct from your answers, this is the result you want:
struct test: View {
var body: some View {
GeometryReader { geo in
VStack {
Spacer()
HStack {
ScrollView(.horizontal,showsIndicators:false) {
LazyHStack{
ForEach(0...4,id:\.self){i in
Rectangle()
.fill(Color.red)
.frame(width: 60, height: 50, alignment: .center)
}
}
.padding(.horizontal,40)
}
.background(Color.yellow)
.frame(width: geo.size.width, height: 60, alignment: .center)
}
Spacer()
}
}
}
}
Result:

View centered in superview with view on top of it in SwiftUI

I'm trying to achieve something that is quite easy in UIKit - one view that is always in in the center (image) and the second view (text) is on top of it with some spacing between two views. I tried many different approaches (mainly using alignmentGuide but nothing worked as I'd like).
code:
ZStack {
Rectangle()
.foregroundColor(Color.red)
VStack {
Text("Test")
.padding([.bottom], 20) // I want to define spacing between two views
Image(systemName: "circle")
.resizable()
.alignmentGuide(VerticalAlignment.center, computeValue: { value in
value[VerticalAlignment.center] + value.height
})
.frame(width: 20, height: 20)
}
}
.frame(width: 100, height: 100)
result:
As you can see image is not perfectly centered and it actually depends on the padding value of the Text. Is there any way to force vertical and horizontal alignment to be centered in the superview and layout second view without affecting centered view?
I think the “correct” way to do this is to define a custom alignment:
extension VerticalAlignment {
static var custom: VerticalAlignment {
struct CustomAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.center]
}
}
return .init(CustomAlignment.self)
}
}
Then, tell your ZStack to use the custom alignment, and use alignmentGuide to explicitly set the custom alignment on your circle:
PlaygroundPage.current.setLiveView(
ZStack(alignment: .init(horizontal: .center, vertical: .custom)) {
Color.white
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
VStack {
Text("Test")
Circle()
.stroke(Color.white)
.frame(width: 20, height: 20)
.alignmentGuide(.custom, computeValue: { $0.height / 2 })
}
}
.frame(width: 300, height: 300)
)
Result:
You can center the Image by moving it to ZStack. Then apply .alignmentGuide to the Text:
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.red)
Text("Test")
.alignmentGuide(VerticalAlignment.center) { $0[.bottom] + $0.height }
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
}
.frame(width: 100, height: 100)
}
}
Note that as you specify the width/height of the Image explicitly:
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
you can specify the .alignmentGuide explicitly as well:
.alignmentGuide(VerticalAlignment.center) { $0[.bottom] + 50 }
Here is possible alternate, using automatic space consuming feature
Tested with Xcode 12 / iOS 14
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.red)
VStack(spacing: 0) {
Color.clear
.overlay(
Text("Test").padding([.bottom], 10),
alignment: .bottom)
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
Color.clear
}
}
.frame(width: 100, height: 100)
}
}
Note: before I used Spacer() for such purpose but with Swift 2.0 it appears spacer becomes always just a spacer, ie. nothing can be attached to it - maybe bug.

Swiftui - only show a percentage of a view

I have a RoundedRectangle, and my objective is to lay another RoundedRectangle over it such that the overlay shows a "percentage complete". I can't seem to find the proper incantation to do so, though.
I think that ideally, the overlay should somehow only show a percentage of itself. Resizing itself to match the percentage skews the shape of the overlay.
import PlaygroundSupport
struct ContentView: View {
#State private var value: Double = 0
var body: some View {
GeometryReader { geom in
VStack {
Slider(value: self.$value, in: 0...1, step: 0.01)
ZStack {
ZStack(alignment: .leading) {
// The main rectangle
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue)
.frame(width: geom.size.width,
height: 200)
// The progress indicator...
RoundedRectangle(cornerRadius: 10)
.fill(Color.red)
.opacity(0.5)
.frame(width: CGFloat(self.value) * geom.size.width,
height: 200)
}
Text("\(Int(self.value * 100)) %")
}
}
}
.padding()
}
}
PlaygroundPage.current.setLiveView(ContentView())
In the above playground, if you look at 1, 2, or even 3 %, you can see that the red overlay is out of the blue background rectangle bounds in the upper and lower left. See image below.
I feel like this is not the proper solution, but I also couldn't find the right mix of things (trying scaleEffect, a bunch of offset and position math) to really nail it.
To me, like I said above, it feels like the overlay should be able to say "only make my left-most 40% visible" when the value is 0.4.
That was long-winded, I apologize for that. If you've read this far, and have any advice to impart, I'd be incredibly appreciative.
Thanks!
If I correctly understood your concern, here is a solution. Tested with Xcode 11.4 / iOS 13.4.
ZStack {
ZStack(alignment: .leading) {
// The main rectangle
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue)
.frame(width: geom.size.width,
height: 200)
// The progress indicator...
RoundedRectangle(cornerRadius: 10)
.fill(Color.red)
.opacity(0.5)
.frame(width: CGFloat(self.value) * geom.size.width,
height: 200)
}
.clipShape(RoundedRectangle(cornerRadius: 10)) // << here !!
This how I approach it so I don't have to manage more than one cornerRadius.
VStack {
ZStack(alignment: .leading) {
// The main rectangle
Rectangle()
.fill(Color.blue)
.frame(width: geom.size.width,
height: 200)
// The progress indicator...
Rectangle()
.fill(Color.red)
.opacity(0.5)
.frame(width: CGFloat(self.value) * geom.size.width,
height: 200)
}
.cornerRadius(10)

SwiftUI Font scaling in a complex View

My goal is to make sure Text in a container to scale according to its parent. It works well when the container only contains one Text view, as following:
import SwiftUI
struct FontScalingExperiment: View {
var body: some View {
Text("Hello World ~!")
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.scaledToFill()
)
}
}
struct FontScalingExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
FontScalingExperiment()
.previewLayout(.fixed(width: 100, height: 100))
FontScalingExperiment()
.previewLayout(.fixed(width: 200, height: 200))
FontScalingExperiment()
.previewLayout(.fixed(width: 300, height: 300))
FontScalingExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
the result:
However, when we have more complex View, we cant use same approach to automatically scale the text based on its parent size, for example:
import SwiftUI
struct IndicatorExperiment: View {
var body: some View {
VStack {
HStack {
Text("Line 1")
Spacer()
}
Spacer()
VStack {
Text("Line 2")
Text("Line 3")
}
Spacer()
Text("Line 4")
}
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
}
}
struct IndicatorExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
IndicatorExperiment()
.previewLayout(.fixed(width: 100, height: 100))
IndicatorExperiment()
.previewLayout(.fixed(width: 200, height: 200))
IndicatorExperiment()
.previewLayout(.fixed(width: 300, height: 300))
IndicatorExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
Simply adding these 3 modifiers:
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
wont produce result like the first example; Text enlarged beyond the frame.
I did successfully, produce the result that I want by using GeometryReader then scale the font size based on geometry.size.width. Is this the only approach for achieving the desired result in SwiftUI?
You can try make all the Texts the same height. To do this you will need to set the padding and spacing explicitly, so this will scale rather than the fixed default values.
Also, the Spacer() didn't make much sense here - if the requirement was that all the Text stay the same size, the Spacer would just make all the text small. For Text to scale based on space, and where Spacer tries to use as much space as possible, it's a contradiction. Instead, I decided to just set the VStack's spacing in the initializer.
Working code:
struct IndicatorExperiment: View {
private let size: CGFloat
private let padding: CGFloat
private let primarySpacing: CGFloat
private let secondarySpacing: CGFloat
private let textHeight: CGFloat
init(size: CGFloat) {
self.size = size
padding = size / 10
primarySpacing = size / 15
secondarySpacing = size / 40
let totalHeights = size - padding * 2 - primarySpacing * 2 - secondarySpacing
textHeight = totalHeights / 4
}
var body: some View {
VStack(spacing: primarySpacing) {
HStack {
scaledText("Line 1")
Spacer()
}
.frame(height: textHeight)
VStack(spacing: secondarySpacing) {
scaledText("Line 2")
scaledText("Line 3")
}
.frame(height: textHeight * 2 + secondarySpacing)
scaledText("Line 4")
}
.padding(padding)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
.frame(width: size, height: size)
}
private func scaledText(_ content: String) -> some View {
Text(content)
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(height: textHeight)
}
}
Code to test with:
struct ContentView: View {
var body: some View {
ScrollView {
VStack(spacing: 50) {
IndicatorExperiment(size: 100)
IndicatorExperiment(size: 200)
IndicatorExperiment(size: 300)
IndicatorExperiment(size: 400)
}
}
}
}
Result:
Using GeometryReader and a .minimumScaleFactor modifier would probably the only way to scale text in a view. To have more control on sizing, one possible way is to provde the .frame size from the parent view.
Scalable Text View
GeometryReader { geo in
Text("Foo")
.font(
.system(size: min(geo.size.height, geo.size.width) * 0.95))
.minimumScaleFactor(0.05)
.lineLimit(1)
}
Parent View that uses the Scalable Text View
GeometryReader { geo in
ScaleableText()
.frame(width: geo.size.width, height: geo.size.height)
}

Resources