2

A background to the issue: I am creating a simple app which stores activity data in a struct. I have created a custom view called "CardView" (held in a separate file within the project) which is to appear onto a basic ContentView. The CardView is populated with Text labels which are to be bound to corresponding data within the aforementioned activity data struct. A ForEach handles the dynamic creation of these cards according to how many activity "items" exist. See following code...

struct ActivityItem: Identifiable, Codable {
let id = UUID()
let name: String
let description: String
let type: String
var amount: Int
}

class AppData: ObservableObject {
@Published var activites: [ActivityItem] {
    didSet {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(activites) {
            UserDefaults.standard.set(encoded, forKey: "Activites")
        }
    }
}

init() {
    if let foundActivities = UserDefaults.standard.data(forKey: "Activites") {
        let decoder = JSONDecoder()
        if let decoded = try? decoder.decode([ActivityItem].self, from: foundActivities) {
            self.activites = decoded
            return
        }
    }

    self.activites = []
}
}

struct ContentView: View {
@ObservedObject var appData = AppData()
@State private var showingAddActivity = false
@State private var showingMoreInfo = false

var body: some View {
    NavigationView {
        ZStack {
            Rectangle()
            .fill(Color(.gray))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
                .edgesIgnoringSafeArea(.all)
            
            ScrollView {
                VStack(spacing: 50) {
                    ForEach(appData.activites) { activity in
                        CardView(appData: self.appData)
                    }
                }
            }

// Below held in separate file
struct CardView: View {
@ObservedObject var appData: AppData
@State private var showingMoreInfo = false

var body: some View {
    ZStack {
        Rectangle()
            .fill(Color(.gray))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .edgesIgnoringSafeArea(.all)
        RoundedRectangle(cornerRadius: 20)
            .fill(Color(.black))
            .frame(width: 325, height: 180)
        VStack {
            Text("Name")
                .font(.title)
            
            Text("Description")

            Text("Type")
            
            Text("Amount")                
        }
        .sheet(isPresented: $showingMoreInfo) {
            Text("View")
        }
    }
}
}

The issue: Despite multiple attempts and countless hours and errors later through using @Binding and leveraging @Environmental, I cannot pass data from the current ActivityItem in the ForEach into the CardView to be used to replace the String placeholder in the Text labels (i.e. Name, Type, Amount). When trying to create and use bindings, such as something like $activity.name, it declares that "activity" cannot become a binding. What am I doing wrong? How, in the simplest way, can I get each activity to pass its data to the CardView struct, populate those Text labels and then create itself on the ContentView "main screen" and then repeat for all remaining objects in the ForEach iteration? Is there any important notes to keep in mind to avoid issues like this in the future? I must add that if I put the CardView code directly into ContentView and not call CardView() in the ForEach, I can easily refer to the data; it only becomes an issue when I use the CardView struct (a long term goal for reusability and less code clutter). Thank you for your help!

1
  • Based on your CardView, since it's only displaying - not modifying - the values, it doesn't need a binding. Just create a property let activity: ActivityItem, and pass it in init: CardView(appData: self.appData, activity: activity). Do you even need appData inside CardView? If not, remove that, and it becomes CardView(activity: activity) Commented Aug 8, 2020 at 5:36

2 Answers 2

0

I assume you meant something like the following (tested with Xcode 12 / iOS 14)

struct ActivityItem: Identifiable, Codable {
var id = UUID()
var name: String            // << made `var` here and other to allow write
var description: String
var type: String
var amount: Int
}

class AppData: ObservableObject {
@Published var activites: [ActivityItem] {
    didSet {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(activites) {
            UserDefaults.standard.set(encoded, forKey: "Activites")
        }
    }
}

init() {
    if let foundActivities = UserDefaults.standard.data(forKey: "Activites") {
        let decoder = JSONDecoder()
        if let decoded = try? decoder.decode([ActivityItem].self, from: foundActivities) {
            self.activites = decoded
            return
        }
    }

    self.activites = []
}
}

struct ContentView: View {
@ObservedObject var appData = AppData()
@State private var showingAddActivity = false
@State private var showingMoreInfo = false

var body: some View {
    NavigationView {
        ZStack {
            Rectangle()
            .fill(Color(.gray))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
                .edgesIgnoringSafeArea(.all)

            ScrollView {
                VStack(spacing: 50) {
                    ForEach(appData.activites.indices, id: \.self) { i in
                        CardView(item: $appData.activites[i])  // binding here  
                    }
                }
            }
        }
    }
}
}

// Below held in separate file
struct CardView: View {
@Binding var item: ActivityItem             // binding here
@State private var showingMoreInfo = false

var body: some View {
    ZStack {
        Rectangle()
            .fill(Color(.gray))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .edgesIgnoringSafeArea(.all)
        RoundedRectangle(cornerRadius: 20)
            .fill(Color(.black))
            .frame(width: 325, height: 180)
        VStack {
            TextField("Name", text: $item.name)    // edit here
                .font(.title)

            Text("Description")

            Text("Type")

            Text("Amount")
        }
        .sheet(isPresented: $showingMoreInfo) {
            Text("View")
        }
    }
}
}
Sign up to request clarification or add additional context in comments.

2 Comments

I see, let me try this. As a quick follow up, how would both preview structs be handled in this case? For ContentView() and CardView() respectively. For CardView, it is suggesting "item: <#Binding<ActivityItem>#>" What would that be? "item: activity" despite putting a variable in above yields nothing.
And lastly, why does this work (trying to understand Bindings better here)? Why wouldn't passing bindings such as name: $activity.name, type: $activity.type (where in CardView view I'd set up the Bindings and I'd just call "name", "type" etc.) work in the original version of the code? Thank you!
0

Swift 5.5 offers bindable collections, which allow the following implementation.

struct ActivityItem: Identifiable, Codable {
    var id = UUID()
    var name: String
    var description: String
    var type: String
    var amount: Int
}

class AppData: ObservableObject {
    @Published var activites: [ActivityItem] {
        didSet {
            let encoder = JSONEncoder()
            if let encoded = try? encoder.encode(activites) {
                UserDefaults.standard.set(encoded, forKey: "Activites")
            }
        }
    }
    
    init() {
        if let foundActivities = UserDefaults.standard.data(forKey: "Activites") {
            let decoder = JSONDecoder()
            if let decoded = try? decoder.decode([ActivityItem].self, from: foundActivities) {
                self.activites = decoded
                return
            }
        }
        // sample data
        self.activites = [ActivityItem(name: "Name1", description: "desc1", type: "type1", amount: 1),
                          ActivityItem(name: "Name2", description: "desc2", type: "type2", amount: 1)
        ]
    }
}

struct ContentView: View {
    @ObservedObject var appData = AppData()
    @State private var showingAddActivity = false
    @State private var showingMoreInfo = false
    
    var body: some View {
        NavigationView {
            ZStack {
                Rectangle()
                    .fill(Color(.gray))
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .edgesIgnoringSafeArea(.all)
                
                ScrollView {
                    VStack(spacing: 50) {
                        ForEach($appData.activites) { $activity in
                            CardView(item: $activity)
                        }
                    }
                }
            }
            
        }
    }
}

// Below held in separate file
struct CardView: View {
    @Binding var item: ActivityItem
    @State private var showingMoreInfo = false
    
    var body: some View {
        ZStack {
            Rectangle()
                .fill(Color(.gray))
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .edgesIgnoringSafeArea(.all)
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.yellow)
                .frame(width: 325, height: 180)
            VStack {
                TextField("Name", text: $item.name)    // edit here
                    .font(.title)
                    .multilineTextAlignment(.center)
                    .textFieldStyle(.roundedBorder)
                Text("Description \(item.description)")
                Text("Type \(item.type)")
                Text("Amount \(item.amount)")
            }
            .sheet(isPresented: $showingMoreInfo) {
                Text("View")
            }
        }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.