Updating an #EnvironmentObject var to pass data to PageViewController in SwiftUI results in loss of swiping between ViewControllers - uiviewcontroller

In my SwiftUI app, I currently have a PageViewController implemented using UIKit. It follows the traditional SwiftUI - UIKit implementation outlined in Apple's SwiftUI UIKit integration tutorials.
I have the data that populates a UIViewController, within the controllers array that is passed to the PageViewController, provided by an #Environment variable. In a different screen in the app, you can issue an action that causes the Environment object to update, triggering a re-render of a ViewController that lives within PageViewController.
This re-render, however, causes an issue as the ViewController is re-made with a new identifier and so the index of the viewController cannot be found in the parent.controller array within the Class Coordinator pageViewController function. This causes the index to default to nil and disables any swiping on the updated viewController. I am still able to navigate between viewControllers using the page control dots but I would like to identify how I can update the parent.controller array to include the new ViewController and to discard the old one.
After hours of searching, debugging and tinkering around I've not been able to find a way how I can reset the parents controller array with the new ViewController replacing the old view which has been discarded. Below is the code for the PageViewController.
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.view.backgroundColor = UIColor.clear
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: false)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
print(parent.controllers)
print(viewController)
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
}
}
}
After updating the environment state that populates the data for a given ViewController, causing a re-render, the coordinator class can print an array of controllers that include the old ViewController and also the new ViewController in the annotated code below but I have not yet been able to find a reliable way to ensure that the new ViewController effectively replaces the old one.
struct PageViewController: UIViewControllerRepresentable {
...
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
...
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
print(parent.controllers)
// prints out array of ViewControllers including old ViewController that has now been
// discarded
// [<_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c93de0>,
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c94be0>,
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c96830>,
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c976b0>]
print(viewController)
// prints out new ViewController that does not exist within the parent.controllers array
// and hence nil is returned from the guard
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd593721710>
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
...
}
Any help or guidance with this issue would be greatly appreciated!

Having the same issue, for few last hours, I found a work around.
It seems that it is a bug. If you look at the UIViewControllerRepresentable cycle, it should go from the Init > Coordinator > makeUIViewController > updateUIViewController.
But if the pages in the PageViewController get updated (our case). The pages get re-created by SwiftUI, great, but the PageViewController get an internal wrong creation cycle going from Init > updateUIViewController. Without makeUIViewController neither Coordinator. A new UIPageViewController is somehow created (How?) Leading to the discrepancy you noticed.
To solve the issue and force a proper recreation of the PageViewController, add .id(UUID) in your page view code, such as :
struct SGPageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#Binding var currentPage:Int
init(_ views: [Page], currentPage:Binding<Int>) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
self._currentPage = currentPage
}
var body: some View {
PageViewController(controllers: viewControllers, currentPage: $currentPage).id(UUID())
}
}
It will work, but you will notice that the scroll position of the pages are reset. Another bug ;)

think the problem stays in the Coordinator:
when you update controllers in pageviewcontroller, the parent property of Coordinator remains the same (and because it is a struct, all his properties). So you can simply add this line of code in your updateUIViewController method:
context.coordinator.parent = self
also, remember that animation of pageViewController.setViewControllers will occur, so you have to set animated to false, or handle it properly.
There are many other ways to solve this (I wrote the most intuitive solution),
the important thing is to know where the error comes from.

Related

Swift detect json parsing detect end

I am parsing a certain json url data to plot in a map and I need to detect that I have all the data to show a spinner while nothing is happening. I have created a variable that goes from false to true after I have all the data but that variable only exists as true inside the for loop
This is part of the code that gets the data
import SwiftUI
import MapKit
var locationsFillTest : Int = 0
var allLocations = [MKPointAnnotation]()
var doneGettingData : Bool = false
struct MapView: UIViewRepresentable {
var startdate : String
func makeUIView(context: Context) -> MKMapView{
MKMapView(frame: .zero)
}
func makeCoordinator() -> MapViewCoordinator{
MapViewCoordinator(self)
}
func updateUIView(_ uiView: MKMapView, context: Context){
uiView.removeAnnotations(allLocations)
allLocations = []
doneGettingData = false
print("Done = \(doneGettingData)")
let url = URL(string: "https://XXXXXX")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode(emsc.self, from: d)
DispatchQueue.main.async {
locationsFillTest = allLocations.count
doneGettingData = false
for locations in decodedLists.features {
let lat = Double(locations.properties.lat)
let long = Double(locations.properties.lon)
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: lat , longitude: long )
if locationsFillTest == 0 {
allLocations.append(annotation)}
}
uiView.addAnnotations(allLocations)
uiView.delegate = context.coordinator
uiView.showAnnotations(allLocations, animated: true)
doneGettingData = true
print("Done = \(doneGettingData)")
}
}else {
print("No Data")
}
} catch {
print("Error decoding JSON: ", error, response!)
}
}.resume()
}
}
The variable doneGettingData becomes false and true by watching the print but if I need to use it for example to create a spinner its false all the time since its only true inside.
How can I make it global ?
Thank you
Unless you have another declaration for doneGettingData inside the closure the instance level property is getting set to true. It may be getting set later than you expect though. Try the following to see when it changes (and to get you setup to react to those changes):
var doneGettingData : Bool = false {
didSet {
if doneGettingData {
print("The instance property doneGettingData is now true.")
} else {
print("The instance property doneGettingData is now false.")
}
}
}
You may want to make this into a custom enum though with cases along the lines of fetching, done, noData, and jsonError. Right now if there is no data you will never have a trigger to either retry, move on, notify the user, etc. The same applies when there is a decoding error. Or at the very least set the flag to true at the very end of the loop so something happens no matter what.
Something like:
enum DataCollectionState {
case fetching, done, noData, jsonError
var doneGettingData : DataCollectionState = fetching {
didSet {
switch doneGettingData {
case fetching:
// Show a spinner or something
case done:
// Hide the spinner
case noData:
// Tell the user there was no data? Try again?
case jsonError:
// Tell the user there was an error? Try again?
}
}
}
Note: I don't have Xcode open right now so syntax may not be exact.

Observing multiple Resources in Siesta

In my class I have multiple Resource Siesta objects that I want all loaded with data before I populate the table view and enable user input.
Here's the code:
var trainees: Resource? {
didSet {
oldValue?.removeObservers(ownedBy: self)
trainees?.addObserver(self).addObserver(statusOverlay, owner: self).loadIfNeeded()
}
}
var points: Resource? {
didSet {
oldValue?.removeObservers(ownedBy: self)
points?.addObserver(self).addObserver(statusOverlay, owner: self).loadIfNeeded()
}
}
var exercises: Resource? {
didSet {
oldValue?.removeObservers(ownedBy: self)
exercises?.addObserver(self).addObserver(statusOverlay, owner: self).loadIfNeeded()
}
}
func resourceChanged(resource: Siesta.Resource, event: Siesta.ResourceEvent) {
tableView.reloadData()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.trainees = DataManager.sharedInstance.api.trainees
self.points = DataManager.sharedInstance.api.points
self.exercises = DataManager.sharedInstance.api.exercises
}
So the question is what is the best way to check that everything has been loaded before calling reloadData in the resourceChanged func. Thanks.
Option 1
If you’re using Siesta’s built-in ResourceStatusOverlay, and if the same status overlay is observing all three resources, then the overlay won’t go away until all three are loaded. One option is to just let the overlay cover everything while they’re loading, even as the tableview gets partly populated underneath.
Option 2
If your logic breaks unless all the data is present, you can check that all three resources have data before touching the table view:
func resourceChanged(resource: Siesta.Resource, event: Siesta.ResourceEvent) {
if trainees.latestData != nil && points.latestData != nil && exercises.latestData != nil {
tableView.reloadData()
}
}
Note that doing it this way would leave the table view populated even after a resource.wipe() call. If you are logging the user out with service.wipeResources(), then this could lead to user data remaining on the screen even after logout. The solution would be to populate some data structure with either all the data or none, but to call reloadData() in either case:
func resourceChanged(resource: Siesta.Resource, event: Siesta.ResourceEvent) {
if trainees.latestData != nil && points.latestData != nil && exercises.latestData != nil {
dataToDisplayInTable = .......;
} else {
dataToDisplayInTable = [];
}
tableView.reloadData()
}
Option 3
Ensure that your logic handles resources with or without data, and populate the table view incrementally as the resources get populated:
func resourceChanged(resource: Siesta.Resource, event: Siesta.ResourceEvent) {
// This is made-up logic; insert your real logic here.
// Note that this example works because jsonArray returns []
// for a resource with no data.
dataToDisplayInTable = trainees.jsonArray
+ points.jsonArray
+ exercises.jsonArray
tableView.reloadData()
}
Because of the lazy cell creation in table views, this approach is rarely a performance concern.

Add "quick actions" to my iOS 9 app

I would like to add the quick actions of iOS 9 to my app.
I put this code in my app delegate:
import UIKit
enum ShortcutType: String {
case NewScan = "QuickAction.NewScan"
case Settings = "QuickAction.Settings"
}
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static let applicationShortcutUserInfoIconKey = "applicationShortcutUserInfoIconKey"
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
UIViewController.prepareInterstitialAds()
if(UIApplication.instancesRespondToSelector(Selector("registerUserNotificationSettings:"))) {
UIApplication.sharedApplication().registerUserNotificationSettings(UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil))
}
// QUICK ACTIONS
var launchedFromShortCut = false
if #available(iOS 9.0, *) {
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem {
launchedFromShortCut = true
handleShortCutItem(shortcutItem)
}
} else {
return true
}
return !launchedFromShortCut
}
/**************** QUICK ACTIONS ****************/
#available(iOS 9.0, *)
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) {
let handledShortCutItem = handleShortCutItem(shortcutItem)
completionHandler(handledShortCutItem)
}
#available(iOS 9.0, *)
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
var handled = false
if let shortcutType = ShortcutType.init(rawValue: shortcutItem.type) {
let rootNavigationViewController = window!.rootViewController as? UINavigationController
let rootViewController = rootNavigationViewController?.viewControllers.first as UIViewController?
rootNavigationViewController?.popToRootViewControllerAnimated(false)
switch shortcutType {
case .NewScan:
rootViewController?.performSegueWithIdentifier("goToCamera", sender: nil)
handled = true
case.Settings:
rootViewController?.performSegueWithIdentifier("goToSettings", sender: nil)
handled = true
}
}
return handled
}
}
Now I can make a force touch on my app icon > quick actions will be shown > I select the Quick Action "New Scan" > the app will open and show me the last view, which I have leave.
But the segue will not be execute.
Here is a part of my storyboard:
Explanation:
A: Navigation Controller and initiale Controller
B: ViewController, after a check this will make a segue to navigation Controller C
C: Navigation Controller
D: Table View Controller
E: ViewController
If I select New Scan with quick actions - I would like to show ViewController E.
It appears that you're doing things correctly based on the example code in the documentation. However, you've got a lot of optional chaining in your handleShortCutItem: implementation. Have you used the debugger to verify none of those expression have nil values? Also, from what I can see (although the image is blurry), the root view controller of the first nav controller in that storyboard does not have a segue to E. So I'm not sure how you intend to get there.
I would suggest that you set a breakpoint in your handleShortCutItem: implementation to first validate that the values you're working with are not nil and the code is actually executing. Once you've done this, you can use your storyboard to instantiate the view controls you want and just create an array of them as you want your view controller hierarchy to be in your navigation controller and set the navigation controller's viewControllers property to this array. Again, it's hard to tell exactly what you want from your image, but perhaps something like this:
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
guard let shortcutType = ShortcutType.init(rawValue: shortcutItem.type) else {
return false
}
guard let rootNavigationController = window?.rootViewController as? UINavigationController else {
return false
}
guard let rootViewController = rootNavigationController?.viewControllers.first else {
return false
}
guard let storyboard = rootNavigationController.storyboard else {
return false
}
var viewControllers = [rootViewController]
switch shortcutType {
case .NewScan:
// Instantiate the necessary view controllers for this case
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some view controller#>")]
...
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some other view controller#>")]
case.Settings:
// Instantiate the necessary view controllers for this case
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some view controller#>")]
...
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some other view controller#>")]
}
// Set the new view controllers array
rootNavigationController.setViewControllers(viewControllers, animated: false)
return true
}
Note: Since you tagged this question with Swift2, I've taken the liberty of adjusting the code to use guard statements.

iOS Swift "Extra argument in 'type' call" error when creating and calling a function

I'm having trouble understanding why I'm getting an error when creating and calling the function below. I used two argument both of type String. Why is this producing an error?
import UIKit
import AVFoundation
class PlaySoundsViewController: UIViewController {
func prepareAudio(sound: String, type: String) -> AVAudioPlayer {
var sound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(sound, ofType: type)!)
var error:NSError?
return AVAudioPlayer(contentsOfURL: sound, error: &error)
}
var audioPlayer = prepareAudio(sound: "movie_quote", type: "mp3")
I receive the Extra argument in 'type' call error when trying to set the audioPlayer variable to the result of the prepareAudio function.
The rest of the controller is below. Ultimately, I am trying to open up the mp3 file "movie_quote" and play it at a slower speed.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func slowSpeed(sender: UIButton) {
audioPlayer.enableRate = true
audioPlayer.rate = 0.5
audioPlayer.prepareToPlay()
audioPlayer.play()
}
In order for the code to work the way you have it, you need prepareAudio to be a class method, rather than an instance method. So the resulting code should look like this:
class PlaySoundsViewController: UIViewController {
class func prepareAudio(sound: String, type: String) -> AVAudioPlayer {
var sound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(sound, ofType: type)!)
var error:NSError?
return AVAudioPlayer(contentsOfURL: sound, error: &error)
}
var audioPlayer: AVAudioPlayer = PlaySoundsViewController.prepareAudio("movie_quote", type: "mp3")
}
Notice also that I changed the function call to prepareAudio("movie_quote", type: "mp3") because the first parameter doesn't have an external name by default. To change this, you can write class func prepareAudio(#sound: String, type: String) when defining the method.

How to let function A wait to proceed till a called function B is finished

The User is on a map view. When doing a long press somewhere on the map the following function gets triggered to set a new annotation inclusively a proper title.
func action(gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizerState.Began {
var newTouch: CGPoint = gestureRecognizer.locationInView(self.mapView)
var newCoordinate: CLLocationCoordinate2D = mapView.convertPoint(newTouch, toCoordinateFromView: self.mapView)
var newLocation = CLLocation(latitude: newCoordinate.latitude, longitude: newCoordinate.longitude)
var newAnnotation = MKPointAnnotation()
newAnnotation.coordinate = newCoordinate
CLGeocoder().reverseGeocodeLocation(newLocation, completionHandler: {(placemarks, error) in
if error != nil { println(error) }
let p: CLPlacemark = placemarks[0] as CLPlacemark
var thoroughfare: String? = p.thoroughfare
var subThoroughfare: String? = p.subThoroughfare
if p.thoroughfare == nil || p.subThoroughfare == nil {
var date = NSDate()
newAnnotation.title = "Added \(date)"
} else {
newAnnotation.title = thoroughfare! + " " + subThoroughfare!
}
})
self.mapView.addAnnotation(newAnnotation)
self.mapView.selectAnnotation(newAnnotation, animated: true)
places.append(["name":"\(newAnnotation.title)", "lat":"\(newCoordinate.latitude)", "lon":"\(newCoordinate.longitude)"])
}
}
I know it is working fine when keeping the last three lines of code within the CLGeocoder block (closure?). But if I separate those and list them after the }) (or put some of the code to another thread) I'm facing the problem that its running asynchronous (as I don't understand how to control async vs sync) and by the time the annotation is added to the map and saved to places its title is not set yet by the CLGeocoder.
A beginner to programming is asking: What would be necessary to be implemented (disptach_sync...-something) so the last lines of code wait for the CLGeocoder block to finish? I haven't managed to implement this command in the right way yet...
You correctly point out that that it works when you put those three lines within the geocoder's closure. So, you should do precisely that: Leverage this asynchronous pattern and put everything dependent upon the geocode process inside the closure.
In answer to your question, yes, there are patterns which can make an asynchronous task behave in a synchronous manner. For example, you can use dispatch groups or dispatch semaphores. But you are calling this from an IBAction, which runs on the main thread. And you never want to block the main thread. So any attempt to make this geocode request run synchronously from the main thread is ill-advised.
Furthermore, the above process is complicated by a wrinkle of reverseGeocodeLocation: Specifically, the closure, itself, runs on the main thread, so if you block the main thread waiting for the closure to finish, the app will deadlock. So the above patterns won't even work with reverseGeocodeLocation (if done from the main thread).
Bottom line, you should embrace the asynchronous patterns, keeping dependent code inside the closure.
As an aside, your example was a simple one, where you'd just put the code you want to perform inside the geocoder's closure. But what if you wanted to have two separate functions, for example, one for geocoding which returned an annotation, and another function for adding the annotation to the map. Perhaps you were anticipating something like:
func handleLongPress(gesture: UILongPressGestureRecognizer) {
if gesture.state == .Began {
let location = gesture.locationInView(self.mapView)
let annotation = geocodeLocationInMapView(self.mapView, location: location)
addAnnotationToMapView(self.mapView, annotation: annotation)
}
}
And then the question would be how would you have the second function, addAnnotationToMapView, wait for the first, geocodeLocationInMapView, to complete. Again, the answer is "you don't." Instead, just like Apple does with their asynchronous methods, you would employ a completion block pattern:
func handleLongPress(gesture: UILongPressGestureRecognizer) {
if gesture.state == .Began {
let location = gesture.locationInView(self.mapView)
geocodeLocationInMapView(self.mapView, location: location) { annotation, error in
guard annotation != nil && error == nil else {
print("error = \(error)")
return
}
self.addAnnotationToMapview(self.mapView, annotation: annotation!)
}
}
}
In that case, the geocodeLocationInMapView might look like:
func geocodeLocationInMapView(mapView: MKMapView, location: CGPoint, completion: (MKAnnotation?, NSError?) -> ()) {
let coordinate = mapView.convertPoint(location, toCoordinateFromView: mapView)
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
guard placemarks != nil && error == nil else {
completion(nil, error)
return
}
if let placemark = placemarks!.first {
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
let thoroughfare = placemark.thoroughfare
let subThoroughfare = placemark.subThoroughfare
switch (thoroughfare, subThoroughfare) {
case (nil, nil):
annotation.title = "Added \(NSDate())"
case (_, nil):
annotation.title = thoroughfare
default:
annotation.title = thoroughfare! + " " + subThoroughfare!
}
completion(annotation, nil)
}
}
}
And the addAnnotationToMapview might look like:
func addAnnotationToMapview(mapView: MKMapView, annotation: MKAnnotation) {
mapView.addAnnotation(annotation)
mapView.selectAnnotation(annotation, animated: true)
places.append(["name":"\(annotation.title)", "lat":"\(annotation.coordinate.latitude)", "lon":"\(annotation.coordinate.longitude)"])
}
This is, admittedly, a contrived example, and I'd suggest you handle your IBAction like I described at the start of this answer. But in answer to the broader question of "how do I have function A wait until function B is finished", you might employ a completion block pattern as outlined here.