buildMenu is called in AppDelegate but not UIViewController - ios

I'm attempting to create a custom menu for each view in my app, however it appears buildMenu is not being called in View Controllers. Here's an example:
In my AppDelegate, this code is used, which works 100% as expected.
override func buildMenu(with builder: UIMenuBuilder) {
print("Updating menu from AppDelegate")
super.buildMenu(with: builder)
let command = UIKeyCommand(
input: "W",
modifierFlags: [.command],
action: #selector(self.helloWorld(_:))
)
command.title = "Hello"
builder.insertChild(UIMenu(
__title: "World",
image: nil,
identifier: UIMenu.Identifier(rawValue: "com.hw.hello"),
options: [],
children: [command]
), atEndOfMenu: .file)
}
#objc private func helloWorld(_ sender: AppDelegate) {
print("Hello world")
}
However I need to change the options available in the menu depending on where the user is in the app, so I tried doing this in a UIViewController:
override func viewDidAppear(_ animated:Bool){
// Tried all of these to see if any work
UIMenuSystem.main.setNeedsRebuild()
UIMenuSystem.context.setNeedsRebuild()
UIMenuSystem.main.setNeedsRevalidate()
UIMenuSystem.context.setNeedsRevalidate()
}
and again..
// This is never called
override func buildMenu(with builder: UIMenuBuilder) {
print("Updating menu in View Controller")
}
but the buildMenu in the UIViewController is never called :(
Any ideas if this is intended behavior or if there are any workarounds?

For main menus, the system only consults UIApplication and UIApplicationDelegate, since main menus can exist without any window and hence without any UIViewController hierarchy. That's why your override on UIViewController doesn't get called for main menus.
For context menus, the system does consult the full responder chain starting at the view.
If you need to update main menu commands depending on their context:
You could leave buildMenu(with:) in UIApplicationDelegate, arrange for delegate to figure out when and what changed and call UIMenuSystem.main.setNeedsRebuild() when it does change, or
You could define a private method buildMyMenu(with:) in your UIViewController subclasses, and arrange for buildMenu(with:) in UIApplicationDelegate to call it, or
You could build a static menu in buildMenu, and rely on your overrides of canPerformAction(_:withSender:) and validate(_:) to enable or disable or even hide particular commands e.g. by updating the attributes property in your validate(_:) override.

This is the intended behavior. Quoting from docs:
Because menus can exist with no window or view hierarchy, the system only consults UIApplication and UIApplicationDelegate to build the app’s menu bar.
The same docs page explains how you can adjust the menu commands from view controllers, and there is a great sample project too, so make sure to check it.

Related

Embed iOS UIViewController in a Flutter widget

I have a Flutter fullscreen modal widget with a header, a footer and some content which should be rendered natively for iOS. I know I can host iOS UIViews in Flutter using Platform Views and I managed to do all the logic to get this working.
My issue is that I need to host a whole view controller within this widget, not only a simple view, and this view controller belongs to a third-party framework.
An option would be implementing the header and footer natively, but this would take a lot of time since this would involve passing lot of data, performing network requests, adding callbacks and so on. I read online that a UIKitViewController exists, but it can only be created from PlatformViewServices, which is still a work in progress and should not be used. I didn't manage to find proper documentation online.
I think you can try this.
class NativeView: NSObject, FlutterPlatformView {
private var _vc: UIViewController
init(
frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?,
binaryMessenger messenger: FlutterBinaryMessenger?
) {
_vc = UIViewController()
super.init()
}
func view() -> UIView {
return _vc.view
}
}
Calling _vc.view will call loadView() and viewDidLoad() when view is not initialized yet.

How to prevent timer reset using pushViewController method?

I'm trying to keep a timer running even if I switch view controllers. I played around with the Singleton architecture, but I don't quite get it. Pushing a new view controller seems a little easier, but when I call the below method, the view controller that is pushed is blank (doesn't look like the view controller that I created in Storyboards). The timer view controller that I'm trying to push is also the second view controller, if that changes anything.
#objc func timerPressed() {
let timerVC = TimerViewController()
navigationController?.pushViewController(timerVC, animated: true)
}
You need to load it from storyboard
let vc = self.storyboard!.instantiateViewController(withIdentifier: "VCName") as! TimerViewController
self.navigationController?.pushViewController(timerVC, animated: true)
Not sure if your problem is that your controller is blank or that the timer resets. Anyway, in case that you want to keep the time in the memory and not deallocate upon navigating somewhere else I recommend you this.
Create some kind of Constants class which will have a shared param inside.
It could look like this:
class AppConstants {
static let shared = AppConstants()
var timer: Timer?
}
And do whatever you were doing with the timer here accessing it via the shared param.
AppConstants.shared.timer ...
There are different parts to your question. Sh_Khan told you what was wrong with the way you were loading your view controller (simply invoking a view controller’s init method does not load it’s view hierarchy. Typically you will define your view controller’s views in a storyboard, so you need to instantiate it from that storyboard.)
That doesn’t answer the question of how to manage a timer however. A singleton is a good way to go if you want your timer to be global instead of being tied to a particular view controller.
Post the code that you used to create your singleton and we can help you with that.
Edit: Updated to give the TimeManager a delegate:
The idea is pretty simple. Something like this:
protocol TimeManagerDelegate {
func timerDidFire()
}
class TimerManager {
static let sharedTimerManager = TimerManager()
weak var delegate: TimeManagerDelegate?
//methods/vars to manage a shared timer.
func handleTimer(timer: Timer) {
//Put your housekeeping code to manage the timer here
//Now tell our delegate (if any) that the timer has updated.
//Note the "optional chaining" syntax with the `?`. That means that
//If `delegate` == nil, it doesn't do anything.
delegate?.timerDidFire() //Send a message to the delegate, if there is one.
}
}
And then in your view controller:
//Declare that the view controller conforms to the TimeManagerDelegate protocol
class SomeViewController: UIViewController, TimeManagerDelegate {
//This is the function that gets called on the current delegate
func timerDidFire() {
//Update my clock label (or whatever I need to do in response to a timer update.)
}
override func viewWillAppear() {
super.viewWillAppear()
//Since this view controller is appearing, make it the TimeManager's delegate.
sharedTimerManager.delegate = self
}

Can't make custom segue in Xcode 10/iOS 12

I'm having the hardest time implementing a presentation of a drawer sliding partway up on the screen on iPhone.
EDIT: I've discovered that iOS is not respecting the .custom modalTransitionStyle I've set in the Segue. If I set that explicitly in prepareForSegue:, then it calls my delegate to get the UIPresentationController.
I have a custom Segue that is also a UIViewControllerTransitioningDelegate. In the perform() method, I set the destination transitioningDelegate to self:
self.destination.transitioningDelegate = self
and I either call super.perform() (if it’s a Present Modal or Present as Popover Segue), or self.source.present(self.destination, animated: true) (if it’s a Custom Segue, because calling super.perform() throws an exception).
The perform() and animationController(…) methods get called, but never presentationController(forPresented…).
Initially I tried making the Segue in the Storyboard "Present Modally" with my custom Segue class specified, but that kept removing the presenting view controller. I tried "Present as Popover," and I swear it worked once, in that it didn't remove the presenting view controller, but then on subsequent attempts it still did.
So I made it "Custom," and perform() is still being called with a _UIFullscreenPresentationController pre-set on the destination view controller, and my presentationController(forPresented…) method is never called.
Other solutions dealing with this issue always hinge on some mis-written signature for the method. This is mine, verbatim:
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?
I've spent the last four days trying to figure out “proper” custom transitions, and it doesn't help that things don’t seem to behave as advertised. What am I missing?
Instead of using a custom presentation segue, you could use a Container View for your drawer. This way, you can use a UIViewController for your Drawer content, while avoiding the issue with the custom segue.
You achieve this in two steps:
First pull a Container View into your main view controller and layout it properly. The storyboard would look like this: (You can see you have two view controllers. One for the main view and one for the drawer)
Second, you create an action that animates the drawer in and out as needed. One simple example could look like this:
#IBAction func toggleDrawer(_ sender: Any) {
let newHeight: CGFloat
if drawerHeightConstraint.constant > 0 {
newHeight = 0
} else {
newHeight = 200
}
UIView.animate(withDuration: 1) {
self.drawerHeightConstraint.constant = newHeight
self.view.layoutIfNeeded()
}
}
Here, I simply change the height constraint of the drawer, to slide it in and out. Of course you could do something more fancy :)
You can find a demo project here.

Call method on all the existing instances of UIViewController

I want to be able to allow the user to change some properties of the graphical user interface immediately through all the app. To achieve that, I thought about creating a protocol like
protocol MyProtocol {
func changeProperties()
}
so each UIViewController can change those properties in its own way and then call this method in all the current instantiated controllers.
However, I don't know if this is possible. My first idea was to access the most root controller of the app, and then iterate through all the child recursively calling the method. Something like
func updatePropertiesFrom(_ vc: UIViewController) {
for child in vc.childViewControllers {
if let target = child as? MyProtocol {
target.changeProperties()
}
updatePropertiesFrom(child)
}
}
let appRootController = ...
updatePropertiesFrom(appRootController)
I don't know how to get that appRootController and I would like to know if there's any more elegant way for doing this. Thanks.
You can use NotificationCenter. E.g. define a notification name:
extension Notification.Name {
static let changeViewProperties = Notification.Name(Bundle.main.bundleIdentifier! + ".changeViewProperties")
}
And your various view controllers can then register to be notified when this notification is posted:
NotificationCenter.default.addObserver(forName: .changeViewProperties, object: nil, queue: .main) { _ in
// do something here
}
(If you're supporting iOS versions prior to iOS 9, make sure to remove your observer in deinit.)
And to post the notification when you want to initiate the change:
NotificationCenter.default.post(name: .changeViewProperties, object: nil)
You can access the app's root controller with:
UIApplication.shared.keyWindow.rootViewController
Of course this assumes the current key window is the one with your app's root controller.
You could also access the window property of your app delegate and get the rootViewController from that window.
Your general approach is valid. There is no other "more elegant" way to walk your app's current view controllers. Though you should also traverse presented view controllers as well as the child view controllers.
However, a simpler solution might be to post a notification using NotificationCenter and have all of your view controllers respond accordingly.
You could do this with a central dispatcher and having each UIViewController register with it.
Example:
class MyDispatcher{
static let sharedInstance = MyDispatcher()
var listeners: [MyProtocol]()
private init() {}
public func dispatch(){
for listener in listeners{
listener.changeProperties()
}
}
}
From each UIViewController you want to update the properties, you call MyDispatcher.sharedInstance.listeners.append(self). When you want to update the properties, you call MyDispatcher.sharedInstance.dispatch().

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