Wait for closure result for doing the return - ios

I got a book reader coded in Swift, the first chapter is OK, but when I try to load the second from the webservice (the books come from a server chapter by chapter), the data source method of the pageviewcontroller has to return a viewcontroller, and it doesn't wait to the closure which gets the new chapter, it always return nil. I've tried using dispatch_semaphore_t, dispatch_group_t, etc, and I don't get it. The method is the following:
// MARK: - PageViewControllerDataSource
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if self.currentPageInSpineIndex! + 1 < self.currentChapterPages {
self.currentPageInSpineIndex!++
} else if self.selectedChapter + 1 < self.tale?.chapters.count {
self.selectedChapter++
self.currentPageInSpineIndex = 0
}
self.pageContentViewControllerAtIndex(self.currentPageInSpineIndex!, onCompletePage: { (let pageContentViewController: PageContentViewController) -> Void in
return pageContentViewController
})
return nil
}
The pageContentViewControllerAtIndex method checks into de DB if it has the text of the chapter, and if not, it asks the server for it, then create a pagecontentviewcontroller with the text in it, and then returns it on the onCompletePage block, the thing is, that it always reachs the return nil line before the closure ends, so it doesn't work. I want to wait for the closure, have the pagecontentviewcontroller, and then return that result. Some ideas?

The pageViewController:viewControllerAfterViewController: method expects a UIViewController (or subclass) to be returned, you won't be able to use asynchronous code within this method to create and return a view controller.
Suggestions based on waiting for the asynchronous method to complete are not appropriate. This is because the UIPageViewController delegate methods are invoked on the main thread and introducing any waiting mechanism will block the UI resulting in an unresponsive feel and potentially causing the system to terminate the app.
What I would recommend doing is creating a view controller, return it, and load the content from that view controller. Most likely you will want to display some kind of loading indicator on that view until the content has been retrieved.

Have a look at my Swift library which has a bunch of functions to convert between asynchronous and synchronous functions: https://github.com/Kametrixom/Swift-SyncAsync There's a bunch of examples on the website, as well as a very detailed playground
You'd be able to do something this:
return toSync(pageContentViewControllerAtIndex)(currentPageInSpineIndex!)
EDIT: You're doing a web request -> asynchronous!

Related

How to read in data just the once when using a tabcontroller

I feel like I'm missing something and this should not be too hard.
I'm reading in some data in the initial scene in my app.
I've got a singleton and I make the call in viewDidLoad to singleton.getData().
This initial scene is part of a tab controller. And while I thought viewDidLoad would only get called once for each scene I'm pretty sure it's being called a few times during the lifecycle of my app.
So just wondering if there is a way to ensure a function call to retrieve some data only happens once.
viewDidLoad will be called when selected tab is changed, you can change the place you call getData.
If you want to call getData in viewDidLoad and be sure it won't be called multiple times you can create a flag and check, if it is previously called or not.
class Singleton {
static let sharedInstance = Singleton()
private static var getDataCalled = false
func getData() {
if Singleton.getDataCalled {
return
}
Singleton.getDataCalled = true
// request data
print("data requested")
}
}
Singleton.sharedInstance.getData()
Singleton.sharedInstance.getData()
Calling getData multiple times print data requested only once.

Swift iOS, UITableView nil

I have a Swift (2.2) iOS app (my first) with a couple of UITableViews. One of the views lists payments which are added / removed throughout the life of the program.
This all works fine 99% of the time, but a few times I have come across an issue where the UITableView all of a sudden becomes nil.
The IBOutlet must be hooked up correctly, or it would not work at all.
What could possibly be causing this when I am not assigning to the IBOutlet variable anywhere (just calling methods on it)?
Or (if I cannot find the cause), advice on best handling when it happens (if I need to recreate it, what about outlets, events, autolayout, etc.?)
#IBOutlet weak var paymentTableView: UITableView!
func handlePayment(payment: PaymentRecord) -> Void {
let existingPaymentIndex = payments.indexOf({ $0.payNo == payment.payNo })
if (existingPaymentIndex != nil) {
payments.removeAtIndex(existingPaymentIndex!)
}
if (self.paymentTableView == nil) { // Here is where I notice the issue
Log.error?.message("handlePayment: paymentTableView is nil!!")
return
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.paymentTableView.reloadData()
})
}
What is happening is that the tableview is being unloaded from memory when the view is disposed of. You likely have an asynchronous task completing, most usually from making a network call, that is returning and calling this function after the view was disposed. I've seen this happen many times when a view is trying to load a network resource and the user is able to switch to a different view before the network call's completion handler is called.

Many view controllers - performance issue

I have an IOS app that lets user swipe through weeks of notes. Each week is a UIViewController - the swiping and switching between the view controllers are handled by a UIPageViewController.
On startup all the view controllers are initialised with their data.
When the user swipes I grap a view controller like this:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if let currentPageViewController = viewController as? SinglePageViewController {
let currentIndex = currentPageViewController.index
return self.weeks[currentIndex - 1]
}
return nil
}
The app work flawless, until a use has many weeks, and thereby many view controllers. Startup time start to become an issue - and this will of cause only get worse as the weeks go on.
I've played around with initialising the each view controller when the user swipes. Like this:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if let currentPageViewController = viewController as? SinglePageViewController {
let currentIndex = currentPageViewController.index
let newVC = SinglePageViewController()
newVC.index = currentIndex - 1
return newVC
}
return nil
}
This approach works and the startup time is great - however, the swiping has now become sluggish and not smooth at all.
Can any one advise on how this issue can be resolved?
The second method (creation on demand) is the correct way to do it. If the swipping gets slow then because you spend to much CPU time in init(), viewDidLoad, viewWillAppear, etc... Look at the initialization and move every CPU intensive task to background threads.
If you depend on data to create the ViewController then you have to preload the data in advance. But it is not needed to preload the data for more then 2 or 3 of them. If it takes to much time and you still run into performane problems then you have to accept that the device is not fast enough for your requirements and you have to present the user an loading indicator. (like UIActivityIndicator)
If you need help in optimizing the initialization then post your code.
I had a similar issue with using too many UIScrollViews inside a UIScrollView. My solution was to monitor where the user was looking, by using the scrollViewDidScroll delegate method, hooked up to my container scrollview, and populate/remove view according to the direction of the user's scroll.
The direction you can get from let direction = scrollView.panGestureRecognizer.translationInView(scrollView.superview!) within the scrollViewDidScroll method.
Would this type of method work for you? I could give you more of my code if you'd like!

Go back to previous controller from class swift

I have an app that use http calls to stream video from external storage.
When the user's device isn't connected to a network service, I need the app to go back to the previous controller.
the flow is the following: the user access a list of elements (table view cell), select one, then the app goes to the player controller. On this controller, the call is made to stream the file.
I use an api call handler in a class outside of the controller and I don't know how to proceed to make it go back to the previous controller from here (the element list).
Connectivity issues errors are all catched within the api class.
I don't show any code as Im not sure it would be relevant. If you need to see anything, let me know and I will update the question. What should be the way to do that? (of course I use a navigation controller)
Thanks
If you want go back to the previous view controller you should use:
navigationController?.popViewControllerAnimated(true)
If you need to use this function not in the view-controller but in another class you can use NSNotificationCenter for notify the view-controller when it's needed to show the previous controller, just like this:
YourViewController
override func viewDidLoad()
{
...
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "goBack:",
name: "goBackNotification",
object: nil)
...
}
func goBack(notification: NSNotification)
{
navigationController?.popViewControllerAnimated(true)
}
AnotherClass
NSNotificationCenter.defaultCenter().postNotificationName("goBackNotification", object: nil)
Don't forget to remove the observer in your YourViewController:
deinit
{
NSNotificationCenter.defaultCenter().removeObserver(self)
}
EDIT 1: you can use obviously a delegate instead of a NSNotification method. If you don't know the differences between NSNotification and delegate I recommend to you this answer.
A common approach besides NSNotificationCenter is to utilize closures or delegates to inform your ViewController that the streaming attempt failed. Using closures, the API of the class responsible for the streaming could be extended to take a completion closure as parameter, and call it with an NSError, if one occurred, or nil if it didn't.
func streamFile(completion: NSError? -> Void) {
// Try to start the streaming and call the closure with an error or nil
completion(errorOrNil)
}
When the call to the API in the ViewController is made you can then pass a closure to the method and check for errors. In case something went wrong an error should be present and the ViewController should be dismissed
func startStream() {
StreamingAPIClient.streamFile(completion: { [weak self] error in
if error != nil {
// Handle error
self.dismissViewControllerAnimated(true, completion: nil)
} else {
// Proceed with the streaming
}
})
}

ios swift parse: methods with async results

When I go to a viewController I call within my viewDidAppear Method a function:
override func viewDidAppear(animated: Bool) {
getLessons()
}
This methods loads from parse.com a list of data I want to use in a pickerView.
The function itself:
func getLessons(){
var query = PFQuery(className:"Lesson")
query.orderByAscending("name")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
for object in objects {
var name = object["name"] as String
self.languagePickerKeys.append(object.objectId)
self.languagePickerValues.append(name)
self.selectedLanguage.text = self.languagePickerValues.first // set the first lessons name into the text field
self.selectedLessonObjectId = self.languagePickerKeys.first // set the first objectId for the lesson
self.languagePicker?.reloadAllComponents()
}
} else {
// Log details of the failure
println("\(error.userInfo)")
}
}
println("getLessons done")
}
The thing is, that the textfield is empty, as the getLesson() gets the data async and the data is not available to the textfield.
I also tried to put the getLesson into the viewDidAppear method, but this doesn't help me, the textfield is empty anyway.
What can I do, to have the data from the getLessons() method ready and loaded its first value into my textfield when the view is shown to the user?
You certainly have to get the data from asyncTask before setting it to pickerView.
Here's the ViewController lifecycle after instantiation:
Preparation if being segued to.
Outlet setting
Appearing and Disappearing.
So, you have two options:
Load the data in previous ViewController and then perform the segue. You need to follow these steps for it.
a. Create a segue from previous ViewController to your ViewController.
b. Call the function when you want to go next ViewController which fetches the data, and the end (after getting the data) call performSegueWithIdentifier which will lead to your ViewController.
c. Set the data in prepareForSegue
let navigationController = segue.destinationViewController as UINavigationController
navigationController.data = yourData //you got from async call
Now when you reach your ViewController, you are sure that your data is present, and you can set it to your pickerView.
If you want to do it in the same ViewController: here's is the lifeCycle of ViewController:so you need to call your function in viewDidLoad, and always set your pickerView after completion of the async network call.
Make sure that you initiate all changes to the UI from the main thread e.g. like so:
dispatch_async(dispatch_get_main_queue(), {
selectedLanguage.text = languagePickerValues.first
self.languagePicker?.reloadAllComponents()
})
The problem is that findObjectsInBackgroundWithBlock is an asynchronous method, so even if you fire it in the ViewDidLoad you will never know when you will receive the response data and you can't be sure that the data will be ready by the time you view appear.
I think you have just 2 possibility:
The first one is to load the data in the previous view controller and then just pass the data that got ready to you view controller.
The second is to use a synchronous method (the findobject method maybe?) and put the call in a method that is fired BEFORE the view appear (like the viewWillAppear: method). But your view will stuck for a moment (I think) while the data is retreiving... However this second solution probably resolve your problem but using synchronous method to retrieve data from a slower data source is usually bad design solution.
D.

Resources