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

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))

Related

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 text fit within boundaries of circular shape/image SwiftUI

I can't seem to grasp how to make sure that my text overlays and fits within the circular image's boundaries. This is what I have so far.
Circle()
.frame(width: 100, height: 100)
.overlay(
Text("Reaaaallllllyyyyyy Looooooongggggg")
.foregroundColor(Color.red)
.frame(minWidth: 0, maxWidth: .infinity)
)
Is there a way in SwiftUI to get this to work? How would I go about achieving this?
struct ClippedCircleView: View {
#State var text: String = "Reaaaallllllyyyyyy Looooooongggggg"
// Make this any number you are comfortable with
#State var letterLimit: Int = 12
var body: some View {
Circle()
.frame(width: 100, height: 100)
.overlay(
Text(text)
//Limit to 1 line until your letterLimit
.lineLimit(text.count <= letterLimit ? 1 : nil)
//Clips it to shape
.clipShape(ContainerRelativeShape()).padding()
.foregroundColor(Color.red)
//This makes super tiny letters so adjust to your use case
.minimumScaleFactor(0.1)
)
}
}

Swift UI: Center Text into Circle

I try to put a circle-shaped frame around a text view, but I just can't get it properly aligned and I can't see where the problem is. As you can see in the picture below, the text is sometimes offset to left, while it should be centred. Any ideas on how to fix it?
struct ContentView: View {
let result = getDate();
var body: some View {
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 5) {
ForEach(result, id: \.self) {
day in
Text(day.name!)
.frame(width: 35, height: 35, alignment: .center)
.padding()
.overlay(
Circle()
.size(width: 35, height: 35)
.offset(x: 17.5,y: 17.5)
.scale(1.4)
.stroke(Color.orange, lineWidth: 4)
)
}
}
}
Spacer()
}.background(Color.white)
}
}
The circle is offset by half of the size of the frame, so its origin should be in the centre. The Text should be centered as well in .frame(width: 35, height: 35, alignment: .center).
Thanks a lot for helping! :)
.overlay already has size of container with Text an Circle centred in it, so it is just needed to operate with insets of circle not shifting it, like below
Text(day.name!)
.frame(width: 35, height: 35, alignment: .center)
.padding()
.overlay(
Circle()
.stroke(Color.orange, lineWidth: 4)
.padding(6)
)

How to access parents frame in SwiftUI

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.