ContextMenu on a rounded LinearGradient produces sharp edges in SwiftUI - swift

I have the following view:
struct ContentView: View {
var body: some View {
LinearGradient(gradient: Gradient(colors: [.blue, .red]), startPoint: .topTrailing, endPoint: .bottomLeading)
.cornerRadius(16)
.frame(width: 140, height: 140)
.contextMenu {
Button("", action: {})
}
}
}
However, when the ContextMenu is invoked, the edges are not rounded:
I've tried several things, such as:
Applying the clipShape modifier to clip it to a RoundedRectangle
Wrapping the gradient as the background of a RoundedRectangle view
Using a Color instead of a LinearGradient (which works as expected, but not what I need)
However none work. Any advice would be appreciated, thanks!

Add the following code after .frame(...):
.contentShape(RoundedRectangle(cornerRadius: 16, style: .continuous))

Updated for Swift 5.7.2
TLDR:
// Use the method that takes in ContentShapeKinds
.contentShape(.contextMenuPreview, RoundedRectangle(cornerRadius: 30))
Placed after .frame(...), and before .contextMenu { ... }
Detailed example:
var body: some View {
VStack(alignment: .leading, spacing: .zero) {
Text("Hello")
.foregroundColor(.white)
.font(.title)
}
.frame(maxWidth: .infinity, minHeight: 120)
// These corner radii should match
.background(RoundedRectangle(cornerRadius: 30).foregroundColor(.blue))
.contentShape(.contextMenuPreview, RoundedRectangle(cornerRadius: 30))
.contextMenu {
Button("Look!") { print("We did it!") }
}
}
The above code produces a contextMenu that looks like this:
... instead of this:

Related

Animating background gradient on view

Attempting to fool around with animating a background gradient. When animation is toggled, it would change the end radius. It is creating weird behavior whenever I incorporate the animation (gif below) where it moves the whole VStack.
I tried to put the stack in a ZStack thinking that would be a solution, but end result still the same.
Curious what exactly is causing the behavior
struct LandingPage: View {
#AppStorage("signedIn") var signedIn = false
#Environment (\.dismiss) var dismiss
#StateObject var vm = DashboardLogic()
#State private var animateGradient = false
#ViewBuilder
var body: some View {
if(signedIn){
// Text("Random Page")
}
else{
NavigationView{
VStack{
Image("bodybuilding-1") // << main image
.resizable()
.scaledToFit()
.frame(width:150, height:150)
//.renderingMode(.template)
.foregroundColor(.black)
.padding(.top, 200)
Text("Welcome to Meal Journal")
.font(.title)
.padding()
.offset(y:-25) // << adjusts title
VStack{
NavigationLink(destination:dummyPage() .navigationBarHidden(true),
label:{
Text("Get Started").fontWeight(.bold)
.frame(minWidth: 0, maxWidth: 200)
.padding(10)
.foregroundColor(.white)
//draw rectange around buttons
.background(
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
colors: [.orange, .yellow],
startPoint: .topLeading,
endPoint: .bottomTrailing
)))
})
NavigationLink(destination: DummyPage().navigationBarHidden(true), label: {
Text("Login").fontWeight(.semibold)
.frame(minWidth:0, maxWidth: 200)
.padding(10)
.foregroundColor(.black)
.overlay( RoundedRectangle(cornerRadius: 25)
.stroke(Color.gray, lineWidth: 3)
)
})
.padding()
}
Rectangle()
.frame(height: 0)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
}
//.background(Color.purple)
.background(RadialGradient(gradient: Gradient(colors: [.yellow, .green]), center: .center, startRadius: 312, endRadius: animateGradient ? 100 : 450))
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: true)) {
animateGradient.toggle()
}
}
}
}
}
}
}
I didn't carefully check your entire code, but one thing that may help is remembering that background is a View on its own, so you can (and often have to) apply modifiers to the background view itself. So instead of .onAppear on the parent view, add it to a background view, i.e.:
var backgroundView: some View {
RadialGradient(gradient: Gradient(colors: [.yellow, .green]), center: .center, startRadius: 312, endRadius: animateGradient ? 100 : 450)
.onAppear {
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: true)) {
animateGradient.toggle()
}
}
}
}
and then parent view simply uses background view:
VStack {
Image("bodybuilding-1")
...
}
.background(backgroundView)
Playground example:
struct AnimatedBackgroundView: View {
#State private var animateGradient = false
#State var scale = 1.0
var body: some View {
VStack {
Text("Hello")
Button("Do it", action: {})
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
.scaleEffect(scale)
.background(backgroundView)
}
var backgroundView: some View {
RadialGradient(gradient: Gradient(colors: [.yellow, .green]), center: .center, startRadius: 312, endRadius: animateGradient ? 100 : 450)
.onAppear {
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: true)) {
animateGradient.toggle()
}
}
}
}
struct ContentView: View {
var body: some View {
AnimatedBackgroundView()
.frame(
width: 500,
height: 600
)
}
}
PlaygroundPage.current.setLiveView(ContentView())
But like I said: I didn't investigate your entire code, so maybe some other issues prevent it from working

Ignore safe area in safe area

I'm trying to use safeAreaInset(edge:alignment:spacing:content:), to create a floating view at the bottom. The green view should ignore the bottom safe area, but this isn't working.
Code:
struct ContentView: View {
var body: some View {
ScrollView {
LinearGradient(
colors: [.red, .blue],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 1000)
}
.safeAreaInset(edge: .bottom) {
Color.green
.frame(height: 50)
.ignoresSafeArea(edges: .bottom)
}
}
}
Current result:
How do I make the green view ignore the bottom safe area so it touches the bottom?
Once you spoke about keyboard you add another difficulty to your original question! I just made simple code with overlay method it can easily converted to ZStack method as well! You do which way you like! Also I just add some fun on green View just for show and need refactor for sure, but that is not the part of issue or question!
struct ContentView: View {
#State private var string: String = String()
var body: some View {
let myDivider = Spacer().overlay(Color.secondary.frame(width: 1, height: 40))
Color.clear
.ignoresSafeArea(.all)
.overlay(
ScrollView {
ZStack {
LinearGradient(gradient: .init(colors: [.red, .blue]), startPoint: .top, endPoint: .bottom)
.frame(height: 1000)
VStack {
Spacer()
TextField("Enter youer text here ...", text: $string)
Spacer()
}
}
}
)
.overlay(
Color.green
.overlay(HStack { Spacer(); Text("🙈").padding(20); myDivider; Text("🙉").padding(20); myDivider; Text("🙊").padding(); Spacer() }.font(Font.system(.largeTitle)))
.frame(height: 80)
, alignment: .bottom)
.ignoresSafeArea(.container)
}
}
Creating an overlay(alignment:content:) after ignoring the safe area works. A view such as Color.clear can be used as the base.
Code:
struct ContentView: View {
var body: some View {
ScrollView {
LinearGradient(
colors: [.red, .blue],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 1000)
}
.safeAreaInset(edge: .bottom) {
Color.clear
.frame(height: 50)
.ignoresSafeArea(edges: .bottom)
.overlay(
Color.green
)
}
}
}
Result:
And if you have content and want a background in the safe area:
Color.red
.frame(height: 50)
.ignoresSafeArea(edges: .bottom)
.background(
Color.green
)
Result:
Note: be careful with the 2nd version. If you use modifiers such as clipShape on the background, it can cause it to break again. Fix this by using .ignoresSafeArea(edges: .bottom) after you may clip or anything else which breaks this.

Text opacity gradient

I'm looking to make text to appear to fade out on the edge. Here is what I have so far:
struct ContentView: View {
var body: some View {
ZStack {
Color.red
Text("Hello world!")
// .blendMode(.luminosity) // <- Here?
.overlay(
LinearGradient(colors: [.white.opacity(0), .white], startPoint: .leading, endPoint: .trailing)
.frame(width: 50)
// .blendMode(.exclusion) // <- Here?
.frame(maxWidth: .infinity, alignment: .trailing)
)
}
}
}
Producing:
You can see the white gradient over the right side of the text. This should act like an opacity filter. Where the white isn't over, the text is fully visible. Below the white, the text should be completely transparent. It should be a gradual transition by using the gradient.
I think this can be solved using blendMode(_:), but I'm not too sure. I don't know which mode to use, nor where to apply it.
The solution to this should work in light & dark mode (i.e. the text may be black or white). The actual background behind is not a constant color (such as red) so using a red gradient overlay is not what I'm looking for.
Just use .mask instead.
struct ContentView: View {
var body: some View {
ZStack {
Color.red
Text("Hello world!")
/// for iOS 14 and below, replace { } with ( )
.mask {
/// no need for .opacity, just use .clear
LinearGradient(colors: [.white, .clear], startPoint: .leading, endPoint: .trailing)
}
}
}
}
Result:
With thanks to aheze's answer, it now works.
Just a small thing I'll add, to show the way to get the exact same results as in the question (because mask sort of inverts it):
struct ContentView: View {
var body: some View {
ZStack {
Color.red
Text("Hello world!")
.mask(
HStack(spacing: 0) {
Color.white
LinearGradient(colors: [.white, .clear], startPoint: .leading, endPoint: .trailing)
.frame(width: 50)
}
)
}
}
}
Result:

Overlay text on Image SwiftUI and WidgetKit

I'm currently creating a widget for my new iOS 14 app and I encountered a weird problem when I'm trying to add and Overlay with Text on top of my Image. The overlay is correctly working on a blank SwiftUI project but it's not working on the Widget.
Below you can find the code that I'm using. KFImage is from KingFisherSwiftUI, but if you want to test it, you can just add a static Image from the Asset or from the System.
Thanks!
struct NewItems: View {
var body: some View {
KFImage(URL(string: "https://img.freepik.com/free-vector/triangular-dark-polygonal-background_23-2148261453.jpg?size=626&ext=jpg")! )
.resizable()
//.frame(width: 200, height: 200, alignment: .center)
.overlay(TextView(), alignment: .bottomTrailing)
.cornerRadius(20)
}
}
struct TextView: View {
var body: some View {
ZStack {
Text("Hello World Beautiful wallpaper")
.foregroundColor(.white)
.shadow(color: .black, radius: 1, x: 1.0, y: 1.0)
.padding(6)
.opacity(0.8)
}
.background(Color.black)
.cornerRadius(10)
.padding(3)
}
}
[EDIT]
Below the result of the code. Looks like the image is on top of the text, even though it supposed to be in overlay.
In the end after multiple tries I managed to find an alternative solution.
struct TextView: View {
let entry: HelloWorldEntry
var body: some View {
ZStack {
Text("SOME TEXT")
.foregroundColor(.white)
.shadow(color: .black, radius: 1, x: 1.0, y: 1.0)
.padding(6)
.opacity(0.8)
}
.background(Color.black)
.cornerRadius(10)
.padding(6)
}
}
struct NewTest: View {
let entry: HelloWorldEntry
var body: some View {
VStack {}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
KFImage(URL(string: "https://example.com/image.png")!)
.resizable()
.scaledToFill()
)
.overlay(TextView(entry: entry), alignment: .bottomTrailing)
}
}

SwiftUI: How to make entire shape recognize gestures when stroked?

I'm creating a custom button in SwiftUI using some shapes.
As a minimal example, I have a filled rectangle, enclosed by a stroked circle (no fill). This is wrapped in a ZStack and a TapGesture is added to that. It works, but my only issue is that the empty space between the square and the circle is not tappable.
How can I make everything inside the circle tappable, without adding a fill to the circle?
struct ConfirmButton: View {
var action: () -> Void
var body: some View {
ZStack {
Circle()
.stroke(Color.purple, lineWidth: 10.0)
.padding(5)
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200, alignment: .center)
}.gesture(
TapGesture()
.onEnded {
print("Hello world")
self.action()
}
)
}
}
You need to define the hit area, with modifier .contentShape():
struct ConfirmButton: View {
var action: () -> Void
var body: some View {
ZStack {
Circle()
.stroke(Color.purple, lineWidth: 10.0)
.padding(5)
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200, alignment: .center)
}.contentShape(Circle())
.gesture(
TapGesture()
.onEnded {
print("Hello world")
self.action()
}
)
}
}