SwiftUI View not updating when Published variable is changed - ios

On a button press, I my app is trying to contact an api to receive data. This data is then stored in a published variable inside an observable object. For some reason, the view doesn't populate with the data until the button that opens that view is press more than once. I am looking for the view to update with the information received from the api call on the first button press. The code I am referencing is provided below:
DataFetcher.swift:
class DataFetcher: ObservableObject{
#Published var dataHasLoaded: Bool = false
#Published var attendeesLoaded: Bool = false
#Published var useresUventsLoaded: Bool = false
#Published var profilesLoaded: Bool = false
#Published var eventsUpdated: Bool = false
#Published var events: [eventdata] = []
#Published var createdEvents: [eventdata] = []
#Published var profile: profiledata?
#Published var atendees: [atendeedata] = []
#Published var IAmAtending: [atendeedata] = []
#Published var eventNames: [eventdata] = []
#Published var profileList: [profiledata] = []
#Published var token: String = UserDefaults.standard.string(forKey: "Token") ?? ""
private var id: Int = 0
func fetchProfile(id: Int){
// events.removeAll()
profileUrl.append("/\(id)")
self.id = id
let url = URL(string: profileUrl)!
var request = URLRequest(url: url)
if let range = profileUrl.range(of: "/\(id)") {
profileUrl.removeSubrange(range)
}
request.httpMethod = "GET"
print(self.token)
request.addValue("Token \(self.token)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request, completionHandler: parseFetchProfileObject)
task.resume()
}
func parseFetchProfileObject(data: Data?, urlResponse: URLResponse?, error: Error?){
guard error == nil else {
print("\(error!)")
return
}
guard let content = data else{
print("No data")
return
}
if let decodedResponse = try? JSONDecoder().decode(profiledata?.self, from: content) {
DispatchQueue.main.async {
self.profile = decodedResponse
self.profileList.append(self.profile!)
}
}
}
func fetchAtendees(id: Int){
// events.removeAll()
atendeeUrl.append("/\(id)")
print(atendeeUrl)
let url = URL(string: atendeeUrl)!
var request = URLRequest(url: url)
if let range = atendeeUrl.range(of:"/\(id)") {
atendeeUrl.removeSubrange(range)
}
request.httpMethod = "GET"
print(self.token)
request.addValue("Token \(self.token)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request, completionHandler: parseFetchAttendeesObject)
task.resume()
}
EventsUserCreatedView.swift
import Foundation
import SwiftUI
import Mapbox
struct EventsUserCreatedView: View {
#Binding var token: String
#State private var pressedEvent: Bool = false
#State private var selectedEvent: Int = 0
#State private var atendees: [atendeedata] = []
#State private var profileList: [profiledata] = []
#State private var showDeleteEventView: Bool = false
var data: DataFetcher
var mapStyle: URL
var body: some View {
ZStack{
//NavigationView {
if self.mapStyle == MGLStyle.darkStyleURL {
List{
ForEach(self.data.createdEvents){ row in
HStack {
Button("\((row.poi)!)") {
print("Display event information")
self.selectedEvent = row.id
self.pressedEvent = true
}
Spacer()
Button("Delete") {
self.showDeleteEventView = true
print("Deletes the event in this row")
}.buttonStyle(BorderlessButtonStyle())
.padding(4)
.background(Color.red)
.cornerRadius(5)
}.foregroundColor(Color.white)
}
}.background(Color.init(red: 0.05, green: 0.05, blue: 0.05))
//if you hit more buttons than there is room for, it'll be scrollable. make some kind of for loop that iterates over events a user is going to and displays it
// }.navigationBarTitle("My Events")
// .navigationViewStyle(StackNavigationViewStyle())
if pressedEvent{
Group{
if self.data.profilesLoaded == true{
//NavigationView {
List{
ForEach(self.data.profileList){ row in
HStack {
Text("\(row.name)")
.foregroundColor(Color.purple)
Spacer()
}
}
}.background(Color.init(red: 0.05, green: 0.05, blue: 0.05))
//if you hit more buttons than there is room for, it'll be scrollable. make some kind of for loop that iterates over events a user is going to and displays it
//}
} else{
Spacer()
Text("Loading Attendees")
Spacer()
}
}.onAppear{
//this can't be done on appear as it won't update when a different
self.data.profileList = []
self.data.atendees = []
DispatchQueue.main.async{
self.data.fetchAtendees(id: self.selectedEvent)
if self.data.profilesLoaded{
self.profileList = self.data.profileList
self.atendees = self.data.atendees
}
}
}
//.navigationBarTitle("My Attendees")
//.navigationViewStyle(StackNavigationViewStyle())
}
NOTE: datafetcher (the observableobject) is passed to eventsusercreated view by the contentview
any help on how to update my view properly is much appreciated

You've to declare the data as an #ObservedObject.
struct EventsUserCreatedView: View {
//...
#ObservedObject var data = DataFetcher()
//...
}
If you're passing DataFetcher instance as environment object declare it as #EnvironmentObject.
struct EventsUserCreatedView: View {
//...
#EnvironmentObject var data: DataFetcher
//...
}

Related

How to update isLoading state from within URLSession.dataTask() completion handler in Swift?

I created a view (called AddressInputView) in Swift which should do the following:
Get an address from user input
When user hits submit, start ProgressView animation and send the address to backend
Once the call has returned, switch to a ResultView and show results
My problem is that once the user hits submit, then the view switches to the ResultView immediately without waiting for the API call to return. Therefore, the ProgressView animation is only visible for a split second.
This is my code:
AddressInputView
struct AddressInputView: View {
#State var buttonSelected = false
#State var radius = 10_000 // In meters
#State var isLoading = false
#State private var address: String = ""
#State private var results: [Result] = []
func onSubmit() {
if !address.isEmpty {
fetch()
}
}
func fetch() {
results.removeAll()
isLoading = true
let backendUrl = Bundle.main.object(forInfoDictionaryKey: "BACKEND_URL") as? String ?? ""
let escapedAddress = address.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
let params = "address=\(escapedAddress)&radius=\(radius)"
let fullUrl = "\(backendUrl)/results?\(params)"
var request = URLRequest(url: URL(string: fullUrl)!)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: { data, _, _ in
if data != nil {
do {
let serviceResponse = try JSONDecoder().decode(ResultsServiceResponse.self, from: data!)
self.results = serviceResponse.results
} catch let jsonError as NSError {
print("JSON decode failed: ", String(describing: jsonError))
}
}
isLoading = false
})
buttonSelected = true
task.resume()
}
var body: some View {
NavigationStack {
if isLoading {
ProgressView()
} else {
VStack {
TextField(
"",
text: $address,
prompt: Text("Search address").foregroundColor(.gray)
)
.onSubmit {
onSubmit()
}
Button(action: onSubmit) {
Text("Submit")
}
.navigationDestination(
isPresented: $buttonSelected,
destination: { ResultView(
address: $address,
results: $results
)
}
)
}
}
}
}
}
So, I tried to move buttonSelected = true right next to isLoading = false within the completion handler for session.dataTask but if I do that ResultView won't be shown. Could it be that state updates are not possible from within completionHandler? If yes, why is that so and what's the fix?
Main Question: How can I change the code above so that the ResultView won't be shown until the API call has finished? (While the API call has not finished yet, I want the ProgressView to be shown).
I think the problem is that the completion handler of URLSession is executed on a background thread. You have to dispatch the UI related API to the main thread.
But I recommend to take advantage of async/await and rather than building the URL with String Interpolation use URLComponents/URLQueryItem. It handles the necessary percent encoding on your behalf
func fetch() {
results.removeAll()
isLoading = true
Task {
let backendUrlString = Bundle.main.object(forInfoDictionaryKey: "BACKEND_URL") as! String
var components = URLComponents(string: backendUrlString)!
components.path = "/results"
components.queryItems = [
URLQueryItem(name: "address", value: address),
URLQueryItem(name: "radius", value: "\(radius)")
]
do {
let (data, _ ) = try await URLSession.shared.data(from: components.url!)
let serviceResponse = try JSONDecoder().decode(ResultsServiceResponse.self, from: data)
self.results = serviceResponse.results
isLoading = false
buttonSelected = true
} catch {
print(error)
// show something to the user
}
}
}
The URLRequest is not needed, GET is the default.
And you can force unwrap the value of the Info.plist dictionary. If it doesn't exist you made a design mistake.
Your fetch() function calls an asynchronous function session.dataTask which returns immediately, before the data task is complete.
The easiest way to resolve this these days is to switch to using async functions, e.g.
func onSubmit() {
if !address.isEmpty {
Task {
do {
try await fetch()
} catch {
print("Error \(error.localizedDescription)")
}
}
}
}
func fetch() async throws {
results.removeAll()
isLoading = true
let backendUrl = Bundle.main.object(forInfoDictionaryKey: "BACKEND_URL") as? String ?? ""
let escapedAddress = address.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
let params = "address=\(escapedAddress)&radius=\(radius)"
let fullUrl = "\(backendUrl)/results?\(params)"
var request = URLRequest(url: URL(string: fullUrl)!)
request.httpMethod = "GET"
let session = URLSession.shared
let (data, _) = try await session.data(for: request)
let serviceResponse = try JSONDecoder().decode(ResultsServiceResponse.self, from: data)
self.results = serviceResponse.results
isLoading = false
buttonSelected = true
}
In the code above, the fetch() func is suspended while session.data(for: request) is called, and only resumes once it's complete.
From the .navigationDestination documentation:
In general, favor binding a path to a navigation stack for programmatic navigation.
so add a #State var path to your view and use this .navigationDestination initialiser:
enum Destination {
case result
}
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
if isLoading {
ProgressView()
} else {
VStack {
TextField("", text: $address, prompt: Text("Search address").foregroundColor(.gray))
.onSubmit {
onSubmit()
}
Button(action: onSubmit) {
Text("Submit")
}
.navigationDestination(for: Destination.self, destination: { destination in
switch destination {
case .result:
ResultView(address: $address, results: $results)
}
})
}
}
}
}
then at the end of your fetch() func, just set
isLoading = false
path.append(Destination.result)
Example putting it all together
struct Result: Decodable {
}
struct ResultsServiceResponse: Decodable {
let results: [Result]
}
struct ResultView: View {
#Binding var address: String
#Binding var results: [Result]
var body: some View {
Text(address)
}
}
enum Destination {
case result
}
struct ContentView: View {
#State var radius = 10_000 // In meters
#State var isLoading = false
#State private var address: String = ""
#State private var results: [Result] = []
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
if isLoading {
ProgressView()
} else {
VStack {
TextField("", text: $address, prompt: Text("Search address").foregroundColor(.gray))
.onSubmit {
onSubmit()
}
Button(action: onSubmit) {
Text("Submit")
}
.navigationDestination(for: Destination.self, destination: { destination in
switch destination {
case .result:
ResultView(address: $address, results: $results)
}
})
}
}
}
}
func onSubmit() {
if !address.isEmpty {
Task {
do {
try await fetch()
} catch {
print("Error \(error.localizedDescription)")
}
}
}
}
func fetch() async throws {
results.removeAll()
isLoading = true
try await Task.sleep(nanoseconds: 2_000_000_000)
self.results = [Result()]
isLoading = false
path.append(Destination.result)
}
}

Where to make an API call to populate a view with data

I am trying to create a view that displays results from an API call, however I keep on running into multiple errors.
My question is basically where is the best place to make such an API call.
Right now I am "trying" to load the data in the "init" method of the view like below.
struct LandingView: View {
#StateObject var viewRouter: ViewRouter
#State var user1: User
#State var products: [Product] = []
init(_ viewRouter : ViewRouter, user: User) {
self.user1 = user
_viewRouter = StateObject(wrappedValue: viewRouter)
ProductAPI().getAllProducts { productArr in
self.products = productArr
}
}
var body: some View {
tabViewUnique(prodArrParam: products)
}
}
I keep on getting an "escaping closure mutating self" error, and while I could reconfigure the code to stop the error,I am sure that there is a better way of doing what I want.
Thanks
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
In .onAppear you can make api calls

Add a ProgressView during a simple URLSession POST request in SwiftUI

I wanted to show a progress view during a simple URL POST request process. What I would like to do is the button to turn into a ProgressView spinner (usually it's default) as it's going through the requestTest function process. After the request is done, then the progress view goes away and turns back into a button.
here's the code.
struct ContentView: View {
#State private var tweetID = ""
#State private var tweetStatus = ""
#State private var response = ""
#State var showAlert = false
#State var sendToWebhook = false
var body: some View {
NavigationView {
Form {
Section(footer: Text("Test")) {
TextField("Field to place response data", text: $response)
TextEditor( text: $tweetStatus)
.frame(height: 100)
}
Section {
Button("Get Data") {
// Where progress should start before function
ProgressView("Test", value: 100, total: 100)
requestTest() { results in
response = results
if response == "No Data!" {
showAlert = true
}
}
}
if self.requestTest {
ProgressView()
}
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Tweet Sent"), message: Text("Your Tweet is sent! Your Tweet ID is shown in the field"), dismissButton: .default(Text("OK")))
}
}
}
func requestTest(completion: #escaping(String) -> ()) {
if let url = URL(string: "https://requestbin.net/r/ag4ipg7n") {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
components.queryItems = [ URLQueryItem(name: "TweetID", value: response),
URLQueryItem(name: "Status", value: tweetStatus)]
if let query = components.url!.query {
request.httpBody = Data(query.utf8)
}
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let apiResponse = String(data: data, encoding: .utf8) {
// IF Completed, these actions are shown below
completion(apiResponse)
self.showAlert = true
tweetStatus = ""
} else {
completion("No Data!")
}
}
task.resume()
}
}
}
I thought if I tried to do as a if self.requestTest { ProgressView() }, but no avail as it throwed me a error that says Cannot convert value of type '(#escaping (String) -> ()) -> ()' to expected condition type 'Bool'.
Is there a way to do that?
there are a number of ways to do what you ask. This is
just one approach:
struct ContentView: View {
#State private var tweetID = ""
#State private var tweetStatus = ""
#State private var response = ""
#State var showAlert = false
#State var sendToWebhook = false
#State var inProgress = false // <--- here
var body: some View {
NavigationView {
Form {
Section(footer: Text("Test")) {
TextField("Field to place response data", text: $response)
TextEditor( text: $tweetStatus)
.frame(height: 100)
}
Section {
Button("Get Data") {
inProgress = true // <--- here
requestTest() { results in
inProgress = false // <--- here
response = results
if response == "No Data!" {
showAlert = true
}
}
}
if inProgress { // <--- here
ProgressView()
}
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Tweet Sent"), message: Text("Your Tweet is sent! Your Tweet ID is shown in the field"), dismissButton: .default(Text("OK")))
}
}
}

WebImage not updating the View - SwiftUI (Xcode 12)

My problem is that when I change my observed objects string property to another value, it updates the JSON Image values printing out the updated values(Using the Unsplash API), but the WebImage (from SDWebImageSwiftUI) doesn't change.
The struct that Result applies to:
struct Results : Codable {
var total : Int
var results : [Result]
}
struct Result : Codable {
var id : String
var description : String?
var urls : URLs
}
struct URLs : Codable {
var small : String
}
Here is the view which includes the webImage thats supposed to update:
struct MoodboardView: View {
#ObservedObject var searchObjectController = SearchObjectController.shared
var body: some View {
List {
VStack {
Text("Mood Board: \(searchObjectController.searchText)")
.fontWeight(.bold)
.padding(6)
ForEach(searchObjectController.results, id: \.id, content: { result in
Text(result.description ?? "Empty")
WebImage(url: URL(string: result.urls.small) )
.resizable()
.frame(height:300)
})
}.onAppear() {
searchObjectController.search()
}
}
}
}
Here is the class which does the API Request:
class SearchObjectController : ObservableObject {
static let shared = SearchObjectController()
private init() {}
var token = "gQR-YsX0OpwkYpbjhPVi3b4kSR-DtWrR5phwDm2kPMM"
#Published var results = [Result]()
#Published var searchText : String = "forest"
func search () {
let url = URL(string: "https://api.unsplash.com/search/photos?query=\(searchText)")
var request = URLRequest(url: url!)
request.httpMethod = "GET"
request.setValue("Client-ID \(token)", forHTTPHeaderField: "Authorization")
print("request: \(request)")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
print(String(data: data, encoding: .utf8)!)
do {
let res = try JSONDecoder().decode(Results.self, from: data)
DispatchQueue.main.async {
self.results.append(contentsOf: res.results)
}
//print(self.results)
} catch {
print("catch: \(error)")
}
}
task.resume()
}
}
Here is how I change the value of the searchText in a Button, if you would like to see:
struct GenerateView: View {
#ObservedObject var searchObjectController = SearchObjectController.shared
#State private var celsius: Double = 0
var body: some View {
ZStack {
Color.purple
.ignoresSafeArea()
VStack{
Text("Generate a Random Idea")
.padding()
.foregroundColor(.white)
.font(.largeTitle)
.frame(maxWidth: .infinity, alignment: .center)
Image("placeholder")
Slider(value: $celsius, in: -100...100)
.padding()
Button("Generate") {
print("topic changed to\(searchObjectController.searchText)")
searchObjectController.searchText.self = "tables"
}
Spacer()
}
}
}
}
It turns out you update the search text but don't search again. See this does the trick:
Button("Generate") {
print("topic changed to\(searchObjectController.searchText)")
searchObjectController.searchText.self = "tables" // you changed the search text but didnt search again
self.searchObjectController.search() // adding this does the trick
}
Also. I updated your code to use an EnvironmentObject. If you use one instance of an object throughout your app. Consider making it an EnvironmentObject to not have to pass it around all the time.
Adding it is easy. Add it to your #main Scene
import SwiftUI
#main
struct StackoverflowApp: App {
#ObservedObject var searchObjectController = SearchObjectController()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(self.searchObjectController)
}
}
}
And using it even simpler:
struct MoodboardView: View {
// Env Obj. so we reference only one object
#EnvironmentObject var searchObjectController: SearchObjectController
var body: some View {
Text("")
}
}
Here is your code with the changes and working as expected:
I added comments to the changes I made
struct ContentView: View {
var body: some View {
MoodboardView()
}
}
struct GenerateView: View {
#EnvironmentObject var searchObjectController: SearchObjectController
#State private var celsius: Double = 0
var body: some View {
ZStack {
Color.purple
.ignoresSafeArea()
VStack{
Text("Generate a Random Idea")
.padding()
.foregroundColor(.white)
.font(.largeTitle)
.frame(maxWidth: .infinity, alignment: .center)
Image("placeholder")
Slider(value: $celsius, in: -100...100)
.padding()
Button("Generate") {
print("topic changed to\(searchObjectController.searchText)")
searchObjectController.searchText.self = "tables" // you changed the search text but didnt search again
self.searchObjectController.search() // adding this does the trick
}
Spacer()
}
}
}
}
class SearchObjectController : ObservableObject {
//static let shared = SearchObjectController() // Delete this. We want one Object of this class in the entire app.
//private init() {} // Delete this. Empty Init is not needed
var token = "gQR-YsX0OpwkYpbjhPVi3b4kSR-DtWrR5phwDm2kPMM"
#Published var results = [Result]()
#Published var searchText : String = "forest"
func search () {
let url = URL(string: "https://api.unsplash.com/search/photos?query=\(searchText)")
var request = URLRequest(url: url!)
request.httpMethod = "GET"
request.setValue("Client-ID \(token)", forHTTPHeaderField: "Authorization")
print("request: \(request)")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
print(String(data: data, encoding: .utf8)!)
do {
let res = try JSONDecoder().decode(Results.self, from: data)
DispatchQueue.main.async {
self.results.append(contentsOf: res.results)
}
//print(self.results)
} catch {
print("catch: \(error)")
}
}
task.resume()
}
}
struct MoodboardView: View {
// Env Obj. so we reference only one object
#EnvironmentObject var searchObjectController: SearchObjectController
var body: some View {
List {
VStack {
Text("Mood Board: \(searchObjectController.searchText)")
.fontWeight(.bold)
.padding(6)
ForEach(searchObjectController.results, id: \.id, content: { result in
Text(result.description ?? "Empty")
WebImage(url: URL(string: result.urls.small) )
.resizable()
.frame(height:300)
})
}.onAppear() {
searchObjectController.search()
}
// I added your update button here so I can use it.
GenerateView()
}
}
}
struct Results : Codable {
var total : Int
var results : [Result]
}
struct Result : Codable {
var id : String
var description : String?
var urls : URLs
}
struct URLs : Codable {
var small : String
}
Ps. please not that it appears when you search you just append the results to the array and don't delete the old ones. Thats the reason why you still see the first images after updating. The new ones just get appended at the bottom. Scroll down to see them. If you don't want that just empty the array with results upon search

pass values dynamically for network request

I have to pass the value of movie.id which is received from a View which is called ReviewView.
I need to pass the movie.id value received in this view to ReviewFetcher and then make a network request using that movie.id. As of now I have hard coded the movie id in ReviewFetcher but I require this to be received from ReviewView and then make a request and then update the list in ReviewView.
Below is the Code:-
ReviewFetcher.swift
import Foundation
import Alamofire
import SwiftUI
class ReviewObserver: ObservableObject {
#Published var review = ReviewArray(id: 1, page: 9, results: [])
// #State var movieID:Int
init() {
// self.movieID = movieID
getReviews(movieID : 181812)
}
func getReviews(movieID:Int) {
//self.review.results.removeAll()
let reviewURL = "https://api.themoviedb.org/3/movie/"+String(movieID)+"/reviews?api_key=a18f578d774935ef9f0453d7d5fa11ae&language=en-US&page=1"
Alamofire.request(reviewURL)
.responseJSON { response in
if let json = response.result.value {
if (json as? [String : AnyObject]) != nil {
if let dictionaryArray = json as? Dictionary<String, AnyObject?> {
let json = dictionaryArray
if let id = json["id"] as? Int,
let page = json["page"] as? Int,
let results = json["results"] as? Array<Dictionary<String, AnyObject?>> {
for i in 0..<results.count {
if let author = results[i]["author"] as? String,
let content = results[i]["content"] as? String,
let url = results[i]["url"] as? String {
let newReview = ReviewModel(author: author,
content: content,
url: url)
self.review.results.append(newReview)
}
}
}
}
}
}
}
}
}
ReviewView.swift
import SwiftUI
struct ReviewsView: View {
#State var movie: MovieModel
#Binding var reviews:[ReviewModel]
#ObservedObject var fetcher = ReviewObserver()
var body: some View {
VStack(alignment:.leading) {
Text("Review")
.font(.largeTitle)
.bold()
.foregroundColor(Color.steam_rust)
.padding(.leading)
Divider()
// Text(String(fetcher.movieID))
List(fetcher.review.results) { item in
VStack(alignment:.leading) {
Text("Written by : "+item.author)
.font(.body)
.bold()
.padding(.bottom)
Text(item.content)
.font(.body)
.lineLimit(.max)
}
}
}
}
}
MovieModel.swift
import Foundation
import SwiftUI
import Combine
struct MovieArray: Codable {
var page: Int = 0
var total_results: Int = 0
var total_pages: Int = 0
var results: [MovieModel] = []
}
struct MovieModel: Codable, Identifiable {
var id : Int
var original_title: String
var title: String
var original_language:String
var overview: String
var poster_path: String?
var backdrop_path: String?
var popularity: Double
var vote_average: Double
var vote_count: Int
var video: Bool
var adult: Bool
var release_date: String?
}
Remove the init() of your ReviewObserver class. and then call getReviews method in .onAppear modifier of your VStack. The idea of what you need:
class ReviewObserver: ObservableObject {
#Published var review = ReviewArray(id: 1, page: 9, results: [])
func getReviews(movieID:Int) {
//you block,, anything you wanna do with movieID.
//Assume you are going to change 'review' variable
}
}
struct ReviewsView: View {
#State var movie:MovieModel
#Binding var reviews:[ReviewModel]
#ObservedObject var fetcher = ReviewObserver()
var body: some View {
VStack(alignment:.leading){
Text("Review")
Divider()
Text(String(fetcher.movieID))
List(fetcher.review.results)
{
item in
VStack(alignment:.leading){
Text("Written by : "+item.author)
}
}.onAppear {
self.fetcher.getReviews(movieID: movie.id)
}
}
}

Resources