Search code examples
swifttimerswiftui

SwiftUI publishing API data every 60 seconds with a Timer


I’ve been chasing my own tail for days.. Maybe the architecture is all wrong. I just can't get it all to work at the same time. Any help would be greatly appreciated.

I have a LoginView which takes an email and password, and validates to the server.

import SwiftUI
struct LoginView: View {

    @EnvironmentObject var userAuth: UserAuth
    @State private var email: String = ""
    @State private var password: String = ""

    var body: some View {
      TextField("Email Address", text: $email)
      SecureField("Password", text: $password)
      Button(action: {
        guard let url = URL(string: "https://www.SomeLoginApi.com/login") else { return }
        let body: [String: String] = ["emailAddress": email, "password": password]
        let finalBody = try! JSONSerialization.data(withJSONObject: body)
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = finalBody
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        URLSession.shared.dataTask(with: request) { (data, _, _) in
          guard let data = data else { return }
          let loginResponse = try! JSONDecoder().decode(ServerResponse.self, from: data)
          if loginResponse.message == "authorized" {
            DispatchQueue.main.async {
                self.userAuth.isLoggedIn = true
                self.userAuth.userId = loginResponse.userId
                AppData().getData(userId: userAuth.userId)
            }
          } else {
            var isLoggedin = false 
          }
        }
        .resume()
      }) {
        Text("LOGIN")
      }
      .disabled(email.isEmpty || password.isEmpty)
    }

If validated, the above code does the following:

  1. Sets isLoggedIn to true which changes the view to MainView via the following StartingView:
import SwiftUI
struct StartingView: View {

    @EnvironmentObject var userAuth: UserAuth

    var body: some View {
        if !userAuth.isLoggedIn {
            LoginView()
        } else {
            MainView()
        }
    }
}
  1. Sends a 2nd API call AppData().getData(userId: userAuth.userId) to the server for data.

Here is the AppData class that the above API is pointing to.

import Foundation
import SwiftUI
import Combine
import CoreImage
import CoreImage.CIFilterBuiltins

class AppData : ObservableObject {
    @Published var userData: AppDataModel = AppDataModel(data1: "", data2: "", data3: "", data4: "", data5: "", data6: "", data7: "", bool1: false, data8: "", data9: "", bool2: true, bool3: true, bool4: true, bool5: true, bool6: true, data10: "", data11: "", data12: "", data13: "", array1:[], array2: [], array3: [], array4: [], array5: [], array6: [])
    @Published var time = ""
    @Published var greet = ""
    @Published var bgImage = Image.init("")
    
    init() {
        var greetingTimer: Timer?
        greetingTimer = Timer.scheduledTimer(timeInterval: 60.0, target: self, selector: #selector(getData), userInfo: nil, repeats: true)
    }
    
    @objc func getData(userId: String) {
        let bgImgArr = ["appBackAnimals1", "appBackAnimals2", "appBackAnimals3", "appBackAnimals4", "appBackAnimals5", "appBackAnimals6", "appBackAnimals7", "appBackAnimals8", "appBackAnimals9", "appBackAnimals10", "appBackAnimals11", "appBackAnimals12", "appBackAnimals13"]
        let bgImg = bgImgArr.randomElement()!
        guard let inputImage = UIImage(named: bgImg) else { return }
        let beginImage = CIImage(image: inputImage)
        let context = CIContext()
        let currentFilter = CIFilter.vignette()
        currentFilter.inputImage = beginImage
        currentFilter.intensity = 6
        // get a CIImage from our filter or exit if that fails
        guard let outputImage = currentFilter.outputImage else { return }
        // attempt to get a CGImage from our CIImage
        if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
            // convert that to a UIImage
            let uiImage = UIImage(cgImage: cgimg)
            // and convert that to a SwiftUI image
            self.bgImage = Image(uiImage: uiImage)
        }
        let today = Date()
        let formatter = DateFormatter()
        formatter.dateFormat = "EEEE h:mma"
        formatter.amSymbol = "am"
        formatter.pmSymbol = "pm"
        let calendar = Calendar.current
        let hour = calendar.component(.hour, from: today)
        time = formatter.string(from: today)

        if hour >= 5 && hour <= 11 {
            greet = "morning"
        } else if hour >= 12 && hour <= 17 {
            greet = "afternoon"
        } else if hour >= 18 && hour <= 20 {
            greet = "evening"
        } else if hour >= 21 && hour <= 24 {
            greet = "night"
        } else if hour >= 0 && hour <= 4 {
            greet = "night"
        } 

        guard let url = URL(string: "https://www.SomeDataApi.com/data") else { return }
        let body: [String: String] = ["userId": userId]
        let finalBody = try! JSONSerialization.data(withJSONObject: body)
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = finalBody
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        URLSession.shared.dataTask(with: request) { (data, _, _) in
            guard let data = data else { return }
            let apiData = try! JSONDecoder().decode(AppDataModel.self, from: data)
            if apiData.message == "data" {
                DispatchQueue.main.async {
                    self.userData = apiData
                }
            }
        }
        .resume()
        if userData.appTopLine == "" {
            userData.appTopLine = "Good " + greet + " " + userData.appName
        } else {
            userData.appTopLine = "not working"
        }
        if userData.appBottomLine == "" {
            userData.appBottomLine = "It's " + time
        }
    }
}

And here is MainView where I want to display the data

import SwiftUI

struct MainView: View {

  @ObservedObject var profileData = AppData()
  @EnvironmentObject var userAuth: UserAuth
  
  var body: some View {
    ZStack {
      profileData.bgImage
      HStack {
        VStack(alignment: .leading) {
            Text(profileData.userData.appTopLine)
            Text(profileData.userData.appBottomLine)
        }
      }
    }
  }
}

Issues that I am experiencing:

  1. I am able to print(apiData) and see the data, however @Published var userData and self.userData = apiData are not making the data available on MainView via @ObservedObject var profileData = AppData()

  2. getData() is not getting triggered every 60 seconds with the Timer because I am not able to figure out how to pass the (userId: userAuth.userId) parameter in there.

I appreciate any direction at all. If this is not set-up in an ideal way, please tell me, I want to do this correctly.

Thank you!


Solution

  • The instance of AppData().getData(userId: userAuth.userId) in LoginView is not the same as @ObservedObject var profileData = AppData().

    The ObservedObject never sees what the LoginView one is doing.

    You have to share the instance by either using the SwiftUI wrappers like you have with UserAuth or a singleton (less recommended).

    class Singleton {
        static let sharedInstance = Singleton()
    }
    

    Also, what is AppDataModel? is it an ObservableObject? You can't chain them.

    If so, these changes are userData.appTopLine = "not working" are not being observed. You won't see them.