Simbeul
Simbeul

Reputation: 441

Why is my @AppStorage not working on SwiftUI?

I'm trying to set up the @AppStorage wrapper in my project.

I'm pulling Texts from a JSON API (see DataModel), and am hoping to store the results in UserDefautls. I want the data to be fetched .OnAppear and stored into the @AppStorage. When the user taps "Get Next Text", I want a new poem to be fetched, and to update @AppStorage with the newest Text data, (which would delete the past Poem stored).

Currently, the code below builds but does not display anything in the Text(currentPoemTitle).

Data Model

import Foundation

struct Poem: Codable, Hashable {
    let title, author: String
    let lines: [String]
    let linecount: String
}

public class FetchPoem: ObservableObject {
  // 1.
  @Published var poems = [Poem]()
     
    init() {
        getPoem()
    }
    
    func getPoem() {
        let url = URL(string: "https://poetrydb.org/random/1")!
        // 2.
        URLSession.shared.dataTask(with: url) {(data, response, error) in
            do {
                if let poemData = data {
                    // 3.
                    let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
                    DispatchQueue.main.async {
                        self.poems = decodedData
                    }
                } else {
                    print("No data")
                }
            } catch {
                print("Error")
            }
        }.resume()
    }
}

TestView

import SwiftUI

struct Test: View {
    
    @ObservedObject var fetch = FetchPoem()

    @AppStorage("currentPoemtTitle") var currentPoemTitle = ""
    @AppStorage("currentPoemAuthor") var currentPoemAuthor = ""
    
    var body: some View {
        
        VStack{
            Text(currentPoemTitle)
            
            Button("Fetch next text") {
                fetch.getPoem()
            }
            
        }.onAppear{
            if let poem = fetch.poems.first {
                currentPoemTitle = "\(poem.title)"
                currentPoemAuthor = "\(poem.author)"
            }
        }
    
    }
    
}

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

What am I missing? Thanks.

Upvotes: 0

Views: 4645

Answers (1)

nicksarno
nicksarno

Reputation: 4245

Here are a few code edits to get you going.

  1. I added AppStorageKeys to manage the @AppStorage keys, to avoid errors retyping key strings (ie. "currentPoemtTitle")

  2. Your question asked how to update the @AppStorage with the data, and the simple solution is to add the @AppStorage variables within the FetchPoem class and set them within the FetchPoem class after the data is downloaded. This also avoids the need for the .onAppear function.

  3. The purpose of using @ObservedObject is to be able to keep your View in sync with the data. By adding the extra layer of @AppStorage, you make the @ObservedObject sort of pointless. Within the View, I added a Text() to display the title using the @ObservedObject values directly, instead of relying on @AppStorage. I'm not sure if you want this, but it would remove the need for the @AppStorage variables entirely.

  4. I also added a getPoems2() function using Combine, which is a new framework from Apple to download async data. It makes the code a little easier/more efficient... getPoems() and getPoems2() both work and do the same thing :)

Code:

import Foundation
import SwiftUI
import Combine

struct AppStorageKeys {
    static let poemTitle = "current_poem_title"
    static let poemAuthor = "current_poem_author"
}

struct Poem: Codable, Hashable {
    let title, author: String
    let lines: [String]
    let linecount: String
}

public class FetchPoem: ObservableObject {

    @Published var poems = [Poem]()
    @AppStorage(AppStorageKeys.poemTitle) var poemTitle = ""
    @AppStorage(AppStorageKeys.poemAuthor) var poemAuthor = ""
     
    init() {
        getPoem2()
    }
    
    func getPoem() {
        let url = URL(string: "https://poetrydb.org/random/1")!

        URLSession.shared.dataTask(with: url) {(data, response, error) in
            do {
                guard let poemData = data  else {
                    print("No data")
                    return
                }
                
                let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
                DispatchQueue.main.async {
                    self.poems = decodedData
                    self.updateFirstPoem()
                }
            } catch {
                print("Error")
            }
        }
        .resume()
    }
    
    func getPoem2() {
        let url = URL(string: "https://poetrydb.org/random/1")!
        
        URLSession.shared.dataTaskPublisher(for: url)
            // fetch on background thread
            .subscribe(on: DispatchQueue.global(qos: .background))
            // recieve response on main thread
            .receive(on: DispatchQueue.main)
            // ensure there is data
            .tryMap { (data, response) in
                guard
                    let httpResponse = response as? HTTPURLResponse,
                    httpResponse.statusCode == 200 else {
                    throw URLError(.badServerResponse)
                }
                return data
            }
            // decode JSON data to [Poem]
            .decode(type: [Poem].self, decoder: JSONDecoder())
            // Handle results
            .sink { (result) in
                // will return success or failure
                print("poetry fetch completion: \(result)")
            } receiveValue: { (value) in
                // if success, will return [Poem]
                // here you can update your view
                self.poems = value
                self.updateFirstPoem()
            }
            // After recieving response, the URLSession is no longer needed & we can cancel the publisher
            .cancel()
    }
    
    
    func updateFirstPoem() {
        if let firstPoem = self.poems.first {
            self.poemTitle = firstPoem.title
            self.poemAuthor = firstPoem.author
        }
    }
}

struct Test: View {
    
    @ObservedObject var fetch = FetchPoem()

    @AppStorage(AppStorageKeys.poemTitle) var currentPoemTitle = ""
    @AppStorage(AppStorageKeys.poemAuthor) var currentPoemAuthor = ""
    
    var body: some View {
        
        VStack(spacing: 10){
            
            Text("App Storage:")
            Text(currentPoemTitle)
            Text(currentPoemAuthor)
            
            Divider()
            
            Text("Observed Object:")
            Text(fetch.poems.first?.title ?? "")
            Text(fetch.poems.first?.author ?? "")

            Button("Fetch next text") {
                fetch.getPoem()
            }
        }
    }
    
}

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

Upvotes: 2

Related Questions