Search through list (JSON) SwiftUI - json

I am new to SwiftUI. I am currently doing Tutorials that are available on the Apple Developer website.
I was looking at the 'Handling User Input' part and I have a question. In there they take the JSON file and use it to populate the list. From there they create 'Favourite' toggle. My question is, is there a possibility to make JSON list searchable?
import SwiftUI
struct LandmarkList: View {
#EnvironmentObject var userData: UserData
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Favorites only")
}
ForEach(landmarkData) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
.environmentObject(UserData())
}
}
I found a way of making search field, which would look something like this:
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
and then calling it, but I can't find a way to search through the list. I found a lot of tutorials showing how to search through array, but that isn't very helpful.
I tried few things, this is one of them, but it doesn't work:
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText, placeholder: "Search")
List(LandmarkData.filter{searchText == "" ? true : $0.localizedCaseInsensitiveContains(searchText)}, id: \.self){ landmark in
LandmarkRow(landmark: landmark)
}
}.navigationBarTitle("Landmarks")
}
}
}
Can someone explain to me what I'm doing wrong? Thank you

I found an answer to it:
List {
ForEach(LandmarkData.filter {
$0.name.lowercased().contains(self.searchText) || self.searchText.isEmpty
}, id: \.self) { landmark in
LandmarkRow(landmark: landmark)
}
}
The rest of the code stayed the same :) Hope it will help somebody.

Related

SwiftUI JSON Decoder Response is 0 for APIResponse

I'm trying to fetch urls from an API based on a search query. If I hardcode the query parameter to some value in the url (i.e. "fitness"), I get a response.
If I set the query parameter to an interpolated value to be inserted at a later date, the app has no images at runtime-- which makes sense.
However, when I enter a search query into my search bar, I cannot fetch the results, either. In fact, my results are 0.
Here's the error:
po jsonResult
▿ APIResponse
- total : 0
- results : 0 elements
Here's my code:
Models
import Foundation
struct APIResponse: Codable {
let total: Int
let results: [Result]
}
struct Result: Codable {
let id: String
let urls: URLS
}
struct URLS: Codable {
let full: String
}
View
import SwiftUI
struct SimpleView: View {
#ObservedObject var simpleViewModel = SimpleViewModel.shared
#State private var searchText = ""
#State private var selected: String? = nil
var filteredResults: [Result] {
if searchText.isEmpty {
return simpleViewModel.results
} else {
return simpleViewModel.results.filter { $0.urls.full.contains(searchText) }
}
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 0) {
ForEach(filteredResults, id: \.id) { result in
NavigationLink(destination: SimpleDetailView()) {
VStack {
AsyncImage(url: URL(string: result.urls.full)) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.scaledToFill()
.frame(maxWidth: .infinity)
.onTapGesture {
withAnimation(.spring()) {
if self.selected == result.urls.full {
self.selected = nil
} else {
self.selected = result.urls.full
}
}
hideKeyboard()
}
.scaleEffect(self.selected == result.urls.full ? 3.0 : 1.0)
}
}
}
}
}
.onAppear {
simpleViewModel.fetchPhotos(query: searchText)
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
}
}
}
struct SimpleView_Previews: PreviewProvider {
static var previews: some View {
SimpleView()
}
}
ViewModel
import Foundation
class SimpleViewModel: ObservableObject {
static let shared = SimpleViewModel()
private init() {}
#Published var results = [Result]()
func fetchPhotos(query: String) {
let url = "https://api.unsplash.com/search/photos?page=1&query=\(query)&client_id=blahblahblahblahblahblahblahblah"
guard let url = URL(string: url) else { return }
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let data = data, error == nil else { return }
do {
let jsonResult = try JSONDecoder().decode(APIResponse.self, from: data)
DispatchQueue.main.async {
self?.results = jsonResult.results
}
} catch {
print("Error: \(error)")
}
}
task.resume()
}
}
How can I search for images in my SimpleView based on my search query in my SimpleViewModel?
Setting breakpoints (how I discovered 0 values)
Ternary operators to check for search values or not
Setting my computer on fire
UPDATE
I added this to the code as #workingdog suggested, but with an else statement.
.onSubmit(of: .search) {
if searchText.isEmpty {
simpleViewModel.results = filteredResults
} else if !searchText.isEmpty {
simpleViewModel.fetchPhotos(query: searchText)
}
}
Here's what happens:
Images are fetched, but not displayed in view
Search query on submit renders nothing
Pressing cancel enacts the query
The images are displayed
Images remain and are displayed. Go back to 2.
In your SimpleView, the .onAppear { simpleViewModel.fetchPhotos(query: searchText } is
called only when the view appears, and uses searchText = "". In other words you have an empty query. So remove the .onAppear{...}, it does nothing.
Add something like this, to fetch the photos when the searchText
is submitted.
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.onSubmit(of: .search) {
if !searchText.isEmpty {
simpleViewModel.fetchPhotos(query: searchText)
}
}

SwiftUI: Instance member 'enableFilter' cannot be used on type 'ContentView'; did you mean to use a value of this type instead?

I'm a beginner with SwiftUI and I wanted to create a dynamic list with the birthdays of people. I also wanted to integrate a filter that facilitates to find birthdays. But in the lines:
if enableFilter == true {
return json.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return json
}
I always get these errors:
Instance member 'enableFilter' cannot be used on type 'ContentView';
did you mean to use a value of this type instead?
and
Instance member 'filterDate' cannot be used on type 'ContentView'; did
you mean to use a value of this type instead?
I think I understand why the errors are present but I don't no how to fix it. I tried:
#State static var
but then I cannot change the values with my
filterView
Thank you for your help, here is the full source code:
import SwiftUI
struct person: Codable, Hashable, Identifiable {
var id: Int
var Birthday: Date
var BirthdayString: String
}
func filter(date: Date) -> String {
let DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: date)
let DateComponentsString: String = "\(DateComponents.day)/\(DateComponents.month)/\(DateComponents.year)"
return DateComponentsString
}
struct ContentView: View {
#State var people: [person] = {
guard let data = UserDefaults.standard.data(forKey: "people") else { return [] }
if let json = try? JSONDecoder().decode([person].self, from: data) {
if enableFilter == true {
return json.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return json
}
}
return []
}()
#State var filterDate: Date = Date()
#State var enableFilter: Bool = false
#State var showFilter: Bool = false
#State var newPersonDate: Date = Date()
var body: some View {
NavigationView {
VStack {
HStack {
DatePicker(selection: $newPersonDate, label: {Text("Birthday")}).padding()
Button(action: {didTapAddTask()}, label: {Text("Add")}).padding()
}
List {
ForEach($people) { $person in
Text("\(person.Birthday)")
}
}
}
.navigationTitle(Text("People's birthday"))
}
}
var filterView: some View {
VStack {
DatePicker(selection: $filterDate, label: {Text("Date")}).padding()
Toggle(isOn: $enableFilter, label: {Text("enable filter")}).padding()
}
}
func didTapAddTask() {
let id = people.reduce(0) { max($0, $1.id) } + 1
people.insert(person(id: id, Birthday: newPersonDate, BirthdayString: filter(date: newPersonDate)), at: 0)
newPersonDate = Date()
save()
}
func save() {
guard let data = try? JSONEncoder().encode(people) else { return }
UserDefaults.standard.set(data, forKey: "people")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can use another Computed property which will get the data based on some another property's value. i.e enableFilter.
#State var people: [person] = {
guard let data = UserDefaults.standard.data(forKey: "people") else { return [] }
if let json = try? JSONDecoder().decode([person].self, from: data) {
return json
}
return []
}()
var data : [person] {
if enableFilter {
return people.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return people
}
}
And use this property to get the relevant data:
var body: some View {
NavigationView {
VStack {
HStack {
DatePicker(selection: $newPersonDate, label: {Text("Birthday")}).padding()
Button(action: {didTapAddTask()}, label: {Text("Add")}).padding()
}
List {
ForEach(data) { person in // <<--- Here `data`
Text("\(person.Birthday)")
}
}
}
.navigationTitle(Text("People's birthday"))
}
}

Swiftui Creating a load more button for Json data items

I have not gotten to 50 reputation so i could not comment on this question SwiftUI Issue displaying specific number of Json Data Items to ask how it was done. The idea is to have the first 10 items in a json array show when the view is loaded and then a load more button to show more items.
This is how my code looks like.
Group {
HStack {
Text("Recommended Events")
.font(.title3)
.foregroundColor(.white)
.fontWeight(.bold)
Spacer()
Button(action: {
}) {
Text("Show all")
.font(.title3)
.foregroundColor(Color.white)
.fontWeight(.bold)
}
}
.padding(15)
ForEach(recommendeds) { recommended in
NavigationLink(destination: RecommendedEventsDetailView(recommended: recommended)) {
RecommendedEventsView(recommended: recommended)
}
}
}
Edited
After https://stackoverflow.com/users/14733292/raja-kishan response i tried it and got this error Failed to produce diagnostic for expression; please file a bug report This is the stage of my code now.
struct RecommendedModel: Identifiable {
var id = UUID()
var number: Int
init(_ number: Int) {
self.number = number
}
}
struct PlacesView: View {
private var arrData: [RecommendedModel] = (0...10).map({RecommendedModel($0)})
#State private var isMore: Bool = false
//E-MARK: - Body
var body: some View {
Group {
HStack {
Text("Recommended Events")
.font(.title3)
.foregroundColor(.white)
.fontWeight(.bold)
Spacer()
Button(action: {
withAnimation {
isMore.toggle()
}
}) {
Text("Show all")
.font(.title3)
.foregroundColor(Color.white)
.fontWeight(.bold)
}
}
.padding(15)
ForEach( (isMore ? arrData : recommendeds(arrData.prefix(5)))) { recommended in
NavigationLink(destination: RecommendedEventsDetailView(recommended: \(recommended.number)) {
RecommendedEventsView(recommended: \(recommended.number))
}
}
}
}
}
Below is the Data model i had before that loads the json data saved as RecommendedModel.swift
struct Recommended: Codable, Identifiable {
let id: String
let image: String
let date: String
let month: String
let like: String
let rating: String
let heading: String
let place: String
let article: String
let more: String
}
You can do this.
You can load the first 10 or 50 by .prefix() from the array.
Demo code
struct DataModel: Identifiable {
var id = UUID()
var number: Int
init(_ number: Int) {
self.number = number
}
}
struct LoadMoreDemo: View {
private var arrData: [DataModel] = (0...100).map({DataModel($0)})
#State private var isMore: Bool = false
var body: some View {
VStack {
ScrollView {
ForEach( (isMore ? arrData : Array(arrData.prefix(15)))) { item in
Text("\(item.number)")
}
}
Button("Load More") {
withAnimation {
isMore.toggle()
}
}
}
}
}

Problems with JSON visualization in SwiftUI App

I'm working on my first app, which loads a JSON with the elements that need to be displayed.
At the moment everything seems to work, except a few details which I hope you can help me solve:
My interface is composed of a TabView contained within a NavigationView (which contains a button that brings up a SheetView) ... if I go to the Tab that brings up the list with the contents downloaded from the JSON everything works, but if I move to another Tab and then I go back to the one that displays the JSON, the data is no longer visible.
I obviously inserted in the code (where necessary) ObservableObject, ObservedObject and onAppear that should reload the JSON, but without success ... it is clear that something is missing ...
How could I go about displaying a list of categories starting from a JSON field? Let me explain, I used the "autore" field of the JSON to define categories, so I would need to first display a list of these categories, then, entering the respective categories, I have to display a list with the elements relating to the selected category and then obviously the DetailView for the single elements.
Of course, since is my first time writing here, ask me everything you need to help me solving the problem...
This is the relevant code:
Code: BussoModel.swift
import SwiftUI
class BussoModel: Identifiable {
var id: UUID = UUID()
var titolo: String = ""
var autore: String = ""
var testo: String = ""
var data: String = ""
var extra1: String = ""
var extra2: String = ""
var fotoUrl: String = ""
}
Code: DownloadManager.swift
import SwiftUI
import Combine
import Alamofire
class DownloadManager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
var storage : [BussoModel] = []
func scaricaJSON() {
AF.request("http://geniuspointfrezza.altervista.org/index.php?json=1").responseJSON { response in
if let errore = response.error {
debugPrint(errore.localizedDescription)
return
}
guard let ilJson = response.value else { return }
guard let json = JSON(ilJson).array else { return }
self.storage = []
let totale = json.count
for i in 0..<totale {
let busso = BussoModel()
if let titolo = json[i]["titolo"].string {
busso.titolo = titolo
}
if let autore = json[i]["autore"].string {
busso.autore = autore
}
if let testo = json[i]["testo"].string {
busso.testo = testo
}
if let data = json[i]["data"].string {
busso.data = data
}
if let extra1 = json[i]["extra1"].string {
busso.extra1 = extra1
}
if let extra2 = json[i]["extra2"].string {
busso.extra2 = extra2
}
if let foto = json[i]["fotoUrl"].string {
busso.fotoUrl = foto
}
self.storage.append(busso)
}
self.objectWillChange.send()
}
}
}
Code: CategoryView.swift
import SwiftUI
import URLImage
struct CategoryView: View {
#ObservedObject var dm: DownloadManager
var body: some View {
List {
ForEach(dm.storage) { busso in
NavigationLink(
destination: DetailView(busso: busso)) {
HStack {
URLImage(URL(string: busso.fotoUrl) ?? furl)
.resizable()
.aspectRatio(contentMode: .fit)
.clipped()
.padding()
VStack(alignment: .leading) {
Text(busso.titolo)
.font(.headline)
Text(busso.autore)
.font(.subheadline)
}
Spacer().layoutPriority(-0.1)
}
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 50)
}
}
}
.onAppear {
self.dm.scaricaJSON()
}
}
}
struct CategoryView_Previews: PreviewProvider {
static var previews: some View {
CategoryView(dm: DownloadManager())
}
}
Code: MainTabView.swift
import SwiftUI
struct MainTabView: View {
#State private var selected = 0
var body: some View {
TabView(selection: $selected) {
HomeView()
.tabItem {
Image(systemName: (selected == 0 ? "house.fill" : "house"))
Text("Home")
}.tag(0)
CategoryView(dm: DownloadManager())
.tabItem {
Image(systemName: (selected == 1 ? "text.justify" : "text.justify"))
Text("Categorie")
}.tag(1)
Text("Galleria?")
.tabItem {
Image(systemName: (selected == 2 ? "photo.fill" : "photo"))
Text("Galleria")
}.tag(2)
Text("Preferiti?")
.tabItem {
Image(systemName: (selected == 3 ? "star.fill" : "star"))
Text("Preferiti")
}.tag(3)
}
.accentColor(.white)
.onAppear() {
UINavigationBar.appearance().barTintColor = UIColor(red: 115.0/255.0, green: 90.0/255.0, blue: 143.0/255.0, alpha: 1.0)
UINavigationBar.appearance().titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
UITabBar.appearance().barTintColor = UIColor(red: 115.0/255.0, green: 90.0/255.0, blue: 143.0/255.0, alpha: 1.0)
}
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView()
}
}
Code: ContentView.swift
import SwiftUI
import URLImage
let furl = URL(fileURLWithPath: "path")
struct ContentView: View {
#State var showInfoView = false
var body: some View {
NavigationView {
MainTabView()
.sheet(isPresented: $showInfoView) {
InfoView(showInfoView: $showInfoView)
}
.navigationBarTitle((Text("ViviBusso")), displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
debugPrint("QR Code")
}) {
Image(systemName: "qrcode")
}
.foregroundColor(.white), trailing: Button(action: {
self.showInfoView.toggle()
}) {
Image(systemName: "info.circle")
}
.foregroundColor(.white)
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Waiting for suggestions, thanks!
EDIT: New code tested:
import SwiftUI
import URLImage
struct Busso: Codable, Identifiable {
public var id: UUID
public var titolo: String
public var autore: String
public var testo: String
public var data: String
public var extra1: String
public var extra2: String
public var foto: String
public var fotoUrl: String
}
class FetchBusso: ObservableObject {
#Published var Bussos = [Busso]()
init() {
let url = URL(string: "https://geniuspointfrezza.altervista.org/index.php?json=1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let bussoData = data {
let decodedData = try JSONDecoder().decode([Busso].self, from: bussoData)
DispatchQueue.main.async {
self.Bussos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
struct CategoryView: View {
#ObservedObject var fetch = FetchBusso()
var body: some View {
VStack {
List(fetch.Bussos) { todo in
VStack(alignment: .leading) {
Text(todo.titolo)
}
}
}
}
}
struct CategoryView_Previews: PreviewProvider {
static var previews: some View {
CategoryView()
}
}

ObservedObject, derived from Json, not loading into Picker, but loads in List. In SwiftUI, Xcode 11

I'm new to mobile development, and in the process of learning SwiftUI.
I've been struggling to figure out what's wrong with my picker. I am successfully returning the data from my URLSession, adding it to my model. I can confirm this by adding my #ObservedObject to a List, which returns all of the items. Putting the same #ObservedObject into a picker returns an empty picker for some reason. Any help would be appreciated. Thanks.
Here's my view with the Picker(). When run, the Picker is empty. I can comment out the Picker(), leaving just the ForEach() with Text(), and the text appears.
import Foundation
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject var countries = CulturesViewModel()
#State private var selectedCountries = 0
var body: some View {
VStack {
//loop through country array and add them to picker
Picker(selection: $selectedCountries, label: Text("Select Your Country")) {
ForEach(0 ..< countries.cultures.count, id: \.self) { post in
Text(self.countries.cultures[post].Culture).tag(post)
}
}.labelsHidden()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here's my ViewModel. It set's the #Published variable to the results of the JSON request in the WebService(). If I hard-code the #Published variable to the value that's begin returned, the Picker works.
import Foundation
import Combine
import SwiftUI
class CulturesViewModel: ObservableObject {
init() {
fetchCultures()
}
#Published var cultures = [Culture](){
didSet {
didChange.send(self)
}
}
private func fetchCultures(){
WebService().GetCultures {
self.cultures = $0
}
}
let didChange = PassthroughSubject<CulturesViewModel, Never>()
}
Here's my WebService(). Unfortunately, I'm unable to share the JSON url, I've added in the json that's returned.
import Foundation
import SwiftUI
import Combine
class WebService {
func GetCultures(completion: #escaping([Culture]) ->()) {
guard let url = URL("")
[
{
"CultureId": 0,
"Culture": "Select Your Country"
},
{
"CultureId": 1078,
"Culture": "English (United States)"
},
{
"CultureId": 6071,
"Culture": "English (Canada)"
}
]
URLSession.shared.dataTask(with: url) { (data,_,_) in
do {
if let data = data {
let culturesList = try JSONDecoder().decode([Culture].self, from: data)
DispatchQueue.main.async {
completion(culturesList)
}
} else {
DispatchQueue.main.async {
completion([])
}
}
} catch {
print(error)
DispatchQueue.main.async {
completion([])
}
}
}.resume()
}
}
Lastly, here's my Model.
import Foundation
struct Culture: Codable, Identifiable {
var id = UUID()
var CultureId: Int
var Culture: String
}
The work around to make the picker refresh is to add a unique id. Refreshing (or) reloading the countries, will create a new UUID for the picker items. This will force the picker to refresh. I've modified your code to include an id.
//loop through country array and add them to picker
Picker(selection: $selectedCountries, label: Text("Select Your Country")) {
ForEach(0 ..< countries.cultures.count, id: \.self) { post in
Text(self.countries.cultures[post].Culture).tag(post)
}
}.labelsHidden()
.id(UUID())
This seems to be a known issue with the picker.