Show specific items in a ForEach loop in Swift - arrays

I am very new to Swift and coding in general and I have a question about accessing specific items in an array. I want to only show two items(creatures) from the creatures array in my view instead of returning all of the items. How would I go about doing this?
Here is the view where I want to show only two items(creatures):
struct PlayingCreatures: View {
#EnvironmentObject var data : CreatureZoo
var body: some View {
// Want to only show 2 animals on this screen
VStack {
ZStack {
ForEach(data.creatures) {creature in
Text(creature.emoji)
.resizableFont()
.offset(creature.offset)
.rotationEffect(creature.rotation)
.animation(.spring(response: 0.5, dampingFraction: 0.5), value: creature.rotation)
.animation(.default.delay(data.indexFor(creature) / 10), value: creature.offset)
}
}
.onTapGesture {
data.randomizeOffsets()
}
}
}
}
Here is the view where the data is stored:
class CreatureZoo : ObservableObject {
#Published var creatures = [
Creature(name: "Gorilla", emoji: "🦍"),
Creature(name: "Peacock", emoji: "🦚"),
Creature(name: "Squid", emoji: "🦑"),
Creature(name: "T-Rexxx", emoji: "🦖"),
Creature(name: "Sloth", emoji: "🦥")
]
}
struct Creature : Identifiable {
var name : String
var emoji : String
var id = UUID()
var offset = CGSize.zero
var rotation : Angle = Angle(degrees: 0)
// Should be hidden probably
extension CreatureZoo {
func randomizeOffsets() {
for index in creatures.indices {
creatures[index].offset = CGSize(width: CGFloat.random(in: -200...200), height: CGFloat.random(in: -200...200))
creatures[index].rotation = Angle(degrees: Double.random(in: 0...720))
}
}
func synchronizeOffsets() {
let randomOffset = CGSize(width: CGFloat.random(in: -200...200), height: CGFloat.random(in: -200...200))
for index in creatures.indices {
creatures[index].offset = randomOffset
}
}
func indexFor(_ creature: Creature) -> Double {
if let index = creatures.firstIndex(where: { $0.id == creature.id }) {
return Double(index)
}
return 0.0
}
}

Use prefix
ForEach(Array(data.creatures.prefix(2))) {creature in

Related

Swift Struct in Array of Structs not updating to new values

This is my data structure
struct SPPWorkout: Codable {
static let setKey = "Sets"
static let exerciseID = "id"
var id: Double? = 0.0
var duration: String?
var calories: Int?
var date: String?
var exercises: [ExerciseSet]
[...]
}
struct ExerciseSet: Codable {
let id: String
let name: String
var reps: Int
var weight: Double
[...]
}
extension ExerciseSet: Equatable {
static func ==(lhs: ExerciseSet, rhs: ExerciseSet) -> Bool {
return lhs.id == rhs.id
}
}
and in a SwiftUI view I'm trying to modify an ExerciseSet from user input
#State private var sppWorkout: SPPWorkout!
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ?? 0.0
self.editingIndex = nil
})
}
The issue is here
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ??
and I've tried in all ways to update it, both from the view and with a func in SPPWorkout. I've also tried to replace the object at index
var newSet = ExerciseSet(id: [...], newValues)
self.exercises[editingIndex] = newSet
but in no way it wants to update. I'm sure that somewhere it creates a copy that it edits but I have no idea why and how to set the new values.
Edit: if I try to delete something, it's fine
sppWorkout.exercises.removeAll(where: { $0 == sppWorkout.exercises[index]})
Edit 2:
It passes the guard statement and it does not change the values in the array.
Edit 3:
At the suggestion below from Jared, I've copied the existing array into a new one, set the new values then tried to assign the new one over to the original one but still, it does not overwrite.
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
print(sppWorkout.exercises)
guard let editingIndex = editingIndex else { return }
var copyOfTheArray = sppWorkout.exercises
copyOfTheArray[editingIndex].reps = Int(reps) ?? 0
copyOfTheArray[editingIndex].weight = Double(weight) ?? 0.0
//Copy of the array is updated correctly, it has the new values
sppWorkout.exercises = copyOfTheArray
//Original array doesn't get overwritten. It still has old values
self.editingIndex = nil
Edit 4: I've managed to make progress by extracting the model into a view model and updating the values there. Now the values get updated in sppWorkout, but even though I call objectWillChange.send(), the UI Update doesn't trigger.
full code:
class WorkoutDetailsViewModel: ObservableObject {
var workoutID: String!
#Published var sppWorkout: SPPWorkout!
func setupData(with workoutID: String) {
sppWorkout = FileIOManager.readWorkout(with: workoutID)
}
func update(_ index: Int, newReps: Int, newWeight: Double) {
let oldOne = sppWorkout.exercises[index]
let update = ExerciseSet(id: oldOne.id, name: oldOne.name, reps: newReps, weight: newWeight)
sppWorkout.exercises[index] = update
self.objectWillChange.send()
}
}
struct WorkoutDetailsView: View {
var workoutID: String!
#StateObject private var viewModel = WorkoutDetailsViewModel()
var workout: HKWorkout
var dateFormatter: DateFormatter
#State private var offset = 0
#State private var isShowingOverlay = false
#State private var editingIndex: Int?
#EnvironmentObject var settingsManager: SettingsManager
#Environment(\.dismiss) private var dismiss
var body: some View {
if viewModel.sppWorkout != nil {
VStack {
ListWorkoutItem(workout: workout, dateFormatter: dateFormatter)
.padding([.leading, .trailing], 10.0)
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
let index = viewModel.sppWorkout.exercises.firstIndex(of: exercise) ?? 0
DetailListSetItem(exerciseSet: viewModel.sppWorkout.exercises[index], set: index + 1)
.environmentObject(settingsManager)
.swipeActions {
Button(role: .destructive, action: {
viewModel.sppWorkout.exercises.removeAll(where: { $0 == viewModel.sppWorkout.exercises[index]})
} ) {
Label("Delete", systemImage: "trash")
}
Button(role: .none, action: {
isShowingOverlay = true
editingIndex = index
} ) {
Label("Edit", systemImage: "pencil")
}.tint(.blue)
}
}
.padding([.leading, .trailing], -30)
//iOS 16 .scrollContentBackground(.hidden)
}
.overlay(alignment: .bottom, content: {
editOverlay
.animation(.easeInOut (duration: 0.5), value: isShowingOverlay)
})
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
do {
try FileIOManager.write(viewModel.sppWorkout, toDocumentNamed: "\(viewModel.sppWorkout.id ?? 0).json")
} catch {
Debugger.log(error: error.localizedDescription)
}
dismiss()
}){
Image(systemName: "arrow.left")
})
} else {
Text("No workout details found")
.italic()
.fontWeight(.bold)
.font(.system(size: 35))
.onAppear(perform: {
viewModel.setupData(with: workoutID)
})
}
}
#ViewBuilder private var editOverlay: some View {
if isShowingOverlay {
ZStack {
Button {
isShowingOverlay = false
} label: {
Color.clear
}
.edgesIgnoringSafeArea(.all)
VStack{
Spacer()
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
print(viewModel.sppWorkout.exercises)
print("dupa aia:\n")
viewModel.update(editingIndex, newReps: Int(reps) ?? 0, newWeight: Double(weight) ?? 0.0)
print(viewModel.sppWorkout.exercises)
self.editingIndex = nil
})
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color("popupBackground"),
lineWidth: 3)
)
}
}
}
}
}
So I got a very good explanation on reddit on what causes the problem. Thank you u/neddy-seagoon if you are reading this.
The explanation
. I believe that updating an array will not trigger a state update. The only thing that will, with an array, is if the count changes. So
sppWorkout.exercises[index].reps = newReps
will not cause a trigger. This is not changing viewModel.sppWorkout.exercises.indices
So all I had to to was modify my List from
List(viewModel.sppWorkout.exercises, id: \.id)
to
List(viewModel.sppWorkout.exercises, id: \.hashValue)
as this triggers the list update because the hashValue does change when updating the properties of the entries in the list.
For the line
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
Replace with
List(viewModel.sppWorkout.exercises, id: \.self) { exercise in

Multiple filters for list in SwiftUI

how I can filtering my List with multiple
toggles? Need filter for Red, Yellow and Green toggles, for default (all toggles switch off) shows all Array non-filtered
struct Test: View {
#State var redToggle: Bool = false
#State var yellowToggle: Bool = false
#State var greenToggle: Bool = false
let testArray = ["red", "green", "yellow", "mint", "red", "green", "white"]
var body: some View {
VStack {
List {
ForEach(testArray.filter({
return (
(!redToggle && $0.description != "red") ||
(!yellowToggle && $0.description == "red")
//need add here some filters for green toggle
)
}), id: \.self) { colors in
Text("Color are \(colors)")
}
}
Spacer()
HStack(alignment: .center) {
Toggle(isOn: $redToggle) {
Text("Red")
}.toggleStyle(.switch)
Toggle(isOn: $yellowToggle) {
Text("Yellow")
}.toggleStyle(.switch)
Toggle(isOn: $greenToggle) {
Text("Green")
}.toggleStyle(.switch)
}
}
}
}
If you want to be able to filter multiple things from a list you can try something like this
import SwiftUI
struct MultipleFilterView: View {
let testArray = ["red", "green", "yellow", "mint", "red", "green", "white"]
//Dictionary that contains your filter word and wether it is selected or now
//If you want more options just add them here
#State var keywordDict: [String: Bool] = ["red": false, "yellow": false, "green": false]
//An array that contains all the selected keys
var selectedKeys: [String]{
keywordDict.keys.filter({ key in
keywordDict[key] ?? false
})
}
//identify if the user has selected items
var hasSelectedItem: Bool{
var results = false
//returns false unless it finds a true in the dictionary
//https://whatis.techtarget.com/definition/logic-gate-AND-OR-XOR-NOT-NAND-NOR-and-XNOR
keywordDict.values.forEach({
results = results || $0
})
return results
}
//List will show the selected items
var filteredList: [String]{
testArray.filter({ value in
selectedKeys.contains(where: {
$0 == value
})
})
}
var body: some View {
VStack {
List {
//Show the filtered list if the user has selected an item
ForEach(hasSelectedItem ? filteredList: testArray, id: \.self) { colors in
Text("Color is \(colors)")
}
}
Spacer()
HStack(alignment: .center) {
//Create a toggle for every key
//You need a custom Binding for a dictionary
ForEach(keywordDict.keys.sorted(by: <), id:\.self, content: { key in
Toggle(key, isOn: Binding(get: {
keywordDict[key] ?? false
}, set: {
keywordDict[key] = $0
})).toggleStyle(.switch)
})
}
}
}
}
struct MultipleFilterView_Previews: PreviewProvider {
static var previews: some View {
//SingleFilterView()
MultipleFilterView()
}
}

Swift - Cannot append Object to Array

I have created a New View in my App, but the Identifiable Object won't append to the Array.
I really don't know why its not appending...
Here is the Code:
struct FirstSettingsIdentifiables: Identifiable {
var id: UUID = UUID()
var name: String
var icon: String
}
struct SettingsView: View {
#EnvironmentObject var settingItems: ContentModel
#State var firstArr: [FirstSettingsIdentifiables] = []
init() {
createFirstList()
print("Settings successfully initialized.")
}
var body: some View {
return VStack {
Text("Einstellungen")
.font(.title)
NavigationView {
//Mitteilungen Liste
List(firstArr) { x in
// ForEach(firstArr) { x in
// VStack {
// Image(systemName: x.icon)
Text("Das ist ein test")
// }
// }
}.navigationBarTitle("Mitteilungen")
}
}
}
func createFirstList() {
let aText = "Mitteilungen"
let aIcon = "info.circle.fill"
let aObject = FirstSettingsIdentifiables(name: aText, icon: aIcon)
firstArr.append(aObject)
print(firstArr.count)
}
}
The problem is probably in the createFirstList() Section. In this function, the Object aObject is full of data(This is working fine), but then the Object won't append to my firstArr. The count is always 0.
What am I doing wrong here?
You are changing the value of firstArr too early. Instead of calling createFirstList() in the init, remove that and instead add the following code onto the view body:
VStack {
/* ... */
}
.onAppear(perform: createFirstList)
Alternatively, you could do the following:
init() {
_firstArr = State(initialValue: getFirstList())
print(firstArr.count)
print("Settings successfully initialized.")
}
/* ... */
func getFirstList() -> [FirstSettingsIdentifiables] {
let aText = "Mitteilungen"
let aIcon = "info.circle.fill"
let aObject = FirstSettingsIdentifiables(name: aText, icon: aIcon)
return [aObject]
}

Find if a different array has the same element and open an Alert

I know from the question it looks like something that has been already answered on this website before, but please read until the end, because I can't find the answer.
So, I have an Array that contains values of TagsModel:
import SwiftUI
import Combine
class DataManager : Equatable, Identifiable, ObservableObject {
static let shared = DataManager()
#Published var storageTags : [TagsModel] = []
typealias StorageTags = [TagsModel]
//The rest of the code
}
And the TagsModel:
import SwiftUI
import Combine
class TagsModel : Codable, Identifiable, Equatable, ObservableObject {
var id = UUID()
var tagName : String
var value : [ValueModel] = []
init(tagName: String) {
self.tagName = tagName
}
static func == (lhs: TagsModel, rhs: TagsModel) -> Bool {
return lhs.id.uuidString == rhs.id.uuidString
}
}
If you need it, the ValueModel is:
import SwiftUI
import Combine
class ValueModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var name : String
var notes : String?
var expires : Date?
init(name: String, notes: String?, expires: Date?) {
self.name = name
self.notes = notes
self.expires = expires
}
}
Now what I would like to do is let the user add the elements of type ValueModel to the array Value of each TagsModel (which in English means I would like users to be able to add values inside their belonging tags). I can do all this, but I would like to add a check: if any other TagsModel contains the value that the user is trying to add, show an Alert (since every value can have only one tag). This Alert should be asking the user whether he/she wants to add that value to the selected tag and remove it from the other one, or cancel the action.
What I managed to do up to now is this:
import SwiftUI
struct SelectValuesForTagsView: View {
#ObservedObject var dm: DataManager
var tm: TagsModel
#Binding var showSheetSelectValuesForTagsView : Bool
#State var showAlertValueAlreadyInTag = false
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#GestureState private var dragOffset = CGSize.zero
#Environment(\.colorScheme) var colorScheme
var body: some View {
NavigationView {
Form {
ForEach(dm.storageValues) { valuesOfForEach in
HStack {
if tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
Image(systemName: "checkmark.circle.fill")
.frame(width: 22, height: 22)
.foregroundColor(.green)
} else {
Image(systemName: "circle")
.frame(width: 22, height: 22)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
Button(action: {
if !tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
tm.value.append(valuesOfForEach)
dm.save()
} else {
guard let indexValue = tm.value.firstIndex(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) else { return }
tm.value.remove(at: indexValue)
dm.save()
}
}, label: {
if tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
Text(valuesOfForEach.name)
.foregroundColor(.blue)
} else {
Text(valuesOfForEach.name)
.foregroundColor(.yellow)
}
})
}
}
}
.navigationBarTitle(Text("Add Values"), displayMode: .automatic)
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing: saveButton)
}
}
var saveButton: some View {
Button(action: {
let tag : TagsModel
if dm.storageTags.contains(where: {$0.value == tm.value}) {
showAlertValueAlreadyInTag = true
//The problem is here, that the Alert ALWAYS shows up, even though there's only this Tag containing that value
}
else {
dm.save()
showSheetSelectValuesForTagsView = false
}
}, label: {
Text("Save")
.foregroundColor(.blue)
}).alert(isPresented: $showAlertValueAlreadyInTag, content: { alertValueAlreadyInTag })
}
var alertValueAlreadyInTag : Alert {
Alert(title: Text("Attention!"), message: Text("Every value can only be assigned to one tag. One or more values you selected are already into another tag. Would you like to substitute the belonging tag for these values?"), primaryButton: Alert.Button.default(Text("Yes, substitute"), action: {
dm.save()
showAlertValueAlreadyInTag = false
showSheetSelectValuesForTagsView = false
}), secondaryButton: Alert.Button.default(Text("Cancel"), action: {
showAlertValueAlreadyInTag = false
}))
}
}
How can I check if another tag inside the storageTags has the already the same value inside of its value Array?

SwiftUI - How do I create a Picker that selects values from a property?

I have these two models, the first one:
import SwiftUI
import Combine
class FolderModel : Codable, Identifiable, Equatable, ObservableObject {
var id = UUID()
var folderName : String
var values : [ValueModel] = []
init(folderName: String) {
self.folderName = folderName
}
}
And the second one:
import SwiftUI
import Combine
class ValueModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var name : String
var notes : String?
var expires : Date?
init(name: String, notes: String?, expires: Date?) {
self.name = name
self.notes = notes
self.expires = expires
}
}
And these storages:
import SwiftUI
import Combine
class DataManager : Equatable, Identifiable, ObservableObject {
static let shared = DataManager()
#Published var storageValues : [ValueModel] = []
typealias StorageValues = [ValueModel]
#Published var storageFolder : [FolderModel] = []
typealias StorageFolder = [FolderModel]
//The rest of the code
}
And then I have a Detail View of the Value, which shows all of his properties. From there, I would like to select the folder that the user wants to put it in (which in code translates to appending that value into the array "values" of the FolderModel).
To do this, I tried to create a Picker that display all the folders (by name) and that can be selected, so that when I press "Save", I can do something like "selectedFolder.append(value)". The Picker I tried to create is this:
import SwiftUI
struct DetailValueView: View {
#ObservedObject var dm : DataManager
#State private var selector = 0
#State var selectedFolder : FolderModel?
var body: some View {
Form {
Section(header: Text("Properties")) {
folderCell
if hasFolder == true {
picker
}
}
}
}
var folderCell : some View {
VStack {
Toggle(isOn: $hasFolder) {
if hasFolder == true {
VStack(alignment: .leading) {
Text("Folder: " + "//Here I would like to display the selected value")
}
} else if hasFolder == false {
Text("Folder")
}
}
}
}
var picker : some View {
Picker(selection: $selector, label: Text("Select Folder")) {
ForEach(dm.storageFolder) { foldersForEach in
Button(action: {
selectedFolder = foldersForEach
}, label: {
Text(foldersForEach.folderName)
})
}
}.pickerStyle(DefaultPickerStyle())
}
I tried to find a solution online but I don't really understand how the Picker works, I don't understand how to use that "#State private var selector = 0" to get the value that I want.
Thanks to everyone who will help me!
Two things to stress here: First, you need to either wrap your form in a NavigationView or change the picker style to WheelPickerStyle. Otherwise the picker won't work (see here for a detailed explanation). Second, your state selector is of type integer. So make sure to loop through integers as well. Now your state selector holds the index of the selected folder from the list of folders.
Please see my working example below:
struct ContentView: View {
#ObservedObject var dm: DataManager
#State private var selector = 0
#State private var hasFolder = false
var body: some View {
NavigationView {
Form {
Section(header: Text("Properties")) {
folderCell
if !dm.storageFolder.isEmpty {
picker
}
}
}
}
}
var folderCell : some View {
VStack {
Toggle(isOn: $hasFolder) {
if hasFolder == true {
VStack(alignment: .leading) {
Text("Folder: \(dm.storageFolder[selector].folderName)")
}
} else if hasFolder == false {
Text("Folder")
}
}
}
}
var picker : some View {
Picker(selection: $selector, label: Text("Select Folder")) {
ForEach(0 ..< dm.storageFolder.count) {
Text(dm.storageFolder[$0].folderName)
}
}.pickerStyle(DefaultPickerStyle())
}
}

Resources