Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Update] Improve Sample Viewer search #228

Merged
merged 34 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fa7d112
Update sample search functionality.
CalebRas Jul 19, 2023
acf201a
Update comments.
CalebRas Jul 20, 2023
f0f6598
Add bolded query text.
CalebRas Jul 20, 2023
76dce08
Update search text.
CalebRas Jul 27, 2023
4cd0684
Update search section names.
CalebRas Jul 28, 2023
ee03f36
Update section headers.
CalebRas Jul 31, 2023
ad833a9
Apply suggestions from code review.
CalebRas Aug 2, 2023
71bcbff
Add temporary results.
CalebRas Aug 2, 2023
786ed77
Apply suggestions from code review.
CalebRas Aug 2, 2023
f292a59
Update search methods.
CalebRas Aug 2, 2023
c0802f6
Remove returns from methods.
CalebRas Aug 3, 2023
f187a74
Add newValue to onChange.
CalebRas Aug 3, 2023
cc3de81
Update bold text.
CalebRas Aug 3, 2023
fc79817
Add string extension.
CalebRas Aug 4, 2023
47626b4
Update code generation.
CalebRas Aug 4, 2023
4e849ee
Update set operations.
CalebRas Aug 4, 2023
2c05af4
Merge pull request #235 from Esri/Caleb/Fix-RemoveRelevantAPIsFromTags
CalebRas Aug 4, 2023
ba8edfa
Apply suggestions from code review.
yo1995 Aug 7, 2023
569ce4e
Address feedback from code review.
yo1995 Aug 8, 2023
2b6c5d3
Adjust newlines.
yo1995 Aug 8, 2023
528a95d
Add files to project file.
yo1995 Aug 8, 2023
fee4d34
Update content display logics.
yo1995 Aug 8, 2023
a98173f
Update comments in code generation script.
yo1995 Aug 8, 2023
4792c37
Fix the script.
yo1995 Aug 8, 2023
17ac880
Apply suggestions from code review.
yo1995 Aug 9, 2023
ab11d91
Update Shared/Supporting Files/Views/SampleRow.swift
yo1995 Aug 9, 2023
d7e652f
Update Scripts/GenerateSampleViewSourceCode.swift.
yo1995 Aug 9, 2023
b96f96b
Rename views and add `Sidebar`.
yo1995 Aug 9, 2023
9e3106c
Address feedback from code review.
yo1995 Aug 10, 2023
16224c0
Use sidebar as before.
yo1995 Aug 10, 2023
9311c82
Remove unneeded property.
yo1995 Aug 10, 2023
0dc8ff2
Reorder properties in ContentView.
yo1995 Aug 11, 2023
3f8c221
Make properties non-private.
yo1995 Aug 11, 2023
62e0b96
Merge branch 'v.next' into Caleb/Update-SampleViewerSearchBar
yo1995 Aug 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions Shared/Supporting Files/Extensions/CategoryView+Search.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,44 @@
import SwiftUI

extension CategoryView {
/// Searches the sample list using the query.
/// - Returns: A set of samples matching the query criteria.
func searchSamples() -> [Sample] {
if query.isEmpty {
return samples
} else {
return samples.filter { $0.name.localizedCaseInsensitiveContains(query) }
/// Searches through a list of samples using the sample's name and the query.
/// - Parameters:
/// - samples: The `Array` of samples to search through.
/// - query: The `String` to search with.
yo1995 marked this conversation as resolved.
Show resolved Hide resolved
/// - Returns: The samples whose name partially matches the query.
func searchNames(in samples: [Sample], with query: String) -> [Sample] {
yo1995 marked this conversation as resolved.
Show resolved Hide resolved
// Perform a partial text search using the sample's name and the query.
let nameSearchResults = samples.filter { sample in
sample.name.localizedCaseInsensitiveContains(query)
}
return nameSearchResults
CalebRas marked this conversation as resolved.
Show resolved Hide resolved
}

/// Searches through a list of samples using the sample's description and the query.
/// - Parameters:
/// - samples: The `Array` of samples to search through.
/// - query: The `String` to search with.
/// - Returns: The samples whose description partially matches the query.
func searchDescriptions(in samples: [Sample], with query: String) -> [Sample] {
// Perform a partial text search using the sample's description and the query.
let descriptionSearchResults = samples.filter { sample in
sample.description.localizedCaseInsensitiveContains(query)
}
return descriptionSearchResults
}

/// Searches through a list of samples using the sample's tags and the query.
/// - Parameters:
/// - samples: The `Array` of samples to search through.
/// - query: The `String` to search with.
/// - Returns: The samples which have a tag that fully matches the query.
func searchTags(in samples: [Sample], with query: String) -> [Sample] {
// Perform a full text search using the sample's tags and the query.
let tagsSearchResults = samples.filter { sample in
sample.tags.contains { tag in
tag.localizedCaseInsensitiveCompare(query) == .orderedSame
}
}
return tagsSearchResults
}
}
6 changes: 4 additions & 2 deletions Shared/Supporting Files/Views/CategoryGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ struct CategoryGridView: View {
LazyVGrid(columns: columns) {
ForEach(categories, id: \.self) { category in
NavigationLink {
SampleListView(samples: samples.filter { $0.category == category })
.navigationTitle(category)
List {
yo1995 marked this conversation as resolved.
Show resolved Hide resolved
SampleListView(samples: samples.filter { $0.category == category })
}
.navigationTitle(category)
} label: {
CategoryTitleView(category: category)
}
Expand Down
67 changes: 61 additions & 6 deletions Shared/Supporting Files/Views/CategoryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,47 @@ struct CategoryView: View {
/// The search query in the search bar.
@Binding private(set) var query: String

/// The samples to display in the name section of the search list.
@State private var nameSearchResults: [Sample] = []

/// The samples to display in the description section of the search list.
@State private var descriptionSearchResults: [Sample] = []

/// The samples to display in the tags section of the search list.
@State private var tagsSearchResults: [Sample] = []
yo1995 marked this conversation as resolved.
Show resolved Hide resolved

/// A Boolean value that indicates whether to present the about view.
@State private var isAboutViewPresented = false

/// The samples to display in the search list. Searching adjusts this value.
private var displayedSamples: [Sample] {
searchSamples()
}

var body: some View {
Group {
if !isSearching {
CategoryGridView(samples: samples)
} else {
SampleListView(samples: displayedSamples)
// The search results list.
List {
if !nameSearchResults.isEmpty {
Section(header: Text("Name Results")) {
SampleListView(samples: nameSearchResults, query: query)
}
}
if !descriptionSearchResults.isEmpty {
Section(header: Text("Description Results")) {
SampleListView(samples: descriptionSearchResults, query: query)
}
}
if !tagsSearchResults.isEmpty {
Section(header: Text("Tag Results")) {
SampleListView(samples: tagsSearchResults, query: query)
}
}
}
.onChange(of: query) { _ in
searchSamples(in: samples, with: query)
CalebRas marked this conversation as resolved.
Show resolved Hide resolved
}
.onAppear {
searchSamples(in: samples, with: query)
}
}
}
.navigationTitle("Samples")
yo1995 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -54,4 +81,32 @@ struct CategoryView: View {
}
}
}

/// Searches through a list of samples to find ones that match the query.
/// - Parameters:
/// - samples: The `Array` of samples to search through.
/// - query: The `String` to search with.
private func searchSamples(in samples: [Sample], with query: String) {
// Show all samples in the name section when query is empty.
guard !query.isEmpty else {
nameSearchResults = samples
yo1995 marked this conversation as resolved.
Show resolved Hide resolved
return
}

// The names of the samples already found in a previous section.
var previousSearchResults: Set<String> = []

// Update the name section results.
nameSearchResults = searchNames(in: samples, with: query)
previousSearchResults.formUnion(nameSearchResults.map(\.name))

// Update the description section results.
descriptionSearchResults = searchDescriptions(in: samples, with: query)
.filter { !previousSearchResults.contains($0.name) }
previousSearchResults.formUnion(descriptionSearchResults.map(\.name))

// Update the tags section results.
tagsSearchResults = searchTags(in: samples, with: query)
.filter { !previousSearchResults.contains($0.name) }
}
}
2 changes: 1 addition & 1 deletion Shared/Supporting Files/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ struct ContentView: View {

var sidebar: some View {
CategoryView(samples: samples, query: $query)
.searchable(text: $query, prompt: "Search By Sample Name")
.searchable(text: $query, prompt: "Search")
philium marked this conversation as resolved.
Show resolved Hide resolved
}

var detail: some View {
Expand Down
51 changes: 46 additions & 5 deletions Shared/Supporting Files/Views/SampleListView.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 Esri
// Copyright 2023 Esri
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -18,9 +18,17 @@ struct SampleListView: View {
/// All samples that will be displayed in the list.
let samples: [Sample]

/// The search query from the search bar.
let query: String

init(samples: [Sample], query: String = "") {
self.samples = samples
self.query = query
}

var body: some View {
List(samples, id: \.name) { sample in
SampleRow(sample: sample)
ForEach(samples, id: \.name) { sample in
SampleRow(sample: sample, boldedText: query)
}
}
}
Expand All @@ -30,6 +38,9 @@ private extension SampleListView {
/// The sample displayed in the row.
let sample: Sample

/// The text to bold.
let boldedText: String

/// A Boolean value that indicates whether to show the sample's description.
@State private var isShowingDescription = false

Expand All @@ -39,9 +50,10 @@ private extension SampleListView {
} label: {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(sample.name)
Text(.init(boldSubstring(sample.name, substring: boldedText)))

if isShowingDescription {
Text(sample.description)
Text(.init(boldSubstring(sample.description, substring: boldedText)))
.font(.caption)
.foregroundColor(.secondary)
.transition(.move(edge: .top).combined(with: .opacity))
Expand All @@ -58,5 +70,34 @@ private extension SampleListView {
.animation(.easeOut(duration: 0.2), value: isShowingDescription)
}
}

/// Bolds the first occurrence of substring within a given string using markdown.
/// - Parameters:
/// - text: The `String` containing the substring.
/// - substring: The substring to bold.
/// - Returns: The `String` with the bolded substring.
func boldSubstring(_ text: String, substring: String) -> String {
if let range = text.localizedLowercase.range(of: substring.localizedLowercase) {
yo1995 marked this conversation as resolved.
Show resolved Hide resolved
var boldedText = text
CalebRas marked this conversation as resolved.
Show resolved Hide resolved

// Add "**" to the front of the substring.
let lowerIndex = boldedText.distance(from: boldedText.startIndex, to: range.lowerBound)
boldedText.insert(contentsOf: "**", at: boldedText.index(
boldedText.startIndex,
offsetBy: lowerIndex
))

// Add "**" to the end of the substring.
let upperIndex = boldedText.distance(from: boldedText.startIndex, to: range.upperBound)
boldedText.insert(contentsOf: "**", at: boldedText.index(
boldedText.startIndex,
offsetBy: upperIndex + 2
))

return boldedText
}

return text
}
}
}
Loading