diff --git a/Shared/Components/RefreshableScrollView.swift b/Shared/Components/RefreshableScrollView.swift new file mode 100644 index 0000000..561ee30 --- /dev/null +++ b/Shared/Components/RefreshableScrollView.swift @@ -0,0 +1,69 @@ +import SwiftUI +import UIKit + +struct RefreshableScrollView: UIViewRepresentable { + var content: Content + var onRefresh: () -> Void + var isRefreshing: Binding + + init(isRefreshing: Binding, onRefresh: @escaping () -> Void, @ViewBuilder content: () -> Content) { + self.content = content() + self.onRefresh = onRefresh + self.isRefreshing = isRefreshing + } + + func makeUIView(context: Context) -> UIScrollView { + let scrollView = UIScrollView() + scrollView.delaysContentTouches = false + + let refreshControl = UIRefreshControl() + refreshControl.addTarget(context.coordinator, action: #selector(Coordinator.handleRefresh), for: .valueChanged) + scrollView.refreshControl = refreshControl + + let hostingController = UIHostingController(rootView: content) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(hostingController.view) + + 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 { + // Отложенное завершение, чтобы избежать цикла обновлений + DispatchQueue.main.async { + uiView.refreshControl?.endRefreshing() + } + } + + 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() { + parent.onRefresh() + } + } +} diff --git a/Shared/Components/TopBarView.swift b/Shared/Components/TopBarView.swift new file mode 100644 index 0000000..4426740 --- /dev/null +++ b/Shared/Components/TopBarView.swift @@ -0,0 +1,63 @@ +import SwiftUI + +struct TopBarView: View { + var title: String + + // Состояния для ProfileTab + @Binding var selectedAccount: String + @Binding var sheetType: ProfileTab.SheetType? + var accounts: [String] + var viewModel: LoginViewModel + + var isHomeTab: Bool { + return title == "Home" + } + + var isProfileTab: Bool { + return title == "Profile" + } + + var body: some View { + VStack(spacing: 0) { + HStack { + if isHomeTab{ + Text("Yobble") + .font(.largeTitle) + .fontWeight(.bold) + Spacer() + } else if isProfileTab { + Spacer() + Button(action: { sheetType = .accountShare }) { + HStack(spacing: 4) { + Text(selectedAccount) + .font(.headline) + .foregroundColor(.primary) + Image(systemName: "chevron.down") + .font(.subheadline) + .foregroundColor(.gray) + } + } + Spacer() + } else { + Text(title) + .font(.largeTitle) + .fontWeight(.bold) + Spacer() + } + + if isProfileTab { + NavigationLink(destination: SettingsView(viewModel: viewModel)) { + Image(systemName: "wrench") + .imageScale(.large) + .foregroundColor(.primary) + } + } + } + .padding() + .frame(height: 50) // Стандартная высота для нав. бара + + Divider() + } + .background(Color(UIColor.systemBackground)) + } +} diff --git a/Shared/Views/Tab/ChatsTab.swift b/Shared/Views/Tab/ChatsTab.swift index 804b05a..4b22288 100644 --- a/Shared/Views/Tab/ChatsTab.swift +++ b/Shared/Views/Tab/ChatsTab.swift @@ -9,16 +9,12 @@ import SwiftUI struct ChatsTab: View { var body: some View { - NavigationView { - VStack { - Text("Чаты") - .font(.largeTitle) - .bold() - .padding() - - Spacer() - } - .navigationTitle("Чаты") + VStack { + Text("Здесь будут чаты") + .font(.title) + .foregroundColor(.gray) + + Spacer() } } } diff --git a/Shared/Views/Tab/HomeTab.swift b/Shared/Views/Tab/HomeTab.swift index a027c92..5d3645d 100644 --- a/Shared/Views/Tab/HomeTab.swift +++ b/Shared/Views/Tab/HomeTab.swift @@ -6,33 +6,26 @@ struct HomeTab: View { @State private var isRefreshing = false var body: some View { - NavigationView { - VStack { - if isLoading { - ProgressView("Загрузка ленты...") - } else { - RefreshableScrollView(isRefreshing: $isRefreshing, onRefresh: { - fetchData() - }) { - LazyVStack(spacing: 24) { - ForEach(posts) { post in - PostDetailView(post: post) - } + VStack { + if isLoading { + ProgressView("Загрузка ленты...") + } else { + RefreshableScrollView(isRefreshing: $isRefreshing, onRefresh: { + fetchData() + }) { + LazyVStack(spacing: 24) { + ForEach(posts) { post in + PostDetailView(post: post) } } } } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) {} - } - .onAppear { - if posts.isEmpty { - fetchData(isInitialLoad: true) - } + } + .onAppear { + if posts.isEmpty { + fetchData(isInitialLoad: true) } } - .navigationViewStyle(StackNavigationViewStyle()) } private func fetchData(isInitialLoad: Bool = false) { diff --git a/Shared/Views/Tab/MainView.swift b/Shared/Views/Tab/MainView.swift index 19263ab..81da0d3 100644 --- a/Shared/Views/Tab/MainView.swift +++ b/Shared/Views/Tab/MainView.swift @@ -10,31 +10,83 @@ import SwiftUI struct MainView: View { @ObservedObject var viewModel: LoginViewModel @State private var selectedTab: Int = 0 + + // Состояния для TopBarView, когда активна вкладка Profile + @State private var selectedAccount = "@user1" + @State private var accounts = ["@user1", "@user2", "@user3"] + @State private var sheetType: ProfileTab.SheetType? = nil + + private var tabTitle: String { + switch selectedTab { + case 0: + return "Home" + case 1: + return "Search" + case 2: + return "Chats" + case 3: + return "Profile" + default: + return "Home" + } + } var body: some View { - VStack(spacing: 0) { - ZStack { - switch selectedTab { - case 0: - HomeTab() - case 1: - SearchTab() - case 2: - ChatsTab() - case 3: - ProfileTab(viewModel: viewModel) - default: - HomeTab() + NavigationView { // NavigationView нужен здесь для NavigationLink в TopBarView + VStack(spacing: 0) { + TopBarView( + title: tabTitle, + selectedAccount: $selectedAccount, + sheetType: $sheetType, + accounts: accounts, + viewModel: viewModel + ) + + ZStack { + switch selectedTab { + case 0: + HomeTab() + case 1: + SearchTab() + case 2: + ChatsTab() + case 3: + // Передаем состояния в ProfileTab + ProfileTab( + viewModel: viewModel, + sheetType: $sheetType, + selectedAccount: $selectedAccount, + accounts: $accounts + ) + default: + HomeTab() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + CustomTabBar(selectedTab: $selectedTab) { + // Действие для кнопки "Создать" + print("Create button tapped") } } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - CustomTabBar(selectedTab: $selectedTab) { - // Действие для кнопки "Создать" - print("Create button tapped") + .ignoresSafeArea(edges: .bottom) + .navigationBarHidden(true) // Скрываем стандартный NavigationBar + .sheet(item: $sheetType) { type in + // Обработка sheet, перенесенная из ProfileTab + switch type { + case .accountShare: + AccountShareSheet( + isPresented: Binding( + get: { sheetType != nil }, + set: { if !$0 { sheetType = nil } } + ), + selectedAccount: $selectedAccount, + accounts: accounts + ) + } } } - .ignoresSafeArea(edges: .bottom) + .navigationViewStyle(StackNavigationViewStyle()) // Важно для корректной работы } } diff --git a/Shared/Views/Tab/Profile/ProfileContentTabbedGrid.swift b/Shared/Views/Tab/Profile/ProfileContentTabbedGrid.swift index 00c8743..8b255de 100644 --- a/Shared/Views/Tab/Profile/ProfileContentTabbedGrid.swift +++ b/Shared/Views/Tab/Profile/ProfileContentTabbedGrid.swift @@ -43,15 +43,6 @@ struct ProfileContentTabbedGrid: View { .cornerRadius(8) .font(.subheadline) - Button { - // Создать пост - } label: { - Label("Создать", systemImage: "plus") - .font(.subheadline) - .padding(8) - .background(Color.blue.opacity(0.2)) - .cornerRadius(8) - } } else { TextField("Поиск", text: $searchQuery) .padding(.horizontal, 10) diff --git a/Shared/Views/Tab/ProfileTab.swift b/Shared/Views/Tab/ProfileTab.swift index 0806505..3af6100 100644 --- a/Shared/Views/Tab/ProfileTab.swift +++ b/Shared/Views/Tab/ProfileTab.swift @@ -3,14 +3,15 @@ import SwiftUI struct ProfileTab: View { @ObservedObject var viewModel: LoginViewModel @State private var isContentLoaded = true - - @State private var accounts = ["@user1", "@user2", "@user3"] - @State private var selectedAccount = "@user1" + // Привязки к состояниям в MainView + @Binding var sheetType: SheetType? + @Binding var selectedAccount: String + @Binding var accounts: [String] + let followers = ["@alice", "@bob", "@charlie"] let following = ["@dev", "@design", "@ios"] - @State private var sheetType: SheetType? = nil enum SheetType: Identifiable { case accountShare var id: Int { self.hashValue } @@ -26,7 +27,7 @@ struct ProfileTab: View { @State private var isShowingFollowing = false var body: some View { - NavigationView { + VStack { if !isContentLoaded { SplashScreenView() } else { @@ -51,7 +52,6 @@ struct ProfileTab: View { }) { VStack(spacing: 12) { header -// .frame(minHeight: 300) .frame(maxWidth: .infinity) .background(Color(.systemBackground)) @@ -66,43 +66,8 @@ struct ProfileTab: View { } } } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Button(action: { sheetType = .accountShare }) { - HStack(spacing: 4) { - Text(selectedAccount) - .font(.headline) - .foregroundColor(.primary) - Image(systemName: "chevron.down") - .font(.subheadline) - .foregroundColor(.gray) - } - } - } - ToolbarItem(placement: .navigationBarTrailing) { - NavigationLink(destination: SettingsView(viewModel: viewModel)) { - Image(systemName: "wrench") - .imageScale(.large) - } - } - } - .sheet(item: $sheetType) { type in - switch type { - case .accountShare: - AccountShareSheet( - isPresented: Binding( - get: { sheetType != nil }, - set: { if !$0 { sheetType = nil } } - ), - selectedAccount: $selectedAccount, - accounts: accounts - ) - } - } } } - .navigationViewStyle(StackNavigationViewStyle()) .onAppear { if allPosts.isEmpty { fetchData(isInitialLoad: true) diff --git a/Shared/Views/Tab/SearchTab.swift b/Shared/Views/Tab/SearchTab.swift index 4aa30bb..609b506 100644 --- a/Shared/Views/Tab/SearchTab.swift +++ b/Shared/Views/Tab/SearchTab.swift @@ -4,25 +4,27 @@ struct SearchTab: View { @StateObject private var viewModel = SearchViewModel() var body: some View { - NavigationView { - VStack { - SearchBar(text: $viewModel.searchText) - .padding(.top, 8) - - List(viewModel.users) { user in - NavigationLink(destination: ProfileTab(viewModel: LoginViewModel())) { // Placeholder destination - HStack { - Image(systemName: "person.crop.circle") - .resizable() - .frame(width: 40, height: 40) - .clipShape(Circle()) - Text(user.username) - } + VStack { + SearchBar(text: $viewModel.searchText) + .padding(.top, 8) + + List(viewModel.users) { user in + NavigationLink(destination: ProfileTab( + viewModel: LoginViewModel(), + sheetType: .constant(nil), + selectedAccount: .constant("@\(user.username)"), + accounts: .constant(["@\(user.username)"])) + ) { + HStack { + Image(systemName: "person.crop.circle") + .resizable() + .frame(width: 40, height: 40) + .clipShape(Circle()) + Text(user.username) } } - .listStyle(PlainListStyle()) } - .navigationTitle("Поиск") + .listStyle(PlainListStyle()) } } } diff --git a/yobble.xcodeproj/project.pbxproj b/yobble.xcodeproj/project.pbxproj index 6dbee37..c000660 100644 --- a/yobble.xcodeproj/project.pbxproj +++ b/yobble.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 1AB4F8CD2E22E341002B6E40 /* AccountShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4F8CC2E22E341002B6E40 /* AccountShareSheet.swift */; }; 1AB4F8F32E22EC9F002B6E40 /* FollowersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4F8F22E22EC9F002B6E40 /* FollowersView.swift */; }; 1AB4F8F72E22ECAC002B6E40 /* FollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4F8F62E22ECAC002B6E40 /* FollowingView.swift */; }; + 1AB7F5162E32EC1C003756F3 /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB7F5142E32EC1C003756F3 /* RefreshableScrollView.swift */; }; + 1AB7F5172E32EC1C003756F3 /* TopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB7F5152E32EC1C003756F3 /* TopBarView.swift */; }; 1ACE60F82E22F3DC00B37AC5 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ACE60F72E22F3DC00B37AC5 /* SettingsView.swift */; }; 1ACE61012E22F55C00B37AC5 /* EditProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ACE61002E22F55C00B37AC5 /* EditProfileView.swift */; }; 1ACE61052E22F56800B37AC5 /* SecuritySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ACE61042E22F56800B37AC5 /* SecuritySettingsView.swift */; }; @@ -40,7 +42,6 @@ 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 */; }; @@ -78,6 +79,8 @@ 1AB4F8CC2E22E341002B6E40 /* AccountShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountShareSheet.swift; sourceTree = ""; }; 1AB4F8F22E22EC9F002B6E40 /* FollowersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersView.swift; sourceTree = ""; }; 1AB4F8F62E22ECAC002B6E40 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = ""; }; + 1AB7F5142E32EC1C003756F3 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; + 1AB7F5152E32EC1C003756F3 /* TopBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopBarView.swift; sourceTree = ""; }; 1ACE60F72E22F3DC00B37AC5 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 1ACE61002E22F55C00B37AC5 /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = ""; }; 1ACE61042E22F56800B37AC5 /* SecuritySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecuritySettingsView.swift; sourceTree = ""; }; @@ -86,7 +89,6 @@ 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 = ""; }; @@ -125,7 +127,7 @@ 1A7940792DF77BC2002569DA /* Shared */ = { isa = PBXGroup; children = ( - 1AD757D02E27640F0069C1FD /* RefreshableScrollView.swift */, + 1AB7F5132E32EBF1003756F3 /* Components */, 1A7940FA2DF7B898002569DA /* Resources */, 1A7940E52DF7B341002569DA /* Services */, 1A7940A02DF77DCD002569DA /* Network */, @@ -222,6 +224,15 @@ path = Resources; sourceTree = ""; }; + 1AB7F5132E32EBF1003756F3 /* Components */ = { + isa = PBXGroup; + children = ( + 1AB7F5142E32EC1C003756F3 /* RefreshableScrollView.swift */, + 1AB7F5152E32EC1C003756F3 /* TopBarView.swift */, + ); + path = Components; + sourceTree = ""; + }; 1ACE60FF2E22F54700B37AC5 /* Settings */ = { isa = PBXGroup; children = ( @@ -389,6 +400,7 @@ 1A7940B02DF77E26002569DA /* LoginView.swift in Sources */, 1A79410C2DF7C81D002569DA /* RegistrationView.swift in Sources */, 1A0276112DF9247000D8BC53 /* CustomTextField.swift in Sources */, + 1AB7F5172E32EC1C003756F3 /* TopBarView.swift in Sources */, 1A7940CE2DF7A9AA002569DA /* ProfileTab.swift in Sources */, 1ACE61212E22FFD000B37AC5 /* ProfileContentTabbedGrid.swift in Sources */, 1AB4F8F72E22ECAC002B6E40 /* FollowingView.swift in Sources */, @@ -398,6 +410,7 @@ 1A6FB9552E32D2B200E89EBE /* CustomTabBar.swift in Sources */, 1A0276032DF909F900D8BC53 /* refreshtokenex.swift in Sources */, 1A7940B62DF77F21002569DA /* MainView.swift in Sources */, + 1AB7F5162E32EC1C003756F3 /* RefreshableScrollView.swift in Sources */, 1A7940E22DF7B1C5002569DA /* KeychainService.swift in Sources */, 1A7940A62DF77DF5002569DA /* User.swift in Sources */, 1A7940A22DF77DE9002569DA /* AuthService.swift in Sources */, @@ -413,7 +426,6 @@ 1A7940AA2DF77E05002569DA /* LoginViewModel.swift in Sources */, 1AB4F8F32E22EC9F002B6E40 /* FollowersView.swift in Sources */, 1A79408D2DF77BC3002569DA /* yobbleApp.swift in Sources */, - 1AD757D12E27640F0069C1FD /* RefreshableScrollView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };