티스토리 뷰
약 두달정도 사이드 프로젝트 팀 'WEAVE'에 들어가 열심히 프로젝트를 진행했다.
프로젝트 진행 중 예상치 못하게 그라데이션에서 고민했던 부분이 있어서 글로 남겨보려고 한다.
디자인 요구사항은 이러했다. 그라데이션이 이어지는 형태의 Stepper 다.
하지만 저 디자인을 코드로 옮기려니 그라데이션 부분에서 쉽지 않았다.
일단 Setpper 를 구현해보자.
Stepper 구현
public struct WeaveStepper: View {
let maxStepCount: Int
let currentStep: Int
public init(
maxStepCount: Int,
currentStep: Int
) {
self.maxStepCount = maxStepCount
self.currentStep = currentStep
}
public var body: some View {
HStack(spacing: 3) {
ForEach(0 ..< maxStepCount, id: \.self) { index in
if currentStep < index {
RoundedRectangle(cornerRadius: 4)
.foregroundStyle(DesignSystem.Colors.darkGray)
} else {
RoundedRectangle(cornerRadius: 4)
.foregroundStyle(
.white
)
}
}
}
.frame(height: 8)
}
}
#Preview {
VStack(spacing: 60) {
WeaveStepper(
maxStepCount: 5,
currentStep: 2
)
WeaveStepper(
maxStepCount: 8,
currentStep: 1
)
WeaveStepper(
maxStepCount: 4,
currentStep: 3
)
}
.padding(.horizontal, 16)
}
Stepper 코드는 간단하다.
step의 maxCount를 넣고, 현재의 단계를 받아 Hstack을 이용해 구현했다.
Preview 결과는 아래처럼 원하는대로 잘 나왔다.
그라데이션을 어떻게 넣지?
문제가 생겼다.
디자인 요구사항을 다시 보면
그라데이션이 모든 Step들에 걸쳐서 있고
첫번째 Step 부터 마지막 Step 까지 연속되어야 한다.
여러가지 고민을 해보았다.
1. 디자인의 Step은 5개니까, 각 Step별 그라데이션 컬러값을 지정하기
-> 요구되는 Step 이 더 많아지게 될 경우 수정이 쉽지 않다.
-> 개발자스럽지 않음
2. Hstack의 백그라운드에 그라데이션을 칠하고,
도달한 Step은 clear색으로 칠하고, 아직 도달하지 못한 Step은 회색으로 칠하기
-> 실제로 시도를 해보았는데, SwiftUI에서는 구현이 쉽지 않았다.
-> UIKit이라면 이 방법이 가능할 수도 있을 것 같음
3. 그라데이션 객체에서 start 지점과 end 지점의 %값을 구하고,
start와 end에 해당하는 컬러값을 뽑아내 그 컬러로 새로운 그라데이션을 만든다.
-> 이 방법으로 위 UI를 구현했다.
그럼 만들어보자!
먼저, 그라데이션을 만들 컬러의 배열을 만들었다.
(Figma dev 모드에서 이렇게 만들어줬다. 쓸모있는듯..)
public extension Color {
static let weaveGradientColors: [Color] = [
Color(red: 0.45, green: 0.81, blue: 0.08),
Color(red: 0.6, green: 0.75, blue: 0.09),
Color(red: 0.72, green: 0.69, blue: 0.08),
Color(red: 0.8, green: 0.62, blue: 0.04),
Color(red: 0.81, green: 0.54, blue: 0.21),
Color(red: 0.8, green: 0.35, blue: 0.43),
Color(red: 0.8, green: 0.18, blue: 0.68)
]
}
그라데이션이 시작되는 start 지점과 끝나는 end 지점의 0.0 ~ 1.0 사이의 값을 구해주었다.
func processGradient(index: Int) {
let start = CGFloat(1) / CGFloat(maxStepCount) * CGFloat(index)
let end = CGFloat(1) / CGFloat(maxStepCount) * CGFloat(index + 1)
}
제네릭으로 Color의 배열에 Extension을 만들어주었다.
사용할 때는 위에서 구한 start와 end 지점의 point 값을 파라미터로 받는다.
/*
1. [Color] 내부의 컬러 요소로 Linear Gradient를 만들고
2. 특정 point (0.0 ~ 1.0) 의 컬러값을 리턴합니다.
-> 예를들어 point가 0.2 인 경우 Gradient의 20% 지점의 Color를 연산하여 리턴합니다.
*/
public extension Array where Element == Color {
func colorAtPoint(at point: CGFloat) -> Color {
let count = CGFloat(self.count)
let step = 1.0 / (count - 1)
let currentIndex = Swift.min(Swift.max(Int(point / step), 0), self.count - 2)
let nextIndex = currentIndex + 1
let currentPosition = step * CGFloat(currentIndex)
let nextPosition = step * CGFloat(nextIndex)
let percentageDiff = (point - currentPosition) / (nextPosition - currentPosition)
// 현재 및 다음 색상 구하기
let currentColor = self[currentIndex]
let nextColor = self[nextIndex]
// CIColor로 변환
let currentCIColor = CIColor(color: UIColor(currentColor))
let nextCIColor = CIColor(color: UIColor(nextColor))
// 컴포넌트 추출
let currentRed = currentCIColor.red
let currentGreen = currentCIColor.green
let currentBlue = currentCIColor.blue
let currentAlpha = currentCIColor.alpha
let nextRed = nextCIColor.red
let nextGreen = nextCIColor.green
let nextBlue = nextCIColor.blue
let nextAlpha = nextCIColor.alpha
// 색상 보정 및 RGBA 구하기
let red = currentRed + (nextRed - currentRed) * percentageDiff
let green = currentGreen + (nextGreen - currentGreen) * percentageDiff
let blue = currentBlue + (nextBlue - currentBlue) * percentageDiff
let alpha = currentAlpha + (nextAlpha - currentAlpha) * percentageDiff
return Color(red: Double(red), green: Double(green), blue: Double(blue), opacity: Double(alpha))
}
}
함수가 복잡해보이지만 차례대로 설명을 하면.
1. [color]의 extension 이기 때문에 self 는 [color] 다.
2. self의 count를 가지고 내부는 Step으로 요소들을 관리하며 index로 각 컬러에 접근
3. 파라미터로 들어온 point에 속하는 color 배열의 index값과 next index값을 구함. (최소, 최대 validation)
4. 여기서 currentIndex와 nextIndex로 접근하기 때문에 비율에 맞는 정확한 값이 아님
5. 정환한 값으로 보정하기 위해 currentIndex와 nextIndex 사이 몇 % 지점에 해당하는지 구함 (percentageDiff)
6. currentIndex에 해당하는 컬러와 nextIndex에 해당하는 각각 rgb 값을 추출
7. 앞서 구한 percentageDiff 를 이용해 선형보간법을 이용해 색상 보정하여 정확한 값을 새로 만들어줌
8. Color 객체를 만들어 리턴
복잡해 보이지만 복잡한게 맞다.
특히, rgb를 추출해내는 메소드가 SwiftUI의 Color 에는 없어서 UIKit을 빌려서 사용했고,
그라데이션을 그린 뒤 뷰에서 추출하는 것이 아니기 때문에 보정이 들어가야 했다.
(UIKit이였다면 임시로 UIView로 그린 뒤에 좌표값 비율로 뽑아냈을까?)
그렇게 최종 완성된 Stepper는 다음 코드다.
public struct WeaveStepper: View {
let maxStepCount: Int
let currentStep: Int
public init(
maxStepCount: Int,
currentStep: Int
) {
self.maxStepCount = maxStepCount
self.currentStep = currentStep
}
public var body: some View {
HStack(spacing: 3) {
ForEach(0 ..< maxStepCount, id: \.self) { index in
if currentStep < index {
RoundedRectangle(cornerRadius: 4)
.foregroundStyle(DesignSystem.Colors.darkGray)
} else {
RoundedRectangle(cornerRadius: 4)
.foregroundStyle(
processGradient(index: index)
)
}
}
}
.frame(height: 8)
}
func processGradient(index: Int) -> some ShapeStyle {
let start = CGFloat(1) / CGFloat(maxStepCount) * CGFloat(index)
let end = CGFloat(1) / CGFloat(maxStepCount) * CGFloat(index + 1)
return LinearGradient(
colors: [
.weaveGradientColors.colorAtPoint(at: start),
.weaveGradientColors.colorAtPoint(at: end)
],
startPoint: UnitPoint(x: 0.0, y: 0.5),
endPoint: UnitPoint(x: 1, y: 0.5)
)
}
}
WeaveGradientColors 배열에서 각각 start, end 지점으로 뽑아낸 두개의 새로운 컬러값으로
새로운 LinearGradient 그라데이션을 만들어 리턴한다.
'iOS' 카테고리의 다른 글
[iOS] SwiftUI 섹션이 있는 메뉴 리스트 만들기 (0) | 2024.04.22 |
---|---|
[iOS] 서버 환경 분리하기(prod/dev, with Tuist) (0) | 2024.04.19 |
[iOS] 오픈소스 기여를 해보았다. (2) | 2024.01.04 |
[iOS] DateFormatter 연도가 잘못 나올 때(주 기반 연도, YYYY) (0) | 2024.01.02 |
[iOS] Watch Connectivity 데이터 전송이 안될 때 (기타 이슈들) (0) | 2023.11.11 |
- Total
- Today
- Yesterday
- auth
- watch connectivity
- SwiftUI
- OAS
- 토큰
- avplayer
- keyboardtype
- Xcode15
- KVO
- 소수점
- demical
- openapi-generator
- Swift
- AVFoundation
- open-api-generator
- swift날짜
- watchOS
- easy cue
- IOS
- DateFormatter
- musicplayer
- 회고
- 애플워치
- 2024년
- retry
- Xcode
- flo
- TextField
- 애플워치 데이터 전송
- locale
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |