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:
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()
}
}
}
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:
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()
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!
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.
#Published var timeElapsed = false
func delayText() {
// Delay of 7.5 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 7.5) {
self.timeElapsed = true
}
}
That is a timer, hope this can help. This works for me, hope it can for you.
Related
I was trying to make a weather api call, the api call needs to have a location. The location that I pass is a variable, but now I want to change the location value based on a TextField's input.
I made the apiKey shorter just for safety measures. There's more code, but it's not relevant.
I just need to know how to change the city variable that is on the WeatherClass using the TextField that is in the cityTextField View.
Thanks.
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
var city: String = ""
func fetch() {
let location = city
let apiKey = "AP8LUYMSTHZ"
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(location)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}//----------------------------------- End of fetch()
}
struct WeatherData: Decodable {
let resolvedAddress: String
let days: [WeatherDays]
}
struct WeatherDays: Hashable, Decodable {
let datetime: String
let tempmax: Double
let tempmin: Double
let description: String
}
struct cityTextField: View {
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city).frame(height:30).multilineTextAlignment(.center).background().cornerRadius(25).padding(.horizontal)
}
}
I already watched a lot of tutorials for similar things buts none of them really helped me.
Try this approach using minor modifications to
func fetch(_ city: String){...} to fetch the weather for the city in your
TextField using .onSubmit{...}
struct ContentView: View {
#StateObject var weatherModel = WeatherClass()
var body: some View {
VStack {
cityTextField(weatherModel: weatherModel)
}
}
}
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city)
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch(city) // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
func fetch(_ city: String) { // <-- here
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}
Alternatively, as suggested by synapticloop, you could use this approach:
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
var body: some View {
TextField("Search city", text: $weatherModel.city) // <-- here
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch() // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
#Published var city: String = "" // <-- here
func fetch() {
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}
struct LoginView: View {
#ObservedObject var vm : ViewModel
#State private var username = ""
private var searchAllowed : Bool{
if(username.count>2)
{
return false
}
return true
}
var body: some View {
NavigationView{
VStack{
Text("Enter your Username:")
.font(.title.bold())
TextField("Username", text: $username)
.frame(width: 280)
.padding(.bottom)
NavigationLink{
if (vm.apiLoaded)
{
ProfileView(vm: vm)
}
else{
ProgressView()
.task {
await vm.userToUUID(username: username)
await vm.fetchPlayerData()
}
}
} label:
{
ZStack{
RoundedRectangle(cornerRadius: 5)
.fill(.gray)
.frame(width: 200, height: 50)
Text("Search")
.foregroundColor(.white)
}
}
.disabled(searchAllowed)
}
}
}
}
So I have these two Views with two async methods getting called in the else clause in the NavigationLink, but my problem is that I'm not getting the expected result and I'm assuming that's because both async functions start running at the same time.
The fetchPlayerData one uses UUID to get the PlayerData, is there some way I can make it get the UUID first and only then fetchPlayerData?
ViewModel:
import Foundation
#MainActor final class ViewModel : ObservableObject{
#Published var apiLoaded : Bool = false
#Published var player : Player? //player is kinda our model
func userToUUID(username : String) async {
let playersURLString = "https://api.mojang.com/users/profiles/minecraft//(username)"
if let url = URL(string: playersURLString){
do{
let (data, ) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
let uuid = try decoder.decode(UUIDConversion.self, from: data).id
print(uuid)
player?.uuid = uuid
} catch{
print("bad")
}
}
}
func fetchPlayerData() async{
let playersURLString = "https://api.hypixel.net/player?key=key&uuid=81f5de24b017466aaab4551a3fb38e5c"
if let url = URL(string: playersURLString){
do {
let (data, ) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
player = try decoder.decode(ApiResponse.self, from: data).player
print(player)
apiLoaded = true
} catch {
print("Error")
}
}
}
}
Edit: If I add a UUID property to my ViewModel and then set that in the UUID function if I refer to player.uuid then it works for some reason
#Published var uuid : String = ""
#Published var player : Player? //player is kinda our model
func userToUUID(username : String) async {
let playersURLString = "https://api.mojang.com/users/profiles/minecraft//(username)"
if let url = URL(string: playersURLString){
do{
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
let uuid = try decoder.decode(UUIDConversion.self, from: data).id
print(uuid)
player?.uuid = uuid
self.uuid = uuid
} catch{
print("bad")
}
}
}
But there's something really weird here, in my profileview:
struct ProfileView: View {
#ObservedObject var vm : ViewModel
var body: some View {
List{
VStack{
Text(vm.player?.displayname ?? "T")
.padding(.vertical, 0)
AsyncImage(url: URL(string: "https://crafatar.com/renders/body//(vm.uuid)%22)) { image in
image
.center()
} placeholder: {
ProgressView()
}
}
vm.player?.displayname works and gives me the value but for some reason vm.player?.uuid doesn't work and only vm.uuid works..
I am showing daily step information in one of the tabs. Unfortunately, when I select the steps tab again it adds one more of the same data below the previous one.
I have tried to solve it via toggle a boolean. But it did not help either.
import SwiftUI
import HealthKit
struct StepView: View {
private var healthStore: HealthStore?
#State private var presentClipboardView = true
#State private var steps: [Step] = [Step]()
init() {
healthStore = HealthStore()
}
private func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
statisticsCollection.enumerateStatistics(from: startOfDay, to: now) { (statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
let step = Step(count: Int(count ?? 0), date: statistics.startDate, wc: Double(count ?? 0 / 1000 ))
steps.append(step)
}
}
var body: some View {
VStack {
ForEach(steps, id: \.id) { step in
VStack {
HStack{
Text("WC")
Text("\(step.wc)")
}
HStack {
Text("\(step.count)")
Text("Total Steps")
}
Text(step.date, style: .date)
.opacity(0.5)
Spacer()
}
}
.navigationBarBackButtonHidden(true)
}
.onAppear() {
if let healthStore = healthStore {
healthStore.requestAuthorization { (success) in
if success {
healthStore.calculateSteps { (statisticsCollection) in
if let statisticsCollection = statisticsCollection {
updateUIFromStatistics(statisticsCollection)
}
}
}
}
}
}
.onDisappear() {
self.presentClipboardView.toggle()
}
}
}
Step Model
struct Step: Identifiable {
let id = UUID()
let count: Int?
let date: Date
let wc: Double
}
HealthStore file
class HealthStore {
var healthStore: HKHealthStore?
var query: HKStatisticsCollectionQuery?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
func calculateSteps(completion: #escaping (HKStatisticsCollection?) -> Void ) {
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let daily = DateComponents(day:1)
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
query = HKStatisticsCollectionQuery(quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum, anchorDate: startOfDay, intervalComponents: daily)
query!.initialResultsHandler = { query, statisticCollection, error in
completion(statisticCollection)
}
if let healthStore = healthStore, let query = self.query {
healthStore.execute(query)
}
}
func requestAuthorization(completion: #escaping (Bool) -> Void) {
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
guard let healthStore = self.healthStore else { return completion (false) }
healthStore.requestAuthorization(toShare: [], read: [stepType]) { (success, error) in
completion(success)
}
}
}
Those are all related files according to my problem.
Thanks in advance.
The steps is annotated with #State which means it will be persisted even when the view is redrawn.
And you never reset it. You only append new steps. Try clearing steps in updateUIFromStatistics:
private func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
steps = [] // remove previous values
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
statisticsCollection.enumerateStatistics(from: startOfDay, to: now) { (statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
let step = Step(count: Int(count ?? 0), date: statistics.startDate, wc: Double(count ?? 0 / 1000 ))
steps.append(step)
}
}
Ok.. probably bad title. But here, the problem.
struct DeckView: View {
#State public var results = [ScryfallCard]()
var body: some View {
List(results, id: \.id ) { item in
Mkae a list containing the results.
}.onAppear {
ScryfallData().parseBulkData()
print("Type of results::", type(of: results))
print("results.capacity:", results.capacity)
}
}
}
struct ScryfallData {
func parseBulkData() {
let fm = FileManager.default
let path = Bundle.main.resourcePath
let items = try! fm.contentsOfDirectory(atPath: path!)
var oracleFileName = ""
for fileName in items {
if fileName .hasPrefix("oracle-cards"){
oracleFileName = fileName
}
}
print("if let savedJson = Bundle.main.url")
if let savedJson = Bundle.main.url(forResource: oracleFileName, withExtension: "") {
if let dataOfJson = try? Data(contentsOf: savedJson) {
print("if let dataOfJSON: \(dataOfJson)")
do {
let scryfallDecodeData = try JSONDecoder().decode([ScryfallCard].self, from: dataOfJson)
print("scryfallDecodeData.capacity:", scryfallDecodeData.capacity)
/* error here*/ DeckView().results = scryfallDecodeData
print("DeckView().results: ", DeckView().results)
print("Decoded data:", type(of: scryfallDecodeData))
} catch {
debugPrint("decode failed")
}
}
}
}
}
I keep getting a blank List this in the debugger...
if let dataOfJSON: 73545913 bytes
scryfallDecodeData.capacity: 24391
DeckView().results: []
Decoded data: Array<ScryfallCard>
Type of results:: Array<ScryfallCard>
results.capacity: 0
This means that oiver on the line marked Error Here, I'm asigning the decoded data to the DeckView().results var, but the end result is the data is not getting asigned. Any idea what I'm doing wrong?
You should not be creating a View from view model (ScryfallData), but instead return the decoded data from the parseBulkData function and assign that to results inside the onAppear of your View.
Your models should never know about your UI. Your UI (View in case of SwiftUI) should own the models, not the other way around. This achieves good separation of concerns and also makes your business logic platform and UI agnostic.
struct DeckView: View {
#State public var results = [ScryfallCard]()
var body: some View {
List(results, id: \.id ) { item in
Text(item.text)
}.onAppear {
self.results = ScryfallData().parseBulkData()
}
}
}
struct ScryfallData {
func parseBulkData() -> [ScryfallCard] {
let fm = FileManager.default
let path = Bundle.main.resourcePath
let items = try! fm.contentsOfDirectory(atPath: path!)
var oracleFileName = ""
for fileName in items {
if fileName .hasPrefix("oracle-cards"){
oracleFileName = fileName
}
}
if let savedJson = Bundle.main.url(forResource: oracleFileName, withExtension: "") {
do {
let jsonData = try Data(contentsOf: savedJson)
let scryfallDecodeData = try JSONDecoder().decode([ScryfallCard].self, from: jsonData)
return scryfallDecodeData
} catch {
debugPrint("decode failed")
return []
}
}
return []
}
}
I have such view:
struct PersonalPage: View {
let preferences = UserDefaults.standard
var body: some View {
VStack(alignment:.leading){
HStack {
Image(systemName: "mappin.circle.fill")
VStack {
Text("88")
Text("88")
Text("88")
}
}
}.onAppear {
self.getPersonalData()
}
}
var mainSession = Session(configuration: URLSessionConfiguration.default, interceptor: EnvInterceptor())
func getPersonalData(){
var request = URLRequest(url: URL(string:Pathes.init().userInfo)!)
request.httpMethod = "GET"
mainSession.request(request).responseDecodable(of:PersonalInfo.self) { (response) in
switch response.result{
case .success:
guard let userData = response.value else { return }
self.preferences.set(userData.applicant.email,forKey: "applicant_email")
self.preferences.set(userData.applicant.id, forKey: "applicant_id")
self.preferences.set(userData.applicant.delete, forKey: "applicant_can_delete")
self.preferences.set(userData.applicant.caseManager, forKey: "casemanager_assigned")
self.preferences.set(userData.applicant.photoChecksum, forKey: "photo_checksum")
self.preferences.set(userData.consultant.firstname, forKey: "cons_firstname")
self.preferences.set(userData.consultant.lastname, forKey: "cons_lastname")
self.preferences.set(userData.consultant.id, forKey: "cons_id")
self.preferences.synchronize()
HomeScreen().self.addBadges()
case .failure:
print(response.response?.statusCode as Any)
}
}
}
}
and as you can see I have method for getting data from api. So, is it possible to update for example Text with some text from server response and if it possible how I can do it? As I understood I can't do smth like global view variable and have access to its elements from any place of struct?
If you declare a #State property for each of your desired Text views you can use them in the View body and update them from your function. Here is a brief example I hope helps;
struct PersonalPage: View {
let preferences = UserDefaults.standard
#State private var textOne = ""
#State private var textTwo = ""
#State private var textThree = ""
var body: some View {
VStack(alignment:.leading){
HStack {
Image(systemName: "mappin.circle.fill")
VStack {
Text(textOne)
Text(textTwo)
Text(textThree)
}
}
}.onAppear {
self.getPersonalData()
}
}
func getPersonalData() {
textOne = "Hello World"
textTwo = "Map"
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
textThree = "Delayed Text"
}
}
}
struct PersonalPage: View {
#State var personalDataModel = PersonalDataModel()
var body: some View {
VStack(alignment:.leading){
HStack {
Image(systemName: "mappin.circle.fill")
VStack {
Text(personalDataModel.cons_lastname)
Text(personalDataModel.cons_firstname)
Text(personalDataModel.photo_checksum)
}
}
}.onAppear {
self.getPersonalData()
}
}
var mainSession = Session(configuration: URLSessionConfiguration.default, interceptor: EnvInterceptor())
func getPersonalData(){
var request = URLRequest(url: URL(string:Pathes.init().userInfo)!)
request.httpMethod = "GET"
mainSession.request(request).responseDecodable(of:PersonalInfo.self) { (response) in
switch response.result{
case .success:
guard let userData = response.value else { return }
let personalDataModel = PersonalDataModel()
personalDataModel.applicant_email = userData.applicant.email
personalDataModel.applicant_id = userData.applicant.id
personalDataModel.cons_firstname = userData.consultant.firstname
self.personalDataModel = personalDataModel
HomeScreen().self.addBadges()
case .failure:
print(response.response?.statusCode as Any)
}
}
}
}
struct PersonalDataModel : Codable {
public var applicant_email: String = ""
public var id: String = ""
public var applicant_can_delete: String = ""
public var applicant_id: String = ""
public var designation: String = ""
public var casemanager_assigned: String = ""
public var photo_checksum: String = ""
public var cons_firstname: String = ""
public var cons_lastname: String = ""
public var cons_id: String = ""
}