Home > Mobile >  SwiftUI: How can I update an array of [struct]'s variable with a @Binding?
SwiftUI: How can I update an array of [struct]'s variable with a @Binding?

Time:08-19

To preface, I'm nearly one month into learning SwiftUI and I've been watching a few YouTube channels to learn (e.g. Swiftful Thinking (Paul Hudson), Hacking with Swift (Nick Sarno), Sean Allen, etc.).

I'm testing something in Playgrounds and I need to implement a @Binding variable and, as such, I've watched all three of the aforementioned channel's tutorials on @Binding. The tutorials are explained clearly and I understand the concept, but I still seem to be running into a wall

I have a struct which contains a View — named TactileTrigger — that creates a draggable Circle(). I want to send the coordinates of this drag to another struct which contains a View — named CreateNewTrigger — that actually creates new instances of the TactileTrigger struct based on an array of information.

From within the CreateNewTrigger struct, I receive the error message: Cannot use instance member 'bindingXY' within property initializer; property initializers run before 'self' is available.

I've searched StackOverflow and have seen this same error, and I did try to implement an init() within the struct but, apparently, I'm still doing something wrong. Therefore, I have removed the init().

To clarify, I need the [TrigInformation]'s XY value updated for each respective $binding. I made a mockup in SwiftUI as an example of what I'm after:

enter image description here

CreateNewTactileTrigger:

import SwiftUI

class NotesManager: ObservableObject {
    @Published var manager: [TrigInformation] = [
        TrigInformation(trigNumber: 1,
                        trigType: .note,
                        noteValue: .Db, 
                        XY: //<-- NEEDS TO APPEND HERE
                       ),
        TrigInformation(trigNumber: 2,
                        trigType: .note,
                        noteValue: .C,
                        XY: //<-- NEEDS TO APPEND HERE
                       ),
        TrigInformation(trigNumber: 3,
                        trigType: .note,
                        noteValue: .Eb,
                        XY: //<-- NEEDS TO APPEND HERE
                       ),
        TrigInformation(trigNumber: 4,
                        trigType: .trigger,
                        XY: //<-- NEEDS TO APPEND HERE
                       )
    ]
}

struct CreateNewTactileTrigger: View {
    @StateObject var notesManager = NotesManager()
    
    var body: some View {
        VStack {
            ForEach($notesManager.manager) { $note in
                TactileTrigger(label: "\(note.trigNumber.description): [\(note.noteValue?.rawValue ?? "T")]",
                               bindingXY: $note.XY)
                .frame(width: 25, height: 25)
                .onAppear { 
//                    notesManager.manager.append(
//                        TrigInformation(trigNumber: note.trigNumber,
//                                        trigType: note.trigType,.        <-- SOMETHING LIKE THIS
//                                        noteValue: note.noteValue,
//                                        XY: note.XY)
//                    )
                }
                
                VStack {
                    Text("\(note.trigNumber)")
                    Text("\(note.trigType.rawValue)")
                    Text("\(note.noteValue?.rawValue ?? "—")")
                    Text("X: \(note.XY.x)")
                    Text("Y: \(note.XY.y)")
                }
                .font(.caption)
                .foregroundColor(.white)
                .offset(x: 25,
                        y: 25)
            }
        }
    }
}

struct TrigInformation: Identifiable {
    let id          = UUID()
    var trigNumber:   Int
    var trigType:     TrigType
    var noteValue:    Notes?
    var XY:           CGPoint
}

enum TrigType: String {
    case trigger
    case note
}

enum Notes: String {
    case Ab = "Ab"
    case A  = "A"
    case Bb = "Bb"
    case B  = "B"
    case C  = "C"
    case Db = "Db"
    case D  = "D"
    case Eb = "Eb"
    case E  = "E"
    case F  = "F"
    case Gb = "Gb"
    case G  = "G"
}

Tactile Trigger:

import SwiftUI

struct TactileTrigger: View {
    @State var label:       String  = ""
    @State var setLocation: CGPoint = CGPoint(x: 100,
                                              y: 100)
    @Binding var bindingXY: CGPoint
    
    var body: some View {
        ZStack {
            Circle()
                .fill(.blue)
                .overlay(
                    Text("\(label)").bold()
                        .font(.subheadline)
                        .foregroundColor(.white)
                )
                .frame(width: 75,
                       height: 75)
            
                .position(x: setLocation.x,
                          y: setLocation.y)
                .gesture(
                    DragGesture()
                        .onChanged({ currentPosition in
                            calculateDrag(value: currentPosition)
                        })
                    
                        .onEnded({ endPosition in
                            calculateDrag(value: endPosition)
                        })
                )
        }
    }
    
    func calculateDrag(value: DragGesture.Value) {
        let coordinates = CGPoint(x: value.location.x,
                                  y: value.location.y)
        
        setLocation     = CGPoint(x: coordinates.x,
                                  y: coordinates.y)
        
        // BINDING VARIABLE
        bindingXY       = setLocation
    }
}

MyApp:

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            CreateNewTactileTrigger()
        }
    }
}

CodePudding user response:

At first every state must be initialized (you have uninitialised so error), at second you try to bind all notes to one state (even if you'd fix first error, you would got wrong behavior). You need own binding for each note in ForEach.

But better to bind directly to model.

So,

struct CreateNewTactileTrigger: View {
//    @State var bindingXY: CGPoint    // << remove !!
NoteInformation(trigNumber: 1,
                trigType: .note,
                noteValue: .Db,
                XY: .zero        // << initialize !!
               ),
ForEach($notes) { $note in    // << work with bindings
    TactileTrigger(label: "\(note.trigNumber.description): [\(note.noteValue?.rawValue ?? "T")]",
                   bindingXY: $note.XY) // << bind directly

no more changes, tested with Xcode 13.4 / iOS 15.5

enter image description here

CodePudding user response:

I've rekindled my deleted answer and updated it to cater for your new question about an array of [TrigInformation].

Try this approach (works well for me), where you have a class NotesManager: ObservableObject, that holds your array of [TrigInformation]. As the trigArray element changes, the ObservableObject will be notified and the UI refreshed with the new XY positions.

// -- here
class NotesManager: ObservableObject {
    
    @Published var trigArray: [TrigInformation] = [
        TrigInformation(trigNumber: 1,trigType: .note,noteValue: .Db, XY: CGPoint(x: 123, y: 123)),
        TrigInformation(trigNumber: 2,trigType: .note,noteValue: .C,XY: CGPoint(x: 456, y: 456)),
        TrigInformation(trigNumber: 3,trigType: .note,noteValue: .Eb,XY: CGPoint(x: 789, y: 789)),
        TrigInformation(trigNumber: 4,trigType: .trigger, XY: CGPoint(x: 101, y: 101))
    ]
    
}

struct CreateNewTactileTrigger: View {
    @StateObject var notesManager = NotesManager()  // <-- here
    
    var body: some View {
        VStack {
            ForEach($notesManager.trigArray) { $note in  // <-- here
                TactileTrigger(label: "\(note.trigNumber.description): [\(note.noteValue?.rawValue ?? "T")]",
                               bindingXY: $note.XY)
                .frame(width: 25, height: 25)
                
                VStack {
                    Text("\(note.trigNumber)")
                    Text("\(note.trigType.rawValue)")
                    Text("\(note.noteValue?.rawValue ?? "—")")
                    Text("X: \(note.XY.x)")
                    Text("Y: \(note.XY.y)")
                }
                .font(.caption)
                .foregroundColor(.red)  // <-- here for testing
                .offset(x: 25,  y: 25)
            }
        }
    }
}
  • Related