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..
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()
}
}
I want to refactor the API call that is made using async and await but I am getting the error as the publishing needs to be done on the main thread.
The below is the code that I wrote in the file named LogIn View:-
#State private var quotes = [Quote]()
var body: some View {
NavigationView {
List(quotes, id:\.quote_id) { quote in
VStack(alignment: .leading) {
Text(quote.author)
.font(.headline)
Text(quote.quote)
.font(.body)
}
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Log out") {
authentication.updateValidation(success: false)
}
}
}
.navigationTitle("Dashboard Screen ")
}
.task {
await fetchData()
}
}
func fetchData() async {
//create url
guard let url = URL(string: "https://breakingbadapi.com/api/quotes") else {
print("URL does not work")
return
}
//fetch data from url
do {
let (data, _) = try await URLSession.shared.data(from: url)
//decode that data
if let decodeResponse = try? JSONDecoder().decode([Quote].self, from: data) {
quotes = decodeResponse
}
} catch {
print("Data not valid")
}
}
I want to write the function fetchData() in a separate file and use it here in LogIn View
but upon trying to do so I am getting the error mentioned above. Can anyone Please help me with this.
PS:- all the variables are defined inside another file named variables. The code for that is as follows:-
import Foundation
struct Quote: Codable {
var quote_id: Int
var quote: String
var author: String
var series: String
}
A nice place is in an extension of NSURLSession, e.g.
extension NSURLSession {
func fetchQuotes() async throws -> [Quote] {
//create url
guard let url = URL(string: "https://breakingbadapi.com/api/quotes") else {
print("URL does not work")
return
}
//fetch data from url
let (data, _) = try await data(from: url)
//decode that data
return try JSONDecoder().decode([Quote].self, from: data)
}
}
Then you can simply do:
.task {
do {
quotes = try await URLSession.shared.fetchQuotes()
} catch {
errorMessage = error.description
}
}
This has the advantage you can use it with a different kind of URLSession, e.g. for API requests we usually use an ephemeral session. Another good place would be a static async func in the Quote struct.
to put your func fetchData() in a model and avoid the error, try this approach:
class QuotesModel: ObservableObject {
#Published var quotes = [Quote]()
#MainActor // <-- here
func fetchData() async {
guard let url = URL(string: "https://breakingbadapi.com/api/quotes") else {
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
quotes = try JSONDecoder().decode([Quote].self, from: data)
} catch {
print(error)
}
}
}
struct ContentView: View {
#StateObject var model = QuotesModel()
var body: some View {
NavigationView {
List(model.quotes, id: \.quote_id) { quote in
VStack(alignment: .leading) {
Text(quote.author)
.font(.headline)
Text(quote.quote)
.font(.body)
}
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Log out") {
// authentication.updateValidation(success: false)
}
}
}
.navigationTitle("Dashboard Screen ")
}
.task {
await model.fetchData()
}
}
}
struct Quote: Codable {
var quote_id: Int
var quote: String
var author: String
var series: String
}
Make #MainActor method with #Published in ObservableObject class.
and for model use codable.
struct QuoteView: View {
#State var quotes: [Quote] = []
#ObservedObject var quoteStore = QuoteStore()
var body: some View {
NavigationView {
List(quotes, id:\.quote_id) { quote in
VStack(alignment: .leading) {
Text(quote.author)
.font(.headline)
Text(quote.quote)
.font(.body)
}
}
.navigationTitle("Quotes")
}
.task {
quotes = try! await quoteStore.fetchData()
}
}
}
struct Quote: Codable {
let quote_id = UUID()
let quote: String
let author: String
}
class QuoteStore: ObservableObject {
#Published var quotes: [Quote] = []
#MainActor
func fetchData() async throws -> [Quote] {
guard var url = URL(string: "https://breakingbadapi.com/api/quotes") else { throw AppError.invalidURL }
let (data, _) = try await URLSession.shared.data(from: url)
let repos = try JSONDecoder().decode([Quote].self, from: data)
return repos
}
}
enum AppError: Error {
case invalidURL
}
depending on your code it may just be as simple as putting the code giving you the error inside await MainActor.run { ... }
That said, as a general rule async code is easier to manage when it returns values to use, rather than setting variables from inside functions.
struct SomeFetcher {
func fetchData() async -> [Quotes] {
...
if let decodeResponse = try? JSONDecoder().decode([Quote].self, from: data) {
return decodeResponse
}
...
}
}
struct TheView: View {
var dataGetter = SomeFetcher()
#State private var quotes = [Quote]()
var body: some View {
NavigationView {
}
.task {
quotes = await dataGetter.fetchData()
}
}
}
Editied: Sorry I wrote this code while sleepy. It does not need to be observed in this case when the view is calling the fetch. Other answers have pointed out that the downloaded info is frequently managed by the class that downloads it and published by it.
So I want to search books from google books api, but only through url query, how can I call API again when I enter the text in the search bar? How to reload the call?
I tried also with textfield onSumbit method, but nothing work.
I just want to insert value of textSearch to network.searchText and that network.searchText to insert into q=
here is my code of ContentView:
//
// ContentView.swift
// BookApi
//
// Created by Luka Šalipur on 7.6.22..
//
import SwiftUI
struct URLImage: View{
var urlString: String
#State var data: Data?
var body: some View{
if let data = data, let uiimage = UIImage(data:data) {
Image(uiImage: uiimage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:80, height:120)
.background(Color.gray)
} else {
Image(systemName: "book").onAppear {
fetch()
}
}
}
private func fetch(){
guard let url = URL(string: urlString) else {
return
}
let task = URLSession.shared.dataTask(with:url) { data, _, error in
self.data = data
}
task.resume()
}
}
// ContentView
struct ContentView: View {
#ObservedObject var network = Network()
#State var textSearch:String = "knjiga"
#State private var shouldReload: Bool = false
func context(){
network.searchText = self.textSearch
print(network.searchText)
}
var body: some View {
NavigationView{
List{
ForEach(network.book, id:\.self){ item in
NavigationLink{
Webview(url: URL(string: "\(item.volumeInfo.previewLink)")!)
} label: {
HStack{
URLImage(urlString: item.volumeInfo.imageLinks.thumbnail)
Text("\(item.volumeInfo.title)")
}
}
}
}
.onAppear{
context()
}
.onChange(of: textSearch, perform: { value in
self.shouldReload.toggle()
})
.searchable(text: $textSearch)
.navigationTitle("Books")
.task{
await network.loadData()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And here is my API(network) call:
//
// Network.swift
// BookApi
//
// Created by Luka Šalipur on 7.6.22..
//
import Foundation
import SwiftUI
class Network: ObservableObject{
#Published var book = [Items]()
var searchText: String = "watermelon" {
willSet(newValue) {
print(newValue)
}
}
func loadData() async {
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=\(searchText)&key=API_KEY_PRIVATE") else {
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Books.self, from: data) {
book = decodedResponse.items
}
} catch {
print("There is an error")
}
}
}
This is a perfect candidate for the Combine framework.
In Network create a publisher which removes duplicates, debounces the input for 0.3 seconds, builds the URL, loads the data and decodes it.
I don't have your types, probably there are many errors. But this is a quite efficient way for dynamic searching. By the way your naming with regard to singular and plural form is pretty confusing.
import Combine
import SwiftUI
class Network: ObservableObject {
#Published var book = [Items]()
#Published var query = ""
private var subscriptions = Set<AnyCancellable>()
init() {
searchPublisher
.sink { completion in
print(completion) // show the error to the user
} receiveValue: { [weak.self] books in
self?.book = books.items
}
.store(in: &subscriptions)
}
var searchPublisher : AnyPublisher<Books,Error> {
return $query
.removeDuplicates()
.debounce(for: 0.3, scheduler: RunLoop.main)
.compactMap{ query -> URL? in
guard !query.isEmpty else { return nil }
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=\(query)&key=API_KEY_PRIVATE") else {
return nil
}
return url
}
.flatMap { url -> AnyPublisher<Data, URLError> in
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.eraseToAnyPublisher()
}
.decode(type: Books.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
In the view create the view model (must be #StateObject!)
#StateObject var network = Network()
and bind searchable to query in network
.searchable(text: $network.query)
The view is updated when the data is available in network.book
The .task modifier ist not needed
There is another version of task that runs again when a value changes task(id:priority:_:). If a task is still running when the param changes it will be cancelled and restarted automatically. In your case use it as follows:
.task(id: textSearch) { newValue in
books = await getBooks(newValue)
}
Now we have async/await and task there is no need for an ObservableObject anymore.
I have an observable object class that downloads an image from a url to display:
class ImageLoader : ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(urlString:String){
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
print("imageloader1")
}
}
task.resume()
}
and I show it using:
struct ShowImage1: View {
#ObservedObject var imageLoader:ImageLoader
#State var image:UIImage = UIImage()
init(withURL url:String) {
imageLoader = ImageLoader(urlString:url)
}
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.top)
.onReceive(imageLoader.didChange) {
data in self.image = UIImage(data: data) ?? UIImage()
}
}
The problem I'm having is this is only capable of running once, If i click off the ShowImage1 view and then click back on to it, ImageLoader doesn't run again, and I'm left with a blank page.
How can I ensure that ImageLoader Runs every time the ShowImage1 view is accessed?
EDIT:
I access ShowImage1 like this:
struct PostCallForm: View {
var body: some View {
NavigationView {
Form {
Section {
Button(action: {
if true {
self.showImage1 = true
}
}){
Text("View Camera 1 Snapshot")
}.overlay(NavigationLink(destination: ShowImage1(withURL: "example.com/1.jpg"), isActive: self.$showImage1, label: {
EmptyView()
}))
}
}
Section {
Button(action: {
}){
Text("Submit")
}
}
}.disabled(!submission.isValid)
}
}
}
import SwiftUI
import Combine
class ImageLoader : ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
func loadImage(urlString:String) {
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
print("imageloader1")
}
}
task.resume()
}
}
struct ShowImage1Parent: View {
#State var url: String = ""
var sampleURLs: [String] = ["https://image.shutterstock.com/image-vector/click-here-stamp-square-grunge-600w-1510095275.jpg", "https://image.shutterstock.com/image-vector/certified-rubber-stamp-red-grunge-600w-1423389728.jpg", "https://image.shutterstock.com/image-vector/sample-stamp-square-grunge-sign-600w-1474408826.jpg" ]
var body: some View {
VStack{
Button("load-image", action: {
url = sampleURLs.randomElement()!
})
ShowImage1(url: $url)
}
}
}
struct ShowImage1: View {
#StateObject var imageLoader:ImageLoader = ImageLoader()
#State var image:UIImage = UIImage()
#Binding var url: String
var body: some View {
VStack{
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.top)
.onReceive(imageLoader.didChange) {
data in self.image = UIImage(data: data) ?? UIImage()
}
.onChange(of: url, perform: { value in
imageLoader.loadImage(urlString: value)
})
}
}
}
I am very new to SwiftUi and I am trying to view the json data and I am currently working on retreiving the weather data from the openweathermap.org which is a free api to retrieve current weather. I am getting Error parsing Weather Json message. I am not sure what I am doing wrong!! Any help would be greatly appreciated and I have been stuck on this for a day. I referred many blogs and tutorials on how to use the Published var and ObservableObject I am not able to fix the problem.
This is my swift file
struct WeatherData {
public var Id: Int
public var main: String
public var weather: [Weather]
public var icon: String
}
extension WeatherData: Decodable, Identifiable {
var id: Int {return Id}
}
struct WeatherView: View {
#ObservedObject var fetch = FetchWeather()
var body: some View {
VStack {
List(fetch.weatherData) {
wthr in
VStack(alignment: .leading){
Text("\(wthr.id)")
Text("\(wthr.weather[0].description)")
Text("\(wthr.icon)")
.font(.system(size:11))
.foregroundColor(Color.gray)
}
}
}
}
}
struct Weather: Decodable {
let description: String
}
struct WeatherView_Previews: PreviewProvider {
static var previews: some View {
WeatherView()
}
}
class FetchWeather: ObservableObject {
#Published var weatherData = [WeatherData] ()
init() {
load()
}
func load() {
let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=London&appid=myapikey")!
URLSession.shared.dataTask(with: url) {
(data, response, error) in
do {
if let wthData = data {
let decodedData = try JSONDecoder().decode([WeatherData].self, from: wthData)
DispatchQueue.main.sync {
self.weatherData = decodedData
}
}
else {
print("No json Data available")
}
}catch {
print("Error parsing Weather Json")
}
}.resume()
}
}
Try this code. I have signed up to get the api and corrected the Model, ViewModel and View accordingly. I have not added the image loader for icon strings.
import SwiftUI
struct Weather: Decodable{
var description: String
var icon :String
}
struct MainData: Decodable {
var temp: Double
var pressure: Int
var humidity: Int
var temp_min: Double
var temp_max: Double
}
struct WeatherData: Decodable, Identifiable {
var id: Int
var main: MainData
var weather: [Weather]
var name: String
}
struct WeatherView: View {
#ObservedObject var fetch = FetchWeather()
var body: some View {
VStack(alignment: .leading) {
Text("Current Weather").font(.title).padding()
List(fetch.weatherData) { wthr in
HStack {
VStack(alignment: .leading){
Text("\(wthr.name)")
Text("\(wthr.weather[0].description)")
.font(.system(size:11))
.foregroundColor(Color.gray)
}
Spacer()
VStack(alignment: .trailing){
Text("\(wthr.main.temp-273.15, specifier: "%.1f") ºC")
}
Text("\(wthr.weather[0].icon)") // Image from "https://openweathermap.org/img/w/\(wthr.weather[0].icon).png"
.foregroundColor(Color.gray)
}
}
}
}
}
class FetchWeather: ObservableObject {
#Published var weatherData = [WeatherData]()
private let baseURL = "https://api.openweathermap.org/data/2.5/weather?q="
private let cities = [ "London", "Mumbai", "New+york", "Vatican+City" ]
private let api = "&appid="+"e44ebeb18c332fff46ab956bb38f9e07"
init() {
for city in self.cities {
self.load(self.baseURL+city+self.api)
}
}
func load(_ urlString: String) {
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
if let wthData = data {
let decodedData = try JSONDecoder().decode(WeatherData.self, from: wthData)
DispatchQueue.main.sync {
self.weatherData.append(decodedData)
}
}
else {
print("No json Data available")
}
} catch let error as NSError{
print(error.localizedDescription)
}
}.resume()
} else {
print("Unable to decode URL")
}
}
}