[SwiftUI] ChartStocksClone: Yahoo API

Junyoung Park·2022년 12월 11일
0

SwiftUI

목록 보기
126/136
post-thumbnail

Build Swift Charts Stocks App - Part 1 - Yahoo Finance API Service - SwiftUI

ChartStocksClone: Yahoo API

구현 목표

  • 야후 API 연동
  • 프로젝트 셋업

구현 태스크

  • Swift Package 셋업
  • 디바이스 지원 및 디펜던지 설정
  • 야후 API 연동
  • Codable 프로토콜을 통한 JSON 파싱
  • API 함수 구조체 구현

핵심 코드

import PackageDescription

let package = Package(
    name: "StocksAPI",
    platforms: [
        .iOS(.v13), .macOS(.v12), .macCatalyst(.v13), .tvOS(.v13), .watchOS(.v8)
    ],
    products: [
        .library(
            name: "StocksAPI",
            targets: ["StocksAPI"]),
        .executable(name: "StocksAPIExec",
                    targets: ["StocksAPIExec"])
    ],
    targets: [
        .target(
            name: "StocksAPI",
            dependencies: []),
        .executableTarget(name: "StocksAPIExec", dependencies: ["StocksAPI"]),
        .testTarget(
            name: "StocksAPITests",
            dependencies: ["StocksAPI"]),
    ]
)
  • 라이브러리의 이름은 StocksAPI, 실행되는 프로그램 이름은 StocksAPIExec으로 스위프트 프로그램의 main에 해당
import Foundation

public struct Quote: Decodable, Identifiable, Hashable {
    
    public var id: String {
        return UUID().uuidString
    }
    
    public let symbol: String

    public let currency: String?
    public let marketState: String?
    
    public let fullExchangeName: String?
    public let displayName: String?
    
    public let regularMarketPrice: Double?
    public let regularMarketChange: Double?
    public let regularMarketChangePercent: Double?
    public let regularMarketChangePreviousClose: Double?
    public let regularMarketTime: Date?
    
    public let postMarketPrice: Double?
    public let postMarketChange: Double?
    
    public let regularMarketOpen: Double?
    public let regularMarketDayHigh: Double?
    public let regularMarketDayLow: Double?
    
    public let regularMarketVolume: Double?
    public let trailingPE: Double?
    public let marketCap: Double?
    
    public let fiftyTwoWeekLow: Double?
    public let fiftyTwoWeekHigh: Double?
    public let averageDailyVolume3Month: Double?
    
    public let trailingAnnualDividendYield: Double?
    public let epsTrailingTwelveMonths: Double?
    
    public init(symbol: String, currency: String? = nil, marketState: String? = nil, fullExchangeName: String? = nil, displayName: String? = nil, regularMarketPrice: Double? = nil, regularMarketChange: Double? = nil, regularMarketChangePercent: Double? = nil, regularMarketChangePreviousClose: Double? = nil, regularMarketTime: Date? = nil, postMarketPrice: Double? = nil, postMarketChange: Double? = nil, regularMarketOpen: Double? = nil, regularMarketDayHigh: Double? = nil, regularMarketDayLow: Double? = nil, regularMarketVolume: Double? = nil, trailingPE: Double? = nil, marketCap: Double? = nil, fiftyTwoWeekLow: Double? = nil, fiftyTwoWeekHigh: Double? = nil, averageDailyVolume3Month: Double? = nil, trailingAnnualDividendYield: Double? = nil, epsTrailingTwelveMonths: Double? = nil) {
        self.symbol = symbol
        self.currency = currency
        self.marketState = marketState
        self.fullExchangeName = fullExchangeName
        self.displayName = displayName
        self.regularMarketPrice = regularMarketPrice
        self.regularMarketChange = regularMarketChange
        self.regularMarketChangePercent = regularMarketChangePercent
        self.regularMarketChangePreviousClose = regularMarketChangePreviousClose
        self.regularMarketTime = regularMarketTime
        self.postMarketPrice = postMarketPrice
        self.postMarketChange = postMarketChange
        self.regularMarketOpen = regularMarketOpen
        self.regularMarketDayHigh = regularMarketDayHigh
        self.regularMarketDayLow = regularMarketDayLow
        self.regularMarketVolume = regularMarketVolume
        self.trailingPE = trailingPE
        self.marketCap = marketCap
        self.fiftyTwoWeekLow = fiftyTwoWeekLow
        self.fiftyTwoWeekHigh = fiftyTwoWeekHigh
        self.averageDailyVolume3Month = averageDailyVolume3Month
        self.trailingAnnualDividendYield = trailingAnnualDividendYield
        self.epsTrailingTwelveMonths = epsTrailingTwelveMonths
    }
}
  • 야후 API가 리턴하는 JSON 구조체에 따라 Decodable 프로토콜에 따라 구현한 데이터 모델
  • Identifiable 프로토콜을 따르기 위해 id를 리턴할 때 연산 프로퍼티 적용
import Foundation

public struct QuoteResponse: Decodable {
    
    public let data: [Quote]?
    public let error: ErrorResponse?
    
    enum CodingKeys: CodingKey {
        case quoteResponse
        case finance
    }
    
    enum ResponseKeys: CodingKey {
        case result
        case error
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let quoteResponseContainer = try? container.nestedContainer(keyedBy: ResponseKeys.self, forKey: .quoteResponse) {
            self.data = try? quoteResponseContainer.decodeIfPresent([Quote].self, forKey: .result)
            self.error = try? quoteResponseContainer.decodeIfPresent(ErrorResponse.self, forKey: .error)
        } else if let financeResponseContainer = try? container.nestedContainer(keyedBy: ResponseKeys.self, forKey: .finance) {
            self.data = nil
            self.error = try? financeResponseContainer.decodeIfPresent(ErrorResponse.self, forKey: .error)
        } else {
            self.data = nil
            self.error = nil
        }
    }
}
  • URLSession이 리턴하는 데이터 리스폰스 타입 또한 Decodable 프로토콜을 통해 JSON 파싱이 가능
  • 코딩 키를 통해 해당 변수 명으로 들어오는 타입을 받을 수 있음
  • decoder를 통해 해당 데이터를 이니셜라이즈할 때 컨테이너를 가져온 뒤 컨테이너가 어떤 키가 담긴 컨테이너를 가지고 있을지에 따라 조건문 캐스팅 가능
public func fetchQuotes(symbols: String) async throws -> [Quote] {
        guard var urlComponents = URLComponents(string: "\(baseURL)/v7/finance/quote") else {
            throw APIError.invalidURL
        }
        urlComponents.queryItems = [.init(name: "symbols", value: symbols)]
        guard let url = urlComponents.url else {
            throw APIError.invalidURL
        }
        let (response, statusCode): (QuoteResponse, Int) = try await fetch(url: url)
        if let error = response.error {
            throw APIError.httpStatusCodeFailed(statusCode: statusCode, error: error)
        }
        return response.data ?? []
    }
  • Quote를 패치하는 퍼블릭 API 함수
  • 쿼리할 파라미터를 받아 URL 컴포넌트를 구성한 뒤 해당 URL을 통해 데이터 패치
  • fetch를 사용하기 위해 QuoteResponse라는 Decodable 프로토콜을 준수하는 데이터 타입을 직접 작성해줌
private func fetch<D: Decodable>(url: URL) async throws -> (D, Int) {
        let (data, response) = try await session.data(from: url)
        let statusCode = try validateHTTPResponseCode(response)
        return (try jsonDecoder.decode(D.self, from: data), statusCode)
    }
  • URLSession이 리턴하는 데이터를 통해 주어진 JSON 디코터로 디코딩한 D 타입의 데이터를 리턴
  • Decodable 프로토콜을 준수하는 데이터를 받는 제네릭 타입의 함수
import Foundation
import StocksAPI

@main
struct StocksAPIExec {
    static let stocksAPI = StocksAPI()
    static func main() async {
        do {
            let quotes = try await stocksAPI.fetchQuotes(symbols: "AAPL")
            print(quotes)
        } catch {
            print(error.localizedDescription)
        }
    }
}
  • StocksAPIExec 파일은 StocksAPI와 별도의 타겟으로 해당 API를 실행해보기 위한 메인 스트럭
profile
JUST DO IT

0개의 댓글