알파벳 내비게이터 만들기
아래와 같이 알파벳으로 섹션이 나뉘어져 있으며 해당 알파벳을 클릭하면 섹션으로 이동하는 기능을 알파벳 내비게이터라고 칭하겠습니다. (정식 명칭은 다를 수 있습니다.)
출처
기본 형태
Contacts
배열에 있는 사람 목록을 보여주는 뷰입니다.- 예제를 복잡하지 않게 하기 위해 단순 [String] 배열로 만들었습니다.
- 아래 기본 형태를 바탕으로 진행합니다.
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: 전역 변수로 알파벳 배열을 추가합니다.
let alphabet: [String] = (65...90).map { String(UnicodeScalar($0)!) }
65
는 대문자A
의 아스키 코드이며,90
은 대문자Z
의 아스키 코드입니다.A
부터Z
까지의 아스키 코드 범위에서 아스키 코드를 실제 문자로 변환해서 배열로 저장합니다.
Step 2: 알파벳 첫문자로 Contacts 배열 필터링하기
func contactsFilter(by letter: String) -> [String] { contacts.filter { $0.prefix(1) == letter } }
contact
의 첫문자(prefix(1)
)가letter
(알파벳 문자)와 일치할 경우만 필터링합니다.
Step 3: 알파벳 섹션 만들기
List {...}
안에 다음을 추가합니다.
ForEach(alphabet, id: \.self) { letter in Section(header: Text(letter).id(letter)) { } }
ForEach
를 통해 알파벳 문자마다 섹션을 만들고, 헤더 텍스트와id
를 해당 알파벳으로 지정합니다.id
는 나중에 알파벳 내비게이터에서 버튼을 눌렀을 때 스크롤 위치를 지정하기 위한 역할입니다.
Step 4: 섹션별로 필터링된 사람 목록 보여주기
위 Section {...}
안에 사람 목록을 보여주는 ForEach
문을 넣되, 대상 자료를 필터링된 Contacts
인 contactsFilter(by:)
로 바꿔줍니다.
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 {...}
의 오버레이를 추가합니다.
.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: 뷰 분리
.overlay(alignment: .top) { AlphabetNavigator(scrollViewProxy: scrollProxy) }
- 위 버튼 예제에서
overlay
안의 컨텐츠를 위와 같이 바꾸고 별도의struct
로 분리합니다. View
를 준수하는AlphabetNavigator
구조체입니다.ScrollViewProxy
는ScrollViewReader
의 프록시를 넘겨줍니다.
Step 2: AlphabetNavigator 뷰 구현
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초 뒤에 선택을 해제해서 계속 투명하게 보이지 않도록 합니다.
탭뿐만 아니라 드래그로도 이동할 수 있습니다. 실제 기기라면 진동도 울립니다.
0개의 댓글