Say I get below code, and it works fine.
override func viewDidLoad() {
super.viewDidLoad()
// 1. put loadLevel() in background queue
DispatchQueue.global().async { [weak self] in
self?.loadLevel()
}
}
func loadLevel() {
var clueString = ""
var solutionString = ""
var letterBits = [String]()
// 2. some heavy code here
DispatchQueue.main.async { [weak self] in
// 3. push some UI code back to main thread
}
However, when I move the background queue to inside loadLevel() and cover the heavy code and UI code, I get an issue that UI is updated with empty values when launching the app. So what is the different of this two ways?
override func viewDidLoad() {
super.viewDidLoad()
// 1. call loadLevel
loadLevel()
}
func loadLevel() {
var clueString = ""
var solutionString = ""
var letterBits = [String]()
DispatchQueue.global().async { [weak self] in
// 2. some heavy code in background queue
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
Update the 2nd code with the heavy code inside.
I found the issue, it is not related with GCD actually. This issue is in line Bundle.main.url(forResource: "level\(self?.level)", which produces a String interpolation warning. And result resource load get nil I guess.
As I used weak reference [weak self] as capture list here, I need to put self? before the global variable level in case to use it in closure. If I give it a default value like \(self?.level ?? 0), then this issue is fixed.
But is it that the property way to deal with this String interpolation here? Or some better approach should be involved here?
override func viewDidLoad() {
super.viewDidLoad()
// 1. call loadLevel
loadLevel()
}
func loadLevel() {
var clueString = ""
var solutionString = ""
var letterBits = [String]()
DispatchQueue.global().async { [weak self] in
if let levelFileURL = Bundle.main.url(forResource: "level\(self?.level)", withExtension: "txt") {
if let levelContents = try? String(contentsOf: levelFileURL) {
var lines = levelContents.components(separatedBy: "\n")
lines.shuffle()
self?.correctGuess = 0
print("AAA")
for (index, line) in lines.enumerated() {
let parts = line.components(separatedBy: ": ")
let answer = parts[0]
let clue = parts[1]
clueString += "\(index + 1). \(clue)\n"
let solutionWord = answer.replacingOccurrences(of: "|", with: "")
solutionString += "\(solutionWord.count) letters\n"
self?.solutions.append(solutionWord)
let bits = answer.components(separatedBy: "|")
letterBits += bits
print("ABC")
}
}
}
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
You have a reference to:
let resource = Bundle.main.url(forResource: "level\(self?.level)" withExtension: ...)
The warning is
String interpolation produces a debug description for an optional value; did you mean to make this explicit?
The compiler is warning you that you are performing string interpolation of an optional value.
Let's consider a simpler example, to show what happens when you do string interpolation with optionals:
print("\(self?.level)")
If level was xxx, it would print
Optional("xxx")
And obviously if self or level were optional, it would just say:
nil
Clearly, neither of these are quite what you want. So, unwrap the optional. E.g.
guard let level = self?.level else { return }
let resource = Bundle.main.url(forResource: "level\(level)" withExtension: ...)
Let me start off by saying, I have no idea, but I have an idea for you to test. Move DispatchQueue.global().async to the first line of loadLevel().
func loadLevel() {
DispatchQueue.global().async { [weak self] in
var clueString = ""
var solutionString = ""
var letterBits = [String]()
// 2. some heavy code in background queue
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
This isolates the change to just calling loadLevel(). If this works as expected, then keep moving the DispatchQueue.global().async call down until it does break.
func loadLevel() {
var clueString = ""
DispatchQueue.global().async { [weak self] in
var solutionString = ""
var letterBits = [String]()
// 2. some heavy code in background queue
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
Related
I'm working on iOS App that uses the IP Stack API for geolocation. I'd like to optimise the IP Stack Api usage by asking for external (public) IP address first and then re-use lat response for that IP if it hasn't changed.
So what I'm after is that I ask every time the https://www.ipify.org about external IP, then ask https://ipstack.com with given IP address. If I ask the second time but IP doesn't changed then re-use last response (or actually cached dictionary with IP's as keys and responses as values).
I have a solution but I'm not happy with this cache property in my code. It is some state and some other part of code can mutate this. I was thinking about using some scan() operator in RxSwfit but I just can't figure out any new ideas.
class ViewController: UIViewController {
#IBOutlet var geoButton: UIButton!
let disposeBag = DisposeBag()
let API_KEY = "my_private_API_KEY"
let provider = PublicIPProvider()
var cachedResponse: [String: Any] = [:] // <-- THIS
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func geoButtonTapped(_ sender: UIButton) {
// my IP provider for ipify.org
// .flatMap to ignore all nil values,
// $0 - my structure to contains IP address as string
let fetchedIP = provider.currentPublicIP()
.timeout(3.0, scheduler: MainScheduler.instance)
.flatMapLatest { Observable.from(optional: $0.ip) }
.distinctUntilChanged()
// excuse me my barbaric URL creation, it's just for demonstration
let geoLocalization = fetchedIP
.flatMapLatest { ip -> Observable<Any> in
// check if cache contains response for given IP address
guard let lastResponse = self.cachedResponse[ip] else {
return URLSession.shared.rx.json(request: URLRequest(url: URL(string: "http://api.ipstack.com/\(ip)?access_key=\(API_KEY)")! ))
.do(onNext: { result in
// store cache as a "side effect"
print("My result 1: \(result)")
self.cachedResponse[ip] = result
})
}
return Observable.just(lastResponse)
}
geoLocalization
.subscribe(onNext: { result in
print("My result 2: \(result)")
})
.disposed(by: disposeBag)
}
}
Is it possible to achieve the same functionality but without var cachedResponse: [String: Any] = [:] property in my class?
OMG! I spent a bunch of time with the answer for this question (see below) and then realized that there is a much simpler solution. Just pass the correct caching parameter in your URLRequest and you can do away with the internal cache completely! I left the original answer because I also do a general review of your code.
class ViewController: UIViewController {
let disposeBag = DisposeBag()
let API_KEY = "my_private_API_KEY"
let provider = PublicIPProvider()
#IBAction func geoButtonTapped(_ sender: UIButton) {
// my IP provider for ipify.org
let fetchedIP: Maybe<String> = provider.currentPublicIP() // `currentPublicIP()` returns a Single
.timeout(3.0, scheduler: MainScheduler.instance)
.map { $0.ip ?? "" }
.filter { !$0.isEmpty }
// excuse me my barbaric URL creation, it's just for demonstration
let geoLocalization = fetchedIP
.flatMap { (ip) -> Maybe<Any> in
let url = URL(string: "http://api.ipstack.com/\(ip)?access_key=cce3a2a23ce22922afc229b154d08393")!
return URLSession.shared.rx.json(request: URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad))
.asMaybe()
}
geoLocalization
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { result in
print("My result 2: \(result)")
})
.disposed(by: disposeBag)
}
}
Original Answer
The short answer here is no. The best you can do is wrap the state in a class to limit its access. Something like this generic approach:
final class Cache<Key: Hashable, State> {
init(qos: DispatchQoS, source: #escaping (Key) -> Single<State>) {
scheduler = SerialDispatchQueueScheduler(qos: qos)
getState = source
}
func data(for key: Key) -> Single<State> {
lock.lock(); defer { lock.unlock() }
guard let state = cache[key] else {
let state = ReplaySubject<State>.create(bufferSize: 1)
getState(key)
.observeOn(scheduler)
.subscribe(onSuccess: { state.onNext($0) })
.disposed(by: bag)
cache[key] = state
return state.asSingle()
}
return state.asSingle()
}
private var cache: [Key: ReplaySubject<State>] = [:]
private let scheduler: SerialDispatchQueueScheduler
private let lock = NSRecursiveLock()
private let getState: (Key) -> Single<State>
private let bag = DisposeBag()
}
Using the above isolates your state and creates a nice reusable component for other situations where a cache is necessary.
I know it looks more complex than your current code, but gracefully handles the situation where there are multiple requests for the same key before any response is returned. It does this by pushing the same response object to all observers. (The scheduler and lock exist to protect data(for:) which could be called on any thread.)
I have some other suggested improvements for your code as well.
Instead of using flatMapLatest to unwrap an optional, just filter optionals out. But in this case, what's the difference between an empty String and a nil String? Better would be to use the nil coalescing operator and filter out empties.
Since you have the code in an IBAction, I assume that currentPublicIP() only emits one value and completes or errors. Make that clear by having it return a Single. If it does emit multiple values, then you are creating a new chain with every function call and all of them will be emitting values. It's unlikely that this is what you want.
URLSession's json(request:) function emits on a background thread. If you are going to be doing anything with UIKit, you will need to observe on the main thread.
Here is the resulting code with the adjustments mentioned above:
class ViewController: UIViewController {
private let disposeBag = DisposeBag()
private let provider = PublicIPProvider()
private let responses: Cache<String, Any> = Cache(qos: .userInitiated) { ip in
return URLSession.shared.rx.json(request: URLRequest(url: URL(string: "http://api.ipstack.com/\(ip)?access_key=cce3a2a23ce22922afc229b154d08393")!))
.asSingle()
}
#IBAction func geoButtonTapped(_ sender: UIButton) {
// my IP provider for ipify.org
let fetchedIP: Maybe<String> = provider.currentPublicIP() // `currentPublicIP()` returns a Single
.timeout(3.0, scheduler: MainScheduler.instance)
.map { $0.ip ?? "" }
.filter { !$0.isEmpty }
let geoLocalization: Maybe<Any> = fetchedIP
.flatMap { [weak responses] ip in
return responses?.data(for: ip).asMaybe() ?? Maybe.empty()
}
geoLocalization
.observeOn(MainScheduler.instance) // this is necessary if your subscribe messes with UIKit
.subscribe(onSuccess: { result in
print("My result 2: \(result)")
}, onError: { error in
// don't forget to handle errors.
})
.disposed(by: disposeBag)
}
}
I'm afraid that unless you have some way to cache your network responses (ideally with URLRequest's native caching mechanism), there will always be side-effects.
Here's a suggestion to try to keep them contained though:
Use Rx for your button tap as well, and get rid of the #IBAction. It's not great to have all that code in an #IBAction anyway (unless you did that for demonstration purposes).
That way, you can use a local-scope variable inside the setup function, which will only be captured by your flatMapLatest closure. It makes for some nice, clean code and helps you make sure that your cachedResponse dictionary is not tampered by other functions in your class.
class ViewController: UIViewController {
#IBOutlet var geoButton: UIButton!
let disposeBag = DisposeBag()
let API_KEY = "my_private_API_KEY"
let provider = PublicIPProvider()
override func viewDidLoad() {
super.viewDidLoad()
prepareGeoButton()
}
func prepareGeoButton() {
// ----> Use RxCocoa UIButton.rx.tap instead of #IBAction
let fetchedIP = geoButton.rx.tap
.flatMap { _ in self.provider.currentPublicIP() }
.timeout(3.0, scheduler: MainScheduler.instance)
.flatMapLatest { Observable.from(optional: $0.ip) }
.distinctUntilChanged()
// ----> Use local variable.
// Still has side-effects, but is much cleaner and safer.
var cachedResponse: [String: Any] = [:]
let geoLocalization = fetchedIP
.flatMapLatest { ip -> Observable<Any> in
// check if cache contains response for given IP address
guard let lastResponse = cachedResponse[ip] else {
return URLSession.shared.rx.json(request: URLRequest(url: URL(string: "http://api.ipstack.com/\(ip)?access_key=cce3a2a23ce22922afc229b154d08393")! ))
.do(onNext: { result in
print("My result 1: \(result)")
cachedResponse[ip] = result
})
}
return Observable.just(lastResponse)
}
geoLocalization
.subscribe(onNext: { result in
print("My result 2: \(result)")
})
.disposed(by: disposeBag)
}
}
I did not want to change the code too much from what you had and add more implementation details into the mix, but if you choose to go this way, please:
a) Use a Driver instead of an observable for your button tap. More on Drivers here.
b) Use [weak self] inside your closures. Don't retain self as this might lead to your ViewController being retained in memory multiple times when you move away from the current screen in the middle of a network request, or some other long-running action.
I'm trying to get certain child nodes named City from Firebase using observeSingleEvent but I am having issues trying to pull it into the main thread. I have used a combination of completion handlers and dispatch calls but I am not sure what I am doing wrong, in addition to not being that great in async stuff. In viewDidLoad I'm trying to append my keys from the setupSavedLocations function and return it back to savedLocations I feel like I am close. What am I missing?
Edit: Clarity on question
import UIKit
import Firebase
class SavedLocationsViewController: UIViewController {
var userID: String?
var savedLocations: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
self.savedLocations = savedData
print("inside", self.savedLocations)
})
}
print("outside",savedLocations)
}
func setupSavedLocations(completion: #escaping ([String]) -> ()) {
guard let user = userID else { return }
let databaseRef = Database.database().reference(fromURL: "https://************/City")
var dataTest : [String] = []
databaseRef.observeSingleEvent(of: .value, with: {(snapshot) in
let childString = "Users/" + user + "/City"
for child in snapshot.children {
let snap = child as! DataSnapshot
let key = snap.key
dataTest.append(key)
}
completion(dataTest)
})
}
sample output
outside []
inside ["New York City", "San Francisco"]
The call to setupSavedLocations is asynchronous and takes longer to run than it does for the cpu to finish viewDidLoad that is why your data is not being shown. You can also notice from your output that outside is called before inside demonstrating that. The proper way to handle this scenario is to show the user that they need to wait for an IO call to be made and then show them the relevant information when you have it like below.
class SavedLocationsViewController: UIViewController {
var myActivityIndicator: UIActivityIndicatorView?
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
showSavedLocations(locations: savedData)
})
}
// We don't have any data here yet from the IO call
// so we show the user an indicator that the call is
// being made and they have to wait
let myActivityIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)
myActivityIndicator.center = view.center
myActivityIndicator.startAnimating()
self.view.addSubview(myActivityIndicator)
self.myActivityIndicator = myActivityIndicator
}
func showSavedLocations(locations: [String]) {
// This function has now been called and the data is passed in.
// Indicate to the user that the loading has finished by
// removing the activity indicator
myActivityIndicator?.stopAnimating()
myActivityIndicator?.removeFromSuperview()
// Now that we have the data you can do whatever you want with it here
print("Show updated locations: \(locations)")
}
This is the class where the value is
class CurrentWeather{
var _date:String!
var _cityName:String!
var _temp:Double!
var _weatherType:String!
var cityName:String{
if _cityName==nil{
_cityName = ""
}
return _cityName
}
var currentTemprature:Double{
if _temp==nil{
_temp = 0.0
}
return self._temp
}
var weathertype:String{
if _weatherType==nil{
_weatherType = ""
}
return _weatherType
}
var date:String{
if _date==nil{
_date = ""
}
let dateFormater=DateFormatter()
dateFormater.dateStyle = .long
dateFormater.timeStyle = .none
let currentDate = dateFormater.string(from: Date())
self._date="\(currentDate)"
return _date
}
func weatherDataDownload(completed : downloadComplete){
let weatherUrl=URL(string: constant)!
Alamofire.request(weatherUrl , method:.get).responseJSON{response in
if let dict=response.result.value as? Dictionary<String,AnyObject>{
if let name=dict["name"] as? String{
self._cityName = name.capitalized
print(name.capitalized)
}
if let weather=dict["weather"] as? [Dictionary<String,AnyObject>]{
if let main=weather[0]["main"] as? String{
self._weatherType=main.capitalized
print(main.capitalized)
}
}
if let tempr=dict["main"] as? Dictionary<String,AnyObject>{
if let temp=tempr["temp"] as? Double{
let convertedTemp=Double(round(temp-273.15))
self._temp=convertedTemp
print(convertedTemp)
}
}
}
}
completed()
}}
This is the ViewController class
var currentWeatherOj = CurrentWeather()
override func viewDidLoad() {
super.viewDidLoad()
table.delegate=self
table.dataSource=self
currentWeatherOj.weatherDataDownload {
self.updateUIweather()
}
}
func updateUIweather () {
weatherType.text=currentWeatherOj.weathertype
presentDate.text=currentWeatherOj.date
presentLocation.text=currentWeatherOj.cityName
presentTemp.text="\(currentWeatherOj.currentTemprature)"
}
when I try to call in ViewController its showing the default value which I set inside of computed variable other than _date but I am able print values inside the func of weatherDataDownload.I am confused how variables in swift 3 works because of this.
See the comments in the following code sample. You need to move the call to "completed()"
func weatherDataDownload(#escaping completed : downloadComplete) {
let weatherUrl=URL(string: constant)!
Alamofire.request(weatherUrl , method:.get).responseJSON { response in
// ... leave your code here alone
// put the call to completed() here
completed()
}
// not here
}
When you make the all to Alamofire, it executes it's request on a background thread. When that request completes, it calls the closure that you've defined (the one that starts "response in..."). You don't want to call updateUIweather until that has been done so you put the call to "completed()" inside of the same completion handler.
When the call to completed was outside of that completion handler, it would be called right away... immediately after the Alamofire request was sent (but before it had finished on that background thread). None of the code in the completion handler has run yet so your variables aren't updated yet.
Finally because your completed closure was passed to a block that was then sent off to a background thread, that closure "escapes" the current function. You add the #escaping so that folks reading your code will know that the closure will live on beyond the life of that function.
I'm using this library in my app for banners. I am trying to get the Link by parsing the JSON.
The Images are are not showing in the slideshow view. If I press the slideshow view, after that everything works fine. I thought that there was some issue with my completion handler.
But I can't solve it yet :)
#IBOutlet weak var slideshow: ImageSlideshow!
var transitionDelegate: ZoomAnimatedTransitioningDelegate?
var Banner : [AlamofireSource] = []
override func viewDidLoad() {
super.viewDidLoad()
Banners { (imagesource) in
if imagesource != nil {
self.bannershow()
}
}
}
func Banners(completionHandler: ([AlamofireSource]?) -> ()) -> (){
Alamofire.request(.GET, "http://46.420.116.11/mobileapp/gps/api.php?rquest=get_banners")
.responseJSON{ response in
if let data = response.result.value{
let json = JSON(data)
let count = json["image_path"].count
for index in 0...count-1 {
let image :String = json["image_path"][index].stringValue
let source : AlamofireSource = AlamofireSource(urlString: image)!
self.Banner.append(source)
}
completionHandler(self.Banner)
}
}
}
func bannershow(){
self.slideshow.backgroundColor = UIColor.whiteColor()
self.slideshow.slideshowInterval = 2.0
self.slideshow.contentScaleMode = UIViewContentMode.ScaleToFill
self.slideshow.setImageInputs(self.Banner)
let recognizer = UITapGestureRecognizer(target: self, action: "click")
self.slideshow.addGestureRecognizer(recognizer)
}
func click() {
let ctr = FullScreenSlideshowViewController()
ctr.pageSelected = {(page: Int) in
self.slideshow.setScrollViewPage(page, animated: false)
}
ctr.initialPage = slideshow.scrollViewPage
ctr.inputs = slideshow.images
self.transitionDelegate = ZoomAnimatedTransitioningDelegate(slideshowView: slideshow);
ctr.transitioningDelegate = self.transitionDelegate!
self.presentViewController(ctr, animated: true, completion: nil)
}
You probably have a threading problem. There is no guarantee that the Banners completion handler is called on the main thread. You need to step out to the main thread explicitly before doing anything that touches your properties or (especially) the interface.
I think your problem might be that you're expecting the images to be available immediately but they need to be downloaded before, so they won't be available immediately after your viewDidLoad method finished. That's why you should probably configure your slideshow in the viewDidLoad and not in your bannershow() method. Something like this might be an improvement:
#IBOutlet weak var slideshow: ImageSlideshow!
var bannerImages : [AlamofireSource] = []
override func viewDidLoad() {
super.viewDidLoad()
slideshow.backgroundColor = UIColor.whiteColor()
slideshow.slideshowInterval = 2.0
slideshow.contentScaleMode = UIViewContentMode.ScaleToFill
let recognizer = UITapGestureRecognizer(target: self, action: "click")
slideshow.addGestureRecognizer(recognizer)
getBanners { imagesource in
self.showBanner()
}
}
func getBanners(completionHandler: ([AlamofireSource]?) -> ()) -> (){
Alamofire.request(.GET, "http://46.420.116.11/mobileapp/gps/api.php?rquest=get_banners")
.responseJSON{ response in
if let data = response.result.value{
let json = JSON(data)
let count = json["image_path"].count
for index in 0...count-1 {
let image :String = json["image_path"][index].stringValue
let source : AlamofireSource = AlamofireSource(urlString: image)!
self.bannerImages.append(source)
}
}
completionHandler(self.bannerImages)
}
}
func showBanner() {
slideshow.setImageInputs(bannerImages)
}
Move the code to viewWillAppear.
override func viewWillAppear(animated: Bool) {
Banners { (imagesource) in
if imagesource != nil {
self.bannershow()
}
}
}
func Banners(completionHandler: ([AlamofireSource]?) -> ()) -> (){
Alamofire.request(.GET, "http://46.420.116.11/mobileapp/gps/api.php?rquest=get_banners")
.responseJSON{ response in
if let data = response.result.value{
let json = JSON(data)
let count = json["image_path"].count
for index in 0...count-1 {
let image :String = json["image_path"][index].stringValue
let source : AlamofireSource = AlamofireSource(urlString: image)!
self.Banner.append(source)
}
completionHandler(self.Banner)
}
}
}
You are executing for loop in Banners fun
for index in 0...count-1 {
let image :String = json["image_path"][index].stringValue
let source : AlamofireSource = AlamofireSource(urlString: image)!
self.Banner.append(source)
}
Replace this code in some other method and place an Optional
var image :String? = json["image_path"][index].stringValue
or place an thumb image, That will make you confirm that image is downloaded successfully or not .
Let me know if it works
Thanks, Happy Coding
Maybe you don't see images because you update them in a secondary thread, you have to perform image update in main thread;
In swift ( performSelectorOnMainThread() is not available), you may use something like this :
dispatch_async(dispatch_get_main_queue(), {
myslideshow.updateimage();
})
Not really sure, but I am guessing that since your images need to get downloaded, they are nil when you call self.slideshow.setImageInputs(self.Banner), which internally sets the image from the list to an imageview which is added inside the scrollView. So one way I can think of is use SDWebImage to set the image to the imageView so that it updates the respective imageViews once the image is ready(downloaded). I think you will need to use it in the InputSource.swift class in the setToImageView(_:) method. You will have to check this though, this is the only possible problem i could think of that might be causing your issue.
I try to use Firebase framework and use it in my app, but I have a problem with undestanding, how to retrieving data from firebase and save it in my own array. Firebase structure is very simple and looks like that:
Now my code looks like that:
`var rootRef = Firebase(url: "https://mathgamepio.firebaseio.com/")
var array:[String]?
var count: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
rootRef.observeEventType(.ChildAdded) { (snap: FDataSnapshot!) -> Void in
self.count++
let math = snap.value["math"] as! String
print(self.count)
print(math)
self.array?.append(math)
print(self.array)
print("--------")
}`
The result of this operation looks like that:
self.array.append doesn't work and is nil always. How to add this data to my own array?
It looks like your array is never initialized, so self.array? will always skip append.
One solution is to check of the array exists:
if (self.array?.append(math)) == nil {
self.array = [math]
}
There are probably more ways to solve this, but I modified the answer from this question: Adding elements to optional arrays in Swift
Update
Even easier is to simply create the array, before using it:
var array:[String]?
var count: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.array = [];
rootRef.observeEventType(.ChildAdded) { (snap: FDataSnapshot!) -> Void in
self.count++
let math = snap.value["math"] as! String
print(self.count)
print(math)
self.array?.append(math)
print(self.array)
print("--------")
}
}
Or simply not mark the array as optional:
var array:[String] = []
var count: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
rootRef.observeEventType(.ChildAdded) { (snap: FDataSnapshot!) -> Void in
self.count++
let math = snap.value["math"] as! String
print(self.count)
print(math)
self.array.append(math)
print(self.array)
print("--------")
}
}
TIL I learned some basic Swift syntax. :-)