Swipe to delete function - swift

using my code below how can I swipe right to left on a Task to have an option to be able to delete it.
I have tried using .SwipeAction but have gotten no luck with using that and am stuck with what to use
Here is my view, I am using core data
import CoreData
import SwiftUI
struct TaskView: View {
#FetchRequest(sortDescriptors: []) var tasks: FetchedResults<Tasks>
#State private var isPresented = false
#EnvironmentObject var taskModel: TaskViewModel
#Environment(\.managedObjectContext) var moc
#State var TaskStatus: [String] = ["Todays Tasks", "Upcomming", "Completed"]
#Namespace var animation
var body: some View {
ScrollView{
VStack{
HStack{
Spacer()
Text(TaskStatus[0])
.font(.system(size: 30))
.fontWeight(.semibold)
Button {
print("")
} label: {
Text("May 2022")
.foregroundColor(.black)
.padding(.horizontal)
.font(.system(size: 25))
}
Spacer()
}
.padding()
.padding(.top)
customBar()
.padding()
ForEach(tasks,id: \.self){item in
Task(name: item.title ?? "Task Name", desc: item.desc ?? "Task Desc", image: item.icon ?? "sportscourt", image1: "clock", Time: "10", Colour: item.colour ?? "70d6ff", deadline: item.deadline ?? Date())
}
}
}
.overlay(alignment: .bottom){
HStack{
Spacer()
Button {
isPresented.toggle()
} label: {
Image(systemName: "plus.square")
.font(.system(size: 50))
.foregroundColor(.black)
}
.fullScreenCover(isPresented: $isPresented, content: AddTask.init)
.padding()
}
.padding(.horizontal)
}
}
func deleteTask(at offsets: IndexSet) {
for offset in offsets {
let tasky = tasks[offset]
moc.delete(tasky)
}
try?
moc.save()
}
#ViewBuilder
func customBar()-> some View{
let tabs = ["Today", "Upcoming"," Completed"]
HStack(spacing:10){
ForEach(tabs,id: \.self){tab in
Text(tab)
.font(.system(size: 20))
.scaleEffect(0.9)
.foregroundColor(taskModel.currentTab == tab ? .white:.black)
.padding(.vertical,6)
.frame(maxWidth:.infinity)
.background{
if taskModel.currentTab == tab{
Capsule()
.fill(Color(hex: "ff006e"))
.matchedGeometryEffect(id: "TAB", in: animation)
}
}
.contentShape(Capsule())
.onTapGesture{
withAnimation{taskModel.currentTab = tab}
}
}
}
}
}
struct TaskView_Previews: PreviewProvider {
static var previews: some View {
TaskView()
.previewDevice(PreviewDevice(rawValue: "iPhone 13"))
TaskView()
.previewDevice(PreviewDevice(rawValue: "iPhone 8"))
}
}
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
I would like someone to be able to point me in the right direction of what I need to do please.
Many Thanks

Here comes my version of a custom SwipeAction for Non-Lists:
It's close to the original, only can't cancel on taps outside of the container. Any improvements or suggestions are welcome.
Usage:
ItemView(item)
.mySwipeAction { // red + trash icon as default
deleteMyItem(item)
}
ItemView(item)
.mySwipeAction(color: .green, icon: "flag" ) { // custom color + icon
selectMyItem(item)
}
Example:
Code:
extension View {
func mySwipeAction(color: Color = .red,
icon: String = "trash",
action: #escaping () -> ()) -> some View {
return self.modifier(MySwipeModifier(color: .red, icon: "trash", action: action ))
}
}
struct MySwipeModifier: ViewModifier {
let color: Color
let icon: String
let action: () -> ()
#AppStorage("MySwipeActive") var mySwipeActive = false
#State private var contentWidth: CGFloat = 0
#State private var isDragging: Bool = false
#State private var isDeleting: Bool = false
#State private var isActive: Bool = false
#State private var dragX: CGFloat = 0
#State private var iconOffset: CGFloat = 40
let miniumDistance: CGFloat = 20
func body(content: Content) -> some View {
ZStack(alignment: .trailing) {
content
.overlay( GeometryReader { geo in Color.clear.onAppear { contentWidth = geo.size.width }})
.offset(x: -dragX)
Group {
color
Image(systemName: icon)
.symbolVariant(.fill)
.foregroundColor(.white)
.offset(x: isDeleting ? 40 - dragX/2 : iconOffset)
}
.frame(width: max(dragX, 0))
// tap on red area after being active > action
.onTapGesture {
withAnimation { action() }
}
}
.contentShape(Rectangle())
// tap somewhere else > deactivate
.onTapGesture {
withAnimation {
isActive = false
dragX = 0
iconOffset = 40
mySwipeActive = false
}
}
.gesture(DragGesture(minimumDistance: miniumDistance)
.onChanged { value in
// if dragging started new > reset dragging state for all (others)
if !isDragging && !isActive {
mySwipeActive = false
isDragging = true
}
if value.translation.width < 0 {
dragX = -min(value.translation.width + miniumDistance, 0)
} else if isActive {
dragX = max(80 - value.translation.width + miniumDistance, -30)
}
iconOffset = dragX > 80 ? -40+dragX/2 : 40-dragX/2
withAnimation(.easeOut(duration: 0.3)) { isDeleting = dragX > contentWidth*0.75 }
// full drag > action
if value.translation.width <= -contentWidth {
withAnimation { action() }
mySwipeActive = false
isDragging = false
isActive = false
return
}
}
.onEnded { value in
withAnimation(.easeOut) {
isDragging = false
// half drag > change to active / show icon
if value.translation.width < -60 && !isActive {
isActive = true
mySwipeActive = true
} else {
isActive = false
mySwipeActive = false
}
// in delete mode > action
if isDeleting { action() ; return }
// in active mode > show icon
if isActive {
dragX = 80
iconOffset = 0
return
}
dragX = 0
isDeleting = false
}
}
)
// reset all if swipe in other cell
.onChange(of: mySwipeActive) { newValue in
print("changed", newValue)
if newValue == false && !isDragging {
withAnimation {
dragX = 0
isActive = false
isDeleting = false
iconOffset = 40
}
}
}
}
}

Related

Changing translation of a DragGesture swift

I am working with a slider and dealing with translation. Once the slider performs an action, I mock an api call and then on completion the image is unlocked but now I would like to return the slider to the original position once the api call is completed. Here is what the slider looks like in code.
struct DraggingComponent: View {
#Binding var isLocked: Bool
#Binding var isLoading: Bool
let maxWidth: CGFloat
#State private var width = CGFloat(50)
private let minWidth = CGFloat(50)
init(isLocked: Binding<Bool>, isLoading: Binding<Bool>, maxWidth: CGFloat) {
_isLocked = isLocked
self._isLoading = isLoading
self.maxWidth = maxWidth
}
var body: some View {
RoundedRectangle(cornerRadius: 16)
.fill(Color.black)
.opacity(width / maxWidth)
.frame(width: width)
.overlay(
Button(action: { }) {
ZStack {
image(name: "lock", isShown: isLocked)
progressView(isShown: isLoading)
image(name: "lock.open", isShown: !isLocked && !isLoading)
}
.animation(.easeIn(duration: 0.35).delay(0.55), value: !isLocked && !isLoading)
}
.buttonStyle(BaseButtonStyle())
.disabled(!isLocked || isLoading),
alignment: .trailing
)
.simultaneousGesture(
DragGesture()
.onChanged { value in
guard isLocked else { return }
if value.translation.width > 0 {
width = min(max(value.translation.width + minWidth, minWidth), maxWidth)
}
}
.onEnded { value in
guard isLocked else { return }
if width < maxWidth {
width = minWidth
UINotificationFeedbackGenerator().notificationOccurred(.warning)
} else {
UINotificationFeedbackGenerator().notificationOccurred(.success)
withAnimation(.spring().delay(0.5)) {
isLocked = false
}
}
}
)
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 0), value: width)
}
private func image(name: String, isShown: Bool) -> some View {
Image(systemName: name)
.font(.system(size: 20, weight: .regular, design: .rounded))
.foregroundColor(Color.black)
.frame(width: 42, height: 42)
.background(RoundedRectangle(cornerRadius: 14).fill(.white))
.padding(4)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
private func progressView(isShown: Bool) -> some View {
ProgressView()
.progressViewStyle(.circular)
.tint(.white)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
}
Where it is used:
#State private var isLocked = true
#State private var isLoading = false
GeometryReader { geometry in
ZStack(alignment: .leading) {
BackgroundComponent()
DraggingComponent(isLocked: $isLocked, isLoading: $isLoading, maxWidth: geometry.size.width)
}
}
.frame(height: 50)
.padding()
.padding(.bottom, 20)
.onChange(of: isLocked) { isLocked in
guard !isLocked else { return }
simulateRequest()
}
private func simulateRequest() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
isLoading = false
}
}
How can I get the translation back to the initial position.
Pull your width #State up into the containing view and pass it on as #Binding. After simulateRequest set it to its initial state.
In your DraggingComponent use
struct DraggingComponent: View {
#Binding var width: CGFloat
.....
in the View that contains DraggingComponent:
#State private var width = CGFloat(50)
and:
.onChange(of: isLocked) { isLocked in
guard !isLocked else { return }
simulateRequest()
self.width = CGFloat(50) //reset it here probably with animation
}

How to dynamically change GridItems in LazyVGrid with MagnificationGesture [Zoom In, Out] in SwiftUI?

The idea is to recreate the same photo layout behaviour like in Apple Photo Library when I can zoom in and out with 1, 3 or 5 photos in a row. I'm stack in a half way. For that I use a MagnificationGesture() and based on gesture value I update number of GridItems() in LazyVGrid().
Please let me know how to achieve it. Thanks a lot ๐Ÿ™
Here's code:
import SwiftUI
struct ContentView: View {
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
#State private var colums = Array(repeating: GridItem(), count: 1)
// #GestureState var magnifyBy: CGFloat = 1.0
#State var magnifyBy: CGFloat = 1.0
#State var lastMagnifyBy: CGFloat = 1.0
let minMagnifyBy = 1.0
let maxMagnifyBy = 5.0
var magnification: some Gesture {
MagnificationGesture()
// .updating($magnifyBy) { (currentState, pastState, trans) in
// pastState = currentState.magnitude
// }
.onChanged { state in
adjustMagnification(from: state)
print("Current State \(state)")
}
.onEnded { state in
adjustMagnification(from: state)
// withAnimation(.spring()) {
// validateMagnificationLimits()
// }
lastMagnifyBy = 1.0
}
}
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: colums) {
ForEach(1..<101) { number in
colors[number % colors.count]
.overlay(Text("\(number)").font(.title2.bold()).foregroundColor(.white))
.frame(height: 100)
}
}
.scaleEffect(magnifyBy)
.gesture(magnification)
.navigationTitle("๐Ÿงจ Grid")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
withAnimation(.spring(response: 0.8)) {
colums = Array(repeating: .init(), count: colums.count == 5 ? 1 : colums.count % 5 + 2)
}
} label: {
Image(systemName: "square.grid.3x3")
.font(.title2)
.foregroundColor(.primary)
}
}
}
}
}
}
private func adjustMagnification(from state: MagnificationGesture.Value) {
let stepCount = Int(min(max(1, state), 5))
// let delta = state / lastMagnifyBy
// magnifyBy *= delta
withAnimation(.linear) {
colums = Array(repeating: GridItem(), count: stepCount)
}
lastMagnifyBy = state
}
private func getMinMagnificationAllowed() -> CGFloat {
max(magnifyBy, minMagnifyBy)
}
private func getMaxMagnificationAllowed() -> CGFloat {
min(magnifyBy, maxMagnifyBy)
}
private func validateMagnificationLimits() {
magnifyBy = getMinMagnificationAllowed()
magnifyBy = getMaxMagnificationAllowed()
}
}
Here you go. This uses a TrackableScrollView (git link in the code).
I implemented an array of possible zoomStages (cols per row), to make switching between them easier.
Next to dos would be scrolling back to the magnification center, so the same item stays in focus. And maybe an opacity transition in stead of rearranging the Grid. Have fun ;)
import SwiftUI
// https://github.com/maxnatchanon/trackable-scroll-view.git
import SwiftUITrackableScrollView
struct ContentView: View {
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
let zoomStages = [1, 3, 5, 9, 15]
#State private var zoomStageIndex = 0
var colums: [GridItem] { Array(repeating: GridItem(spacing: 0), count: zoomStages[zoomStageIndex]) }
#State var magnifyBy: CGFloat = 1.0
#State private var scrollViewOffset = CGFloat.zero // SwiftUITrackableScrollView: Content offset available to use
var body: some View {
NavigationView {
TrackableScrollView(.vertical, showIndicators: false, contentOffset: $scrollViewOffset) {
LazyVGrid(columns: colums, spacing: 0) {
ForEach(0..<500) { number in
colors[number % colors.count]
.overlay(
Text("\(number)").font(.title2.bold()).foregroundColor(.white)
.minimumScaleFactor(0.1)
)
.aspectRatio(1, contentMode: .fit) // always squares
.id(number)
}
}
.scaleEffect(magnifyBy, anchor: .top)
// offset to correct magnify "center" point
.offset(x: 0, y: (scrollViewOffset + UIScreen.main.bounds.midY) * (1 - magnifyBy) )
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
withAnimation(.spring(response: 0.8)) {
if zoomStageIndex < zoomStages.count-1 {
zoomStageIndex += 1
} else {
zoomStageIndex = 0
}
}
} label: {
Image(systemName: "square.grid.3x3")
.font(.title2)
.foregroundColor(.primary)
}
}
}
.gesture(magnification)
}
.ignoresSafeArea()
}
}
var magnification: some Gesture {
MagnificationGesture()
.onChanged { state in
magnifyBy = state
}
.onEnded { state in
// find predefined zoom(index) that is closest to actual pinch zoom value
let newZoom = Double(zoomStages[zoomStageIndex]) * 1 / state
let newZoomIndex = findClosestZoomIndex(value: newZoom)
// print("***", zoomStages[zoomStageIndex], state, newZoom, newZoomIndex)
withAnimation(.spring(response: 0.8)) {
magnifyBy = 1 // reset scaleEffect
zoomStageIndex = newZoomIndex // set new zoom level
}
}
}
func findClosestZoomIndex(value: Double) -> Int {
let distanceArray = zoomStages.map { abs(Double($0) - value) } // absolute difference between zoom stages and actual pinch zoom
// print("dist:", distanceArray)
return distanceArray.indices.min(by: {distanceArray[$0] < distanceArray[$1]}) ?? 0 // return index of element that is "closest"
}
}

#State Property not accumulating

Users can swipe left or right on a stack of cards to simulate "true" or "false" to a series of quiz questions. I have a #State var called userScore initialized to 0. When dragGesture .onEnded, I compare the "correctAnswer" with the "userAnswer." If they match, add 1 point to the userScore.
The problem: The console prints 1 or 0. The score does not ACCUMULATE.
Please help me calculate the user's final score? Thanks in advance. . .
import SwiftUI
struct CardView: View {
#State var offset: CGSize = .zero
#State var userScore: Int = 0
#State var userAnswer: Bool = false
private var currentQuestion: Question
private var onRemove: (_ user: Question) -> Void
init(user: Question, onRemove: #escaping (_ user: Question) -> Void) {
self.currentQuestion = user
self.onRemove = onRemove
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.fill(
Color.black
.opacity(1 - Double(abs(offset.width / 50)))
)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.fill(offset.width > 0 ? Color.green : Color.red)
//.overlay(offset.width > 0 ? likeGraphics() : dislikeGraphics(), alignment: .topLeading)
)
.shadow(color: .red, radius: 5, x: 0.0, y: 0.0)
VStack {
Image(currentQuestion.imageIcon)
.resizable()
.scaledToFit()
.foregroundColor(Color.white)
.frame(width: 75, height: 75)
ScrollView {
Text(currentQuestion.questionText)
.font(.largeTitle)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
}
}
.padding()
}
.frame(width: 300, height: 375) //check: www.ios-resolution.com
.offset(offset)
.rotationEffect(.degrees(offset.width / 400.0 ) * 15, anchor: .bottom)
//.opacity(2 - Double(abs(offset.width / 50.0))) //fade on drag too?
.gesture(
DragGesture()
.onChanged { gesture in
withAnimation(.spring()) {
offset = gesture.translation
}
}
.onEnded { value in
withAnimation(.interactiveSpring()) {
if abs(offset.width) > 100 {
onRemove(currentQuestion)
if (currentQuestion.correctAnswer == determineSwipeStatus()) {
userScore += 1
}
print(userScore)
} else {
offset = .zero
}
}
}
)
}
func determineSwipeStatus() -> Bool {
if(offset.width > 0) {
userAnswer = true
} else if (offset.width < 0) {
userAnswer = false
}
return userAnswer
}
func getAlert() -> Alert {
return Alert(
title: Text("Your group is \(userScore)% cult-like!"),
message: Text("Would you like to test another group?"),
primaryButton: .default(Text("Yes, test another group"),
action: {
//transition back to screen one
//self.showQuizPageScreen.toggle()
}),
secondaryButton: .cancel())
}
}
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView(user: Question(id: 1,
imageIcon: "icon1",
questionText: "Dummy placeholder text",
correctAnswer: true),
onRemove: { _ in
//leave blank for preview purposes
})
}
}

State variable not update in SwiftUI

I have struct DBScrollViewCellWrapper. that display contain
struct DBScrollViewCellWrapper: View, Identifiable, Equatable {
let id = UUID().uuidString
let view: AnyView
#State var showSelectionLine: Bool = false
var body: some View {
VStack(spacing: 0){
view
if self.showSelectionLine{
Rectangle()
.frame(width: 10, height: 1)
.foregroundColor(.red)
}
}
}
static func == (lhs: DBScrollViewCellWrapper, rhs: DBScrollViewCellWrapper) -> Bool { lhs.id == rhs.id }
}
then generate number of DBScrollViewCellWrapper cell. when tap on cell, display tapped cell selected with line.
struct DBScrollView: View {
let views: [DBScrollViewCellWrapper]
var showsIndicators = false
var completion:(DBScrollViewCellWrapper,Int)->Void = {x,index in}
var isHorizontal: Bool = false
var leadingSpacing: CGFloat = 0
var trailingSpacing: CGFloat = 0
var itemSpacing: CGFloat = 5
var isFixSize: Bool = false
var fixWidth: CGFloat = .infinity
var fixHeight: CGFloat = .infinity
#State var showSelectionLine: Bool = false
#State private var previousItem : DBScrollViewCellWrapper?
init(views: [DBScrollViewCellWrapper],
showsIndicators: Bool = false,
isHorizontal: Bool = false,
leadingSpacing: CGFloat = 0,
trailingSpacing: CGFloat = 0,
itemSpacing: CGFloat = 5,
isFixSize: Bool = false,
fixWidth: CGFloat = .infinity,
fixHeight: CGFloat = .infinity,
completion: #escaping (DBScrollViewCellWrapper,Int)->Void = {val,index in}) {
self.views = views.map { $0 } //DBScrollViewCellWrapper(view: $0)
self.showsIndicators = showsIndicators
self.completion = completion
self.isHorizontal = isHorizontal
self.leadingSpacing = leadingSpacing
self.trailingSpacing = trailingSpacing
self.itemSpacing = itemSpacing
self.isFixSize = isFixSize
self.fixWidth = fixWidth
self.fixHeight = fixHeight
}
var body: some View {
GeometryReader(content: { geometry in
ScrollView(isHorizontal ? .horizontal : .vertical, showsIndicators: showsIndicators, content: {
self.generateViews(in: geometry)
})
.padding(.leading, self.leadingSpacing)
.padding(.trailing, self.trailingSpacing)
})
}
private func generateViews(in geometry: GeometryProxy) -> some View{
return ZStack{
if isHorizontal{
HStack(spacing: itemSpacing){
ForEach(self.views) { item in
item
.padding(5)
.border(Color.black)
.onTapGesture(count: 1, perform: {
self.tapped(value: item)
})
}
Spacer()
}
}else{
VStack(spacing: itemSpacing){
ForEach(self.views, id: \.id) { item in
item
.padding(5)
.border(Color.clear)
.onTapGesture(count: 1, perform: {
self.tapped(value: item)
})
}
Spacer()
}
}
}
}
func tapped(value: DBScrollViewCellWrapper) {
guard let index = views.firstIndex(of: value) else { assert(false, "This should never happen"); return }
value.showSelectionLine = true
completion(value,index)
}
}
Preview Code:
struct DBScrollView_Previews: PreviewProvider {
static var previews: some View {
let arr = Array(0...100)
let arrView = arr.map{DBScrollViewCellWrapper(view: AnyView(Text("\($0)")))}
DBScrollView(views: arrView, isHorizontal: false) { (cell, inx) in
cell.showSelectionLine = true
}
}
}
Problem
when tapped on cell, changed the value of cell but that not update.
Redesigned code for selection
struct DBScrollView: View {
private let views: [DBScrollViewCellWrapper]
var showsIndicators = false
var completion:(DBScrollViewCellWrapper,Int)->Void = {x,index in}
var isHorizontal: Bool = false
var leadingSpacing: CGFloat = 0
var trailingSpacing: CGFloat = 0
var itemSpacing: CGFloat = 5
var isFixSize: Bool = false
var fixWidth: CGFloat = .infinity
var fixHeight: CGFloat = .infinity
#State var selectedIndex: Int = -1
#State var showSelectionLine: Bool = false
#State private var previousItem : DBScrollViewCellWrapper?
init(views: [AnyView],
showsIndicators: Bool = false,
isHorizontal: Bool = false,
leadingSpacing: CGFloat = 0,
trailingSpacing: CGFloat = 0,
itemSpacing: CGFloat = 5,
isFixSize: Bool = false,
fixWidth: CGFloat = .infinity,
fixHeight: CGFloat = .infinity,
completion: #escaping (DBScrollViewCellWrapper,Int)->Void = {val,index in}) {
self.views = views.map { DBScrollViewCellWrapper(view: AnyView($0))}
self.showsIndicators = showsIndicators
self.completion = completion
self.isHorizontal = isHorizontal
self.leadingSpacing = leadingSpacing
self.trailingSpacing = trailingSpacing
self.itemSpacing = itemSpacing
self.isFixSize = isFixSize
self.fixWidth = fixWidth
self.fixHeight = fixHeight
}
var body: some View {
GeometryReader(content: { geometry in
ScrollView(isHorizontal ? .horizontal : .vertical, showsIndicators: showsIndicators, content: {
self.generateViews(in: geometry)
})
.padding(.leading, self.leadingSpacing)
.padding(.trailing, self.trailingSpacing)
})
}
private func generateViews(in geometry: GeometryProxy) -> some View{
return ZStack{
if isHorizontal{
HStack(alignment: .center, spacing: itemSpacing){
ForEach(self.views.indices, id:\.self) { index in
let item = self.views[index]
VStack(spacing: 0){
item.view
.foregroundColor(self.selectedIndex == index ? Color.yellow : Color.white)
.padding(5)
.onTapGesture(count: 1, perform: {
self.selectedIndex = index
self.tapped(value: item)
})
Rectangle()
.frame(width: self.selectedIndex == index ? 10 : 0, height: 1, alignment: .center)
.foregroundColor(Color.yellow)
}
}
Spacer()
}
}else{
VStack(spacing: itemSpacing){
ForEach(self.views.indices, id: \.self) { index in
let item = self.views[index]
VStack(spacing: 0){
item.view
.padding(5)
.border(Color.white)
.onTapGesture(count: 1, perform: {
self.selectedIndex = index
self.tapped(value: item)
})
Rectangle()
.frame(width: self.selectedIndex == index ? 10 : 0, height: 1, alignment: .center)
.foregroundColor(Color.yellow)
}
}
Spacer()
}
}
}
}
func tapped(value: DBScrollViewCellWrapper) {
guard let index = views.firstIndex(of: value) else { assert(false, "This should never happen"); return }
completion(value,index)
}
}
struct DBScrollViewCellWrapper: Identifiable, Equatable {
let id = UUID().uuidString
let view: AnyView
static func == (lhs: DBScrollViewCellWrapper, rhs: DBScrollViewCellWrapper) -> Bool { lhs.id == rhs.id } } struct DBScrollView_Previews: PreviewProvider {
static var previews: some View {
let arr = Array(0...100)
let arrView = arr.map{AnyView(Text("\($0)"))}
DBScrollView(views: arrView, isHorizontal: true) { (cell, inx) in
}
.background(Color.black)
}
}

in SwiftUI, in a ForEach(0 ..< 3), animate the tapped button only (not all 3), animate the other 2 differently

After a full day trying to animate these buttons I give up and ask for help.
I would like to animate the correct button only like this:
.rotation3DEffect(.degrees(self.animationAmount), axis: (x: 0, y: 1, z: 0))
and at the same time make the other two buttons fade out to 25% opacity.
When the player clicks the wrong button I would like to
animate the wrong button like this:
.rotation3DEffect(.degrees(self.animationAmount), axis: (x: 1, y: 1, z: 1)) (or anyway else you can think to indicate disaster) and leave the other two alone.
After that happened I would like the alert to show.
Below is my code. I commented what I would like to do and where if at all possible.
It all works like I want but cannot get the animation going.
Thanks for your help in advance
import SwiftUI
struct ContentView: View {
#State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"]
#State private var correctAnswer = Int.random(in: 0...2)
#State private var showingScore = false
#State private var scoreTitle = ""
#State private var userScore = 0
#State private var userTapped = ""
#State private var animationAmount = 0.0
var body: some View {
ZStack {
LinearGradient(gradient: Gradient(colors: [.blue, .black]), startPoint: .top, endPoint: .bottom)
.edgesIgnoringSafeArea(.all)
VStack(spacing: 20) {
VStack {
Text("Tap the flag of...")
.foregroundColor(.white).font(.title)
Text(countries[correctAnswer])
.foregroundColor(.white)
.font(.largeTitle).fontWeight(.black)
}
ForEach(0 ..< 3) { number in
Button(action: {
self.flagTapped(number)
if self.correctAnswer == number {
//animate the correct button only like this:
//.rotation3DEffect(.degrees(self.animationAmount), axis: (x: 0, y: 1, z: 0))
// and
// make the other two buttons fade out to 25% opacity
} else {
// animate the wrong button like this:
//.rotation3DEffect(.degrees(self.animationAmount), axis: (x: 1, y: 1, z: 1))
}
}) {
Image(self.countries[number])
.renderingMode(.original)
.clipShape(Capsule())
.overlay(Capsule().stroke(Color .black, lineWidth: 1))
.shadow(color: .black, radius: 2)
}
}
Text ("your score is:\n \(userScore)").foregroundColor(.white).font(.title).multilineTextAlignment(.center)
}
}
.alert(isPresented: $showingScore) {
Alert(title: Text(scoreTitle), message: Text("You chose the flag of \(userTapped)\nYour score is now: \(userScore)"), dismissButton: .default(Text("Continue")) {
self.askQuestion()
})
}
}
func flagTapped(_ number: Int) {
userTapped = countries[number]
if number == correctAnswer {
scoreTitle = "Correct"
userScore += 1
} else {
scoreTitle = "Wrong"
userScore -= 1
}
showingScore = true
}
func askQuestion() {
countries.shuffle()
correctAnswer = Int.random(in: 0...2)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I had a problem similar to yours when solving this challenge. I figured out a solution without using DispatchQueue.main.asyncAfter. I made the following as the final solution:
Spin around 360 degrees the correct chosen flag and fade out the other flags to 25 % of opacity.
Blur the background of the wrong flag chosen with red and fade out the other flags to 25 % of opacity.
Here is the full solution (I comment on the parts that were important to achieve the above solution):
import SwiftUI
// Create a custom view
struct FlagImage: View {
var countryFlags: String
var body: some View {
Image(countryFlags)
.renderingMode(.original)
.clipShape(Capsule())
.overlay(Capsule().stroke(Color.black, lineWidth: 1))
.shadow(color: .black, radius: 2)
}
}
struct ContentView: View {
#State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Russia", "Spain", "UK", "US"].shuffled()
#State private var correctAnswer = Int.random(in: 0...3)
#State private var showingScore = false
#State private var scoreTitle = ""
#State private var userScore = 0
// Properties for animating the chosen flag
#State private var animateCorrect = 0.0
#State private var animateOpacity = 1.0
#State private var besidesTheCorrect = false
#State private var besidesTheWrong = false
#State private var selectedFlag = 0
var body: some View {
ZStack {
LinearGradient(gradient: Gradient(colors: [.blue, .black]), startPoint: .top, endPoint: .bottom).edgesIgnoringSafeArea(.all)
VStack(spacing: 30) {
VStack {
Text("Tap on the flag!")
.foregroundColor(.white)
.font(.title)
Text(countries[correctAnswer])
.foregroundColor(.white)
.font(.largeTitle)
.fontWeight(.black)
}
ForEach(0 ..< 4) { number in
Button(action: {
self.selectedFlag = number
self.flagTapped(number)
}) {
FlagImage(countryFlags: self.countries[number])
}
// Animate the flag when the user tap the correct one:
// Rotate the correct flag
.rotation3DEffect(.degrees(number == self.correctAnswer ? self.animateCorrect : 0), axis: (x: 0, y: 1, z: 0))
// Reduce opacity of the other flags to 25%
.opacity(number != self.correctAnswer && self.besidesTheCorrect ? self.animateOpacity : 1)
// Animate the flag when the user tap the wrong one:
// Create a red background to the wrong flag
.background(self.besidesTheWrong && self.selectedFlag == number ? Capsule(style: .circular).fill(Color.red).blur(radius: 30) : Capsule(style: .circular).fill(Color.clear).blur(radius: 0))
// Reduce opacity of the other flags to 25% (including the correct one)
.opacity(self.besidesTheWrong && self.selectedFlag != number ? self.animateOpacity : 1)
}
Spacer()
Text("Your total score is: \(userScore)")
.foregroundColor(Color.white)
.font(.title)
.fontWeight(.black)
Spacer()
}
}
.alert(isPresented: $showingScore) {
Alert(title: Text(scoreTitle), dismissButton: .default(Text("Continue")) {
self.askQuestion()
})
}
}
func flagTapped(_ number: Int) {
if number == correctAnswer {
scoreTitle = "Correct!"
userScore += 1
// Create animation for the correct answer
withAnimation {
self.animateCorrect += 360
self.animateOpacity = 0.25
self.besidesTheCorrect = true
}
} else {
scoreTitle = "Wrong!"
// Create animation for the wrong answer
withAnimation {
self.animateOpacity = 0.25
self.besidesTheWrong = true
}
}
showingScore = true
}
func askQuestion() {
// Return the booleans to false
besidesTheCorrect = false
besidesTheWrong = false
countries = countries.shuffled()
correctAnswer = Int.random(in: 0...3)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Basically, my solution is to add ternary operators inside the view modifiers (e.g., .rotation3DEffect(...), .opacity(...) and .background(...)) after the Button view. The tricky part is to correctly combine the checking condition.
I prefer to add the withAnimation modifier to my flagTapped function. In this place I have more control of the animations if the user select a correct or a wrong flag.
I made a small change to the original challenge: just add one more flag to the view.
The final result when a user presses the correct and wrong flag is here:
.
I had the same problem. Here is my solution (only the code that's different from yours):
The ForEach:
ForEach(0 ..< 3, id: \.self){ number in
Button(action: {
withAnimation{
self.tappedFlag = number
self.flagTapped(number)
}
}){
FlagImage(imageName: self.countries[number])
}
.rotation3DEffect(.degrees(self.isCorrect && self.selcectedNumber == number ? 360 : 0), axis: (x: 0, y: 1, z: 0))
.opacity(self.isFadeOutOpacity && self.selcectedNumber != number ? 0.25 : 1)
.rotation3DEffect(.degrees(self.wrongAnswer && number != self.correctAnswer ? 180 : 0), axis: (x: 1, y: 0, z: 0))
.opacity(self.wrongAnswer && number != self.correctAnswer ? 0.25 : 1)
}
The alert:
.alert(isPresented: $showingScore){
if scoreTitle == "Correct"{
return Alert(title: Text(scoreTitle), message: Text("Your score is \(userScore)"), dismissButton: .default(Text("Continue")){
self.askQuestion()
})
}else{
return Alert(title: Text(scoreTitle), message: Text("That is the flag of \(countries[tappedFlag]), you lost one point!"), dismissButton: .default(Text("Continue")){
self.askQuestion()
})
}
}
The two functions:
func flagTapped(_ number: Int){
self.selcectedNumber = number
self.alreadyTapped = true
if number == correctAnswer{
scoreTitle = "Correct"
userScore += 1
self.isCorrect = true
self.isFadeOutOpacity = true
}else{
self.wrongAnswer = true
scoreTitle = "Wrong"
if userScore != 0{
userScore -= 1
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.showingScore = true
}
}
func askQuestion(){
countries = countries.shuffled()
correctAnswer = Int.random(in: 0...2)
self.isCorrect = false
self.isFadeOutOpacity = false
self.wrongAnswer = false
}
You have to declare some new variables :)
I hope I could help.
PS: There is a playlist on Youtube for 100DaysOfSwiftUI with the solutions to almost every own task.
https://www.youtube.com/watch?v=9AUGceRIUSA&list=PL3pUvT0fmHNhb3qcpvuym6KeM12eQK3T1