JEP's Diary

SwiftUI의 LazyVStack의 아이템 갱신 이슈 본문

Development/개발일지

SwiftUI의 LazyVStack의 아이템 갱신 이슈

지으니88 2023. 4. 25. 00:01

간헐적인 이슈가 수정 하기 어려운데.. 간헐적으로 리스트의 아이템이 갱신이 되지 않는 이슈를 만났다!

 

이슈현상

1.A 계정의 컬렉션 데이터 리스트를 LazyVStack을 이용하여 리스트를 보여준다.

2.계정을 B로 변경하여 컬렉션 데이터 리스트를 다시 구성한 후 같은 뷰에 데이터를 갱신한다.

3. 1,2번 과정을 반복하다보면 간헐적으로 A계정의 리스트 세번째 Row에 변경 직전 B계정의 세번째 Row가 보이는 이슈가 발생했다. 또는 B계정의 컬렉션 리스트 세번째 Row에 A 계정의 세번째 Row가 보이기도 했다.

특이하게도 세번째 Row가 이슈였다.

 

해결해보자

원인을 찾아야 하는데 솔직히 정확한 원인은 못찾았다...ㅜ

느낌적인 느낌은 LazyVStack의 각 Row를 스크롤할때 화면에 표시되는 부분정도의 Row만 그리고 스크롤하여 화면 밖으로 없어지는 Row들이 재사용되어 기존꺼가 남아있는 느낌인데, 잘 모르겠다ㅜ
일단 데이터 자체는 새로운 데이터로 모두 갱신되는 것으로 확인했다. A계정으로 변경하여 B계정의 Row가 보이는 이슈 현상이 나타나더라도 로그로 출력해본 데이터 리스트는 이슈가 없었다. 오직 View에서 세번째 Row만 이전 계정의 Row가 남아 있는 것이다... 또 특이했던 것은 해당이슈가 나타난 상태에서 스크롤을 쭉 더 내려서 이슈가 되는 Row가 없어질때까니 스크롤 했다가 다시 위로 스크롤 하면 이슈가 됐던 세번째 Row가 원했던 값으로 제대로 나오고 있었다. :(

 

해결 방법(?)

팀원이 구글링 해서 수정한 방법은 LazyVStack 에 id를 부여해주었다. 그리고 50번 가량 테스트해봐도 이슈가 발생하지 않았음!

UIKit에서는 UITableView의 reloadData() 함수로 명확하게 갱신을 해주지만, SwiftUI에서는 그러한 함수가 없다고 하여 id를 부여하여 유니크한 id를 부여해줌으로써 해결했다.

LazyVStack(spacing: 32) {
	SkeletonForEach(with: viewModel.items, quantity: 2) { isLoading, data in
		RowView(data)
		...
	}
}
.id(UUID()) // <- 요부분
.padding(.top, 32)
extension View {

    /// Binds a view's identity to the given proxy value.
    ///
    /// When the proxy value specified by the `id` parameter changes, the
    /// identity of the view — for example, its state — is reset.
    @inlinable public func id<ID>(_ id: ID) -> some View where ID : Hashable

}

.id(UUID)의 한줄이 정확한 해결책이었을까에 대한 의문이 들어서, 조금 더 살펴봤다.

위의 LazyVStack 코드를 보면 리스트를 표시할때 SkeletonForEach를 사용하고 있다. SkeletonForEach 안쪽 코드를 보면 ForEach의 id를 self로 설정하고 있는데, 이때 self가 혹시 중복이 되는 케이스가 생긴건가...싶기도 했다.

id로 명확하게 각각의 데이터가 명확하게 식별값이 생기다 보니, 간헐적으로 발생됐던 이슈가 해결된 거 같긴하다.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public struct SkeletonForEach<Data, Content>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Content: View {
    private let data: Data
    private let quantity: Int
    private let content: (Bool, Data.Element?) -> Content

    public init(with data: Data, quantity: Int = 1, @ViewBuilder content: @escaping (Bool, Data.Element?) -> Content) {
        self.data = data
        self.quantity = quantity
        self.content = content
    }

    public var body: some View {
        ForEach(0 ..< (data.isEmpty ? quantity : data.count), id: \.self) { index in
            self.content(self.data.isEmpty, self.data.isEmpty ? nil : self.data.map { $0 }[index])
        }
    }
}

 

----------------------------------------------------------------------------------------------------------------------

04.26 갱신 이슈의 원인을 찾았음...ㅜ

원인

리스트라서 로딩되는 동안 스켈레톤 화면을 보여주고 있었는데, 이것이 문제였다.

LazyVStack(spacing: 32) {
// SkeletonForEach 안에서 RowView를 그리는 것이 문제였음
	SkeletonForEach(with: viewModel.items, quantity: 2) { isLoading, data in
		RowView(data)
		.skeleton(with: viewModel.state == .isLoading)
		.shape(type: .rounded(RoundedType.radius(16)))
		.animation(type: .linear())
		.frame(width: width, height: height)
	}
}
.padding(.top, 32)

해결

스켈레톤 리스트와 Collection 리스트를 명확히 구분해주었다.

그리고 위에서 언급했던 해결책이라고 생각했던 .id(UUID)를 추가했을때의 새로운 이슈는 RowView가 onAppear 될때의 이벤트가 여러번 콜이 되어 API 콜이 여러번 발생했던 이슈가 있었다. 위의 이슈가 있는 코드에서 .id(UUID)를 추가한 것이라 명확한 해결책은 아니었던 것이다. 

스켈레톤 리스트 구조 안에 그리려고 하는 RowView를 섞어서 보여주지 말자....ㅜㅜ  데이터 갱신할때 영향이 가는듯! 

if viewModel.state == .isLoading {
	SkeletonListView()
} else {
	LazyVStack(spacing: 32) {
		ForEach(with: viewModel.items, id:\.self) { data in
			RowView(data)
		}	
	}
}