9

I'm new to iOS dev, so sorry if it's an obvious question. But I can't figure out how to update the data in SwiftUI List. I'm fetching the data from API and using @ObservedObject to pass it to the ContentView. It works fine when I'm launching my app, but after I change my API request (by typing a keyword in the SearchBar) and fetch it again, it doesn't seem to update the List, even though the data was changed.

ContentView.swift

struct ContentView: View {

@ObservedObject var networkManager = NetworkManager()

@State var searchText: String = ""

var body: some View {
    NavigationView{
        VStack {
            SearchBar(text: $searchText, placeholder: "Enter a keyword")
            List(networkManager.posts) { post in
                NavigationLink(destination: DetailView(url: post.url)) {
                    HStack {
                        Text(post.title)
                    }
                }
                
            }.gesture(DragGesture().onChanged { _ in UIApplication.shared.endEditing() })
            
        }.navigationBarTitle("News")
        
    }
    .onAppear {
        self.networkManager.fetchData(self.searchText)
    }
    
}
}

NetworkManager.swift

class NetworkManager: ObservableObject {



@Published var posts = [Post]()

func fetchData(_ keyword: String?){
    var urlString = "https://newsapi.org/v2/top-headlines?country=us&apiKey=5dcef32f4c69413e8fe128cc5c7ba4cf"
    if keyword != nil {
        urlString = "https://newsapi.org/v2/top-headlines?country=us&apiKey=5dcef32f4c69413e8fe128cc5c7ba4cf&q=\(keyword!)"
    }
    print(urlString)
    if let url = URL(string: urlString){
        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(News.self, from: safeData)
                        DispatchQueue.main.async {
                            self.posts = results.articles
                            print(self.posts)
                        }
                    } catch{
                        print(error)
                    }
                }
            }
        }
        task.resume()
    }
    
}

}

SearchBar.swift (I fetch data again inside searchBarSearchButtonClicked)

struct SearchBar: UIViewRepresentable {
    
    
    @Binding var text: String
    var placeholder: String
    
    class Coordinator: NSObject, UISearchBarDelegate {
       
        @ObservedObject var networkManager = NetworkManager()
        
        @Binding var text: String
        
        init(text: Binding<String>) {
            _text = text
        }
        
        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            text = searchText
        }
        
        func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
            print(text)
            DispatchQueue.main.async {
                self.networkManager.fetchData(self.text)
            }
            
            UIApplication.shared.endEditing()
            
        }
    }
    
    func makeCoordinator() -> SearchBar.Coordinator {
        return Coordinator(text: $text)
    }
    
    func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
        let searchBar = UISearchBar(frame: .zero)
        searchBar.delegate = context.coordinator
        searchBar.placeholder = placeholder
        searchBar.searchBarStyle = .minimal
        searchBar.autocapitalizationType = .none
        return searchBar
    }
    
    func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
        uiView.text = text
    }
    
}

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

News.swift

struct News: Decodable {
    let articles: [Post]
}

struct Post: Decodable, Identifiable {
    var id: String{
        return url!
    }
    let title: String
    let url: String?
}
2
  • Try calling objectWillChange.send() after decoding. Off-topic but I think you can remove the DispatchQueue.main.async part and assign self.post directly Commented Jun 29, 2020 at 14:55
  • @JoakimDanielson I added objectWillChange.send() after self.posts = results.articles and it didn't work. And about DispatchQueue, when I remove it Xcode says: "Publishing changes from background threads is not allowed" Commented Jun 29, 2020 at 15:08

1 Answer 1

8

I've made a few minor modifications and made the code work in Xcode-playgrounds. Here's how:

Model:

struct News: Codable { var articles: [Post] }
struct Post: Identifiable, Codable { var title: String; var id: String { title } }

ContentView:

struct ContentView: View {

    @ObservedObject var networkManager = NetworkManager()

    var body: some View {
        NavigationView {
            VStack {
                TextField("Enter a keyword", text: $networkManager.searchText)
                List(networkManager.posts) { post in
                    NavigationLink(destination: EmptyView()) {
                        HStack {
                            Text(post.title)
                        }
                    }
                }
            }.navigationBarTitle("News")
        }
        .onAppear {
            self.networkManager.fetchData()
        }

    }
}

NetworkManager:

class NetworkManager: ObservableObject {

    @Published var searchText: String = "" {
        didSet {
            fetchData()
        }
    }
    @Published var posts = [Post]()

    func fetchData() {
        let urlString = "https://newsapi.org/v2/top-headlines?country=us&apiKey=5dcef32f4c69413e8fe128cc5c7ba4cf&q=\(searchText)"
        if let url = URL(string: urlString){
            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(News.self, from: safeData)
                            DispatchQueue.main.async {
                                self.posts = results.articles
                                print(self.posts)
                            }
                        } catch{
                            print(error)
                        }
                    }
                }
            }
            task.resume()
        }
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Check the update. It works I've tested in Xcode playgrounds.
I've edited my question, added SearchBar.swift. Could you please check and tell me how I can use it with SearchBar instead of TextField, that you had in your example? When you have time. Thanks a lot!
Modify the SearchBar to be just like TextField which accepts the same parameters of binding text and title. SearchBar should only needs to handle the View part.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.