this is my PersistenceController:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentCloudKitContainer(name: "LogModel")
container.loadPersistentStores {(descr, error) in
if let error = error as NSError? {
fatalError("Container load failed: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
}
here I inject the container.
import SwiftUI
#main
struct MyApp: App {
var persistanceContainer = PersistenceController()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(CoreBluetoothViewModel())
.environment(\.managedObjectContext, persistanceContainer.container.viewContext)
}
}
}
I want to use this View to toggle between Cloud and Local store.
struct iCloudSyncView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var cloudEnabled = true
var body: some View {
Toggle("iCloud sync", isOn: $cloudEnabled)
}
}
I know that i have to use the NSPersistentCloudKitContainerfor the cloud and the NSPersistentCloudKitContainerfor the local store.
Also I found this article:
CoreData+CloudKit | On/off iCloud sync toggle
But I just can't get it to work :/
Can someone please tell me how I have to implement this?
I would add it like this:
struct PersistenceController {
let iCloud = UserDefaults.standard.bool(forKey: "iCloud")
static let shared = PersistenceController()
var container: NSPersistentContainer
init() {
if iCloud {
container = NSPersistentCloudKitContainer(name: "LogModel")
} else {
container = NSPersistentContainer(name: "LogModel")
}
container.loadPersistentStores {(descr, error) in
if let error = error as NSError? {
fatalError("Container load failed: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
}
But I don't know how to load the new container when toggle is changed.
Toggle("iCloud sync", isOn: $cloudEnabled)
.onChange(of: cloudEnabled) { value in
saveContext()
UserDefaults.standard.set(value, forKey: "iCloud")
//get new container
}
There are 2 ways
add local and cloud configurations to the model editor and set them on the store description and assign different entities to each config.
Use affectedStores for fetching and assign(to:) when creating new objects to manually select the store.
1 is best.
Related
Whenever I am using a pre-configured NSFetchRequest like so:
extension Note {
static var requestPrivateDBNotesByDate: NSFetchRequest<Note> {
let request = Note.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Note.createdDate, ascending: true)]
request.affectedStores = [PersistenceController.shared.privatePersistentStore]
return request
}
to do a #FetchRequest within a SwiftUI view:
#FetchRequest(fetchRequest: Note.requestPrivateDBNotesByDate)
private var notes: FetchedResults<Note>
the SwiftUI view is not updating when I add a Note entity to CoreData:
func addNote(name: String, context: NSManagedObjectContext) {
context.perform {
let note = Note(context: context)
note.displayName = name
note.createdDate = .now
try? context.save()
}
}
If I use a simple #FetchRequest within my SwiftUI view like so:
#FetchRequest(sortDescriptors: [SortDescriptor(\.displayName, order: .forward)]
) private var notes: FetchedResults<Note>
the view updates whenever I add a now Note.
Why is the pre-configured #FetchRequest not updating my SwiftUI view?
Note: I can force a view update by adding context.refresh(chat, mergeChanges: false) after context.save() but then my question would be, why do I need to force a refresh with a pre-configured #FetchRequest while it is not necessary with a simple #FetchRequest.
Is the forced refresh the only/correct way to go?
Am I missing something?
Update:
This is how I get the privatePersistentStore for the affectedStores property in the pre-configured NSFetchRequest.
var privatePersistentStore: NSPersistentStore {
var privateStore: NSPersistentStore?
let descriptions = persistentContainer.persistentStoreDescriptions
for description in descriptions {
if description.cloudKitContainerOptions?.databaseScope == .private {
guard let url = description.url else { fatalError("NO STORE URL!") }
guard let store = persistentContainer.persistentStoreCoordinator.persistentStore(for: url) else { fatalError("NO STORE!") }
privateStore = store
}
}
guard let privateStore else { fatalError("NO PRIVATE STORE!") }
return privateStore
}
you forgot to assign the new note to the store you are fetching from, e.g.
context.assign(to: PersistenceController.shared.privatePersistentStore)
try? context.save()
I'm writing a chat application using firebase , I notice memory leak in ChatView while observing database changes which is when a message is sent or received.
when I comment out the database observation the memory leak dose not happen anymore so I'm guessing this is a firebase problem .
I'm sharing the code so please if you know what is acutely causing the memory leak help me out.
ChatViewModel:
class ChatViewModel : ObservableObject {
/// - sub ViewModels :
#Published private(set) var messages : [MessageModel] = []
private(set) var conversationID : String? = nil
/// set shared conversationID
/// - Parameter convesationID: shared conversationID if exist
func setConverationID(convesationID : String?){
guard let convesationID = convesationID else {
print("CONVERSATION ID DOUS NOT EXIT")
return
}
self.conversationID = convesationID
startObservingConversation()
}
/// start observing the conversation with viewModel conversationID
private func startObservingConversation(){
guard let conversationID = self.conversationID else {
return
}
DatabaseManager.shared.observeMessagesForConversation(conversationId: conversationID) { [weak self] message in
self?.messages += message
}
}}
ChatView :
struct ChatView: View {
#StateObject var viewModel = ChatViewModel()
var body: some View {
VStack(alignment : .leading , spacing: 0){
ScrollViewReader { scrollViewReader in
List{
ForEach(viewModel.messages) { item in
MessageView(messsage: item.text)
.id(item.id)
}
}
}
}
}}
observerMessages :
func observeMessagesForConversation(conversationId id :String,compelition : #escaping ([MessageModel]) -> Void ) {
database.child(id).child("messages").observe(.childAdded) { snapshot in
guard let value = snapshot.value as? [String:Any] else {
compelition([])
return
}
var messages : [MessageModel] = []
let decoder = JSONDecoder()
guard
let jsonData = try? JSONSerialization.data(withJSONObject:value),
let message = try? decoder.decode(MessageModel.self, from: jsonData) else {
compelition([])
return
}
messages.append(message)
compelition(messages)
}
}
In the following code I have a List of Cars and each Car from that list has its own list of Services, I can add and delete Cars without a problem by calling carViewModel.addNewCar(make:String, model:String) and carViewModel.deleteCar(at indexSet:IndexSet).
Car.swift
import RealmSwift
final class Car: Object, ObjectKeyIdentifiable{
#objc dynamic var make: String = ""
#objc dynamic var model: String = ""
// creation date, ID etc.
dynamic var services = List<CarService>()
}
CarList.swift
import RealmSwift
final class CarList: Object, ObjectKeyIdentifiable{
#objc dynamic var name: String = ""
// creation date, ID etc.
var cars = RealmSwift.List<Car>()
}
CarService.swift
import RealmSwift
final class CarService: Object, ObjectKeyIdentifiable{
#objc dynamic var serviceName: String = ""
// creation date, ID etc.
}
View Model
import RealmSwift
class CarViewModel: ObservableObject{
#Published var cars = List<Car>()
#Published var selectedCarList: CarList? = nil
var token: NotificationToken? = nil
init(){
// Create a the default lists if they don't already exist.
createDefaultCarList()
createDefaultServiceList()
// Initialize the SelectedCarList and the cars variables items from the Default Car List.
if let list = realm?.objects(CarList.self).first{
self.selectedCarList = list
self.cars = list.cars
}
token = selectedCarList?.observe({ [unowned self] (changes) in
switch changes{
case .error(_): break
case.change(_, _):self.objectWillChange.send()
case.deleted: self.selectedCarList = nil
}
})
}
func addNewCar(make:String, model:String){
if let realm = selectedCarList?.realm{
try? realm.write{
let car = Car()
car.make = make
car.model = model
selectedCarList?.cars.append(car)
}
}
}
func deleteCar(at indexSet:IndexSet){
if let index = indexSet.first,
let realm = cars[index].realm{
try? realm.write{
realm.delete(cars[index])
}
}
}
func addService(toCar: Car, serviceName: String){
try? realm?.write{
let service = CarService()
service.serviceName = serviceName
toCar.services.append(service)
}
}
/// Creates the Default Car List if it doesn't already exists otherwise just prints the error.
func createDefaultCarList(){
do {
if (realm?.objects(CarList.self).first) == nil{
try realm?.write({
let defaultList = CarList()
defaultList.name = "Default Car List"
realm?.add(defaultList)
})
}
}catch let error{
print(error.localizedDescription)
}
}
/// Creates the Default Serivice List if it doesn't already exists otherwise just prints the error.
func createDefaultServiceList(){
do {
if (realm?.objects(ServiceList.self).first) == nil{
try realm?.write({
let defaultList = ServiceList()
defaultList.listName = "Default Service List"
realm?.add(defaultList)
})
}
}catch let error{
print(error.localizedDescription)
}
}
}
My issue is adding or deleting Services to existing Cars. When I call carViewModel.addService(toCar: Car, serviceName: String) I get the error below...
Calling the addService() method.
struct NewServiceFormView: View {
#ObservedObject var carViewModel: CarViewModel
#State var selectedCar:Car // pass from other cars view
var body: some View {
NavigationView {
Form {
// fields
}
.navigationBarItems( trailing:Button("Save", action: addNewCar))
}
}
func addNewCar(){
carViewModel.addService(toCar: selectedCar, serviceName: "Oil Change")
}
}
Error
"Cannot modify managed RLMArray outside of a write transaction."
I can add new Services by explicitly selecting a Car from the cars list. I don't get any errors but the UI doesn't update; I don't see the newly added Service until the app is relaunched.
No errors doing it this way but the UI doesn't update.
carViewModel.addService(toCar: carViewModel.cars[1], serviceName: "Rotors")
How can I properly watch, delete and add Services to existing Cars?
EDIT: Added the following code per Mahan's request.
View to present the NewServiceFormView
struct CarServicesView: View {
#State var selectedCar:Car // a car from parent view
#ObservedObject var carViewModel: CarViewModel
var body: some View {
VStack{
List {
Section(header: Text("Services: \(selectedCar.services.count)")) {
ForEach(selectedCar.services) { service in
}
}
}
.listStyle(GroupedListStyle())
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: openNewServiceForm) {
Image(systemName: "plus")
}
}
}
}.sheet(isPresented: $newServiceFormIsPresented){
NewServiceFormView(carViewModel: carViewModel, selectedCar: selectedCar)
}
}
func openNewServiceForm() {
newServiceFormIsPresented.toggle()
}
}
One issue is how the Realm objects are being observed - remember they are ObjC objects under the hood so you need to use Realm observers. So this
#ObservedObject var carViewModel: CarViewModel
should be this
#ObservedRealmObject var carViewModel: CarViewModel
See the documentation for observedRealmObject
Also, keep in mind if you're observing a Results, the same thing applies, use
#ObservedResults
as shown in the documentation
I want to add a Button to my view to load more data. In my Environment Object the data is generated randomly via an API.
How can I reload my Environment object to get new items. The Code Below should make it clear. Thanks in advance
class observer : ObservableObject{
#Published var shows = [stacks]()
#Published var last = -1
var results = [Result1]()
init(){
let number = Int.random(in: 1...35)
print("das ist dier etste radodm nube: \(number)")
let endnumber = number + 8
print("das ist dier etste radodm nube: \(endnumber)")
for n in number...endnumber{
guard let url = URL(string:"https://api.themoviedb.org/3/discover/tv?api_key=3ed2bd15f916d0e3fbb77c193bf33b61&language=de-DE®ion=DE&with_networks=213&page=\(n)" ) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.results = decodedResponse.results
for i in self.results{
self.shows.append(stacks(id: "\(i.id)", name: i.name, typ: "Serie", status: "",overview: i.overview, vote: "\(i.vote_average)", image: i.poster_path, swipe: 0, degree: 0, commercal: "no"))
}
self.shows.shuffle()
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
}
struct View: View {
#EnvironmentObject var cards : observer
var body: some View {
VStack{
Button(action: {
//reload
}){
Text("reload")
}
}
I would put the fetching/loading data code that is inside the init in a function.
func codeToFetchData() {
//Code from your init
}
The call that method in your init and your Button for example cards.codeToFetchData()
When creating a class conforming to ReferenceFileDocument, how do you indicate the document needs saving. i.e. the equivalent of the NSDocument's updateChangeCount method?
I've met the same problem that the SwiftUI ReferenceFileDocument cannot trigger the update. Recently, I've received feedback via the bug report and been suggested to register an undo.
Turns out the update of ReferenceFileDocument can be triggered, just like UIDocument, by registering an undo action. The difference is that the DocumentGroup explicitly implicitly setup the UndoManager via the environment.
For example,
#main
struct RefDocApp: App {
var body: some Scene {
DocumentGroup(newDocument: {
RefDocDocument()
}) { file in
ContentView(document: file.document)
}
}
}
struct ContentView: View {
#Environment(\.undoManager) var undoManager
#ObservedObject var document: RefDocDocument
var body: some View {
TextEditor(text: Binding(get: {
document.text
}, set: {
document.text = $0
undoManager?.registerUndo(withTarget: document, handler: {
print($0, "undo")
})
}))
}
}
I assume at this stage, the FileDocument is actually, on iOS side, a wrapper on top of the UIDocument, the DocumentGroup scene explicitly implicitly assign the undoManager to the environment. Therefore, the update mechanism is the same.
The ReferenceFileDocument is ObservableObject, so you can add any trackable or published property for that purpose. Here is a demo of possible approach.
import UniformTypeIdentifiers
class MyTextDocument: ReferenceFileDocument {
static var readableContentTypes: [UTType] { [UTType.plainText] }
func snapshot(contentType: UTType) throws -> String {
defer {
self.modified = false
}
return self.storage
}
#Published var modified = false
#Published var storage: String = "" {
didSet {
self.modified = true
}
}
}
ReferenceFileDocument exists for fine grained controll over the document. In comparison, a FileDocument has to obey value semantics which makes it very easy for SwiftUI to implement the undo / redo functionality as it only needs to make a copy before each mutation of the document.
As per the documentation of the related DocumentGroup initializers, the undo functionality is not provided automatically. The DocumentGroup will inject an instance of an UndoManger into the environment which we can make use of.
However an undo manager is not the only way to update the state of the document. Per this documentation AppKit and UIKit both have the updateChangeCount method on their native implementation of the UI/NSDocument object. We can reach this method by grabbing the shared document controller on macOS from within the view and finding our document. Unfortunately I don't have a simple solution for the iOS side. There is a private SwiftUI.DocumentHostingController type which holds a reference to our document, but that would require mirroring into the private type to obtain the reference to the native document, which isn't safe.
Here is a full example:
import SwiftUI
import UniformTypeIdentifiers
// DOCUMENT EXAMPLE
extension UTType {
static var exampleText: UTType {
UTType(importedAs: "com.example.plain-text")
}
}
final class MyDocument: ReferenceFileDocument {
// We add `Published` for automatic SwiftUI updates as
// `ReferenceFileDocument` refines `ObservableObject`.
#Published
var number: Int
static var readableContentTypes: [UTType] { [.exampleText] }
init(number: Int = 42) {
self.number = number
}
init(configuration: ReadConfiguration) throws {
guard
let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8),
let number = Int(string)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.number = number
}
func snapshot(contentType: UTType) throws -> String {
"\(number)"
}
func fileWrapper(
snapshot: String,
configuration: WriteConfiguration
) throws -> FileWrapper {
// For the sake of the example this force unwrapping is considered as safe.
let data = snapshot.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
}
// APP EXAMPLE FOR MACOS
#main
struct MyApp: App {
var body: some Scene {
DocumentGroup.init(
newDocument: {
MyDocument()
},
editor: { file in
ContentView(document: file.document)
.frame(width: 400, height: 400)
}
)
}
}
struct ContentView: View {
#Environment(\.undoManager)
var _undoManager: UndoManager?
#ObservedObject
var document: MyDocument
var body: some View {
VStack {
Text(String("\(document.number)"))
Button("randomize") {
if let undoManager = _undoManager {
let currentNumber = document.number
undoManager.registerUndo(withTarget: document) { document in
document.number = currentNumber
}
}
document.number = Int.random(in: 0 ... 100)
}
Button("randomize without undo") {
document.number = Int.random(in: 0 ... 100)
// Let the system know that we edited the document, which will
// eventually trigger the auto saving process.
//
// There is no simple way to mimic this on `iOS` or `iPadOS`.
let controller = NSDocumentController.shared
if let document = controller.currentDocument {
// On `iOS / iPadOS` change the argument to `.done`.
document.updateChangeCount(.changeDone)
}
}
}
}
}
Unfortunatelly SwiftUI (v2 at this moment) does not provide a native way to mimic the same functionality, but this workaround is still doable and fairly consice.
Here is a gist where I extended the example with a custom DocumentReader view and a DocumentProxy which can be extended for common document related operations for more convenience: https://gist.github.com/DevAndArtist/eb7e8aa5e7134610c20b1a7aca358604