티스토리 뷰

News의 api data를 parsing하여 SwiftUI list에 뿌려주기!

를 해볼건데, 기존 Swift에는 없는 SwiftUI만의 바인딩 특성을 사용하여 데이터를 뿌려보겠습니다.

https://newsapi.org/ 의 뉴스 api로 작업해보겠습니다.

 

 

SwiftUI에서는 내장되어있는 combine이라는 언어의

observableObject를 사용하여 fetch한 data를 뿌려주는 방법입니다.

 

소스코드는 맨밑에 깃헙주소있어요

 

 

 

data를 가져오는 RequestAPI를 구현합니다.

 

import Foundation


class RequestAPI {
    static let shared = RequestAPI()
    private init() { }
    
    private let apiKey = Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String
    
    func fetchData(){
        
        guard let apiKey = apiKey else { return }
        
        guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=kr&apiKey=\(apiKey)") else{
            return
        }
        
        let session = URLSession(configuration: .default)
        
        let task = session.dataTask(with: url) { data, response, error in
            if let error = error{
                print(error.localizedDescription)
                return
            }
            guard let response = response as? HTTPURLResponse, response.statusCode == 200 else{
                // 실패
                return
            }
            guard let data = data else{
                return
            }
            do{
                let apiResponse = try JSONDecoder().decode(Results.self, from: data)
                // 성공
            }catch(let err){
                print(err.localizedDescription)
            }
        }
        task.resume()
    }
}

 

 

 

이렇게 가져온 data를

스유는 @Published, state를 사용하여 간단하게 뿌려주기가 가능합니다. 

 

 

 

 

1. 프로토콜 ObservableObject를 채택합니다.

class RequestAPI: ObservableObject { .. }

 

 

 

2. @Published 

data가 변할때마다 view를 update하려고 하는 data에 @published를 붙여줍니다.

 

@Published var posts = [Article]()

 

 

@Published의 기능은 해당 값이 변할 때 마다 observer에게 변경사항을 전달할 수 있게 합니다.

observer는 알림이 오면 view를 update하도록 합니다.

 

 

import Foundation


class RequestAPI: ObservableObject {
    static let shared = RequestAPI()
    private init() { }
    @Published var posts = [Article]()
    
    private let apiKey = Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String
    
    func fetchData(){
        
        guard let apiKey = apiKey else { return }
        
        guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=kr&apiKey=\(apiKey)") else{
            return
        }
        let session = URLSession(configuration: .default)
        
        let task = session.dataTask(with: url) { data, response, error in
            if let error = error{
                print(error.localizedDescription)
                return
            }
            guard let response = response as? HTTPURLResponse, response.statusCode == 200 else{
                self.posts = []
                return
            }
            guard let data = data else{
                return
            }
            do{
                let apiResponse = try JSONDecoder().decode(Results.self, from: data)
                DispatchQueue.main.async {
                    self.posts = apiResponse.articles
                }
            }catch(let err){
                print(err.localizedDescription)
            }
        }
        task.resume()
    }
}

 

 

3. 마지막으로 알림이 올때 마다 view를 update시키기 위해, view에 쓰이는 값인 ObservableObject를 인스턴스화 합니다 ( @State )

 

update하고자 하는 view에 연결된 값에 @State를 붙여주면 됩니다.

 

@State 애플 문서입니다

 

 

1. @State는 값 자체가 아니라 값을 읽고 쓰는 수단이다.

2. @State는 private와 한 쌍이라고 해요. 이유는 view 외부에서 사용되지 않아야 한다고 합니다.

(목적이 view를 업데이트 되도록 하는 기능이므로)

 

+ 다른 뷰 계층에서 사용 하려면 $붙이면 된다고 합니다. (애플 예제)

 

https://developer.apple.com/documentation/swiftui/state

 

PlayeView의 state 변수 isPlaying을 PlayButton라는 다른 뷰에 써주려고 $붙여줌

 

 

 

저의 예제입니다. (News data를 fetch하고 fetch한 data를 view에 binding)

 

import SwiftUI

struct ContentView: View {
    
    @StateObject private var network = RequestAPI.shared
    
    var body: some View {
        NavigationView{
            List{
                ForEach(network.posts, id: \.self) { result in
                    Text(result.title)
                }
            }.navigationTitle("뉴스 둘러보기")
        }.onAppear {
            network.fetchData()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

 

 

헌데 class 형태 이므로 @StateObject로 해준 모습.

 

이렇게 해주니 잘 나옵니다.

 

 

잘 나옵니다.

 

 

JsonDecoder Model도 딕셔너리 형태이므로, Hashable을 채택합니다.

 

import Foundation

// MARK: - Results
struct Results: Decodable {
    let articles: [Article]
}

// MARK: - Article
struct Article: Decodable, Hashable {
    let title: String
    let url: String
    let urlToImage: String?
}

 

그럼 이제 바인딩은 끝 입니다. 

 

이미지도 추가해보겠습니다.

 

 

//
//  ContentView.swift
//  SwiftUINewsAPP
//
//  Created by jh on 2021/10/26.
//

import SwiftUI

struct URLImage: View {
    let urlString: String?
    @State var data: Data?

    var body: some View {
        if let data = data, let uiimage = UIImage(data: data) {
            Image(uiImage: uiimage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 130, height: 70)
                .background(Color.white)
        } else {
            Image("")
                .frame(width: 130, height: 70)
                .background(Color.gray)
                .onAppear {
                    fetchImageData()
                }
        }
    }
    
    private func fetchImageData(){
        guard let urlString = urlString else{
            return
        }
        guard let url = URL(string: urlString) else{
            return
        }
        let task = URLSession.shared.dataTask(with: url) { data, _, _ in
            self.data = data
        }
        task.resume()
    }
}

struct ContentView: View {
    
    @StateObject private var network = RequestAPI.shared
    
    var body: some View {
        NavigationView{
            List{
                ForEach(network.posts, id: \.self) { result in
                    HStack{
                        URLImage(urlString: result.urlToImage)
                            .frame(width: 130, height: 70)
                            .background(Color.gray)
                        Text(result.title)
                            .bold()
                    }.padding(3)
                }
            }.navigationTitle("뉴스 둘러보기")
        }.onAppear {
            network.fetchData()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

 

이미지를 담는 data 변수를 @State로 선언해주고,

회색배경의 빈 이미지뷰가 onApear될 때, fetchImageData()로 변수 data에 fetch한 값을 저장해줍니다.

그러면 View가 새로 그려지면서, fetch한 image data로 Image를 갱신합니다.

 

 

이렇게 잘 나오는 모습입니다.

 

여기까지 swiftUI로 new API를 받아 Observable과 binding으로 fetch한 데이터를 뿌려주기 였습니다.

 

 

https://github.com/JangJaeHyung1/SwiftUINewsApp

 

GitHub - JangJaeHyung1/SwiftUINewsApp: news api를 가져와서 스유에 뿌려주기

news api를 가져와서 스유에 뿌려주기. Contribute to JangJaeHyung1/SwiftUINewsApp development by creating an account on GitHub.

github.com

 

 

 

부족한 글 읽어주셔서 감사합니다

 

수정해주실 내용 있으시면 남겨주시면 감사드리겠습니다 :)

 

 

 

 

댓글