10

I'm still pretty new to SwiftUI and Firebase. Recently, as a hobby, I have been developing an app for my school. After the launch of Xcode 12, I decided to experiment with the new features such as Widgets. However, since my app gets its data from Firebase, I've been having some problems. My most recent problem is this "Thread 1: "Failed to get FirebaseApp instance. Please call FirebaseApp.configure() before using Firestore". I'm not entirely sure where to put "FirebaseApp.configure()" as there is no AppDelegate.swift for the widget. My code is below.

Edit: I've rearranged my code so that I am now getting the data from the original iOS app data model. I am therefore not importing Firebase within the widgets Swift file. However, I still get the same error ("SendProcessControlEvent:toPid: encountered an error: Error Domain=com.apple.dt.deviceprocesscontrolservice Code=8" and "-> 0x7fff5bb6933a <+10>: jae 0x7fff5bb69344 ; <+20> - Thread 1: "Failed to get FirebaseApp instance. Please call FirebaseApp.configure() before using Firestore""). I've also included @Wendy Liga's code, but I still got the same error. My newer code is below :

iOS App Data Model

import Foundation
import SwiftUI
import Firebase
import FirebaseFirestore

struct Assessment: Identifiable {
    var id:String = UUID().uuidString
    var Subject:String
    var Class:Array<String>
    var Day:Int
    var Month:String
    var Title:String
    var Description:String
    var Link:String
    var Crit:Array<String>
}

class AssessmentsViewModel:ObservableObject {
    @Published var books = [Assessment]()
    
    private var db = Firestore.firestore()
    
    // Add assessment variables
    @Published var AssessmentSubject:String = ""
    //@Published var AssessmentClass:Array<String> = [""]
    @Published var AssessmentDay:Int = 1
    @Published var AssessmentMonth:String = "Jan"
    @Published var AssessmentTitle:String = ""
    @Published var AssessmentDescription:String = ""
    @Published var AssessmentLink:String = ""
    @Published var AssessmentCrit:Array<String> = [""]
    @Published var AssessmentDate:Date = Date()
    
    func fetchData() {
        db.collection("AssessmentsTest").order(by: "date").addSnapshotListener { (QuerySnapshot, error) in
            guard let documents = QuerySnapshot?.documents else {
                print("No documents")
                return
            }
            
            self.books = documents.map { (QueryDocumentSnapshot) -> Assessment in
                let data = QueryDocumentSnapshot.data()
                
                let Subject = data["subject"] as? String ?? ""
                let Class = data["class"] as? Array<String> ?? [""]
                let Day = data["day"] as? Int ?? 0
                let Month = data["month"] as? String ?? ""
                let Title = data["title"] as? String ?? ""
                let Description = data["description"] as? String ?? ""
                let Link = data["link"] as? String ?? ""
                let Crit = data["crit"] as? Array<String> ?? [""]
                
                return Assessment(Subject: Subject, Class: Class, Day: Day, Month: Month, Title: Title, Description: Description, Link: Link, Crit: Crit)
            }
        }
    }
    
    func writeData() {
        let DateConversion = DateFormatter()
        DateConversion.dateFormat = "DD MMMM YYYY"
        let Timestamp = DateConversion.date(from: "20 June 2020")
        
        db.collection("AssessmentsTest").document(UUID().uuidString).setData([
            "subject": AssessmentSubject,
            "month": AssessmentMonth,
            "day": AssessmentDay,
            "title": AssessmentTitle,
            "description": AssessmentDescription,
            "link": AssessmentLink,
            "crit": AssessmentCrit,
            "date": AssessmentDate
        ]) { err in
            if let err = err {
                print("Error writing document: \(err)")
            } else {
                print("Document successfully written!")
            }
        }
    }
}

Widgets View

struct WidgetsMainView: View {
    
    @ObservedObject private var viewModel = AssessmentsViewModel()
    
    var body: some View {
        HStack {
            Spacer().frame(width: 10)
            VStack(alignment: .leading) {
                Spacer().frame(height: 10)
                
                ForEach(self.viewModel.books) { Data in
                    HStack {
                        VStack {
                            Text(String(Data.Day))
                                .bold()
                                .font(.system(size: 25))
                            Text(Data.Month)
                        }
                        .padding(EdgeInsets(top: 16, leading: 17, bottom: 16, trailing: 17))
                        .background(Color(red: 114/255, green: 112/255, blue: 110/255))
                        .foregroundColor(Color.white)
                        .cornerRadius(10)
                        
                        VStack(alignment: .leading, spacing: 0) {
                            Text("\(Data.Subject) Crit \(Data.Crit.joined(separator: " + "))")
                                .bold()
                            if Data.Title != "" {
                                Text(Data.Title)
                            } else {
                                Text(Data.Class.joined(separator: ", "))
                            }
                        }
                        .padding(.leading, 10)
                    }
                }
                .onAppear {
                    viewModel.books.prefix(2)
                }
                
                Spacer()
            }
            Spacer()
        }
    }
}

Widgets @main

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        FirebaseApp.configure()
        return true
    }
}

@main
struct AssessmentsWidget: Widget {
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    private let kind: String = "Assessments Widget"

    public var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
            AssessmentsWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Assessments Widget")
        .description("Keep track of your upcoming assessments.")
        .supportedFamilies([.systemMedium])
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
Daniel Ho
  • 131
  • 1
  • 7
  • I can't really answer this question since I am having some what the same problem. But to help you find more information, you may have to encapsulate your data models and logic to a swift package, then import the modules. In my case, I am stuck in adding Firebase, and Realm as a dependency into my swift package. Take a look at this video, where they do the same, but with a watch extension instead of the widget extension : https://developer.apple.com/wwdc19/410 – J Arango Aug 14 '20 at 19:59
  • Did you find a solution @j-arango @daniel-ho? – lazerrouge Sep 03 '20 at 23:06
  • @lazerrouge I did, I will give an answer. – J Arango Sep 04 '20 at 19:55

3 Answers3

10

Your main app needs to pass data to your extension, this can be achieved by allowing your app to use "App Groups" capability. What App Groups does is, it creates a container where your app can save data for you to share with your app extensions. So follow these steps to enable "App Groups".

1. Select your main App Target>Signing & Capabilities then tap + Capability and select "App Groups"

enter image description here

2. Tap on "+" to add a new container, and add a name to it after group. example : "group.com.widgetTest.widgetContainer"

enter image description here

Once you have created the "App Group" on your main app, you should take the same steps but on your "Widget Extension" target. This time, instead of creating a container, you should be able to select the container you already have from the main app. You can find a good video on YouTube explaining this process really well on here How to Share UserDefaults with app extensions

The next step I recommend is to create a Swift Package or a Framework, and add a new Model Object, this model object is the one you will be passing from your main app, to your widget extension. I chose a Swift Package.

To do this follow these steps:

1. File>New>Swift Package

enter image description here

A good video from the WWDC19 about this can be seen here

2. In your Swift Package, inside the "Sources" folder, Create a Custom Model which you will use in both your Main App, and Widget Extension

enter image description here

Make your object conform to "Codable" and that it is Public.

Important Make sure you import "Foundation" so that when you are decoding/encoding your object, it will do it properly.

3. Add your Package to your Main App and Widget Extension

  • Select your App's Target>General> Scroll to "Frameworks, Libraries, and Embedded Content"
  • Tap "+" and search for your Package

enter image description here

  • Do the same steps on your Widget's Extension

enter image description here

Now, all you need to do is "import" your module in the file that you will be creating your custom object in both your Main App, and on your WidgetExtension, then initialize your shared object on your main app and save it to UserDefaults by first encoding the object to JSON and then saving it to UserDefaults(suiteName: group.com.widgetTest.widgetContainer)

let mySharedObject = MySharedObject(name: "My Name", lastName: "My Last Name")
                   
 do {
     let data = try JSONEncoder().encode(mySharedObject)

      /// Make sure to use your "App Group" container suite name when saving and retrieving the object from UserDefaults
      let container = UserDefaults(suiteName:"group.com.widgetTest.widgetContainer")
          container?.setValue(data, forKey: "sharedObject")
                        
      /// Used to let the widget extension to reload the timeline
      WidgetCenter.shared.reloadAllTimelines()

      } catch {
        print("Unable to encode WidgetDay: \(error.localizedDescription)")
   }

Then in your widget extension, you want to retrieve your object from UserDefaults, decode it and you should be good to go.

Short Answer

Download your Firebase data, create a new object from that data, encode it to JSON, save it on your container by using UserDefaults, retrieve the object in your extension from the container, decode it and use it for your widget entry. Of course, all of this is assuming you follow the steps above.

J Arango
  • 939
  • 1
  • 8
  • 21
  • 1
    Excellent explanation but what happens if the main application is no longer running in the background and remote data from Firebase is still updating? The widget would have a bunch of stale data. In this case would the widget have to link to Firebase directly and get the the authentication token from the main application to directly access the information? – Vivek Sep 29 '20 at 23:48
  • That's a great question. I answered this based on sharing data from the main app to the widget. I am sure that if you want your widget to interact with Firebase directly you will need to implement some sort of connectivity between the widget and firebase. But keep in mind, the widget will only make a call to a URL when updating the timeline, or when the timeline is updating the widget. – J Arango Sep 30 '20 at 05:24
7

I can confirm after testing that the following method works to use Firebase in the Widget Target without incorporating an app group, user defaults or anything else.

@main
struct FirebaseStartupSequence: Widget {
  init() {
    FirebaseApp.configure()
  }

  let kind: String = "FirebaseStartupSequence"

  var body: some WidgetConfiguration {
    IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
      FirebaseStartupSequenceEntryView(entry: entry)
    }
    .configurationDisplayName("My Widget")
    .description("This is an example widget.")
  }
}

Simply use the init method in your widget to access a firebase instance.

This was the easiest solution for me as of today.

Taken from: https://github.com/firebase/firebase-ios-sdk/issues/6683

Additional Edit: Do you need to share authentication? No problem. Firebase has that covered here: https://firebase.google.com/docs/auth/ios/single-sign-on?authuser=1

Jprofficial
  • 329
  • 3
  • 10
  • this method works BUT im debugging my widget is staying in placeholder mode for bunch of time.. like 20 secs... im getting the idea Firebase initiation is lagging the startup of the widget... :/ ill be investigating – lorenzo gonzalez Jul 26 '23 at 17:45
-4

You can add the appDelegate to your @main SwiftUI view

First create your appdelegate on your widget extension

import Firebase

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        FirebaseApp.configure()
        return true
    }
}

look at @main, inside your widget extension,

@main
struct TestWidget: Widget {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    private let kind: String = "ExampleWidget"

    public var body: some WidgetConfiguration {
       ...
    }
}

@main is new swift 5.3 feature that allows value type entry point, so this is will be your main entry point for your widget extension

just add @UIApplciationDelegateAdaptor, inside your @main

Wendy Liga
  • 634
  • 5
  • 12
  • Hi! Thanks for your answer! Unfortunately, even after using your code, I still got the same error. "-> 0x7fff5bb6933a <+10>: jae 0x7fff5bb69344 ; <+20>" - "Thread 1: "Failed to get FirebaseApp instance. Please call FirebaseApp.configure() before using Firestore"" If it helps, I've included 2 images. https://send.firefox.com/download/be18829cae081518/#jOq_u01L47J-ocHg1cjOjg, https://send.firefox.com/download/1c7e750ebb55b28a/#bkHZuxZgz0lnve2oTH3MgA – Daniel Ho Jun 28 '20 at 14:28
  • This wil not work, for some reason the appDelegate is not getting called from the widget scope, please prove me wrong I've tested it already. – XcodeNOOB Dec 08 '20 at 19:14