I’m developing an iOS application with SwiftUI, and I’m having trouble displaying my data fetched from database.
Code
import SwiftUI
import Firebase
import FirebaseDatabase
var ref = Database.database().reference()
class Observe {
static func currentSingleEventObserve(completion: #escaping ((String?) -> ())) {
let path = "supersonic/current"
let ref = Database.database().reference().child(path)
_ = ref.observeSingleEvent(of: .value, with: { snapshot in
let temp = (snapshot.value! as AnyObject).description
completion(temp)
})
}
}
struct CurrentDistance: View {
var value: NSDictionary?
var refHandle: UInt = 0
#State var distance: String
init() {
Observe.currentSingleEventObserve(completion: { temp in
self.distance = temp // I want to mutate self.distance here
})
}
var body: some View {
VStack {
Text("Distance")
Text(self.distance)
}
}
}
struct CurrentDistance_Previews: PreviewProvider {
static var previews: some View {
CurrentDistance()
}
}
Database
|-supersonic
|- current: 30
Problem
I want to mutate self.distance in the initializer. Trying to mutate self.distance in the closure I got an error Escaping closure captures mutating 'self' parameter, and I don't know how to update the value.
How can I display the value fetched from the database?
This is a somewhat generic answer as we are just going to be updating a UI element based on a value read from Firebase.
Firebase is asynchronous and values are only valid following the Firebase function, within the closure.
Suppose we have a simple SwiftUI app that displays a Text object, a button to click to load the data from Firebase, and then a var that holds what the text should be.
struct ContentView: View {
#State var buttonText = "Initial Button Label"
var body: some View {
VStack {
Text(buttonText)
Button(action: {
self.readFirebase()
}) {
Text("Click Me!")
}
}
}
func readFirebase() {
let ref = my_firebase_ref
let textRef = ref.child("string_node")
textRef.observeSingleEvent(of: .value, with: { snapshot in
let myText = snapshot.value as? String ?? "No String"
self.buttonText = myText
})
}
}
Here's my Firebase structure
root
string_node: "Hello, World"
Related
I want to make it so you can favourite a "landmark" in one view (LandmarkDetail), and access a list of all the "landmarks" in another view with the ones I've favourited highlighted. First I used "#AppStorrage" but I was told too to use Core Data for it instead. So far I have the favourite button working in the LandmarkDetail view with "#AppStorage" but apparently I need to change that so it uses Core Data.
I've look around to get an understanding of how to do it with Core Data but I could really use a helping hand if anyone can help. I've already seen and read some tutorials about core data and how to set it up, but I can't find anything for my specific problem where I pull in data from a JSON and I need Core Data to handle the favourite feature.
Here is my code for the favourite button
struct FavoriteButton: View {
#AppStorage ("isFavorite") var isFavorite: Bool = false
var body: some View {
Button {
isFavorite.toggle()
} label: {
Label("Toggle Favorite", systemImage: isFavorite ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(isFavorite ? .yellow : .gray)
}
}
}
Code from the landmark detail view
struct LandmarkDetail: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton()
}
}
}
}
}
Code for the rows in the list view
This is the one not working yet, so far it just pulls the data from a JSON.
MODEL
import Foundation
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
var park: String
var state: String
var description: String
var isFavorite: Bool
var isFeatured: Bool
var category: Category
enum Category: String, CaseIterable, Codable {
case lakes = "Lakes"
case rivers = "Rivers"
case mountains = "Mountains"
}
private var imageName: String
var image: Image{
Image(imageName)
}
var featureImage: Image? {
isFeatured ? Image(imageName + "_feature") : nil
}
private var coordinates: Coordinates
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
import Foundation
import Combine
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark] = load("landmarkData.json")
var hikes: [Hike] = load("hikeData.json")
#Published var profile = Profile.default
var features: [Landmark] {
landmarks.filter { $0.isFeatured }
}
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarks,
by: { $0.category.rawValue }
)
}
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
First of all you need to create a .xcdatamodeld file named Landmarks. You can create it by pressing right button on the principal folder of your project and searching Data Model. After you need to create a new Entity named Landmark. You can add attributes showed in your model like id, name, park, etc... with their types. After you need to create a new Swift file in which you can create you Core Data Controller like this:
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "Landmarks")
init() {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
Successively, you need to add to your LandmarksApp.swift file the following code:
struct LandmarksApp: App {
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
Continue adding this to your LandmarkDetail view:
struct LandmarkDetail: View {
#Environment(\.managedObjectContext) var moc
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton()
}
}
}
}
}
To create a new item and add to Core Data you can write:
let landmark = Landmark(context: moc)
landmark.id = id
landmark.name = name
landmark.park = park
etc...
try? moc.save()
For your JSON data you can create a function that convert all JSON data in Core Data following these steps.
I'm trying to create a List with data from my firebase reali-time database but i'm getting this error on the List line:
The error:
Type 'Void' cannot conform to 'View'
My code:
struct ActiveGoalsView: View {
#State var goals = ["finish this project"]
#State var ref = Database.database().reference()
var body: some View {
NavigationView {
List {
ref.child("users").child(Auth.auth().currentUser?.uid ?? "noid").child("goals").observeSingleEvent(of: .value) { snapshot in
for snap in snapshot.children {
Text(snap.child("title").value)
}
}
}.navigationBarHidden(true)
}
}
}
struct ActiveGoalsView_Previews: PreviewProvider {
static var previews: some View {
ActiveGoalsView()
}
}
You can't use imperative code like observeSingleEvent in the middle of your view hierarchy that doesn't return a View. As a commenter suggested, you'd be better off moving your asynchronous code outside of the body (I'd recommend to an ObservableObject). Here's one solution (see inline comments):
class ActiveGoalsViewModel : ObservableObject {
#Published var children : [String] = []
private var ref = Database.database().reference()
func getChildren() {
ref.child("users").child(Auth.auth().currentUser?.uid ?? "noid").child("goals").observeSingleEvent(of: .value) { snapshot in
self.children = snapshot.children.map { snap in
snap.child("title").value //you may need to use ?? "" if this returns an optional
}
}
}
}
struct ActiveGoalsView: View {
#State var goals = ["finish this project"]
#StateObject private var viewModel = ActiveGoalsViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.children, id: \.self) { child in //id: \.self isn't a great solution here -- you'd be better off returning an `Identifiable` object, but I have no knowledge of your data structure
Text(child)
}
}.navigationBarHidden(true)
}.onAppear {
viewModel.getChildren()
}
}
}
I have a problem with observed object in SwiftUI.
I can see changing values of observed object on the View struct.
However in class or function, even if I change text value of TextField(which is observable object) but "self.codeTwo.text still did not have changed.
here's my code sample (this is my ObservableObject)
class settingCodeTwo: ObservableObject {
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: settingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: settingCodeTwo.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
and the main problem is... "self.codeTwo.text" never changed!
class NetworkManager: ObservableObject {
#ObservedObject var codeTwo = settingCodeTwo()
#Published var posts = [Post]()
func fetchData() {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "\(self.codeTwo.text)" //This one I want to use observable object
urlComponents.path = "/mob_json/mob_json.aspx"
urlComponents.queryItems = [
URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
URLQueryItem(name: "param", value: "1000|1000|\(Gpass.hahaha)")
]
if let url = urlComponents.url {
print(url)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.Table
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
and this is view, I can catch change of the value in this one
import SwiftUI
import Combine
struct SettingView: View {
#ObservedObject var codeTwo = settingCodeTwo()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $codeTwo.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.blue)
Text(codeTwo.text)
}
}
}
}
Help me please.
Non-SwiftUI Code
Use ObservedObject only for SwiftUI, your function / other non-SwiftUI code will not react to the changes.
Use a subscriber like Sink to observe changes to any publisher. (Every #Published variable has a publisher as a wrapped value, you can use it by prefixing with $ sign.
Reason for SwiftUI View not reacting to class property changes:
struct is a value type so when any of it's properties change then the value of the struct has changed
class is a reference type, when any of it's properties change, the underlying class instance is still the same.
If you assign a new class instance then you will notice that the view reacts to the change.
Approach:
Use a separate view and that accepts codeTwoText as #Binding that way when the codeTwoText changes the view would update to reflect the new value.
You can keep the model as a class so no changes there.
Example
class Model : ObservableObject {
#Published var name : String //Ensure the property is `Published`.
init(name: String) {
self.name = name
}
}
struct NameView : View {
#Binding var name : String
var body: some View {
return Text(name)
}
}
struct ContentView: View {
#ObservedObject var model : Model
var body: some View {
VStack {
Text("Hello, World!")
NameView(name: $model.name) //Passing the Binding to name
}
}
}
Testing
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let model = Model(name: "aaa")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
model.name = "bbb"
}
return ContentView(model: model)
}
}
It is used two different instances of SettingCodeTwo - one in NetworkNamager another in SettingsView, so they are not synchronised if created at same time.
Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults)
Tested with Xcode 11.4 / iOS 13.4
Modified code below (see also important comments inline)
extension UserDefaults {
#objc dynamic var textKey2: String { // helper keypath
return string(forKey: "textKey2") ?? ""
}
}
class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
private var observer: NSKeyValueObservation!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
}
observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
self.text = newValue
}
}
}
}
class NetworkManager: ObservableObject {
var codeTwo = SettingCodeTwo() // no #ObservedObject needed here
...
Has anyone been able to successfully integrate Realm with SwiftUI, especially deleting records/rows from a SwiftUI List? I have tried a few different methods but no matter what I do I get the same error. After reading some related threads I found out that other people have the same issue.
The following code successfully presents all of the items from Realm in a SwiftUI List, I can create new ones and they show up in the List as expected, my issues is when I try to delete records from the List by either manually pressing a button or by left-swiping to delete the selected row, I get an Index is out of bounds error.
Any idea what could be causing the error?
Here is my code:
Realm Model
class Dog: Object {
#objc dynamic var name = ""
#objc dynamic var age = 0
#objc dynamic var createdAt = NSDate()
#objc dynamic var userID = UUID().uuidString
override static func primaryKey() -> String? {
return "userID"
}
}
SwiftUI Code
class BindableResults<Element>: ObservableObject where Element: RealmSwift.RealmCollectionValue {
var results: Results<Element>
private var token: NotificationToken!
init(results: Results<Element>) {
self.results = results
lateInit()
}
func lateInit() {
token = results.observe { [weak self] _ in
self?.objectWillChange.send()
}
}
deinit {
token.invalidate()
}
}
struct DogRow: View {
var dog = Dog()
var body: some View {
HStack {
Text(dog.name)
Text("\(dog.age)")
}
}
}
struct ContentView : View {
#ObservedObject var dogs = BindableResults(results: try! Realm().objects(Dog.self))
var body: some View {
VStack{
List{
ForEach(dogs.results, id: \.name) { dog in
DogRow(dog: dog)
}.onDelete(perform: deleteRow )
}
Button(action: {
try! realm.write {
realm.delete(self.dogs.results[0])
}
}){
Text("Delete User")
}
}
}
private func deleteRow(with indexSet: IndexSet){
indexSet.forEach ({ index in
try! realm.write {
realm.delete(self.dogs.results[index])
}
})
}
}
Error
Terminating app due to uncaught exception ‘RLMException’, reason: ‘Index 23 is out of bounds (must be less than 23).’
Of course, the 23 changes depending on how many items are in the Realm database, in this case, I had 24 records when I swiped and tapped the delete button.
FYI - The error points to the AppDelegate file with a Thread 1: signal SIGABRT.
Here is an example of how i do this. This is without realm operations but i hope u get the idea where you can put the realm stuff. (I also almost never use the realm objects directly but instead convert them to structs or classes.)
import Foundation
import Realm
import Combine
import SwiftUI
struct dogs: Hashable {
let name: String
}
class RealmObserverModel: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
#Published var dogsList: [dogs] = [dogs(name: "Dog 1"), dogs(name: "Dog 2")]
// get your realm objects here and set it to
// the #Publsished var
func getDogs() {
let count = dogsList.count + 1
dogsList.append(dogs(name: "Dog \(count)"))
}
// get your realm objects here and set it to
// the #Publsished var
func deletetDogs() {
_ = dogsList.popLast()
}
}
/// Master View
struct DogView: View {
#EnvironmentObject var observer: RealmObserverModel
var body: some View {
VStack{
DogsListView(dogsList: $observer.dogsList)
HStack{
Button(action: {
self.observer.getDogs()
}) {
Text("Get more dogs")
}
Button(action: {
self.observer.deletetDogs()
}) {
Text("Delete dogs")
}
}
}
}
}
// List Subview wiht Binding
struct DogsListView: View {
#Binding var dogsList: [dogs]
var body: some View {
VStack{
List{
ForEach(dogsList, id:\.self) { dog in
Text("\(dog.name)")
}
}
}
}
}
struct DogView_Previews: PreviewProvider {
static var previews: some View {
DogView().environmentObject(RealmObserverModel())
}
}
Not a great solution but my work around was copying each realm result to a local object/array. I updated my Lists/Views to use the realmLocalData instead of the data returned from the realm object itself.
class ContentViewController: ObservableObject {
private var realmLocalData: [ScheduleModel] = [ScheduleModel]()
private let realm = try! Realm()
func updateData() {
realmLocalData.removeAll()
let predicate = NSPredicate(format: "dateIndex >= %# && dateIndex <= %#", argumentArray: [startDate, endDate])
let data = self.realm.objects(MonthScheduleModel.self).filter(predicate)
for obj in data {
realmLocalData.append(ScheduleModel(realmObj: obj))
}
}
}
I followed this https://www.youtube.com/watch?v=_N10q1iR2HQ&t=328s tutorial; however, adjusted for some changes made to Xcode 11 GM seed 2 syntax and terms, because some stuff is obsolete now. Build is successful, but crashes for some reason.
I would appreciate any help and suggestions, tried all trial and error I could think of.
Here is the code below (pasting all of it since GM seed 2 has many command changes):
import SwiftUI
import Combine
import FirebaseFirestore
struct dataset : Identifiable {
var id = ""
var name = ""
var phone = ""
var services = ""
}
class getData : ObservableObject {
var didChange = PassthroughSubject<getData, Never>()
var data = [dataset](){
didSet{
didChange.send(self)
}
}
init(){
let db = Firestore.firestore()
let settings = db.settings
settings.areTimestampsInSnapshotsEnabled = true
db.settings = settings
db.collection("venues").addSnapshotListener {(snap,err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in (snap?.documentChanges)!{
let name = i.document.data()["name"] as! String
let phone = i.document.data()["phone"] as! String
let services = i.document.data()["services"] as! String
let id = i.document.documentID
DispatchQueue.main.async {
self.data.append(dataset(id: id, name: name, phone: phone, services:services))
}
}
}
}
}
struct ContentView: View {
#ObservedObject var data1 = getData()
var body: some View {
VStack {
Text("Hello World")
List(data1.data){ i in
cellView(name: i.name, phone: i.phone, services: i.services)
}
}
}
}
struct cellView : View {
#State var name = ""
#State var phone = ""
#State var services = ""
var body : some View{
VStack{
Text(name)
Text(phone)
Text(services)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
These are the results
2019-09-19 20:30:34.724594+0600 Venue[16661:712640] - [I-ACS036002] Analytics screen reporting is enabled. Call +[FIRAnalytics setScreenName:setScreenClass:] to set the screen name or override the default screen class name. To disable screen reporting, set the flag FirebaseScreenReportingEnabled to NO (boolean) in the Info.plist
2019-09-19 20:30:34.975422+0600 Venue[16661:712648] 5.12.0 - [Firebase/Analytics][I-ACS023007] Analytics v.50300000 started
2019-09-19 20:30:34.975634+0600 Venue[16661:712648] 5.12.0 - [Firebase/Analytics][I-ACS023008] To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled
Could not cast value of type '__NSCFNumber' (0x10b8a8610) to 'NSString' (0x10954f978).
2019-09-19 20:30:35.073897+0600 Venue[16661:711704] Could not cast value of type '__NSCFNumber' (0x10b8a8610) to 'NSString' (0x10954f978).
(lldb)
As mentioned in the first comment, it looks like you have a cast issue between types.
I tried the same example and was able to build and run successfully, but no data was shown on the screen. After some investigation, I re-wrote some parts of the code and was finally able to get results.
Instead of the video's dataset, I used the following structure:
struct Profile : Identifiable {
var id: Int
var name: String
var phoneNumber: String
init(id: Int, name: String, phoneNumber: String) {
self.id = id
self.name = name
self.phoneNumber = phoneNumber
}
}
And these are the SwiftUI views:
import SwiftUI
import FirebaseFirestore
struct ContentView: View {
#State var data1 = [Profile]()
var body: some View {
return VStack {
HStack {
Text("Penya").font(.largeTitle)
}
Divider()
List(data1) { i in
cellView(name: i.name, phoneNumber: i.phoneNumber)
}
}.onAppear {
self.getData()
}
}
func getData() {
var data = [Profile]()
let db = Firestore.firestore()
db.collection("patients").addSnapshotListener { (snap, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
for doc in (snap?.documentChanges)! {
let name = doc.document.data()["name"] as! String
let phoneNumber = doc.document.data()["phoneNumber"] as! String
print (name)
print (phoneNumber)
DispatchQueue.main.async {
data.append(Profile(id: 0,
name: name,
phoneNumber: phoneNumber))
self.data1 = data
}
}
}
}
}
struct cellView : View {
#State var name = ""
#State var phoneNumber = ""
var body: some View {
VStack {
Text("Valors:")
Text(name)
Text(phoneNumber)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Whenever a record is changed in Firestore, an update is automatically performed on the simulator screen. There is only one thing I am still working on: the updated values are correctly shown in the console (I included two print()), but I always see the same original value on the simulator screen even I change the record in Firestore.
Works with Sergi's Code but he was missing the following:
let id = doc.document.documentID
DispatchQueue.main.async {
data.append(Profile(id: id,
name: name,
phoneNumber: phoneNumber))
self.data1 = data
and change all values for id to be String instead of Int.
I suggest using CombineFirebase
with pod 'FirebaseFirestoreSwift'
Then you can use pure combine pattern for Firestore in your project