이미지랑 칼라 Assets에 추가하고,
Color Extension 만듭시다
헤더구성, 버튼 구성
struct CircleButtonView: View {
let iconName: String
var body: some View {
Image(systemName: iconName)
.font(.headline)
.foregroundColor(Color.theme.accent)
.frame(width: 50, height: 50)
.background(
Circle()
.foregroundColor(Color.theme.background)
)
.shadow(color: Color.theme.accent.opacity(0.25), radius: 10, x: 0, y: 0)
.padding()
}
}
struct CoinModel: Identifiable, Codable {
let id, symbol, name: String
let image: String
let currentPrice: Double
let marketCap, marketCapRank, fullyDilutedValuation: Double?
let totalVolume, high24H, low24H: Double?
let priceChange24H, priceChangePercentage24H: Double?
let marketCapChange24H: Double?
let marketCapChangePercentage24H: Double?
let circulatingSupply, totalSupply, maxSupply, ath: Double?
let athChangePercentage: Double?
let athDate: String?
let atl, atlChangePercentage: Double?
let atlDate: String?
let lastUpdated: String?
let sparklineIn7D: SparklineIn7D?
let priceChangePercentage24HInCurrency: Double?
let currentHoldings: Double?
enum CodingKeys: String, CodingKey {
case id, symbol, name, image
case currentPrice = "current_price"
case marketCap = "market_cap"
case marketCapRank = "market_cap_rank"
case fullyDilutedValuation = "fully_diluted_valuation"
case totalVolume = "total_volume"
case high24H = "high_24h"
case low24H = "low_24h"
case priceChange24H = "price_change_24h"
case priceChangePercentage24H = "price_change_percentage_24h"
case marketCapChange24H = "market_cap_change_24h"
case marketCapChangePercentage24H = "market_cap_change_percentage_24h"
case circulatingSupply = "circulating_supply"
case totalSupply = "total_supply"
case maxSupply = "max_supply"
case ath
case athChangePercentage = "ath_change_percentage"
case athDate = "ath_date"
case atl
case atlChangePercentage = "atl_change_percentage"
case atlDate = "atl_date"
case lastUpdated = "last_updated"
case sparklineIn7D = "sparkline_in_7d"
case priceChangePercentage24HInCurrency = "price_change_percentage_24h_in_currency"
case currentHoldings
}
func updateHoldings(amount: Double) -> CoinModel {
return CoinModel(id: id, symbol: symbol, name: name, image: image, currentPrice: currentPrice, marketCap: marketCap, marketCapRank: marketCapRank, fullyDilutedValuation: fullyDilutedValuation, totalVolume: totalVolume, high24H: high24H, low24H: low24H, priceChange24H: priceChange24H, priceChangePercentage24H: priceChangePercentage24H, marketCapChange24H: marketCapChange24H, marketCapChangePercentage24H: marketCapChangePercentage24H, circulatingSupply: circulatingSupply, totalSupply: totalSupply, maxSupply: maxSupply, ath: ath, athChangePercentage: athChangePercentage, athDate: athDate, atl: atl, atlChangePercentage: atlChangePercentage, atlDate: atlDate, lastUpdated: lastUpdated, sparklineIn7D: sparklineIn7D, priceChangePercentage24HInCurrency: priceChangePercentage24HInCurrency, currentHoldings: amount)
}
var currentHoldingsValue: Double {
return (currentHoldings ?? 0) * currentPrice
}
var rank: Int {
return Int(marketCapRank ?? 0)
}
}
// MARK: - SparklineIn7D
struct SparklineIn7D: Codable {
let price: [Double]?
}
홈뷰모델 environmentObject로 만들기
import Foundation
import Combine
class NetworkingManager {
enum NetworkingError: LocalizedError {
case badURLResponse(url: URL)
case unknown
var errorDescription: String? {
switch self {
case .badURLResponse(url: let url): return "[🔥]Bad response from URL: \(url)"
case .unknown: return "[⚠️]Unknown error occured"
}
}
}
static func donwload(url: URL) -> AnyPublisher<Data, Error> {
return URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .default))
.tryMap { try handleURLResponse(output: $0, url: url) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data {
guard let response = output.response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw NetworkingError.badURLResponse(url: url)
}
return output.data
}
static func handleCompletion(completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}
}
이미지서비스, 이미지뷰모델, 이미지뷰 만들기
class LocalFileManager {
static let instance = LocalFileManager()
private init() { }
func saveImage(image: UIImage, imageName: String, folderName: String) {
// create folder
createFolderIfNeeded(folderName: folderName)
// get path for image
guard let data = image.pngData(),
let url = getURLForImage(imageName: imageName, folderName: folderName)
else { return }
do {
try data.write(to: url)
} catch let error {
print("Error saving image. ImageName: \(imageName) \(error)")
}
}
func getImage(imageName: String, folderName: String) -> UIImage? {
guard let url = getURLForImage(imageName: imageName, folderName: folderName),
FileManager.default.fileExists(atPath: url.path)
else { return nil }
return UIImage(contentsOfFile: url.path)
}
private func createFolderIfNeeded(folderName: String) {
guard let url = getURLForFolder(folderName: folderName) else { return }
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} catch let error {
print("Error creating directory. \(folderName). \(error)")
}
}
}
private func getURLForFolder(folderName: String) -> URL? {
guard let url = FileManager.default
.urls(for: .cachesDirectory, in: .userDomainMask).first
else { return nil }
return url.appendingPathComponent(folderName)
}
private func getURLForImage(imageName: String, folderName: String) -> URL? {
guard let folderURL = getURLForFolder(folderName: folderName) else { return nil }
return folderURL.appendingPathComponent(imageName + ".png")
}
}
combine써서 합쳐봅시당
진짜 매쥑!
import Foundation
struct StatisticModel: Identifiable {
let id = UUID().uuidString
let title: String
let value: String
let percentageChange: Double?
init(title: String, value: String, percentageChange: Double? = nil) {
self.title = title
self.value = value
self.percentageChange = percentageChange
}
}
market 데이터들 합쳐보자
이전엔 market을 사용했는데 이번엔 global data를 사용해보자
포트폴리오 뷰 만들기
스태티스틱 포폴 밸류 추가, 리로드 기능 추가, 햅틱매니저
sortOption이라는 enum 만들고 이 값에 따라서 sort 값 switch
combine으로 함께 묶어줌
Navigation 링크 형태로 그대로 디테일뷰를 구성하게 되면
디테일뷰가 그냥 다 로드 되버리게 됨
api콜 같은 게 있었다면 앱에 큰 영향들을 미치게 되겠죠
그리고 .contentShape를 사용하면 뷰의 빈공간도 터치가 되게끔 만들어준다!!
아직 해결하지 못한 거 하나!
DetailView가 ForEach문에서 생성이 될 때 init에서 출력하는 프린트문이 두번 출력되는 현상...
코인 디테일모델, 데이타서비스, 뷰모델 만들어줌