Realm object predicate filter is invalid - swift

I use predicate to filter the converstions of models. I got the wrong result with Realm filter (0 models though there should be one). After that, I did another check and she showed me that there is one model with these criteria. Please tell me what may be wrong here.
let checkConversations = Array(realm.objects(ChatConversationModel.self)).filter({ $0.lastMessage != nil && $0.toDelete == false })
debugPrint("checkConversations", checkConversations.count) received one model (this is the right result).
var conversations = Array(realm.objects(ChatConversationModel.self).filter("lastMessage != nil && toDelete == false"))
debugPrint("conversations", conversations.count) I did not receive any models at all
Models:
class ChatConversationModel: Object, Mappable {
/// oneToOne = friend
enum ChatType {
case oneToOne, group, nonFriend
var index: Int {
switch self {
case .oneToOne:
return 0
case .group:
return 1
case .nonFriend:
return 2
}
}
}
#objc dynamic var id = ""
#objc dynamic var typeIndex = ChatType.oneToOne.index
#objc dynamic var lastMessage: ChatMessageRealmModel?
#objc dynamic var lastActivityTimeStamp = 0.0
#objc dynamic var modelVersion = AppStaticSettings.versionNumber
let createTimestamp = RealmOptional<Double>()
// for group chat
#objc dynamic var groupConversationOwnerID: String?
/// for group chat equal card photos
#objc dynamic var cardInfo: ChatConversationCardInfoModel?
// Local
#objc dynamic var toDelete = false
override class func primaryKey() -> String? {
return "id"
}
convenience required init?(map: Map) {
self.init()
}
func mapping(map: Map) {
if map.mappingType == .fromJSON {
id <- map["id"]
typeIndex <- map["typeIndex"]
lastMessage <- map["lastMessage"]
lastActivityTimeStamp <- map["lastActivityTimeStamp"]
createTimestamp.value <- map["createTimestamp"]
modelVersion <- map["modelVersion"]
// for group chat
cardInfo <- map["cardInfo"]
groupConversationOwnerID <- map["groupConversationOwnerID"]
} else {
id >>> map["id"]
typeIndex >>> map["typeIndex"]
lastMessage >>> map["lastMessage"]
lastActivityTimeStamp >>> map["lastActivityTimeStamp"]
createTimestamp.value >>> map["createTimestamp"]
modelVersion >>> map["modelVersion"]
// for group chat
cardInfo >>> map["cardInfo"]
groupConversationOwnerID >>> map["groupConversationOwnerID"]
}
}
}
Update: When I receive actual conversations, I start comparing which already exists in the application. And looking for ids which are not in the result. Next, I find these irrelevant conversations and put toDelete = false, in order to "safely" delete inactive conversations. Since I listened to a podcast with one of Realm's developers, he advised not to delete an object that can be used. And since when receiving results with backend, any inactive conversation can be active again, so I chose this method. You can view the code for these functions.
private func syncNewConversations(_ conversations: [ChatConversationModel], userConversations: [ChatUserPersonalConversationModel], completion: ((_ error: Error?) -> Void)?) {
DispatchQueue.global(qos: .background).async {
let userConversationsIDs = userConversations.map { $0.id }
DispatchQueue.main.async {
do {
let realm = try Realm()
let userConversationPredicate = NSPredicate(format: "NOT id IN %#", userConversationsIDs)
let notActualUserConversations = realm.objects(ChatUserPersonalConversationModel.self).filter(userConversationPredicate)
let filteredNotActualUserConversations = Array(notActualUserConversations.filter({ $0.lastActivityTimeStamp < $0.removedChatTimeStamp }))
let notActualConversationIDs = filteredNotActualUserConversations.map { $0.id }
let notActualConversationPredicate = NSPredicate(format: "id IN %#", notActualConversationIDs)
let notActualConversations = realm.objects(ChatConversationModel.self).filter(notActualConversationPredicate)
let notActualMessagesPredicate = NSPredicate(format: "conversationID IN %#", notActualConversationIDs)
let notActualMessages = realm.objects(ChatMessageRealmModel.self).filter(notActualMessagesPredicate)
try realm.write {
realm.add(userConversations, update: true)
realm.add(conversations, update: true)
for notActualUserConversation in notActualUserConversations {
notActualUserConversation.toDelete = true
}
for notActualConversation in notActualConversations {
notActualConversation.toDelete = true
}
for notActualMessage in notActualMessages {
notActualMessage.toDelete = true
}
completion?(nil)
}
} catch {
debugPrint(error.localizedDescription)
completion?(error)
}
}
}
}

Related

Combine array of object from different endpoint so it can also pagination

I have 2 different endpoint:
The first one have a pagination.
The second one doesn't have pagination
I mapping the object from the first and second endpoint so they have the same object when i display it and limit only 10 item.
The Question is..
Is that possible to combine the API called so i can use pagination with different endpoint? so the result is
Merge the object into 1
Sort by date
Limit the item only 10 item
So far i can't figure it out how to combine an API, this is my service setup
func getMessageList(page: Int) -> Single<[Message]> {
return platformBanking.getMessageList(token: sessionStore.token, page: page, pageSize: 10)
}
func getMoInbox() -> Single<[Message]> {
return Single.create { single in
MOInbox.sharedInstance.getInboxMessages { inboxMessages, accountMeta in
var messages: [Message] = []
inboxMessages.forEach { entry in
let message: Message = .init()
message.title = entry.notificationTitle
message.subtitle = entry.notificationSubTitle
message.body = entry.notificationBody
message.messageType = !(entry.campaignID?.isEmpty ?? false) ? 5 : 1
message.imageName = entry.notificationMediaURL ?? ""
message.date = entry.receivedDate?.string(withFormat: "dd MMM") ?? ""
message.isRead = entry.isRead
message.action = entry.deepLinkURL ?? ""
messages.append(message)
}
single(.success(messages))
}
return Disposables.create()
}
}
This is in my ViewModel
var filterMessages: [Message] = []
private var page: Int = 1
private var isLoading: Bool = false
private var endOfMessage: Bool = false
private func getMessageInboxList() {
var inboxMessages: [Message] = []
isLoading = true
Single.zip(manageMessages.getMessageList(page: page), manageMessages.getMoInbox())
.subscribe(onSuccess: { [weak self] firstMessage, secondMessage in
inboxMessages.append(contentsOf: firstMessage)
inboxMessages.append(contentsOf: secondMessage)
self?.processMessages(messages: inboxMessages)
}).disposed(by: disposedBag)
}
private func processMessages(messages: [Message]) {
self.messages.append(contentsOf: messages)
self.filterMessages = self.messages.sorted(by: { $0.date > $1.date })
eventShowHideLoadingIndicator.onNext(false)
if messages.count < 10 {
endOfMessage = true
}
eventMessagesDataUpdated.onNext(())
isLoading = false
}
This is a function to called pagination in viewModel, when i try paginate i just realize i make a duplicate item from getMoInbox API called. but still combining the object and limiting by 10 item i still can't find the answer.
func loadMoreMessageInbox() {
guard !endOfMessage, !isLoading, selectedIndex == 0 else { return }
page = page + 1
getMessageInboxList()
}
Please help me guys.
This requires a state machine. There are a number of different libraries that you could use (a partial list is at the bottom.)
Here is an example using the cycle function from my library.
enum Input {
case nextPageRequested // emit this to `input` when you are ready for the next page.
case pageReceived(Int, [Message]) // this is emitted with the page results.
}
struct State<T> {
var pages: [Int: [T]] = [:] // stores the pages as they come in. The MoMessages will be stored in page 0
}
func example(input: Observable<Input>, messageManager: MessageManager) -> Observable<[Message]> {
Single.zip(messageManager.getMoInbox(), messageManager.getMessageList(page: 1))
.asObservable()
.flatMap { moMessages, page1Messages in
// create state machine initialized with the moMessages and page1Messages
cycle(
input: input,
initialState: State(pages: [0: moMessages, 1: page1Messages]),
reduce: { state, input in
// when a new page is received, store it
if case let .pageReceived(page, messages) = input {
state.pages[page] = messages
}
},
reaction: reaction(
request: { state, input in
// when a new page is requested, figure out what page number you need and return it (otherwise return nil)
guard case .nextPageRequested = input else { return nil }
return state.pages.keys.max() + 1
},
effect: { page in
// get the page you need
messageManager.getMessageList(page: page)
.map { Input.pageReceived(page, $0) }
.asObservable()
}
)
)
}
.map { state in
// sort the messages in the pages and return them
state.pages.values.flatMap { $0 }.sorted(by: { $0.date > $1.date })
}
}
Here's that promised list:
My CLE library contains a state machine system.
RxFeedback is the OG tool, developed by the initial designer of RxSwift.
RxState is part of the RxSwiftCommunity.

Filter Realm Results array returns elements that should be filtered

Any ideas why this filter is not working correctly ?
for item in activeItems {
print("item.product: \(item.product), \(item.spaceRequired)")
}
returns
item.product: nil, 40.0
Filtering where product is nil
let f1 = activeItems.filter{$0.product != nil}
print("f1: \(f1)")
print("f1.count: \(f1.count)")
returns a count of ZERO but the array still appears to contain an item
f1: LazyFilterSequence<Results<AssortmentItem>>(_base: Results<AssortmentItem> <0x109ce2c90> (
[0] AssortmentItem {
...
f1.count: 0
And then filtering and mapping just spaceRequired
let f11 = f1.filter{$0.product!.isProduct == true}.map({$0.spaceRequired})
print("f11: \(f11)")
returns the same array with a single item
f11: LazyMapSequence<LazyFilterSequence<Results<AssortmentItem>>, Double>(_base: Swift.LazyFilterSequence<RealmSwift.Results<Merchandise_Manager.AssortmentItem>>(_base: Results<AssortmentItem> <0x109ce2c90> (
[0] AssortmentItem {
And then trying to reduce crashes
let w = f11.reduce(0,+)
This seems to fix the problem
let width = Array(activeItems.filter{$0.product != nil}).filter{$0.product!.isProduct == true}.map({$0.spaceRequired}).reduce(0,+)
Is this a bug in Swift 5 or in Realm ?
EDIT: It looks like this is a bug in Realm's handling of things.
To be a bit cleared below is a more complete set of the Realm objects.
import Foundation
import RealmSwift
let activeDate: NSDate = Date() as NSDate
let defaultWidth: Double = 40.0
class MyObject: Object {
#objc dynamic var number: Int = 0
#objc dynamic var name: String?
let items = List<ChildObject>()
}
extension MyObject {
var activeItems: Results<ChildObject> {
let activeDate = activeDate // Some globally defined value
let active = items.filter("startDate <= %# && (endDate >= %# || endDate == nil)", activeDate, activeDate).sorted(byKeyPath: "number")
return active
}
/// Works Correctly
var totalWidth: Double {
let width = Array(activeItems.filter{$0.product != nil}).filter{$0.product!.isProduct == true}.map({$0.spaceRequired}).reduce(0,+)
let width2 = Array(activeItems.filter{$0.product == nil}.map({$0.spaceRequired})).reduce(0,+)
return width+width2
}
/// Crashes
var totalWidth: Double {
let width = activeItems.filter{$0.product != nil}.filter{$0.product!.isProduct == true}.map({$0.spaceRequired}).reduce(0,+)
let width2 = activeItems.filter{$0.product == nil}.map({$0.spaceRequired}).reduce(0,+)
return width+width2
}
}
class ChildObject: Object {
#objc dynamic var parent: MyObject?
#objc dynamic var number: Int = 0
#objc dynamic var product: Product?
#objc dynamic var name: String?
#objc dynamic var spaceRequired: Double = 40.0
#objc dynamic var startDate: NSDate?
#objc dynamic var endDate: NSDate?
}
extension ChildObject {
var spaceRequired: Double {
if let p = product {
return p.width
} else {
return defaultWidth
}
}
}
class Product: Object {
#objc dynamic var isProduct: Bool = false
#objc dynamic var width: Double = 30.0
}
There's a couple of issues at work here but the main problem is that Realm Results are live updating; while you can filter data using the Swifty
let f1 = activeItems.filter{$0.product != nil}
it's going to give intermittent results since Realm doesn't know which items are filtered or not as .filter { is not a Realm function and Realm won't know what to update within the results.
You should generally use the built in Realm filtering mechanism
let results = realm.objects(ItemClass.self).filter("product != nil")
Those results will be live updating - if an object leaves the filter parameter, the results follow that. If an object matches the filter the results are updated as well.
I believe this Github issue #2138 provides some more light on the issue.
If you absolutely need static data, then I would suggest extending the Results class to return an array; like this
extension Results {
func toArray() -> [Element] {
return compactMap { $0 }
}
}
Keeping in mind this will use more memory as Realm objects are lazy loaded and and array isn't.
EDIT:
There's some additonal information in the question so I crafted up a simple example trying to replicate the issue. There's a HouseClass object which contains a List of RoomClass objects and then the HouseClass is extended to return the total width of all of the rooms in its list.
class RoomClass: Object {
#objc dynamic var room_name = ""
#objc dynamic var width = 0
#objc dynamic var length = 0
#objc dynamic var belongs_to_house: HouseClass!
}
class HouseClass: Object {
#objc dynamic var house_id = NSUUID().uuidString
#objc dynamic var house_name = ""
let rooms = List<RoomClass>()
override static func primaryKey() -> String? {
return "house_id"
}
}
extension HouseClass {
var totalWidth: Int {
let width = Array(rooms).map {$0.width}.reduce(0,+)
return width
}
var anotherTotalWidth: Int {
let width = rooms.map {$0.width}.reduce(0,+)
return width
}
}
and then the code to get all of the houses and output their room widths based on two different functions (see the HouseClass extension)
let houseResults = realm.objects(HouseClass.self)
for house in houseResults {
let w0 = house.totalWidth
print(w0)
let w1 = house.anotherTotalWidth
print(w1)
}
I added 100 houses each with three rooms and ran the above code several times without crash.
Count of f1 is 0 so map is not worked.
You can optimize your width calculation as following
let width = activeItems
.filter { $0.product?.isProduct ?? false }
.map { $0.spaceRequired }
.reduce(0,+)

How to instantiate a mapped class? (swift - alamofireObjectMapper)

I have this mapped class caled Movie and I make an API request that returns me this type. How can I instantiate this class with the values of my API response?
Movie mapped class:
class Movie: Mappable {
var posterURL : String?
var title : String?
var runtime : String?
var director : String?
var actors : String?
var genre : String?
var plot : String?
var production : String?
var released : String?
var year : String?
var imdbID : String?
var imdbRating : String?
required init?(map: Map) {
}
func mapping(map: Map) {
posterURL <- map["Poster"]
title <- map["Title"]
runtime <- map["Runtime"]
director <- map["Director"]
actors <- map["Actors"]
genre <- map["Genre"]
plot <- map["Plot"]
production <- map["Production"]
released <- map["Released"]
year <- map["Year"]
imdbID <- map["imdbID"]
imdbRating <- map["imdbRating"]
}
}
And in my MovieViewController I'm making the API call and passing the values for my outlet label.
But I would like to instantiate this class by assigning the values ​​obtained in my API call.
func getMovieById() {
let requestURL = "https://www.omdbapi.com/?i=\(String(describing: imdbID!))"
print("URL: \(requestURL)")
Alamofire.request(requestURL).responseObject{ (response: DataResponse<Movie>) in
print("|MovieController| Response is: \(response)")
DispatchQueue.main.async {
let spinnerActivity = MBProgressHUD.showAdded(to: self.view, animated: true)
spinnerActivity.label.text = "Loading";
spinnerActivity.isUserInteractionEnabled = false;
}
let movie = response.result.value
if let posterURL = movie?.posterURL {
print("Poster URL: \(posterURL)")
let imgStg: String = posterURL
print("---> Image string: \(imgStg)")
let imgURL: URL? = URL(string: imgStg)
let imgSrc = ImageResource(downloadURL: imgURL!, cacheKey: imgStg)
self.movPosterImageView.layer.cornerRadius = self.movPosterImageView.frame.size.width/2
self.movPosterImageView.clipsToBounds = true
//image cache with KingFisher
self.movPosterImageView.kf.setImage(with: imgSrc)
}
if let title = movie?.title {
print("Title: \(title)")
self.movTitleLabel.text = title
}
if let runtime = movie?.runtime {
print("Runtime: \(runtime)")
self.movRuntimeLabel.text = runtime
}
if let genre = movie?.genre {
print("Genre: \(genre)")
self.movGenreLabel.text = genre
}
if let plot = movie?.plot {
print("Plot: \(plot)")
self.movPlotTextView.text = plot
}
if let rating = movie?.imdbRating {
print("Rating: \(rating)")
self.movRatingLabel.text = rating
}
if let director = movie?.director {
print("Director: \(director)")
self.movDirectorLabel.text = director
}
if let production = movie?.production {
print("Production: \(production)")
self.movProductionLabel.text = production
}
if let actors = movie?.actors {
print("Actors: \(actors)")
self.movActorsLabel.text = actors
}
if let released = movie?.released {
print("Released in: \(released)")
self.movReleasedLabel.text = released
}
DispatchQueue.main.async {
MBProgressHUD.hide(for: self.view, animated: true)
}
}//Alamofire.request
}//getMovieByID()
It would be something like
let movieDetails: Movie = Movie(plot = movie?.plot, title = movie?.title, ...)
How can I do this with a mappable class?
Update
I'm trying to organize this things and also I'll have to reuse code, so did this inside functions seems better for me. So, I started separating the API call putting like this:
file: OMDB.swift
import Foundation
import Alamofire
import AlamofireObjectMapper
func getMovieIdFromAPI(imdbID: String, completionHandler: #escaping (Movie) -> () ) {
let requestURL = "https://www.omdbapi.com/?i=\(imdbID)"
print("|getMovieIdFromAPI| URL: \(requestURL)")
Alamofire.request(requestURL).responseObject{ (response: DataResponse<Movie>) in
print("|Alamofire request| Response is: \(response)")
if let movieResult = response.result.value{
completionHandler(movieResult)
}
}
}
Next step, I'm trying to create a MovieDAO, and here I'll have to instantiate my object, right? So, in the same file as my Movie class is, I've created a MovieDAO class with this function:
class MovieDAO {
func getMovieDetailed<Movie: Mappable>(imdbID: String, completionHandler: #escaping (Movie) -> ()) {
getMovieIdFromAPI(imdbID: imdbID, completionHandler: {
(movieResult) in
let mapper = Mapper<Movie>()
let movieDetailed = mapper.map(movieResult)!
completionHandler(movieDetailed)
})
}
}
But I didn't understood very well the answer and the xcode gives me an error in
let movieDetailed = mapper.map(movieResult)!
^Error: Argument labels '(_:)' do not match any available overloads
Could you explain how can I use the answer given in this case?
ObjectMapper is what helps you get an instance of the model class, with the property values set as per your API response. You will need to do the last step where in you tell ObjectMapper to do the 'mapping' procedure with the json you provide it.You can use this generic method to parse response for any Mappable class
static func parseModel<Model: Mappable>(modelResponse modelResponse: AnyObject, modelClass: Model.Type) -> Model? {
let mapper = Mapper<Model>()
let modelObject = mapper.map(modelResponse)!
return modelObject
}

how to filter data from object mapper class

i want to implement search functionality in my app but i get data from services. i have an array like this in object mapper
class Country : Mappable {
var countryName:String = ""
var countryID:Int = 0
var countryImage:String = ""
var countryColor:String = ""
required init?(_ map: Map) {
}
func mapping(map: Map) {
countryID <- map["id"]
countryName <- map["name"]
countryColor <- map["color"]
countryImage <- map["image"]
}
}
from here i want to filter my data for search functionality how to do this.
here i am filtering only country names but i want to filter whole array how i can do that
func updateSearchResultsForSearchController(searchController: UISearchController) {
self.filteredData = self.countryNames.filter { (country:String) -> Bool in
if country.lowercaseString.containsString(self.searchController.searchBar.text!.lowercaseString) {
return true
} else {
return false
}
}
print(filteredData)
// update results table view
self.resultController.tableView.reloadData()
}
You can filter your array like this way.
let filter = countries.filter { $0.countryName.lowercaseString.containsString(self.searchCon‌​troller.searchBar.te‌​xt!.lowercaseString) }
self.resultController.tableView.reloadData()

ObjectMapper not serialising new fields

I have an class:
class ChatMessage: Object, Mappable {
dynamic var fromId = ""
dynamic var toId = ""
dynamic var message = ""
dynamic var fromName = ""
dynamic var created: Int64 = 0
required convenience init?(map: Map) {
self.init()
}
func configure(_ fromId:String,toId:String, message:String) {
self.fromId=fromId
self.toId=toId
self.message=message
self.created = Int64((NSDate().timeIntervalSince1970 * 1000.0))
}
func mapping(map: Map) {
created <- map["created"] //a: this was added later
fromId <- map["fromId"]
toId <- map["toId"]
message <- map["message"]
fromName <- map["fromName"]
}
}
I am using ObjectMapper to serialise the object to JSON and Realm to store it in the local database.
I had added the created field later to the mapping when the Realm db was already storing the ChatMessage object.
Now when I am instantiating the ChatMessage object and trying to convert it into JSON object using ObjectMapper. Following is the code:
func sendChatMessage(_ chatMessage:ChatMessage, callback: #escaping DataSentCallback) -> Void {
var chatMessageString:String!
let realm = DBManager.sharedInstance.myDB
try! realm?.write {
chatMessageString = Mapper().toJSONString(chatMessage, prettyPrint: false)!
}
...
}
Now when I print chatMessage, I get:
ChatMessage {
fromId = 14;
toId = 20;
message = 2;
fromName = ;
created = 1477047392597;
}
And when I print chatMessageString, I get:
"{\"toId\":\"20\",\"message\":\"2\",\"fromName\":\"\",\"fromId\":\"14\"}"
How come does the created field not appear in the string?
The problem was in the mapping of Int64 type as mentioned in this issue on github.
By changing the mapping of created to the following form, everything worked fine:
created <- (map["created"], TransformOf<Int64, NSNumber>(fromJSON: { $0?.int64Value }, toJSON: { $0.map { NSNumber(value: $0) } }))