포스트

BB Quotes (4)

Version 2로 업그레이드

Image

이제 이렇게 조금 더 개선을 해보려고 한다.

그리고 기능은

Image

위의 내용을 기반으로 하려고 한다.

우선 이미지와 samplejson 파일을 추가해주고 Color set도 추가를 해준다.

우리가 할것은


Version 2 Feature List:

  • Add El Camino tab
  • Utilize all character images on CharacterView
  • On CharacterView, auto-scroll to bottom after status is shown
  • Fetch episode data
  • Extend String to get rid of long image and color names
  • Create static constants for show names

이렇게 나뉘어 진다.

1. Tab 추가

이건 뭐 굳이 크게 언급할만 한건 없다.

1
2
3
4
Tab("El Camino", systemImage: "car") {
    QuoteView(show: "El Camino")
        .toolbarBackgroundVisibility(.visible, for: .tabBar)
}

Image

그러면 이렇게 정상적으로 보여지는 것 까지는 확인했다.

물론 작동도 되지만 그건 패스

2. TabView를 사용하여 이미지 보여주기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ScrollView {
    TabView {
        ForEach(character.images, id: \.self) { characterImageURL in
            AsyncImage(url: characterImageURL) { image in
                image
                    .resizable()
                    .scaledToFill()
            } placeholder: {
                ProgressView()
            }
        }
        
    }
    .tabViewStyle(.page)
    .frame(width: geo.size.width / 1.2, height: geo.size.height / 1.7)
    .clipShape(.rect(cornerRadius: 25))
    .padding(.top, 60)
    // 생략
}

TabView를 통해 이미지를 페이징 하여 보여주는건 여기서 언급하지는 않았는데 개인적으로 가지고 있는 Ebook에서 본적이 있다.

SwiftUI의 새로운 TabView 스타일을 사용하면, 탭 뷰 안의 콘텐츠를 가로로 스와이프하여 넘길 수 있고, 화면에 진입할 때 자동으로 “스냅”되듯 정렬되는 효과를 만들 수 있다.

Image

이렇게 페이징하면서 이미지들을 확인 할 수 있다.

3. ScrollViewReader 사용하기

현재는 disclosuregroup 부분을 탭하면 자동으로 내려가지않고 직점 스크롤 다운을 해야하는데

그 문제를 새로운 ScrollViewReader를 사용하여 해결해보자

ScrollViewReader는 내부에 있는 특정 뷰로 스크롤을 프로그래밍적으로 이동할 수 있게 도와주는 SwiftUI 컴포넌트이다.

이것도 생소하니 Docs 참고

사용 위치는 여기

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
struct CharacterView: View {
    let character: Char
    let show: String
    
    var body: some View {
        GeometryReader { geo in
            ScrollViewReader { proxy in // new
                ZStack(alignment: .top) {
                    // 생략
                    
                    ScrollView {
                        TabView {
                            // 생략
                        }
                        . // 생략
                        
                        VStack(alignment: .leading) {
                             // 생략
                        }
                        .frame(width: geo.size.width / 1.25, alignment: .leading)
                        .padding(.bottom, 50)
                        .id(1) // new
                    }
                }
            }
        }
        .ignoresSafeArea()
    }
}

이렇게 Zstack을 감싸주자.

이건 특이하게 id가 필요하다. 그래서 위에 보면 Vstack의 마지막에 id(1)을 해주었다.

이걸 잘 기억해두자.

이제는 onAppear를 통해 image가 로드가 되었을때 자동으로 스크롤 다운을 하게 만들어준다.

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
DisclosureGroup("Status (spoiler alert!):") {
    VStack(alignment: .leading) {
        Text(character.status)
            .font(.title2)
        
        if let death = character.death {
            AsyncImage(url: death.image) { image in
                image
                    .resizable()
                    .scaledToFill()
                    .clipShape(.rect(cornerRadius: 15))
                    .onAppear { // new
                        withAnimation {
                            proxy.scrollTo(1, anchor: .bottom)
                        }
                    }
            } placeholder: {
                ProgressView()
            }
            
            Text("How: \(death.details)")
                .padding(.bottom, 7)
            
            Text("Last words: \"\(death.lastWords)\"")
            
        }
    }
    .frame(maxWidth: .infinity, alignment: .leading)
}

onAppear를 통해 image가 로드가 되었을때 자동으로 스크롤 다운을 하게 만들어준다.

실행해보면

Image

이렇게 progress가 사라지고 자동으로 스크롤 다운이 되는걸 알 수 있다.

바로 proxy.scrollTo(1, anchor: .bottom) 이 부분 때문인데

위에서 우리는 id(1)을 통해 Vstack의 마지막 부분해 해주었다. 그렇기에 proxy를 통해 1번쪽에서 이동하고 bottom으로 맞추라고했기에, 우리가 볼때는 이미지가 로드 되자마자 스크롤이 자동으로 내려가도록 보이는 것이다.


정리

CharacterView에서 DisclosureGroup 내부에 있는 AsyncImage가 로드되었을 때, 자동으로 스크롤이 하단으로 이동하게 만들고 싶었다. 이를 위해 사용한 방식은 다음과 같다:

핵심 코드 구조
1
2
3
4
5
6
7
8
9
10
ScrollViewReader { proxy in
    ZStack {
        ScrollView {
            VStack {
                // 여러 뷰들
            }
            .id(1) // 스크롤 타겟
        }
    }
}

그리고 AsyncImage 안에서 이미지가 로드되었을 때 다음과 같이 스크롤 명령을 넣었다:

1
2
3
4
5
.onAppear {
    withAnimation {
        proxy.scrollTo(1, anchor: .bottom)
    }
}

보완 설명

  • ScrollViewReader는 내부에 있는 뷰들 중 .id()로 식별 가능한 뷰로 스크롤할 수 있다.
  • .id(1)은 우리가 이동하고자 하는 타깃 뷰에 붙인다. 여기서는 VStack에 붙여 해당 부분이 id == 1임을 지정한다.
  • .scrollTo(1, anchor: .bottom)id == 1인 뷰로 스크롤하되, 그 뷰가 뷰포트의 하단(anchor: .bottom)에 맞춰지도록 한다.
  • 이 방식은 비동기 이미지 로딩처럼 동적으로 화면이 바뀔 때 유용하며, 사용자 개입 없이도 자동 스크롤을 구현할 수 있다.

요약

  • ScrollViewReader로 특정 위치로 스크롤할 수 있다.
  • .id()는 스크롤의 타깃이 되는 뷰에 붙여야 한다.
  • .scrollTo(_:anchor:)를 통해 정확한 위치로 애니메이션 스크롤이 가능하다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.