SwiftUI 예제: 알파벳 내비게이터(Alphabet Navigator) 만들기
SwiftUI 예제: 알파벳 내비게이터(Alphabet Navigator) 만들기
알파벳 내비게이터 만들기
아래와 같이 알파벳으로 섹션이 나뉘어져 있으며 해당 알파벳을 클릭하면 섹션으로 이동하는 기능을 알파벳 내비게이터라고 칭하겠습니다. (정식 명칭은 다를 수 있습니다.)
출처
기본 형태
Contacts배열에 있는 사람 목록을 보여주는 뷰입니다.- 예제를 복잡하지 않게 하기 위해 단순
[String]배열로 만들었습니다.
- 예제를 복잡하지 않게 하기 위해 단순
- 아래 기본 형태를 바탕으로 진행합니다.
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
32
33
34
35
import SwiftUI
struct ContentView: View {
@State private var searchText = ""
var contacts = [String]()
var body: some View {
ScrollViewReader { scrollProxy in
List {
ForEach(contacts, id: \.self) { contact in
HStack {
Image(systemName: "person.circle.fill")
.font(.largeTitle)
.padding(.trailing, 5)
Text(contact)
}
}
}
.navigationTitle("Contacts")
.listStyle(PlainListStyle())
}
}
init() {
contacts = [
"Chris", "Ryan", "Allyson", "Ryan", "Jonathan", "Ryan", "Brendan", "Ryaan",
"Jaxon", "Riner", "Leif", "Adams", "Frank", "Conors", "Allyssa", "Bishop",
"Justin", "Bishop", "Johnny", "Appleseed", "George", "Washingotn", "Abraham", "Lincoln",
"Steve", "Jobs", "Steve", "Woz", "Bill", "Gates", "Donald", "Trump", "Darth", "Vader",
"Clark", "Kent", "Bruce", "Wayne", "John", "Doe", "Jane", "Doe", "Rei", "Kim",
"James", "Elephant", "Julius", "Fucik", "Kane", "Hammersmith",
]
contacts.sort()
}
}
사람 이름을 알파벳 섹션으로 분류하기
Step 1: 전역 변수로 알파벳 배열을 추가합니다.
1
let alphabet: [String] = (65...90).map { String(UnicodeScalar($0)!) }
65는 대문자A의 아스키 코드이며,90은 대문자Z의 아스키 코드입니다.A부터Z까지의 아스키 코드 범위에서 아스키 코드를 실제 문자로 변환해서 배열로 저장합니다.
Step 2: 알파벳 첫문자로 Contacts 배열 필터링하기
1
2
3
func contactsFilter(by letter: String) -> [String] {
contacts.filter { $0.prefix(1) == letter }
}
contact의 첫문자(prefix(1))가letter(알파벳 문자)와 일치할 경우만 필터링합니다.
Step 3: 알파벳 섹션 만들기
List {...} 안에 다음을 추가합니다.
1
2
3
4
5
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter).id(letter)) {
}
}
ForEach를 통해 알파벳 문자마다 섹션을 만들고, 헤더 텍스트와id를 해당 알파벳으로 지정합니다.id는 나중에 알파벳 내비게이터에서 버튼을 눌렀을 때 스크롤 위치를 지정하기 위한 역할입니다.
Step 4: 섹션별로 필터링된 사람 목록 보여주기
위 Section {...} 안에 사람 목록을 보여주는 ForEach문을 넣되, 대상 자료를 필터링된 Contacts인 contactsFilter(by:)로 바꿔줍니다.
1
2
3
4
5
6
7
8
9
10
11
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter).id(letter)) {
ForEach(contactsFilter(by: letter), id: \.self) { contact in
HStack {
Image(systemName: "person.circle.fill")
.font(.largeTitle)
.padding(.trailing, 5)
Text(contact)
}
}
}
letter에 따라 필터링된 목록을 보여줍니다.
알파벳 내비게이터 추가: 탭 방식
알파벳을 누르면 해당 섹션으로 이동하는 기초적인 내비게이터를 추가하겠습니다. ScrollView {...}의 오버레이를 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.overlay(alignment: .top) {
VStack {
ForEach(alphabet, id: \.self) { letter in
HStack {
Spacer()
Button {
withAnimation {
scrollProxy.scrollTo(letter, anchor: .top)
}
} label: {
Text(letter)
.font(.system(size: 15))
.padding(.trailing, 7)
}
}
}
}
}
overlay(alignment: .top) {...}- 오버레이를 추가하며, 오버레이 뷰가 top을 기준으로 정렬됩니다.- alignment를 추가하지 않으면 스크롤 뷰의 한가운데에 위치하게 됩니다.
HStack에Spacer()를 줘서 내비게이터가 화면 오른쪽으로 붙어있도록 합니다.Button의 첫 번째 트레일링 클로저(action)에scrollTo(아이디)명령을 추가해 버튼을 누르면 해당 섹션 타이틀로 이동하도록 합니다.anchor가.top이어야 헤더가 눈에 보이는 제일 위에 위치하게 됩니다.withAnimation으로 감싸면 스크롤 애니메이션이 되면서 자연스럽게 이동하고, 사용하지 않으면 애니메이션 없이 바로 이동합니다.
탭(클릭)하면 해당 알파벳 헤더로 이동합니다.
알파벳 내비게이터 추가: 탭 + 드래그 방식
위에 예제도 바로 사용가능하긴 하지만, 기존에 알던 알파벳 내비게이터는 드래그로도 선택할 수 있고, 진동도 울렸던 것으로 기억합니다.
Step 1: 뷰 분리
1
2
3
.overlay(alignment: .top) {
AlphabetNavigator(scrollViewProxy: scrollProxy)
}
- 위 버튼 예제에서
overlay안의 컨텐츠를 위와 같이 바꾸고 별도의struct로 분리합니다. View를 준수하는AlphabetNavigator구조체입니다.ScrollViewProxy는ScrollViewReader의 프록시를 넘겨줍니다.
Step 2: AlphabetNavigator 뷰 구현
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
struct AlphabetNavigator: View {
let scrollViewProxy: ScrollViewProxy
@GestureState private var dragLocation: CGPoint = .zero
@State private var currentLetter = ""
func dragObserver(title: String) -> some View {
GeometryReader { geometry in
dragObserver(geometry: geometry, title: title)
}
}
func dragObserver(geometry: GeometryProxy, title: String) -> some View {
if geometry.frame(in: .global).contains(dragLocation) {
DispatchQueue.main.async {
currentLetter = title
withAnimation {
scrollViewProxy.scrollTo(title, anchor: .top)
}
}
}
return Rectangle().fill(.clear)
}
var body: some View {
VStack {
ForEach(alphabet, id: \.self) { letter in
HStack {
Spacer()
Text(letter)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(.cyan)
.padding(.trailing, 7)
.opacity(letter == currentLetter ? 0.3 : 1)
.background(dragObserver(title: letter))
}
}
}
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .global)
.updating($dragLocation) { value, state, _ in
state = value.location
}
)
.onChange(of: currentLetter) { _ in
if currentLetter != "" {
Vibration.light.vibrate()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
withAnimation {
currentLetter = ""
}
}
}
}
}
}
scrollViewProxy: 스크롤 뷰 이동기능을 위한 프록시입니다.dragLocation:@GestureState를 사용해 현재 드래그중인 영역을 저장합니다.currentLetter: 현재 선택한 ID를 저장합니다. 선택된 알파벳을 서식 처리하고 진동을 울리기 위해 필요합니다.dragObserver:GeometryReader를 사용해 현재 드래그 위치가 리더가 제공하는 해당 영역에 있다면 스크롤 이동 명령을 실행합니다.Text(letter) ...: 액션을 외부 함수에서 실행하므로 버튼을 제거하고 텍스트만 남겨둡니다..opacity(letter == currentLetter ? 0.3 : 1): 현재 선택중이라면 불투명도를 낮춥니다(=> 더 투명해집니다.).background(dragObserver(title: letter)): 배경으로 현재 위치에 있는 알파벳을dragObserver로 넘깁니다.GeometryReader를 배경에 배치한 것과 동일합니다.
onChange: 현재 선택된 알파벳에 따라 진동을 울리고, 2초 뒤에 선택을 해제해서 계속 투명하게 보이지 않도록 합니다.
탭뿐만 아니라 드래그로도 이동할 수 있습니다. 실제 기기라면 진동도 울립니다.
전체 코드
This post is licensed under
CC BY 4.0
by the author.





