SwiftUI: How do I access a controller from within a coordinator (MKMapView delegate) inside a UIViewRepresentable? - uikit

Edit:
MapViews have the ability to show locations by adding pins (or custom views) to the map. I am attempting the simplest version of customization, where I am just trying to replace the default popup view (Title, Subtitle, Info symbol (i with a circle around it)) with a custom view.
Using a MKMapView from SwiftUI requires using a UIViewRepresentable class with a coordinator to handle its delegate, because MKMapView is a UIKit class, not a SwiftUI struct.
In a UIViewController, I could add a view (or a tableviewcontroller's main view even) as a subview to the annotationView. My question is how to do that using a native SwiftUI struct View.
I am trying to add a custom detailCalloutAccessoryView to a MKPinAnnotation (MKAnnotationView)
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
addDropDown(annotationView: annotationView)
}
Here's are two attempts at writing addDropDown():
func addDropDown(annotationView: UIView?) {
guard let annotationView = annotationView as? MKPinAnnotationView else { return }
// This is a UIKit UITableViewController Instance
let tvc = RandomCapitalDropDown(nibName: nil, bundle: nil)
annotationView.addSubview(tvc.view)
// error: Cannot convert value of type 'MKPinAnnotationView' to expected argument type 'UIViewController?'
tvc.didMove(toParent: annotationView)
tvc.view.leadingAnchor.constraint(equalTo: annotationView.leadingAnchor).isActive = true
tvc.view.trailingAnchor.constraint(equalTo: annotationView.trailingAnchor).isActive = true
tvc.view.topAnchor.constraint(equalTo: annotationView.topAnchor).isActive = true
tvc.view.bottomAnchor.constraint(equalTo: annotationView.bottomAnchor).isActive = true
}
// This is for a SwiftUI Struct called `PinDropDown`
func addDropDown(annotationView: UIView?) {
guard let annotationView = annotationView as? MKPinAnnotationView else { return }
let child = UIHostingController(rootView: PinDropDown(gameMode: .byContinent(continent: 0)))
child.view.translatesAutoresizingMaskIntoConstraints = false
child.view.frame = parent.view.bounds
// parent is UIViewRepresentable, so there is no view - this won't work
parent.view.addSubview(child.view)
// same issue
parent.addChild(child)
}
I have reviewed:
SwiftUI UIViewRepresentable and Custom Delegate
and
Access controller method from inside a model
and
How to find the frame of a swiftui uiviewrepresentable
none of these answered my question.

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) {
}
}

Creating a Scroll View Protocol in swift 2.2

I am currently developing an iOS application with login and sign up forms. To make sure that the keyboard does not cover any UITextFields I've implemented the following solution provided by Apple and discussed in this issue.
To briefly sum it up, this solution uses a UIScrollView in which the different UI elements are placed and UIKeyboardDidShowNotification and UIKeyboardDidHideNotification to move the elements up and down when the keyboard appears/disappears so that the UITextFields aren't hidden.
This works like a charm except for one thing: for all my UIViewControllers I have to repeat the same code. To tackle my problem I have tried:
to create a base UIViewController, providing an implementation for the different functions, that can be subclasses by the other UIViewControllers;
to use a protocol and a protocol extension to provide a default implementation for the different functions and make my UIViewControllers conform to it.
Both solutions didn't solve my problem. For the first solution, I wasn't able to connect the UIScrollView of my base class through the Interface Builder although it was declared.
#IBOutlet weak var scrollView: UIScrollView!
When trying to implement the second solution, the UIViewController implementing my protocol somehow did not recognise the declared methods and their implementations.
The protocol declaration:
protocol ScrollViewProtocol {
var scrollView: UIScrollView! { get set }
var activeTextField: UITextField? { get set }
func addTapGestureRecognizer()
func singleTapGestureCaptured()
func registerForKeyboardNotifications()
func deregisterForKeyboardNotifications()
func keyboardWasShown(notification: NSNotification)
func keyboardWillBeHidden(notification: NSNotification)
func setActiveTextField(textField: UITextField)
func unsetActiveTextField()
}
The protocol extension implements all functions expect for the addTapGestureRecognizer() as I would like to avoid using #objc:
extension ScrollViewProtocol where Self: UIViewController {
// The implementation for the different functions
// as described in the provided links expect for the following method
func registerFromKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidShowNotification, object: nil, queue: nil, usingBlock: { notification in
self.keyboardWasShown(notification)
})
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidHideNotification, object: nil, queue: nil, usingBlock: { notification in
self.keyboardWillBeHidden(notification)
})
}
}
Does anyone have a good solution to my problem, knowingly how could I avoid repeating the code related to moving the UITextFields up and down when the keyboard appears/disappears? Or does anyone know why my solutions did not work?
I found a solution. I'll post it in case someone once to do the same thing.
So, I ended up deleting the UIScrollView outlet in my base class and replacing it with a simple property that I set in my inheriting classes. The code for my base class look as follow:
import UIKit
class ScrollViewController: UIViewController, UITextFieldDelegate {
// MARK: Properties
var scrollView: UIScrollView!
var activeTextField: UITextField?
// MARK: View cycle
override func viewDidLoad() {
super.viewDidLoad()
let singleTap = UITapGestureRecognizer(target: self, action: #selector(singleTapGestureCaptured))
scrollView.addGestureRecognizer(singleTap)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
registerForKeyboardNotifications()
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
deregisterFromKeyboardNotifications()
}
// MARK: Gesture recognizer
func singleTapGestureCaptured(sender: AnyObject) {
view.endEditing(true)
}
// MARK: Keyboard management
func registerForKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWasShown), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWillBeHidden), name: UIKeyboardWillHideNotification, object: nil)
}
func deregisterFromKeyboardNotifications() {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWasShown(notification: NSNotification) {
scrollView.scrollEnabled = true
let info : NSDictionary = notification.userInfo!
let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue().size
let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize!.height, 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
var aRect : CGRect = self.view.frame
aRect.size.height -= keyboardSize!.height
if let activeFieldPresent = activeTextField {
if (!CGRectContainsPoint(aRect, activeFieldPresent.frame.origin)) {
scrollView.scrollRectToVisible(activeFieldPresent.frame, animated: true)
}
}
}
func keyboardWillBeHidden(notification: NSNotification) {
let info : NSDictionary = notification.userInfo!
let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue().size
let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, -keyboardSize!.height, 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
view.endEditing(true)
scrollView.scrollEnabled = false
}
// MARK: Text field management
func textFieldDidBeginEditing(textField: UITextField) {
activeTextField = textField
}
func textFieldDidEndEditing(textField: UITextField) {
activeTextField = nil
}
}
And here is the inheriting class code:
class ViewController: ScrollViewController {
#IBOutlet weak var scrollViewOutlet: UIScrollView! {
didSet {
self.scrollView = self.scrollViewOutlet
}
}
// Your view controller functions
}
I hope this will help!

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 would I reference the main storyboard in my app programmatically in swift?

How would I reference the main storyboard in my app programmatically in swift? I looked into the app delegate for a reference but so far I haven't found one.
Oh whoops I found the answer...
In another view controller of the that is connected to a storyboard you can simply use:
self.storyboard?
Or any object can get a storyboard by referencing its name and bundle:
let storyboard = UIStoryboard(name: "storyboardNameHere", bundle: nil) //if bundle is nil the main bundle will be used
It's easy. When I've come across a similar problem, I wrote a class that can obtain any resources from main bundle.
//Generate name of the main storyboard file, by default: "Main"
var kMainStoryboardName: String {
let info = NSBundle.mainBundle().infoDictionary!
if let value = info["TPMainStoryboardName"] as? String
{
return value
}else{
return "Main"
}
}
public class TPBundleResources
{
class func nib(name: String) -> UINib?
{
let nib = UINib(nibName: name, bundle: NSBundle.mainBundle());
return nib
}
//Main storybord
class func mainStoryboard() -> UIStoryboard
{
return storyboard(kMainStoryboardName)
}
class func storyboard(name: String) -> UIStoryboard
{
let storyboard = UIStoryboard(name: name, bundle: NSBundle.mainBundle())
return storyboard
}
//Obtain file from main bundle by name and fileType
class func fileFromBundle(fileName: String?, fileType: String?) -> NSURL?
{
var url: NSURL?
if let path = NSBundle.mainBundle().pathForResource(fileName, ofType: fileType)
{
url = NSURL.fileURLWithPath(path)
}
return url
}
class func plistValue(key:String) -> AnyObject?
{
let info = NSBundle.mainBundle().infoDictionary!
if let value: AnyObject = info[key]
{
return value
}else{
return nil
}
}
}
public extension TPBundleResources
{
//Obtain view controller by name from main storyboard
class func vcWithName(name: String) -> UIViewController?
{
let storyboard = mainStoryboard()
let viewController: AnyObject! = storyboard.instantiateViewControllerWithIdentifier(name)
return viewController as? UIViewController
}
class func vcWithName(storyboardName:String, name: String) -> UIViewController?
{
let sb = storyboard(storyboardName)
let viewController: AnyObject! = sb.instantiateViewControllerWithIdentifier(name)
return viewController as? UIViewController
}
//Obtain view controller by idx from nib
class func viewFromNib(nibName: String, atIdx idx:Int) -> UIView?
{
let view = NSBundle.mainBundle().loadNibNamed(nibName, owner: nil, options: nil)[idx] as! UIView
return view
}
class func viewFromNib(nibName: String, owner: AnyObject, atIdx idx:Int) -> UIView?
{
let bundle = NSBundle(forClass: owner.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
let view = nib.instantiateWithOwner(owner, options: nil)[idx] as? UIView
return view
}
class func viewFromNibV2(nibName: String, owner: AnyObject, atIdx idx:Int) -> UIView?
{
let view = NSBundle.mainBundle().loadNibNamed(nibName, owner: owner, options: nil)[idx] as! UIView
return view
}
}
Here are simple examples:
//Get a main storyboard
TPBundleResources.mainStoryboard()
//Get view controller form main storyboard
TPBundleResources.vcWithName("MyViewController")
//Get view from MyView.nib at index 0
TPBundleResources.viewFromNib("MyView", atIdx: 0)
//Get plist value by key
TPBundleResources.plistValue("key")
Even if #ManOfPanda's answer is correct, there are cases where you simply don't have a reference to a UIViewController, so you can grab it from the rootViewController of the UIWindow object from your AppDelegate.
// First import your AppDelegate
import AppDelegate
// ...
// Then get a reference of it.
let appDelegate = UIApplication().shared.delegate as! AppDelegate
// From there, get your UIStoryboard reference from the
// rootViewController in your UIWindow
let rootViewController = appDelegate.window?.rootViewController
let storyboard = rootViewController?.storyboard
You could also, of course, simply create a UIStoryboard by using (as #Mario suggested):
let storyboard = UIStoryboard(name: "storyboard", bundle:nil)
But that will, according to Apple documentation, create a new instance of the Storyboard (even if you already have one working). I always prefer to use an existing instance.
init(name:bundle:)
Creates and returns a storyboard object for the specified storyboard resource file.
init(name: String, bundle storyboardBundleOrNil: Bundle?)
Parameters
name: The name of the storyboard resource file without the filename extension. This method raises an exception if this parameter is nil.
storyboardBundleOrNil: The bundle containing the storyboard file and its related resources. If you specify nil, this method looks in the main bundle of the current application.
Source: Apple documentation
UIStoryboard * mainStoryboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];

Resources