{"id":3999,"date":"2026-01-12T19:00:00","date_gmt":"2026-01-12T10:00:00","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=3999"},"modified":"2026-01-12T22:18:19","modified_gmt":"2026-01-12T13:18:19","slug":"swift-horizontalinfinite-scroll-component","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/01\/12\/swift-horizontalinfinite-scroll-component\/","title":{"rendered":"[swift]\uac00\ub85c\ubb34\ud55c\uc2a4\ud06c\ub864\ucef4\ud3ec\ub10c\ud2b8\/Horizontal Infinite Scroll Component"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb \uac00\ub85c \ubb34\ud55c \uc2a4\ud06c\ub864\uae30\ub2a5\uc744 \ucef4\ud3ec\ub10c\ud2b8\ub85c \uad6c\ud604\ud55c \uc608\uc81c\uc785\ub2c8\ub2e4.<br>This is an example of implementing a horizontal infinite scroll function as a component.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uac01 \uc139\uc158\ubcc4\ub85c \uac00\ub85c\ubb34\ud55c\uc2a4\ud06c\ub864\uc744 \uad6c\ud604 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. <br>You can implement infinite horizontal scrolling for each section.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb API\ub294 \ub79c\ub364\uc720\uc800 API\ub97c \uc0ac\uc6a9\ud574\uc11c \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.<br>The API was implemented using the random user API.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb\ucf54\ub4dc  \/ Code<\/p>\n\n\n\n<p>\u2714\ufe0f ContentView<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import SwiftUI\n\nstruct ContentView: View {\n    var body: some View {\n        NavigationStack {\n            ScrollView {\n                VStack(spacing: 30) {\n                    \n                    \/\/ \uc139\uc158 1: \uc778\uae30 \uc0ac\uc6a9\uc790\n                    \/\/ Section 1: Popular Users\n                    HorizontalUserScrollView(\n                        title: \"\uc778\uae30 \uc0ac\uc6a9\uc790\",\n                        url:\"https:\/\/randomuser.me\/api\/?results=20\",\n                        viewModel: RandomUserViewModel()\n                    )\n                    \n                    \/\/ \uc139\uc158 2: \uc0c8\ub85c\uc6b4 \uc0ac\uc6a9\uc790\n                    \/\/ Section 2: New Users\n                    HorizontalUserScrollView(\n                        title: \"\uc0c8\ub85c\uc6b4 \uc0ac\uc6a9\uc790\",\n                        url:\"https:\/\/randomuser.me\/api\/?results=20\",\n                        viewModel: RandomUserViewModel()\n                    )\n                    \n                    \/\/ \uc139\uc158 3: \ucd94\ucc9c \uc0ac\uc6a9\uc790\n                    \/\/ Section 3: Recommended Users\n                    HorizontalUserScrollView(\n                        title: \"\uc624\ub298\uc758 \ucd94\ucc9c\",\n                        url:\"https:\/\/randomuser.me\/api\/?results=20\",\n                        viewModel: RandomUserViewModel()\n                    )\n\n                }\n                .padding(.vertical)\n            }\n            .navigationTitle(\"Horizontal Infinite Scroll\")\n            .refreshable {\n                \/\/ \uc804\uccb4 \uc0c8\ub85c\uace0\uce68 \uc6d0\ud558\uba74 \uc5ec\uae30\uc11c \ubaa8\ub4e0 ViewModel \ub9ac\ud504\ub808\uc2dc\n                \/\/ \ud604\uc7ac\ub294 \uac01 \uc139\uc158\uc774 \ub3c5\ub9bd\uc801\uc774\ubbc0\ub85c pull-to-refresh\ub294 \uc0dd\ub7b5\ud558\uac70\ub098 \ucee4\uc2a4\ud140 \ucc98\ub9ac\n                \n                \/\/ If you want a full refresh, refresh all ViewModels here.\n                \/\/ Currently, each section is independent, so either omit pull-to-refresh or handle it customally.\n            }\n        }\n    }\n}\n\n#Preview {\n    ContentView()\n}\n<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0fHorizontalUserScrollView<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import SwiftUI\n\n\/\/ \uac00\ub85c \ubb34\ud55c \uc2a4\ud06c\ub864 \ucef4\ud3ec\ub10c\ud2b8\n\/\/ Horizontal infinite scroll component\nstruct HorizontalUserScrollView: View {\n    let title: String\n    let url: String\n    @StateObject var viewModel: RandomUserViewModel\n    \n    var body: some View {\n        VStack(alignment: .leading) {\n            \/\/ \uc139\uc158 \uc81c\ubaa9 \/ section title\n            Text(title)\n                .font(.title2)\n                .fontWeight(.bold)\n                .padding(.horizontal)\n            \n            ScrollView(.horizontal, showsIndicators: false) {\n                LazyHStack(spacing: 20) {\n                    ForEach(viewModel.users) { user in\n                        VStack {\n                            Text(\"\\(user.name.first) \\(user.name.last)\")\n                                .font(.headline)\n                                .multilineTextAlignment(.center)\n                            \n                            AsyncImage(url: URL(string: user.picture.large)) { phase in\n                                switch phase {\n                                case .success(let image):\n                                    image\n                                        .resizable()\n                                        .scaledToFit()\n                                        .frame(width: 120, height: 120)\n                                        .clipShape(Circle())\n                                case .empty, .failure:\n                                    ProgressView()\n                                        .frame(width: 120, height: 120)\n                                @unknown default:\n                                    EmptyView()\n                                }\n                            }\n                        }\n                        .frame(width: 160)\n                        .padding()\n                        .background(Color(.secondarySystemBackground))\n                        .cornerRadius(16)\n                        .shadow(radius: 3)\n                        \/\/ \ub9c8\uc9c0\ub9c9 \uc0ac\uc6a9\uc790 \uce74\ub4dc\uac00 \ub098\ud0c0\ub0a0 \ub54c \ucd94\uac00 \ub85c\ub4dc\n                        \/\/ Load additional cards when the last user card appears\n                        .onAppear {\n                            if user == viewModel.users.last {\n                                viewModel.fetchMoreUsers(url:url)  \/\/ \ucd94\uac00 \ub85c\ub4dc (append)\n                                \/\/ viewModel.fetchMoreUsers(url: \"https:\/\/randomuser.me\/api\/?results=20\")  \/\/ \ucd94\uac00 \ub85c\ub4dc (append)\n                            }\n                        }\n                    }\n                    \n                    \/\/ \ub85c\ub529 \uc778\ub514\ucf00\uc774\ud130 \/ loading indicator\n                    if viewModel.isLoadingMore {\n                        ProgressView(\"\ubd88\ub7ec\uc624\ub294 \uc911\u2026\")\n                            .frame(width: 160, height: 240)\n                            .padding()\n                    }\n                }\n                .padding(.horizontal)\n            }\n        }\n        \/\/ \uc139\uc158\uc774 \ud654\uba74\uc5d0 \ub098\ud0c0\ub0a0 \ub54c \ucd08\uae30 \ub370\uc774\ud130 \ub85c\ub4dc (\ud55c \ubc88\ub9cc!)\n        \/\/ Load initial data when the section appears on screen (only once!)\n        .onAppear {\n            if viewModel.users.isEmpty {\n                Task {\n                    \/\/ \uc804\uccb4 \uad50\uccb4 (\ucd08\uae30 \ub85c\ub4dc \uc804\uc6a9)\n                    \/\/ Full replacement (initial load only)\n                    await viewModel.loadInitialUsers(url:url)\n                    \/\/ await viewModel.loadInitialUsers(url:\"https:\/\/randomuser.me\/api\/?results=20\")  \/\/ \uc804\uccb4 \uad50\uccb4 (\ucd08\uae30 \ub85c\ub4dc \uc804\uc6a9)\n                }\n            }\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f RandomUserModel<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\nimport Foundation\n\nstruct RandomUserResponse: Codable {\n    let results: &#91;RandomUser]\n}\n\nstruct RandomUser: Codable, Identifiable, Equatable {\n\n    let login: Login\n    \n    let name: Name\n    let picture: Picture\n    \n    \/\/ Identifiable\n    var id: String { login.uuid }\n    \n    \/\/ MARK: - \ub0b4\ubd80 \uad6c\uc870\uccb4\n    struct Login: Codable, Equatable {\n        let uuid: String\n        \/\/ \ud544\uc694\ud558\uba74 username, password \ub4f1 \ucd94\uac00 \uac00\ub2a5\n        \/\/ You can add username, password, etc. if needed.\n    }\n    \n    struct Name: Codable, Equatable {\n        let title: String?   \/\/ \"Mr\", \"Mrs\" ...\n        let first: String\n        let last: String\n    }\n    \n    struct Picture: Codable, Equatable {\n        let large: String\n        let medium: String?\n        let thumbnail: String?\n    }\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f RandomUserRowInfiniteApp<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import SwiftUI\n\n@main\nstruct RandomUserRowInfiniteApp: App {\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f RandomUserViewModel<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import Foundation\nimport Combine\n\n@MainActor\nclass RandomUserViewModel: ObservableObject {\n    @Published var users: &#91;RandomUser] = &#91;]\n    @Published var errorMessage: String?\n    @Published var isLoadingMore = false\n    \n    private var debounceTask: Task&lt;Void, Never&gt;?\n    \n    \/\/ \ucc98\uc74c \ub370\uc774\ud130 \ub85c\ub4dc\uc6a9 (\ubdf0\uac00 \ub098\ud0c0\ub0a0 \ub54c \ud638\ucd9c)\n    \/\/ For initial data load (called when the view appears)\n    func loadInitialUsers(url: String = \"https:\/\/randomuser.me\/api\/?results=20\") async {\n        guard users.isEmpty else { return } \/\/ \uc774\ubbf8 \ub370\uc774\ud130 \uc788\uc73c\uba74 \uc911\ubcf5 \ub85c\ub4dc \ubc29\uc9c0\n        \n        isLoadingMore = true\n        await fetchUsers(from: url, append: false)\n        isLoadingMore = false\n    }\n    \n    \/\/ \uc591\ucabd \ub05d\uc5d0\uc11c \ucd94\uac00 \ub85c\ub4dc (\uc88c\uc6b0 \ubb34\ud55c \uc2a4\ud06c\ub864\uc6a9)\n    \/\/ Additional loads from both ends (for infinite left and right scrolling)\n    func fetchMoreUsers(url: String = \"https:\/\/randomuser.me\/api\/?results=10\") {\n        debounceTask?.cancel()\n        \n        debounceTask = Task {\n            try? await Task.sleep(nanoseconds: 300_000_000) \/\/ 0.3\ucd08 \ub514\ubc14\uc6b4\uc2a4 \/0.3 second debounce\n            \n            guard !isLoadingMore else { return }\n            isLoadingMore = true\n            \n            await fetchUsers(from: url, append: true)\n            \n            isLoadingMore = false\n        }\n    }\n    \n    \/\/ \uacf5\ud1b5 \ub124\ud2b8\uc6cc\ud06c \uba54\uc11c\ub4dc (\uc774\uc804\uacfc \ub3d9\uc77c) \/\n    \/\/ Common network methods (same as before)\n    private func fetchUsers(from urlString: String, append: Bool) async {\n        guard let url = URL(string: urlString) else {\n            self.errorMessage = \"\uc798\ubabb\ub41c URL\uc785\ub2c8\ub2e4.\"\n            return\n        }\n        \n        do {\n            let (data, _) = try await URLSession.shared.data(from: url)\n            let decoded = try JSONDecoder().decode(RandomUserResponse.self, from: data)\n            \n            if append {\n                self.users.append(contentsOf: decoded.results)\n            } else {\n                self.users = decoded.results\n            }\n        } catch {\n            self.errorMessage = error.localizedDescription\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uc2a4\ud06c\ub9b0 \uc0f7 \/ ScreenShot<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"491\" height=\"1024\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/\uc2a4\ud06c\ub9b0\uc0f7-2026-01-12-\uc624\ud6c4-1.19.00-491x1024.png\" alt=\"\" class=\"wp-image-4008\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/\uc2a4\ud06c\ub9b0\uc0f7-2026-01-12-\uc624\ud6c4-1.19.00-491x1024.png 491w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/\uc2a4\ud06c\ub9b0\uc0f7-2026-01-12-\uc624\ud6c4-1.19.00-144x300.png 144w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/\uc2a4\ud06c\ub9b0\uc0f7-2026-01-12-\uc624\ud6c4-1.19.00-400x835.png 400w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/\uc2a4\ud06c\ub9b0\uc0f7-2026-01-12-\uc624\ud6c4-1.19.00.png 548w\" sizes=\"auto, (max-width: 491px) 100vw, 491px\" \/><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb \uac00\ub85c \ubb34\ud55c \uc2a4\ud06c\ub864\uae30\ub2a5\uc744 \ucef4\ud3ec\ub10c\ud2b8\ub85c \uad6c\ud604\ud55c \uc608\uc81c\uc785\ub2c8\ub2e4.This is an example of implementing a horizontal infinite scroll function as a component. \ud83d\udc49\ud83c\udffb \uac01 \uc139\uc158\ubcc4\ub85c \uac00\ub85c\ubb34\ud55c\uc2a4\ud06c\ub864\uc744 \uad6c\ud604 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. You can implement infinite horizontal scrolling for each section. \ud83d\udc49\ud83c\udffb API\ub294 \ub79c\ub364\uc720\uc800 API\ub97c \uc0ac\uc6a9\ud574\uc11c \uad6c\ud604\ud588\uc2b5\ub2c8\ub2e4.The API was implemented using the random user API. \ud83d\udc49\ud83c\udffb\ucf54\ub4dc \/ Code \u2714\ufe0f [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[19,1],"tags":[],"class_list":["post-3999","post","type-post","status-publish","format-standard","hentry","category-swift","category-uncategorized","missing-thumbnail"],"_links":{"self":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/3999","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/comments?post=3999"}],"version-history":[{"count":8,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/3999\/revisions"}],"predecessor-version":[{"id":4010,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/3999\/revisions\/4010"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=3999"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=3999"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=3999"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}