diff --git a/Shared/Components/RefreshableScrollView.swift b/Shared/Components/RefreshableScrollView.swift new file mode 100644 index 0000000..b6c3c4b --- /dev/null +++ b/Shared/Components/RefreshableScrollView.swift @@ -0,0 +1,71 @@ +import SwiftUI +import UIKit + +struct RefreshableScrollView: UIViewRepresentable { + var content: Content + var onRefresh: (UIRefreshControl) -> Void + var isRefreshing: Binding + + init(isRefreshing: Binding, onRefresh: @escaping (UIRefreshControl) -> Void, @ViewBuilder content: () -> Content) { + self.content = content() + self.onRefresh = onRefresh + self.isRefreshing = isRefreshing + } + + func makeUIView(context: Context) -> UIScrollView { + let scrollView = UIScrollView() + + // Создаем UIRefreshControl и добавляем его + let refreshControl = UIRefreshControl() + refreshControl.addTarget(context.coordinator, action: #selector(Coordinator.handleRefresh), for: .valueChanged) + scrollView.refreshControl = refreshControl + + // Создаем хостинг для нашего SwiftUI контента + let hostingController = UIHostingController(rootView: content) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(hostingController.view) + + // Настраиваем Auto Layout + NSLayoutConstraint.activate([ + hostingController.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + hostingController.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + hostingController.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) + ]) + + context.coordinator.hostingController = hostingController + + return scrollView + } + + func updateUIView(_ uiView: UIScrollView, context: Context) { + // Обновляем состояние индикатора + if isRefreshing.wrappedValue { + uiView.refreshControl?.beginRefreshing() + } else { + uiView.refreshControl?.endRefreshing() + } + + // Обновляем SwiftUI View, если нужно + context.coordinator.hostingController?.rootView = content + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + var parent: RefreshableScrollView + var hostingController: UIHostingController? + + init(_ parent: RefreshableScrollView) { + self.parent = parent + } + + @objc func handleRefresh(_ sender: UIRefreshControl) { + parent.isRefreshing.wrappedValue = true + parent.onRefresh(sender) + } + } +} diff --git a/Shared/Views/Tab/HomeTab.swift b/Shared/Views/Tab/HomeTab.swift index 320a61e..aa5215d 100644 --- a/Shared/Views/Tab/HomeTab.swift +++ b/Shared/Views/Tab/HomeTab.swift @@ -3,6 +3,7 @@ import SwiftUI struct HomeTab: View { @State private var posts: [Post] = [] @State private var isLoading = true + @State private var isRefreshing = false var body: some View { NavigationView { @@ -10,7 +11,9 @@ struct HomeTab: View { if isLoading { ProgressView("Загрузка ленты...") } else { - ScrollView { + RefreshableScrollView(isRefreshing: $isRefreshing, onRefresh: { _ in + fetchData() + }) { LazyVStack(spacing: 24) { ForEach(posts) { post in PostDetailView(post: post) @@ -22,12 +25,25 @@ struct HomeTab: View { .navigationTitle("Лента") .onAppear { if posts.isEmpty { - PostService.shared.fetchAllPosts { fetchedPosts in - self.posts = fetchedPosts - self.isLoading = false - } + fetchData(isInitialLoad: true) } } } } + + private func fetchData(isInitialLoad: Bool = false) { + if isInitialLoad { + isLoading = true + } + + PostService.shared.fetchAllPosts { fetchedPosts in + self.posts = fetchedPosts + + if isInitialLoad { + print("content updated") + self.isLoading = false + } + self.isRefreshing = false + } + } } diff --git a/yobble.xcodeproj/project.pbxproj b/yobble.xcodeproj/project.pbxproj index 9a28ca7..5e74271 100644 --- a/yobble.xcodeproj/project.pbxproj +++ b/yobble.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 1ACE61192E22FF1400B37AC5 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ACE61182E22FF1400B37AC5 /* Post.swift */; }; 1ACE61212E22FFD000B37AC5 /* ProfileContentTabbedGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ACE61202E22FFD000B37AC5 /* ProfileContentTabbedGrid.swift */; }; 1AD757CD2E27608C0069C1FD /* PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AD757CC2E27608C0069C1FD /* PostFeedView.swift */; }; + 1AD757D12E27640F0069C1FD /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AD757D02E27640F0069C1FD /* RefreshableScrollView.swift */; }; 1AE587052E23264800254F06 /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE587042E23264800254F06 /* PostService.swift */; }; 1AE587252E23337000254F06 /* ProfileContentGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE587242E23337000254F06 /* ProfileContentGrid.swift */; }; 1AEE5EAB2E21A83200A3DCA3 /* HomeTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AEE5EAA2E21A83200A3DCA3 /* HomeTab.swift */; }; @@ -81,6 +82,7 @@ 1ACE61182E22FF1400B37AC5 /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; 1ACE61202E22FFD000B37AC5 /* ProfileContentTabbedGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileContentTabbedGrid.swift; sourceTree = ""; }; 1AD757CC2E27608C0069C1FD /* PostFeedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostFeedView.swift; sourceTree = ""; }; + 1AD757D02E27640F0069C1FD /* RefreshableScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RefreshableScrollView.swift; path = Components/RefreshableScrollView.swift; sourceTree = ""; }; 1AE587042E23264800254F06 /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = ""; }; 1AE587242E23337000254F06 /* ProfileContentGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileContentGrid.swift; sourceTree = ""; }; 1AEE5EAA2E21A83200A3DCA3 /* HomeTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTab.swift; sourceTree = ""; }; @@ -119,6 +121,7 @@ 1A7940792DF77BC2002569DA /* Shared */ = { isa = PBXGroup; children = ( + 1AD757D02E27640F0069C1FD /* RefreshableScrollView.swift */, 1A7940FA2DF7B898002569DA /* Resources */, 1A7940E52DF7B341002569DA /* Services */, 1A7940A02DF77DCD002569DA /* Network */, @@ -402,6 +405,7 @@ 1A7940AA2DF77E05002569DA /* LoginViewModel.swift in Sources */, 1AB4F8F32E22EC9F002B6E40 /* FollowersView.swift in Sources */, 1A79408D2DF77BC3002569DA /* yobbleApp.swift in Sources */, + 1AD757D12E27640F0069C1FD /* RefreshableScrollView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };