I have to make a Contacts-like app. I have a model object. Please notice the 2 properties phoneNumbers & addresses are Arrays
class Contact: NSObject {
var name: String?
var companyName: String?
var phoneNumbers: [String]?
var addresses: [String]?
.. // Custom init methods etc
}
Now I have to populate these and search them using search View Controller.
I followed the simple way, implemented a searchViewController in TableViewController. Much like here: https://www.raywenderlich.com/113772/uisearchcontroller-tutorial
func filterContentForSearchText(searchText: String, scope: String = "All") {
filteredContacts.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "name CONTAINS[c] %# OR companyName CONTAINS[c] %#", searchController.searchBar.text!, searchController.searchBar.text!, searchController.searchBar.text!)
let array = (contacts as NSArray).filteredArrayUsingPredicate(searchPredicate)
filteredContacts = array as! [Contact]
tableView.reloadData()
}
My problem is how to search throught the array of phone numbers and addresses? The above code works good searching through non-array properties, but how to search through the arrays?
Here is code you paste in a playground to test searching within properties that are arrays. I kept the search functions simple for clarity, but the code can be shortened using filters and closures.
import UIKit
class Contact {
var name: String?
var companyName: String?
var phoneNumbers: [String]?
var addresses: [String]?
init (name: String, companyName: String, phoneNumbers: [String], addresses: [String]) {
self.name = name
self.companyName = companyName
self.phoneNumbers = phoneNumbers
self.addresses = addresses
}
}
var contacts: [Contact] =
[(Contact.init(name: "Tom Jones", companyName: "Jones", phoneNumbers: ["111-1111", "111-1112", "111-1113"], addresses: ["Hollywood", "New York"])),
(Contact.init(name: "Aretha Franklin", companyName: "Franklin", phoneNumbers: ["111-1114", "111-1115", "111-1116"], addresses: ["Detroit", "Hollywood"])),
(Contact.init(name: "Axel Rose", companyName: "Rose", phoneNumbers: ["111-1117", "111-11128", "111-1111"], addresses: ["London", "New York"])) ]
let phoneNumberSearchString = "111-1111"
func searchByPhoneNumber (searchNumber: String, inDataSet contacts: [Contact]) -> [Contact] {
var searchResult: [Contact] = []
for contact in contacts {
for phoneNumber in contact.phoneNumbers! {
if phoneNumber == searchNumber {
searchResult.append(contact)
}
}
}
return searchResult
}
func searchByAddress (searchAddress: String, inDataSet contacts: [Contact]) -> [Contact] {
var searchResult: [Contact] = []
for contact in contacts {
for address in contact.addresses! {
if address == searchAddress {
searchResult.append(contact)
}
}
}
return searchResult
}
let foundContacts = searchByPhoneNumber("111-1111", inDataSet: contacts)
for contact in foundContacts {
print(contact.name!)
}
let foundContacts2 = searchByAddress("Hollywood", inDataSet: contacts)
for contact in foundContacts2 {
print(contact.name!)
}
/* Print results
Tom Jones
Axel Rose
Tom Jones
Aretha Franklin
*/
Related
I have a JSON response (bellow) and I need to parse this -
[
{
"id":123,
"name":"Fahim Rahman",
"age":25,
"friends":[
{
"firstName": "Imtiaz",
"lastName": "Khan",
"avatar_url": null
}
],
"groups":{
"xcet":{
"name":"xcek cert etsh tnhg",
"createdDate":"2022-10-31T10:00:48Z"
},
"juyt":{
"name":"jfd uyt you to",
"createdDate":"2021-09-13T10:00:00Z"
},
"some random key":{
"name": "some name",
"createdDate":"2026-03-27T10:00:00Z"
}
}
}
]
To parse this in my code I've created this model. I can not able to parse the groups as that is not a list but an object -
import ObjectMapper
class Person: BaseObject {
#objc dynamic var ID: Int = -1
#objc dynamic var name: String = ""
#objc dynamic var age: Int = -1
var friendsList = List<Friends>()
override func mapping(map: ObjectMapper.Map) {
ID <- map["id"]
name <- map["name"]
age <- map["age"]
friendsList <- map["friends"]
}
}
class Friends: BaseObject {
#objc dynamic var firstName: String = ""
#objc dynamic var lastName: String = ""
#objc dynamic var avatarURL: String = ""
override func mapping(map: ObjectMapper.Map) {
firstName <- map["firstName"]
lastName <- map["name"]
avatarURL <- map["avatar_url"]
}
}
I know it's a bad JSON. The groups should be on the list instead of the nested objects but unfortunately, I'm getting this response.
Here in the response of groups, the number of nested objects is dynamic and the key of the nested object is also dynamic. Thus I can not able to parse this as friends attribute.
So my question is, how can I map the "groups"?
Before mapping groups, we need a class that can hold each Group alongside its key (i.e. xct)
For example
Class Groups: BaseObject {
#objc dynamic var key: String = ""
#objc dynamic var value: GroupsItem?
convenience init(key: String, value: GroupsItem) {
self.init()
self.key = key
self.value = value
}
}
Class GroupsItem: BaseObject {
#objc dynamic var name: String?
#objc dynamic var createdDate: String?
...
}
Then inside your Person class you can map this as -
private func mapGroupsItems(map: ObjectMapper.Map) -> List<GroupsItem> {
var rowsDictionary: [String: Groups]?
rowsDictionary <- map["groups"]
let rows = List<GroupsItem>()
if let dictionary = rowsDictionary {
for (key, value) in dictionary {
rows.append(GroupsItem(key: key, value: value))
}
}
return rows
}
dont forget to call this method from mapping -
override public func mapping(map: ObjectMapper.Map) {
...
groups = mapGroupsItems(map: map)
}
try this approach, using a custom init(from decoder: Decoder) for Groups, works well for me. Use a similar approach for non-SwiftUI systems.
struct ContentView: View {
#State var people: [Person] = []
var body: some View {
ForEach(people) { person in
Text(person.name)
ForEach(Array(person.groups.data.keys), id: \.self) { key in
Text(key).foregroundColor(.red)
Text(person.groups.data[key]?.name ?? "no name").foregroundColor(.blue)
Text(person.groups.data[key]?.createdDate ?? "no date").foregroundColor(.blue)
}
}
.onAppear {
let json = """
[
{
"id":123,
"name":"Fahim Rahman",
"age":25,
"friends":[
{
"firstName": "Imtiaz",
"lastName": "Khan",
"avatar_url": null
}
],
"groups":{
"xcet":{
"name":"xcek cert etsh tnhg",
"createdDate":"2022-10-31T10:00:48Z"
},
"juyt":{
"name":"jfd uyt you to",
"createdDate":"2021-09-13T10:00:00Z"
},
"some random key":{
"name": "some name",
"createdDate":"2026-03-27T10:00:00Z"
}
}
}
]
"""
if let data = json.data(using: .utf8) {
do {
self.people = try JSONDecoder().decode([Person].self, from: data)
print("---> people: \(people)")
} catch {
print("decode error: \(error)")
}
}
}
}
}
struct Person: Identifiable, Codable {
let id: Int
var name: String
var age: Int
var friends: [Friend]
var groups: Groups
}
struct Friend: Codable {
var firstName, lastName: String
var avatarURL: String?
enum CodingKeys: String, CodingKey {
case firstName, lastName
case avatarURL = "avatar_url"
}
}
struct Info: Codable {
var name: String
var createdDate: String
}
struct Groups: Identifiable, Codable {
let id = UUID()
var data: [String:Info] = [:]
init(from decoder: Decoder) throws {
do {
let container = try decoder.singleValueContainer()
self.data = try container.decode([String:Info].self)
} catch {
print(error)
}
}
}
Your Model classes structure will be
// MARK: - Welcome7Element
struct Welcome7Element {
let id: Int
let name: String
let age: Int
let friends: [Friend]
let groups: Groups
}
// MARK: - Friend
struct Friend {
let firstName, lastName: String
let avatarURL: NSNull
}
// MARK: - Groups
struct Groups {
let xcet, juyt, someRandomKey: Juyt
}
// MARK: - Juyt
struct Juyt {
let name: String
let createdDate: Date
}
Thank you #shakif_ for your insightful answer. Here is how I solved this based on that answer -
import ObjectMapper
import RealmSwift
class Person: BaseObject {
#objc dynamic var ID: Int = -1
#objc dynamic var name: String = ""
#objc dynamic var age: Int = -1
var friendsList = List<Friends>()
var groups = List<Group>
override func mapping(map: ObjectMapper.Map) {
ID <- map["id"]
name <- map["name"]
age <- map["age"]
friendsList <- map["friends"]
groups = extractGroups(map)
}
private func extractGroups(_ map: ObjectMapper.Map) -> List<Group> {
let items = List<Group>()
var modifiedJSON = [String: Group]
modifiedJSON <- map["groups"]
for (key,value) in modifiedJSON {
let item = GroupMapper(key: key, value: value)
if let group = item.value {
items.append(group)
}
}
return items
}
}
class Friends: BaseObject {
#objc dynamic var firstName: String = ""
#objc dynamic var lastName: String = ""
#objc dynamic var avatarURL: String = ""
override func mapping(map: ObjectMapper.Map) {
firstName <- map["firstName"]
lastName <- map["name"]
avatarURL <- map["avatar_url"]
}
}
class Group: BaseObject {
#objc dynamic var name: String = ""
#objc dynamic var createdDate: String = ""
override func mapping(map: ObjectMapper.Map) {
name <- map["name"]
createdDate <- map["createdDate"]
}
}
struct GroupMapper {
var key: String = ""
var value: Group?
}
I have data in local file, I have loaded them into the tableView, and now I want to search/filter via search bar.
I have tried some solutions I've found on the Internet, but I don't know how to access specific data in my structure.
Here is part of the structure:
struct Model: Codable {
var data: [Description]
}
struct Description: Codable {
var id: Int
var name: String
var address: String
var city: String
var description: String
var countryId: Int
var webSite: String
var status: Int
var accountType: Int
var addDate: String
var placeGroupId: Int
var longitude: Double
var latitude: Double
var distance: Double
var working: Bool
var promotion: String
var repertoire: Repertoire
var workingHour: WorkingHour
var country: Country
var reviewNum: Int
var score: Double
var placeImgUrl: String
}
var model: Model?
and here is some code from my ViewController:
var filteredObjects = [Description]()
let searchController = UISearchController(searchResultsController: nil)
func searchBarIsEmpty() -> Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
}
extension ViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
<#code#>
}
}
I have variable model, which is type of struct Model. Structure Model have a variable data which is type of struct Description. And finally, in structure Description I have variable name. My tableView cells shows names of models (model?.data[indexPathrow.row].name). Now, I need to search by names shown in tableView cells. I hope I explained it clearly. Thank You very much!
Assuming objects is also [Description] and represents the data source array
func updateSearchResults(for searchController : UISearchController)
{
let searchString = searchController.searchBar.text!
if searchString.isEmpty {
filteredObjects.removeAll()
} else {
filteredObjects = objects.filter{ $0.name.range(of: searchString, options: .caseInsensitive) != nil }
}
tableView.reloadData()
}
In the table view data source methods display objects or filteredObjects depending on searchController.isActive
My scenario, I created UITableView with below structured JSON data load. I need to add global search for firstname, price, date, title and description. Here, I tried but it didn’t worked well for me. Please give me some solution.
For e.g.: User searched by name or price, date, title and description UITableView should sortout list.
My Codable Struture
struct Welcome: Codable {
let status: Int
let message: String
let ode: Int
let data: [Datum]
}
struct Datum: Codable {
let id: Int
let title, description: String
let price: Int
let user: User
let Appcode: Appcode
let Appbase: Appbase
enum CodingKeys: String, CodingKey {
case id
case userID = "userid"
case title, description
case price
case Date = "date"
case location, user
case appcode = "appcode”
case appbase = "appbase”
}
}
struct Appcode: Codable {
let card: String
}
struct Appbase: Codable {
let base: String
}
struct User: Codable {
let firstname, lastname: String
}
My Tableview Code
var tableArray = [Datum]()
var filteredResults = [Datum]()
lazy var searchBar:UISearchBar = UISearchBar()
var isSearching = false
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isSearching {
return filteredResults.count
} else {
return self.tableArray.count
}
return 0
}
My Search bar code
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchBar.text == nil || searchBar.text == "" { //
isSearching = false
view.endEditing(true)
tableView.reloadData()
}
else {
isSearching = true
filteredResults = tableArray.filter({ value -> Bool in
guard let text = searchBar.text else { return false}
return value.description.contains(text) // According to title from JSON
})
tableView.reloadData()
}
}
In your struct Datum your CodingKeys don't match your property names
The names of the enumeration cases should match the names you've given to the corresponding properties in your type.
If you update your struct like this:
struct Datum: Codable {
let id: Int
let title, description: String
let price: Int
let user: User
let appCode: Appcode
let appBase: Appbase
enum CodingKeys: String, CodingKey {
case id
case title, description
case price
case user
case appCode = "appcode"
case appBase = "appbase"
}
}
You can later filter the array of elements like this:
let datum1 = Datum(id: 1, title: "test", description: "test1", price: 10, user: User(firstname: "a", lastname: "b"), appCode: Appcode(card: "app"), appBase: Appbase(base: "base"))
let datum2 = Datum(id: 1, title: "test", description: "test2", price: 10, user: User(firstname: "a", lastname: "b"), appCode: Appcode(card: "app"), appBase: Appbase(base: "base"))
let datum3 = Datum(id: 1, title: "test", description: "test3", price: 10, user: User(firstname: "a", lastname: "b"), appCode: Appcode(card: "app"), appBase: Appbase(base: "base"))
let array = [datum1, datum2, datum3]
array.filter{$0.user.firstname == "a"}
array.filter{$0.description.contains("2")}
I'm working in swift 4.0, i have model object in MutableArray,
NSPredicate not working.
//Here is code
//Model class:
class Modelclass: NSObject
{
var firstName:String!
var lastName:String!
}
// Viewcontroller doing predicate:
let ary = NSMutableArray()
let userModel = Modelclass()
userModel.firstName = "Arrya"
userModel.lastName = "stark"
ary.add(userModel)
let commitPredicate = NSPredicate(format: "firstName == %#", "Arrya")
let resultAry = ary.filtered(using: commitPredicate)
print(resultAry)
I solved the problem,
Solution 1:
//I just added below code in model class
extension Modelclass {
#objc override func value(forKey key: String) -> Any? {
switch key {
case "firstName":
return firstName
case "lastName":
return lastName
default:
return nil
}
}
}
Solution 2:
Add #objc before variable:
class Modelclass: NSObject
{
#objc var firstName:String!
#objc var lastName:String!
}
var is introduced for Mutable Content/Variables
In swift you can use filter alternative to NSPredicate
Try Like this:
//Model class:
class Modelclass
{
var firstName: String
var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
// Viewcontroller doing predicate:
var ary:[Modelclass] = []
let userModel = Modelclass(firstName: "Arrya", lastName: "stark")
ary.append(userModel)
let resultAry = ary.filter{ $0.firstName == "Arrya" }
print(resultAry)
I´m trying to save an array of structs into coredata. I did a lot of research, but i cannot find the solution.
Here´s what i´ve got:
import Cocoa
import CoreData
class ViewController: NSViewController {
struct StudentsStruct {
let firstName: String
let lastName: String
let age: Int
}
let Studentsdata: [StudentsStruct] = [StudentsStruct(firstName: "Albert", lastName: "Miller", age: 24), StudentsStruct(firstName: "Susan", lastName: "Gordon", age: 24), StudentsStruct(firstName: "Henry", lastName: "Colbert", age: 24)]
override func viewDidLoad() {
super.viewDidLoad()
let student: Student = NSEntityDescription.insertNewObject(forEntityName: "Student", into: DatabaseController.getContext()) as! Student
for items in Studentsdata {
student.firstName = StudentsStruct.firstName
student.lastName = StudentsStruct.lastName
student.age = StudentsStruct.age
}
DatabaseController.saveContext()
let fetchRequest: NSFetchRequest<Student> = Student.fetchRequest()
print (student)
}
}
The DatabaseController is solution i´ve got from this tutorial:
https://www.youtube.com/watch?v=da6W7wDh0Dw
It´s not so important, it´s just making the "getContext" function.
Whats important, in teh commandline "student.firstName = StudentsStruct.firstName" i´m getting the error "instance member "firstName" cannot be used on type ViewController.StudentStruct.
After trying and trying, i´m running out of ideas how to get the array of structs into coredata.
This is the DatabaseController file:
import Foundation
import CoreData
class DatabaseController {
private init() {
}
class func getContext() -> NSManagedObjectContext {
return DatabaseController.persistentContainer.viewContext
}
// MARK: - Core Data stack
static var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "StudentCoreFile")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error {
fatalError("Unresolved error \(error)")
}
})
return container
}()
class func saveContext () {
let context = DatabaseController.persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
for any help thanks in advance!
Ok, you are right, i forgot to execute the fetchrequest. Here´s my current code:
import Cocoa
import CoreData
class ViewController: NSViewController {
struct StudentsStruct {
let firstName: String
let lastName: String
let age: Int
}
let Studentsdata: [StudentsStruct] = [StudentsStruct(firstName: "Albert", lastName: "Miller", age: 24), StudentsStruct(firstName: "Susan", lastName: "Gordon", age: 24), StudentsStruct(firstName: "Henry", lastName: "Colbert", age: 24)]
override func viewDidLoad() {
super.viewDidLoad()
let student: Student = NSEntityDescription.insertNewObject(forEntityName: "Student", into: DatabaseController.getContext()) as! Student
for item in Studentsdata {
let student: Student = NSEntityDescription.insertNewObject(forEntityName: "Student", into: DatabaseController.getContext()) as! Student
student.firstName = item.firstName
student.lastName = item.lastName
student.age = Int16(item.age)
}
DatabaseController.saveContext()
let fetchRequest: NSFetchRequest<Student> = Student.fetchRequest()
do {
let searchResults = try DatabaseController.getContext().fetch(fetchRequest)
print("number of results: \(searchResults.count)")
for result in searchResults as [Student] {
print(student)
}
} catch {
print ("error")
}
}
}
It´s running without errors. Now i´m getting 32 search results. Every entry is: age = 0; firstName = nil; lastName = nil;
For comparison, this code, without the loop is working:
import Cocoa
import CoreData
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let student: Student = NSEntityDescription.insertNewObject(forEntityName: "Student", into: DatabaseController.getContext()) as! Student
student.firstName = "henry"
student.lastName = "miller"
student.age = 22
DatabaseController.saveContext()
let fetchRequest: NSFetchRequest<Student> = Student.fetchRequest()
do {
let searchResults = try DatabaseController.getContext().fetch(fetchRequest)
print("number of results: \(searchResults.count)")
for result in searchResults as [Student] {
print(student)
}
} catch {
print ("error")
}
}
}
You need to access the item in your for loop also you are currently accessing the same object Student object in for loop instead of that you need to create a new Student in every iteration of for loop.
for item in Studentsdata {
//Create new student in for loop
let student = NSEntityDescription.insertNewObject(forEntityName: "Student", into: DatabaseController.getContext()) as! Student
//To get firstName, lastName and age access the item
student.firstName = item.firstName
student.lastName = item.lastName
student.age = item.age
}
//Save context now
DatabaseController.saveContext()
In case someone is interested, I found the solution:
You first have to set up the struct in the CoredataEntity Class like that:
import Foundation
import CoreData
struct StudentsStruct {
let firstName: String
let lastName: String
let age: Int
}
#objc(Student)
public class Student: NSManagedObject {
#NSManaged public var firstName: String?
#NSManaged public var lastName: String?
#NSManaged public var age: Int16
var allAtributes : StudentsStruct {
get {
return StudentsStruct(firstName: self.firstName!, lastName: self.lastName!, age: Int(self.age))
}
set {
self.firstName = newValue.firstName
self.lastName = newValue.lastName
self.age = Int16(newValue.age)
}
}
}
Then use the same struct to paste the data:
import Cocoa
import CoreData
class ViewController: NSViewController {
let studentsdata: [StudentsStruct] = [StudentsStruct(firstName: "Albert", lastName: "Miller", age: 24), StudentsStruct(firstName: "Susan", lastName: "Gordon", age: 24), StudentsStruct(firstName: "Henry", lastName: "Colbert", age: 24)]
override func viewDidLoad() {
super.viewDidLoad()
for items in studentsdata {
let student: Student = NSEntityDescription.insertNewObject(forEntityName: "Student", into: DatabaseController.getContext()) as! Student
student.allAtributes = items
}
DatabaseController.saveContext()
let fetchRequest: NSFetchRequest<Student> = Student.fetchRequest()
do {
let searchResults = try DatabaseController.getContext().fetch(fetchRequest)
print("number of results: \(searchResults.count)")
for result in searchResults as [Student] {
print("student: \(firstName), \(lastName), \(age)" )
}
} catch {
print ("error: \(error)")
}
}
}
Thats it.
Perhaps this is lazy. You could also just encode your array as a json object and then create a field on your NSManagedObject for it as a transformable. When you want to retrieve you'd just decode and downcast to the proper type. That's what I did in one of my projects; worked fine.