Can't access class variables besides from in SwiftIU view - swift

So I'm trying to access my data by id using a url such as http://localhost:8000/albums/whateverid.
First I gain the ids
class Webservice {
func getAllPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "http://localhost:8000/albums")
else {
fatalError("URL is not correct!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try!
JSONDecoder().decode([Post].self, from: data!); DispatchQueue.main.async {
completion(posts)
}
}.resume()
}
}
struct Post: Codable, Hashable, Identifiable {
let id: String
let title: String
let path: String
let description: String
}
Set the variables to the data from class Webservice
final class PostListViewModel: ObservableObject {
init() {
fetchPosts()
}
#Published var posts = [Post]()
private func fetchPosts() {
Webservice().getAllPosts {
self.posts = $0
print("posts \(self.posts)")
}
}
}
And this is how I'm trying to grab album by id by using the id I fetched from the code above
I create a class that when a id is inserted will give me the album data back by id
class SecondWebService: Identifiable {
var id:String = ""
init(id: String?) {
self.id = id!
}
func getAllPostsById(completion: #escaping ([PostById]) -> ()) {
guard let url = URL(string: "http://localhost:8000/albums/\(id)")
else {
fatalError("URL is not correct!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try!
JSONDecoder().decode([PostById].self, from: data!); DispatchQueue.main.async {
completion(posts)
}
}.resume()
}
}
Variables
struct PostById: Codable, Hashable, Identifiable {
let id: String
let name: String?
let path: String
}
Here's where I try to insert the id from class PostListViewModel into my class SecondWebService to get the data back set my variables to that data
final class PostListViewByIdModel: ObservableObject {
#ObservedObject var model = PostListViewModel()
init() {
fetchPostsById()
}
#Published var postsById = [PostById]()
private func fetchPostsById() {
for post in model.posts {
SecondWebService(id: post.id).getAllPostsById {
self.postsById = $0
print("postById \(post)")
}
}
}
}
For some reason above when I try to print nothing will display because I believe posts in model.posts isn't getting read
When I use it here in List() it works but not in init:
struct ContentView: View {
#ObservedObject var model = PostListViewModel()
init() {
for post in model.posts {
print(post)
}
}
var body: some View {
NavigationView {
List(model.posts) { post in
VStack{
Text("Title: ").bold()
+ Text("\(post.title)")
NavigationLink(destination: Album(post: post)) {
ImageView(withURL: "http://localhost:8000/\(post.path.replacingOccurrences(of: " ", with: "%20"))")
}
Text("Description: ").bold()
+ Text("\(post.description)")
}
}
}
}
}
I'm very curious on why nothing is printing when I use model.posts in my for loop. Only when I use it in the SwiftUI functions does it work.

I'm very curious on why nothing is printing when I use model.posts in my for loop
It is because of asynchronous nature of the following call
private func fetchPosts() {
Webservice().getAllPosts {
so in init
#ObservedObject var model = PostListViewModel() // just created
init() {
for post in model.posts { // posts are empty because
// `Webservice().getAllPosts` has not finished yet
print(post)
}
Update: You need to call second service after first one got finished, here is possible approach (only idea - cannot test)
struct ContentView: View {
#ObservedObject var model = PostListViewModel()
#ObservedObject var model2 = PostListViewByIdModel()
// delete init() - it is not needed here
...
var body: some View {
NavigationView {
List(model.posts) { post in
...
}
.onReceive(model.$posts) { posts in // << first got finished
self.model2.fetchPostsById(for: posts) // << start second
}
.onReceive(model2.$postsById) { postById in
// do something here
}
}
}
and updated second service
final class PostListViewByIdModel: ObservableObject {
#Published var postsById = [PostById]()
func fetchPostsById(for posts: [Post]) { // not private now
for post in model.posts {
SecondWebService(id: post.id).getAllPostsById {
self.postsById = $0
print("postById \(post)")
}
}

Related

How to change the value of a var with a TextField SwiftUI

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()
}
}

Problem trying to add Search bar for simple SwiftUI app retrieving web data

I have a small project which is an extension of a Swift UI exercise making a web call to Github from Greg Lim's book Beginning Swift UI:
https://github.com/ethamoos/GitProbe
I’ve been using this to practise basic skills and to try and add other features that could be useful in a realworld app.
My main change from the initial exercise was to add the option to choose which user to lookup (this was previously hardcoded) and allow the user to enter this. Because this can return a lot of data I would now like to make the resulting List .searchable so that the user can filter the results.
I’ve been following this tutorial here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-search-bar-to-filter-your-data
but I’ve realised that this is based upon the data being returned being Strings, and therefore the search is a string.
I am returning JSON decoded into a list of User data objects so a straight search does not work. I am assuming that I can adjust this to match a string search against my custom objects but I'm not sure how to do this.
To give you an idea of what I mean here is the code:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
/// With suggestion added
/// The search results
private var searchResults: [User] {
if searchText.isEmpty {
return fetchUsers.users // your entire list of users if no search input
} else {
return fetchUsers.search(for: searchText) // calls your search method passing your search text
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}
Any help with this would be much appreciated.
Your UserListView is not properly constructed. I don't see why you would need a ScrollView with an empty text inside? I removed that.
So I removed searchText from the View to the FetchUsers class so we can delay the server requests thus avoiding unnecessary multiple calls. Please adjust it to your needs (check Apple's Debounce documentation. Everything should work as expected now.
import Combine
class FetchUsers: ObservableObject {
#Published var users = [User]()
#Published var searchText = ""
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(500), scheduler: RunLoop.main) // debounces the string publisher, delaying requests and avoiding unnecessary calls.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.users = [] // cleans the list results when empty search
return nil
}
return string
}) // prevents sending numerous requests and sends nil if the count of the characters is less than 1.
.compactMap{ $0 } // removes the nil values
.sink { (_) in
//
} receiveValue: { [self] text in
search(for: text)
}.store(in: &subscription)
}
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user.lowercased())]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
guard error == nil else {
print("Error: \(error!.localizedDescription)")
return
}
guard let data = data else {
print("No data received")
return
}
do {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct UserListView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
var body: some View {
NavigationView {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}
.searchable(text: $fetchUsers.searchText) // we move the searchText to fetchUsers
.navigationTitle("Users")
}
}
}
I hope this helps! :)
In the end, I think I've figured this out - thanks to the suggestions from Andre.
I need to correctly filter my data and then return the remainder.
Here's the corrected (abridged) version:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
// print("error")
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(searchResults, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
var searchResults: [User] {
if searchText.isEmpty {
print("Search is empty")
return fetchUsers.users
} else {
print("Search has a value - is filtering")
return fetchUsers.users.filter { $0.login.contains(searchText) }
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}

JSON with SwiftUI using Array

I am new to SwiftUI and only used UIKit before. I tried to use JSON to show a title but all tutorial videos work with lists. I dont want to use any list with JSON which shows all data. Only want to fetch for example the second or a specific array for title.
How can I remove the list in SwiftUI?
My View:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List(networkManager.posts) { post in
HStack {
Text(String(post.points))
Text(post.title)
}}
.navigationBarTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fetchData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
NetworkManager:
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fetchData() {
if let url = URL(string: "https://hn.algolia.com/api/v1/search?tags=front_page") {
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.hits
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
And my struct files for Json:
struct Results: Decodable {
let hits: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return objectID
}
let objectID: String
let points: Int
let title: String
}
I dont want to use any list with JSON which shows all data. Only want
to fetch for example the second or a specific array for title.
You can use a computed property to access the specific element (and its title) from the posts array:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
// return the title of the second item in the `posts` array
var title: String {
guard networkManager.posts.count >= 2 else {
// decide what to do when data is not yet loaded or count is <= 1
return "Loading..."
}
return networkManager.posts[1].title
}
var body: some View {
NavigationView {
Text(title)
.navigationBarTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fetchData()
}
}
}

Swift load user data for dashboard after login

I am trying to retrieve user data once the user gets to the dashboard of my app
I have essentially this to get data:
class UserController: ObservableObject {
#Published var firstName: String = ""
func fetchUser(token: String) {
/* Do url settings */
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
let rData = try! JSONDecoder().decode(User.self, from: data)
let userData = [
"id": rData.id,
"firstName": rData.firstName,
"lastName": rData.lastName,
"department": rData.department,
]
UserDefaults.standard.set(userData, forKey: "user")
DispatchQueue.main.async {
self.firstName = rData.firstName
}
}.resume()
}
}
And then my view looks like this
struct HomeViewCollection: View {
#Binding var isAuthenticated: Bool
#ObservedObject var userController: UserController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
}
}
I'm just not sure how can I activate fetchUser from the View.
I have tried this in the controller
init() {
guard let tokenData = KeyChain.load(key: "token") else { return }
var token = String(data: tokenData, encoding: .utf8)
if(token != nil) {
print("Token: \(token)")
fetchUser(token: token!)
}
}
That didn't work, and then I tried userController.fetchUser(token: KeyChainTokenHere) and that didn't work because it doesn't conform to the struct.
Try passing the token to HomeViewCollection and initiating the call in onAppear completion block.
struct HomeViewCollection: View {
var token: String
#Binding var isAuthenticated: Bool
#ObservedObject var userController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
.onAppear {
self.userController.fetchUser(token: self.token)
}
}
}
Also, make sure the firstName property is getting set.
#Published var firstName: String = "" {
didSet {
print("firstName is set as \(firstName)")
}
}

SwiftUI - Not able to render the jsonData

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")
}
}
}