Swift - Using a closure inside a repeat loop - ios

I am trying to implement a user registration system and I need to know whether a user id (generated randomly) has already been assigned to another user or not. To do so, I connect my Firebase database and use the observer() method to check to availability of the generated id.
However, since the Firebase database query runs asynchronously and I can only know the result once the query returns, I am not able to use the correct return value from within the calling method.
My approach here is
repeat {
id = generateUniqueId()
check availability
} while (id is not unique)
The implementation I have is
var id:String
var test = true
repeat {
id = generateId()
ref.child("\(databaseReferenceName)").observe(.value) { (snapshot) in
test = snapshot.hasChild("\(id)")
}
} while (test == true)
This loop keeps running even though the test variable is set to false by the hasChild() method.
How can I change the code so that I would be able to capture the right value of the test variable?
I am using Swift 4.1
Thanks

You don't say, but I'm assuming the observe() function you're using is asynchronous, and will return before the work is done. As PaulW11 says, you need to trigger the next round in the completion handler:
func observeChildren() {
var id:String
id = generateId()
ref.child("\(databaseReferenceName)").observe(.value) { (snapshot) in
if snapshot.hasChild("\(id)") {
observeChildren() //recursively call observeChildren again.
}
}

Since the process is asynchronous the loop will run until hit the first test = false , but i think you need this recursive way until find an available id
func checkID() {
id = generateId()
ref.child("\(databaseReferenceName)").observeSingleEvent(.value) { (snapshot) in
let test = snapshot.hasChild("\(id)")
if test {
print("id exists")
checkID() // check another one
}
else {
print("this is the one \(id)")
}
}
}
another thing is that it should be observeSingleEvent instead of observe

Related

Custom Combine Publisher for a single database listener

I have an iOS app with a custom database, To retrieve data from my database I setup a listener like this:
var listener: DatabaseListener?
self.listener = db.items.addListener { items in
}
// later when I don't need the listener any more:
self.listener?.cancel()
This listener gives the items as soon as I set it up and notifies me whenever my data is updated, It also stays alive until I manually cancel it. I also store a cache of the retrieved items in UserDefaults to speed things up (See it in action in the example bellow).
Now I'm trying to start using Combine to retrieve my items, I want to setup the database listener as soon as a new subscription is created (for example when sink or assign are called) and cancel it when there's no more subscriptions left.
So here's what I came up with:
class ItemsSubscription: Subscription {
private var subscriber: (any Subscriber<[Item], Never>)?
private var listener: DatabaseListener?
private var items: [Item] = UserDefaults.standard.cacheItems
init(subscriber: any Subscriber<[Item], Never>) {
self.subscriber = subscriber
}
func request(_ demand: Subscribers.Demand) {
let _ = subscriber?.receive(items)
self.listener = db.items.addListener {
UserDefaults.standard.cacheItems = $0
self.items = $0
let _ = self.subscriber?.receive($0)
}
}
func cancel() {
self.listener?.cancel()
self.listener = nil
self.subscriber = nil
}
}
struct ItemsPublisher: Publisher {
typealias Output = [Item]
typealias Failure = Never
func receive<S>(subscriber: S) where S: Subscriber, S.Input == [Item], S.Failure == Never {
let subscription = ItemsSubscription(subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
Then I'm using ItemsPublisher like this:
private var cancellables: Set<AnyCancellable> = []
ItemsPublisher()
.sink { items in
}
.store(&cancellables)
Currently this method is working but it's creating a new database listener (which is an expensive resource) for every ItemsPublisher I create. Instead I want to maintain a single database listener while I have a least 1 subscriber and I want any following subscriber to receive the latest items from the same subscription.
I considered creating a single ItemsPublisher instance and using it throughout the app, but later subscribers didn't receive any data at all.
I also considered using CurrentValueSubject (or a #Published property) to store the items but I couldn't figure out when to setup database listener or when to cancel it for that matter.
Any help or advice would be appreciated.
Instead I want to maintain a single database listener while I have a least 1 subscriber and I want any following subscriber to receive the latest items from the same subscription.
That's exactly what share() is for. View the documentation for more information.
You might also want to consider using multicast with a CurrentValueSubject depending on the situation.

Initializing a RAC ReactiveSwift Property that has multiple dependencies?

I am fairly new with using ReactiveSwift and ReactiveCocoa and I seem to have hit a road block regarding the proper way of initializing a Property that has a dependencies.
For instance in the following code, I try to initialize a Property but i get a compiler error which is expected. My question is how/ what is the "correct" way to do this.
class SomeViewModel {
// illustration purposes, in reality the property (dependency) we will observe will change over time
let dependency = Property(value: true)
let dependency2 = Property(value: "dependency2")
let dependency3 = Property(value: 12345)
let weightLabel: Property<String>
// private(set) var weightLabel: Property<String>!
// using private(set) var weightLabel: Property<String>! works,
// however this changes the meaning behind using let, because we could
// reinitalize weightLabel again which is not similar to using a let so not a good alternative
// let weightLabel: Property<String> = Property(value: "")
// another solution that will work but will result in a wrong value
// upon initalization then, changed into the "correct value" thus, i
// am discrading this as well
init() {
weightLabel = dependency.map {
// compiler error, 'self' captured by closure before all members were initalized.
// My question is if there is a way to handle this scenario properly
if $0 && self.dependency2.value == "dependency2" && self.dependency3.value == 12345 {
return ""
}
return ""
}
}
}
So as you might have noticed above in the comments I am wondering if there is a way to handle this scenario with ReactiveSwift other then the ones i mentioned above that are not really ideal solutions.
The instrument that fits the scenario is combineLatest, which provides a combined version of all these properties (streams) whenever any of them has been updated.
weightLabel = Property.combineLatest(dependency, dependency2, dependency3)
.map { d1, d2, d3 in
return "Hello World! \(d1) \(d2) \(d3)"
}
Regarding the compiler error, the issue is that you are capturing/referring to self in a closure before every stored property has been initialised. Depending on the intention, you may use a capture list to capture directly the values and objects you are interested w/o self.
let title: String
let action: () -> Void
init() {
title = "Hello World!"
// 🚫 `action` has not been initialised when `self` is
// being captured.
action = { print(self.title) }
// ✅ Capture `title` directly. Now the compiler is happy.
action = { [title] in print(title) }
}

Automatic UI updates with Apollo in Swift not working

I have the following setup for a small Apollo iOS app where I display a list of conferences in a table view and want to be able to add a conference to the list:
GraphQL:
query AllConferences {
allConferences {
...ConferenceDetails
}
}
mutation CreateConference($name: String!, $city: String!, $year: String!) {
createConference(name: $name, city: $city, year: $year) {
...ConferenceDetails
}
}
fragment ConferenceDetails on Conference {
id
name
city
year
attendees {
...AttendeeDetails
}
}
fragment AttendeeDetails on Attendee {
id
name
conferences {
id
}
}
ConferencesTableViewController:
class ConferencesTableViewController: UITableViewController {
var allConferencesWatcher: GraphQLQueryWatcher<AllConferencesQuery>?
var conferences: [ConferenceDetails] = [] {
didSet {
tableView.reloadData()
}
}
deinit {
allConferencesWatcher?.cancel()
}
override func viewDidLoad() {
super.viewDidLoad()
allConferencesWatcher = apollo.watch(query: AllConferencesQuery()) { result, error in
print("Updating conferences: ", result?.data?.allConferences)
guard let conferences = result?.data?.allConferences else {
return
}
self.conferences = conferences.map { $0.fragments.conferenceDetails }
}
}
// ...
// standard implementation of UITableViewDelegate
// ...
}
AddConferenceViewController:
class AddConferenceViewController: UIViewController {
// ... IBOutlets
#IBAction func saveButtonPressed() {
let name = nameTextField.text!
let city = cityTextField.text!
let year = yearTextField.text!
apollo.perform(mutation: CreateConferenceMutation(name: name, city: city, year: year)) { result, error in
if let _ = result?.data?.createConference {
self.presentingViewController?.dismiss(animated: true)
}
}
}
}
I also implemented cacheKeyForObject in AppDelegate like so:
apollo.cacheKeyForObject = { $0["id"] }
My question is whether it is possible to benefit from automatic UI updates with this setup? Currently when the CreateConferenceMutation is performed, the table view is not updated. Am I missing something or am I hitting the limitation that is mentioned in the docs:
In some cases, just using cacheKeyFromObject is not enough for your application UI to update correctly. For example, if you want to add something to a list of objects without refetching the entire list, or if there are some objects that to which you can’t assign an object identifier, Apollo cannot automatically update existing queries for you.
This is indeed a limitation of automatic UI updates. Although Apollo uses cacheKeyFromObject to match objects by ID, and this covers many common cases, it can't automatically update lists of objects.
In your schema, there is no way for Apollo to know that a newly added conference should be added to the allConferences list. All it knows is that allConferences returns a list of conference objects, but these could be arbitrarily selected and ordered.
So in cases like these, you will have to refetch the query from the server yourself, or change the mutation result to include the updated list.
Another option would be to manually add the new conference to the list in the client store. For this, the next version of Apollo iOS will include a manual update option similar to updateQueries in the Apollo JavaScript client.

Swift Array what happens when passing var that is nil to .contains and .filter

I'm trying to understand some code in a project I'm working on. I have an array property of strings:
var names: [String]!
func findName(name: String?) -> [Name]? {
if name != nil {
return nameManager.namesForSearchString(name)?.filter({self.names.contains($0.name)})
} else {
return nameManager.allNames.filter({self.names.contains($0.name)}) //<-what get's returned here?
}
}
What I don't understand is if the name is nil, what happens when .contains is called, and with that, what happens when .filter gets called? This is implemented in a Favorites class, and I need to call this function to return all favorites if a button is tapped, so what would I pass to this function to ensure that all the contents of Names: [Name] are returned?
On a lower level, I want to understand how .contains and .filter work and what gets returned if nil is passed to them.
Another version of the same method from a different commit (that I also did not write) is this:
func findFavorites(name: String?) -> [Station]? {
if name != nil {
return nameManager.namesForSearchString(name)!.filter({contains(self.names, $0.objectId)})
} else {
return nameManager.allNames.filter({contains(self.names, $0.objectId)})
}
}
I don't want to post a non-answer, but I do want this to be properly formatted so I guess a comment won't do. This might help you understand what's going on, and what happens with filter/contains. If you have any more questions, let me know, and I'll answer the question. If I'm completely off-base, let me know as well!
// I don't know why this is implicitely unwrapped, as a nil in this Array crashes Playground execution
var localNames: [String!] = ["Troy", "Bob", "Donald"]
// I'm just modelling what I know about NameManager
struct NameManager {
var allNames = [Name(name: "Bob"), Name(name: "Liz"), Name(name: "Anastasia")]
}
// I also assume the `name` in Name is a non-optional.
struct Name {
var name: String = "some name"
}
var nameManager = NameManager()
func findName(name: String?) -> [Name]? {
// Case where `name` is non-nil is excluded for demonstration purposes
// I have expanded all the closure short-hands so we always see what we're doing.
let allNames = nameManager.allNames
// namesMatchingName is of type [Name], that we get by applying a filter.
// `filter` works on a predicate basis: it goes through each element, one at a time,
// and checks if it meets the "predicate", that is, a boolean
// condition that returns true or false. If it DOES meet the criteria, it will be included in
let namesMatchingName = allNames.filter { (currentName) -> Bool in
// Now we're inside the filter-predicate. What we do here is check if the `currentName`
// is in `localNames`.
let namesHasCurrentName = localNames.contains(currentName.name)
// If the name IS in `localNames` we return true to the filter,
// which means it will be included in the final array, `namesMatchingName`.
return namesHasCurrentName
}
// So now we have all the names that appear in both `nameManager.allNames` and `localNames`
return namesMatchingName
}
findName(nil) // returns [{name: "Bob"}]

Parse checking if nil

I have some code that pulls users based on what they type on a login view. I am trying to check if a specific field is blank (nil) with something along the following
for user in users{
if user["secretkey"] == nil {
query.getObjectInBackgroundWithId(user.objectId) {
However, when the column "secretkey" is in fact nil, tested with println(user["secretkey"], the if statement continues to its default.
Use the form:
for user in users {
if let secretKey = user["secretkey"] {
query.getObjectInBackgroundWithId(user.objectId) {
}
}
(you can also then use secretKey directly inside the if block if you wanted to)
Fixed it in my situation by declaring it as a variable first using the following.
let secretKeyParse = user["secretkey"] as String?
if secretKeyParse == nil {

Resources