Bryce
Bryce

Reputation: 6660

Why doesn't this SwiftUI View update when changing a @State variable?

I've got a SwiftUI View that takes a Hacker News API submission ID, and then fetches the details for that item in fetchStory().

When fetchStory() completed its HTTP call, it updates the @State private var url on the View, however the View never re-renders to show the new value -- it always shows the initial empty value.

Why?

import Foundation
import SwiftUI

struct StoryItem: Decodable {
    let title: String
    let url: String?
}

struct StoryView: View {
    public var _storyId: Int

    @State private var url: String = "";

    init(storyId: Int) {
        self._storyId = storyId
        self.fetchStory()
    }

    var body: some View {
        VStack {
            Text("(Load a webview here for the URL of Story #\(self._storyId))")
            Text("URL is: \(self.url)") // this never changes!
        }
    }

    private func fetchStory() {
        let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(self._storyId).json")!

        let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
            guard let data = data else {
                print(String(error.debugDescription))
                return
            }

            do {
                let item: StoryItem = try JSONDecoder().decode(StoryItem.self, from: data)

                if let storyUrl = item.url {
                    self.url = storyUrl
                } else {
                    print("No url")
                }
            } catch {
                print(error)
            }
        }

        task.resume()
    }
}

struct StoryView_Previews: PreviewProvider {
    static var previews: some View {
        StoryView(storyId: 22862053)
    }
}

Upvotes: 1

Views: 882

Answers (2)

something like this:

import SwiftUI

struct StoryItem: Decodable {
let title: String
let url: String?
}

class ObservedStoryId: ObservableObject {
@Published var storyId: String = ""
init(storyId: String) {
    self.storyId = storyId
}
}

struct StoryView: View {

@ObservedObject var storyId: ObservedStoryId
@State var url: String = ""

var body: some View {
    VStack {
        Text("(Load a webview here for the URL of Story #\(self.storyId.storyId))")
        Text("URL is: \(self.url)")
        Text(self.storyId.storyId)
    }.onReceive(storyId.$storyId) { _ in self.fetchStory() }
}

private func fetchStory() {
    let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(self.storyId.storyId).json")!
    let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
        guard let data = data else {
            print(String(error.debugDescription))
            return
        }
        do {
            let item: StoryItem = try JSONDecoder().decode(StoryItem.self, from: data)
            if let storyUrl = item.url {
                self.url = storyUrl
            } else {
                print("No url")
            }
        } catch {
            print(error)
        }
    }
    task.resume()
}
}

and call it like this:

struct ContentView: View {

    @ObservedObject var storyId = ObservedStoryId(storyId: "22862053")

    var body: some View {
     StoryView(storyId: storyId)
    }
}

Upvotes: 1

to fix your problem:

init(storyId: Int) {
    self._storyId = storyId
}

var body: some View {
    VStack {
        Text("(Load a webview here for the URL of Story #\(self._storyId))")
        Text("URL is: \(self.url)") // this now works
    }.onAppear(perform: fetchStory)
}

why does this works and not your code, my guess is this: "self.url" can only be updated/changed within the special SwiftUI View functions, such as onAppear(), elsewhere it does not change a thing.

Upvotes: 1

Related Questions