19

I'm trying to follow the MVVM pattern in SwiftUI, and I’m running into a problem with the core data and the fetch request.

All of the videos I've seen and articles I have read on it have a @FetchRequest in the view that accesses and modifies the core data.

How would I put this in the SettingsVCModel? I can't seem to figure it out, and as such I have kept the fetch request inside the view struct(SettingsVC) and used it there.

However this has so far been with buttons, where you are able to perform an action. Now I need to do it with a toggle, that only has a binding variable associated with it and no action like buttons have.

I tried the didSet approach on the @Published bio inside SettingsVCModel, but they you don't have access to the fetch request.

The code I’m working with is below:

struct SettingsVC: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(fetchRequest: UserSettings.getUserSettings()) var userSettings : FetchedResults<UserSettings>
    @ObservedObject var model = SettingsVCModel()
    
    var body: some View {
        NavigationView {
            Form{
                Section(header: Text("Application")){
                    Toggle(isOn: $model.bio, label: {Text(model.determineBiometricType())})
                    Picker(selection: $model.unitSelection, label: Text("Units")) {
                        Text("Imperial").tag(0)
                        Text("Metric").tag(1)
                    }
                    SettingsButton(toggleButton: $model.openSettings, title: "System Authorizations")
                }
                Section(header: Text("Feedback")){
                    NavigationLink(destination: ContactVC()){
                        Text("Contact Me")
                    }
                    SettingsButton(toggleButton: $model.rateApp, title: "Please Rate Body Insights")
                    SettingsButton(toggleButton: $model.tellAFriend, title: "Tell a Friend")
                }
                Section(header: Text("General")){
                    NavigationLink(destination: AboutVC()){
                        Text("About")
                    }
                    SettingsButton(toggleButton: $model.openPrivacyPolicy, title: "Privacy Policy", openPrivacyPolicy: true)
                }
            }
            .onAppear{
                self.model.bio = self.userSettings.first!.useBiometricUnlock
                self.model.unitSelection = self.userSettings.first!.usesMetric ? 1 : 0
            }
            .navigationBarTitle("Settings")
            .sheet(isPresented: $model.tellAFriend, content: {
                ShareSheetView(activityItems: ["Hey, check out this cool app! https://apps.apple.com/uy/app/body-insights/id1397531585"])
            })
            
        }
    }
} 


final class SettingsVCModel : ObservableObject{
    @Published var unitSelection = 0
    @Published var tellAFriend = false
    @Published var openPrivacyPolicy = false
    @Published var bio = false
    @Published var openSettings = false {
        didSet{
            if openSettings{
                openAppSettingsApp()
                openSettings = false
            }
        }
    }
    @Published var rateApp = false {
        didSet{
            if rateApp{
                openRateApp()
                rateApp = false
            }
        }
    }
    
    func openRateApp() {
        let appID = "1397531585"
        let urlString = "https://itunes.apple.com/us/app/appName/id\(appID)?mt=8&action=write-review"
        let url = URL(string: urlString)!
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
    }
    
    func openAppSettingsApp() {
        guard
            let settingsURL = URL(string: UIApplication.openSettingsURLString),
            UIApplication.shared.canOpenURL(settingsURL)
            else {
                return
        }
        
        UIApplication.shared.open(settingsURL)
        return
    }
    
    func determineBiometricType() -> String {
        let authContext = LAContext()
        let _ = authContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
        switch(authContext.biometryType) {
        case .none:
            return "Not Avaliable"
        case .touchID:
            return "TouchID"
        case .faceID:
            return "FaceID"
        @unknown default:
            return "Not Avaliable"
        }
    }
}
 
public class UserSettings : NSManagedObject, Identifiable {
    @NSManaged public var useBiometricUnlock : Bool
    @NSManaged public var usesMetric : Bool
    @NSManaged public var name : String
    @NSManaged public var birthday : Date
    @NSManaged public var age : Int
    
    static func getUserSettings() -> NSFetchRequest<UserSettings> {
        let request : NSFetchRequest<UserSettings> = UserSettings.fetchRequest() as! NSFetchRequest<UserSettings>
        request.sortDescriptors = []
        return request
    }
    
    static func save(){
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        let context = appDelegate.persistentContainer.viewContext

        do {
            try context.save()
        } catch{
            print(error)
        }
    }
    
    static func preloadData(){
        let preloadKey: String  = "preloadKey"
        let isPreloaded = UserDefaults.standard.bool(forKey: preloadKey)
        
        if !isPreloaded {
            let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
            let defaultSettings = UserSettings(context: context)
            let deviceName = UIDevice.current.name
            let firstName = deviceName.components(separatedBy: " ").first
            let isMetric = NSLocale.current.usesMetricSystem

            defaultSettings.name = firstName ?? ""
            defaultSettings.useBiometricUnlock = false
            defaultSettings.usesMetric = isMetric
            defaultSettings.age = 0
            defaultSettings.birthday = Date()

            UserDefaults.standard.set(true, forKey: preloadKey)
            UserSettings.save()
        }
    }
}
 

3 Answers 3

19

It appears you can only use SwiftUI's FetchRequest inside a view. If you check its definition, FetchRequest conforms to DynamicProperty, and if you read the documentation around both they imply it is designed to be used in a SwiftUI view.

FetchRequest:

/// Property wrapper to help Core Data clients drive views from the results of
/// a fetch request. The managed object context used by the fetch request and
/// its results is provided by @Environment(\.managedObjectContext).
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct FetchRequest<Result> : DynamicProperty where Result : NSFetchRequestResult {

DynamicProperty:

/// Represents a stored variable in a `View` type that is dynamically
/// updated from some external property of the view. These variables
/// will be given valid values immediately before `body()` is called.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol DynamicProperty {

    /// Called immediately before the view's body() function is
    /// executed, after updating the values of any dynamic properties
    /// stored in `self`.
    mutating func update()
}
Sign up to request clarification or add additional context in comments.

Comments

0

I've end up giving up with the idea of EnvironmentObject and use a wrapper service with protocol around the singleton of the core data storage. I don't think there's other option of doing this if you still want to keep the MVVM principles.

1 Comment

This is more or less what I've ended up doing as well.
0

CoreData can be thought of as a ViewModel itself. We use @FetchRequest in a view to "observe" changes happening with the model CoreData describes. If you're also using CloudKit with CoreData, then while your view is active, your @FetchRequest is also active and any inbound changes from iCloud will be "observed" and will cause your view to update.

If you want to fetch data from CoreData from a ViewModel you built yourself though, just use the non-SwiftUI functions. I literally just wrote this in a ViewModel today:

let request = API.fetchRequest()
let sort = NSSortDescriptor(keyPath: \API.priority, ascending: true)
request.sortDescriptors = [sort]
do {
  if let apis = try container.viewContext.fetch(request) as? [API] {
    logger.debug("APIs fetched: \(apis.count)")
    self.apis = apis
  }
} catch {
  logger.debug("Fetch failed 😭")
}

So just like that, I'm fetching data into my ViewModel where I can now assign it to some of the @Published properties. Happy coding!

1 Comment

This seems anti-pattern. Can we convince that publisher emit changes from any update from iCloud or local?

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.