Swift - Cannot append Object to Array - arrays

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

Related

Create array of attributes values from fetch entity of CoreData [duplicate]

struct PickerView: View {
var items0 : [PageName]
#State var selectedPage: PageName?
init(items0: [PageName]) {
self.items0 = items0
self._selectedPage = State(initialValue: items0.first)
}
var body: some View {
Picker(selection: $selectedPage, label: Text("Page")) {
ForEach(items0) { item in
Text(item.pageName ?? "").tag(item as PageName?)
}
}
Text("\((selectedPage?.pageName)!)")
}
NavigationView {
Form {
PickerView(items0: Array(items0))
}
.foregroundColor(.blue)
.background(Color.yellow)
}
I can successfully pick the value of Core Data in Picker, but I don't know how can I use this value in my main View...
Some much things I already tried, but that Core Data drives me just crazy.
Can someone help me that I can use the picked value of the picker with Core Data in SwiftUI?
I misunderstood your issue with my original comment see below. There is a lot of info in the comments.
import SwiftUI
struct ParentPickerView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \PageName.pageName, ascending: true)],
animation: .default)
private var items0: FetchedResults<PageName>
//State is a source of truth
#State var selectedPage: PageName? = nil
var body: some View {
NavigationView {
Form {
Text(selectedPage?.pageName ?? "not selected")
PickerView(items0: Array(items0), selectedPage: $selectedPage)
}
.foregroundColor(.blue)
.background(Color.yellow)
}
}
}
struct PickerView: View {
//If you don't need all your PageName in your parentView it would be best to have the fetch here
// #Environment(\.managedObjectContext) private var viewContext
// #FetchRequest(
// sortDescriptors: [NSSortDescriptor(keyPath: \PageName.pageName, ascending: true)],
// animation: .default)
// private var items0: FetchedResults<PageName>
var items0: [PageName]
//Binding is a two-way connection
#Binding var selectedPage: PageName?
//You don't need custom init in SwiftUI because they are struct
// init(items0: [PageName], selectedPage: Binding<PageName?>) {
// self.items0 = items0
// self._selectedPage = selectedPage
//
// //#State Init here is bad practice as you are experiencing it becomes a dead end
// //self._selectedPage = State(initialValue: items0.first)
// }
var body: some View {
Picker(selection: $selectedPage, label: Text("Page")) {
ForEach(items0) { item in
Text(item.pageName ?? "").tag(item as PageName?)
}
}
Text("\((selectedPage?.pageName ?? "not selected"))")
.onAppear(){
//Set your initial item here only if the selected item is nil
if selectedPage == nil{
selectedPage = items0.first
}
}
}
}

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

How to unite two different structures into one array and sort it? Swift

I'm building a feed, where I want to unite two different structures and sort the feed by date, here's how I've tried. It shows mistakes in View. It writes either "compiler unable to type check in reasonable time", either something else that connects to the ForEach loop.
Maybe you have an idea what can be an issue? Or, if there's other way to build and sort the feed?
EDIT: The following error: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
class FeedViewModel: ObservableObject {
#Published var feed: [FeedCommonContent] = []
// download(ed) articles
self.feed.append(Article(id: doc.document.documentID, title: title, pic: pic, time: time.dateValue(), user: user))
// download(ed) tutorials
self.feed.append(Tutorial(id: doc.document.documentID, title: title, steps: [steps], notes: notes, pic: pic, time: time.dateValue(), user: user))
// sort the feed
self.feed.sort { (p1, p2) -> Bool in
return p1.time > p2.time
}
}
protocol FeedCommonContent {
// Define anything in common between objects
var time: Date { get set }
var id: String { get set }
var feedIdentity: FeedIdentity.RawValue { get } // Article, or Tutorial; or - { get set }
}
enum FeedIdentity: String {
// case Article, Tutorial
case Article = "Article"
case Tutorial = "Tutorial"
}
struct Article: Identifiable, FeedCommonContent {
var feedIdentity = FeedIdentity.Article.rawValue
var id: String = UUID().uuidString
var title: String
var pic: String
var time: Date
var user: User
}
struct Tutorial: Identifiable, FeedCommonContent {
var feedIdentity = FeedIdentity.Tutorial.rawValue
var id: String = UUID().uuidString
var titleImage: String
var name: String
var user: User
var inventory: [String]
var steps: [String]
var notes: String
var time: Date
var warnings: String
}
struct FeedView: View {
#StateObject var feedData = FeedViewModel()
var body: some View {
ScrollView {
ForEach(feedData.feed, id: \.self.id) { item in
// also tried type checking:
// if item is Article // or, if let article = item as? Article
if item.feedIdentity == FeedIdentity.Article.rawValue {
NavigationLink(destination: ArticleView(article: item, articleData: feedData)) {
ArticleUnitView(article: item, feedData: feedData)
}
} else if item.feedIdentity == FeedIdentity.Tutorial.rawValue { // if item is Tutorial
NavigationLink(destination: TutorialView(tutorial: item, feedData: feedData)) {
TutorialUnitView(tutorial: item, feedData: feedData)
}
}
}
}
When I did an array only of one of data structures, all worked. (i.e. either var articles: [Article] = [], or var tutorials: [Tutorial] = [] etc..)
this is the (test) code I used to show how to "sort" your feed by date, and display it in a View:
EDIT, using "Joakim Danielson" enum suggestion:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
FeedView()
}
}
}
class FeedViewModel: ObservableObject {
#Published var feed: [FeedCommonContent] = []
// for testing
init() {
self.feed.append(Article(title: "article title", pic: "pic1", time: Date(), user: User()))
self.feed.append(Tutorial(titleImage: "title1", name: "tut1", user: User(), inventory: [], steps: [], notes: "note1", time: Date(), warnings: ""))
sortFeed()
}
func sortFeed() {
feed.sort { $0.time > $1.time }
}
}
protocol FeedCommonContent {
// Define anything in common between objects
var time: Date { get set }
var id: String { get set }
var feedIdentity: FeedIdentity { get } // Article, or Tutorial; or - { get set }
}
enum FeedIdentity: String {
// case Article, Tutorial
case Article
case Tutorial
}
struct User: Identifiable {
var id: String = UUID().uuidString
var name: String = "user"
}
struct Article: Identifiable, FeedCommonContent {
let feedIdentity = FeedIdentity.Article
var id: String = UUID().uuidString
var title: String
var pic: String
var time: Date
var user: User
}
struct Tutorial: Identifiable, FeedCommonContent {
let feedIdentity = FeedIdentity.Tutorial
var id: String = UUID().uuidString
var titleImage: String
var name: String
var user: User
var inventory: [String]
var steps: [String]
var notes: String
var time: Date
var warnings: String
}
struct FeedView: View {
#StateObject var feedData = FeedViewModel()
var body: some View {
ScrollView {
ForEach(feedData.feed, id: \.id) { item in
// switch item.feedIdentity {
// case FeedIdentity.Article:
// NavigationLink(destination: ArticleView(article: item, articleData: feedData)) {
// ArticleUnitView(article: item, feedData: feedData)
// }
// case FeedIdentity.Tutorial:
// NavigationLink(destination: TutorialView(tutorial: item, feedData: feedData)) {
// TutorialUnitView(tutorial: item, feedData: feedData)
// }
// }
// for testing
switch item.feedIdentity {
case FeedIdentity.Article:
NavigationLink(destination: Text(item.feedIdentity.rawValue + " " + item.id)) {
Text(item.feedIdentity.rawValue)
}
case FeedIdentity.Tutorial:
NavigationLink(destination: Text(item.feedIdentity.rawValue + " " + item.id)) {
Text(item.feedIdentity.rawValue)
}
}
}
}
}

SwiftUI MVVM how to loop through data and store them inside array

I am using MVVM in my SwiftUI project and after I request to fetch data from the API, I want to loop through the data and store some of them inside an array, however it returns error, what is the correct method to do this?
Here is my code and data struct
MenuDetailView
struct MenuDetailView: View {
#ObservedObject var viewModel = MenuDetailViewModel()
var body: some View {
ForEach(self.viewModel.variantGroup, id: \.self) { vg in
/*** I Need to loop and store vg.variantGroupName into array viewModel.variantChosen, how? ***/
self.viewModel.variantChosen.append(vg.VariantGroupName)
// This always return error:
// Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
VStack {
HStack {
Text(vg.variantGroupName)
Text(String(self.viewModel.arrVariantChoosen[0]))
}
VStack {
ForEach(vg.variant, id: \.self) { v in
Text(v.variantName)
}
}
}
}
}
}
}
MenuDetailViewModel.swift
class MenuDetailViewModel: ObservableObject, MenuDetailService {
var apiSession: APIService
#Published var detaildata: MenuDetailData?
#Published var variantGroup = [MenuDetailVariantGroup]()
#Published var variantChosen: Array<String> = []
var cancellables = Set<AnyCancellable>()
init(apiSession: APIService = APISession()) {
self.apiSession = apiSession
}
func getMenuDetail() {
let cancellable = self.getMenuDetail()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error: \(error)")
case .finished:
break
}
}) { (detail) in
self.detaildata = detail.data
self.variantGroup = detail.data.variantGroup
}
cancellables.insert(cancellable)
}
}
MenuDetailData.swift
struct MenuDetailData: Codable, Identifiable {
let id = UUID()
let idMenu: String
let menuName: String
let variantGroup: [MenuDetailVariantGroup]
}
MenuDetailVariantGroup.swift
struct MenuDetailVariantGroup: Codable, Identifiable, Hashable {
let id = UUID()
let variantGroupName: String
let variant: [MenuDetailVariant]
let limit: Int
}
MenuDetailVariant.swift
struct MenuDetailVariant: Codable, Identifiable, Hashable {
let id = UUID()
let variantName: String
}
Thank you all in advance
You can not add this inside the ForEach. In SwiftUI, ForEach is a view. It accepts view data, it's not the same as Array.forEach.
You need to do it inside the view model. Like this
func getMenuDetail() {
let cancellable = self.getMenuDetail()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error: \(error)")
case .finished:
break
}
}) { (detail) in
self.detaildata = detail.data
self.variantGroup = detail.data.variantGroup
self.variantChosen = self.variantGroup.map{$0.variantGroupName} //<--here
}
cancellables.insert(cancellable)
}
Remove this from ForEach
self.viewModel.variantChosen.append(vg.VariantGroupName)

How do I make the Observable Object update the list?

So I know my items are being added to the 'vitallist'(through printing the list in the terminal), but I am not seeing them appear on list view. I think it has something to do with the 'ObservedObject' not being linked correctly. Any suggestions?
struct Vital: Identifiable {
let id = UUID()
var name: String
}
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
}
struct Row: View {
var vital: Vital
#State var completed:Bool = false
var body: some View {
HStack{
Image(systemName: completed ? "checkmark.circle.fill" : "circle").onTapGesture {
self.completed.toggle()
}
Text(vital.name)
}
}
}
struct Lists: View {
#ObservedObject var vitallist = VitalList()
var body: some View {
NavigationView{
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
I also had same problem.
I am not sure why, but it works that creating a new element in the array, not changing the element itself. I confirmed directly updating works only in data, but not for binding UI.
In my code, element change in TobuyData class.
class Tobuy: Identifiable {
let id = UUID()
var thing: String
var isDone = false
init(_ thing: String, isDone: Bool = false) {
self.thing = thing
self.isDone = isDone
}
}
class TobuyData: ObservableObject {
#Published var tobuys: [Tobuy]
init() {
self.tobuys = [
Tobuy("banana"),
Tobuy("bread"),
Tobuy("pencil"),
]
}
func toggleDone(_ tobuy: Tobuy) {
if let j = self.tobuys.firstIndex(where: { $0.id == tobuy.id }) {
self.tobuys[j] = Tobuy(self.tobuys[j].thing, isDone: !self.tobuys[j].isDone)
// self.tobuys[j].isDone.toggle() // this works only in data, but not for binding UI
}
}
}
In View
struct ContentView: View {
#EnvironmentObject var tobuyData: TobuyData
var body: some View {
List {
ForEach(tobuyData.tobuys) { tobuy in
Text(tobuy.thing)
.strikethrough(tobuy.isDone)
.onTapGesture { self.tobuyData.toggleDone(tobuy) }
...
p.s.
Changing Tobuy Class to Struct made direct element updating work, the comment out part above. This referenced to Apple's official tutorial: "Handling User Input"
change
#ObservedObject var vitallist = VitalList()
to
#EnvironmentObject var vitallist = VitalList()
The code seems fine. I added a simple add method to VitalList
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
func addVital(){
self.vitallist.append(Vital(name: UUID().description))
}
}
And a Button to the body
var body: some View {
NavigationView{
VStack{
Button(action: {self.vitallist.addVital()}, label: {Text("add-vital")})
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
The list updates as expected. check your code that adds your items to
#Published var vitallist = [Vital]()
Are you using the same instance of VitalList? A singleton might help.
https://developer.apple.com/documentation/swift/cocoa_design_patterns/managing_a_shared_resource_using_a_singleton

Resources