Initialize app with an Async function | SwiftUI - swiftui

I need my app to configure the backend at start, here's the function to do so:
// Initializes Amplify
final func configureAmplify() async {
do {
// Amplify.Logging.logLevel = .info
let dataStore = AWSDataStorePlugin(modelRegistration: AmplifyModels())
let syncWithCloud = AWSAPIPlugin()
let userAuth = AWSCognitoAuthPlugin()
try Amplify.add(plugin: userAuth)
try Amplify.add(plugin: dataStore)
try Amplify.add(plugin: syncWithCloud)
try Amplify.configure()
print("Amplify initialized")
} catch {
print("Failed to initialize Amplify with \(error)")
}
}
I tried placing it in the #main init like so:
init() async {
await networkController.configureAmplify()
}
but I get the following error:
Type 'MyApplicationNameApp' does not conform to protocol 'App'
I try to apply the suggestions after that which is to initialize it:
init() {
}
but it seems odd, so now I have 2 init. What is going on here and what is the correct way to initialize multiple async functions at the start of the app, example:
Code above (configure amplify)
Check if user is logged in
Set session
etc
Note: The init() async never gets called in the example above which is another problem within this question, so what is the correct way to initialize async function when the app starts.

Use the ViewModifier
.task{
await networkController.configureAmplify()
}
You can add a Task to the init but you might have issues because SwiftUI can re-create the View as it deems necessary
init(){
Task(priority: .medium){
await networkController.configureAmplify()
}
}
Or you can use an ObservableObject that is an #StateObject
With an #StateObject SwiftUI creates a new instance of the object only once for each instance of the structure that declares the object.
https://developer.apple.com/documentation/swiftui/stateobject
#main
struct YourApp: App {
#StateObject var networkController: NetworkController = NetworkController()
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class NetworkController: ObservableObject{
init() {
Task(priority: .medium){
await configureAmplify()
}
}
// Initializes Amplify
final func configureAmplify() async {
do {
// Amplify.Logging.logLevel = .info
let dataStore = AWSDataStorePlugin(modelRegistration: AmplifyModels())
let syncWithCloud = AWSAPIPlugin()
let userAuth = AWSCognitoAuthPlugin()
try Amplify.add(plugin: userAuth)
try Amplify.add(plugin: dataStore)
try Amplify.add(plugin: syncWithCloud)
try Amplify.configure()
print("Amplify initialized")
} catch {
print("Failed to initialize Amplify with \(error)")
}
}
}

Related

Why does a pre-configured #FetchRequest not update the SwiftUI view?

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()

toggle between local and iCloud CoreData store

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.

Updating Core Data Database iniciated of publisher

I would like to use Contacts in an app. The user should select them and I will store the name and the id in the Core Data database.
When the Contact gets edited from the iOS Contacts app I would like to update the Core Data entities. When doing this I get the purple warning: "This method should not be called on the main thread as it may lead to UI unresponsiveness." (see comment in code) when fetching the contacts.
How to use the right threads to do it how it should be done? Im not really familiar with the best practices. Do I have to go back to the main thread for UI related things or is this not necessary here because of the #FetchRequest in other parts of the app where the saved contacts from Core Data are used?
class KontakteUpdater{
func update(moc: NSManagedObjectContext){
var contacts: [CNContact] = []
let key = [CNContactGivenNameKey,CNContactFamilyNameKey] as [CNKeyDescriptor]
let Kontakt_request = CNContactFetchRequest(keysToFetch: key)
// get contacts from the store
do{
try CNContactStore().enumerateContacts(with: Kontakt_request, usingBlock: { (contact, stoppingPointer) in // here with warning
contacts.append(contact)
})
}
catch
{
print(error)
}
// fetch currently saved contacts from core data
var kontakteUpdate: [Kontakt] = []
let request = NSFetchRequest<Kontakt>(entityName: "Kontakt")
do{
kontakteUpdate = try moc.fetch(request)
}
catch let error{
print("Error fetching. \(error)")
}
// update saved contacts in core data
for k in kontakteUpdate{
if let c = contacts.first(where: {contact in
contact.identifier == k.id
}){
k.vorname = c.givenName
k.nachname = c.familyName
}
}
do{
try moc.save()
}
catch{
print("\(error) - Saving didn't succeed")
}
}
}
Here is the entry point of the sample app:
import SwiftUI
import CoreData
#main
struct testApp: App {
let pub = NotificationCenter.default
.publisher(for: NSNotification.Name.CNContactStoreDidChange)
let kontakteUpdater = KontakteUpdater()
let persistenceContainer = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.onReceive(pub, perform: {output in
kontakteUpdater.update(moc: persistenceContainer.container.viewContext)
})
.environment(\.managedObjectContext, persistenceContainer.container.viewContext)
}
}
}

SwiftUI - ReferenceFileDocument - Inability to indicate a document needs saving

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

Dismiss a .sheet in SwiftUI after an async process has completed?

I'm trying to dismiss a .sheet in SwiftUI, after calling an async process to confirm the user's MFA code. (I'm using the AWS Amplify Framework).
I have a binding variable set on the main view, and reference it in the view the sheet presents with #Binding var displayMFAView: Bool. I have an authentication helper that tracks the user state: #EnvironmentObject var userAuthHelper: UserAuthHelper.
The following code dismisses the sheet as expected:
func confirmMFACode(verificationCode: String) {
// Code to confifm MFA...
print("User confirmed MFA")
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
}
However, if I call the auth process via Amplify's confirmSignIn method,
func confirmVerificationMFA(verificationCode: String) {
AWSMobileClient.default().confirmSignIn(challengeResponse: verificationCode) { (signInResult, error) in
if let error = error as? AWSMobileClientError {
// ... error handling ...
} else if let signInResult = signInResult {
switch (signInResult.signInState) {
case .signedIn:
print("User confirmed MFA")
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
default:
print("\(signInResult.signInState.rawValue)")
}
}
}
}
the sheet does not get dismissed. I have tried wrapping the variable assignment in DispatchQueue.main.async {..., but that hasn't solved the issue either.
...
DispatchQueue.main.async {
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
}
...
In fact, this throws the following into my logs:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
Wrapping the switch (... in a DispatchQueue per https://stackoverflow.com/a/58288437/217101 gave me the same warning in my log.
Admittedly I don't have a firm grasp on SwiftUI or AWS Amplify. What am I not understanding?
From what I can tell the async call does something unexpected with the state variables, but not with an EnvironmentObject. So, nstead of #Binding var displayMFAView: Bool, I stored displayMFAView in an EnvironmentObject,
#EnvironmentObject var settings: UserSettings
#State var mfaCode: String = ""
and then can show or hide the .sheet(... by updating a boolean in that object:
Button(action: {
self.signIn() // Async call happens here
self.settings.displayMFAView.toggle()
}) {
Text("Sign In")
}.sheet(isPresented: self.$settings.displayMFAView) {
// Example code to capture text
TextField("Enter your MFA code", text: self.$mfaCode)
}
Button(action: {
self.verifyMFACode(verificationCode: self.mfaCode) // async call
}) {
Text("Confirm")
}
In func verifyMFACode(), I can make an async call to validate my user, then toggle the sheet to disappear on success:
func verifyMFACode(verificationCode: String) {
AWSMobileClient.default().confirmSignIn(challengeResponse: verificationCode) {
...
case .signedIn:
self.settings.displayMFAView.toggle()
...