SwiftUI-How to formate seconds into Minutes:Seconds? - swift

I am trying to make a timer in SwiftUI and everything works well so far but I can't figure out how to format the seconds into seconds and minutes. So for example if the countdown was 299 seconds, I want it to show 4(mins):59(secs) instead of just 300 seconds.
I saw a video on youtube making a timer too and they managed to format it via making minutes and seconds constants then format it via a function but it didn't really work for me.
import SwiftUI
struct TimerView: View {
var timerEnd = 0
#State var countdownTimer = 300
#State var timerRunning = false
#State var isPaused = false
#State var isActive = false
#State var showingAlert = false
let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
func format(result: Int) -> String {
let value = String(format:"%d:%02\(300/60)", countdownTimer)
return value
}
func reset() {
countdownTimer = 300
timerRunning = true
}
var body: some View {
VStack {
Text("Time: \(format(result: countdownTimer))")
.padding()
.onReceive(timer) { _ in
if countdownTimer > 0 && timerRunning {
countdownTimer -= 1
} else {
timerRunning = false
}
}
.font(.system(size: 30))
HStack(spacing:30) {
Button(timerRunning ? "Reset" : "Start") {
reset()
}
.padding()
.background((Color(red: 184/255, green: 243/255, blue: 255/255)))
.foregroundColor(.black)
.cornerRadius(10)
.font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
Button(timerRunning ? "Pause" : "Resume") {
if timerRunning == true {
timerRunning = false
} else {
timerRunning = true
}
}
.padding()
.foregroundColor(.black)
.background(.red)
.cornerRadius(10)
.font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
}
}
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
TimerView()
}
}

You can do with an extension class like this. After that code you will see a method in your int value.
You can use countdownTimer.convertDurationToString()
extension Int {
public func hmsFrom() -> (Int, Int, Int) {
return (self / 3600, (self % 3600) / 60, (self % 3600) % 60)
}
public func convertDurationToString() -> String {
var duration = ""
let (hour, minute, second) = self.hmsFrom()
if (hour > 0) {
duration = self.getHour(hour: hour)
}
return "\(duration)\(self.getMinute(minute: minute))\(self.getSecond(second: second))"
}
private func getHour(hour: Int) -> String {
var duration = "\(hour):"
if (hour < 10) {
duration = "0\(hour):"
}
return duration
}
private func getMinute(minute: Int) -> String {
if (minute == 0) {
return "00:"
}
if (minute < 10) {
return "0\(minute):"
}
return "\(minute):"
}
private func getSecond(second: Int) -> String {
if (second == 0){
return "00"
}
if (second < 10) {
return "0\(second)"
}
return "\(second)"
}
}

Related

SwiftUI- Timer auto adds 30 minutes to countdown

I'm trying to make a timer in swiftui but whenever I run it, it auto adds 30 minutes to the countdown. For example, when I set the countdown time to 5 minutes and click the "Start" button, it will show up as 35 minutes instead but when I click the button again, it will then just keep switching to random times. Above is the random times it will switch to.
I got this timer from a tutorial on youtube by Indently but changed some things to fit what I wanted it to do. I tried to set a custom time so the timer will always countdown from 5 minutes. From my understanding, the timer works by taking the difference between the current date and the end date then using the amount of time difference as the countdown. Below is the code for the TimerStruct (ViewModel) and the TimerView.
TimerStruct:
import Foundation
extension TimerView {
final class ViewModel: ObservableObject {
#Published var isActive = false
#Published var showingAlert = false
#Published var time: String = "5:00"
#Published var minutes: Float = 5.0 {
didSet {
self.time = "\(Int(minutes)):00"
}
}
var initialTime = 0
var endDate = Date()
// Start the timer with the given amount of minutes
func start(minutes: Float) {
self.initialTime = 5
self.endDate = Date()
self.isActive = true
self.endDate = Calendar.current.date(byAdding: .minute, value: Int(minutes), to: endDate)!
}
// Reset the timer
func reset() {
self.minutes = Float(initialTime)
self.isActive = false
self.time = "\(Int(minutes)):00"
}
// Show updates of the timer
func updateCountdown(){
guard isActive else { return }
// Gets the current date and makes the time difference calculation
let now = Date()
let diff = endDate.timeIntervalSince1970 - now.timeIntervalSince1970
// Checks that the countdown is not <= 0
if diff <= 0 {
self.isActive = false
self.time = "0:00"
self.showingAlert = true
return
}
// Turns the time difference calculation into sensible data and formats it
let date = Date(timeIntervalSince1970: diff)
let calendar = Calendar.current
let minutes = calendar.component(.minute, from: date)
let seconds = calendar.component(.second, from: date)
// Updates the time string with the formatted time
self.minutes = Float(minutes)
self.time = String(format:"%d:%02d", minutes, seconds)
}
}
}
TimerView:
import SwiftUI
struct TimerView: View {
#ObservedObject var vm = ViewModel()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let width: Double = 250
var body: some View {
VStack {
Text("Timer: \(vm.time)")
.font(.system(size: 50, weight: .medium, design: .rounded))
.alert("Timer done!", isPresented: $vm.showingAlert) {
Button("Continue", role: .cancel) {
}
}
.padding()
HStack(spacing:50) {
Button("Start") {
vm.start(minutes: Float(vm.minutes))
}
.padding()
.background((Color(red: 184/255, green: 243/255, blue: 255/255)))
.foregroundColor(.black)
.cornerRadius(10)
.font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
//.disabled(vm.isActive)
if vm.isActive == true {
Button("Pause") {
vm.isActive = false
//self.timer.upstream.connect().cancel()
}
.padding()
.foregroundColor(.black)
.background(.red)
.cornerRadius(10)
.font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
} else {
Button("Resume") {
vm.isActive = true
}
.padding()
.foregroundColor(.black)
.background(.green)
.cornerRadius(10)
.font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
}
}
.frame(width: width)
}
.onReceive(timer) { _ in
vm.updateCountdown()
}
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
TimerView()
}
}
I noticed and fixed a number of things in your code:
start() is being called with the current value of vm.minutes, so it is going to start from that value and not 5. I changed it to use self.initialTime which means it's currently not using the value passed in. You need to decide if start() really wants to take a value and how to use it.
reset() wasn't being called. I call it from start().
Pause was only pausing the screen update. I changed it to keep track of the start time of the pause and to compute the amount of time paused so that it could accurately update the displayed time.
I made the Pause/Resume button one button with conditional values for title and color based upon vm.active.
Here is the updated code:
extension TimerView {
final class ViewModel: ObservableObject {
#Published var isActive = false
#Published var showingAlert = false
#Published var time: String = "5:00"
#Published var minutes: Float = 5.0 {
didSet {
self.time = "\(Int(minutes)):00"
}
}
var initialTime = 0
var endDate = Date()
var pauseDate = Date()
var pauseInterval = 0.0
// Start the timer with the given amount of minutes
func start(minutes: Float) {
self.initialTime = 5
self.reset()
self.endDate = Date()
self.endDate = Calendar.current.date(byAdding: .minute, value: self.initialTime, to: endDate)!
self.isActive = true
}
// Reset the timer
func reset() {
self.isActive = false
self.pauseInterval = 0.0
self.minutes = Float(initialTime)
self.time = "\(Int(minutes)):00"
}
func pause() {
if self.isActive {
pauseDate = Date()
} else {
// keep track of the total time we're paused
pauseInterval += Date().timeIntervalSince(pauseDate)
}
self.isActive.toggle()
}
// Show updates of the timer
func updateCountdown(){
guard isActive else { return }
// Gets the current date and makes the time difference calculation
let now = Date()
let diff = endDate.timeIntervalSince1970 + self.pauseInterval - now.timeIntervalSince1970
// Checks that the countdown is not <= 0
if diff <= 0 {
self.isActive = false
self.time = "0:00"
self.showingAlert = true
return
}
// Turns the time difference calculation into sensible data and formats it
let date = Date(timeIntervalSince1970: diff)
let calendar = Calendar.current
let minutes = calendar.component(.minute, from: date)
let seconds = calendar.component(.second, from: date)
// Updates the time string with the formatted time
//self.minutes = Float(minutes)
self.time = String(format:"%d:%02d", minutes, seconds)
}
}
}
struct TimerView: View {
#ObservedObject var vm = ViewModel()
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
let width: Double = 250
var body: some View {
VStack {
Text("Timer: \(vm.time)")
.font(.system(size: 50, weight: .medium, design: .rounded))
.alert("Timer done!", isPresented: $vm.showingAlert) {
Button("Continue", role: .cancel) {
}
}
.padding()
HStack(spacing:50) {
Button("Start") {
vm.start(minutes: Float(vm.minutes))
}
.padding()
.background((Color(red: 184/255, green: 243/255, blue: 255/255)))
.foregroundColor(.black)
.cornerRadius(10)
.font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
//.disabled(vm.isActive)
Button(vm.isActive ? "Pause" : "Resume") {
vm.pause()
//vm.isActive = false
//self.timer.upstream.connect().cancel()
}
.padding()
.foregroundColor(.black)
.background(vm.isActive ? .red : .green)
.cornerRadius(10)
.font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
}
.frame(width: width)
}
.onReceive(timer) { _ in
vm.updateCountdown()
}
}
}

Swipe to delete function

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
}
}
}
}
}

How to make my timer more precise in SwiftUI?

I would like to develop an app that includes a timer - everything works fine so far - yet I have the problem that the CircleProgress is not quite at 0 when the counter is. (as you can see in the picture below)
Long story short - my timer is not precise... How can I make it better?
So this is my code:
This is the View where I give my Binding to the ProgressCircleView:
struct TimerView: View {
//every Second change Circle and Value (Circle is small because of the animation)
let timerForText = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let timerForCircle = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
#State var progress : Double = 1.0
#State var counterCircle : Double = 0.0
#State var counterText : Int = 0
#State var timerSeconds : Int = 60
let customInverval : Int
var body: some View {
ZStack{
ProgressCircleView(progress: self.$progress, timerSeconds: self.$timerSeconds, customInterval: customInverval)
.padding()
.onReceive(timerForCircle){ time in
//if counterCircle is the same value as Interval -> break
if self.counterCircle == Double(customInverval){
self.timerForCircle.upstream.connect().cancel()
} else {
decreaseProgress()
}
counterCircle += 0.001
}
VStack{
Text("\(timerSeconds)")
.font(.system(size: 80))
.bold()
.onReceive(timerForText){time in
//wenn counterText is the same value as Interval -> break
if self.counterText == customInverval{
self.timerForText.upstream.connect().cancel()
} else {
incrementTimer()
print("timerSeconds: \(self.timerSeconds)")
}
counterText += 1
}.multilineTextAlignment(.center)
}
.accessibilityElement(children: .combine)
}.padding()
}
func decreaseProgress() -> Void {
let decreaseValue : Double = 1/(Double(customInverval)*1000)
self.progress -= decreaseValue
}
func incrementTimer() -> Void {
let decreaseValue = 1
self.timerSeconds -= decreaseValue
}
}
And this is my CircleProgressClass:
struct ProgressCircleView: View {
#Binding var progress : Double
#Binding var timerSeconds : Int
let customInterval : Int
var body: some View {
ZStack{
Circle()
.stroke(lineWidth: 25)
.opacity(0.08)
.foregroundColor(.black)
Circle()
.trim(from: 0.0, to: CGFloat(Double(min(progress, 1.0))))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(270.0))
.foregroundColor(getCircleColor(timerSeconds: timerSeconds))
.animation(.linear)
}
}
}
func getCircleColor(timerSeconds: Int) -> Color {
if (timerSeconds <= 10 && timerSeconds > 3) {
return Color.yellow
} else if (timerSeconds <= 3){
return Color.red
} else {
return Color.green
}
}
You cannot control the timer, it will never be entirely accurate.
Instead, I suggest you save the end date and calculate your progress based on it:
struct TimerView: View {
//every Second change Circle and Value (Circle is small because of the animation)
let timerForText = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let timerForCircle = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
#State var progress : Double = 1.0
#State var timerSeconds : Int = 60
#State var endDate: Date? = nil
let customInverval : Int
var body: some View {
ZStack{
ProgressCircleView(progress: self.$progress, timerSeconds: self.$timerSeconds, customInterval: customInverval)
.padding()
.onReceive(timerForCircle){ _ in
decreaseProgress()
}
VStack{
Text("\(timerSeconds)")
.font(.system(size: 80))
.bold()
.onReceive(timerForText){ _ in
incrementTimer()
}.multilineTextAlignment(.center)
}
.accessibilityElement(children: .combine)
}.padding()
.onAppear {
endDate = Date(timeIntervalSinceNow: TimeInterval(customInverval))
}
}
func decreaseProgress() -> Void {
guard let endDate = endDate else { return}
progress = max(0, endDate.timeIntervalSinceNow / TimeInterval(customInverval))
if endDate.timeIntervalSinceNow <= 0 {
timerForCircle.upstream.connect().cancel()
}
}
func incrementTimer() -> Void {
guard let endDate = endDate else { return}
timerSeconds = max(0, Int(endDate.timeIntervalSinceNow.rounded()))
if endDate.timeIntervalSinceNow <= 0 {
timerForText.upstream.connect().cancel()
print("stop")
}
}
}

SwiftUI - Fatal error: index out of range on deleting element from an array

I have an array on appstorage. I'm displaying its elements with foreach method. It has swipe to delete on each element of the array. But when i delete one, app is crashing. Here is my first view;
struct View1: View {
#Binding var storedElements: [myElements]
var body: some View{
GeometryReader {
geometry in
VStack{
ForEach(storedElements.indices, id: \.self){i in
View2(storedElements: $storedElements[i], pic: $storedWElements[i].pic, allElements: $storedElements, index: i)
}
}.frame(width: geometry.size.width, height: geometry.size.height / 2, alignment: .top).padding(.top, 25)
}
}
}
And View 2;
struct View2: View {
#Binding var storedElements: myElements
#Binding var pic: String
#Binding var allElements: [myElements]
var index: Int
#State var offset: CGFloat = 0.0
#State var isSwiped: Bool = false
#AppStorage("pics", store: UserDefaults(suiteName: "group.com.some.id"))
var arrayData: Data = Data()
var body : some View {
ZStack{
Color.red
HStack{
Spacer()
Button(action: {
withAnimation(.easeIn){
delete()}}) {
Image(systemName: "trash").font(.title).foregroundColor(.white).frame(width: 90, height: 50)
}
}
View3(storedElements: $storedElements).background(Color.red).contentShape(Rectangle()).offset(x: self.offset).gesture(DragGesture().onChanged(onChanged(value:)).onEnded(onEnd(value:)))
}.frame(width: 300, height: 175).cornerRadius(30)
}
}
func onChanged(value: DragGesture.Value) {
if value.translation.width < 0 {
if self.isSwiped {
self.offset = value.translation.width - 90
}else {
self.offset = value.translation.width
}
}
}
func onEnd(value: DragGesture.Value) {
withAnimation(.easeOut) {
if value.translation.width < 0 {
if -value.translation.width > UIScreen.main.bounds.width / 2 {
self.offset = -100
delete()
}else if -self.offset > 50 {
self.isSwiped = true
self.offset = -90
}else {
self.isSwiped = false
self.offset = 0
}
}else {
self.isSwiped = false
self.offset = 0
}
}
}
func delete() {
//self.allElements.remove(at: self.index)
if let index = self.allElements.firstIndex(of: storedElements) {
self.allElements.remove(at: index)
}
}
}
OnChange and onEnd functions are for swiping. I think its the foreach method that is causing crash. Also tried the commented line on delete function but no help.
And I know its a long code for a question. I'm trying for days and tried every answer here but none of them solved my problem here.
In your myElements class/struct, ensure it has a unique property. If not add one and upon init set a unique ID
public class myElements {
var uuid: String
init() {
self.uuid = NSUUID().uuidString
}
}
Then when deleting the element, instead of
if let index = self.allElements.firstIndex(of: storedElements) {
self.allElements.remove(at: index)
}
Use
self.allElements.removeAll(where: { a in a.uuid == storedElements.uuid })
This is array bound safe as it does not use an index

SwiftUI Table Custom Swipe?

Is there a way to swipe table rows to the left and to the right? I haven't found something for the new Framework SwiftUI so maybe there is no chance to use SwiftUI for this? I need to delete rows and use custom Swipes
It is possible to implement a delete action and the ability to reorder list items quite simply.
struct SwipeActionView: View {
#State var items: [String] = ["One", "two", "three", "four"]
var body: some View {
NavigationView {
List {
ForEach(items.identified(by: \.self)) { item in
Text(item)
}
.onMove(perform: move)
.onDelete(perform: delete)
}
.navigationBarItems(trailing: EditButton())
}
}
func delete(at offsets: IndexSet) {
if let first = offsets.first {
items.remove(at: first)
}
}
func move(from source: IndexSet, to destination: Int) {
// sort the indexes low to high
let reversedSource = source.sorted()
// then loop from the back to avoid reordering problems
for index in reversedSource.reversed() {
// for each item, remove it and insert it at the destination
items.insert(items.remove(at: index), at: destination)
}
}
}
Edit: There is this article by apple that I cannot believe I didn't find previously. Composing SwiftUI Gestures. I haven't experimented with it yet, but the article seems to do a great job!
I wanted the same and have now the following implementation.
The SwipeController checks when to execute a swipe action and performs the SwipeAction, for now you can add your swipe actions under the print lines in the executeAction function. But it is better make an abstract class from this.
Then in the SwipeLeftRightContainer struct we have most of the logic in the DragGesture. What it does is while your dragging its gonna change the offset and then make calls to the SwipeController to see if the threshold for swipe left or right are reached. Then when you finish the dragging it will come into the onEnded callback of the DragGesture. Here we will reset the offset and let the SwipeController decide to execute an action.
Keep in mind lot of the variables in the view are static for an iPhone X so you should change them to what fits best.
import SwiftUI
/** executeRight: checks if it should execute the swipeRight action
execute Left: checks if it should execute the swipeLeft action
submitThreshold: the threshold of the x offset when it should start executing the action
*/
class SwipeController {
var executeRight = false
var executeLeft = false
let submitThreshold: CGFloat = 200
func checkExecutionRight(offsetX: CGFloat) {
if offsetX > submitThreshold && self.executeRight == false {
Utils.HapticSuccess()
self.executeRight = true
} else if offsetX < submitThreshold {
self.executeRight = false
}
}
func checkExecutionLeft(offsetX: CGFloat) {
if offsetX < -submitThreshold && self.executeLeft == false {
Utils.HapticSuccess()
self.executeLeft = true
} else if offsetX > -submitThreshold {
self.executeLeft = false
}
}
func excuteAction() {
if executeRight {
print("executed right")
} else if executeLeft {
print("executed left")
}
self.executeLeft = false
self.executeRight = false
}
}
struct SwipeLeftRightContainer: View {
var swipeController: SwipeController = SwipeController()
#State var offsetX: CGFloat = 0
let maxWidth: CGFloat = 335
let maxHeight: CGFloat = 125
let swipeObjectsOffset: CGFloat = 350
let swipeObjectsWidth: CGFloat = 400
#State var rowAnimationOpacity: Double = 0
var body: some View {
ZStack {
Group {
HStack {
Text("Sample row")
Spacer()
}
}.padding(10)
.zIndex(1.0)
.frame(width: maxWidth, height: maxHeight)
.cornerRadius(5)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.gray))
.padding(10)
.offset(x: offsetX)
.gesture(DragGesture(minimumDistance: 5).onChanged { gesture in
withAnimation(Animation.linear(duration: 0.1)) {
offsetX = gesture.translation.width
}
swipeController.checkExecutionLeft(offsetX: offsetX)
swipeController.checkExecutionRight(offsetX: offsetX)
}.onEnded { _ in
withAnimation(Animation.linear(duration: 0.1)) {
offsetX = 0
swipeController.prevLocX = 0
swipeController.prevLocXDiff = 0
self.swipeController.excuteAction()
}
})
Group {
ZStack {
Rectangle().fill(Color.red).frame(width: swipeObjectsWidth, height: maxHeight).opacity(opacityDelete)
Image(systemName: "multiply").font(Font.system(size: 34)).foregroundColor(Color.white).padding(.trailing, 150)
}
}.zIndex(0.9).offset(x: swipeObjectsOffset + offsetX)
Group {
ZStack {
Rectangle().fill(Color.green).frame(width: swipeObjectsWidth, height: maxHeight).opacity(opacityLike)
Image(systemName: "heart").font(Font.system(size: 34)).foregroundColor(Color.white).padding(.leading, 150)
}
}.zIndex(0.9).offset(x: -swipeObjectsOffset + offsetX)
}
}
var opacityDelete: Double {
if offsetX < 0 {
return Double(abs(offsetX) / 50)
}
return 0
}
var opacityLike: Double {
if offsetX > 0 {
return Double(offsetX / 50)
}
return 0
}
}
struct SwipeListView: View {
var body: some View {
ScrollView {
ForEach(0..<10) { index in
SwipeLeftRightContainer().listRowInsets(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
}
}
}
}
struct SwipeLeftRight_Previews: PreviewProvider {
static var previews: some View {
SwipeListView()
}
}