SwiftUI - Changing text as well as showing new string character by character - swift

I have an array of string. I want to display it below manner.
Eg- ["one", "two", "three"]
first one should be displayed character by character
then two then three.
Below code is changing string from one to two and two to three. but not anble to do character by character animation.
struct ContentView: View {
#State var index: Int = 0
var fontColor: String
var displayData: [String]
let timer = Timer.publish(every: 1.5, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Text(displayData[safe: index] ?? displayData[0])
.foregroundColor(Color(hex: fontColor))
.font(.uiFont(.subtitle3))
.animation(.linear(duration: 1.0))
}.onReceive(timer) { _ in
if index < displayData.count {
index += 1
} else {
index = 0
}
}
}
}
Can anyone help me to display character one by one when string changes.

Here's possible solution. Each word (one, two, three, etc) is displayed in a
CharacterByCharacter view. This creates its own array of strings for each word passed in, for example ["o", "on", "one"], then displays them one at a time to give the appearance of presenting character-by-character:
struct ContentView: View {
#State private var index = 0
#State private var finished = false
let strings = ["one", "two", "three", "four", "five", "six"]
var longestString: String {
strings.max(by: { $0.count < $1.count }) ?? ""
}
var body: some View {
if finished {
Text("Finished")
.font(.title)
} else {
HStack(spacing: 8) {
ProgressView()
Text(longestString)
.hidden()
.overlay(alignment: .leading) { // Overlay on the hidden longest string to give a fixed frame
CharacterByCharacter(text: strings[index]) {
if index < strings.count - 1 {
index += 1
} else {
finished = true
}
}
}
}
}
}
}
struct CharacterByCharacter: View {
init(text: String, didFinish: #escaping() -> Void) {
var strings = Array(repeating: "", count: text.count)
text.enumerated().forEach { index, c in
(index..<text.count).forEach { strings[$0].append(String(c)) }
}
displayData = strings
self.didFinish = didFinish
}
#State var index: Int = 0
private let displayData: [String]
let didFinish: () -> Void
let timer = Timer.publish(every: 0.2, on: .main, in: .common).autoconnect()
var body: some View {
Text(displayData[index])
.onReceive(timer) { _ in
if index < displayData.count - 1 {
index += 1
} else {
didFinish()
index = 0
timer.upstream.connect().cancel()
}
}
}
}

Related

Dynamically sized #State var

I'm loading data into a struct from JSON. With this data, a new structure (itemStructures) is created and filled for use in a SwiftUI View. In this View I have a #State var which I manually initialise to have enough space to hold all items. This state var holds all parameters which are nested within the items, hence the nested array.
As long as this #State var has enough empty spaces everything works fine. But my question is, how do I modify this #State programmatically for when the number of items increases er decreases with the loading of a new JSON? I could make it really large but I'd rather have it the exact size after each load.
//Structs used in this example
struct MainViewState {
var itemStructures: [ItemStructure]
}
struct ItemStructure: Identifiable, Hashable {
var id: String {name}
var name: String
var parameters: [Parameter]
}
struct Parameter: Identifiable, Hashable {
var id: String {name}
var name: String
var value: Double
var range: [Double]
}
struct ContentView: View {
//In this model json is loaded, this seemed out of scope for this question to include this
#ObservedObject var viewModel: MainViewModel
//This is the #State var which should be dynamically allocated according to the content size of "itemStructures"
//For now 3 items with 10 parameters each are enough
#State var parametersPerItem: [[Float]] = [
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0]
]
init(viewModel: MainViewModel) {
self.viewModel = viewModel
}
var body: some View {
let itemStructures = viewModel.mainState.itemStructures
ForEach( Array(itemStructures.enumerated()), id: \.element ) { index, item in
Text(item.name)
ForEach( Array(item.parameters.enumerated()), id: \.element ) { i, parameter in
Text(parameter.name)
SliderView(
label: parameter.name,
value: Binding(
get: { self.parametersPerItem[index][i] },
set: { (newVal) in
self.parametersPerItem[index][i] = newVal
//Function to send slider values and ranges to real time processing
//doStuffWithRangesAndValues()
}
),
range: parameter.range,
showsLabel: false
).onAppear {
//Set initial value slider
parametersPerItem[index][i] = Float(parameter.value)
}
}
}
}
}
struct SliderView: View {
var label: String
#Binding var value: Float
var range: [Double]
var showsLabel: Bool
init(label: String, value: Binding<Float>, range: [Double], showsLabel: Bool = true) {
self.label = label
_value = value
self.range = range
self.showsLabel = showsLabel
}
var body: some View {
GeometryReader { geometry in
ZStack{
if showsLabel { Text(label) }
HStack {
Slider(value: $value)
.foregroundColor(.accentColor)
.frame(width: geometry.size.width * 0.8)
//In the real app range calculations are done here
let valueInRange = value
Text("\(valueInRange, specifier: range[1] >= 1000 ? "%.0f" : "%.2f")")
.foregroundColor(.white)
.font(.subheadline)
.frame(width: geometry.size.width * 0.2)
}
}
}
.frame(height: 40.0)
}
}
If you are looking for a solution where you want to initialise the array after the json has been loaded you could add a computed property in an extension to the main/root json model and use it to give the #State property an initial value.
extension MainViewState {
var parametersPerItem: [[Float]] {
var array: [[Float]] = []
if let max = itemStructures.map(\.parameters.count).max(by: { $0 < $1 }) {
for _ in itemStructures {
array.append(Array(repeating: 0.0, count: max))
}
}
return array
}
}

Display function output live without Button press

My Swift UI code currently calls a function to display calculations upon a button call. I'd like to display the function's output without the button call (in other words, the function is "live" and constantly calculating anytime a necessary variable is changed). Basically, I'm looking to get rid of the button that triggers this function call calculation, and always have the function's display shown. It has default values so it should have info even before the user inputs or something is changed.
The first screenshot shows the code currently, and the second shows where I'd like the time calculation string to always be. Note: this uses a Create ML file, so if you're inputting this code into your editor, it's not necessary to have the model use to calculate. Any use and output of the variables will do and I've left some commented code that might help.
I'm thinking there might be a calculate on change of X, Y, Z variable needed here. I'm not sure the best way to approach this and would love any ideas. Thanks!
import CoreML
import SwiftUI
struct ContentView: View {
#State var wakeUpTime = defaultWakeTime
#State var coffeeAmount = 1.0
#State var sleepAmount = 8.0
#State var alertTitle = ""
#State var alertMessage = ""
#State var showAlert = false
static var defaultWakeTime: Date {
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? Date.now
}
var body: some View {
NavigationView {
Form {
Section {
DatePicker("Please enter a time", selection: $wakeUpTime, displayedComponents: .hourAndMinute)
.labelsHidden()
} header: {
Text("When do you want to wake up?")
.font(.headline)
}
VStack(alignment: .leading, spacing: 0) {
Text("Hours of sleep?")
.font(.headline)
Stepper(sleepAmount == 1 ? "1 hour" : "\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 1...12, step: 0.25)
}
VStack(alignment: .leading, spacing: 0) {
Text("Cups of coffee?")
.font(.headline)
Stepper(coffeeAmount == 1 ? "1 cup" : "\(coffeeAmount.formatted()) cups", value: $coffeeAmount, in: 1...12, step: 0.25)
}
Section {
Text("Head to bed at: IDEAL TIME HERE")
}
}
.navigationTitle("BetterRest")
.toolbar {
Button("Calculate", action: calculateBedtime)
}
.alert(alertTitle, isPresented: $showAlert) {
Button("Ok") { }
} message: {
Text(alertMessage)
}
}
}
func calculateBedtime() {
do {
let config = MLModelConfiguration()
let model = try SleepCalculator(configuration: config)
let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUpTime)
let hour = (components.hour ?? 0) * 60 * 60
let minute = (components.minute ?? 0) * 60
let predicition = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
let sleepTime = wakeUpTime - predicition.actualSleep
alertTitle = "Your ideal bedtime is..."
alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)
}
catch {
alertTitle = "Error"
alertMessage = "Sorry. There was a problem calculating your bedtime."
}
showAlert = true
// IF TRYING WITHOUT CREATE ML MODEL, comment out all of above^
// let alertTitle = "Showing calculated title"
// let alertMessage = "7:15 am"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
you could try this approach, where you create a class BedTimeModel: ObservableObject to
monitor changes in the various variables that is used to calculate (dynamically)
your sleepTime using func calculateBedtime().
EDIT-1: using Optional sleepTime
class BedTimeModel: ObservableObject {
#Published var sleepTime: Date? = Date() // <-- here optional
#Published var wakeUpTime = defaultWakeTime {
didSet { calculateBedtime() }
}
#Published var coffeeAmount = 1.0 {
didSet { calculateBedtime() }
}
#Published var sleepAmount = 8.0 {
didSet { calculateBedtime() }
}
// can also change this to return the calculated value and use it to update the `sleepTime`
func calculateBedtime() {
// do {
// let config = MLModelConfiguration()
// let model = try SleepCalculator(configuration: config)
// let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUpTime)
// let hour = (components.hour ?? 0) * 60 * 60
// let minute = (components.minute ?? 0) * 60
// let predicition = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
//
// sleepTime = wakeUpTime - predicition.actualSleep // <-- here
// }
// catch {
// sleepTime = nil // <-- here could not be calculated
// }
// for testing, adjust the real calculation to update sleepTime
sleepTime = wakeUpTime.addingTimeInterval(36000 * (sleepAmount + coffeeAmount))
}
static var defaultWakeTime: Date {
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? Date.now
}
}
struct ContentView: View {
#StateObject private var vm = BedTimeModel() // <-- here
var body: some View {
NavigationView {
Form {
Section {
DatePicker("Please enter a time", selection: $vm.wakeUpTime, displayedComponents: .hourAndMinute)
.labelsHidden()
} header: {
Text("When do you want to wake up?").font(.headline)
}
VStack(alignment: .leading, spacing: 0) {
Text("Hours of sleep?").font(.headline)
Stepper(vm.sleepAmount == 1 ? "1 hour" : "\(vm.sleepAmount.formatted()) hours", value: $vm.sleepAmount, in: 1...12, step: 0.25)
}
VStack(alignment: .leading, spacing: 0) {
Text("Cups of coffee?").font(.headline)
Stepper(vm.coffeeAmount == 1 ? "1 cup" : "\(vm.coffeeAmount.formatted()) cups", value: $vm.coffeeAmount, in: 1...12, step: 0.25)
}
Section {
// -- here
if let stime = vm.sleepTime {
Text("Head to bed at: \(stime.formatted(date: .omitted, time: .shortened))")
} else {
Text("There was a problem calculating your bedtime.")
}
}
}
.navigationTitle("BetterRest")
}
}
}

Timer within EnvironmentObject view model not updating the View

I have a view model, that has multiple child view models. I am fairly new to watchOS, SwiftUI and Combine - taking this opportunity to learn.
I have a watchUI where it has
Play Button (View) - SetTimerPlayPauseButton
Text to show Time (View) - TimerText
View Model - that has 1 WatchDayProgramViewModel - N: ExerciseTestClass - N: SetInformationTestClass. For each ExerciseSets, there is a watchTimer & watchTimerSubscription and I have managed to run the timer to update remaining rest time.
ContentView - that has been injected the ViewModel as EnvironmentObject
If I tap SetTimerPlayPauseButton to start the timer, timer is running, working and changing the remainingRestTime(property within the child view model SetInformationTestClass) correctly, but the updates/changes are not being "published" to the TimerText View.
I have done most, if not all, the recommendation in other SO answers, I even made all my WatchDayProgramViewModel and ExerciseTestClass,SetInformationTestClass properties #Published, but they are still not updating the View, when the view model properties are updated as shown in the Xcode debugger below.
Please review my code and give me some advice on how to improve it.
ContentView
struct ContentView: View {
#State var selectedTab = 0
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
var body: some View {
TabView(selection: $selectedTab) {
SetRestDetailView().id(2)
}
.environmentObject(watchDayProgramVM)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(.page(backgroundDisplayMode: .automatic))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView(watchDayProgramVM: WatchDayProgramViewModel())
}
}
}
SetRestDetailView
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
var setCurrentHeartRate: Int = 120
#State var showingLog = false
var body: some View {
HStack {
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
TimerText(elapsedRestTime: elapsedRestTime, totalRestTime: totalRestTime, rect: rect)
.border(Color.yellow)
}
HStack {
SetTimerPlayPauseButton(isSetTimerRunningFlag: false,
playImage: "play.fill",
pauseImage: "pause.fill",
bgColor: Color.clear,
fgColor: Color.white.opacity(0.5),
rect: rect) {
print("playtimer button tapped")
self.watchDayProgramVM.exerciseVMList[0].sets[2].startTimer()
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
print("printing elapsedRestTime from SetRestDetailView \(elapsedRestTime)")
print("printing elapsedRestTime from SetRestDetailView \(totalRestTime)")
}
.border(Color.yellow)
}
}
}
TimerText
struct TimerText: View {
var elapsedRestTime: Int
var totalRestTime: Int
var rect: CGRect
var body: some View {
VStack {
Text(counterToMinutes())
.font(.system(size: 100, weight: .semibold, design: .rounded))
.kerning(0)
.fontWeight(.semibold)
.minimumScaleFactor(0.25)
.padding(-1)
}
}
func counterToMinutes() -> String {
let currentTime = totalRestTime - elapsedRestTime
let seconds = currentTime % 60
let minutes = Int(currentTime / 60)
if currentTime > 0 {
return String(format: "%02d:%02d", minutes, seconds)
}
else {
return ""
}
}
}
ViewModel
import Combine
final class WatchDayProgramViewModel: ObservableObject {
#Published var exerciseVMList: [ExerciseTestClass] = [
(static/hard-coded values for testing)
]
class ExerciseTestClass: ObservableObject {
init(exercise: String, sets: [SetInformationTestClass]) {
self.exercise = exercise
self.sets = sets
}
var exercise: String
#Published var sets: [SetInformationTestClass]
}
class SetInformationTestClass: ObservableObject {
init(totalRestTime: Int, elapsedRestTime: Int, remainingRestTime: Int, isTimerRunning: Bool) {
self.totalRestTime = totalRestTime
self.elapsedRestTime = elapsedRestTime
self.remainingRestTime = remainingRestTime
self.isTimerRunning = isTimerRunning
}
#Published var totalRestTime: Int
#Published var elapsedRestTime: Int
#Published var remainingRestTime: Int
#Published var isTimerRunning = false
#Published var watchTimer = Timer.publish(every: 1.0, on: .main, in: .default)
#Published var watchTimerSubscription: AnyCancellable? = nil
#Published private var startTime: Date? = nil
func startTimer() {
print("startTimer initiated")
self.watchTimerSubscription?.cancel()
if startTime == nil {
startTime = Date()
}
self.isTimerRunning = true
self.watchTimerSubscription = watchTimer
.autoconnect()
.sink(receiveValue: { [weak self] _ in
guard let self = self, let startTime = self.startTime else { return }
let now = Date()
let elapsedTime = now.timeIntervalSince(startTime)
self.remainingRestTime = self.totalRestTime - Int(elapsedTime)
self.elapsedRestTime = self.totalRestTime - self.remainingRestTime
guard self.remainingRestTime > 0 else {
self.pauseTimer()
return
}
self.objectWillChange.send()
print("printing elapsedRest Time \(self.elapsedRestTime) sec")
print("printing remaining Rest time\(self.remainingRestTime)sec ")
})
}
func pauseTimer() {
//stop timer and retain elapsed rest time
print("pauseTimer initiated")
self.watchTimerSubscription?.cancel()
self.watchTimerSubscription = nil
self.isTimerRunning = false
self.startTime = nil
}
Managed to resolve the issue with help of #lorem ipsum and his feedback. As per his comment, the problem lied with the fact that
it is more than likely not working because you are chaining ObservableObjects #Published will only detect a change when the object is changed as a whole now when variables change. One way to test is to wrap each SetInformationTestClass in an #ObservbleObject by using a subview that takes the object as a parameter.
After which, I managed to find similar SO answers on changes in nested view model (esp child), and made the child view model an ObservedObject. The changes in child view model got populated to the view. Please see the changed code below.
SetRestDetailView
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
var setCurrentHeartRate: Int = 120
#State var showingLog = false
var body: some View {
HStack {
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
let setInformatationVM = self.watchDayProgramVM.exerciseVMList[0].sets[2]
TimerText(setInformationVM: setInformatationVM, rect: rect)
.border(Color.yellow)
}
HStack {
SetTimerPlayPauseButton(isSetTimerRunningFlag: false,
playImage: "play.fill",
pauseImage: "pause.fill",
bgColor: Color.clear,
fgColor: Color.white.opacity(0.5),
rect: rect) {
print("playtimer button tapped")
self.watchDayProgramVM.exerciseVMList[0].sets[2].startTimer()
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
print("printing elapsedRestTime from SetRestDetailView \(elapsedRestTime)")
print("printing elapsedRestTime from SetRestDetailView \(totalRestTime)")
}
.border(Color.yellow)
}
}
}
TimerText
struct TimerText: View {
#ObservedObject var setInformationVM: SetInformationTestClass
// #State var elapsedRestTime: Int
// #State var totalRestTime: Int
var rect: CGRect
var body: some View {
VStack {
Text(counterToMinutes())
.font(.system(size: 100, weight: .semibold, design: .rounded))
.kerning(0)
.fontWeight(.semibold)
.minimumScaleFactor(0.25)
.padding(-1)
}
}
func counterToMinutes() -> String {
let currentTime = setInformationVM.totalRestTime - setInformationVM.elapsedRestTime
let seconds = currentTime % 60
let minutes = Int(currentTime / 60)
if currentTime > 0 {
return String(format: "%02d:%02d", minutes, seconds)
}
else {
return ""
}
}
}

Passing calculated variable to another View

I am trying to create my own grid, which resizing to every element. It's okay with that. Here is a code
GeometryReader { geo in
let columnCount = Int((geo.size.width / 250).rounded(.down))
let tr = CDNresponse.data?.first?.translations ?? []
let rowsCount = (CGFloat(tr.count) / CGFloat(columnCount)).rounded(.up)
LazyVStack {
ForEach(0..<Int(rowsCount), id: \.self) { row in // create number of rows
HStack {
ForEach(0..<columnCount, id: \.self) { column in // create columns
let index = row * columnCount + column
if index < (tr.count) {
VStack {
MovieCellView(index: index, tr: tr, qualities: $qualities)
}
}
}
}
}
}
}
But since I don't know the exact number of elements and their indices, I need to calculate them in view.
let index = row * columnCount + column
And that's the problem - if I pass them as usual (MovieCellView(index: index ... )) when the index changing, the new value is not passed to view.
I cannot use #State and #Binding, as I cannot declare it directly in View Builder and can't declare it on struct because I don't know the count. How to pass data correctly?
Code of MovieCellView:
struct MovieCellView: View {
#State var index: Int
#State var tr: [String]
#State var showError: Bool = false
#State var detailed: Bool = false
#Binding var qualities: [String : [Int : URL]]
var body: some View {
...
}
}
The most simple example
Just added Text("\(index)") in VStack with MovieCellView and Text("\(index)") in MovieCellView body. Index in VStack always changing, but not in MovieCellView.
It is necessary to clarify, my application is on a macOS and the window is resized
This is my solution (test code) to your issue of passing calculated variable to another View.
I use an ObservableObject to store the information needed to achieve what you are after.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class MovieCellModel: ObservableObject {
#Published var columnCount: Int = 0
#Published var rowCount: Int = 0
// this could be in the view
func index(row: Int, column: Int) -> Int {
return row * columnCount + column
}
}
struct ContentView: View {
#StateObject var mcModel = MovieCellModel()
#State var qualities: [String : [Int : URL]] = ["":[1: URL(string: "https://example.com")!]] // for testing
let tr = CDNresponse.data?.first?.translations ?? [] // for testing
var body: some View {
VStack {
GeometryReader { geo in
LazyVStack {
ForEach(0..<Int(mcModel.rowCount), id: \.self) { row in // create number of rows
HStack {
ForEach(0..<mcModel.columnCount, id: \.self) { column in // create 3 columns
if mcModel.index(row: row, column: column) < (tr.count) {
VStack {
MovieCellView(index: mcModel.index(row: row, column: column), tr: tr, qualities: $qualities)
}
}
}
}
}
}.onAppear {
mcModel.columnCount = Int((geo.size.width / 250).rounded(.down))
mcModel.rowCount = Int((CGFloat(tr.count) / CGFloat(mcModel.columnCount)).rounded(.up))
}
}
}
}
}
struct MovieCellView: View {
#State var index: Int
#State var tr: [String]
#Binding var qualities: [String : [Int : URL]]
#State var showError: Bool = false
#State var detailed: Bool = false
var body: some View {
Text("\(index)").foregroundColor(.red)
}
}
The only variable that changes outside of a ForEach in your index calculation is columnCount. row and column are simply indices from the loops. So you can declare columnCount as a #State variable on the parent view, hence when it changes, the parent view will update and hence all of the cells will also update with the new columnCount.
#State private var columnCount: CGFloat = 0
var body: some View {
GeometryReader { geo in
columnCount = Int((geo.size.width / 250).rounded(.down))
let tr = CDNresponse.data?.first?.translations ?? []
let rowsCount = (CGFloat(tr.count) / CGFloat(columnCount)).rounded(.up)
LazyVStack {
ForEach(0..<Int(rowsCount), id: \.self) { row in // create number of rows
HStack {
ForEach(0..<columnCount, id: \.self) { column in // create 3 columns
let index = row * columnCount + column
if index < (tr.count) {
VStack {
MovieCellView(index: index, tr: tr, qualities: $qualities)
}
}
}
}
}
}
}
}
Since none of the answers worked, and only one worked half, I solved the problem myself. You need to generate a Binding, and then just pass it
var videos: some View {
GeometryReader { geo in
let columnCount = Int((geo.size.width / 250).rounded(.down))
let rowsCount = (CGFloat((CDNresponse.data?.first?.translations ?? []).count) / CGFloat(columnCount)).rounded(.up)
LazyVStack {
ForEach(0..<Int(rowsCount), id: \.self) { row in // create number of rows
HStack {
ForEach(0..<columnCount, id: \.self) { column in // create 3 columns
let index = row * columnCount + column
if index < ((CDNresponse.data?.first?.translations ?? []).count) {
MovieCellView(translation: trBinding(for: index), qualities: qualityBinding(for: (CDNresponse.data?.first?.translations ?? [])[index]))
}
}
}
}
}
}
}
private func qualityBinding(for key: String) -> Binding<[Int : URL]> {
return .init(
get: { self.qualities[key, default: [:]] },
set: { self.qualities[key] = $0 })
}
private func trBinding(for key: Int) -> Binding<String> {
return .init(
get: { (self.CDNresponse.data?.first?.translations ?? [])[key] },
set: { _ in return })
}
struct MovieCellView: View {
#State var showError: Bool = false
#State var detailed: Bool = false
#Binding var translation: String
#Binding var qualities: [Int : URL]
var body: some View {
...
}
}

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