I thought contentShape() would affect the hover "area" the same way it affects the clickable area.
The following image is an example where the hover should not be triggered.
Full example code:
struct ContentView: View {
#State var hovering: Bool = false
var body: some View {
Rectangle()
.frame(width: 120, height: 120)
.foregroundColor(hovering ? Color.white : Color.red)
.clipShape(Circle())
.contentShape(Circle())
.onHover { hovering in
self.hovering = hovering
}
.onTapGesture {
print("Click")
}
.padding(24)
}
}
Is there a way to clip the hover area like the click area?
Add a clear background that cancels the hover value on hover. It’s not going to be perfect as mouse tracking in SwiftUI has lag errors.
Related
I am using Xcode to create an app that requires buttons. Right now, when I create a button, I get the text label, which I want, but I also get a background with rounded corners around it. I want to have the button with just the label but without the background. I was using Swift Playgrounds before Xcode and did not have this problem.
Here is my code:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.frame(width: 1000, height: 500)
.foregroundColor(.red)
Button(action: {
}) {
Text("Button")
}
}
}
}
Customize the button's style with the .buttonStyle view modifier:
Button(action: {}) {
Text("Button")
}
.buttonStyle(.borderless)
.borderless, .plain, and .link are the options that will result in no border.
Here is a Button without any background or border.
Button("Click") {
//do something
}
.background(.clear) //this
This is probably a custom view but in the Reddit app there's a toolbar and the top left button(3 lines) opens this kind of view from the side that moves the current view to the right so you can only see about 25% of it and a new view that takes up about 75% of the screen slides in. Is there anything like this built into SwiftUI and if there isn't how would I go about implementing something like this?
This is my custom side bar behave similarly to what you just mentioned, you can try it. (Images and Code are below)
Before click:
After clicked:
struct ContentView: View {
#State var isClicked = false
var body: some View {
HStack {
Rectangle()
.fill(.orange)
.frame(width: isClicked ? UIScreen.main.bounds.width * 0.75 : 0)
VStack {
HStack {
Button {
withAnimation {
isClicked.toggle()
}
} label: {
Image(systemName: "menucard.fill")
.padding(.leading)
}
Spacer()
}
Spacer()
}
}
}
}
I am trying to find swiftUI code implementing marquee which shows only the rolling text content inside the text view. Text rolling outside to the left and right of the text view will not be shown.
Put the text in an overlay of your box and animate the .offset to scroll the text. Add .clipped() to the box to clip any text that appears outside of the box:
struct ContentView: View {
#State private var textoffset = 300.0
let text = "This is a test of scrolling text. This is only a test."
var body: some View {
Color.yellow
.frame(width: 250, height: 100)
.overlay (
Text(text)
.fixedSize()
.offset(x: textoffset, y: 0)
)
.animation(.linear(duration: 10)
.repeatForever(autoreverses: false), value: textoffset)
.clipped()
.onAppear {
textoffset = -300.0
}
}
}
I am trying to add a contextmenu to images I display, but when I longpress, the contextmenu opens in a wrong location (see image)
The code I use is:
ForEach(self.document.instruments) { instrument in
Image(instrument.text)
.resizable()
.frame(width: 140, height: 70)
.position(self.position(for: instrument, in: geometry.size))
.gesture(self.singleTapForSelection(for: instrument))
.gesture(self.dragSelectionInstrument(for: instrument))
.shadow(color: self.isInstrumentSelected(instrument) ? .blue : .clear, radius: 10 * self.zoomScale(for: instrument))
.contextMenu {
Button {
print("Deleted selected")
} label: {
Label("Delete", systemImage: "trash")
}
}
}
Update of issue
The issue I still face (as my last comment), is that when I drag an image, there seems to be a delay, as the image remains on its position for a short moment and then moves to the dragged position. I already found out that when I have the contextMenu in the code, the delay dragging happens, when commented out, it works fine.
This is the code I have:
ForEach(self.document.instruments) { instrument in
Image(instrument.text)
.resizable()
.frame(width: 140, height: 70)
.contextMenu {
Button {
print("Deleted selected")
} label: {
Label("Delete", systemImage: "trash")
}
}
.position(self.position(for: instrument, in: geometry.size))
.shadow(color: self.isInstrumentSelected(instrument) ? .blue : .clear, radius: 10 * self.zoomScale(for: instrument))
.gesture(self.singleTapForSelection(for: instrument))
.gesture(self.dragSelectionInstrument(for: instrument))
}
I even tried moving the gestures above the contextmenu, but didn't work.
Please help
Latest Update
Nm, it was only on the simulator, on my physical iPad it works fine
The problem is that your code applies the contextMenu modifier after the position modifier.
Let's consider this slightly modified example:
ZStack {
GeometryReader { geometry in
ForEach(self.document.instruments, id: \.id) { instrument in
Image(instrument.text)
.frame(width: 140, height: 70)
.position(self.position(for: instrument, in: geometry.size))
.contextMenu { ... }
}
}
}
In the SwiftUI layout system, a parent view is responsible for assigning positions to its child views. A view modifier acts as the parent of the view it modifies. So in the example code:
ZStack is the parent of GeometryReader.
GeometryReader is the parent of ForEach.
ForEach is the parent of contextMenu.
contextMenu is the parent of position.
position is the parent of frame.
frame is the parent of Image.
Image is not a parent. It has no children.
(Sometimes the parent is called the “superview” and the child is called the “subview”.)
When contextMenu needs to know where to draw the menu on the screen, it looks at the position given to it by its parent, the ForEach, which gets it from the GeometryReader, which gets it from the ZStack.
When Image needs to know where to draw its pixels on the screen, it looks at the position given to it by its parent, which is the frame modifier, and the frame modifier gets the position from the position modifier, and the position modifier modifies the position given to it by the contextMenu.
This means that the position modifier does not affect where the contextMenu draws the menu.
Now let's rearrange the code so contextMenu is the child of position:
ZStack {
GeometryReader { geometry in
ForEach(self.document.instruments, id: \.id) { instrument in
Image(instrument.text)
.frame(width: 140, height: 70)
.contextMenu { ... }
.position(self.position(for: instrument, in: geometry.size))
}
}
}
Now the contextMenu gets its position from the position modifier, which modifies the position given to it by the ForEach. So in this scenario, the position modifier does affect the where the contextMenu draws the menu.
In SwiftUI, I've managed to make a Button animate right when the view is first drawn to the screen, using the animation(_:) modifier, that was deprecated in macOS 12.
I've tried to replace this with the new animation(_:value:) modifier, but this time nothing happens:
So this is not working:
struct ContentView: View {
#State var isOn = false
var body: some View {
Button("Press me") {
isOn.toggle()
}
.animation(.easeIn, value: isOn)
.frame(width: 300, height: 400)
}
}
But then this is working. Why?
struct ContentView: View {
var body: some View {
Button("Press me") {
}
.animation(.easeIn)
.frame(width: 300, height: 400)
}
}
The second example animates the button just as the view displays, while the first one does nothing
The difference between animation(_:) and animation(_:value:) is straightforward. The former is implicit, and the latter explicit. The implicit nature of animation(_:) meant that anytime ANYTHING changed, it would react. The other issue it had was trying to guess what you wanted to animate. As a result, this could be erratic and unexpected. There were some other issues, so Apple has simply deprecated it.
animation(_:value:) is an explicit animation. It will only trigger when the value you give it changes. This means you can't just stick it on a view and expect the view to animate when it appears. You need to change the value in an .onAppear() or use some value that naturally changes when a view appears to trigger the animation. You also need to have some modifier specifically react to the changed value.
struct ContentView: View {
#State var isOn = false
//The better route is to have a separate variable to control the animations
// This prevents unpleasant side-effects.
#State private var animate = false
var body: some View {
VStack {
Text("I don't change.")
.padding()
Button("Press me, I do change") {
isOn.toggle()
animate = false
// Because .opacity is animated, we need to switch it
// back so the button shows.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
animate = true
}
}
// In this case I chose to animate .opacity
.opacity(animate ? 1 : 0)
.animation(.easeIn, value: animate)
.frame(width: 300, height: 400)
// If you want the button to animate when the view appears, you need to change the value
.onAppear { animate = true }
}
}
}
Follow up question: animating based on a property of an object is working on the view itself, but when I'm passing that view its data through a ForEach in the parent view, an animation modifier on that object in the parent view is not working. It won't even compile. The objects happen to be NSManagedObjects but I'm wondering if that's not the issue, it's that the modifier works directly on the child view but not on the passed version in the parent view. Any insight would be greatly appreciated
// child view
struct TileView: View {
#ObservedObject var tile: Tile
var body: some View {
Rectangle()
.fill(tile.fillColor)
.cornerRadius(7)
.overlay(
Text(tile.word)
.bold()
.font(.title3)
.foregroundColor(tile.fillColor == .myWhite ? .darkBlue : .myWhite)
)
// .animation(.easeInOut(duration: 0.75), value: tile.arrayPos)
// this modifier worked here
}
}
struct GridView: View {
#ObservedObject var game: Game
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 4)
var body: some View {
GeometryReader { geo in
LazyVGrid(columns: columns) {
ForEach(game.tilesArray, id: \.self) { tile in
Button(action: {
tile.toggleSelectedStatus()
moveTiles() <- this changes their array position (arrayPos), and
the change in position should be animated
}) {
TileView(tile: tile)
.frame(height: geo.size.height * 0.23)
}
.disabled(tile.status == .solved || tile.status == .locked)
.animation(.easeInOut(duration: 0.75), value: arrayPos)
.zIndex(tile.status == .locked ? 1 : 0)
}
}
}
}
}