3d touch navigate to specific viewController - segue

I am trying to implement 3d Touch and I want to navigate to a specific viewController in a Master - Detail app.
The viewController I am aiming to get to is the third controller in the hierarchy:
MasterViewController ----> SettingsViewController ----> ThirdViewController
I can access the ThirdViewController in normal operation through TabBarItem (connected via segue to SettingsViewController right from the IB) and then via a UIButton (connected via segue to ThirdViewController right from the IB) to the ThirdViewController.
i.e in MasterViewController ---> (tap barButtonItem) ----> SettingsViewControler ----> (tap uiButton) ---> ThirdViewController.
I have already amended the info.plist and 3D touch works fine and can call my action:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
var shortcutDictionary = shortcutItem.userInfo;
let shortcutString1 = shortcutDictionary!["key1"] as! String
if (shortcutString1 == "value1") {
// how do i get from here to the settingsController??
}
}
EDIT:
already have tried:
let viewController = MasterViewController() viewController.settingsButton.sendActionsForControlEvents(.TouchUpInside)
which gives me an error:fatal error trying to unwrap an optional value!
although this exists in MasterViewController
#IBOutlet weak var settingsButton: UIButton!

This is the code I use for the shortcut....
func actionFromShortcut(sender: AnyObject!) {
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.performSegueWithIdentifier("shotcutToSettings", sender: nil)
//some other stuff here.....
})
Also as per #MattLong's comment I have duplicated the segues to my controllers (one with and one without animation). That way the shortcut transition seems simultaneous when the action is called from the shortcut.
You will notice a delay of 0.1. It is to give the MasterViewController time to load cause whenever I tried to call the shortcut from cold (app not running) I would end up with the viewController I requested but there would be an empty MasterViewController to return to and hence crash....

Related

Unable to connect #IBSegueAction

I have single view controller with a UIButton containing the following code:
import UIKit
class ViewController: UIViewController {
required init?(coder: NSCoder) {
super.init(coder: coder)
}
#IBSegueAction
func makeAnotherController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ViewController? {
return ViewController(coder: coder)
}
}
According to Apple's docs, this is the correct signature for a #IBSegueAction, and it compiles OK, but I'm unable to connect the button to the #IBSegueAction by dragging from the storyboard scene to the assistant editor.
I can connect without issue to other outlets or actions.
Any thoughts? Xcode 11 bug or am I doing something wrong?
Update
Briefly, Apple's docs say…
Create a connection from a segue to an #IBSegueAction method on its source view controller. On new OS versions that support Segue Actions, that method will be called … An IBSegueAction method takes up to three parameters: a coder, the sender, and the segue’s identifier. The first parameter is required, and the other parameters can be omitted from your method’s signature if desired
I'm unable to complete the first step (create a connection)
This was my misunderstanding of Apple's docs. When they say…
Create a connection from a segue to an #IBSegueAction method on its source view controller.
they literally mean drag from the line that represents the segue to the view controller.

setting up LET in closure not returning?? Swift 4 [duplicate]

I have a simple function loading data from Firebase.
func loadFromFireBase() -> Array<Song>? {
var songArray:Array<Song> = []
ref.observe(.value, with: { snapshot in
//Load songArray
})
if songArray.isEmpty {
return nil
}
return songArray
}
Currently this function returns nil always, even though there is data to load. It does this because it doesn't ever get to the perform the completion block where it loads the array before the function returns. I'm looking for a way to make the function only return once the completion block has been called but I can't put return in the completion block.
(Variations on this question come up constantly on SO. I can never find a good, comprehensive answer, so below is an attempt to provide such an answer)
You can't do that. Firebase is asynchronous. Its functions take a completion handler and return immediately. You need to rewrite your loadFromFirebase function to take a completion handler.
I have a sample project on Github called Async_demo (link) that is a working (Swift 3) app illustrating this technique.
The key part of that is the function downloadFileAtURL, which takes a completion handler and does an async download:
typealias DataClosure = (Data?, Error?) -> Void
/**
This class is a trivial example of a class that handles async processing. It offers a single function, `downloadFileAtURL()`
*/
class DownloadManager: NSObject {
static var downloadManager = DownloadManager()
private lazy var session: URLSession = {
return URLSession.shared
}()
/**
This function demonstrates handling an async task.
- Parameter url The url to download
- Parameter completion: A completion handler to execute once the download is finished
*/
func downloadFileAtURL(_ url: URL, completion: #escaping DataClosure) {
//We create a URLRequest that does not allow caching so you can see the download take place
let request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 30.0)
let dataTask = URLSession.shared.dataTask(with: request) {
//------------------------------------------
//This is the completion handler, which runs LATER,
//after downloadFileAtURL has returned.
data, response, error in
//Perform the completion handler on the main thread
DispatchQueue.main.async() {
//Call the copmletion handler that was passed to us
completion(data, error)
}
//------------------------------------------
}
dataTask.resume()
//When we get here the data task will NOT have completed yet!
}
}
The code above uses Apple's URLSession class to download data from a remote server asynchronously. When you create a dataTask, you pass in a completion handler that gets invoked when the data task has completed (or failed.) Beware, though: Your completion handler gets invoked on a background thread.
That's good, because if you need to do time-consuming processing like parsing large JSON or XML structures, you can do it in the completion handler without causing your app's UI to freeze. However, as a result you can't do UI calls in the data task completion handler without sending those UI calls to the main thread. The code above invokes the entire completion handler on the main thread, using a call to DispatchQueue.main.async() {}.
Back to the OP's code:
I find that a function with a closure as a parameter is hard to read, so I usually define the closure as a typealias.
Reworking the code from #Raghav7890's answer to use a typealias:
typealias SongArrayClosure = (Array<Song>?) -> Void
func loadFromFireBase(completionHandler: #escaping SongArrayClosure) {
ref.observe(.value, with: { snapshot in
var songArray:Array<Song> = []
//Put code here to load songArray from the FireBase returned data
if songArray.isEmpty {
completionHandler(nil)
}else {
completionHandler(songArray)
}
})
}
I haven't used Firebase in a long time (and then only modified somebody else's Firebase project), so I don't remember if it invokes it's completion handlers on the main thread or on a background thread. If it invokes completion handlers on a background thread then you may want to wrap the call to your completion handler in a GCD call to the main thread.
Edit:
Based on the answers to this SO question, it sounds like Firebase does it's networking calls on a background thread but invokes it's listeners on the main thread.
In that case you can ignore the code below for Firebase, but for those reading this thread for help with other sorts of async code, here's how you would rewrite the code to invoke the completion handler on the main thread:
typealias SongArrayClosure = (Array<Song>?) -> Void
func loadFromFireBase(completionHandler:#escaping SongArrayClosure) {
ref.observe(.value, with: { snapshot in
var songArray:Array<Song> = []
//Put code here to load songArray from the FireBase returned data
//Pass songArray to the completion handler on the main thread.
DispatchQueue.main.async() {
if songArray.isEmpty {
completionHandler(nil)
}else {
completionHandler(songArray)
}
}
})
}
Making Duncan answer more precise. You can make the function like this
func loadFromFireBase(completionHandler:#escaping (_ songArray: [Song]?)->()) {
ref.observe(.value) { snapshot in
var songArray: [Song] = []
//Load songArray
if songArray.isEmpty {
completionHandler(nil)
}else {
completionHandler(songArray)
}
}
}
You can return the songArray in a completion handler block.

MPMusicPlayerController slow to respond when systemMusicPlayer, fast when application

I have a music app that uses the MPMusicPlayerController.
I originally wrote it using the systemMusicPlayer option under iOS 9.
I had some trouble with the player not shutting down correctly under certain circumstances so I switched to the appplicationMusicPlayer
(see Quitting app causes error "Message from debugger: Terminated due to signal 9")
However, as an application player, I can't get a lot of the benefits like control center handling, bluetooth data display, etc.
So, I switched back to the systemMusicPlayer.
I have also changed to Xcode 9.2 and a compile target of iOS 10.3.
Now when I run my app, it can take several seconds for it to respond to controls like play/pause or next/previous. My whole UI is painfully unresponsive.
I tried switching back to applicationMusicPlayer, recompiled, and sure enough - the UI is at normal speed.
So now I'm in a crappy position - with systemMusicPlayer, the app is barely usable, but with applicationMusicPlayer I lose a ton of capabilities.
This seems directly related to either iOS 11.2.2 on my iPhone, or something to do with targeting iOS 10.3+
Does anyone have any information about what is going on and how to fix it
EDIT:
I created a very basic player and it seems to work fine in either mode, so now I'm puzzled - I'll be testing other MP commands to see what the
issue is but since even my UI slows down I'm not sure.
EDIT 2:
I believe I've found the culprit to be NotificationCenter, and also getting States from the MPMusicPlayerController. I've updated my sample code below which shows the problem. Once running, clicking the 'next' button will be slow sometimes, but clicking 'previous' can cause delays of up to two seconds!!
Here is the basic code if you want to create a simple player.
Be sure to add three buttons in the storyboard and connect them accordingly.
//
// ViewController.swift
// junkplayer
//
import UIKit
import MediaPlayer
let notificationCenter = NotificationCenter.default
let myMP:MPMusicPlayerController = MPMusicPlayerController.systemMusicPlayer
//let myMP:MPMusicPlayerController = MPMusicPlayerController.applicationMusicPlayer()
class ViewController: UIViewController {
#IBOutlet weak var xxx: UIButton!
#IBOutlet weak var nextbut: UIButton!
#IBOutlet weak var prevbut: UIButton!
var qrySongs = MPMediaQuery()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
myMP.repeatMode = MPMusicRepeatMode.none
myMP.shuffleMode = MPMusicShuffleMode.off
myMP.prepareToPlay()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
stopMPNotifications()
startMPNotifications()
}
#IBAction func nextbut(_ sender: Any) {
myMP.skipToNextItem()
}
#IBAction func prevbut(_ sender: Any) {
myMP.skipToPreviousItem()
}
#IBAction func playbut(_ sender: UIButton) {
qrySongs = MPMediaQuery.songs()
myMP.setQueue(with: qrySongs)
myMP.play()
}
func startMPNotifications(){
notificationCenter.addObserver(self, selector: #selector(showNowPlaying), name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: myMP)
notificationCenter.addObserver(self, selector: #selector(handlePlayState), name: .MPMusicPlayerControllerPlaybackStateDidChange, object: myMP)
myMP.beginGeneratingPlaybackNotifications()
}
func stopMPNotifications(){
notificationCenter.removeObserver(self, name: .MPMusicPlayerControllerPlaybackStateDidChange, object: myMP)
notificationCenter.removeObserver(self, name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: myMP)
myMP.endGeneratingPlaybackNotifications()
}
#objc func handlePlayState(){
if myMP.playbackState == .playing {
print("handlePlayState playback state = playing")
}else{
print("handlePlayState playback state NOT playing")
}
print("handlePlayState going to shownowplaying")
showNowPlaying()
}
#objc func showNowPlaying(){
if myMP.nowPlayingItem != nil {
print("shownowplaying nowplaying not null")
}
}
}
The app seems to lock up once you start playing, but if you swipe up to show the Control Centre then dismiss it, the app starts working fine immediately.
I am having the same problem. I think it’s something wrong with the API. It's especially slow for large queries. But you can put a predicate on the quart, that allows no cloud music to go on it.
For anyone browsing this thread in the future, this was a known bug within all versions of iOS 11.2. It affected anyone using the systemMusicPlayer.
Apple sure does love to discourage us third party music app developers, eh? ;)
Regardless of my conspiracy theories (and I'm sure you have yours too), this bug was fixed in iOS 11.3.
We warn users on iOS 11.2 about the bug and recommend that they upgrade to iOS 11.3 for a (more or less) lag-free experience.
I ran into this problem today - working code just stopped running correctly, but I found a (partial) workaround:
\\Do anything that updates (like changing the song title in the UI) in the selector called by this notification
NotificationCenter.default.addObserver(self, selector: #selector(remoteMusicChange), name: NSNotification.Name.MPMusicPlayerControllerNowPlayingItemDidChange, object: nil)
The issue is that calls to say go to the next track for systemMusicPlayer are no longer (always) happening immediately. If you wait for the notification, at least you can be sure systemMusicPlayer has updated.
The problem with this is sometimes it can take a perceptibly long time for that notification to fire (and sometimes it's instant).
EDIT: https://forums.developer.apple.com/thread/96287 I am guessing this is related to these issues
EDIT2: Tested another related issue quickly in iOS12 and the problem no longer existed (changing playback speed) and the pauses when changing songs went away.

UIActivityViewController.completionWithItemsHandler is not called on iOS 10.0 when I select "Print" menu and cancel it

I created a simple application which uses UIActivityViewController as below.
let text = "Test Text"
let printData = UISimpleTextPrintFormatter(text: text)
let vc = UIActivityViewController(activityItems: [text, printData], applicationActivities: nil)
vc.completionWithItemsHandler = { (type,completed,items,error) in
print("completed. type=\(type) completed=\(completed) items=\(items) error=\(error)")
}
vc.modalPresentationStyle = .popover
vc.popoverPresentationController?.sourceView = self.openActivityButton
vc.popoverPresentationController?.sourceRect = self.openActivityButton.bounds
vc.popoverPresentationController?.permittedArrowDirections = .up
vc.popoverPresentationController?.delegate = self
self.present(vc, animated: true) { () in
}
and I run this application on iOS 10 (Xcode 8.0 beta 6).
When I close activity dialog, the completionWithItemsHandler is called.
When I select "Copy" activity, the completionWithItemsHandler is called.
When I select "Mail" activity and cancel it, the completionWithItemsHandler is called.
But, when I select "Print" activity and cancel it, the completionWithItemsHandler is not called.
This strange behavior occurred on iOS 10 but not occurred on iOS9 (the handler was called on iOS9)
Is this iOS 10's bug? If so, are there any workarounds to detect the UIActivityController is dismissed?
I pushed this sample app on https://github.com/kunichiko/ios10-activity-bug
I had a similar problem. In my case, I noticed that the completion handler was not called because the UIActivityController was already dismissed and deallocated. What I did was just add a property to hold a strong reference and set it to nil later. Then the completion handler was called properly.

iOS 6 Game Center authenticateHandler can't login after a cancel

When using the authenticateHandler in iOS 6, game center won't present the login view if the user cancels it. I realize game center will auto lockout an app after 3 cancel attempts, but I'm talking about just 2 attempts. If they cancel the login, they have to leave the app and come back before game center will present the login even through the authenticateHandler is getting set again. Any ideas on how to handle this case in iOS 6?
It works fine when using the older authenticateWithCompletionHandler method:
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_6_0
GKLocalPlayer.localPlayer.authenticateHandler = authenticateLocalPlayerCompleteExtended;
#else
[[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:authenticateLocalPlayerComplete];
#endif
The reason this is important for my app is that it requires Game Center for multi-player. The app tries to authenticate to game center on launch, but if the user cancels we don't ask them at launch again so they won't get nagged. What we do is show a Game Center Login button if they aren't logged in when they select multi-player. The login button forces a game center login by calling authenticateWithCompletionHandler (and now by setting GKLocalPlayer.localPlayer.authenticateHandler again).
Better use runtime checks (instancesRespondToSelector:) instead of preprocessor #if statements, so that you can use recommended methods where they are available and depreciated ones elsewhere. I actually found I need to distinguish three cases before setting the invite handler, as the authentication handler might also get called with a nil view controller:
-(void)authenticateLocalPlayer
{
if ([[GKLocalPlayer class] instancesRespondToSelector:#selector(setAuthenticateHandler:)]) {
[[GKLocalPlayer localPlayer] setAuthenticateHandler:^(UIViewController *gameCenterLoginViewController, NSError *error) {
if (gameCenterLoginViewController) {
[self.presentedViewController presentViewController:gameCenterLoginViewController
animated:YES
completion:^{
[self setInviteHandlerIfAuthenticated];
}];
} else {
[self setInviteHandlerIfAuthenticated];
}
}];
} else { // alternative for iOS < 6
[[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:^(NSError *error) {
[self setInviteHandlerIfAuthenticated];
}];
}
}
Yet more cases must be distinguished within the invite handler, as matchForInvite:: is new in iOS6 as well and avoids yet another round through game center view controllers:
-(void)setInviteHandlerIfAuthenticated
{
if ([GKLocalPlayer localPlayer].isAuthenticated) {
[GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) {
if (acceptedInvite) {
if ([GKMatchmaker instancesRespondToSelector:#selector(matchForInvite:completionHandler:)]) {
[self showInfoAnimating:YES completion:NULL];
[[GKMatchmaker sharedMatchmaker] matchForInvite:acceptedInvite
completionHandler:^(GKMatch *match, NSError *error) {
// ... handle invited match
}];
} else {
// alternative for iOS < 6
GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithInvite:acceptedInvite] autorelease];
mmvc.matchmakerDelegate = self;
// ... present mmvc appropriately
// ... handle invited match found in delegate method matchmakerViewController:didFindMatch:
}
} else if (playersToInvite) {
// ... handle match initiated through game center
}
};
}
}
Let me know if this helps.
I dont' think this is possible in iOS 6.0. There were API calls to do this in the early SDK builds that were removed before release.
In the WWDC 2012 Video: Session 516 - Integrating Your Games with Game Center [8:30] They actually show code where you call an authenticate method:
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
localPlayer.authenticationHandler = //handle the callback...
[localPlayer authenticate];
This method is now private API but you can see it in action by calling:
[[GKLocalPlayer localPlayer] performSelector:#selector(_authenticate)];
It does exactly what you want, but can't be used because it's now private.
You can also trigger the authentication process by posting the UIApplicationWillEnterForegroundNotification notification:
[[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillEnterForegroundNotification object:[UIApplication sharedApplication]];
I assume it would be unadvisable to do this in live code.

Resources