Accessing and Manipulating Array Item in an Environmentobject

Accessing and manipulating array item in an EnvironmentObject

This approach is similar to Apple's in this tutorials.

https://developer.apple.com/tutorials/swiftui/handling-user-input


Confirm to Identifiable and Equatable.


struct Activity: Codable, Identifiable, Equatable {
var id: UUID
var name: String
// var Records: [Record]

init(Name:String) {
self.id = UUID()
self.name = Name
// self.Records = [Record]()
}
}

Iterate over activity.activities and pass your view-model and activity to ActivityListItemView

   ForEach(viewModel.activities) { activity in
HStack {
ActivityListItemView(viewModel: viewModel, activity: activity)
}
}

In ActivityListItemView, find index of its activity

private var activityIndex: Int? {
viewModel.activities.firstIndex(of: activity)
}

Unwrap activityIndex and pass $viewModel.activities[index] to ActivityDetail

var body: some View {
if let index = activityIndex {
NavigationLink(destination: ActivityDetail(activity: $viewModel.activities[index])) {
...
}
...
}
}

Use @Binding wrapper in ActivityDetail.


struct ActivityDetail: View {
@Binding var activity: Activity

var body: some View {
...
}
}

A complete working exammple.

class ActivityViewModel: ObservableObject {
@Published var activities = [Activity]()

init() {
self.activities = [Activity(Name: "A"), Activity(Name: "B"), Activity(Name: "C"), Activity(Name: "D"), Activity(Name: "E")]
}
}

struct Activity: Codable, Identifiable, Equatable {
var id: UUID
var name: String
// var Records: [Record]

init(Name:String) {
self.id = UUID()
self.name = Name
// self.Records = [Record]()
}
}

struct ActivityView: View {
@ObservedObject var viewModel = ActivityViewModel()
var body: some View {
Button(action: {
self.viewModel.activities.append(Activity(Name: "\(Date())"))
}, label: {
Text("Button")
})
ForEach(viewModel.activities) { activity in
HStack {
ActivityListItemView(viewModel: viewModel, activity: activity)
}
}

}
}

struct ActivityListItemView: View {

@ObservedObject var viewModel: ActivityViewModel

let activity: Activity

private var activityIndex: Int? {
viewModel.activities.firstIndex(of: activity)
}

var body: some View {
if let index = activityIndex {
NavigationLink(destination: ActivityDetail(activity: $viewModel.activities[index])) {
HStack {
VStack {
HStack {
Text(activity.name)
// Text("\(activity.Records.count) records")
}
}
Text(">")
}
}
.buttonStyle(PlainButtonStyle())
}
}
}

struct ActivityDetail: View {
@Binding var activity: Activity

var body: some View {
Text("\(activity.name)")
}
}

How to use @EnvironmentObject to populate and update list automatically in SwiftUI?

Have a look at the following tutorials:

How to use environmentobject to share data

Apple - handling user inputs

First You need a @Published property wrapper for your code to work.

class VenDataArray: ObservableObject {
@Published var array : [ListVenue] = [ListVenue(name: "test_name")]
}

Than adjust yout Scene delegate

var window: UIWindow?
var vDArray = VenDataArray()

let contentView = ContentView().environmentObject(vDArray)

Note: I have adjusted the variables with lowerCamelCase acc. to the API design guidelines

Environmentobject keep track of arrays variables

Make Customer a value type (ie. struct):

struct Customer: Identifiable, Codable{
var id: Int

// ... other code

how to use a @EnvironmentObject in combination with a List

If your "source of truth" is an array of some "model instances", and you just need to read values, you can pass those instance around like before:

import SwiftUI
import Combine

struct ContentView: View {
@EnvironmentObject var dm: DataManager

var body: some View {
NavigationView {
List(dm.array, id: \.self) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item)
}
}
}
}
}

struct DetailView: View {
var item : String
var body: some View {
Text(item)
}
}

class DataManager: BindableObject {
var willChange = PassthroughSubject<Void, Never>()
let array = ["Item 1", "Item 2", "Item 3"]
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(DataManager())
}
}
#endif

You need to pass the EnvironmentObject only if some views are able to manipulate the data inside the instances... in this case you can easily update the EnvironmentObject's status and everything will auto-magically updated everywhere!

The code below shows a basic App with "list", "detail" and "add", so you can see 'environment' in action (the only caveat is that you have to manually tap < Back after tapped the Save button). Try it and you'll see the list that will magically update.

import SwiftUI
import Combine

struct ContentView: View {
@EnvironmentObject var dm: DataManager

var body: some View {
NavigationView {
List {
NavigationLink(destination:AddView().environmentObject(self.dm)) {
Image(systemName: "plus.circle.fill").font(.system(size: 30))
}
ForEach(dm.array, id: \.self) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item)
}
}
}
}
}
}

struct DetailView: View {
var item : String
var body: some View {
Text(item)
}
}

struct AddView: View {
@EnvironmentObject var dm: DataManager
@State var item : String = "" // needed by TextField
var body: some View {
VStack {
TextField("Write something", text: $item)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Button(action: {
self.dm.array.append(self.item)
}) {
Text("Save")
}
}
}
}

class DataManager: BindableObject {
var willChange = PassthroughSubject<Void, Never>()
var array : [String] = ["Item 1", "Item 2", "Item 3"] {
didSet {
willChange.send()
}
}
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(DataManager())
}
}
#endif

Using ForEach(data:,content:) with an @EnvironmentObject in SwiftUI without resorting to trailing closure syntax?

Use init with without argumentLabel

struct CellView: View {
@EnvironmentObject var environment: ViewModel

private var cellModel: CellModel

init(_ cellModel: CellModel) {
self.cellModel = cellModel
}

var body: some View {
Text(cellModel.string)
}
}

struct DemoView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(viewModel.cellModels, content: CellView.init)
}
.environmentObject(viewModel)
}
}

Canvas preview doesn't show when accessing environmentObject containing array of objects

The issue is definitely here

func indexOfPlayerInScores(player: Player) -> Int {
return playerScores.firstIndex(where: {$0.player.id == player.id})!
}

you Player does not have id in constructor, so it is autogenerated, so above method just does not find matches in run-time, because created player in preview and added by-default players in Game are different (independently of type, class or struct).

The solution: to change logic of detecting equality of Players (maybe confirming explicitly to Equatable).

Pass EnvironmentObject array value to children in iOS 13

Once you have an object in the environment of your main app you can use an @EnvironmentObject variable to get a read/write reference in any view.

To pass a single value from this object to a second child view you can use @Binding. That is read/write so your child views can also modify the environment object without having a reference to the parent directly.

I built out the code below (note I'm using the new iOS14 App to load in the env object but using the older SceneDelegate will work as well. Pretty sure the below ForEach child view logic below is what you were asking...

enum FilterButtonStatus {
case off, on
}

class LateralMenu: ObservableObject {
@Published var status: [FilterButtonStatus] = [.off, .off, .off, .off, .off]
}

@main
struct StackRepApp: App {
@ObservedObject var lateralMenu:LateralMenu = LateralMenu() // declare at App level

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(lateralMenu) // append to first SwiftUI view environment
}
}
}

struct ContentView: View {
@EnvironmentObject var lateralMenu:LateralMenu // retrieve env using @EnvironmentObject

var body: some View {
VStack {
// loop passing each button status to child view
ForEach(0 ..< lateralMenu.status.indices.count) { idx in
ChildView(status: $lateralMenu.status[idx])
}
}
.onAppear(perform: {
// example of changing values on env object
// @envobject is read/write reference
lateralMenu.status[1] = .on
lateralMenu.status[3] = .on
})
}
}

struct ChildView: View {
@Binding var status:FilterButtonStatus // binding lets child view change value

var body: some View {
Button(action:{status = status == .off ? .on : .off}) {
Text("BUTTON")
.foregroundColor(.white)
.padding(20)
.background(status == .on ? Color.green : Color.gray)
}
}
}

Sample Image

Iterating an array on an @EnvironmentObject when the application closes on macOS

Here is possible approach - to inject data model on ContentView appear, like

#if os(macOS)
class AppDelegate: NSObject, NSApplicationDelegate {

var dataModel: DataModel? // << here

func applicationWillTerminate(_ aNotification: Notification) {
print("app closing")

// use self.dataModel? here
}
}
#endif

@main
struct My_AppApp: App {

#if os(macOS)
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif

private let dataModel = DataModel() // << here !!

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel) // << here !!
.onAppear {
#if os(macOS)
appDelegate.dataModel = self.dataModel // << here !!
#endif
}
}
}
}


Related Topics



Leave a reply



Submit