OnAppear only once the view is opened - swift

I want to update the view with data when a view is opened so I added:
.onAppear {
loadData()
}
But I only want to update it once when the view gets opened not every time it gets reopened e.g. with a back button.
--> Only update on App start

You can put the value in UserDefaults so you will know in the second load that you have already performed loading.
extension UserDefaults {
var firstTimeVisit: Bool {
get {
return UserDefaults.standard.value(forKey: "firstTimeVisit") as? Bool ?? true
} set {
UserDefaults.standard.setValue(newValue, forKey: "firstTimeVisit")
}
}
}
struct ContentView: View {
var body: some View {
VStack {
}.task {
if UserDefaults.standard.firstTimeVisit {
// load Data
UserDefaults.standard.firstTimeVisit = false
}
}
}
}
UPDATE:
extension UserDefaults {
var firstTimeVisit: Bool {
get {
return !UserDefaults.standard.bool(forKey: "firstTimeVisit")
} set {
UserDefaults.standard.set(newValue, forKey: "firstTimeVisit")
}
}
}

Related

Getting the out of range index error when trying to make a Page Tab View from API array

This is my Model:
class Api {
func getRockets(completion: #escaping ([Rocket]) -> ()) {
guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
let rockets = try JSONDecoder().decode([Rocket].self, from: data!)
DispatchQueue.main.async {
completion(rockets)
}
} catch {
print(error.localizedDescription)
}
}
.resume()
}
}
I try to make PageTabView using elements from the API array, but my app crashes with an out of range index error.
This is the View that doesn't work properly:
struct TestTabViewView: View {
#State var rockets: [Rocket] = [] //<-- The array of items from the API I use to make tabs
var body: some View {
TabView {
ForEach(rockets) { rocket in
Text(rocket.name)
}
}
.onAppear {
Api().getRockets { rockets in
self.rockets = rockets
}
}
.tabViewStyle(.page)
}
}
struct TestTabViewView_Previews: PreviewProvider {
static var previews: some View {
TestTabViewView()
}
}
If you put TabView inside an if-else statement, and in turn put this statement inside NavigationView or Group, and then call the .onAppear method for an external container (for example, Group), everything will work properly and there will be no an out of range index error.
struct TestTabViewView: View {
#State var rockets: [Rocket] = []
var body: some View {
Group {
if rockets.isEmpty {
ProgressView()
} else {
TabView {
ForEach(rockets) { rocket in
Text(rocket.name)
}
}
.tabViewStyle(.page)
}
}
.onAppear {
Api().getRockets { rockets in
self.rockets = rockets
}
}
}
}

SwiftUI Firebase Authentication dismiss view after successfully login

I'm a beginner iOS developer and I have a problem with my first application. I'm using Firebase as a backend for my app and I have already sign in and sing up methods implemented. My problem is with dismissing LoginView after Auth.auth().signIn method finishing. I've managed to do this when I'm using NavigationLink by setting ObservableObject in isActive:
NavigationLink(destination: DashboardView(), isActive: $isUserLogin) { EmptyView() }
It's working as expected: when app ending login process screen is going to next view - Dashboard.
But I don't want to use NavigationLink and creating additional step, I want just go back to Dashboard using:
self.presentationMode.wrappedValue.dismiss()
In this case I don't know how to force app to wait till method loginUser() ends. This is how my code looks now:
if loginVM.loginUser() {
appSession.isUserLogin = true
self.presentationMode.wrappedValue.dismiss()
}
I've tried to use closures but it doesn't work or I'm doing something wrong.
Many thanks!
You want to use a AuthStateDidChangeListenerHandle and #EnvrionmentObject, like so:
class SessionStore: ObservableObject {
var handle: AuthStateDidChangeListenerHandle?
#Published var isLoggedIn = false
#Published var userSession: UserModel? { didSet { self.willChange.send(self) }}
var willChange = PassthroughSubject<SessionStore, Never>()
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({ [weak self] (auth, user) in
if let user = user {
let firestoreUserID = API.FIRESTORE_DOCUMENT_USER_ID(userID: user.uid)
firestoreUserID.getDocument { (document, error) in
if let dict = document?.data() {
//Decoding the user, you can do this however you see fit
guard let decoderUser = try? UserModel.init(fromDictionary: dict) else {return}
self!.userSession = decoderUser
}
}
self!.isLoggedIn = true
} else {
self!.isLoggedIn = false
self!.userSession = nil
}
})
}
func logOut() {
do {
try Auth.auth().signOut()
print("Logged out")
} catch let error {
debugPrint(error.localizedDescription)
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
print("deinit - seession store")
}
}
Then simply do something along these lines:
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
ZStack {
Color(SYSTEM_BACKGROUND_COLOUR)
.edgesIgnoringSafeArea(.all)
Group {
if session.isLoggedIn {
DashboardView()
} else if !session.isLoggedIn {
SignInView()
}
}
}.onAppear(perform: listen)
}
}
Then in your app file, you'd have this:
InitialView()
.environmentObject(SessionStore())
By using an #EnvironmentObject you can now access the user from any view, furthermore, this also allows to track the Auth status of the user meaning if they are logged in, then the application will remember.

Change to SwiftUI state variable in for loop (UIKit) does not occur

So I have a swift view as where the minimum example is something as follows (It is a UIView but for simplicity sake I'm going to make it a SwiftUI view):
class ViewName: UIView {
#State var time: String = ""
func setTime() {
for place in self.data.places {
print("the place address is \(place.address) and the representedobject title is \((representedObject.title)!!)")
if (self.representedObject.title)!! == place.address {
print("there was a match!")
print("the time is \(place.time)")
self.time = place.time
print("THE TIME IS \(self.time)")
}
}
print("the final time is \(self.time)")
}
var body: some View {
//setTime() is called in the required init() function of the View, it's calling correctly, and I'm walking through my database correctly and when I print place.time, it prints the correct value, but it's the assignment self.time = place.time that just doesn't register. If I print place.time after that line, it is just the value ""
}
}
Reference type is not allowed to be a SwiftUI View. We cannot do the following:
class ViewName: UIView, View {
...
}
, so probably you meant this
struct ViewName: View {
// ... other properties
#State var time: String = ""
func setTime() {
for place in self.data.places {
if self.representedObject.title == place.address {
self.time = place.time
}
}
}
var body: some View {
Text("Some View Here")
.onAppear {
self.setTime() // << here !!
}
}
}

Reload data in SwiftUI using MVVM using filters

I have this code in an MVVM code in SwiftUI. My Objective is when the app loads for the first time to return the result without the filter. When I press the button on the view to trigger CatLotViewModel to reload the filtered data but can't seem to figure out I can trigger it.
class CatLotViewModel: ObservableObject {
//MARK: - Properties
#Published var catViewModel = [CatViewModel]()
private var cancellabels = Set<AnyCancellable>()
init() {
MyAPIManager().$cat.map{ kitten in
// Filter
let filtered = kitten.filter{ ($0.meals.contains(where: {$0.feed == false}))}
return filtered.map { park in
MyCatViewModel(parking: park)
}
// return kitten.map { park in
// CatViewModel(parking: park)
// }
}
.assign(to: \.catViewModel, on: self)
.store(in: &cancellabels)
}
}
Add a load function to your view model with isFiltered as a parameter. Call the function when pressing a button.
struct ContentView: View {
#ObservedObject var catLotViewModel = CatLotViewModel()
var body: some View {
Button(action: { self.catLotViewModel.load(isFiltered: true) }) {
Text("Reload")
}
}
}
class CatLotViewModel: ObservableObject {
//MARK: - Properties
#Published var catViewModel = [CatViewModel]()
private var cancellabels = Set<AnyCancellable>()
init() {
loadData(isFiltered: false)
}
func loadData(isFiltered: Bool) {
MyAPIManager().$cat.map{ kitten in
if isFiltered {
let filtered = kitten.filter{ ($0.meals.contains(where: {$0.feed == false}))}
return filtered.map { park in
MyCatViewModel(parking: park)
}
} else {
return kitten.map { park in
CatViewModel(parking: park)
}
}
.assign(to: \.catViewModel, on: self)
.store(in: &cancellabels)
}
}

Display sheet within init at initial app lunch

I created an init like this:
init() {
// Show what's new on the update from App Store
if !UserDefaults.standard.bool(forKey: "appStoreUpdateNotification_2.2.0") {
UserDefaults.standard.set(true, forKey: "appStoreUpdateNotification_2.2.0")
}
}
How can I include the sheet there? Until now I was using a button to display it. This is to show the sheet when I update the app to inform the users of changes
You can try setting a #State variable and use it to present a sheet:
struct ContentView: View {
#State var isShowingModal: Bool = false
init() {
if !UserDefaults.standard.bool(forKey: "appStoreUpdateNotification_2.2.0") {
UserDefaults.standard.set(true, forKey: "appStoreUpdateNotification_2.2.0")
_isShowingModal = .init(initialValue: true)
}
}
var body: some View {
Text("")
.sheet(isPresented: $isShowingModal) {
SomeOtherView()
}
}
}