didSet not getting called on every mutation? - arrays

I have a variable that I want to have a listener for when you set and mutate and didSet seems like the perfect candidate. However, in my initial testing of it, the first append triggers the did set and (in my example) the JS loads into my webview, however I never see the second append hit my breakpoint on the didSet and the JS does not run because it must not have loaded. What can be going on?
var additionalJSFilesToRun: [(String, WKUserScriptInjectionTime)] {
didSet {
for (string, time) in additionalJSFilesToRun {
guard !self.jsFiles.contains(string) else { return }
guard let jsPath = Bundle.main.path(forResource: string, ofType: "js"), let jsSource = try? String(contentsOfFile: jsPath) else { return }
let script = WKUserScript(source: jsSource, injectionTime: time, forMainFrameOnly: false)
self.webView.configuration.userContentController.addUserScript(script)
jsFiles.insert(string)
}
}
}
In another class:
webView.additionalJSFilesToRun.append(("ABC", .atDocumentStart))
webView.additionalJSFilesToRun.append(("DEF", .atDocumentStart))
Only ABC is triggering the didSet. Any thoughts why one mutation is doing it but another isn't?

The problem was the return in the guard should be a continue, otherwise since it is based off the oldValue as well, it would end execution after the first tuple.

Related

Why does my array return null or show an empty array, after appending values to it

I am getting values from a firebase real-time database. I want to store these values into an array and display them in a UITableView. Here is what is happening:
I have defined the Array before my viewDidLoad() function as the following:
var taskTitles: [Task] = []
In my viewDidLoad() function I am calling another function to generate the array:
override func viewDidLoad() {
super.viewDidLoad()
//Setting the Title for the nav bar
title = "To Do List"
configureNavigationItems()
taskTitles = createArray() // creating the array of tasks in db
tableView.delegate = self
tableView.dataSource = self
}
In this function, I am passing information to my classes. Task and TaskCell. They are simply just handling the Title of the task.
func createArray() -> [Task] {
taskRef = Database.database().reference(withPath: "Tasks")
//getting values from db, storing them in an array.
refHandle = taskRef?.observe(DataEventType.value, with: { snapshot in
for taskSnapshot in snapshot.children {
let nodeA = taskSnapshot as! DataSnapshot
let keyA = nodeA.key
let theTask = Task(title: String(keyA))
self.taskTitles.append(theTask)
print("In the FOR Loop --> ", self.taskTitles)
}
print("outside of FOR Loop --> ", self.taskTitles)
})
print("outside of observe func --> ", taskTitles)
return taskTitles
}
}
However, it doesn't seem to save my items into the array. I did some debugging to figure where things were going wrong. Hopefully, the picture below can clarify:
Any idea what the issue is?
Your call to taskRef?.observe is asynchronous. That is why you see "outside of observe func --> []" printed before the other lines.
What is happening is that your createArray() function calls observe and then returns taskTitles which is still empty. Then your view finishes loading and shows (presumably) an empty table. After this the observe function calls your closure with the snapshot and you update taskTitles, but at this point tableView is already on screen and empty and you would have to take further actions to reload it (like, for example, calling reloadData() on it).
It is perhaps also worth mentioning that you are modifying your property in that function, then returning it, and assigning it to itself. This is perhaps redundant.

My function returns empty array in swift and parse

HI there I am writing a function to get data from my parse server in swift everything is working alright and the data is getting read well. but when I try to return the array it returns me an empty array.
I also added a print in the "Get data in background" and there, the array was getting full.So the problem is not getting the data.
public func getthearray() -> Array<UIImage>
{
let user = PFUser.current()
let array = user?["Photos"] as! Array<PFFileObject>
var imagearray = [UIImage]()
for x in array
{
x.getDataInBackground
{ (dataa, error) in
var img = UIImage(data: dataa!)
imagearray.append(img!)
}
}
return imagearray
}
The reason the function returns an empty array is the fact that the code in the x.getDataInBackground completion gets called asynchronously, and before any image will get added to the array, the faction will already return.
Here is the solution (in 2 versions), which is your function slightly refactored and rewritten with completion handler and Grand Central Dispatch
Approach 1. Semaphores
public func getTheArray(_ completion: #escaping ([UIImage]) -> Void) {
let user: PFUser = .current()
let array = user?["Photos"] as? [PFFileObject] ?? []
var result = [UIImage]()
let semaphore = DispatchSemaphore(value: 0)
// dispatch to a separate thread which we can safely occupy and block
// while waiting for the results from async calls.
DispatchQueue.global().async {
for file in array {
file.getDataInBackground { (data, error) in
if let data = data, let image = UIImage(data: data) {
result.append(image)
}
// the result have arrived, signal that we are ready
// to move on to another x
semaphore.signal()
}
// wait until the `signal()` is called, thread is blocked
semaphore.wait()
}
// dispatch to main queue where we are allowed to perform UI updates
DispatchQueue.main.async {
// call the completion, but we are doing it only once,
// when the function is finished with its work
completion(result)
}
}
}
Approach 2. Dispatch Groups
public func getTheArray(_ completion: #escaping ([UIImage]) -> Void) {
let user: PFUser = .current()
let array = user?["Photos"] as? [PFFileObject] ?? []
var result = [UIImage]()
let group = DispatchGroup()
for file in array {
group.enter()
file.getDataInBackground { (data, error) in
if let data = data, let image = UIImage(data: data) {
result.append(image)
}
group.leave()
}
}
// notify once all task finish with `leave()` call.
group.notify(queue: .main) {
// call the completion, but we are doing it only once,
// when the function is finished with its work
completion(result)
}
}
Usage
and you would call it like this
getTheArray { result in
// do what you want with result which is your array of UIImages
}
Documentation:
DispatchQueue
DispatchSemaphore
Relevant blogposts:
Grand Central Dispatch by John Sundell
A deep dive into Grand Central Dispatch in Swift by John Sundell
You should use the completion handler closure as discussed by others, in order to asynchronously return the array of images when all the requests are done.
The general pattern is:
Use #escaping completion handler closure.
Use dispatch group to keep track of all of the asynchronous requests.
Store the results in a dictionary as the individual requests finish. We use a dictionary for the results, because with concurrent requests, you do not know in what order the requests will finish, so we will store it in a structure from which we can efficiently retrieve the results later.
Use dispatch group notify closure, to specify what should happen when all the requests are done. In this case, we will build a sorted array of images from our unsorted dictionary, and call the completion handler closure.
As an aside, one should avoid using forced unwrapping operator (the ! or as!), especially when dealing with network requests, whose success or failure is outside of your control. We would generally use guard statement to test that the optionals were safely unwrapped.
Thus:
func fetchImages(completion: #escaping ([UIImage]) -> Void) {
guard
let user = PFUser.current(),
let files = user["Photos"] as? [PFFileObject]
else {
completion([])
return
}
var images: [Int: UIImage] = [:] // dictionary is an order-independent structure for storing the results
let group = DispatchGroup() // dispatch group to keep track of asynchronous requests
for (index, file) in files.enumerated() {
group.enter() // enter the group before asynchronous call
file.getDataInBackground { data, error in
defer { group.leave() } // leave the group when this completion handler finishes
guard
let data = data,
let image = UIImage(data: data)
else {
return
}
images[index] = image
}
}
// when all the asynchronous tasks are done, this `notify` closure will be called
group.notify(queue: .main) {
let array = files.indices.compactMap { images[$0] } // now rebuild the ordered array
completion(array)
}
}
And, you'd use it like so:
fetchImages { images in
// use `images` here, e.g. if updating a model object and refreshing the UI, perhaps:
self.images = images
self.tableView.reloadData()
}
// but not here, because the above runs asynchronously (i.e. later)
But the idea is to embrace asynchronous patterns given that we are calling an asynchronous API, and use dispatch groups to keep track of when they are all done. But dispatch semaphores are generally discouraged, as they force the requests to be performed sequentially, one after another, which will slow it down (e.g., in my experience, between two and five times slower).
As an aside, we would generally want to report success or failure. So rather than [UIImage], we would have the closure’s parameter to be a Result<[UIImage], Error> (if you want a single Error) or [Result<UIImage, Error>] if you want either a UIImage or Error for each file. But there’s not enough here to know what error handling you want. But just a FYI.
While I've answered the tactical question above, we really should ask how many images might you be dealing with. If you are retrieving lots of images and/or the user is on a slow connection, this pattern (known as “eager” fetching of the images) is discouraged. First, it can be slow if there are a lot of them. Second, images take up a lot of memory, and you might not want to load all of them if you only need a subset of them at any given point in time. We would often refactor our app to employ “lazy” fetching of the images, retrieving them as needed, not all up front. Or, if you want to do eager fetch, download the assets as files to caches folder (reducing RAM consumption) and create UIImage instances when required in the UI.

Removing data from array correctly

I am facing the problem that I fill up an array with data and when I want to remove it later, even though I call the removeAll() the array still is not empty.
Better see my code for a more clear view on the problem
Updated new code
#objc func textFieldDidChange() {
doSearch()
if let commentText = commentTextField.text , !commentText.isEmpty {
sendButton.setTitleColor(UIColor.blue, for: UIControlState.normal)
sendButton.isEnabled = true
return
}
sendButton.setTitleColor(UIColor.lightGray, for: UIControlState.normal)
sendButton.isEnabled = false
}
func doSearch() {
let caption = commentTextField.text
let words = caption?.components(separatedBy: CharacterSet.whitespacesAndNewlines)
self.usersSuggestion.removeAll()
for var word in words! {
self.usersSuggestion.removeAll()
if word.hasPrefix("#") {
word = word.trimmingCharacters(in: CharacterSet.punctuationCharacters)
self.isCellSelected = true
self.usersSuggestion.removeAll()
API.User.suggestUsers(withText: word, completion: { (user) in
print("closure", word)
self.usersSuggestion.append(user)
self.tableView.reloadData()
print("#", self.usersSuggestion.count)
})
self.usersSuggestion.removeAll()
tableView.reloadData()
} else {
self.isCellSelected = false
self.usersSuggestion.removeAll()
tableView.reloadData()
}
self.usersSuggestion.removeAll()
tableView.reloadData()
}
}
I am facing the problem here that the array, even though I call multiple times the removeAll() method, never gets back to 0. When I have 2 user, link one, the next time I write an # I get the 2 users+ the linked user (so him twice). I can do it infinitely, having like 100 userSuggestions with just 2 existing users.
photos
You are printing the count immediately after inserting an element in the array. This will alway give a count of 1.
// count was zero
self.hashTags.insert(hashTag, at: 0) // adding one
print("# = ", self.hashTags.count) // count is now 1
Given that the API.user... function uses a completion handler, I would assume that this happens in a separate thread or at least asynchronously so you could event get into situations where the UI shows empty and suddenly shows one.
You may want to structure your code differently to better convey the separation between requesting data and displaying the (asynchronously obtained) results. Embedded completion handlers tend be misleading and give the impression that execution will happen in their visually organized sequence.

Cannot Acessing Array Outside Closure Swift 3

I have this closure which I use to populate my array and dictionary. However, when I try to use it outside the function, it's empty. Here's my code:
var pages: [FBPages] = []
override func viewDidLoad() {
super.viewDidLoad()
Facebook.getUserInfo(greetingLabel: greetingLabel, profileImage: profilePic)
pages = []
let params = ["fields": "about,name,created_time,picture", "limit": "3"]
Facebook.getUserPagesLikes(params: params, handler: { (userData) in
guard let pagesArrays = userData["data"] as? Array<Any> else {return}
for dict in pagesArrays {
let fbPages = FBPages()
let pagesData = dict as! NSDictionary
fbPages.about = pagesData["about"] as! String
fbPages.name = pagesData["name"] as! String
self.pages.append(fbPages)
}
print("pages array: \(pagesArrays)")
print("pages : \(self.pages.count)")
}) { (error) in
print("cannot get FB pages: \(String(describing: error))")
}
//I got 0 here
print("fb array: \(pages.count)")
}
I tried to print array count outside closure but I only got 0 as a result. How can I solve this problem. Thanks in advance.
You will need to understand that the time you are printing array. It is not yet available. getUserPagesLikes is an async call. Which is taking a completion handler. Once the response comes back, you will have values in your array.
You can understand the sequence like you ask getUserPagesLikes to fetch the user likes for you and you don't wait for user likes there. You move ahead, and whatever code you write after completion block gets called. Which in your case turns out to be printing array.
Once the getUserPagesLikes gets back the data for you its completion block gets called. At this time you assign the array values. The values in array would be available after this only. So if you want to reload data in table view do it in completion handler. Also make sure that you do it on main thread.
For a detail information on completion handler and their sequence of call you can go through here.

Swift / Adding objects to NSMutable Array

Im new to Swift and Im having a hard time understanding why this is not working - I've tried many different combinations of this through examples on stackoverflow and my variable "collections" still comes out empty (see last line) so I'm guessing I'm missing a small (but important) detail. Appreciate any help!
class CollectionsViewController: UITableViewController {
var collections = NSMutableArray()
override func viewDidLoad() {
super.viewDidLoad()
SDK.sharedInstance()
.getAuthenticatedUserBoards(withFields: ["id", "name","url","description","image"],
success: { data in
guard let myData = data?.parsedJSONDictionary["data"] as? [[String: Any]]
else {
return
}
for item in myData {
self.collections.add(item)
}
}, andFailure: nil)
print("collections.....\(collections)")
//Output: collections.....()
}
The loading of the data is asynchronous. Since your goal to load your table view after the data loads, you need to call reloadData on your table view at the end of the success block. But UI calls must be made on the main queue so you should use DispatchQueue to do this.
Here is what you need:
override func viewDidLoad() {
super.viewDidLoad()
SDK.sharedInstance()
.getAuthenticatedUserBoards(withFields: ["id", "name","url","description","image"],
success: { data in
guard let myData = data?.parsedJSONDictionary["data"] as? [[String: Any]]
else {
return
}
for item in myData {
self.collections.add(item)
}
print("collections.....\(collections)")
DispatchQueue.main.async {
tableView.reloadData()
}
}, andFailure: nil)
}
Since you are currently updating self.collections in the background, there is a small chance your UI on the main queue will see a partially up-to-date set of data. So I would recommend one further change:
override func viewDidLoad() {
super.viewDidLoad()
SDK.sharedInstance()
.getAuthenticatedUserBoards(withFields: ["id", "name","url","description","image"],
success: { data in
guard let myData = data?.parsedJSONDictionary["data"] as? [[String: Any]]
else {
return
}
var list = NSMutableArray()
for item in myData {
list.add(item)
}
DispatchQueue.main.async {
self.collections = list
print("collections.....\(collections)")
tableView.reloadData()
}
}, andFailure: nil)
}
This ensure the main collections property is only updated on the main thread.
I also suggest you use a Swift array of a specific type instead of using an NSMutableArray.
I'm assuming getAuthenticatedUserBoards is making some sort of asynchronous call to an API. Since we're not sure how long that call is going to take, the task is placed on a background thread while the rest of the app's tasks continue to run on the main thread. Your print statement is one of those tasks that will run while the API call is happening in the background. In essence, the print statement executes before anything inside of the getAuthenticatedUserBoards is completed.
The list of collections should be printed directly after the for loop and before the end of the closure. You can verify the order in which the different tasks are completed by placing print statements at various different points within your function - you should be able to see that your current print statement will print before any other statements that you place inside of the closure.

Resources