Build Swift Charts Stocks App - Part 1 - Yahoo Finance API Service - SwiftUI
Swift Package
셋업Codable
프로토콜을 통한 JSON 파싱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
}
}
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 함수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)
}
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를 실행해보기 위한 메인 스트럭