고도고도
🍎🍏
고도고도
전체 방문자
13,424
오늘
31
어제
64
  • 분류 전체보기 (170)
    • 🔨 프로젝트 (0)
      • TP 1 (0)
      • WhiteHCCTV (0)
      • FootPrint (0)
    • 💻 개발 (61)
      • iOS (28)
      • Android (6)
      • Kotlin (4)
      • Flutter (9)
      • Node.js (5)
      • Architecture (1)
      • 오늘의 삽질 (7)
      • 에러와의 동침 (1)
    • ✏️ 알고리즘 (6)
      • Graph (6)
      • String (0)
      • Sort (0)
    • ✍️ 코테 준비 (44)
      • Math (1)
      • Implementation (3)
      • String (3)
      • Brute Force (5)
      • Back Tracking (7)
      • Greedy (0)
      • Dynamic Programming (13)
      • Binary Search (1)
      • DFS, BFS (5)
      • Shortest Path (2)
      • Two Pointer (4)
      • MST (0)
    • 📚 CS (6)
      • Operating System (6)
    • ⛹️ 라이프 (53)
      • 2020 겨울방학 모칵코(팀) (13)
      • 2020 겨울방학 모각코(개인) (13)
      • 2021 여름방학 모칵코(팀) (8)
      • 2021 여름방학 모각코(개인) (7)
      • 코딩 테스트 (1)
      • 회고 (10)

블로그 메뉴

  • 홈
  • 깃허브
  • 링크드인

공지사항

인기 글

  • [NCSOFT] 2022 엔씨소프트 썸머 인턴 후기 - 1⋯
    2022.08.10
    [NCSOFT] 2022 엔씨소프트 썸머 인턴 후기 - 1⋯
  • [Flutter] SingleChildScrollView,⋯
    2021.08.18
    [Flutter] SingleChildScrollView,⋯
  • [iOS / SwiftUI] MapKit, 실시간으로 도로⋯
    2022.12.20
    [iOS / SwiftUI] MapKit, 실시간으로 도로⋯
  • [Android] 백그라운드에서 소켓 통신으로 이벤트 수신⋯
    2022.06.08
    [Android] 백그라운드에서 소켓 통신으로 이벤트 수신⋯
  • [iOS / SwiftUI] OnAppear, OnDisa⋯
    2022.12.01
    [iOS / SwiftUI] OnAppear, OnDisa⋯

최근 댓글

  • https://developer.apple.com/docu⋯
    고도고도
  • 게시글 잘 보았습니다. 혹시 주소에서 구를 가지고 오시는⋯
    나그네
  • 혹시 댓글이 안보이는데 .. y2e010924@naver.⋯
    eun
  • 글 솜씨가 뛰어나시네요! 좋은 글 잘 보고 갑니다 다음에도⋯
    alpha-traveler
  • NC......가슴이...웅장해집니다.......🤯
    이상한핑구 🐧

최근 글

  • [Architecture] MVVM + Clean Arch⋯
    2023.01.07
    [Architecture] MVVM + Clean Arch⋯
  • [iOS / SwiftUI] MapKit, 실시간으로 도로⋯
    2022.12.20
    [iOS / SwiftUI] MapKit, 실시간으로 도로⋯
  • [iOS / SwiftUI] OnAppear, OnDisa⋯
    2022.12.01
    [iOS / SwiftUI] OnAppear, OnDisa⋯
  • [에러와의 동침] 22년 11월 4주차
    2022.11.28
    [에러와의 동침] 22년 11월 4주차
  • [iOS / SwiftUI] 스크롤, 무한으로 즐겨요~ (⋯
    2022.11.28
    [iOS / SwiftUI] 스크롤, 무한으로 즐겨요~ (⋯

티스토리

hELLO · Designed By 정상우.
고도고도

🍎🍏

[iOS / SwiftUI] 스크롤, 무한으로 즐겨요~ (LazyVStack으로 무한 스크롤 구현하기)
💻 개발/iOS

[iOS / SwiftUI] 스크롤, 무한으로 즐겨요~ (LazyVStack으로 무한 스크롤 구현하기)

2022. 11. 28. 00:34

오늘은 ScrollView와 LazyVStack을 활용하여 SwiftUI에서 무한 스크롤을 구현해보려고 한다.

 

사실, LazyVStack이 조금 생소할 수 있다. 

LazyVStack은 말 그대로 Lazy하게 VStack을 그린다는 느낌으로,

VStack으로 보여줄 항목이 실제로 UI에 보여질 때 렌더링을 진행하는 View이다.

Apple Developer Docs

 

 

Apple Developer Documentation

 

developer.apple.com

 

그렇다면 기존에 사용하던 VStack과는 어떤 차이가 있을까?

 

평소에 사용하던 VStack은 뷰가 보여질 때(onAppear) 모든 항목을 렌더링한다.

그렇기에 ScrollView + VStack 조합으로 List를 나타낸다면 초기에 많은 리소스를 소모하게 된다.

 

적은 개수의 간단한 항목들을 표시할 경우에는 상관이 없지만,

많은 개수의 항목들을 표시할 경우에는 적절하지 못한 방법일 것이다.

 

가령, 중고거래 플랫폼 앱이 있는데 앱을 켰을 때 특정 카테고리에 있는 모든 항목을 가져온다고 해보자.

엄청난 리소스를 소모하게 될 것이다. (사용자의 셀룰러 데이터나, CPU 혹은 RAM과 같은 자원이 될 수 있다.)

 

그래서 초기에는 10개, 혹은 20개와 같이 적은 개수의 데이터만 보여주고

마지막 항목이 보여지면 데이터를 추가하는 방식인 무한 스크롤 방식으로 구현하는 것이 좋다.

 

실제로 두 가지 방식으로 구현한 결과를 보면 그 차이를 명확하게 알 수 있다.

 

onAppear에 print를 달아주고 테스트를 진행했다.

평소에 사용하던 VStack은 View가 보여질 때 모든 항목이 렌더링 되는 것을 알 수 있다. (실제로는 보여지지 않더라도)

VStack

 

LazyVStack은 View가 보여질 때 모든 항목이 렌더링 되지 않고, 실제로 보여지는 것만 렌더링 되는 것을 알 수 있다.

LazyVStack

 

또한 자주 쓰는 List 역시 Lazy하게 동작한다. (List를 애용하도록 하자)

List

그럼 이제 본론으로 들어가서, 실제 코드를 보면서 무한스크롤을 어떤 식으로 구현했는지 알아보자.

 

우선 파일 구조는 이렇게 잡았고, MVVM 패턴으로 구현했다.

 

1. Model

우선 Model의 경우, API를 호출했을 때 받아오는 Json의 구조를 그대로 구현해줬다.

import Foundation

struct Lecture: Codable {
    let pagination: Pagination
    let results: [Result]
}

struct Pagination: Codable {
    let count: Int
    let numPages: Int
    let next: URL
    
    enum CodingKeys: String, CodingKey {
        case count
        case numPages = "num_pages"
        case next
    }
}

struct Result: Codable, Identifiable {
    let id: UUID = UUID() // List를 사용하기 위해 identifiable 추가
    let name: String
    let media: Media
    let teachers: String
    let shortDesc: String
    let startDisplay: String
    
    enum CodingKeys: String, CodingKey {
        case name
        case teachers
        case media
        case shortDesc = "short_description"
        case startDisplay = "start_display"
    }
}

struct Media: Codable {
    let image: ImagePerSize
}

struct ImagePerSize: Codable {
    let raw: URL
    let small: URL
    let large: URL
}

 

사용한 API에 대한 정보는 아래 링크에서 확인할 수 있다.

 

국가평생교육진흥원_K-MOOC_강좌정보API

한국형 온라인 공개강좌(K-MOOC)의 강좌 목록 API 및 강좌 세부 정보 API 제공

www.data.go.kr

 

2. ViewModel

ViewModel에서는 Lecture를 담고 있는 리스트를 @Published로 정의했다.

또한 앱을 처음 켰을 때 강좌를 가져오는 메소드와 스크롤을 끝까지 내렸을 때 추가적으로 강좌를 가져오는 메소드를 정의했다.

import Foundation

class LectureViewModel: ObservableObject {
    @Published var items: [Result] = []
    private var nextURL: String = ""
    private var APIKEY: String = ""
    
    init() {}
    
    // 앱을 처음 켰을 때 강좌를 가져오기 위한 메소드
    func getLecturesOnServer() async throws {
        let responseData: Lecture = try await WebService.shared.loadJson("https://apis.data.go.kr/B552881/kmooc/courseList?serviceKey=\(APIKEY)&Page=1&Org=FUNMOOC&Mobile=1")
 
        DispatchQueue.main.async {
            self.items = responseData.results
            self.nextURL = "\(responseData.pagination.next)"
        }
    }
    
    // 스크롤을 끝까지 내렸을 때 추가적으로 강좌를 가져오기 위한 메소드
    func getLecturesOnServerAtFinishedScroll() async throws {
        let responseData: Lecture = try await WebService.shared.loadJson(nextURL)
        
        // 기존 강좌 목록에 새로 가져온 강좌 목록을 추가
        DispatchQueue.main.async {
            self.items += responseData.results
            self.nextURL = "\(responseData.pagination.next)"
        }
    }
}

프로퍼티 중에 nextURL의 경우, json에서 다음 강좌 목록에 해당하는 URL을 넘겨주는데 이를 활용했다. 

 

3. WebService

WebService에서는 API를 호출할 때 사용하는 메소드를 정의해줬다.

import Foundation

class WebService {
    static let shared = WebService()
    
    // json을 T타입의 데이터로 디코딩하는 메소드
    func loadJson<T: Decodable>(_ url: String) async throws -> T {
        do {
            let url = URL(string: url)!
            let (data, _) = try await URLSession.shared.data(from: url)
            return try JSONDecoder().decode(T.self, from: data)
        } catch {
            fatalError("Unable to parse data : \(error)")
        }
    }
}

타입에 상관없이 해당 클래스와 메소드를 사용할 수 있도록 제네릭을 적용했다.

 

4. View

ViewModel에서 @Published로 정의한 items를 활용했고,

앱을 실행했을 때는 items에 어떠한 데이터도 들어있지 않기에, 이 때는 ProgressBar가 보여지도록 했다.

 

또한 동시에 API를 호출하여 강좌 목록을 가져오게 하였고

강좌 목록을 가져오는데 성공하면 items에 데이터가 들어있게 되고, 이후에는 ProgressBar가 사라지도록 했다.

 

ViewModel, 아니 ObservableObject를 활용하면 이러한 형태의 앱을 정말 손쉽게 구현할 수 있다. (SwiftUI 짱짱!! 😀)

import SwiftUI

struct LectureListView: View {
    @EnvironmentObject var lectureVM: LectureViewModel
    @State private var num: Int = 0
    
    var body: some View {
        if lectureVM.items.isEmpty {
            ProgressView()
            // 앱을 처음 실행했을 때만 ProgressView를 보여줌
                .task {
                    do {
                        try await lectureVM.getLecturesOnServer()
                    } catch (let error) {
                        print("Unable to get data : \(error)")
                    }
                }
        } else {
            ScrollView {
                LazyVStack {
                    ForEach(lectureVM.items) { item in
                        LectureItemView(lecture: item)
                            .onAppear {
                                guard let index = lectureVM.items.firstIndex(where: { $0.id == item.id }) else { return }
                                
                                if index % 10 == 9 {
                                    Task {
                                        do {
                                            try await lectureVM.getLecturesOnServerAtFinishedScroll()
                                        } catch (let error) {
                                            print("Unable to get data : \(error)")
                                        }
                                    }
                                    
                                }
                            }
                    }
                }
            }
            .navigationTitle("K-MOOC 강좌 목록")
        }
        
    }
}

위에서 설명했던 것처럼 LazyVStack은 특정 아이템이 UI에 그려질 타이밍에 렌더링을 진행한다.

 

이를 활용해서 불러온 강좌 목록에서 마지막 강좌가 보여지면,

새롭게 강좌 목록을 불러오도록 구현했다.

 

강좌 목록을 보여주는 ScrollView에서 하나의 강좌를 보여주는 View인, LectureItemView는 이렇게 구현했다.

import SwiftUI

struct LectureItemView: View {
    var lecture: Result
    
    var body: some View {
        HStack {
            AsyncImage(url: lecture.media.image.small) { image in
                image
                    .resizable()
                
            } placeholder: {
                ProgressView()
                .frame(width: 140)
            }
            .scaledToFill()
            .frame(width: 140)
            .clipped()
            .cornerRadius(8)
            
            VStack(alignment: .leading) {
                Text(lecture.name)
                    .font(.body)
                    .fontWeight(.bold)
                
                Spacer()
                
                Text(lecture.teachers)
                    .font(.footnote)
                    .lineLimit(1)
                
                Spacer()
                
                Text(lecture.startDisplay)
                    .font(.footnote)
            }
            .padding(4)
            
            Spacer()
        }
        .padding(8)
    }
}

 

실제 구현 결과를 보자!

마지막 강좌가 보여질 때 API를 호출하여 10개의 강좌를 더 가져오는 것을 볼 수 있다!

저작자표시 비영리 변경금지

'💻 개발 > iOS' 카테고리의 다른 글

[iOS / SwiftUI] MapKit, 실시간으로 도로명 주소 변환하기  (2) 2022.12.20
[iOS / SwiftUI] OnAppear, OnDisappear는 언제 호출될까?  (0) 2022.12.01
[iOS / Swift] Swift 문자열 정복하기 (aka 'Character')  (0) 2022.11.03
[iOS / SwiftUI] 다양한 상태 프로퍼티들을 알아보자!  (0) 2022.10.24
[iOS / SwiftUI] 키보드가 사라지지 않아요...😩  (0) 2022.10.21
    '💻 개발/iOS' 카테고리의 다른 글
    • [iOS / SwiftUI] MapKit, 실시간으로 도로명 주소 변환하기
    • [iOS / SwiftUI] OnAppear, OnDisappear는 언제 호출될까?
    • [iOS / Swift] Swift 문자열 정복하기 (aka 'Character')
    • [iOS / SwiftUI] 다양한 상태 프로퍼티들을 알아보자!
    고도고도
    고도고도
    iOS 꿀잼
    댓글쓰기
    [iOS / SwiftUI] OnAppear, OnDisappear는 언제 호출될까?
    다음 글
    [iOS / SwiftUI] OnAppear, OnDisappear는 언제 호출될까?
    [iOS / Swift] Swift 문자열 정복하기 (aka 'Character')
    이전 글
    [iOS / Swift] Swift 문자열 정복하기 (aka 'Character')

    티스토리툴바