Speed up UISearchController with threads - ios

I have a Greek dictionary app that searches tens of thousands of word entries from Sqlite to fill a UITableView. I'm encountering many of the same problems outlined here (keyboard lag as search text changes & needing to abort search and rerun if search text changes), and the solution provided works great.
My code:
func updateSearchResultsForSearchController(searchController: UISearchController) {
if (self.resultSearchController.searchBar.text == "") {
theResultsArray.removeAll(keepCapacity: true)
searchTable.reloadData()
return
}
self.theResultsArray = filter(self.wordArray) {
$0[self.betaNoSymbols].rangeOfString(self.resultSearchController.searchBar.text) != nil
// ($0[self.betaCode] as NSString).containsString(self.resultSearchController.searchBar.text)
}
searchTable.reloadData()
}
In the current example, I'm searching only one field but I'd like to search multiple, the Greek word with and without diacritics, and beta code (transliterated english) with and without symbols. I'm wondering if it'd be faster to query Sqlite or first place the word list in an Array, and if there's another solution with Swift and iOS 8. If I should adapt the code, help would be appreciated.
Update:
I've put the search in an async queue and it seems to be working. Using an array filter, the search for some reason stopped working after a bunch of edits to the UISearchBar, so it's now using Sqlite.
let searchQueue = dispatch_queue_create("searchQueue", DISPATCH_QUEUE_SERIAL)
func updateSearchResultsForSearchController(searchController: UISearchController) {
if (self.resultSearchController.searchBar.text == "") {
theResultsArray.removeAll(keepCapacity: true)
searchTable.reloadData()
println("Search field empty.")
return
}
let searchText = self.resultSearchController.searchBar.text
var tempArray = [Row]()
dispatch_async(searchQueue) {
// abort if text changed
if searchText != self.resultSearchController.searchBar.text {
println("Search changed, aborted.")
return
}
// do search
if resultSearchController.searchBar.selectedScopeButtonIndex == 0 {
tempArray = Array(self.liddellQuery.select(self._id, self.greekFullWord).filter(
like("%\(self.resultSearchController.searchBar.text!)%", self.betaNoSymbols)
|| like("%\(self.resultSearchController.searchBar.text!)%", self.betaSymbols)
|| like("%\(self.resultSearchController.searchBar.text!)%", self.greekFullWord)
|| like("%\(self.resultSearchController.searchBar.text!)%", self.greekLowercase)
|| like("%\(self.resultSearchController.searchBar.text!)%", self.greekNoSymbols) println("Searched: \(self.resultSearchController.searchBar.text), results: \(tempArray.count)")
} else {
...
}
// update if search still current
if searchText == self.resultSearchController.searchBar.text {
dispatch_async(dispatch_get_main_queue()) {
self.theResultsArray = tempArray
self.searchTable.reloadData()
}
}
}
}
I'm not sure if as is is the best way. There is also a long delay between searches and an UIIndicatorView to replace the search icon would be helpful.

Related

How to make sure a string of pure whitespace is invalid with a succinct code in Swift

I am making a section with TextFields and Button("Continue") and then use .disable(isValidAddress) modifier to a whole section to disable the button. The code works well, but I am seeking any solution to make it more succinct with no need to write .hasPrefix() or .hasSuffix() to all parameters one by one.
var isValidAddress: Bool {
if name.hasPrefix(" ") || street.hasPrefix(" ") || city.hasPrefix(" ") || country.hasPrefix(" ") {
return false
} else if name.hasSuffix(" ") || street.hasSuffix(" ") || city.hasSuffix(" ") || country.hasSuffix(" ") {
return false
}
return true
}
var isValidAddress: Bool {
[street, name, city, etc..].reduce(true, { result, text in
if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return false
} else {
return result
}
})
}
You can add them to an array and trim white space and check if empty in a loop
func isValidAddress() -> Bool {
for field in [name, street, city, country] {
if field.trimmingCharacters(in: .whitespaces).isEmpty { return false }
}
return true
}
I used a function here but a computed property works just as well.
If you don't mind moving some complexity into extensions, you can tidy up the call site. This makes it more readable, and less error prone - you can easily add in another field without mistakes for example.
extension String {
func hasPrefixOrSuffix(_ s: String) -> Bool {
hasPrefix(s) || hasSuffix(s)
}
var noSpacesAtEnds: Bool {
!hasPrefixOrSuffix(" ")
}
}
let isValid = [name, street, city, country]
.allSatisfy(\.noSpacesAtEnds)
If you do mean none of them are "all whitespace strings" then:
extension String {
var isNotAllWhitespace: Bool {
!trimmingCharacters(in: .whitespaces).isEmpty
}
}
let isValid = [name, street, city, country]
.allSatisfy(\.isNotAllWhitespace)
One benefit of having named functions for these things is you can test the pieces, so you can write a test to ensure that isNotAllWhitespace works the way you expect. You couldn't if the logic like name.hasPrefix(" ") || street.hasPrefix(" ") is mixed in with your isValidAddress function.

How to remove duplicate object in array?

I have a VC where I fetch data for only the 1st object in an array of objects. so I only fetch arrayOfObjects[0] then when I enter a second VC I need to fetch all the other data associated with the other objects in that same array. But then I have a problem where I end up fetching that first bit of data I already had again. So my array would look like, data1, data1, data2, data3 ... which is not what I want of course.
Currently what I had tried to fix this issue, was to do the following: MainObject?.arrayOfSubObjects.remove(at: 0), this however means that on the first go it works well, but every time I go back to the preceding VC and then back I subtract one of the objects that I want to be there. So I end up with: data2, data3 ...
So my question is how can I remove that extra object from the beginning, but not delete anything after its been deleted?
Some things i have tried:
if selectedPost?.media[0].videoURL != nil {
if selectedPost?.media[0].videoURL == selectedPost?.media[1].videoURL {
selectedPost?.media.remove(at: 0)
} else {
print("NO!!!!!! they are not the same ")
}
} else if selectedPost?.media[0].image != nil {
if selectedPost?.media[0].image == selectedPost?.media[1].image {
selectedPost?.media.remove(at: 0)
} else {
print("NO!!! they are not the same ")
}
}
This however does not do anything, it always ends up going into the else. I have also tried stuff like setting number schemes, but this failed because the VC kept reloading
You can try to do it like this:
guard let selectedPost = selectedPost else { return }
if selectedPost.media.contains(where: {$0.image == image}) { return } // Here the $0.image should be replaced with $0.mediaURL == mediaURL for the videos or however you videoURL is called in Media
selectedPost.media.append(Media(image: image, timeStamp: Double(timeStamp)))
self.tableView.reloadData()
Try conforming Hashable in you Media class:
class Media : Hashable {
var hashValue : Int {
return image.hashValue
}
var image = UIImage()
// .....
static func == (lhs: Media, rhs: Media) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
Then you can compare two media objects with ==, So you can do something similar to the top part of this answer. Good luck

How to identify object that is deleted from a collection (array)?

I have following code to handle pull to refresh response
let copyData = data.reversed() // it is pull to refresh response (load page 1)
for (_,element) in copyData.enumerated() {
let foundElement = allObjects.filter{$0.id == element.id} // Find element in main array
if let firstElement = foundElement.first, let index = allObjects.index(of: firstElement) {
allObjects[index] = element // Replace if found
} else {
allObjects.insert(element, at: 0) // Insert if not found
}
}
self.arrayPosts = allObjects
Where data is codable class which is API response of pull to refresh. allObjects is preloaded data with pagination
Question : Suppose In allObjects i have 50 Object (5 Pages of 10 ID is (1 to 50))
User pull to refresh And I load first Page from API (ID 1,2,3,4,5,6,7,10,11) then how to identify which object is deleted (8,9) ?
Should I compare allObjects 's 10th index with data's 10th index object's ID ?
Is it better way to handle this ? Please suggest
Don't compare pages (i.e. 10 items at a time) - if an item is added/deleted the pages will get out of sync, and you'll end up with missing / duplicate objects.
Presumably your objects are sorted by some key / date, etc.
Take the value of the key in your last downloaded object.
Copy all the existing objects with keys <= that last key into a new array.
Compare your downloaded array against this sub-array.
Objects in the downloaded array that are not in the sub-array should be removed.
Here How I handle this. Code is quite complex for first time read but added comments to understand it
func handleResponse(page:Int,isForRefersh:Bool = false, data:Array<InspirePost>) {
guard data.count != 0 else {
// Check if we are requesting data from pull to referesh and First page is empty then we don't have data to show change state to empty
if self.arrayPosts.count == 0 || (isForRefersh && page == 1) {
self.state = .empty
self.tableView.reloadData()
} else {
self.state = .populated(posts: self.arrayPosts)
}
return
}
// Now we need to check if data called by referesh control then
//1) Replace object in array other wise just append it.
var allObjects = self.state.currentPost
if isForRefersh {
// If both array Has same number of element i.e both has page one loaded
if data.count >= allObjects.count {
allObjects = data
} else {
let copyData = data.reversed()
for (_,element) in copyData.enumerated() {
if let index = allObjects.firstIndex(where: {$0.id == element.id}) {
allObjects[index] = element // Replace if found
} else {
allObjects.insert(element, at: 0) // Insert if not
}
}
let minID = data.min(by: {$0.id ?? 0 < $1.id ?? 0})?.id
// DELETE item
let copyAllObject = allObjects
for (_,element) in copyAllObject.enumerated() {
guard let id = element.id, id >= minID ?? 0 else {
continue
}
if !data.contains(element) {
if let indexInMainArray = allObjects.index(where: {$0.id == id}) {
allObjects.remove(at: indexInMainArray)
}
}
}
}
//When we pull to refersh check the curent state
switch self.state {
case .empty,.populated : // if empty or populated set it as populated (empty if no record was avaiable then pull to refersh )
self.state = .populated(posts: allObjects)
case .error(_, let lastState) : // If there was error before pull to referesh handle this
switch lastState {
case .empty ,.populated: // Before the error tableview was empty or popluated with data
self.state = .populated(posts: allObjects)
case .loading,.error: // Before error there was loading data (There might more pages if it was in loading so we doing paging state ) or error
self.state = .paging(posts: allObjects, nextPage: page + 1)
case .paging(_,let nextPage): // Before error there was paging then we again change it to paging
self.state = .paging(posts: allObjects, nextPage: nextPage)
}
case .loading: // Current state was loading (this might not been true but for safety we are adding this)
self.state = .paging(posts: allObjects, nextPage: page + 1)
case .paging(_,let nextPage): // if there was paging on going don't break anything
self.state = .paging(posts: allObjects, nextPage: nextPage)
}
self.arrayPosts = allObjects
} else {
allObjects.append(contentsOf: data)
self.isMoreDataAvailable = data.count >= self.pageLimit
if self.isMoreDataAvailable {
self.state = .paging(posts: allObjects, nextPage: page + 1)
} else {
self.state = .populated(posts: allObjects)
}
self.arrayPosts = self.state.currentPost
}
self.tableView.reloadData()
}
Where I have
indirect enum PostListStatus {
case loading
case paging(posts:[InspirePost],nextPage:Int)
case populated(posts:[InspirePost])
case error (error:String,lastState:PostListStatus) // keep last state for future if we need to know about data or state
case empty
var currentPost:[InspirePost] {
switch self {
case .paging(let posts , _):
return posts
case .populated( let posts):
return posts
case .error( _, let oldPost) :
switch oldPost {
case .paging(let posts , _):
return posts
case .populated( let posts):
return posts
default:
return []
}
default:
return []
}
}
var nextPage : Int {
switch self {
case .paging(_, let page):
return page
default:
return 1
}
}
}
You may make set for both allObjects and data. Then use subtracting(_:) method in set to find the missing one.
Remove those missing ones from the main array and use it. Once you have correct main array elements, page them while displaying.

rac_textSignal in Swift 3

I am attempting to migrate a Swift 2.2 app that I did not write to Swift 3. All was going well until I ran into some RAC calls. I am not familiar with ReactiveCocoa, which makes this even more challenging.
/**
* Method name: Enable Login Button
* Description: Enable or disable login button based on
* username and password entry
* Parameters: None
*/
func enableLoginButton() -> Void {
self.btnLogin.isEnabled = false
let serviceUrlSignal = self.txtServiceUrl.rac_textSignal()
.toSignalAssumingHot()
.assumeNoErrors()
.map { text in text as! String }
let usernameSignal = self.txtFieldUsername.rac_textSignal()
.toSignalAssumingHot()
.assumeNoErrors()
.map { text in text as! String }
let userNameCharSignal = usernameSignal.map { $0.characters.count }
let passwordSignal = self.txtFieldPassword.rac_textSignal()
.toSignalAssumingHot()
.assumeNoErrors()
.map { text in text as! String }
let passwordCharSignal = passwordSignal.map { $0.characters.count }
userNameCharSignal.combineLatestWith(passwordCharSignal)
.map
{
charArray in charArray.0 > 0 && charArray.1 > 0
}
.observe { event in
if let buttonEnabled = event.value {
self.btnLogin.enabled = buttonEnabled
}
}
viewModel.username <~ usernameSignal
viewModel.password <~ passwordSignal
viewModel.serviceUrl <~ serviceUrlSignal
btnLogin.rac_signalForControlEvents(.TouchUpInside)
.subscribeNext{
button in
print("Authenticate Click!")
}
}
From what I understand, rac_textSignal does not exist in ReactiveSwift. Do I need to install ReactiveObjC or is there a Swift approach that I could use to replace this functionality? Thanks.
You'll need to add ReactiveCocoa in addition to ReactiveSwift.
Since the repo split, the core of what was ReactiveCocoa is now implemented in ReactiveSwift, but ReactiveCocoa adds Cocoa specific things like UIKit bindings which you'll need.
UIKit bindings are accessible via the reactive property, e.g. textfield.reactive.continuousTextValues.
Since these bindings are already correctly typed (as opposed to rac_textSignal), you can replace the whole chunk of
let usernameSignal = self.txtFieldUsername.rac_textSignal()
.toSignalAssumingHot()
.assumeNoErrors()
.map { text in text as! String }
...
viewModel.username <~ usernameSignal
with
viewModel.username <~ self.txtFieldUsername.reactive.continuousTextValues

generic tap function for XCUIApplication

We are trying to migrate from UIAutomation to XCUITests.
For the UIAutomation we came up with a handy 'tapOnName' function which just crawled thru a whole sub element tree and tapped on the element with the first match.
function log(msg) {
UIALogger.logDebug(msg);
}
//recursive function crawling thru an elements hierarchy
//and tapping on the first match of accessibilityIdentifier
//or button text
function tapOnNameWithRoot(name,el) {
if (el.name()==name && el.isVisible()) {
log("tap on itt!!!")
el.tap();
return true;
}
if (el.toString()=="[object UIAButton]" && el.label()==name) {
log("tap on Button!!!")
el.tap();
return true;
}
var elements=el.elements();
if (elements===null || elements===undefined) {
log("elements null or undefined for:"+el.toString());
return false;
}
for(var i=0,len=elements.length ;i<len;i++) {
if (tapOnNameWithRoot(name,elements[i])) {
return true;
}
}
return false;
}
var win = UIATarget.localTarget().frontMostApp().mainWindow();
//for ex taps on a button with the text "pushme" in the
//main UIWindow
tapOnNameWithRoot("pushme",win);
No the question : is it possible to implement the same function using XCUIApplication ?
There is shorthand support for this function in XCTest.
For tapping the first match out of any element, you can get all elements and tap the first one:
let app = XCUIApplication()
let element = app.descendentsMatchingType(.Any)["someIdentifier"]
element.tap()
If you know what type of element it is going to be, it's better to filter by that type first:
let app = XCUIApplication()
let element = app.buttons["someIdentifier"]
element.tap()
Are you looking for something like this:
func tapBasedOnAccessibilityIdentifier(elementType elementType: XCUIElementQuery, accessibilityIdentifier: String) {
var isElementExist = false
for element in elementType.allElementsBoundByIndex {
if element.label == accessibilityIdentifier {
element.tap()
isElementExist = true
break
}
}
if !isElementExist {
XCTFail("Failed to find element")
}
}
where you call the method in the test like:
tapBasedOnAccessibilityIdentifier(elementType: app.staticTexts, accessibilityIdentifier: "Accessibility Identifier")
You can tweak it a little so that it cover all the requirements.

Resources