Detect when UISplitViewController changes display mode - ios

I'm trying to use a UISplitViewController where the secondary controller should expose a "close" function (via a button or button bar item) whenever the UISplitViewController is in side-by-side mode, but should hide the function at other times. I tried putting this in the secondary view controller:
override func viewWillAppear(_ animated: Bool) {
if splitViewController!.primaryHidden {
// hide the "close" UI artifact
} else {
// show the "close" UI artifact
}
}
This correctly sets the visibility of the "close" function when the secondary view is first displayed, but if the UISplitViewController switches between expanded and collapsed (say, by rotating an iPhone 6s Plus), then this function is not called again (which makes sense, as the secondary controller remains visible). Consequently, the "close" function remains in its initial state--hidden or shown--even as the UISplitViewController changes mode.
How can I get the "close" function to hide or show in reaction to changes in the mode of the UISplitViewController?

There is the UIViewControllerShowDetailTargetDidChangeNotification notification for that:
// Sometimes view controllers that are using showViewController:sender and
// showDetailViewController:sender: will need to know when the split view
// controller environment above it has changed. This notification will be
// posted when that happens (for example, when a split view controller is
// collapsing or expanding). The NSNotification's object will be the view
// controller that caused the change.
UIKIT_EXTERN NSNotificationName const UIViewControllerShowDetailTargetDidChangeNotification NS_AVAILABLE_IOS(8_0);
Use as follows
- (void)viewDidLoad{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(showDetailTargetDidChange:) name:UIViewControllerShowDetailTargetDidChangeNotification object:self.splitViewController];
}
- (void)showDetailTargetDidChange:(NSNotification *)notification{
// changed from collapsed to expanded or vice versa
}
This Apple sample demonstrates how the table cell accessory changes from a disclosure indicator in portrait (denoting that a push will happen) to it being removed when changing to landscape split view:
https://developer.apple.com/library/archive/samplecode/AdaptivePhotos/Introduction/Intro.html
Note on iOS 13 beta, use addObserver with object nil because there currently is a bug they send the notification using the wrong object. They use their new UISplitViewControllerPanelImpl from the internal class cluster instead of the UISplitViewController object.
http://www.openradar.appspot.com/radar?id=4956722791710720

For future reference:
What about using the UISplitViewControllerDelegate??
It has a method called
splitViewController:willChangeToDisplayMode:
that should do exactly what you where looking for.
Documentation here

Okay, I found a simple solution. I was making a novice mistake. The trick is to override viewWillLayoutSubviews() instead of viewWillAppear(animated:). Then everything works as I want. It seems that viewWillLayoutSubviews() is called (sometimes more than once) every time the containing UISplitViewController changes its display mode, and that's exactly what I need to respond to. The only gotcha is that splitViewController might be nil on some of those calls, so it needs to be implemented like this:
override func viewWillAppear(_ animated: Bool) {
if let svc = splitViewController {
if svc.primaryHidden {
// hide the "close" UI artifact
} else {
// show the "close" UI artifact
}
}
}
As part of my stumbling around to find a solution, I tried overriding traitCollectionDidChange(previousTraitCollection:). (I tried this because I wanted to react to device rotations.) At first I thought I was onto something, because this function also get called whenever the device rotates. Interestingly (and, frustratingly) I found that my view's splitViewController property was nil when this function is called. It seems odd that this should be so, since neither viewDidDisappear(animated:) nor viewWillAppear(animated:) is called when the UISplitViewController reconfigures itself. But why it should be nil is, I suppose, a question for another day.

Related

Supported Interface Orientations in info.plist seem to block calls to shouldAutorotate

I’m designing an iPad/iPhone application in which the interface is designed to look as good in landscape as in portrait, and I’d like the interface to appear at launch as gracefully in one orientation as the other. At the same time, I need the ability to enable/disable autorotations at various times. I think I know how to do both these things, but they don’t seem to play nicely together.
I can set permitted device orientations in the Xcode project, and this seems to create a couple of array items in the project info.plist: "Supported interface orientations (iPad)" and "Supported interface orientations (iPhone)" which list the allowed orientations for each type of device. The presence of these items makes transitions from the launch screen as smooth as silk in all orientations.
I can enable/disable autorotations by overriding shouldAutorotate in my root view controller (there is only one controller at the moment). The problem is that the presence of Supported Interface items in info.plist seem to suppress all calls to shouldAutorotate, thus rendering control of autorotation inoperative.
class RootController: UIViewController
{
var allowAutorotation = true // My autorotation switch.
{ didSet { guard allowAutorotation else { return }
UIViewController.attemptRotationToDeviceOrientation()
/* Do other stuff. */ } }
override var supportedInterfaceOrientations: UIInterfaceOrientationMask
{ NSLog ("RootController: supportedInterfaceOrientations")
return .allButUpsideDown }
override var shouldAutorotate: Bool
{ NSLog ("RootController: shouldAutorotate")
return allowAutorotation }
override func viewDidLoad()
{ allowAutorotation = true
super.viewDidLoad() }
}
I’m using viewDidLoad to call attemptRotationToDeviceOrientation() at the earliest possible moment. If I don’t do this, the app screen appears in Portrait even when the device is in Landscape. (It appears correctly for Portrait, but Portrait isn’t correct.) I’ve tried making a similar call from other places, including didFinishLaunchingWithOptions in the app delegate, which I think may be the most logical place for it, but it doesn’t seem to make a difference.
If I remove the Supported Orientation items from info.plist, and define allowed orientations in code as shown, I can see from my NSLog telltales that calls to shouldAutorotate do occur at appropriate moments, but the launch then looks a trifle awkward in landscape. In the launch screen, the status bar is oriented wrong: along one of the vertical edges rather than at top, and when the transition to the app screen comes, it typically fades in canted about 10-20° off of horizontal. It instantly snaps to horizontal (it’s so quick, in fact, that it’s sometimes difficult to see), and the status bar is then correctly at the top, and from that moment on everything looks good, but the effect still seems a little unprofessional.
Since it only occurs momentarily, and only once at launch, I suppose I could live with it (I can’t live without an autorotation control) but I was hoping someone could suggest a way to get shouldAutorotate calls to work even with orientations defined in info.plist. Or perhaps some other strategy?
I think I have workarounds for these problems. To eliminate the anomalous rotation animation as the Launch Screen fades out, I use CATransactions to disable implicit animations until the app becomes active for the first time:
class RootController: UIViewController {
private var appIsLaunching = true // Set false when app first becomes active. Never set true again.
func appDidBecomeActive()
{ if appIsLaunching { appIsLaunching = false; CATransaction.setDisableActions (false) } }
override func viewWillTransition (to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
{ if appIsLaunching { CATransaction.setDisableActions (true) }
super.viewWillTransition (to: size, with: coordinator) }
}
And in the appDelegate:
func applicationDidBecomeActive ( _ application: UIApplication )
{ let root = window!.rootViewController as! RootController; root.appDidBecomeActive() }
I believe this works because a default CATransaction is always in effect, even without an explicit transaction. The cross-dissolve from Launch Screen to first view seems perfect.
There remains the status bar appearing in the wrong orientation in the Launch Screen. I’ve concluded it’s best to just turn it off, either in the project settings or by setting Status bar is initially hidden in info.plist. Not ideal, but acceptable.

UIButton selector not working after button tapped within WKWebView

I have a custom titleView with 2 custom UIButtons with arrow images that allow navigation to the next view controller in the paging structure. They work perfectly fine until a button is tapped within the WKWebView. Then they don't work anymore and the selector is not called. Note that other buttons in the nav bar still work (UIBarButtonItems). The buttons work properly again after the user swipes over to the next view controller.
After looking into it some, it looks like a WKCompositingView becomes first responder and if I override becomeFirstResponder() in a WKWebView subclass, the issue goes away. I'm still a little baffled though, and would like to understand the root of the problem.
class NonFirstRespondableWebView: WKWebView {
override func becomeFirstResponder() -> Bool {
return false
}
}
Does anyone have any insight into why this is happening?
Most UI elements in swift have a UIResponder. Unhandled events are passed up the responder chain to enclosing views. My guess is that the WKWebView is absorbing all touch events once the window has become active. You can learn more about the responder chain here
Regarding a first responder. From the docs:
The first responder is usually the first object in a responder chain to receive an event or action message. In most cases, the first responder is a view object that the user selects or activates with the mouse or keyboard.
Assuming you want to keep interactivity with the WKWebView fully functional (e.g. you need to bring up a keyboard or something), you can use
webView.resignFirstResponder()
To resign the responder at any time.
Otherwise, an extension that would give you the same functionality might look something like this:
extension WKWebView {
open override func becomeFirstResponder() -> Bool {
if self.superview?.superview is UIWebView {
return false
} else {
return super.becomeFirstResponder()
}
}
}

(Swift) Putting a NSNotificationCenter observer in ViewDidLoad isn't working for my project... Where should I put it?

I'm using SWRevealController to display 3 (left, center, right) panels. Basically, the right panel is a tableview of numbers and the middle panel shows possible even divisions when a user clicks on a number. I connected a segue to the IBAction of a reusable tablecell in the right view controller which loads the MainViewController. This all works fine. The problem is that if the number can't be divided evenly it triggers a notification, which the main view controller observes on ViewDidLoad. This notification sets the alpha of a pseudo-"alert" (UIView at the bottom of screen) to 1.0 for 4 seconds, at which point it returns to 0. Unfortunately this is where the problem starts: the notification box appears for a brief second while the animation runs but when the main viewcontroller finishes animating, the alert box disappears. I have a hunch it's because the ViewDidLoad fires at this point and resets the NSNotificationOberser – if I remove the segue on Touch Up Inside and manually switch view controllers the alert-box remains present.
Can you help me think of what I'm getting wrong? Like I said, I think it's because the observer is initialized in the ViewDidLoad. Assuming this is the case, where should I initialize the observer so that this doesn't happen anymore?
Basically the main VC displays a calculator, the code for which runs in a Calculator.swift file. If, when the number is passed through Calculate(), there is an error, it triggers an "alert" which the main VC picks up on, then reveals the box so that the user knows. Each time Calculate() is called, it logs the user's calculation to a tableview in the RVC – idea being they can reload previous calculations. Is this an improper usage of Notification Center?
The way I want the timeline of events to be:
User clicks on cell in the right panel
Main view controller (the calculator) is pushed via segue
Calculate() is called on the selected number, if there is a remainder a notification is triggered
The main view displays the results from Calculate(), if an alert fired then it would unhide a popup box on main view.
What is currently happening:
User clicks on the cell in right panel
Calculate() is called on the number, if there is a remainder the notification fires
Before the main view is pushed via segue, I can see briefly in the animation the result of the trigger firing and the calculation
As soon as the segue animation completes the view hides
My main VC Code (PopUpView is the alert box)
class CalculatorVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "triggerAlert:", name: "alert", object: nil)
self.PopUpView.alpha = 0.0
}
the triggerAlert function:
func triggerAlert(notification: NSNotification) {
PopUpView.alpha = 1.0
let returnedRemainder = (notification.userInfo)
let sample: Double = (returnedRemainder!["userTotal"]as! Double)
self.label.text = "Warning! Remainder: \(Double(round(100 * sample)/100))"
}
the tableView didSelect of the right VC:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
newUser.calculate()
//code here assigns the value in the selected table cell to newUser
}
Then in the Calculate() function:
dispatch_async(dispatch_get_main_queue(), {
NSNotificationCenter.defaultCenter().postNotificationName("alert", object: nil, userInfo: userTotalDictionary)
}
Notification observation/de-observation should generally be balanced between viewDidAppear/viewWillDisappear or viewWillAppear/viewDidDisappear. Either pair is usually fine, but it's wise to keep things that happen "when just offscreen" separate from things that happen "when just onscreen".
viewDidLoad is a poor place to set up observation, because you don't have a good place to balance removing the observation (viewDidUnload no longer exists). You should only remove observations in deinit that you set up in init, and view controllers really should never be observing things when they are not on screen.

Swift: Perform a function on ViewController after dismissing modal

A user is in a view controller which calls a modal. When self.dismissViewController is called on the modal, a function needs to be run on the initial view controller. This function also requires a variable passed from the modal.
This modal can be displayed from a number of view controllers, so the function cannot be directly called in a viewDidDisappear on the modal view.
How can this be accomplished in swift?
How about delegate?
Or you can make a ViewController like this:
typealias Action = (x: AnyObject) -> () // replace AnyObject to what you need
class ViewController: UIViewController {
func modalAction() -> Action {
return { [unowned self] x in
// the x is what you want to passed by the modal viewcontroller
// now you got it
}
}
}
And in modal:
class ModalViewController: UIViewController {
var callbackAction: Action?
override func viewDidDisappear(_ animated: Bool) {
let x = … // the x is what you pass to ViewController
callbackAction?(x)
}
}
Of course, when you show ModalViewController need to set callbackAction like this modal.callbackAction = modalAction() in ViewController
The answer supplied and chosen by the question asker (Michael Voccola) didn't work for me, so I wanted to supply another answer option. His answer didn't work for me because viewDidAppear does not appear to run when I dismiss the modal view.
I have a table and a modal VC that appears and takes some table input. I had no trouble sending the initial VC the modal's new variable info. However, I was having trouble getting the table to automatically run a tableView.reloadData function upon dismissing the modal view.
The answer that worked for me was in the comments above:
You likely want to do this using an unwind segue on the modal, that
way you can set up a function on the parent that gets called when it
unwinds. stackoverflow.com/questions/12561735/… – porglezomp Dec 15
'14 at 3:41
And if you're only unwinding one step (VC2 to VC1), you only need a snippet of the given answer:
Step 1: Insert method in VC1 code
When you perform an unwind segue, you need to specify an action, which
is an action method of the view controller you want to unwind to:
#IBAction func unwindToThisViewController(segue: UIStoryboardSegue) {
//Insert function to be run upon dismiss of VC2
}
Step 2: In storyboard, in the presented VC2, drag from the button to the exit icon and select "unwindToThisViewController"
After the action method has been added, you can define the unwind
segue in the storyboard by control-dragging to the Exit icon.
And that's it. Those two steps worked for me. Now when my modal view is dismissed, my table updates. Just figured I'd add this, in case anyone else's issue wasn't solved by the chosen answer.
I was able to achieve the desired result by setting a Global Variable as a boolean value from the modal view controller. The variable is initiated and made available from a struct in a separate class.
When the modal is dismissed, the viewDidAppear method on the initial view controller responds accordingly to the value of the global variable and, if needed, flips the value on the global variable.
I am not sure if this is the most efficient way from a performance perspective, but it works perfectly in my scenario.

Launch Watch App into middle view

Basically, my app is laid out in the page format and I would like it to launch into the middle of the three pages. There is no way of setting a previous page segue, so I have been trying to do it in code.
I have the main view set to the first view, and I have tried a variety of methods to segue to the middle view as soon as the app is launched.
Here is the two ways I tried:
if segueCheck == true {
self.pushControllerWithName("budget", context: self)
self.presentControllerWithName("budget", context: self)
segueCheck = false
}
The first presents the view, but as a completely separate view, and the second replaces the first view with the middle view.
Does anyone know how I can launch into the middle view and allow the user to swipe left and right of it?
Thanks.
WKInterfaceController's becomeCurrentPage() should be what you're looking for.
Let's create a new class for the center view controller, CenterPageViewController, and change its initWithContext: method as follows
import WatchKit
class CenterPageViewController: WKInterfaceController {
override init(context: AnyObject?) {
super.init(context: context)
super.becomeCurrentPage()
}
}
Now let's set the Custom Class for the middle page in your storyboard to CenterPageViewController
and finally hit run.
You won't be able to get rid of the initial transition from the left page to the center page, but the app will finally begin on the middle page.
Update Swift 3.0
class CenterPageViewController: WKInterfaceController {
override init (){
super.init()
super.becomeCurrentPage()
}
}
This will works...!!!
Thanks
The new way to do this in watchOS 4 and higher is:
WKInterfaceController.reloadRootPageControllers(withNames:
["Controller1" "Controller2", "Controller3"],
contexts: [context1, context2, context3],
orientation: WKPageOrientation.horizontal,
pageIndex: 1)
Now you don't get the annoying animation when using becomeCurrentPage() when you want to start with the middle page.

Resources