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)
}
}
}
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
Is There Difference Between Using a Constructor and Using .Init
Swift Function Compiler Error 'Missing Return'
Disable Email Detection in Swiftui's Text
My Button Is Centered for iPhone 6 and 6 Plus, But Not for iPhone 5
Bulk Fix Hundreds of "#Selector Not Exposed to Objective-C" Errors in Xcode 9 or 9.1
Swift Updating Screen in Between Steps of While Loop
Access Custom Object Property While Iterating Over Dictionary
Using @Viewbuilder to Create Views Which Support Multiple Children
Why Do We Need to Specify Init Method
Datefromstring() Returns Incorrect Date
Cannot Stop Background Music from Within Game Scenes, Swift 3/Spritekit
Why Does Classa Adopting Protocolb Not Satisfy the Protocolb Requirement
New Value Is Only Available in Sendasynchronousrequest - Swift
Raw Value of Enumeration, Default Value of a Class/Structure, What's the Different