Is there any way to use storyboard and SwiftUI in same iOS Xcode project? - xcode

As Swift 5 introduces the SwiftUI framework for creating the views, but we are currently using the storyboard for UI design.
So I just wanted to know the procedure to use Storyboard and Swift UI in same iOS Single View Application.

I just started to look at the SwiftUI. Sharing a small example.
In the storyboard add Hosting View Controller
Subclass the UIHostingController with your own class (ChildHostingController)
ChildHostingController should look something like that:
import UIKit
import SwiftUI
struct SecondView: View {
var body: some View {
VStack {
Text("Second View").font(.system(size: 36))
Text("Loaded by SecondView").font(.system(size: 14))
}
}
}
class ChildHostingController: UIHostingController<SecondView> {
required init?(coder: NSCoder) {
super.init(coder: coder,rootView: SecondView());
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
For more details have a look at Custom UIHostingController
Apple Docs UIhostingController (Unfortunatelly it hasn't been documented yet)
Integrating SwiftUI Video

Mixing UIKit with SwiftUI
Both can be embedded and mixed SwiftUI in Storyboards and the other way around
UIViewControllerRepresentable used for embedding Storyboards in SwiftUI
Create the ViewController on a Storyboard and give it a Storyboard ID
import SwiftUI
struct ContentView: View {
var body: some View {
StoryboardViewController()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct StoryboardViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
let storyboard = UIStoryboard(name: "Storyboard", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier: "Main")
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Communication between UIKit and SwiftUI can be through #Binding variable. This will let SwiftUI view inject variable state and control the it, func updateUIViewController will be called to do the work when changed.
UIViewRepresentable used same way for views
import SwiftUI
struct ContentView: View {
#State var color = UIColor.green
var body: some View {
SampleView(color: $color).frame(width: 100, height: 100
, alignment: .center)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct SampleView: UIViewRepresentable {
#Binding var color: UIColor
func makeUIView(context: Context) -> some UIView {
return UIView()
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.backgroundColor = color
}
}
UIHostingController used for embedding SwiftUI in UIKit
First is to be created programmatically
let childViewController = UIHostingController(rootView: SwiftUIContentView())
addChild(childViewController)
childViewController.view.frame = frame
view.addSubview(childViewController.view)
childViewController.didMove(toParent: self)
Second with Xcode 11 by adding Hosting View Controller to the storyboard and create a segue to it and then create an #IBSegueAction by Control-drag from the segue to your ViewController and then create an instance of the SwiftUI view and pass it to HostingViewController initializer
Third is by adding Hosting View Controller to the storyboard and then subclassing it as mentioned in the previous answer above https://stackoverflow.com/a/58250271/3033056

Yes you can do that! Here are the steps you can take to do so:
Go to your current Xcode project -> Storyboard, click on the + sign (right upper corner) and search for Hosting Controller (just like you would for a button or label).
Drag Hosting Controller to your Storyboard. Create a Segue connection from your UI element (I'm using a button) to that Hosting Controller and select Push.
Create an outlet connection from that Segue to your View Controller (it's a new feature - just like you would create an outlet for a Label), and name it.
Declare your view inside of this outlet connection (you can do that, don't have to use PrepareForSegue method), and return it.
For example: I created a SwiftUI view in my current project (in Xcode: File -> New -> File -> SwiftUI View) and called it DetailsView. My outlet connection would look like this:
import UIKit
import SwiftUI
class ViewController: UIViewController {
#IBSegueAction func showDetails(_ coder: NSCoder) -> UIViewController? {
let detailsView = DetailsView()
return UIHostingController(coder: coder, rootView: detailsView)
}
override func viewDidLoad() {
super.viewDidLoad()
// some code
}
}
That's it! Now run it.

as Edgell Mentioned, there is a new ViewController named HostViewController that can host a SwiftUI page inside it.
there's a complete talk about integrating SwiftUI in existing project at WWDC that answers your question very well.
Integrating SwiftUI:
https://developer.apple.com/videos/play/wwdc2019/231/
WWDC19:
https://developer.apple.com/videos/

Add SwiftUI to a UIKit project:
1) Display a complete screen from SwiftUI:
If this is your case, you need just to follow this article or check one of the above answers.
2) Add SwiftUI view inside UIViewController:
In this case, follow the following steps:
Add this extension to your project:
This extension is copy pasted from a scale project in production:
import UIKit
import SwiftUI
extension UIViewController {
/// component: View created by SwiftUI
/// targetView: The UIView that will host the component
func host(component: AnyView, into targetView: UIView) {
let controller = UIHostingController(rootView: component)
self.addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
targetView.addSubview(controller.view)
controller.didMove(toParent: self)
NSLayoutConstraint.activate([
controller.view.widthAnchor.constraint(equalTo: targetView.widthAnchor, multiplier: 1),
controller.view.heightAnchor.constraint(equalTo: targetView.heightAnchor, multiplier: 1),
controller.view.centerXAnchor.constraint(equalTo: targetView.centerXAnchor),
controller.view.centerYAnchor.constraint(equalTo: targetView.centerYAnchor)
])
}
}
Use the extension inside your viewDidLoad:
import UIKit
import SwiftUI
class ViewController: UIViewController {
// MARK: - Outlets
// This view should have the needed constraints
// bc we are going to magically convert it to your `SwiftUI` view!
#IBOutlet weak var myUIView: UIView!
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Create instance from your `SwiftUI` view
let mySwiftUIView = SomeAmazingView()
// Use the extension
self.host(component: AnyView(mySwiftUIView), into: myUIView)
}
}
That's all, good luck!

You need to add a "Host View Controller" to the Storyboard. The SwiftUI form will be displayed in the Host View Controller and is callable from any Storyboard Form.
Be advised, the Host View Controller does not display in the Library for Xcode 11 on Mohave, you must be on Catalina. This is a bug, because once you have a project with a Host View Controller created on Catalina, that same project will run fine on Mohave, in fact, you can even copy that Host View Controller to other Storyboards and it will wok fine.

Related

Approach to use a SwiftUI view together with a UIView

I'm using a 3rd party library developed with UIKit. It's API needs a reference to a UIView.
How can I use this library inside SwiftUI? And how can I convert a SwiftUI view to a UIView?
I've tried creating a UIViewRepresentable like this:
struct SomeView: UIViewRepresentable {
let contentViewController: UIViewController
init<Content: View>(contentView: Content) {
self.contentViewController = UIHostingController(rootView: contentView)
}
func makeUIView(context: Context) -> UIKitView {
// Whatever I do here doesn't work. Frame is always 0
contentViewController.loadViewIfNeeded()
contentViewController.view.setNeedsDisplay()
contentViewController.view.layoutIfNeeded()
print(contentViewController.view.frame)
let uikitView = UIKitView()
uikitView.show(contentViewController.view)
return popover
}
func updateUIView(_ uiView: UIKitView, context: Context) {
}
}

Cocoa - Present NSViewController programmatically

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

Replace NSViewController under Swift2 Storyboard MAC OSX

I am new to Mac OSX and with Apple promoting the fact that the bodies of code are becoming similar decided to tell the folk I am writing code for we should be able to do a Mac OSX version. iPhone and iPad versions are all good and about to release second version so no issues there.
So I am subclassing NSWindowController to get access to the Toolbar and worked out how to remove and add items on the toolbar, but for the life of me I can not get one NSViewController (firstViewController) to dismiss and bring up the second NSViewController (secondViewController) in the same NSWindowController.
So the 2 issues are that
1. I want to be able to performSegueWithIdentifier from the first NSViewController in code and
2. bring up the second NSViewController by replacing the first NSViewController in the same NSWindowController.
If I add a button to the firstViewController and put a segue to the secondViewController then when I select the button the secondViewController comes up just fine but in a seperate window not the same NSWindowController that I want it to and the firstViewController does not get replaced but stays in the NSWindowController.
So I know the segue idea will work but its not working in code and when I do insert the segue from a button it works but into a seperate NSViewController that is not part of the NSWindowController.
I am trying to find some programming guide from Apple on the issue but no luck so far.
Here is an overview from my Storyboard:
Here is my NSWindowController subclassed and the func loginToMe2Team is trigger from the NSToolBar and its working just find as the print statements show up on the console.
import Cocoa
class me2teamWindowsController: NSWindowController {
#IBOutlet var mySignUp : NSToolbarItem!
#IBOutlet var myToolbar : NSToolbar!
let controller = ViewController()
override func windowDidLoad() {
super.windowDidLoad()
print("window loaded")
}
override func windowWillLoad() {
print("window will load")
}
#IBAction func logInToMe2Team(sender: AnyObject){
controller.LogIn() //THIS IS THE FUNC I AM TESTING WITH
}
#IBAction func signUpToMe2Team(sender: AnyObject){
controller.signUp()
}
Here is my NSViewController subclassed with the func LogIn. Its getting selected just fine but the performSegueWithIdentifier is not. And I did cut and past the Identifier to make absolutely sure it was the same.
import Cocoa
import WebKit
class ViewController: NSViewController {
#IBOutlet weak var theWebPage: WebView!
#IBOutlet weak var progressIndicator: NSProgressIndicator!
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "https://thewebpage.com.au"
self.theWebPage.mainFrame.loadRequest(NSURLRequest(URL: NSURL(string: urlString)!))
}
override func viewDidAppear() {
}
func LogIn() {
print("I logged in")
self.performSegueWithIdentifier("goToTeamPage", sender: self)
//THIS IS THE BIT THATS NOT WORKING
}
func signUp() {
print("I have to sign up now")
}
override var representedObject: AnyObject? {
didSet {
}
}
func webView(sender: WebView!, didStartProvisionalLoadForFrame frame: WebFrame!)
{
self.progressIndicator.startAnimation(self)
}
func webView(sender: WebView!, didFinishLoadForFrame frame: WebFrame!)
{
self.progressIndicator.stopAnimation(self)
}
}
You need to use a custom segue class (or possibly NSTabViewController if it’s enough for your needs). Set the segue’s type to Custom, with your class name specified:
…and implement it. With no animation, it’s simple:
class ReplaceSegue: NSStoryboardSegue {
override func perform() {
if let src = self.sourceController as? NSViewController,
let dest = self.destinationController as? NSViewController,
let window = src.view.window {
// this updates the content and adjusts window size
window.contentViewController = dest
}
}
}
In my case, I was using a sheet and wanted to transition to a different sheet with a different size, so I needed to do more:
class ReplaceSheetSegue: NSStoryboardSegue {
override func perform() {
if let src = self.sourceController as? NSViewController,
let dest = self.destinationController as? NSViewController,
let window = src.view.window {
// calculate new frame:
var rect = window.frameRectForContentRect(dest.view.frame)
rect.origin.x += (src.view.frame.width - dest.view.frame.width) / 2
rect.origin.y += src.view.frame.height - dest.view.frame.height
// don’t shrink visible content, prevent minsize from intervening:
window.contentViewController = nil
// animate resizing (TODO: crossover blending):
window.setFrame(window.convertRectToScreen(rect), display: true, animate: true)
// set new controller
window.contentViewController = dest
}
}
}

how to presentViewControllerAsSheet on OSX Mavericks?

It's a long story, but to cut it short; my first OSX app was written (on Yosemite) in Swift using a storyboard until I found out my (finished) app will not run on Mavericks. I need to run on Mavericks, so I have replaced the storyboard with NIBs.
My problem is with the segues; I was using 'sheet type' segues to show other view controllers in a sheet over the main view controller. A call to the presentViewControllerAsSheet method of NSViewController is a good replacement as it looks the same, but this API was introduced in Yosemite - so I need to work out how to do this for Mavericks.
In the action for a button on the main view, I've tried using beginSheet like this:
secondViewController = SecondViewController(nibName: "SecondViewController", bundle: nil)
self.view.window?.beginSheet(secondViewController!view.window!, completionHandler: nil)
But the second view controller's window is null at runtime. I've tried adding the new view controller as a subview to the application window but this is an unrecognised selector:
NSApplication.sharedApplication().windows[0].addSubView(secondViewController!.view)
I've search high and low for a description of how to show a sheet and all I can find is: Can a view controller own a sheet? but I'm sorry to admit I don't understand the answer. Can anybody help me with some working code? I'm beginning to worry that I'm trying to do something unusual but it looks OK on Yosemite, so how did people do this before Yosemite was released?
EDIT
I still haven't got to the solution, so I have put together a small app which shows the problems I'm having.
In AppDelegate.swift:
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var mainViewController: FirstView!
func applicationDidFinishLaunching(aNotification: NSNotification) {
mainViewController = FirstView(nibName:"FirstView", bundle: nil)
window.contentView = mainViewController.view
mainViewController.view.frame = (window.contentView as! NSView).bounds
}
}
In FirstView.swift (associated NIB has a 'open sheet' button)
class FirstView: NSViewController {
var secondView: SecondView?
var secondWindow: SecondWinCon?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func pressButton(sender: AnyObject) {
secondView = SecondView(nibName: "SecondView", bundle: nil)!
// method 1 - this is the behaviour I want (but it only works on OSX 10.10)
// presentViewControllerAsSheet(secondView!)
// method 2 - this just creates a floating window
// self.view.addSubview(secondView!.view)
// self.view.window?.beginSheet(secondView!.view.window!, completionHandler: nil)
// method 3 - this also creates a floating window
secondWindow = SecondWinCon(windowNibName: "SecondWinCon")
self.view.window?.beginSheet(secondWindow!.window!, completionHandler: nil)
}
}
In SecondView.swift (associated NIB has a 'close' button)
class SecondView: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func dismissPressed(sender: AnyObject) {
if (presentingViewController != nil) {
presentingViewController?.dismissViewController(self)
} else {
self.view.window?.sheetParent?.endSheet(self.view.window!)
}
}
}
In SecondWinCon.swift (Associated NIB is empty)
class SecondWinCon: NSWindowController {
var secondView: SecondView?
override func windowDidLoad() {
super.windowDidLoad()
secondView = SecondView(nibName: "SecondView", bundle: nil)!
self.window?.contentView.addSubview(secondView!.view)
}
}
If method 1 is uncommented, you will see the behaviour I'm trying to emulate (remember it only works on OS X 10.10). Method 2 or 3 displays the second view, but not as a sheet.
I have the same problem, and found maybe is't an issue related to view life cycle.
When I call presentViewControllerAsSheet in viewDidLoad, sheet will not shown, and you will get this in console:
Failed to set (contentViewController) user defined inspected property on (NSWindow): presentViewController:animator:: View '''s view is not in a window/view hierarchy.
If you trigger this in viewWillAppear or viewDidAppear, it's totally no problem.
UPDATE
Okay, let's make it clear.
For this initial storyboard, NSWindowController is connected with a view controller, think this as a root view controller (RootVC).
Create another view controller desired as a sheet in storyboard (SheetVC).
in viewWillAppear or viewDidAppear of RootVC, [self presentViewControllerAsSheet: SheetVC]
The sheet will show, no additional code required.
If you get here looking for a solution, I was nearly there with method 3. The important step I had missed was to turn off "Visible At Launch" in the NSWindowController's NIB (it's an attribute of the NSWindow). In my sample code, this was in SecondWinCon.nib.

Connect to ViewController from AppDelegate (Swift)

I have created a new OS X Cocoa Application using the standard Xcode Swift template (using StoryBoards).
I have implemented an IBAction in AppDelegate.swift to handle when the users selects "Open..." from the "File" menu. If the chosen file is a valid image file, I create an NSImage which I then want to display in the view of ViewController.
#IBAction func openFile(sender: NSMenuItem) {
var openPanel = NSOpenPanel()
openPanel.beginWithCompletionHandler { (result :Int) -> Void in
if result == NSFileHandlingPanelOKButton {
if let imageURL = openPanel.URL {
let image = NSImage(contentsOfURL: imageURL)
// PRESENT image IN THE VIEW CONTROLLER
}
}
}
However, I don't see any way to connect to ViewController from AppDelegate. I have only managed to find suggestions that I should look at self.window! in AppDelegate, but there is no such thing as a window in AppDelegate.
Thanks,
Michael Knudsen
It seems that AppDelegate can connect to objects only within Application Scene in a storyboard. If you want to get a ViewController, instantiate it from a storyboard.
sample:
#IBAction func menuAction(sender: AnyObject) {
if let storyboard = NSStoryboard(name: "Main", bundle: nil) {
let controller = storyboard.instantiateControllerWithIdentifier("VC1") as NSViewController
if let window = NSApplication.sharedApplication().mainWindow {
window.contentViewController = controller // just swap
}
}
}
You can access the mainWinow property and the contentViewController property to create a reference to your custom ViewController class. This is similar to the iOS rootViewController property.
let rootViewController = NSApplication.shared().mainWindow?.windowController?.contentViewController as! ViewController
Now you can use this reference to access IBOutlets on your main storyboard from your AppDelegate.
rootViewController.myTextView.textStorage?.mutableString.setString("Cats and dogs.")
This is good for a simple app with one Window with one ViewController.
I was stuck trying to do this same thing recently and managed to get the event I needed to update my view by creating the #IBAction in my ViewController and control dragging to my Application's First Responder (above the menu in my storyboard view).
Here's the question that got me out of the woods:
Application Menu Items Xcode
And thanks to Bluedome for the suggestion to connect it to First Responder's action.
If you control-drag from the menu to the first responder (red cube above menu) and picked an existing action, then you can "responder chain" to your view controller. In my case I attached Open to openFile and then in my view controller I added the following
override var acceptsFirstResponder: Bool {
return true
}
func openFile(sender: NSMenuItem) {
print("In view controller")
}
and it worked without any changes in AppDelegate. Most of the menus are already hooked up to first responder so just add the matching function name in your view controller.
See this comment and this document on Event Handling Basics for more info.
In Swift 5 and accessing new windows array:
#IBAction func menuAction(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateInitialViewController()
// The windows in the array are ordered from back to front by window level;
// thus, the last window in the array is on top of all other app windows.
// On app launch, UIApplication.shared.windows.count == 1 anyway.
if let window = UIApplication.shared.windows.last {
window.rootViewController = controller
}
}

Resources