How to access parents frame in SwiftUI - swift

Still working on SwiftUI, but I got a problem.
I created an Image with a dynamic frame, and I want a label which needs to be half width of the image (e.g Image width = 300; Label width 150)
In UIKit I should code something like this:
Label.widthAnchor.constraint(image.widthAnchor, multiplier: 1/2).isActive = true
I tried something similar in SwiftUI:
Image(systemName: "j.circle.fill")
.resizable()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width)
Text("Placeholder").frame(width: Image.frame.width/2, height: 30)
But this doesn't work.
What shall I do?

Why not setting hard sizes for both then :
Image(systemName: "j.circle.fill")
.resizable()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width)
Text("Placeholder")
.frame(width: UIScreen.main.bounds.width/2, height: 30)
If you want to change the whole size of the component in one place you could just do it like this :
struct MyView : View {
private var compWidth: CGFloat { UIScreen.main.bounds.width }
var body: some View {
VStack {
Image(systemName: "j.circle.fill")
.resizable()
.frame(width: compWidth, height: compWidth)
Text("Placeholder")
.frame(width: compWidth/2, height: 30)
}
}
}
Or, for a more elegant approach you could look into GeometryReader.

Related

Is it possible to define a RoundedRectangle with some properties as a variable for later usage in SwiftUI?

I want to reuse a RoundedRectangle that has certain properties multiple times in my View. Is defining it as a variable possible?
And while my code is pretty much like this
Struct ContentView: View {
var body: some View {
RoundedRectangle(cornerRadius: 24)
.frame(width: 180, height: 240)
.foregroundColor(Color(.white))
RoundedRectangle(cornerRadius: 24)
.frame(width: 180, height: 240)
.foregroundColor(Color(.white))
RoundedRectangle(cornerRadius: 24)
.frame(width: 180, height: 240)
.foregroundColor(Color(.white))
}
}
I'd want to do something like this
Struct ContentView: View {
let basicPanel = RoundedRectangle(cornerRadius: 24)
.frame(width: 180, height: 240)
.foregroundColor(Color(.white))
var body: some View {
basicPanel
basicPanel
basicPanel
}
}
Sincerely sorry if the question is written terribly, It's my first question on StackOverflow ever.
Yes, you can just add type declaration some View for variable, like
let basicPanel: some View = RoundedRectangle(cornerRadius: 24)
.frame(width: 180, height: 240)
.foregroundColor(Color(.white))

SwiftUI - How to center a component in a horizontal ScrollView when tapped?

I have a horizontal ScrollView, and within it an HStack. It contains multiple Subviews, rendered by a ForEach. I want to make it so that when these Subviews are tapped, they become centered vertically in the view. For example, I have:
ScrollView(.horizontal) {
HStack(alignment: .center) {
Circle() // for demonstration purposes, let's say the subviews are circles
.frame(width: 50, height: 50)
Circle()
.frame(width: 50, height: 50)
Circle()
.frame(width: 50, height: 50)
}
.frame(width: UIScreen.main.bounds.size.width, alignment: .center)
}
I tried this code:
ScrollViewReader { scrollProxy in
ScrollView(.horizontal) {
HStack(alignment: .center) {
Circle()
.frame(width: 50, height: 50)
.id("someID3")
.frame(width: 50, height: 50)
.onTapGesture {
scrollProxy.scrollTo(item.id, anchor: .center)
}
Circle()
.frame(width: 50, height: 50)
.id("someID3")
.frame(width: 50, height: 50)
.onTapGesture {
scrollProxy.scrollTo(item.id, anchor: .center)
}
...
}
}
But it seemingly had no effect. Does anyone know how I can properly do this?
You can definitely do this with ScrollView and ScrollViewReader. However, I see a couple of things that could cause problems in your code sample:
You use the same id "someID3" twice.
I can't see where your item.id comes from, so I can't tell if it actually contains the same id ("someID3").
I don't know why you have two frames with the same bounds on the same view area. It shouldn't be a problem, but it's always best to keep things simple.
Here's a working example:
import SwiftUI
#main
struct MentalHealthLoggerApp: App {
var body: some Scene {
WindowGroup {
ScrollViewReader { scrollProxy in
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 10) {
Color.clear
.frame(width: (UIScreen.main.bounds.size.width - 70) / 2.0)
ForEach(Array(0..<10), id: \.self) { id in
ZStack(alignment: .center) {
Circle()
.foregroundColor(.primary.opacity(Double(id)/10.0))
Text("\(id)")
}
.frame(width: 50, height: 50)
.onTapGesture {
withAnimation {
scrollProxy.scrollTo(id, anchor: .center)
}
}
.id(id)
}
Color.clear
.frame(width: (UIScreen.main.bounds.size.width - 70) / 2.0)
}
}
}
}
}
}
Here you can see it in action:
[EDIT: You might have to click on it if the GIF won't play automatically.]
Note that I added some empty space to both ends of the ScrollView, so it's actually possible to center the first and last elements as ScrollViewProxy will never scroll beyond limits.
Created a custom ScrollingHStack and using geometry reader and a bit calculation, here is what we have:
struct ContentView: View {
var body: some View {
ScrollingHStack(space: 10, height: 50)
}
}
struct ScrollingHStack: View {
var space: CGFloat
var height: CGFloat
var colors: [Color] = [.blue, .green, .yellow]
#State var dragOffset = CGSize.zero
var body: some View {
GeometryReader { geometry in
HStack(spacing: space) {
ForEach(0..<15, id: \.self) { index in
Circle()
.fill(colors[index % 3])
.frame(width: height, height: height)
.overlay(Text("\(Int(dragOffset.width))"))
.onAppear {
dragOffset.width = geometry.size.width / 2 - ((height + space) / 2)
}
.onTapGesture {
let totalItems = height * CGFloat(index)
let totalspace = space * CGFloat(index)
withAnimation {
dragOffset.width = (geometry.size.width / 2) - (totalItems + totalspace) - ((height + space) / 2)
}
}
}
}
.offset(x: dragOffset.width)
.gesture(DragGesture()
.onChanged({ dragOffset = $0.translation})
)
}
}
}

How to scale a SwiftUI Image including its overlay?

Given this view with an image that has an overlay:
struct TestView: View {
var body: some View {
Image("image")
.overlay {
Image("logo")
.position(x: 20, y: 130) // I want to freely position the overlay image
}
}
}
The size of the images shouldn’t matter. I my example they are 150×150 pixels (“image”) and 20×20 pixels (logo).
How can I scale this up to its bounds (like superviews frame), including the overlay?
Some must-have conditions:
I need to freely position the overlay image, so I can not use the alignment parameter of overlay in conjunction with a padding on the logo.
I also need to position the overlay image absolutely, not relatively. So I can’t use a GeometryReader (or alignment).
I need the view to be reusable in different scenarios (different devices, different view hierarchies). That means I don’t know the scale factor, so I can’t use scaleEffect.
I tried:
A) .aspectRatio(contentMode: .fit)
struct TestView: View {
var body: some View {
Image("image")
.resizable()
.overlay {
Image("logo")
.position(x: 20, y: 130)
}
.aspectRatio(contentMode: .fit)
}
}
B) .scaledToFit()
struct TestView: View {
var body: some View {
Image("image")
.resizable()
.overlay {
Image("logo")
.position(x: 20, y: 130)
}
.scaledToFit()
}
}
C) Making logo resizable
struct TestView: View {
var body: some View {
Image("image")
.resizable()
.overlay {
Image("logo")
.resizable()
.position(x: 20, y: 130)
}
.scaledToFit() // or .aspectRatio(contentMode: .fit)
}
}
A and B looking like this:
C looking like this:
Both of which gave me a relative position of the logo that was different from the original (see linked screenshot). The relative size differs as well (it is now too small).
Wrapping in a stack and scale this stack instead also didn’t help.
[UPDATE] Accepted answer
Apparently not having a real view hierarchy in SwiftUI vs UIKit comes at a price. Scaling a hierarchy shouldn’t be that complicated imho. Instead I still would expect .scaledToFit (if added at the end) to scale everything before it (see A), B), and C)).
Adapted accepted answer (minus contentSize and alignment: .topLeading):
struct ContentView: View {
var body: some View {
Image("image")
.resizable()
.scaledToFit()
.overlay {
GeometryReader { geometry in
let imageSize = CGSize(width: 150, height: 150)
let logoSize = CGSize(width: 20, height: 20)
let logoPosition = CGPoint(x: 20, y: 130)
Image("logo")
.resizable()
.position(
x: logoPosition.x / imageSize.width * geometry.size.width,
y: logoPosition.y / imageSize.height * geometry.size.height
)
.frame(
width: logoSize.width / imageSize.width * geometry.size.width,
height: logoSize.height / imageSize.height * geometry.size.height
)
}
}
}
}
Then like this with GeometryReader:
struct ContentView: View {
let imageSize = CGSize(width: 150, height: 150) // Size of orig. image
let logoPos = CGSize(width: 10, height: 120) // Position of Logo in relation to orig. image
let logoSize = CGSize(width: 20, height: 20) // Size of logo in relation to orig. image
#State private var contentSize = CGSize.zero
var body: some View {
Image("dog")
.resizable()
.scaledToFit()
.overlay(
GeometryReader { geo in
Color.clear.onAppear {
contentSize = geo.size
}
}
)
.overlay(
Image(systemName: "circle.hexagongrid.circle.fill")
.resizable()
.foregroundColor(.pink)
.offset(x: logoPos.width / imageSize.width * contentSize.width,
y: logoPos.height / imageSize.height * contentSize.height)
.frame(width: logoSize.width / imageSize.width * contentSize.width,
height: logoSize.height / imageSize.height * contentSize.height)
, alignment: .topLeading)
}
}
The problem is the absolute positioning of the logo. I see two ways:
scale the whole resulting image with .scaleEffect.
Be aware this is a render scaling of the underlying image, it will not be redrawn in the new size, so can become blurry.
Image("dog")
.overlay {
Image(systemName: "circle.hexagongrid.circle.fill")
.foregroundColor(.pink)
.position(x: 20, y: 130)
}
.scaleEffect(2.0)
or – and my preference
2. Position the Logo relative to the lower left corner
ZStack(alignment: .bottomLeading) {
Image("dog")
.resizable()
.scaledToFit()
Image(systemName: "circle.hexagongrid.circle.fill")
.resizable()
.foregroundColor(.pink)
.frame(width: 70, height: 70)
.padding()
}

How to have 2x2 made of squares fit screen in SwiftUI

I'm trying to create a 2x2 grid through either VStacks/HStacks or LazyVGrid to have the squares on each row fit the screen. For example, the first and second squares each take up half the width of the screen and based on that length, that'll determine the height to make it a square. How would I go about doing that in the two ways that I've mentioned or is there a better way to do it? Here's what I have so far.
VStack {
HStack {
Rectangle()
.fill(Color.gray)
.frame(width: 100, height: 100)
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
}
HStack {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
}
}
It feels wrong to hardcode the width and height for the frame property here or is that the way to go about removing the gaps between the squares? Would this way of hardcoding values scale to other phone sizes?
LazyVGrid(columns: layout) {
Rectangle()
.fill(Color.gray)
.frame(width: 210, height: 210)
Rectangle()
.fill(Color.blue)
.frame(width: 210, height: 210)
Rectangle()
.fill(Color.red)
.frame(width: 210, height: 210)
Rectangle()
.fill(Color.green)
.frame(width: 210, height: 210)
}
EDIT: Here's what the new code looks like to get what I wanted.
VStack(spacing: 0) {
HStack(spacing: 0) {
Rectangle()
.fill(Color.gray)
Rectangle()
.fill(Color.blue)
}
HStack(spacing: 0) {
Rectangle()
.fill(Color.red)
Rectangle()
.fill(Color.green)
}
}
.aspectRatio(1.0, contentMode: .fit)
Now to get it working with LazyVGrid.
It feels wrong to hardcode the width and height for frame here or is that the way to go about removing the gaps between the squares?
To remove the gaps between the squares, just modify the HStack / VStack to include spacing:
HStack(alignment: .center, spacing: 0, content: {
Rectangle()
.fill(Color.gray)
.frame(width: 100, height: 100)
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
})
You can use GeometryReader, as seen here.
Example usage:
struct MyView: View {
var body: some View {
GeometryReader { geo in
//You now have access to geo.size.width and geo.size.height
}
}
}
Just apply the aspectRatio (value 1 and fit the screen) modifier, there is no need to specify any frame.
VStack {
HStack {
Rectangle()
.fill(Color.gray)
Rectangle()
.fill(Color.blue)
}
HStack {
Rectangle()
.fill(Color.red)
Rectangle()
.fill(Color.green)
}
}
.aspectRatio(1.0, contentMode: .fit)

SwiftUI: How to find the height of an image and use it to set the size of a frame

So I've been trying to figure out how to find the height of an image in SwiftUI for a while now with no success. Essentially I have portrait/landscape images, which the portrait ones I want to display at about 600 pt tall, whereas the landscape images I want to display so the width of the image is used, so the height is whatever. That being said, I'm wanting it inside of a GeometryReader, as the offset I am using uses it so that when you scroll down, the image doesn't scroll, but if you scroll up, it does. I don't believe I can share images, but in any case, this should be enough code to run it successfully!
struct ContentView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var backButton: some View {
Button(action: {
withAnimation {
self.mode.wrappedValue.dismiss()
}
}) {
Image(systemName: "chevron.left.circle.fill")
.opacity(0.8)
.font(.system(size: 30))
.foregroundColor(.black)
.background(Color.gray.mask(Image(systemName: "chevron.left").offset(x: 9.4, y: 7.6)))
}
}
var body: some View {
ScrollView(showsIndicators: false) {
GeometryReader { geometry in
Image("image1")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: UIScreen.main.bounds.width, minHeight: 350, maxHeight: 600, alignment: .top)
.clipped()
.overlay(
Rectangle()
.foregroundColor(.clear)
.background(LinearGradient(gradient: Gradient(colors: [.clear, .white]), startPoint: .center, endPoint: .bottom))
)
.offset(y: geometry.frame(in: .global).minY > 0 ? -geometry.frame(in: .global).minY : 0)
}
.frame(minWidth: UIScreen.main.bounds.width, maxWidth: UIScreen.main.bounds.width, minHeight: 350, idealHeight: 600, maxHeight: 600)
//Message
VStack(alignment: .leading) {
Text("\(07/22/20) - Day \(1)")
.font(.body)
.fontWeight(.light)
.foregroundColor(Color.gray)
.padding(.leading)
Text("what I Love)
.font(.title)
.fontWeight(.bold)
.padding([.leading, .bottom])
Text("paragraph")
.padding([.leading, .bottom, .trailing])
}
}
.edgesIgnoringSafeArea(.all)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
}
}
Any help would be much appreciated!
EDIT: Here are two images for reference- Sorry I didn't realize you were able to, though I should've figured that!
If I understood correctly, you want the size of the image so you can use it's dimensions in it's own frame? You could make a function that takes in an image name and returns your desired image while also setting #State vars for the width and height which you can use in the frame.
#State var width: CGFloat = 0
#State var height: CGFloat = 0
func image(_ name: String) -> UIImage {
let image = UIImage(named: name)!
let width = image.size.width
let height = image.size.height
DispatchQueue.main.async {
if width > height {
// Landscape image
// Use screen width if < than image width
self.width = width > UIScreen.main.bounds.width ? UIScreen.main.bounds.width : width
// Scale height
self.height = self.width/width * height
} else {
// Portrait
// Use 600 if image height > 600
self.height = height > 600 ? 600 : height
// Scale width
self.width = self.height/height * width
}
}
return image
}
Usage:
GeometryReader { geometry in
Image(uiImage: image("image1"))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: width, height: height)
.overlay(
Rectangle()
.foregroundColor(.clear)
.background(LinearGradient(gradient: Gradient(colors: [.clear, .white]), startPoint: .center, endPoint: .bottom))
)
.offset(y: geometry.frame(in: .global).minY > 0 ? -geometry.frame(in: .global).minY : 0)
edit: Added the DispatchQueue.main.async block as a quick fix for 'Modifying state during view update' warning. Works but probably not the absolute best way to go about it. Might be better to put the image into it's own #State and set it in the view's onAppear method.
edit 2: Moved the image into it's own #State var which I believe is a better solution as it will make the view more reusable as you can pass an image instead of hard coding the name of the image in the view.
#State var image: UIImage {
didSet {
let width = image.size.width
let height = image.size.height
DispatchQueue.main.async {
if width > height {
// Landscape image
self.width = width > UIScreen.main.bounds.width ? UIScreen.main.bounds.width : width
self.height = self.width/width * height
} else {
self.height = height > 600 ? 600 : height
self.width = self.height/height * width
}
}
}
}
Usage:
GeometryReader { geometry in
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: width, height: height)
.overlay(
Rectangle()
.foregroundColor(.clear)
.background(LinearGradient(gradient: Gradient(colors: [.clear, .white]), startPoint: .center, endPoint: .bottom))
)
.offset(y: geometry.frame(in: .global).minY > 0 ? -geometry.frame(in: .global).minY : 0)
Will have to update how the ContentView is declared by passing in a UIImage:
ContentView(image: UIImage(named: "image0")!)