Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 2 additions & 53 deletions AIProject/iCOWidget/CoinWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,8 @@ struct CoinCardView: View {
.frame(maxWidth: .infinity, alignment: .trailing)

}

SparklineView(prices: coin.history)
LineChartView(values: coin.history)
.padding(8)

HStack {
Expand All @@ -255,54 +255,3 @@ struct CoinCardView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

struct SparklineView: View {
let prices: [Double]

private var normalized: [CGFloat] {
guard prices.count > 1 else { return [] }
guard let min = prices.min(), let max = prices.max(), min != max else {
return Array(repeating: 0.5, count: prices.count)
}
return prices.map { CGFloat(($0 - min) / (max - min)) }
}

var body: some View {
GeometryReader { geo in
if normalized.isEmpty {
Path { path in
let midY = geo.size.height / 2
path.move(to: CGPoint(x: 0, y: midY))
path.addLine(to: CGPoint(x: geo.size.width, y: midY))
}
.stroke(Color.gray.opacity(0.4), lineWidth: 1)
} else {
ZStack {
Path { path in
for (index, value) in normalized.enumerated() {
let x = geo.size.width * CGFloat(index) / CGFloat(normalized.count - 1)
let y = geo.size.height * (1 - value)
if index == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
}
}
}
.stroke(Color.iCoAccent, style: StrokeStyle(lineWidth: 2, lineJoin: .round))

if let last = normalized.last {
let x = geo.size.width
let y = geo.size.height * (1 - last)

Circle()
.fill(Color.blue)
.frame(width: 6, height: 6)
.position(x: x, y: y)
}
}
}
}
//.frame(height: 36)
}
}
1 change: 1 addition & 0 deletions AIProject/iCo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
App/Resource/Assets.xcassets,
Core/Local/UserDefaults/AppStorageKey.swift,
"Core/Util/Double+Util.swift",
Features/Base/LineChartView.swift,
);
target = B7C71D282E7BD9D6000E1191 /* iCOWidgetExtension */;
};
Expand Down
80 changes: 80 additions & 0 deletions AIProject/iCo/Features/Base/LineChartView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// LineChartView.swift
// iCo
//
// Created by 백현진 on 10/24/25.
//

import SwiftUI

/// 공통으로 사용할 수 있는 라인 차트(미니 차트) 컴포넌트
///
/// - Parameters:
/// - values: 차트에 표시할 값 목록 (예: 가격 데이터)
/// - lineColor: 차트 선 색상 (기본값은 `Color.iCoAccent`)
/// - showsLastDot: 마지막 지점(최근 데이터)에 점을 표시할지 여부 (`true` 시 표시)
/// - lineWidth: 선의 두께 (기본값: `2`)
struct LineChartView: View {
/// 차트에 표시할 값 목록 (예: 가격 데이터)
let values: [Double]

/// 차트 선 색상 (기본값: aiCoAccent)
var lineColor: Color = .iCoAccent

/// 마지막 지점 표시 여부
var showsLastDot: Bool = true

/// 선 두께
var lineWidth: CGFloat = 2

/// 내부 계산용 정규화된 값
private var normalizedValues: [CGFloat] {
guard values.count > 1 else { return [] }
guard let min = values.min(), let max = values.max(), min != max else {
return Array(repeating: 0.5, count: values.count)
}
return values.map { CGFloat(($0 - min) / (max - min)) }
}

var body: some View {
GeometryReader { geo in
if normalizedValues.isEmpty {
// 값이 없을 때 — 가운데 회색 선 표시
Path { path in
let midY = geo.size.height / 2
path.move(to: CGPoint(x: 0, y: midY))
path.addLine(to: CGPoint(x: geo.size.width, y: midY))
}
.stroke(Color.gray.opacity(0.4), lineWidth: 1)
} else {
ZStack {
// 스파크라인
Path { path in
for (index, value) in normalizedValues.enumerated() {
let x = geo.size.width * CGFloat(index) / CGFloat(normalizedValues.count - 1)
let y = geo.size.height * (1 - value)
if index == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
}
}
}
.stroke(lineColor, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round))

// 마지막 점
if showsLastDot, let last = normalizedValues.last {
let x = geo.size.width
let y = geo.size.height * (1 - last)

Circle()
.fill(lineColor)
.frame(width: 6, height: 6)
.position(x: x, y: y)
}
}
}
}
}
}