I have 2 View Controller Scenes. Scene2 holds settings that will be used in scene1. Scene2 is accessed by menu item and its presented as show.
Now the issue is that the settings used in scene2 will not be used by scene1 unless the latter is refreshed.
I did try to trigger a viewDidLoad() of scene1 when the settings were saved at scene2 but it crashed.
class replaceSegue: NSStoryboardSegue {
override func perform() {
if let fromViewController = sourceController as? NSViewController {
if let toViewController = destinationController as? NSViewController {
// no animation.
fromViewController.view.window?.contentViewController = toViewController
}
}
}
}
Related
I dragged a SplitViewController onto the main storyboard and changed the root view to use a custom class: SplitViewController
Inside SplitViewController:
class SplitViewController: UIViewController {
let tbc = UITabBarController()
override func viewDidLoad() {
[..]
// I thought this would work
tbc.tabBar.itemPositioning = .centered
let vc1 = UIViewController()
vc1.title = "Home"
let vc2 = UIViewController()
let vc3 = UIViewController()
tbc.setViewControllers([vc1, vc2, vc3], animated: false)
// I was experimenting here
// self.splitViewController!.maximumPrimaryColumnWidth = 50000
// One view controller is hidden for demo purposes:
self.splitViewController!.viewControllers = [
UINavigationController(rootViewController: tbc),
UINavigationController(rootViewController: [..])
]
}
}
I got this on iPad:
Should I drag from far left to right, the sidebar extends a bit and I can see the "hidden" tabBarItem (Home). How to get all tabBarItems to be in view?
I want to push a UIImagePickerViewController with a button in my parent view controller, but I want to display the image in a different view, not the parent view. I've tried
Pushing a new view and calling the image picker from a button there. However, because I have this view embedded inside a navigation view, I have a problem of 2 navigation bars. I can't hide navigation bar because then, I can't go back to the parent view.
Pushing a new view and calling the image picker directly (with no button). However, the image picker is not closing on its own and I can't go back to my parent view controller.
How about something like this:
Have an observed object to be the main object (your enviornmentObject).
Add a UIImage property to it or any property you want to share between views (after all that's the job of an environmentObject
Share the enviornmentObject
This is your class
class AppState: ObservableObject {
#Published var selectedImage: UIImage? = nil // default it to nil in case nothing is selected
}
This is your main view
struct ContentView: View {
#EnviornmentObject var appState: AppState
#State var presentModal: Bool = false
var body: some View {
VStack {
// Image can now easily be accessed by calling self.appState.selectedImage in any view that has #EnviornmentObject var appState: AppState
if(self.appState.selectedImage != nil) {
Image(uiImage: self.appState.selectedImage!)
} else {
// Image doesn't exist, add a placeholder
Text("No image selected")
}
Button("Show Modal") {
self.presentModal.toggle()
}
}.sheet(isPresented: self.$presentModal) {
ModalView(presentModal: self.$presentModal)
}
}
}
// Your ImagePicker view or any other view that will change the selected Image
struct ModalView: View {
#EnviornmentObject var appState: AppState
#Binding var presentModal: Bool = false
var body: some View {
// Your logic to pick image goes here, I will simulate a button click
Button("I will set an image") {
self.appState.selectedImage = UIImage(named: "test.jpg")
self.presentModal.toggle()
}
}
}
In your SceneDelegate (VERY IMPORTANT)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(AppState()) // <- The important part
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
Your modal view (or your image picker view, really anything)
EDIT: I agree it should be presented as a modal; however, it is not 100% necessary. Presenting it as a modal or not this should work though.
As the docs for UIUmagePickerController will tell you, it must be presented modally, not pushed. You are also responsible for dismissing it via the delegate.
I am trying to implement drop delegates on a NSCollectionViewController and having issues using a custom NSCollectionViewItem with an additional View Layer I've added onto the CollectionView Item. FWIW, The additional view is used draw a dashed border to indicate a drop area.
The drag event works fine on this collectionItem, and all other collectionItems without this view when it is hidden, but as soon as the drag event occurs on top of this view, the drag event pauses.
The drag event resumes as soon as the mouse is dragged outside of the view, but nothing happens if I release the drag while the mouse is over the view.
I would love to know what is happening here and how to prevent the custom view from "stealing" the mouse event from the CollectionViewContoller.
Delegate Method on DropViewController
func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionView.DropOperation>) -> NSDragOperation {
print("1")
if proposedDropIndexPath.pointee.item <= self.destinationDirectoryArray.count {
if proposedDropOperation.pointee == NSCollectionView.DropOperation.on {
return .move
}
} else if proposedDropIndexPath.pointee.item == self.destinationDirectoryArray.count {
//There's some stuff here validating the URL removed for brevity. It works okay when the focus is outside the view, but happy to add back in if helpful
if proposedDropOperation.pointee == NSCollectionView.DropOperation.on {
return .move
}
}
return[]
}
Configuring Collection View
func configureCollectionView() {
let flowLayout = NSCollectionViewFlowLayout()
flowLayout.minimumInteritemSpacing = 8.0
flowLayout.minimumLineSpacing = 8.0
destinationCollectionView.delegate = self
destinationCollectionView.dataSource = self
destinationCollectionView.register(NSNib(nibNamed: "DestinationCollectionItem", bundle: nil), forItemWithIdentifier: directoryItemIdentifier)
destinationCollectionView.collectionViewLayout = flowLayout
destinationCollectionView.registerForDraggedTypes([.fileURL])
destinationCollectionView.setDraggingSourceOperationMask(NSDragOperation.move, forLocal: true)
}
Collection View Item Setup
class DestinationCollectionItem: NSCollectionViewItem {
#IBOutlet weak var backgroundLayer: NSView!
override func viewDidLoad() {
super.viewDidLoad()
self.highlightState = .none
view.wantsLayer = true
view.layer?.cornerRadius = 8.0
backgroundLayer.isHidden = true
}
}
Custom Border View - Applied custom class in Xib and linked to File's Owner
class BorderedView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path : NSBezierPath = NSBezierPath(roundedRect: self.bounds, xRadius: 10.0, yRadius: 10.0)
path.addClip()
let dashHeight: CGFloat = 2
let dashLength: CGFloat = 7
let dashColor: NSColor = .lightGray
// setup the context
let currentContext = NSGraphicsContext.current!.cgContext
currentContext.setLineWidth(dashHeight)
currentContext.setLineDash(phase: 0, lengths: [dashLength])
currentContext.setStrokeColor(dashColor.cgColor)
// draw the dashed path
let cgPath : CGPath = CGPath(roundedRect: NSRectToCGRect(self.bounds), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil)
currentContext.addPath(cgPath)
currentContext.strokePath()
}
}
Well - I solved this one pretty quick.
While I previously tried adding unregisterDraggedTypes() to the backgroundLayer, the issue turned out to also be occurring on the image layer. I applied it to both the Image and backgroundLayer and it works now.
Collection View Item Setup
class DestinationCollectionItem: NSCollectionViewItem {
#IBOutlet weak var backgroundLayer: NSView!
override func viewDidLoad() {
super.viewDidLoad()
self.highlightState = .none
view.wantsLayer = true
view.layer?.cornerRadius = 8.0
backgroundLayer.isHidden = true
backgroundLayer.unregisterDraggedTypes()
self.imageView?.unregisterDraggedTypes()
self.textField?.unregisterDraggedTypes()
}
}
Generally, We can able to display next view controller from first view controller by having different kind of NSStoryboardSeque like Present, Show, Sheet etc., But, How we can achieve the same programmatically?.
Comparing with UIViewController, presenting a view controller modally by presentViewController:animated:. Is there any same kind of approach for NSViewController?
Thanks in advance.
The two different presentation types I use are:
func presentViewControllerAsModalWindow(_ viewController: NSViewController)
func presentViewControllerAsSheet(_ viewController: NSViewController)
After doing some more research another way to do using:
func presentViewController(_ viewController: NSViewController, animator: NSViewControllerPresentationAnimator)
And eating a custom presentation animator. Here you have the freedom to do what you like :)
In case someone is looking for the solution in 2022,
extension NSViewController {
func presentInNewWindow(viewController: NSViewController) {
let window = NSWindow(contentViewController: viewController)
var rect = window.contentRect(forFrameRect: window.frame)
// Set your frame width here
rect.size = .init(width: 1000, height: 600)
let frame = window.frameRect(forContentRect: rect)
window.setFrame(frame, display: true, animate: true)
window.makeKeyAndOrderFront(self)
let windowVC = NSWindowController(window: window)
windowVC.showWindow(self)
}
}
1.Create a NSViewController instance with StoryBoard Identifier
let theTESTVCor = self.storyboard?.instantiateController(withIdentifier: "TESTVCor") as! NSViewController
2.Present In Via the current NSViewController
theNSViewController.presentViewControllerAsModalWindow(theTESTVCor)
⚠️ DO NOT FORGET to set the Identifier of the NSViewController in Storyboard
If you have a view controller (presenting) than it's as simple as following function are provided:
open func presentAsSheet(_ viewController: NSViewController)
open func presentAsSheet(_ viewController: NSViewController)
open func present(_ viewController: NSViewController, asPopoverRelativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge, behavior: NSPopover.Behavior)
If you need to present a view controller in a new window (NOT MODAL) you need to create own NSWindow, NSWindowController
let gridView = NSGridView(views: [
[NSTextField(labelWithString: "label1"),NSTextField(labelWithString: "label2")],
[NSTextField(labelWithString: "label3"),NSTextField(labelWithString: "label4")]
])
let viewController = NSViewController()
viewController.view = gridView
let window = NSWindow(contentViewController: viewController)
window.center()
let windowController = NSWindowController(window: window)
windowController.showWindow(nil)
EXPLANATION:
Storyboards are using seques to perform some magic. The show seque is simply calling action "perform:" on object NSStoryboardShowSegueTemplate ([NSApp sendAction:to:from]). This seque will create NSWindowController and NSWindow (private method windowWithContentViewController:) for you and on top it will layoutSubviews/resize and center the window. Magic bonus is self retaining the window so you don't care about memory management.
Example of programatic calling (using Storyboards to instantiate windowController with viewController)
import Cocoa
import Contacts
class ShorteningHistoryWindowController : NSWindowController, Storyboarded {
static var defaultStoryboardName = "ShorteningHistory"
}
struct ShorteningHistory {
static let shared = ShorteningHistory()
private var windowController : NSWindowController
private init() {
windowController = ShorteningHistoryWindowController.instantiate()
}
public func showHistory() {
windowController.showWindow(self)
}
}
extension Storyboarded where Self: NSWindowController {
static var defaultStoryboardName: NSStoryboard.Name { return String(describing: self) }
static var defaultIdentifer: NSStoryboard.SceneIdentifier {
let fullName = NSStringFromClass(self)
let className = fullName.components(separatedBy: ".")[1]
return className
}
static func instantiate() -> Self {
let storyboard = NSStoryboard(name: defaultStoryboardName, bundle: Bundle.main)
guard let vc = storyboard.instantiateController(withIdentifier: defaultIdentifer) as? Self else {
fatalError("Could not instantiate initial storyboard with name: \(defaultIdentifer)")
}
return vc
}
}
PS: Don't forget to set Storyboard Identifiers in Storyboard
I am working on Xcode 6.1.1 on OSX 10.10. I am trying out storyboards for Mac apps. I have a NSTabViewController using the new NSTabViewControllerTabStyleToolbar tabStyle and it is set as the default view controller for the window controller. How do I make my window resize according to the current selected view controller?
Is it possible to do entirely in Interface Builder?
Here is what my storyboard looks like:
The auto layout answer is half of it. You need to set the preferredContentSize in your ViewController for each tab to the fitting size (if you wanted the tab to size to the smallest size satisfying all constraints).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = view.fittingSize
}
If your constraints are causing an issue below try first with a fixed size, the example below sets this in the tab item's view controller's viewWillAppear function (Swift used here, but the Objective-C version works just as well).
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = NSSize(width: 400, height: 280)
}
If that works, fiddle with your constraints to figure out what's going on
This solution for 'toolbar style' tab view controllers does animate and supports the nice crossfade effect. In the storyboard designer, add 'TabViewController' in the custom class name field of the NSTabViewController. Don't forget to assign a title to each viewController, this is used as a key value.
import Cocoa
class TabViewController: NSTabViewController {
private lazy var tabViewSizes: [String : NSSize] = [:]
override func viewDidLoad() {
// Add size of first tab to tabViewSizes
if let viewController = self.tabViewItems.first?.viewController, let title = viewController.title {
tabViewSizes[title] = viewController.view.frame.size
}
super.viewDidLoad()
}
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions, completionHandler completion: (() -> Void)?) {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.5
self.updateWindowFrameAnimated(viewController: toViewController)
super.transition(from: fromViewController, to: toViewController, options: [.crossfade, .allowUserInteraction], completionHandler: completion)
}, completionHandler: nil)
}
func updateWindowFrameAnimated(viewController: NSViewController) {
guard let title = viewController.title, let window = view.window else {
return
}
let contentSize: NSSize
if tabViewSizes.keys.contains(title) {
contentSize = tabViewSizes[title]!
}
else {
contentSize = viewController.view.frame.size
tabViewSizes[title] = contentSize
}
let newWindowSize = window.frameRect(forContentRect: NSRect(origin: NSPoint.zero, size: contentSize)).size
var frame = window.frame
frame.origin.y += frame.height
frame.origin.y -= newWindowSize.height
frame.size = newWindowSize
window.animator().setFrame(frame, display: false)
}
}
The window containing a toolbar style tab view controller does resize without any code if you have auto layout constraints in your storyboard tab views (macOS 11.1, Xcode 12.3). I haven't tried other style tab view controllers.
If you want to resize with animation as in Finder, it is sufficient to add one override in your tab view controller. It will resize the window with system-calculated resize animation time and will hide the tab view during resize animation:
class PreferencesTabViewController: NSTabViewController {
override func transition(from fromViewController: NSViewController, to toViewController: NSViewController, options: NSViewController.TransitionOptions = [], completionHandler completion: (() -> Void)? = nil) {
guard let window = view.window else {
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
return
}
let fromSize = window.frame.size
let toSize = window.frameRect(forContentRect: toViewController.view.frame).size
let widthDelta = toSize.width - fromSize.width
let heightDelta = toSize.height - fromSize.height
var toOrigin = window.frame.origin
toOrigin.x += widthDelta / 2
toOrigin.y -= heightDelta
let toFrame = NSRect(origin: toOrigin, size: toSize)
NSAnimationContext.runAnimationGroup { context in
context.duration = window.animationResizeTime(toFrame)
view.isHidden = true
window.animator().setFrame(toFrame, display: false)
super.transition(from: fromViewController, to: toViewController, options: options, completionHandler: completion)
} completionHandler: { [weak self] in
self?.view.isHidden = false
}
}
}
Please adjust closure syntax if you are using Swift versions older than 5.3.
Use autolayout. Set explicit size constraints on you views. Or once you have entered the UI into each tab view item's view set up the internal constraints such that they force view to be the size you want.