carson-katri / swift-request

Declarative HTTP networking, designed for SwiftUI
MIT License
727 stars 41 forks source link

RequestView refresh? #45

Closed ca13ra1 closed 3 years ago

ca13ra1 commented 3 years ago

I'm using RequestView to fetch data from a News API. I attempted to use your example of adding a Query string and update the number incrementally like in your example but it didn't actually refresh the view.

My query string is Query(["q": text, "apiKey": "API_KEY"]) where text is my variable for a TextField string. I need to refresh the RequestView when my key changes. I've been unable to do so using RequestView but was able to using URLRequest manually and calling a func load() on my TextField's onCommit function. I'd really like to keep using RequestView in my project because it's very useful and easy to use for a beginner like myself! 😄

RequestView code not updating here Working code without RequestView here

carson-katri commented 3 years ago

I think I need to reimplement both RequestView and RequestImage, neither one has a very good implementation, and I don't see anything wrong with your code immediately.

You should still be able to use Request in your manual load function using .call() or as a publisher in the meantime.

carson-katri commented 3 years ago

Ok, I have it working in this PR: https://github.com/carson-katri/swift-request/pull/46 Please try out the swiftui branch and lmk if it works properly now. You may want to refactor to use the new initializers for RequestView, as I deprecated the old ones. For the refresh to work properly you will need to make sure your app only supports iOS 14+/macOS 11+. You should also not need that refresh query param trick. That is just to refresh when there is no other change, but it looks like you are actually searching for something different, so the change will still trigger.

ca13ra1 commented 3 years ago

@carson-katri Thanks for the quick fix! I'm testing now but can't seem to figure out the correct syntax. I'm getting Failed to produce diagnostic for expression; please file a bug report

struct NYTimes: Codable {
    let copyright: String?
    let response: Response
}

struct Response: Codable {
    let docs: [Doc]
}

struct Doc: Codable {
    let lead_paragraph: String?
}

struct ContentView: View {
    var body: some View {
        RequestView(NYTimes.self, Request {
            Url("https://api.nytimes.com/svc/search/v2/articlesearch.json")
            Query(["api-key": "API_KEY"])
        }) { news in
            switch news {
            case .loading: ProgressView()
            case let .failure(error): Text(error.localizedDescription)
            case let .success(news):
                List(news?.response.docs ?? [], id: \.lead_paragraph) { article in
                    Text(article.lead_paragraph ?? "")
                }
            }
        }
    }
}
carson-katri commented 3 years ago

I think your body should be:

RequestView(AnyRequest<NYTimes> {
    Url("https://api.nytimes.com/svc/search/v2/articlesearch.json")
    Query(["api-key": "API_KEY"])
}) { res in
    switch res {
    case .loading: ProgressView()
    case let .failure(error): Text(error.localizedDescription)
    case let .success(news):
        List(news.response.docs, id: \.lead_paragraph) { article in
            Text(article.lead_paragraph ?? "")
        }
    }
}

The result is no longer Optional, since there are now loading and failure cases, and you pass an AnyRequest instead of the type and Request separately. Let me know if that works for you.

ca13ra1 commented 3 years ago

@carson-katri It does in fact work but I'm seeing incorrect data in my list. I added a enum sort for newest, oldest, relevance alongside my search but seems to load incorrectly. Also the API can't call too many times so when typing it refreshes every time I type so it exhausting it's resources quickly.

carson-katri commented 3 years ago

I would suggest adding a debounce on your text field. Then it will wait for you to stop typing before updating the state. There are probably some examples on StackOverflow somewhere. One way would be to create an ObservableObject that contains the raw TextField input and a debounced publisher. I use something similar to this in my apps:

final class Debounce<Value: Equatable>: ObservableObject {
  @Published var rawValue: Value
  @Published var debounced: Value

  init(initialValue: Value) {
    self.rawValue = initialValue
    self.debounced = initialValue
    self.$rawValue
      .removeDuplicates()
      .debounce(for: 2, scheduler: DispatchQueue.main)
      .assign(to: &debounced)
  }
}

struct SomeView: View {
  @ObservedObject var debouncedInput = Debounce<String>(initialValue: "")
  var body: some View {
    TextField(value: $debouncedInput.rawValue)
    Text("The value debounced is: \(debouncedInput.debounced)")
  }
}

Now it will wait for 2 seconds until you stop typing before updating the View.

ca13ra1 commented 3 years ago

@carson-katri I really appreciate your help. I was able to solve without RequestView. Only a month into learning Swift but I'm going to keep trying RequestView since it's an unbelievably awesome swift package! 😄

carson-katri commented 3 years ago

Thanks! And RequestView is certainly not a one size fits all solution. However, if you wanted you could replace the URLSession call with a manual call on your Request in load:

AnyRequest<Article> {
  Url("")
}
.onObject { article in
  // use the article
}
.call()
ca13ra1 commented 3 years ago

I will try that! Thanks once again, you’ve really helped me out 👌

carson-katri commented 3 years ago

No problem! I'm going to leave this issue open until the PR lands on main.

ca13ra1 commented 3 years ago

If I select a NavigationLink in my List and go back to my List it starts out at the very top of my list, no matter what item I select. The previous version worked just fine, I was able to go back to whatever item was selected.

struct ContentView: View {
    var body: some View {
        /// Timestamp for Marvel API
        let ts = String(Date().timeIntervalSince1970)
        /// MD5 hash for Marvel API
        let hash = MD5(string:"\(ts)\("PRIVATE_KEY")\("PUBLIC_KEY")")
        NavigationView {
            RequestView(AnyRequest<Marvel> {
                Url("https://gateway.marvel.com:443/v1/public/characters")
                /// API is limited to 100 per call
                /// How to call more using RequestView?
                Query(["limit": "100", "apikey": "API_KEY", "ts": ts, "hash": hash])
                Method(.get)
                Header.Accept(.json)
                Header.UserAgent(.firefoxMac)
                /// Error handling
            }) { data in
                switch data {
                case .loading: ProgressView()
                case let .failure(error): Text(error.localizedDescription)
                case let .success(marvel):
                    List(marvel.data.results, id: \.name) { character in
                        /// Push MarvelCharacterDetailView to show character information
                        NavigationLink(destination: CharacterDetail(url: "\(character.thumbnail.path).\(character.thumbnail.thumbnailExtension)", text: character.description ?? "", title: character.name ?? "", copyright: marvel.copyright ?? "")) {
                            HStack {
                                /// Load image using RequestImage using path + extension from character information
                                RequestImage(Url("\(character.thumbnail.path).\(character.thumbnail.thumbnailExtension)"), animation: nil)
                                    .aspectRatio(contentMode: .fill)
                                    .frame(width: 30, height: 30, alignment: .center)
                                    .clipped()
                                    .cornerRadius(5.0)
                                Text(character.name ?? "")
                            }
                        }
                    }
                    .listStyle(PlainListStyle())
                    .navigationBarTitle(Text(""), displayMode: .inline)
                }
            }
        }
    }
}
carson-katri commented 3 years ago

I think I know what's causing this. Let me see what I can do.

ca13ra1 commented 3 years ago

Thank you, again! 😊

carson-katri commented 3 years ago

Can you try the latest commit on the SwiftUI branch? I pushed a possible fix.

ca13ra1 commented 3 years ago

It's been fixed in the latest commit!