Search code examples
swiftuimvvmcore-location

@Published variables do not carry over as @Binding variables from Model to View to DetailView


I am trying to pass shotCoord, ballCoord, and waiting from LocationManagerModel to ContentView, and then to the DetailView of items on a list. I need these three variables to maintain their values when returning to the ContentView and selecting another item from the list. Waiting is used to control the state of the DetailView and works perfectly. ballCoord and shotCoord do not persist when returning to the `ContentView` and are reset to nil.

import SwiftUI
import CoreLocation


class LocationManagerModel : NSObject, ObservableObject {
    
    enum LocationMode {
        case ball, shot, putt 
        //addNewShot(distance: Double, shotClub: Club)
    }
    
    @Environment(\.managedObjectContext) private var viewContext
    //@ObservedObject var club: Club
    @Published var shotCoord : CLLocation?
    @Published var ballCoord : CLLocation?
    @Published var waiting = false
    @Published var errorMessage = ""
    @Published var showError = false
    @Published var distance: Double?
    //@Published var shotClub: Club
    private let locationManager = CLLocationManager()
    private var mode : LocationMode = .ball
    
    public override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
    }
    
    public func currentLocation(mode: LocationMode) {
        self.mode = mode
        locationManager.requestLocation()
    }
}

extension LocationManagerModel : CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        errorMessage = error.localizedDescription
        showError = true
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        switch mode {
        case .ball:
            ballCoord = locations.last!
            //distance = ballCoord?.distance(from: shotCoord)
            
            print("shot:\(shotCoord?.coordinate as Any)")
            print("ball:\(ballCoord?.coordinate as Any)")
            //print(distance!)
            //addNewShot(distance: distance!, shotClub: Club)
            
        case .shot:
            shotCoord = locations.last!
            print("shot: \(shotCoord?.coordinate as Any)")
            
        case .putt:
            ballCoord = locations.last!
            waiting = false
        }
    }
    
    func addNewShot(distance: Double, shotClub: Club) -> Void {
        let club = shotClub
        let distanceYards = lround(distance * 1.09361)
        //let avgYards = club.yardsNum * club.strokes
        //club.strokes += 1
        club.strokesList.append(distanceYards)
        club.strokes = club.strokesList.count
        let sumArray = club.strokesList.reduce(0,+)
        club.yardsNum = sumArray / club.strokes
        
        do {
            try viewContext.save()
        } catch {
            print("error")
        }
    }
    
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch manager.authorizationStatus {
        case .authorizedAlways:
            return
            //print("authorizedAlways")
        case .authorizedWhenInUse:
            return
            //print("authorizedWhenInUse")
        case .denied, .restricted: print("denied")
            // show the error to the user
        case .notDetermined: print("notDetermined")
            locationManager.requestWhenInUseAuthorization()
        @unknown default: print("This should never appear")
        }
    }
}
struct ClubDetailView: View {
    
    @ObservedObject var club: Club
    @Environment(\.managedObjectContext) private var viewContext
    @StateObject private var locationManager = LocationManagerModel()
    @State var newShot: Int = 0
    @Binding var waiting: Bool
    @Binding var shotCoord: CLLocation?
    @Binding var ballCoord: CLLocation?
    @Binding var shotClub: Club
    var body: some View{
        //let waiting = locationManager.waiting
        //let shotList = self.club.strokesList
        List {
            if self.club.putter == false {
                Section {
                    Text(self.club.name)
                    Text("Average distance: \(club.yardsNum) yards")
                    Text("\(club.strokes) Strokes Counted")
                }
                
                //ForEach(shotList) {shots in club.strokesList}
            } else {Text("This app is not designed to track putting data at this time.")}
        }
        
        
        if waiting == false && club.putter == false {
            Button(action: {locationManager.currentLocation(mode: .shot)
                waiting = true
                shotClub = club
            },label: {
                Text("Swing Location")
                    .foregroundColor(.white)
                    .font(.system(.title, design: .rounded, weight: .bold))
                    .frame(maxWidth: .infinity)
            })
            .buttonStyle(.borderedProminent)
        }
        
        if waiting == true && club.putter == false{
            Button(action: {locationManager.currentLocation(mode: .ball)
                //ballCoord = getBallLocation()
                //let distanceMeters =
                //                let distanceYards = lround(distanceMeters * 1.09361)
                //addNewShot(newShot: Int(distanceYards), shotClub: shotClub)
                
                print("shot: \(shotCoord?.coordinate as Any)")
                print(shotClub.name)
                shotClub = club
                //print(locationManager.shotCoord!)
                if shotClub.putter == true {
                    waiting = false
                }
            }, label: {
                Text("Ball Location")
                    .foregroundColor(.white)
                    .font(.system(.title, design: .rounded, weight: .bold))
                    .frame(maxWidth: .infinity)
            })
            .buttonStyle(.borderedProminent)
        }
        if waiting == true && club.putter == true{
            Button(action: {
                //ballCoord = getBallLocation()
                //let distanceMeters = ballCoord.distance(from: shotCoord)
                //let distanceYards = lround(distanceMeters * 1.09361)
                //addNewShot(newShot: Int(distanceYards), shotClub: shotClub)
                //print(shotClub.strokesList)
                shotClub = club
                if shotClub.putter == true {
                    waiting = false
                }
            }, label: {
                Text("Start Putting")
                    .foregroundColor(.white)
                    .font(.system(.title, design: .rounded, weight: .bold))
                    .frame(maxWidth: .infinity)
            })
            .buttonStyle(.borderedProminent)
        }
    }
}
import SwiftUI
import CoreLocation

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(
        entity: Club.entity(),
        sortDescriptors:[
                          NSSortDescriptor(keyPath: \Club.yardsNum, ascending: false)],
        animation: .default)
    private var clubs:FetchedResults<Club>
    //@State private var waiting = false
    //@State private var shotCoord = CLLocation(latitude: 0.0, longitude: 0.0)
    //@State private var ballCoord = CLLocation(latitude: 0.0, longitude: 0.0)
    @State private var shotClub = Club()
    @State private var showNewClub: Bool = false
    @StateObject private var locationManager = LocationManagerModel()
    
    var body: some View {
        NavigationView{
            ZStack {
                List {
                    ForEach(clubs) {club in 
                        NavigationLink(destination: ClubDetailView(club: club, waiting: $locationManager.waiting,shotCoord: $locationManager.shotCoord, ballCoord: $locationManager.ballCoord,  shotClub: $shotClub), label:{HStack {
                            Text(club.name)
                            Spacer()
                            if club.putter == false {
                                Text("\(club.yardsNum)y")
                            }
                        }} )
                    } //: ForEach
                    .onDelete(perform: deleteClub)
                } //: List
        
                
                if clubs.count == 0 && self.showNewClub == false{
                    Button("Tap to Start Adding Clubs!") {
                        self.showNewClub = true
                    } .foregroundColor(.accentColor)
                        .font(.system(.largeTitle,design: .rounded))
                        
                }
                
                if self.showNewClub{
                    
                    BlankView()
                        .onTapGesture {self.showNewClub = false}
                        .navigationBarHidden(true)
                    NewClubView(isShow: self.$showNewClub)
                        .transition(.move(edge: .bottom))
                        .animation(.default, value: self.showNewClub)
                        .opacity(100)
                        .padding(.top)
                        
                        
                }
                
            } //: ZStack
            .navigationTitle("Clubs")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: {
                        self.showNewClub = true
                    }) {
                        Image(systemName: "plus.circle.fill")
                            .foregroundColor(.accentColor)
                    } //: Button
                    
                } //: ToolbarItem
                
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                        .foregroundColor(.accentColor)
                        .opacity(100)
                        .disabled(self.clubs.count == 0)
                } //: ToolbarItem
//                if waiting == true{
//                    ToolbarItem(placement: .navigationBarLeading) {
//                        Button(action: {
//                            
//                            
//                        }, label: {
//                            Text("Start Putting")
//                        })
//                    }
//                }
            }
        }//: NavigationView
            //.searchable(text: self.$searchClub)
    }
    
    private func deleteClub(index: IndexSet) -> Void {
        withAnimation {
            index.map { clubs[$0] }.forEach(viewContext.delete)
            
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError.localizedDescription), \(nsError.userInfo)")
            }
        }
    }
}

I have tried passing the variables directly from the LocationManagerModel to the DetailView, passing from LocationManagerModel to ContentView as @State then to DetailView as @Binding, and from LocationManagerView to DetailView as @State with varying success. What is confusing me the most is that waiting works as intended by passing it as an argument to the DetailView when called in the ContentView but the others do not.


Solution

  • Currently you declare two separate @StateObject private var locationManager = LocationManagerModel(), one in ClubDetailView, and one in ContentView, they have no relations to each other.

    You need to have only one source of truth. So you could use @EnvironmentObject var locationManager: LocationManagerModel in ClubDetailView and pass the model to it from ContentView using .environmentObject(locationManager).