import SwiftUI
import Firebase
#main
struct TSUDateApp: App {
#AppStorage(CurrentUserDefaults.userID) var currentUserID: String?
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
if currentUserID != nil {
ContentView().environmentObject(AuthViewModel())
.environmentObject(MessageListVM(userID: currentUserID!))
} else {
ContentView().environmentObject(AuthViewModel())
}
}
}
}
I want to change ContentViews environment object based on Condition, but it iterates only once and it is not updated if currentUserIDchanges, if i want to get logic I want i have to reload app than it is working. I tried .onReceive and .onChange, but it always fails.
I am changin currentUserID in signOut() function here
func signOut() {
navigateToLoginView()
let defaultsDictionary = UserDefaults.standard.dictionaryRepresentation()
defaultsDictionary.keys.forEach { key in
UserDefaults.standard.removeObject(forKey: key)
}
try? Auth.auth().signOut()
}
The issue is that #AppStorage does not refresh your view if something changes UserDefaults through any other method or even another AppStorage with the same key.
The most simple solution in this case would be to move the currentUserID to one of the ViewModels. As you are using AuthViewModel for both cases it seems that this is the appropriate container for this.
class AuthViewModel: ObservableObject{
#AppStorage("currentUserID") var currentUserID: String?
}
struct TSUDateApp: App {
#StateObject private var authViewModel = AuthViewModel()
var body: some Scene {
WindowGroup {
if let currentUserID = authViewModel.currentUserID{
ContentView()
.environmentObject(authViewModel)
.environmentObject(MessageListVM(userID: currentUserID))
} else {
ContentView().environmentObject(authViewModel)
}
}
}
}
and yoursignOut function would look like this:
func signOut() {
navigateToLoginView()
authViewModel.currentUserId = nil
try? Auth.auth().signOut()
}
of course you would need to pull it from the environment first:
#EnvironmentObject private var authViewModel: AuthViewModel
Another approach would be to pass your currentUserID on as a Binding to your ContentView:
struct TSUDateApp: App {
#AppStorage("currentUserID") var currentUserID: String?
var body: some View{
if let currentUserID = currentUserID{
ContentView(currentUserID: $currentUserID)
.environmentObject(AuthViewModel())
.environmentObject(MessageListVM(userID: currentUserID))
} else {
ContentView(currentUserID: $currentUserID).environmentObject(AuthViewModel())
}
}
}
In ContentView create the Binding and manipulate it in signOut.
#Binding var currentUserID: String?
func signOut() {
navigateToLoginView()
currentUserId = nil
try? Auth.auth().signOut()
}
Related
There is a problem of the following nature: it is necessary to create an authorization window for the application, the most logical solution I found the following implementation (I had to do this because the mainView has a tabView which behaves incorrectly if it is in a navigationView)
struct ContentView: View {
#EnvironmentObject var vm: AppSettings
var body: some View {
if vm.isLogin {
MainView()
} else {
LoginView()
}
}
AppSettings looks like this:
struct MyApp: App {
#StateObject var appSetting = AppSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appSetting)
}
}
}
class AppSettings: ObservableObject {
#Published var isLogin = false
}
By default, the user will be presented with an authorization window that looks like this:
struct LoginView: View {
#StateObject var vm = LoginViewModel()
var body: some View {
NavigationView {
VStack {
TextField("Email", text: $vm.login)
TextField("Password", text: $vm.password)
Button {
vm.auth()
} label: {
Text("SignIn")
}
}
}
}
}
And finally the loginViewModel looks like this:
class LoginViewModel: ObservableObject {
#Published var login = ""
#Published var password = ""
//#Published var appSettings = AppSettings() -- error on the first screenshot
//or
//#EnvironmentObject var appSettings: AppSettings -- error on the second screenshot
func auth() {
UserAPI().Auth(req: LoginRequest(email: login, password: password)) { response, error in
if let err = error {
// Error Processing
} else if let response = response {
Defaults.accessToken = response.tokens.accessToken
Defaults.refreshToken = response.tokens.refreshToken
self.appSettings.isLogin = true
}
}
}
}
1 error - Accessing StateObject's object without being installed on a View. This will create a new instance each time
2 error - No ObservableObject of type AppSettings found. A View.environmentObject(_:) for AppSettings may be missing as an ancestor of this view
I ask for help, I just can not find a way for the interaction of two observableObject. I had to insert all the logic into the action of the button to implement such functionality
In addition to this functionality, it is planned to implement an exit from the account by changing the isLogin variable to false in various cases or use other environment variables to easily implement other functions
The example is deliberately simplified for an easy explanation of the situation
I would think using only LoginViewModel at top level would be the easiest way to solve this. But if you want to keep both, you can synchronise them with an .onChanged modifier.
class LoginViewModel: ObservableObject {
#Published var login = ""
#Published var password = ""
#Published var isLogin = false
func auth() {
UserAPI().Auth(req: LoginRequest(email: login, password: password)) { response, error in
if let err = error {
// Error Processing
} else if let response = response {
Defaults.accessToken = response.tokens.accessToken
Defaults.refreshToken = response.tokens.refreshToken
isLogin = true
}
}
}
}
struct LoginView: View {
#StateObject var vm = LoginViewModel()
// grab the AppSettings from the environment
#EnvironmentObject var appSetting: AppSettings
var body: some View {
NavigationView {
VStack {
TextField("Email", text: $vm.login)
TextField("Password", text: $vm.password)
Button {
vm.auth()
} label: {
Text("SignIn")
}
// synchronise the viewmodels here
.onChange(of: vm.isLogin) { newValue in
appSetting.isLogin = newValue
}
}
}
}
}
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))
}
}
}
So, this is my View Model
import Foundation
import SwiftUI
import Combine
import Alamofire
class AllStatsViewModel: ObservableObject {
#Published var isLoading: Bool = true
#Published var stats = [CountryStats]()
func fetchGlobalStats() {
let request = AF.request("https://projectcovid.deadpool.wtf/all")
request.responseDecodable(of: AllCountryStats.self) { (response) in
guard let globalStats = response.value else { return }
DispatchQueue.main.async {
self.stats = globalStats.data
}
self.isLoading = false
}
}
}
And this is my view where I subscribe to change:
struct CardView: View {
#ObservedObject var allStatsVM = AllStatsViewModel()
var body: some View {
VStack {
if self.allStatsVM.stats.count > 0 {
Text(self.allStatsVM.stats[0].country)
} else {
Text("data loading")
}
}
.onAppear {
self.allStatsVM.fetchGlobalStats()
}
}
}
So, when I open the app for the first time, I get the data and then when I go home and reopen the app, all I can see is data loading.
Is there a way to persist data? I know #State helps but, I'm a beginner in SwiftUI and not sure how it works
every time you open CardView you create a new:
#ObservedObject var allStatsVM = AllStatsViewModel()
what you probably want is to create that in the home view, and pass in the ObservedObject from the home view to the CarView, where you declare:
#ObservedObject var allStatsVM: AllStatsViewModel
The data will then persist, and when CardView appear again it will show it.