Compare commits

..

4 Commits

Author SHA1 Message Date
359b516272 add fix to tab bar search 2025-10-07 04:24:38 +03:00
24b718d515 fix top bar 2025-10-07 04:12:21 +03:00
e51a4ed6b2 add animation 2025-10-07 04:09:53 +03:00
3d24e0afce add swipe 2025-10-07 04:03:07 +03:00
3 changed files with 116 additions and 10 deletions

View File

@ -12,6 +12,7 @@ struct TopBarView: View {
// Привязка для управления боковым меню
@Binding var isSideMenuPresented: Bool
@Binding var chatSearchRevealProgress: CGFloat
@State private var searchText: String = ""
@ -106,18 +107,49 @@ struct TopBarView: View {
.frame(height: 50) // Стандартная высота для нав. бара
if isChatsTab {
searchBar
.padding(.horizontal)
.padding(.bottom, 8)
revealableSearchBar
}
Divider()
}
.background(Color(UIColor.systemBackground))
.onChange(of: isChatsTab) { isChats in
if !isChats {
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) {
searchBar
.padding(.horizontal)
.padding(.bottom, searchBarBottomSpacing)
.opacity(progress)
}
.frame(height: searchBarRevealHeight * progress, alignment: .top)
.clipped()
.allowsHitTesting(progress > 0.9)
.accessibilityHidden(progress < 0.9)
}
private var searchBarRevealHeight: CGFloat { searchBarBaseHeight + searchBarBottomSpacing }
// 36 min height + 6 * 2 vertical padding inside searchBar
private var searchBarBaseHeight: CGFloat { 48 }
private var searchBarBottomSpacing: CGFloat { 0 }
var searchBar: some View {
HStack(spacing: 8) {
Image(systemName: "magnifyingglass")
@ -145,7 +177,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())
}
}

View File

@ -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<CGFloat>) {
self.currentUserId = currentUserId
self._searchRevealProgress = searchRevealProgress
}
var body: some View {
content
@ -115,6 +125,7 @@ struct ChatsTab: View {
}
}
.listStyle(.plain)
.simultaneousGesture(searchBarGesture)
// .safeAreaInset(edge: .top) {
// VStack(spacing: 0) {
// searchBar
@ -152,6 +163,36 @@ struct ChatsTab: View {
)
}
private var searchBarGesture: some Gesture {
DragGesture(minimumDistance: 10, coordinateSpace: .local)
.onChanged { value in
let verticalTranslation = value.translation.height
let horizontalTranslation = value.translation.width
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
}
}
}
private var isSearching: Bool {
!searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
@ -552,10 +593,18 @@ private struct ChatRowView: View {
}
struct ChatsTab_Previews: PreviewProvider {
static var previews: some View {
ChatsTab()
struct Wrapper: View {
@State private var progress: CGFloat = 1
var body: some View {
ChatsTab(searchRevealProgress: $progress)
.environmentObject(ThemeManager())
}
}
static var previews: some View {
Wrapper()
}
}
private struct ChatPlaceholderView: View {

View File

@ -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)