Marquee Effect?
HTML 에서 <marquee>로 구현하는, 텍스트가 좌우로 반복적으로 움직이는 전광판 스타일의 애니메이션
광고 배너, 뉴스 속보, 혹은 앱의 메인 타이틀에서 자주 쓰임
이렇게 하려고합니다 ㅎ
처음 접근 방식: offset으로 텍스트 이동
우선 offset(x:)을 마이너스로 주면, 텍스트가 왼쪽으로 이동한다는 개념을 바탕으로 구현해봤습니다.
1. 첫번째 화면
| 화면너비 |
| [텍스트....] |
| |
2. offset: - 50
| 화면너비 |
| [텍스트....] |
| |
3. offset: - 100
| 화면너비 |
| 트....] |
| |
이런 식으로 말이죠
그런데..!
텍스트 한 번 보여지고 사라질 경우에 어떻게 해야할지 고민해보았을 때,
a. 무한히 문자열 추가 (배열 - append / 스트링 - + "text" )
b. text 를 원위치로 돌리고 다시 실행
만약 a 방법을 선택하게 되면, 사용자가 머무르는 시간만큼 무한히 배열 혹은 문자열에 계속해서 문장이 추가가 될거에요.
그렇기 때문에 저는 b의 방법을 활용하고자 offset을 0으로 돌려 처음부터 텍스트 이동을 하고자 했습니다.
그런데 여기서 문제가 생기더라구요
(3번 화면에서 4번 화면으로 갔을 때)
텍스트가 왼쪽 끝까지 이동하고, 나서 offset = 0으로 이동으로 시키면,
사용자 입장에선 텍스트가 갑자기 튕기듯 되돌아가는 듯한 어색함을 줍니다.
해결 방법: 텍스트를 두 번 이어붙이기
HStack(spacing: 0) {
Text(baseText)
Text(baseText)
}
.offset(x: offset)
그냥 Text 두 번.. 연속으로 보여주기 입니다.
1. 첫번째 화면
| 화면너비 |
| [텍스트....] [텍 | 스트....]
| |
2. offset: - 50
| 화면너비 |
| [텍스트....] [텍스| 트....]
| |
3. offset: - 100
| 화면너비 |
|[텍스트....] |
| |
4. offset: 0 (원위치)
| 화면너비 |
| [텍스트....] [텍 | 스트....]
| |
그런데..!
이번에도 3번 화면에서 4번 화면으로 넘어갈 때 사용자 입장에선 갑자기 "텍" 이라는 글자가 갑자기 생긴것 처럼 보일 것 같아요..
저의 경우엔 이 화면에 작성할 텍스트가 화면 너비를 넘어가지만
그렇지 않은 경우를 생각했을 때는 일부러
let baseText = "그냥 이렇게 돌려보는거에요ㅎㅎ" + String(repeating: " ", count: 20)
공백을 넣어주면 좋을 것 같습니다.
그렇게 되면 offset이 0일 때 문장이 바뀌는 작업이 화면 밖에서 일어나기 때문에 사용자 입장에선 계속해서 물 흐르듯이 스무스하게 텍스트가 이어지더라구요~
전체 코드(조잡하지만..)
struct Marquee: View {
@State private var offset: CGFloat = 0
@State private var textWidth: CGFloat = 0
@State private var containerWidth: CGFloat = 0
@State private var isAnimating = false
let baseText = "그냥 이렇게 돌려보는거에요ㅎㅎ" + String(repeating: " ", count: 20)
let speed: CGFloat = 40
var body: some View {
GeometryReader { geo in
let containerW = geo.size.width
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
Text(baseText)
.background(
GeometryReader { textGeo in
Color.clear
.onAppear {
textWidth = textGeo.size.width
containerWidth = containerW
if !isAnimating {
startLoop()
isAnimating = true
}
}
}
)
Text(baseText)
}
.font(.title2.bold())
.foregroundStyle(.white)
.offset(x: offset)
}
.frame(height: 60)
.background(Color.black)
.disabled(true)
.clipped()
.overlay {
let color = Color.white
HStack {
LinearGradient(colors: [color, color.opacity(0.3)], startPoint: .leading, endPoint: .trailing)
.frame(width: 30)
Spacer()
LinearGradient(colors: [color, color.opacity(0.3)].reversed(), startPoint: .leading, endPoint: .trailing)
.frame(width: 30)
}
}
}
}
func startLoop() {
let duration = textWidth / speed
offset = 0
withAnimation(.linear(duration: duration)) {
offset = -textWidth
}
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
startLoop()
}
}
}
'Swift' 카테고리의 다른 글
[iOS] Moya - task (0) | 2025.05.03 |
---|---|
[iOS] 흠..MVI...? 🤔 (2) | 2025.04.29 |
[iOS] Metal - 1 (0) | 2025.02.22 |
[iOS] Metal - 0 (0) | 2025.02.08 |
[iOS] SKIP.tools 사용기 (2) | 2025.02.05 |
- 비대칭키
- 패스트캠퍼스환급
- ios
- http
- 대칭키
- Android
- Kotlin
- 운영체제
- compose
- jetpack compose
- 환급챌린지
- 습관형성
- 네트워크
- CPU
- realm
- 오공완
- GCD
- https
- 직장인자기계발
- 패스트캠퍼스후기
- alamofire
- UIKit
- swift
- 위젯
- SwiftUI
- 자료구조
- AOS
- 앱개발
- 패스트캠퍼스
- 안드로이드
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 29 | 30 | 31 |