How can I add modifiers according to my needs to individual buttons?
I have a ForEach loop running in a range of 0..<5 & I need to add an animation of .rotation3DEffect in clicked button and another .opacity on remaining un-clicked buttons, according to their state.
Note: I can watch for any state value inside buttons action, and then change it to have the animation, but since the modifier is applied on
the Button itself inside the ForEach loop, I am having the animation
applied to every buttons inside the foreach.
#State private var rotationDegree = 0.0
#State private var selectedNum = 0
#State private var correctAnswer = 0 // comes from saved data
var body: some View {
ForEach(0..<5) { num in // image as a button, loops through their prefix name
Button {
selectedNum = (num == correctAnswer) ? num : 0
withAnimation {
// once clicked, will animate all 5 buttons, need only this clicked button to animate.
if (num == correctAnswer) {
rotationDegree += 360
}
}
} label: {
Image("imageName\(num)")
}
.rotation3DEffect((.degrees((selectedNum == correctAnswer) ? rotationDegree : 0.0)), axis: (x: 0, y: (selectedNum == correctAnswer) ? 1 : 0, z: 0)) // animation I want to add to a specific button
}
}
import SwiftUI
struct AnimatedListView: View {
var body: some View {
VStack{
ForEach(0..<5) { num in
//The content of the ForEach goes into its own View
AnimatedButtonView(num: num)
}
}
}
}
struct AnimatedButtonView: View {
//This creates an #State for each element of the ForEach
#State private var rotationDegree = 0.0
//Pass the loops data as a parameter
let num: Int
var body: some View {
Button {
withAnimation {
rotationDegree += 360
}
} label: {
Image(systemName: "person")
Text(num.description)
}
.rotation3DEffect((.degrees(rotationDegree)), axis: (x: 0, y: 1, z: 0))
}
}
struct AnimatedListView_Previews: PreviewProvider {
static var previews: some View {
AnimatedListView()
}
}
Try adding this to your view.
#State var selectedNum: Int = 0
Then change your button to look something like this.
Button {
selectedNum = num
withAnimation {
rotationDegree += 360
}
} label: {
Image("imageName\(num)")
}
// Check if the selected button is equal to the num
.rotation3DEffect((.degrees(selectedNum == num ? rotationDegree : 0.0)), axis: (x: 0, y: 1, z: 0))
Not sure how the rotation3DEffect works. But here is another example
.background(selectedNum == num ? .red : .blue)
Related
I'm trying to rearrange or move items in a LazyVGrid inside a ScrollView using .draggable and .dropDestination view modifier based on the post here.
My problem is that I need to know which item is being dragged and particularly when the user stops dragging the item, much like onEnded for DragGesture. This works fine when the item is dropped inside a view with a .dropDestination but if the user drops it outside the item get "stuck" as being the draggedItem. See the video:
Drag and Drop outside of view
Is there a way to tell when the item is dropped, regardless of where it's dropped?
Here's my code currently which only works if item is dropped within the views with .dropDestination.
import SwiftUI
import UniformTypeIdentifiers
extension UTType {
static var itemTransferable = UTType(exportedAs: "com.styrka.DragableGrid.item")
}
struct ItemDraggable: Identifiable, Equatable, Transferable, Codable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(for: ItemDraggable.self, contentType: .itemTransferable)
}
var id: Int
}
struct MainView: View {
let columns = [
GridItem(.fixed(160)),
GridItem(.fixed(160))
]
#State private var items = (0..<20).map { ItemDraggable(id: $0) }
#State private var draggingItem: ItemDraggable?
var body: some View {
ScrollView {
Text("Items, dragging: \(draggingItem?.id ?? -1)")
LazyVGrid(columns: columns) {
ForEach(items) { item in
DraggableView(item: item, draggingItem: $draggingItem)
}
}
}
.background(Color.white)
.dropDestination(for: ItemDraggable.self) { items, location in
// User to drop items outside but does not cover the whole app
draggingItem = nil
return true
}
}
}
struct DraggableView: View {
var item: ItemDraggable
#Binding var draggingItem: ItemDraggable?
#State private var borderColor: Color = .black
#State private var borderWidth: CGFloat = 0.0
var body: some View {
Text("\(item.id)").font(.caption)
.frame(width: 100, height: 100)
.background(RoundedRectangle(cornerRadius: 16).fill(.blue).opacity(0.6))
.border(borderColor, width: borderWidth)
.opacity(item == draggingItem ? 0.1 : 1)
.draggable(item, preview: {
Text("Dragging item \(item.id)")
.onAppear {
// Set binding when draggable Preview appears..
draggingItem = item
}
.onDisappear{
// Called as soon as the dragged item leaves the 'DraggedView' frame
//draggingItem = nil
}}
)
.dropDestination(for: ItemDraggable.self) { items, location in
draggingItem = nil
return true
} isTargeted: { inDropArea in
borderColor = inDropArea ? .accentColor : .black
borderWidth = inDropArea ? 10.0 : 0.0
}
}
}
How can I add modifiers according to my needs to individual buttons?
I have a ForEach loop running in a range of 0..<5 & I need to add an animation of .rotation3DEffect in clicked button and another .opacity on remaining un-clicked buttons, according to their state.
Note: I can watch for any state value inside buttons action, and then change it to have the animation, but since the modifier is applied on
the Button itself inside the ForEach loop, I am having the animation
applied to every buttons inside the foreach.
#State private var rotationDegree = 0.0
#State private var selectedNum = 0
#State private var correctAnswer = 0 // comes from saved data
var body: some View {
ForEach(0..<5) { num in // image as a button, loops through their prefix name
Button {
selectedNum = (num == correctAnswer) ? num : 0
withAnimation {
// once clicked, will animate all 5 buttons, need only this clicked button to animate.
if (num == correctAnswer) {
rotationDegree += 360
}
}
} label: {
Image("imageName\(num)")
}
.rotation3DEffect((.degrees((selectedNum == correctAnswer) ? rotationDegree : 0.0)), axis: (x: 0, y: (selectedNum == correctAnswer) ? 1 : 0, z: 0)) // animation I want to add to a specific button
}
}
import SwiftUI
struct AnimatedListView: View {
var body: some View {
VStack{
ForEach(0..<5) { num in
//The content of the ForEach goes into its own View
AnimatedButtonView(num: num)
}
}
}
}
struct AnimatedButtonView: View {
//This creates an #State for each element of the ForEach
#State private var rotationDegree = 0.0
//Pass the loops data as a parameter
let num: Int
var body: some View {
Button {
withAnimation {
rotationDegree += 360
}
} label: {
Image(systemName: "person")
Text(num.description)
}
.rotation3DEffect((.degrees(rotationDegree)), axis: (x: 0, y: 1, z: 0))
}
}
struct AnimatedListView_Previews: PreviewProvider {
static var previews: some View {
AnimatedListView()
}
}
Try adding this to your view.
#State var selectedNum: Int = 0
Then change your button to look something like this.
Button {
selectedNum = num
withAnimation {
rotationDegree += 360
}
} label: {
Image("imageName\(num)")
}
// Check if the selected button is equal to the num
.rotation3DEffect((.degrees(selectedNum == num ? rotationDegree : 0.0)), axis: (x: 0, y: 1, z: 0))
Not sure how the rotation3DEffect works. But here is another example
.background(selectedNum == num ? .red : .blue)
How can I add modifiers according to my needs to individual buttons?
I have a ForEach loop running in a range of 0..<5 & I need to add an animation of .rotation3DEffect in clicked button and another .opacity on remaining un-clicked buttons, according to their state.
Note: I can watch for any state value inside buttons action, and then change it to have the animation, but since the modifier is applied on
the Button itself inside the ForEach loop, I am having the animation
applied to every buttons inside the foreach.
#State private var rotationDegree = 0.0
#State private var selectedNum = 0
#State private var correctAnswer = 0 // comes from saved data
var body: some View {
ForEach(0..<5) { num in // image as a button, loops through their prefix name
Button {
selectedNum = (num == correctAnswer) ? num : 0
withAnimation {
// once clicked, will animate all 5 buttons, need only this clicked button to animate.
if (num == correctAnswer) {
rotationDegree += 360
}
}
} label: {
Image("imageName\(num)")
}
.rotation3DEffect((.degrees((selectedNum == correctAnswer) ? rotationDegree : 0.0)), axis: (x: 0, y: (selectedNum == correctAnswer) ? 1 : 0, z: 0)) // animation I want to add to a specific button
}
}
import SwiftUI
struct AnimatedListView: View {
var body: some View {
VStack{
ForEach(0..<5) { num in
//The content of the ForEach goes into its own View
AnimatedButtonView(num: num)
}
}
}
}
struct AnimatedButtonView: View {
//This creates an #State for each element of the ForEach
#State private var rotationDegree = 0.0
//Pass the loops data as a parameter
let num: Int
var body: some View {
Button {
withAnimation {
rotationDegree += 360
}
} label: {
Image(systemName: "person")
Text(num.description)
}
.rotation3DEffect((.degrees(rotationDegree)), axis: (x: 0, y: 1, z: 0))
}
}
struct AnimatedListView_Previews: PreviewProvider {
static var previews: some View {
AnimatedListView()
}
}
Try adding this to your view.
#State var selectedNum: Int = 0
Then change your button to look something like this.
Button {
selectedNum = num
withAnimation {
rotationDegree += 360
}
} label: {
Image("imageName\(num)")
}
// Check if the selected button is equal to the num
.rotation3DEffect((.degrees(selectedNum == num ? rotationDegree : 0.0)), axis: (x: 0, y: 1, z: 0))
Not sure how the rotation3DEffect works. But here is another example
.background(selectedNum == num ? .red : .blue)
I am adding the possibility to swipe in order to update a barchart. What I want to show is statistics for different station. To view different station I want the user to be able to swipe between the stations. I can see that the swiping works and each time I swipe I get the correct data from my controller. The problem is that my view is not redrawn properly.
I found this guide, but cannot make it work.
Say I swipe right from station 0 with data [100, 100, 100] to station 2, the retrieved data from my controller is [0.0, 100.0, 0.0]. The view I have still is for [100, 100, 100]`.
The station number is correctly updated, so I suspect it needs some state somehow.
Here is the code:
import SwiftUI
import SwiftUICharts
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName()).tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
struct BarCharts: View {
var data: [Double]
var title: String
init(data: [Double], disciplineName: String) {
self.data = data
title = disciplineName
print(data)
}
var body: some View {
VStack {
BarChartView(data: ChartData(points: self.data), title: self.title, style: Styles.barChartStyleOrangeLight, form: CGSize(width: 300, height: 400))
}
}
}
class ViewModel: ObservableObject {
#Published var station = 1
let controller = DetailedViewController()
var isPreview = false
func getData(kLatestRounds: Int, station: Int) -> [Double] {
if isPreview {
return [100.0, 100.0, 100.0]
} else {
let data = controller.getResults(kLatestRounds: kLatestRounds, station: station, fileName: userDataFile)
return data
}
}
func getName() -> String {
controller.getDiscipline().name
}
func getNumberOfStations() -> Int {
controller.getDiscipline().getNumberOfStations()
}
func getStation() -> Int {
station
}
func incrementStation() {
station = (station + 1) % getNumberOfStations()
}
func decrementStation() {
station -= 1
if station < 0 {
station = getNumberOfStations() - 1
}
}
}
The data is printed inside the constructor each time I swipe. Shouldn't that mean it should be updated?
I don’t use SwiftUICharts so I can’t test it, but the least you can try is manually set the id to the view
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName())
.id(viewmodel.station) // here. If it doesn’t work, you can set it to the whole TabView
.tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
I am trying to animate individual items on mouseover. The issue I am having is that every item gets animated on mouseover of an item instead of just that specific item.
Here is what I have:
struct ContentView : View {
#State var hovered = false
var body: some View {
VStack(spacing: 90) {
ForEach(0..<2) {_ in
HStack(spacing: 90) {
ForEach(0..<4) {_ in
Circle().fill(Color.red).frame(width: 50, height: 50)
.scaleEffect(self.hovered ? 2.0 : 1.0)
.animation(.default)
.onHover { hover in
print("Mouse hover: \(hover)")
self.hovered.toggle()
}
}
}
}
}
.frame(minWidth:300,maxWidth:.infinity,minHeight:300,maxHeight:.infinity)
}
}
It needs to change onHover view on per-view base, ie. store some identifier of hovered view.
Here is possible solution. Tested with Xcode 11.4.
struct TestOnHoverInList : View {
#State var hovered: (Int, Int) = (-1, -1)
var body: some View {
VStack(spacing: 90) {
ForEach(0..<2) {i in
HStack(spacing: 90) {
ForEach(0..<4) {j in
Circle().fill(Color.red).frame(width: 50, height: 50)
.scaleEffect(self.hovered == (i,j) ? 2.0 : 1.0)
.animation(.default)
.onHover { hover in
print("Mouse hover: \(hover)")
if hover {
self.hovered = (i, j) // << here !!
} else {
self.hovered = (-1, -1) // reset
}
}
}
}
}
}
.frame(minWidth:300,maxWidth:.infinity,minHeight:300,maxHeight:.infinity)
}
}
Every item currently gets animated because they are all relying on hovered to see if the Circle is hovered over. To fix that, we can make every circle have their own hovered state.
struct CircleView: View {
#State var hovered = false
var body: some View {
Circle().fill(Color.red).frame(width: 50, height: 50)
.scaleEffect(self.hovered ? 2.0 : 1.0)
.animation(.default)
.onHover { hover in
print("Mouse hover: \(hover)")
self.hovered.toggle()
}
}
}
and in the ForEach we can just call the new CircleView where every Circle has their own source of truth.
struct ContentView : View {
var body: some View {
VStack(spacing: 90) {
ForEach(0..<2) { _ in
HStack(spacing: 90) {
ForEach(0..<4) { _ in
CircleView()
}
}
}
}
.frame(minWidth:300,maxWidth:.infinity,minHeight:300,maxHeight:.infinity)
}
}
Alternatively, you can create a modifier that allows you to change the View in question when it's hovered:
extension View {
func onHover<Content: View>(#ViewBuilder _ modify: #escaping (Self) -> Content) -> some View {
modifier(HoverModifier { modify(self) })
}
}
private struct HoverModifier<Result: View>: ViewModifier {
#ViewBuilder let modifier: () -> Result
#State private var isHovering = false
func body(content: Content) -> AnyView {
(isHovering ? modifier().eraseToAnyView() : content.eraseToAnyView())
.onHover { self.isHovering = $0 }
.eraseToAnyView()
}
}
Then each Circle on your example would go something like:
Circle().fill(Color.red).frame(width: 50, height: 50)
.animation(.default)
.onHover { view in
_ = print("Mouse hover")
view.scaleEffect(2.0)
}