From 624f36d686be368ccd16db19bef54912bfe7c1f4 Mon Sep 17 00:00:00 2001 From: Levitating Pineapple Date: Tue, 6 Aug 2024 20:29:13 +0300 Subject: [PATCH] show hide search field by observing scroll position --- .../Shared/Views/ChatList/ChatListView.swift | 142 ++++++++++++------ apps/ios/SimpleX.xcodeproj/project.pbxproj | 17 +++ .../xcshareddata/swiftpm/Package.resolved | 11 +- 3 files changed, 122 insertions(+), 48 deletions(-) diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 7aade1e157..588f78a3b2 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -8,6 +8,9 @@ import SwiftUI import SimpleXChat +import SwiftUIIntrospect + +private weak var collectionView: UICollectionView? struct ChatListView: View { @EnvironmentObject var chatModel: ChatModel @@ -21,6 +24,9 @@ struct ChatListView: View { @State private var userPickerVisible = false @State private var showConnectDesktop = false + @State private var isSearchExpanded = true + @State private var contentOffsetObservation: NSKeyValueObservation? + @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false @@ -86,15 +92,47 @@ struct ChatListView: View { )) } .safeAreaInset(edge: .top) { - if oneHandUI { Divider().background(Material.ultraThin) } + if oneHandUI { + Divider().background(Material.thin) + } else { + searchBar + } } + .safeAreaInset(edge: .bottom) { + if oneHandUI { searchBar } + } + + } + + @ViewBuilder + var searchBar: some View { + // TODO: Preserve height, without hardcoding it. Remove `.font(.system(size: 18))` after done. + let height: Double = 56 + let isVisible = isSearchExpanded || searchFocussed + VStack(spacing: 0) { + if oneHandUI { Divider() } + ChatListSearchBar( + searchMode: $searchMode, + searchFocussed: $searchFocussed, + searchText: $searchText, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + ) + .padding(8) + .frame(height: isVisible ? height : 0) + .opacity(isVisible ? 1 : 0) + if !oneHandUI { Divider() } + } + .background(Material.thin) + .padding(oneHandUI ? .top : .bottom, isVisible ? 0 : height) } @ViewBuilder func withToolbar(content: () -> some View) -> some View { if #available(iOS 16.0, *) { content() - .toolbarBackground(.visible, for: oneHandUI ? .bottomBar : .navigationBar) + .toolbarBackground(.hidden, for: .navigationBar) + .toolbarBackground(.hidden, for: .bottomBar) .toolbar { if oneHandUI { bottomToolbar @@ -152,10 +190,7 @@ struct ChatListView: View { @ViewBuilder var principalToolbarItem: some View { HStack(spacing: 4) { - Text("Chats") - .font(.headline) - // TODO: For testing, remove after oneHandUI is implemented - .contentShape(Rectangle()).onTapGesture { oneHandUI.toggle() } + Text("Chats").font(.headline) SubsStatusIndicator() } .frame(maxWidth: .infinity, alignment: .center) @@ -174,19 +209,6 @@ struct ChatListView: View { let cs = filteredChats() ZStack { List { - if !chatModel.chats.isEmpty { - ChatListSearchBar( - searchMode: $searchMode, - searchFocussed: $searchFocussed, - searchText: $searchText, - searchShowingSimplexLink: $searchShowingSimplexLink, - searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink - ) - .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - .frame(maxWidth: .infinity) - } if !oneHandUICardShown { OneHandUICard() .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) @@ -202,6 +224,7 @@ struct ChatListView: View { } .offset(x: -8) } + .introspect(.list, on: .iOS(.v16, .v17, .v18)) { setObservations(for: $0) } .listStyle(.plain) .onChange(of: chatModel.chatId) { chId in if chId == nil, let chatId = chatModel.chatToTop { @@ -224,6 +247,33 @@ struct ChatListView: View { } } + private func setObservations(for cv: UICollectionView) { + if collectionView != cv { + collectionView = cv + var scrollDistance: CGFloat = 0 + contentOffsetObservation?.invalidate() + contentOffsetObservation = cv.observe( + \.contentOffset, + options: [.new, .old] + ) { (cv, change) in + if let newOffset = change.newValue?.y, + let oldOffset = change.oldValue?.y { + let bottomOffset = cv.contentSize.height - cv.visibleSize.height - newOffset + cv.safeAreaInsets.bottom + // Show/Hide search bar when scrolled for more than `MAX` amount + if newOffset > .zero, + bottomOffset > 0 { + let MAX: CGFloat = 64 + scrollDistance = min(max(scrollDistance + oldOffset - newOffset, -MAX), +MAX) + if (isSearchExpanded && scrollDistance == -MAX) || + (!isSearchExpanded && scrollDistance == +MAX) { + withAnimation(.easeOut(duration: 0.15)) { isSearchExpanded.toggle() } + } + } + } + } + } + } + private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View { Circle() .frame(width: size, height: size) @@ -366,39 +416,37 @@ struct ChatListSearchBar: View { @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false var body: some View { - VStack(spacing: 12) { - HStack(spacing: 12) { - HStack(spacing: 4) { - Image(systemName: "magnifyingglass") - TextField("Search or paste SimpleX link", text: $searchText) - .foregroundColor(searchShowingSimplexLink ? theme.colors.secondary : theme.colors.onBackground) - .disabled(searchShowingSimplexLink) - .focused($searchFocussed) - .frame(maxWidth: .infinity) - if !searchText.isEmpty { - Image(systemName: "xmark.circle.fill") - .onTapGesture { - searchText = "" - } - } - } - .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) - .foregroundColor(theme.colors.secondary) - .background(Color(.tertiarySystemFill)) - .cornerRadius(10.0) - - if searchFocussed { - Text("Cancel") - .foregroundColor(theme.colors.primary) + HStack(spacing: 12) { + HStack(spacing: 4) { + Image(systemName: "magnifyingglass") + TextField("Search or paste SimpleX link", text: $searchText) + .font(.system(size: 18)) + .foregroundColor(searchShowingSimplexLink ? theme.colors.secondary : theme.colors.onBackground) + .disabled(searchShowingSimplexLink) + .focused($searchFocussed) + .frame(maxWidth: .infinity) + if !searchText.isEmpty { + Image(systemName: "xmark.circle.fill") .onTapGesture { searchText = "" - searchFocussed = false } - } else if m.chats.count > 0 { - toggleFilterButton() } } - Divider() + .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) + .foregroundColor(theme.colors.secondary) + .background(Color(.tertiarySystemFill)) + .cornerRadius(10.0) + + if searchFocussed { + Text("Cancel") + .foregroundColor(theme.colors.primary) + .onTapGesture { + searchText = "" + searchFocussed = false + } + } else if m.chats.count > 0 { + toggleFilterButton() + } } .onChange(of: searchFocussed) { sf in withAnimation { searchMode = sf } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index d338f7799b..0a44450368 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -201,6 +201,7 @@ CE38A29A2C3FCA54005ED185 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */; }; CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; }; CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7548092C622630009579B7 /* SwipeLabel.swift */; }; + CE7E3A472C628EAD00BECA8F /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = CE7E3A462C628EAD00BECA8F /* SwiftUIIntrospect */; }; CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; }; CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; }; CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; }; @@ -620,6 +621,7 @@ 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */, 8C8118722C220B5B00E6FC94 /* Yams in Frameworks */, D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */, + CE7E3A472C628EAD00BECA8F /* SwiftUIIntrospect in Frameworks */, D7197A1829AE89660055C05A /* WebRTC in Frameworks */, D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */, D7F0E33929964E7E0068AF69 /* LZString in Frameworks */, @@ -1149,6 +1151,7 @@ D7F0E33829964E7E0068AF69 /* LZString */, D7197A1729AE89660055C05A /* WebRTC */, 8C8118712C220B5B00E6FC94 /* Yams */, + CE7E3A462C628EAD00BECA8F /* SwiftUIIntrospect */, ); productName = "SimpleX (iOS)"; productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */; @@ -1293,6 +1296,7 @@ D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */, D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */, 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */, + CE7E3A452C628EAD00BECA8F /* XCRemoteSwiftPackageReference "swiftui-introspect" */, ); productRefGroup = 5CA059CB279559F40002BEB4 /* Products */; projectDirPath = ""; @@ -2334,6 +2338,14 @@ version = 5.1.2; }; }; + CE7E3A452C628EAD00BECA8F /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/swiftui-introspect.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.0; + }; + }; D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/simplex-chat/WebRTC.git"; @@ -2376,6 +2388,11 @@ package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */; productName = SwiftyGif; }; + CE7E3A462C628EAD00BECA8F /* SwiftUIIntrospect */ = { + isa = XCSwiftPackageProductDependency; + package = CE7E3A452C628EAD00BECA8F /* XCRemoteSwiftPackageReference "swiftui-introspect" */; + productName = SwiftUIIntrospect; + }; D7197A1729AE89660055C05A /* WebRTC */ = { isa = XCSwiftPackageProductDependency; package = D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */; diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d3e61c88f9..decfe5dba6 100644 --- a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e2611d1e91fd8071abc106776ba14ee2e395d2ad08a78e073381294abc10f115", + "originHash" : "415d895b8472ba92f7fa95973495513f544dcde3cbf09f6928a489d8c8731c14", "pins" : [ { "identity" : "codescanner", @@ -18,6 +18,15 @@ "revision" : "7f62f21de5b18582a950e1753b775cc614722407" } }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/swiftui-introspect.git", + "state" : { + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" + } + }, { "identity" : "swiftygif", "kind" : "remoteSourceControl",