I'm having trouble positioning text in the center of a black overlay in SwiftUI. I want the white text to be aligned in the middle of the black box where I marked with a red line in the image below. Despite trying different alignments and padding, I can't seem to get the text to appear where I want it.
Here's the image showing where I want the text to be:
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: customSize.width, height: customSize.height)
.clipped()
.overlay(
VStack(alignment: .leading) {
Spacer()
HStack {
VStack(alignment: .leading) {
if let name = channel.tvgName {
Text(formatTitle(name))
.font(.title3.bold()) // Adjust the font size and style
.foregroundColor(.white)
.lineLimit(1)
.shadow(radius: 5) // Add shadow for better contrast
.padding(.all, 0)
} else {
Text(formatTitle(channel.tvgId ?? ""))
.font(.title3.bold()) // Adjust the font size and style
.foregroundColor(.white)
.lineLimit(1)
.shadow(radius: 5) // Add shadow for better contrast
.padding(.all, 0)
}
}
Spacer()
}
.padding([.leading, .bottom, .trailing], 50)
.background(LinearGradient(
gradient: Gradient(colors: [Color.blue, Color.blue.opacity(0.1)]),
startPoint: .bottom,
endPoint: .top
))
}
)
.cornerRadius(10)
.shadow(radius: 5)
Full view code:
import SwiftUI
import CachedAsyncImage
struct ChannelDetailsView: View {
var channel: PlaylistItem
var reader: GeometryProxy
@State var customSize: CGSize = CGSize(width: 256, height: 418) // Updated size
@State private var showByGroup = UserDefaults.standard.bool(forKey: "showByGroup")
var body: some View {
VStack(spacing: 0) {
if let logo = channel.tvgLogo {
CachedAsyncImage(url: URL(string: logo)) { phase in
switch phase {
case .empty:
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray.opacity(0.3))
.cornerRadius(10)
.frame(width: customSize.width,
height: customSize.height)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: customSize.width, height: customSize.height)
.clipped()
.overlay(
VStack(alignment: .leading) {
Spacer()
HStack {
VStack(alignment: .leading) {
if let name = channel.tvgName {
Text(formatTitle(name))
.font(.title3.bold()) // Adjust the font size and style
.foregroundColor(.white)
.lineLimit(1)
.shadow(radius: 5) // Add shadow for better contrast
.padding(.all, 0)
} else {
Text(formatTitle(channel.tvgId ?? ""))
.font(.title3.bold()) // Adjust the font size and style
.foregroundColor(.white)
.lineLimit(1)
.shadow(radius: 5) // Add shadow for better contrast
.padding(.all, 0)
}
}
Spacer()
}
.padding([.leading, .bottom, .trailing], 50)
.background(LinearGradient(
gradient: Gradient(colors: [Color.blue, Color.blue.opacity(0.1)]),
startPoint: .bottom,
endPoint: .top
))
}
)
.cornerRadius(10)
.shadow(radius: 5)
case .failure:
Image(systemName: "play.display")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.padding()
.background(Color.gray.opacity(0.3))
.cornerRadius(10)
.frame(width: customSize.width,
height: customSize.height)
@unknown default:
EmptyView()
}
}
}
}
.onAppear(perform: {
customSize = reader.size
})
.frame(width: customSize.width, height: customSize.height)
.background(Color.black.opacity(0.2))
.cornerRadius(10)
.shadow(radius: 5)
.onAppear {
print("width: \(reader.size.width)")
print("height: \(reader.size.height)")
}
.onChange(of: reader.size) { oldValue, newValue in
customSize = newValue
}
}
// Function to format the title
func formatTitle(_ title: String) -> String {
var formattedTitle = title
// Remove "4K"
formattedTitle = formattedTitle.replacingOccurrences(of: "4K", with: "", options: .caseInsensitive)
// Remove year in parentheses, e.g., "(2021)"
let regex = try! NSRegularExpression(pattern: "\\(\\d{4}\\)", options: [])
formattedTitle = regex.stringByReplacingMatches(in: formattedTitle, options: [], range: NSRange(location: 0, length: formattedTitle.count), withTemplate: "")
// Remove any extra spaces
formattedTitle = formattedTitle.trimmingCharacters(in: .whitespacesAndNewlines)
return formattedTitle
}
}
// Extension to apply corner radius to specific corners
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
Instead of using stacks and spacers to align things, you can just pass an alignment to the overlay
modifier. The Text
will be automatically put on the bottom centre of the image.
Then all you need to do is add some vertical padding on the Text
, and do .frame(maxWidth: .infinity)
so that it occupies the full width of the image. Finally, you add the gradient background and the text will be centered inside that.
Image("some image")
.resizable()
.scaledToFit()
.overlay(alignment: .bottom) {
Text("The Instigators")
.font(.title3.bold())
.foregroundStyle(.white)
.lineLimit(1)
.shadow(radius: 5)
.padding(.vertical, 20)
.frame(maxWidth: .infinity)
.background(.linearGradient(
colors: [.blue, .blue.opacity(0.1)],
startPoint: .bottom,
endPoint: .top
))
}
.padding()
I see that you are using a lot of deprecated view modifiers. In case you need to support versions below iOS 15, the alignment:
argument for overlay
is the second argument before iOS 15.
.overlay(
Text("The Instigators")
.font(.title3.bold())
.foregroundColor(.white)
// ...
,
alignment: .bottom
)
Last but not least, consider replacing the if let
statement - you can just do:
Text(formatTitle(channel.tvgName ?? channel.tvgId ?? ""))