iOS ๐ŸŽ/SwiftUI

[SwiftUI] VStack ๊ณผ LazyVStack ๋ญ๊ฐ€ ๋‹ค๋ฅธ ๊ฑธ๊นŒ?

fram 2023. 10. 8. 01:10

๋ฆฌ์ŠคํŠธ๋Š” ์„œ๋น„์Šค ๋˜๊ณ  ์žˆ๋Š” ์•ฑ์—์„œ ๋น ์ง€์ง€ ์•Š๊ณ  ๋“ฑ์žฅํ•˜๋Š” UI ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค. ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„๋กœ ๋ถ€ํ„ฐ ํ•œ๋ฒˆ์— ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ์•„๋‹Œ ์ผ๋ถ€๋งŒ ๊ฐ€์ ธ์˜จ ๋’ค ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ ์ผ๋ถ€๋ฅผ ๋‹ค ๋ณด๋ฉด ์ƒˆ๋กœ์šด ๋ชฉ๋ก์„ ์„œ๋ฒ„๋กœ ๋ถ€ํ„ฐ ๊ฐ€์ง€๊ณ  ์˜ค๋Š” ๋ฐฉ์‹์ด ์žˆ๋Š”๋ฐ ์ด๋ฅผ load more ํ˜น์€ infinite scroll ์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค. 

 

SwiftUI์—์„œ๋Š” ๋ฆฌ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์—ฌ๋Ÿฌ๊ฐ€์ง€ View๋“ค์ด ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค. ๊ทธ ์ค‘์— VStack์€ ์•„์ดํ…œ๋“ค์„ ์„ธ๋กœ ๋ฐฉํ–ฅ์œผ๋กœ ์Œ“์Šต๋‹ˆ๋‹ค. ์ด VStack์ด ScrollView์™€ ๋งŒ๋‚˜๋ฉด ์ €ํฌ๊ฐ€ UIKit์—์„œ ๋ณด์•˜๋˜ UITableView ํ˜น์€ UICollectionView์™€ ๋™์ผํ•œ UI๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์–ด์š”. ๊ทธ๋Ÿฐ๋ฐ ์—ฌ๊ธฐ์— LazyVStack์ด๋ผ๋Š” ๊ฒƒ์ด ์žˆ์Šต๋‹ˆ๋‹ค! ๊ฐ์ž ์–ด๋””์— ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์ข‹์„๊นŒ์š”? 

 

์ฐจ์ด์ ๊ณผ ๊ฐ๊ฐ ๋™์ž‘ํ•˜๋Š” ๋งค์ปค๋‹ˆ์ฆ˜์„ ์•Œ์•„๋ด…์‹œ๋‹ค ๐Ÿ‘


์ฝ”๋“œ ์ค€๋น„ํ•˜๊ธฐ

๋ณธ๊ฒฉ์ ์œผ๋กœ ์•Œ์•„๋ณด๊ธฐ ์ „์— ์ค€๋น„ํ•ด์•ผ ํ•  ์ฝ”๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌ์ŠคํŠธ์— ์•„์ดํ…œ๋“ค์„ ๋ณด์—ฌ์ค„ ๊ฑฐ๊ธฐ ๋•Œ๋ฌธ์— ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ž‘์„ฑํ•  ViewModel๊ณผ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์ž‘์„ฑํ•ด ๋ณผ๊นŒ์š”?

struct Product: Identifiable {
    var id: UUID
    var name: String
    var price: Int
    
    init(name: String, price: Int) {
        self.id = UUID()
        self.name = name
        self.price = price
    }
}

์ด ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์€ ๋ฐ์ดํ„ฐ ์–ด๋ ˆ์ด๋ฅผ ์ด๋ฃจ๊ณ  ๊ฐ๊ฐ์˜ ๋ฐ์ดํ„ฐ๋Š” ๋ฆฌ์ŠคํŠธ์—์„œ ํ•˜๋‚˜์˜ ํ–‰์œผ๋กœ ํ‘œ์‹œ๋ ๊ฑฐ์—์š”!

 

class InfiniteScrollViewModel: ObservableObject {
    @Published var products = [Product]()
    
    /// ๋‚ด๋ถ€์ ์œผ๋กœ ํ˜„์žฌ ๋ช‡ ๋ฒˆ์งธ ํŽ˜์ด์ง€๋ฅผ ๋ถˆ๋Ÿฌ์™”๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
    private var page: Int = 0
    
    /// paging x
    func requestProducts() {
        // ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š”๋ฐ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„์„ 2์ดˆ๋กœ ์„ค์ •
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            self.products.append(contentsOf: (0...20).map {
                Product(name: "page\(self.page), \($0)item", price: (1000...30000).randomElement() ?? 0)
            })
            
            self.page += 1
        }
    }
    
    /// paging
    func requestProductsWith(id: UUID? = nil) {
        if page == 5 { return } // ์ž„์˜๋กœ ํŽ˜์ด์ง€ ๊ฐฏ์ˆ˜ ์ œํ•œ ๋‘ 
        
        if (id == nil) || (id == products.last?.id) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                self.products.append(contentsOf: (0...20).map {
                    Product(name: "page\(self.page), \($0)item", price: (1000...30000).randomElement() ?? 0)
                })
                print("ViewModel ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต ํŽ˜์ด์ง€ : \(self.page)")
                self.page += 1
            }
        }
    }
}

ViewModel์ด ์กฐ๊ธˆ ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด๋ฉด, View์—์„œ ๋ณด์—ฌ์ค„ products ๋ผ๋Š” ๋ฆฌ์ŠคํŠธ๋ฅผ @Published๋กœ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์™œ Published ์ด๋ƒ๋ฉด ์ด ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์–ด View์—๊ฒŒ UI๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ด์•ผ ํ•จ์„ ์•Œ๋ฆฌ๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค

 

๊ทธ๋ฆฌ๊ณ  ๋‘ ๊ฐœ์˜ ํ•จ์ˆ˜๋Š” ๋„คํŠธ์›Œํฌ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ€์žฅํ•œ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค ใ…Žใ…Žใ…Ž 2์ดˆ ํ›„์— products๋ผ๋Š” ๋ฆฌ์ŠคํŠธ์— ์•„์ดํ…œ๋“ค์„ ์ถ”๊ฐ€ํ•ด ์ค๋‹ˆ๋‹ค. ์ฒซ๋ฒˆ์งธ๋กœ ๋ณด์ด๋Š” ํ•จ์ˆ˜์ธ requestProducts๋Š” ํŽ˜์ด์ง•์„ ํ•˜์ง€ ์•Š๊ณ  ํ˜ธ์ถœํ•˜๋ฉด ๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ์—ญํ™œ๋งŒ ํ•ฉ๋‹ˆ๋‹ค. requestProductsWith(id:)๋Š” Product์˜ id๋ฅผ ๋ฐ›์•„์„œ ์ด id ๋‹ค์Œ ํ•ญ๋ชฉ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์งœ์—ฌ์ง„ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. ์ „์ž๋Š” VStack์—์„œ ํ›„์ž๋Š” LazyVStack์—์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค! ๐Ÿ™‹‍โ™€๏ธ

 

๋งˆ์ง€๋ง‰์œผ๋กœ ์•„์ฃผ ๊ฐ„๋‹จํ•œ View struct๊ฐ€ ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค

struct RowView: View {
    let product: Product
    
    var body: some View {
        let _ = print("body draw:::", product.name)
        HStack {
            Text(product.name)
                .font(.headline)
                .fontWeight(.bold)
            Text("\(product.price) ์›")
                .foregroundStyle(.gray)
        }
        .frame(maxWidth: .infinity)
        .frame(height: 150)
        .background(.green.opacity(0.3))
    }
}

Product๋ฅผ ํ‘œ์‹œํ•ด ์ฃผ๋Š” RowView๋Š” VStack๊ณผ LazyVStack์—์„œ ํ•˜๋‚˜์˜ ํ–‰์œผ๋กœ ํ‘œํ˜„๋ ๊ฑฐ์—์š”. ๋‹จ์ˆœํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. RowView๊ฐ€ ์–ธ์ œ ๊ทธ๋ ค์ง€๋Š” ์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด body ํ”„๋กœํผํ‹ฐ ๋‚ด๋ถ€์— print ๋ฌธ์„ ํ•˜๋‚˜ ์ถ”๊ฐ€ํ•ด ์คฌ์–ด์š”.


VStack

VStack์€ ์„œ๋ธŒ ๋ทฐ๋“ค์„ ์ˆ˜์ง ๋ฐฉํ–ฅ์œผ๋กœ ์Œ“์•„์š”. ViewBuilder๋กœ SubView๋ฅผ ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์— VStack ๋‚ด๋ถ€์—๋Š” ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋‹ค์–‘ํ•œ View๋“ค์„ SubView๋กœ ๋„ฃ์„ ์ˆ˜ ์žˆ์–ด์š”.

VStack {
	Text("row1")
	Text("row2")
}
VStack {
        ForEach(1...10, id: \.self) {
            Text("Item \($0)")
        }
}

ForEach๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ๊ฐœ์˜ ์•„์ดํ…œ์„ ํ•œ๋ฒˆ์— ์‰ฝ๊ฒŒ ๊ทธ๋ ค์ค„ ์ˆ˜ ์žˆ์–ด์š”. ๋งŒ์•ฝ์— VStack์— ๋“ค์–ด๊ฐ€๋Š” SubView ๋“ค์ด ํ™”๋ฉด์„ ๋„˜์–ด๊ฐ„๋‹ค๋ฉด ScrollView๋กœ ํ•œ๋ฒˆ ๊ฐ์‹ธ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

VStack์€ LazyVStack๊ณผ ๋‹ค๋ฅด๊ฒŒ ํ•œ๋ฒˆ์— ๋ชจ๋“  ๋ทฐ๋ฅผ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค! ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๋งŽ์€ ๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌ์ŠคํŠธ์— ํ‘œํ˜„ํ•ด ์ฃผ๊ธฐ์—๋Š” ์ ํ•ฉํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์–ด์š”. ์ด๋ฅผ ์•Œ์•„๋ณด๊ธฐ ์œ„ํ•ด TestVStackView ์ฝ”๋“œ๋ฅผ ์‚ดํŽด ๋ณผ๊ฒŒ์š”

 

struct TestVStackView: View {
    
    @StateObject var viewModel = InfiniteScrollViewModel()
    
    var body: some View {
        let _ = print("TestVStackView body property draw")
        ScrollView {
            VStack {
                ForEach(viewModel.products, id: \.id) { product in
                    RowView(product: product)
                        .onAppear {
                            print("onAppear::: ", product.name)
                        }
                }
            }
            .task {
                print("๋ฐ์ดํ„ฐ ํ˜ธ์ถœ")
                viewModel.requestProducts()
            }
            .onAppear {
                print("VStack ๋‚˜ํƒ€๋‚จ")
            }
        }
    }
}

 

์œ„์—์„œ ๋ถ€ํ„ฐ ํ•˜๋‚˜์”ฉ ์‚ดํŽด ๋ณด๋ฉด 

@StateObject var viewModel = InfiniteScrollViewModel()

์•„์ดํ…œ ๋ชฉ๋ก์ธ product๋ฅผ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด ViewModel์„ StateObject๋กœ ์„ ์–ธํ•ด ์ฃผ์—ˆ์–ด์š”. RowView์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์–ด๋А์‹œ์ ์— ๊ทธ๋ ค์ง€๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด print ๋ฌธ์„ body์— ํฌํ•จ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค. 

VStack์€ ๋‹จ์ˆœํžˆ SubView์„ ์ˆ˜์ง ๋ฐฉํ–ฅ์œผ๋กœ ์Œ“๋Š” View์ด๊ธฐ ๋•Œ๋ฌธ์— ์Šคํฌ๋กค์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด ScrollView๋กœ ๊ฐ์‹ธ์ฃผ์–ด์•ผ ํ•ด์š”. 

VStack์—์„œ task modifier์„ ๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ ์—ฌ๊ธฐ์„œ viewModel์˜ ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด ์ค๋‹ˆ๋‹ค. 

VStack๋‚ด๋ถ€์—์„œ๋Š” ForEach๋ฅผ ์‚ฌ์šฉํ•ด viewModel์˜ products ์•„์ดํ…œ๋“ค์„ ๊ทธ๋ ค์ฃผ๋Š” ์ฝ”๋“œ์—์š”

 

์ด ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์ฝ˜์†”์—์„œ ์ด๋ ‡๊ฒŒ ์ฐํž™๋‹ˆ๋‹ค! ์ฒ˜์Œ TestVStackView๊ฐ€ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚˜๊ณ  task modifier์„ ํ†ตํ•ด viewModel์˜ ๊ฐ€์งœ ๋„คํŠธ์›Œํฌ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜์š”. ์ด๋•Œ 0~20 ์˜ ์•„์ดํ…œ์ด ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. 

์ฝ˜์†”์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค ์‹ถ์ด ํ•œ๋ฒˆ์— 21๊ฐœ (0์—์„œ 20์ด๋ฏ€๋กœ)๊ฐ€ ๊ทธ๋ ค์ง€๊ณ  ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚œ ๊ฒƒ์ž…๋‹ˆ๋‹ค! ์ด๊ฑธ ๋ทฐ ๊ณ„์ธต ๊ตฌ์กฐ๋กœ ํ™•์ธํ•˜๋ฉด ์ข€๋” ์ฒด๊ฐ์ด ๋˜์š”!

Hide Clipped Content๋ฅผ ๋ˆ„๋ฅด๋ฉด ํ™”๋ฉด ๋ฐ–์— ์žˆ๋Š” View๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด์š”. ๊ทธ๋ฆผ๊ณผ ๊ฐ™์ด ๋ชจ๋“  ํ–‰์˜ View๊ฐ€ ๋ฏธ๋ฆฌ ๊ทธ๋ ค์ง€๊ณ  ์ค€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ €ํฌ๊ฐ€ 21๊ฐœ๊ฐ€ ์•„๋‹Œ 42๊ฐœ์˜€๋‹ค๋ฉด? 63๊ฐœ ์˜€๋‹ค๋ฉด? ๊ทธ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•œ ๋ทฐ๊ฐ€ ํ™”๋ฉด์— ๊ทธ๋ ค์กŒ์„ ๊ฑฐ์—์š” (๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์ด ๋ชจ๋“  ๋ทฐ๋ฅผ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ฒ ์ฃ ?)

์ด๋Š” UIKit์—์„œ UITableView ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ just UIScrollView ์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๊ณ  ์žˆ๋Š” ๊ฑฐ์—์š”!

 

์—ฌ๊ธฐ์„œ ์ž ์‹œ SwiftUI์˜ ๋งค์ปค๋‹ˆ์ฆ˜์„ ์‚ดํŽด๋ณด๋ฉด

 

TestVStackView์—์„œ body ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค. ์ด๋•Œ body ํ”„๋กœํผํ‹ฐ ๋‚ด๋ถ€์— ํฌํ•จ๋œ print๋ฌธ์ด ํ˜ธ์ถœ๋˜์–ด ์ฝ˜์†”์— ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค

๊ทธ ๋‹ค์Œ์œผ๋กœ๋Š” ๋‚ด๋ถ€์— VStack์ด ๋‚˜ํƒ€๋‚˜๊ณ  task modifier์— ์˜ํ•ด ViewModel์ด ํ˜ธ์ถœ๋˜์š”

 

ViewModel์ด ํ˜ธ์ถœ๋˜๋ฉด Published๋กœ ์„ ์–ธ๋œ ํ”„๋กœํผํ‹ฐ์˜ ๊ฐ’์„ ์—…๋ฐ์ดํŠธ ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ View์—์„œ ์ด ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. 

์ฆ‰ ์ด published๋กœ ์„ ์–ธ๋œ ํ”„๋กœํผํ‹ฐ์˜ ๊ฐ’์ด ์—…๋ฐ์ดํŠธ ๋˜๋ฉด View struct์˜ body property๋ฅผ ์ƒˆ๋กœ ๊ทธ๋ฆฌ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์—…๋ฐ์ดํŠธ ๋œ products ๊ฐ’์œผ๋กœ UI๋ฅผ ๊ทธ๋ ค์ฃผ๋ฏ€๋กœ ํ™”๋ฉด์— ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋…ธ์ถœ๋˜์š”

 


LazyVStack

๋‹ค์Œ์œผ๋กœ LazyVStack์„ ์•Œ์•„๋ณผ๊ฒŒ์š”! LazyVStack์€ ์ด๋ฆ„์—์„œ ๋ถ€ํ„ฐ ๋А๋ฆฌ๊ฒŒ View๋ฅผ ๊ทธ๋ฆฌ๊ฒ ๋‹ค๋ผ๋Š” ๊ฒŒ ๋А๊ปด์ง€์ง€ ์•Š๋‚˜์š”? 

LazyVStack์€ ํ•„์š”๋กœ ํ• ๋•Œ SubView๋ฅผ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค. 

์œ„ VStack ์„ ํ…Œ์ŠคํŠธํ•  ๋•Œ ์‚ฌ์šฉํ–ˆ๋˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ VStack์„ LazyVStack์œผ๋กœ ๋ฐ”๊พธ์–ด ์ค๋‹ˆ๋‹ค

struct TestLazyVStackView: View {
    
    @StateObject var viewModel = InfiniteScrollViewModel()
    
    var body: some View {
        let _ = print("TestLazyVStackView body property draw")
        ScrollView {
            VStack {
                ForEach(viewModel.products, id: \.id) { product in
                    RowView(product: product)
                        .onAppear {
                            viewModel.requestProductsWith(id: product.id)
                        }
                }
            }
            .task {
                print("๋ฐ์ดํ„ฐ ํ˜ธ์ถœ")
                viewModel.requestProducts()
            }
            .onAppear {
                print("VStack ๋‚˜ํƒ€๋‚จ")
            }
        }
    }
}

VStack๊ณผ ๋‹ค๋ฅด๊ฒŒ LazyVStack์€ ์•„๋ž˜ ์ด๋ฏธ์ง€ ์ฒ˜๋Ÿผ ๋กœ๊ทธ๊ฐ€ ์ฐํžˆ๊ฒŒ ๋˜์š”

0๋ถ€ํ„ฐ 5๊นŒ์ง€์˜ ์•„์ดํ…œ์ด ๊ทธ๋ ค์ง€๊ณ  ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚œ๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ๋„ 0๋ฒˆ์งธ ์•„์ดํ…œ๊ณผ ํ•˜๋‹จ์˜ 5๋ฒˆ์งธ ์•„์ดํ…œ์ด ์‚ด์ง ๋ณด์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ทฐ ๊ณ„์ธต๊ตฌ์กฐ์—์„œ ํ™•์ธํ•˜๋ฉด ๋” ํ™•์‹คํžˆ ์ฐจ์ด๊ฐ€ ๋А๊ปด์ ธ์š”

์‹ ๊ธฐํ–ˆ๋˜ ๊ฑด Scroll์˜ ์˜์—ญ์ด ๋ฏธ๋ฆฌ ์žกํ˜€ ์žˆ๋‹ค๋ผ๋Š” ๊ฑฐ์˜€๋Š”๋ฐ, Row๋Š” ํ•„์š”ํ•œ View๋งŒ ๊ทธ๋ ค์ฃผ๊ณ  ์žˆ๋Š”๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์Šคํฌ๋กค ํ•ด์„œ ํ•˜๋‹จ์œผ๋กœ ๋‚ด๋ ค๋ณด๋ฉด ์—ญ์‹œ๋‚˜ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚˜๋Š” ๋ทฐ๋ฅผ ๊ทธ๋ฆฌ๋Š”๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š” (๊ฐ€์žฅ ์ƒ๋‹จ์— ์ฒซ ๋ฒˆ์งธ ๋กœ์šฐ๋Š” ์™œ ์žกํ˜”๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ์Œ ใ… ใ…  ์Šคํฌ๋กค์„ ๋นจ๋ฆฌํ•ด์„œ ๊ทธ๋Ÿฐ๊ฐ€)


Infinite Scroll

๋‹ค์‹œ ์ฒ˜์Œ์œผ๋กœ ๋Œ์•„์™€์„œ ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๋งŒ๋“ค์–ด ๋ณผ๊ฒŒ์š”! ๋กœ์ง์ด๋‚˜ ์ •์˜์— ๋”ฐ๋ผ ์ฝ”๋“œ๊ฐ€ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์–ด์š”!

struct InfiniteScrollView: View {
    
    @StateObject var viewModel = InfiniteScrollViewModel()
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(viewModel.products, id: \.id) { product in
                    RowView(product: product)
                        .onAppear {
                            viewModel.requestProductsWith(id: product.id)
                        }
                }
            }
            .task {
                viewModel.requestProductsWith()
            }
        }
    }
}

InfiniteScrollView๋Š” LazyVStack์œผ๋กœ viewModel์˜ products๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ฐ ํ–‰์ด appear ๋ ๋•Œ ๋งˆ๋‹ค viewModel์˜ requestProductsWith(id:) ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด์š”. ์ด๋•Œ id ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ•ด๋‹น ํ–‰์˜ ๋ฐ์ดํ„ฐ์˜ id ๊ฐ’์„ ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค. ์ด ๊ฐ’์€ ์ •์˜์— ๋”ฐ๋ผ index ๊ฐ€ ๋  ์ˆ˜๋„ ์žˆ๊ณ  page๊ฐ€ ๋ ์ˆ˜ ์žˆ๊ฒ ์ฃ ? ์ด๋•Œ ์ฃผ์˜ ํ•ด์•ผ ํ•  ๊ฒƒ์€ onAppear์€ View๊ฐ€ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚ ๋•Œ, ์‚ฌ๋ผ์กŒ๋‹ค ๋‹ค์‹œ ๋‚˜ํƒ€๋‚  ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ์ตœ์ดˆ์˜ ํ•œ๋ฒˆ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ฃผ์˜ํ•ด์•ผ ํ•ด์š”. body ํ”„๋กœํผํ‹ฐ๋Š” ์ž์ฃผ ์—…๋ฐ์ดํŠธ ๋˜๊ณ  ๊ทธ๋ ค์ง€๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๊ธฐ์— ๋กœ์ง์„ ๋„ฃ๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ViewModel์—์„œ ๋กœ์ง์„ ์ž‘์„ฑํ•ด ์ค„๊ฒŒ์š”

 

    /// paging
    func requestProductsWith(id: UUID? = nil) {
        if page == 5 { return } // ์ž„์˜๋กœ ํŽ˜์ด์ง€ ๊ฐฏ์ˆ˜ ์ œํ•œ ๋‘ 
        
        if (id == nil) || (id == products.last?.id) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                self.products.append(contentsOf: (0...20).map {
                    Product(name: "page\(self.page), \($0)item", price: (1000...30000).randomElement() ?? 0)
                })
                print("ViewModel ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต ํŽ˜์ด์ง€ : \(self.page)")
                self.page += 1
            }
        }
    }

InfiniteScrollViewModel์€ ์ด์ „ ViewModel๊ณผ ๋™์ผํ•˜๋‚˜ ํ•จ์ˆ˜ ๋ถ€๋ถ„์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. page๋Š” ์ด์ œ ๋” ์ด์ƒ ์„œ๋ฒ„๋กœ ๋ถ€ํ„ฐ ๋ถˆ๋Ÿฌ์˜ฌ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋‹ค๋Š” ์ƒํ™ฉ์„ ๋Œ€์‹ ํ•ด์„œ ์ž‘์„ฑํ•ด ์ค€ ๊ฒƒ์ธ๋ฐ, ๋ถˆ๋Ÿฌ์˜จ ๋ชฉ๋ก์ด 0๊ฐœ ์ด๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ํ”Œ๋ž˜๊ทธ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ฝ”๋“œ๊ฐ€ ๋‹ฌ๋ผ์ง€๊ฒ ์ฃ ?

id == nil์€ InfiniteScrollView๊ฐ€ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚ฌ์„ ๋•Œ ์ตœ์ดˆ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•จ์ด๊ณ  ์ด๋•Œ id ๊ฐ’์ด ์—†์œผ๋ฏ€๋กœ nil์ด๋ผ๋Š” ์ƒํ™ฉ์„ ๊ฐ€์ •ํ•ด์„œ ์ž‘์„ฑํ•ด์ค€ ์ฝ”๋“œ์—์š”. 

product์˜ id ๋ฅผ ๋ฐ›์•„์„œ ์ด id๊ฐ€ product์˜ ๋งˆ์ง€๋ง‰ id์™€ ์ผ์น˜ํ•  ๋•Œ์—๋„ ์ƒˆ๋กœ์šด ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ ViewModel์— isLoding ์ด๋ผ๋Š” Published๋ฅผ ํ•˜๋‚˜ ๋” ๋„ฃ์–ด์„œ ๋กœ๋”ฉ์„ ํ‘œ์‹œํ•˜๊ฑฐ๋‚˜ ๋กœ๋”ฉ์ด ๋Œ๊ณ  ์žˆ๋Š” ๋กœ์šฐ๋ฅผ ํ™”๋ฉด์— ๋ณด์—ฌ์ค„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ViewModel์—์„œ๋Š” ๋กœ๋”ฉ ์ค‘์— requestProductsWith๊ฐ€ ์‹คํ–‰๋˜๋ฉด ์ƒˆ๋กœ์šด ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ•˜๋„๋ก ๋ง‰์„ ์ˆ˜๋„ ์žˆ์–ด์š”


์ •๋ฆฌํ•ด ๋ณด์ž๋ฉด VStack์€ ๋ชจ๋“  row๋ฅผ ๋‹ค ๊ทธ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์— ์ ์€ ๊ฐฏ์ˆ˜๋‚˜ ๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐ, ์ˆ˜์ง์œผ๋กœ ์Œ“์ด๋Š” UI๋ฅผ ํ‘œ์‹œํ•ด ์ฃผ๋Š”๋ฐ ์ ํ•ฉ ํ•ฉ๋‹ˆ๋‹ค. 

LazyVStack์€ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚  ๋•Œ์—๋งŒ View๋ฅผ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์œ„์•„๋ž˜๋กœ ๋น ๋ฅด๊ฒŒ ์Šคํฌ๋กค ํ•œ๋‹ค๋ฉด VStack์˜ ๊ฐ ํ–‰ View๋Š” ํ•œ ๋ฒˆ์˜ appear๋งŒ ํ˜ธ์ถœ๋˜๊ณ , LazyVStack์€ ์ฒ˜์Œ ํ™”๋ฉด์— ๋ณด์˜€์„ ๋•Œ์™€ ํ™”๋ฉด์— ์‚ฌ๋ผ์กŒ๋‹ค๊ฐ€ ๋‹ค์‹œ ๋‚˜ํƒ€๋‚ฌ์„ ๋•Œ์—๋„ appear๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. 

๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๋กœ์ง์„ ์ž‘์„ฑํ•  ๋•Œ ์ด ๋‘˜์˜ ์ฐจ์ด์ ๊ณผ ํŠน์ง•์„ ์ƒ๊ฐํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์–ด์•ผ ํ•ด์š”!

 

SwiftUI ์ •๋ง ์žฌ๋ฏธ์žˆ์ง€ ์•Š๋‚˜์š”? ์„ค๋งˆ ์žˆ๊ฒ ์–ด? ํ•˜๋Š”๊ฑด ์žˆ๊ณ  ์„ค๋งˆ ์—†๊ฒ ์–ด ํ•˜๋Š”๊ฑด ์—†๋Š” ๊ทธ๋Ÿฐ ์ ๋„ SwiftUI์˜ ๋งค๋ ฅ! ๋‹ค๋“ค SwiftUI ํ•˜์„ธ์š”! ๐Ÿฎ