diff --git a/yobble/Components/TopBarView.swift b/yobble/Components/TopBarView.swift index 5bda4c5..ff87f01 100644 --- a/yobble/Components/TopBarView.swift +++ b/yobble/Components/TopBarView.swift @@ -12,9 +12,9 @@ struct TopBarView: View { // Привязка для управления боковым меню @Binding var isSideMenuPresented: Bool - + @Binding var chatSearchRevealProgress: CGFloat + @State private var searchText: String = "" - @State private var isSearchBarVisible: Bool = false var isHomeTab: Bool { return title == "Home" @@ -106,38 +106,47 @@ struct TopBarView: View { .padding() .frame(height: 50) // Стандартная высота для нав. бара - if isChatsTab && isSearchBarVisible { - searchBar - .padding(.horizontal) - .padding(.bottom, 8) - .transition(.move(edge: .top).combined(with: .opacity)) + if isChatsTab { + revealableSearchBar } Divider() } .background(Color(UIColor.systemBackground)) - .animation(.spring(response: 0.35, dampingFraction: 0.85), value: isSearchBarVisible) - .onReceive(NotificationCenter.default.publisher(for: .chatsTabRevealSearchBar)) { _ in - guard isChatsTab else { return } - withAnimation { - isSearchBarVisible = true - } - } - .onReceive(NotificationCenter.default.publisher(for: .chatsTabHideSearchBar)) { _ in - guard isChatsTab else { return } - withAnimation { - isSearchBarVisible = false - } - } .onChange(of: isChatsTab) { isChats in if !isChats { - isSearchBarVisible = false + withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) { + chatSearchRevealProgress = 0 + } } } } } private extension TopBarView { + private var normalizedRevealProgress: CGFloat { + guard isChatsTab else { return 0 } + return max(0, min(chatSearchRevealProgress, 1)) + } + + private var revealableSearchBar: some View { + let progress = normalizedRevealProgress + return VStack(spacing: 0) { + Spacer(minLength: 0) + searchBar + .padding(.horizontal) + .padding(.bottom, 8) + } + .frame(height: searchBarRevealHeight) + .clipped() + .scaleEffect(y: max(progress, 0.0001), anchor: .top) + .opacity(progress) + .allowsHitTesting(progress > 0.9) + .accessibilityHidden(progress < 0.9) + } + + private var searchBarRevealHeight: CGFloat { 56 } + var searchBar: some View { HStack(spacing: 8) { Image(systemName: "magnifyingglass") @@ -165,7 +174,26 @@ private extension TopBarView { } struct TopBarView_Previews: PreviewProvider { + struct Wrapper: View { + @State private var selectedAccount = "@user" + @State private var isSideMenuPresented = false + @State private var revealProgress: CGFloat = 1 + @StateObject private var viewModel = LoginViewModel() + + var body: some View { + TopBarView( + title: "Chats", + selectedAccount: $selectedAccount, + accounts: [selectedAccount], + viewModel: viewModel, + isSideMenuPresented: $isSideMenuPresented, + chatSearchRevealProgress: $revealProgress + ) + } + } + static var previews: some View { - /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/ + Wrapper() + .environmentObject(ThemeManager()) } } diff --git a/yobble/Views/Tab/ChatsTab.swift b/yobble/Views/Tab/ChatsTab.swift index c6214d6..a36c6fe 100644 --- a/yobble/Views/Tab/ChatsTab.swift +++ b/yobble/Views/Tab/ChatsTab.swift @@ -8,10 +8,20 @@ import SwiftUI struct ChatsTab: View { - var currentUserId: String? = nil + var currentUserId: String? + @Binding var searchRevealProgress: CGFloat @StateObject private var viewModel = PrivateChatsViewModel() @State private var selectedChatId: String? @State private var searchText: String = "" + @State private var searchDragStartProgress: CGFloat = 0 + @State private var isSearchGestureActive: Bool = false + + private let searchRevealDistance: CGFloat = 90 + + init(currentUserId: String? = nil, searchRevealProgress: Binding) { + self.currentUserId = currentUserId + self._searchRevealProgress = searchRevealProgress + } var body: some View { content @@ -154,16 +164,31 @@ struct ChatsTab: View { } private var searchBarGesture: some Gesture { - DragGesture(minimumDistance: 24) - .onEnded { value in + DragGesture(minimumDistance: 10, coordinateSpace: .local) + .onChanged { value in let verticalTranslation = value.translation.height let horizontalTranslation = value.translation.width - guard abs(verticalTranslation) > abs(horizontalTranslation) else { return } - if verticalTranslation > 24 { - NotificationCenter.default.post(name: .chatsTabRevealSearchBar, object: nil) - } else if verticalTranslation < -24 { - NotificationCenter.default.post(name: .chatsTabHideSearchBar, object: nil) + if !isSearchGestureActive { + guard abs(verticalTranslation) > abs(horizontalTranslation) else { return } + if searchRevealProgress <= 0.001 && verticalTranslation < 0 { return } + isSearchGestureActive = true + searchDragStartProgress = searchRevealProgress + } + + guard isSearchGestureActive else { return } + + let delta = verticalTranslation / searchRevealDistance + let newProgress = searchDragStartProgress + delta + searchRevealProgress = max(0, min(1, newProgress)) + } + .onEnded { _ in + guard isSearchGestureActive else { return } + isSearchGestureActive = false + + let target: CGFloat = searchRevealProgress > 0.5 ? 1 : 0 + withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) { + searchRevealProgress = target } } } @@ -568,9 +593,17 @@ private struct ChatRowView: View { } struct ChatsTab_Previews: PreviewProvider { + struct Wrapper: View { + @State private var progress: CGFloat = 1 + + var body: some View { + ChatsTab(searchRevealProgress: $progress) + .environmentObject(ThemeManager()) + } + } + static var previews: some View { - ChatsTab() - .environmentObject(ThemeManager()) + Wrapper() } } @@ -636,6 +669,4 @@ private struct ChatPlaceholderView: View { extension Notification.Name { static let debugRefreshChats = Notification.Name("debugRefreshChats") - static let chatsTabRevealSearchBar = Notification.Name("chatsTabRevealSearchBar") - static let chatsTabHideSearchBar = Notification.Name("chatsTabHideSearchBar") } diff --git a/yobble/Views/Tab/MainView.swift b/yobble/Views/Tab/MainView.swift index 6db0581..8327632 100644 --- a/yobble/Views/Tab/MainView.swift +++ b/yobble/Views/Tab/MainView.swift @@ -13,6 +13,7 @@ struct MainView: View { // Состояния для бокового меню @State private var isSideMenuPresented = false @State private var menuOffset: CGFloat = 0 + @State private var chatSearchRevealProgress: CGFloat = 0 private var tabTitle: String { switch selectedTab { @@ -38,7 +39,8 @@ struct MainView: View { selectedAccount: $selectedAccount, accounts: accounts, viewModel: viewModel, - isSideMenuPresented: $isSideMenuPresented + isSideMenuPresented: $isSideMenuPresented, + chatSearchRevealProgress: $chatSearchRevealProgress ) ZStack { @@ -48,8 +50,12 @@ struct MainView: View { FeedbackTab() .opacity(selectedTab == 1 ? 1 : 0) - ChatsTab(currentUserId: viewModel.userId.isEmpty ? nil : viewModel.userId) + ChatsTab( + currentUserId: viewModel.userId.isEmpty ? nil : viewModel.userId, + searchRevealProgress: $chatSearchRevealProgress + ) .opacity(selectedTab == 2 ? 1 : 0) + .allowsHitTesting(selectedTab == 2) ProfileTab() .opacity(selectedTab == 3 ? 1 : 0)