I'm building an app with three views and I want the user to be able to swipe between them but also navigate between them by using custom buttons, like browsing in safari but only with three available views. The user can navigate between them in a somewhat random order and I provide back and forward buttons in a toolbar.
After reading Apple's documentation I believe history mode is the option I'm looking for as I can provide the views separately and navigation is not predefined like in book mode, however when I implement it it adds my view to the arrangedObjects property but the view itself doesn't change. I've correctly wired the page controller view programmatically to a subview of the app's contentView and I can see said view correctly displayed while debugging the view hierarchy.
The "Page Controller Managed View" is set as NSPageController.view programatically.
When I try using navigateForward(to:) the view gets added correctly to the arrangedObjects and the selectedIndex is correctly set but the NSPageController.view doesn't change at all (remains a blank NSView like the loaded view from storyboard). This is my code:
class MainViewController: NSViewController {
// MARK - Instance Properties
var notificationObservers: Array<NSObjectProtocol> = []
var pageController: NSPageController?
// MARK - Interface Builder Outlets
#IBOutlet var mainContentView: NSView? // Named "Page Controller Managed View" in storyboard
override func viewDidLoad() {
super.viewDidLoad()
pageController = (NSStoryboard.main!.instantiateController(
withIdentifier: "MainPageController") as! NSPageController)
pageController!.delegate = AppDelegate
pageController!.view = mainContentView!
notificationObservers.append(
NotificationCenter.default.addObserver(
forName: NSApplication.didFinishLaunchingNotification,
object: NSApp,
queue: .main,
using: { [weak self] (notification) in
// If the 'arrangedObjects' property is 0 it means no other object has set the first view
if self?.pageController!.arrangedObjects.count == 0 {
// Set the first view
self.pageController!.navigateForward(to: AppDelegate.firstView.nsView)
}
})
)
}
deinit {
// Perform cleanup of any Apple Notification Center notifications to which we may have registered
notificationObservers.forEach { (observer) in
NotificationCenter.default.removeObserver(observer)
}
}
}
This is the view hierarchy while running the app:
What am I doing wrong? Why is the page controller view not being updated with the navigateForward(to:) view?
This could shorten the amount of code I have to use by a lot compared to using view controllers (Book mode) so any help is appreciated.
Related
I'm trying to implement an IOS swift app which starts off with a tab bar controller, and in one of the tab bar controllers is an item called "account".
When a user presses the account item, I want the app to decide (onclick event) whether the view that contains sign up/login is displayed or the profile view is displayed based on a global variable "loggedIn" (bool type).
(I've tried navigation controller but what I've understood from that is that it is a sequence of views which can't decide between views)
I want to know how can this be implemented, maybe some kind of "router" if you may that can switch between views...
If you didn't understand here's a picture of what I'm trying to implement
Basic Map of what I'm trying to explain
If you can suggest a more professional way of doing such design please don't hesitate to express your opinion.
I believe a good approach is updating the view controller when loggedIn status changes. If you don't already have, create a class inheriting from UITabBarController to manage your tabs. Here's the code:
class TabController: UITabBarController {}
In your storyboard, select your tab controller, go to the Identity Inspector and set TabController as the custom class. Now TabController will manage all the view controllers in your tab bar.
It's usually not a good approach to use global variables, so let's add loggedIn in the scope of TabController and listen for any of changes in it and update the corresponding view controller:
class TabController: UITabBarController {
var loggedIn = true {
didSet {
updateProfileTab()
}
}
}
Now, whenever you change loggedIn, that change will update the proper tab. Now let's write updateProfileTab():
class TabController: UITabBarController {
func updateProfileTab() {
let viewController: UIViewController
if loggedIn {
viewController = makeProfileViewController()
} else {
viewController = makeLoginViewController()
}
setViewController(viewController, at: 2)
}
func makeProfileViewController() -> ProfileViewController {
// create and return the profile view controller
}
func makeLoginViewController() -> LoginViewController {
// create and return the profile view controller
}
}
Naturally, you might want to write the body of both makeProfileViewController and makeLoginViewController methods. The last thing for TabController is to write setViewController(_:at:) method:
class TabController: UITabBarController {
...
func setViewController(_ viewController: UIViewController, at index: Int) {
let tabBarItem = viewControllers?[index].tabBarItem
viewController.tabBarItem = tabBarItem
viewControllers?[index] = viewController
}
...
}
Now, since TabController manages your tab bar, you can access it from within any of its children view controllers:
guard let tabController = tabBarController as? TabController else { return }
tabController.loggedIn = ...
Also, it's important to select the initial state. So, in the viewDidLoad from one of your tabbed view controllers, you should perform the above code. The first tab (the one that displays first) is probably the best place to do that. Hope this helps!
EDIT
To create your login and signup view controllers, the easiest way to go is by assigning ids in your storyboard. To do that, go to your storyboard, select the view controller, and in the Identity Inspector set a Storyboard ID, which you will use to instantiate the view controller:
func makeProfileViewController() -> ProfileViewController {
let controller = self.storyboard!.instantiateViewController(withIdentifier: "theStoryboardID")
return controller as! ProfileViewController
}
Note that I'm using force unwrap here (!). That's just for brevity. In a real case scenario you will want to use some if let or guard let statements to handle nil values.
I'm having a Window Controller with a toolbar. I also have a View Controller containing some views. How do I reference a view from the View Controller within my Window Controller? I'm still learning macOS development and I'm missing the bigger picture how code is structured and classes are meant to interact.
My concrete problem right now is this: Using XCode 9.4.1 I have a window with a toolbar and a button in it. That's how my WindowsController.swift looks like:
import Cocoa
class WindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
window?.titleVisibility = .hidden
}
#IBAction func startExport(_ sender: NSButton) {
print("Start Export")
}
}
In the ViewControllerScene there's a WKWebView that's loading a web page. When the button in the toolbar is pressed, I want to call that Web Views takeSnapshot method. So I need a reference in WindowsController.swift to that Web View, but control-dragging the Web View from the storyboard to WindowsController.swift in the assistant editor doesn't let me create that outlet.
This:
let vc = contentViewController as? ViewController
will take you to your view controller.
I am making an app at the moment where 1 screen has a segmented control with 3 segments. Initially I had 1 table view and when you change segment I would simply change the data source/cell etc and reload the table. While this works great there is always the problem that when you change segments it will not remember your last scroll position because the table view gets reloaded.
I tried to get around this with storing offset position, rows etc but I could never get it to work like I wanted. Seems especially annoying when you have different cell types for the segments and they are self sizing as well.
I than decided to have a master view controller with the segmented control and 3 container views with their own VC and table view for each segment. I simply hide/show the correct container view when changing segments. This also works great but I have 1 problem with iOS 11 style large headers. Only the 1st container view added as a subview to the ViewControllers view manipulates the collasping/expanding of the title when you scroll.
Therefore when I change to the 2nd or 3rd container view and start scrolling I do not get the large title collapsing animation. How can I get around that?
I tried the following
1) Change Container view zPosition when changing segments
2) Move the container view to the front by calling view.bringSubview(toFront: ...)
3) Looping through the subviews and calling
view.exchangeSubview(at: 0, withSubviewAt: ...)
I believe I could remove all container views and add the one I need again and give them constraints but I wonder if there is a more straight forward solution.
Or if someone has a good solution to remember a tableViews scroll position before reloading it I would appreciate that too.
So I found an answer that seems to work for me, thanks to this great article. https://cocoacasts.com/managing-view-controllers-with-container-view-controllers/
Essentially what I did is
1) Remove the ContainerViews and Segues from the MasterViewController Storyboard.
2) Add a lazy property for each VC of the segmented control in the MasterViewController. They are lazy so that they only get initialised when you actually need them
lazy var viewController1: LibraryViewController = {
let viewController = UIStoryboard.libraryViewController // convenience property to create the VC from Storyboard
// do other set up if required.
return viewController
}()
lazy var viewController2: LibraryViewController = {
let viewController = UIStoryboard.libraryViewController // convenience property to create the VC from Storyboard
// do other set up if required.
return viewController
}()
3) Create an extension of UIViewController with the following 2 methods. I added them in an extension purely for code organisation as they might be reused on other ViewControllers.
extension UIViewController {
func add(asChildViewController viewController: UIViewController) {
// Add Child View Controller
addChildViewController(viewController)
// Add Child View as Subview
view.addSubview(viewController.view)
// Configure Child View
viewController.view.frame = view.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
func remove(asChildViewController viewController: UIViewController) {
// Notify Child View Controller
viewController.willMove(toParentViewController: nil)
// Remove Child View From Superview
viewController.view.removeFromSuperview()
// Notify Child View Controller
viewController.removeFromParentViewController()
}
}
4) Now in my segmented control method that gets called when you change segment I simply add the correct ViewController. The nice thing is that remove/adding them does not actually deallocate them.
func didPressSegmentedControl() {
if segmentedControl.selectedSegmentIndex == 0 {
remove(asChildViewController: viewController2)
add(asChildViewController: viewController1)
} else {
remove(asChildViewController: viewController1)
add(asChildViewController: viewController2)
}
}
5) Make sure you call the method at point 4 in ViewDidLoad so that the correct VC is added when the VC is loaded the 1st time.
func viewDidLoad() {
super.viewDidLoad()
didPressSegmentedControl()
}
This way when we remove a ChildViewController and add another one it will always be the the top VC in the subviews array and I get my nice title collapsing animation.
Another added benefit of this approach is that if you never go to a particular segment that particular VC will never get initialised, because they are lazy properties, which should help with efficiency.
Hope this helps somebody trying to do the same.
This is a horrible issue which I hope will be resolved soon, but there is another fix - although I freely admit that this is a nasty hack.
Basically, the issue only applies to the FIRST container view in the hierarchy. Add another container view to your hierarchy. Set it as hidden, then delete its segue and its target view controller just to be neat. Finally, make sure that this is the first container view in the hierarchy by dragging it to the top of the list.
The main ViewController is embedded in a UINavigationController subclass, and the VC has a subview that is loaded from a nib. The subview is called MenuView, and contains UIButtons that will link to other VCs.
To keep my main ViewController less unruly, I have put all these buttons into a subview that loads from a nib that animates the menu opening and closing.
However, I would like to present other view controllers from these, sometimes "Modally", sometimes "Show". What I have done seems to work, but I just want to know if this is alright, or if I have caused some unwanted effects that I'm unaware of (like a strong reference cycle that would cause a memory leak, or something). Or is there a better way to do this?
Some code:
In MenuView.swift
class MenuView: UIView {
var navigationController = CustomNavigationController()
func combinedInit(){
NSBundle.mainBundle().loadNibNamed("MenuViewXib", owner: self, options: nil)
addSubview(mainView)
mainView.frame = self.bounds
}
#IBAction func optionsAction(sender: AnyObject) {
self.navigationController.performSegueWithIdentifier("presentOptions", sender: self)
}
In ViewController.swift
menuView.navigationController = self.navigationController as! CustomNavigationController
Short answer: No, it is not alright to access a view controller from within some view in the hierarchy, because that would break all the MVC rules written.
UIView objects are meant to display UI components in the screen and are responsible for drawing and laying out their child views correctly. That's all there is. Nothing more, nothing less.
You should handle those kind of interactions between views and controllers always in the controller in which the view in question actually belong. If you need to send messages from a view to its view controller, you can make use of either the delegate approach or NSNotificationCenter class.
If I were in your shoes, I would use a delegate when view needs some information from its view controller. It is more understandable than using notification center as it makes it much easier to keep track of what's going on between. If the view controller needs some information from a view (in other words, the other way around), I'd go with the notification center.
protocol MenuViewDelegate: class {
func menuViewDidClick(menuView: MenuView)
}
class MenuView: UIView {
var weak delegate: MenuViewDelegate?
#IBAction func optionsAction(sender: AnyObject) {
delegate?.menuViewDidClick(self)
}
}
Let's look at what's going on at the view controller side:
class MenuViewController: UIViewController, MenuViewDelegate {
override func viewDidLoad() {
...
self.menuView.delegate = self
}
func menuViewDidClick(menuView: MenuView) {
navigationController?.performSegueWithIdentifier("presentOptions", sender: self)
}
}
For more information about communication patterns in iOS, you might want to take a look at this great article in order to comprehend how they work.
Im having trouble setting a nssplitviewcontroller's split view's view controller. I have a reference from the story board and am trying to set the items view controller programmatically:
override func viewDidLoad() {
dash = storyBoard.instantiateControllerWithIdentifier("dash_viewcontroller") as? NSViewController
print(dash)
main_view.viewController = dash!
}
I get this error from the console(doesn't crash) and doesn't show the programmatically set vc:
2016-02-21 10:03:19.475 HealthDash[62950:3960447] Failed to set (contentViewController) user defined inspected property on (NSWindow): Cannot remove a SplitViewItem's viewController if it is currently in a SplitViewController
Looks like the splitViewItem has a content controller that is actively being displayed. My guess: first you will have to remove that view controller from screen before you can replace it. Probably easier to create a new NSSplitItemView, add that to the NSSplitViewController and remove unwanted NSSplitItemView (and their associated view controllers).