show hide search field by observing scroll position

This commit is contained in:
Levitating Pineapple
2024-08-06 20:29:13 +03:00
parent 559093d0de
commit 624f36d686
3 changed files with 122 additions and 48 deletions
@@ -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 }
@@ -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" */;
@@ -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",