I'm attempting to emulate the Objective-C pattern of accessing an object's delegate using Swift. Normally I would put the protocol in a .h that is shared between the two UIViewControllers.
Purpose: to call a delegate (host) to dismiss a (pushed) UIViewController.
Problem: Unable to access the delegate's hello().
The following codes compile, but I get a run-time error: unknown delegate method (see below).
The Host/Calling (Delegate) Object:
import UIKit
class MainViewController: UIViewController, ProtocolNameDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
// ---------------------------------------------------------------------------------------------------------------
// Protocol Delegate
func hello() {
println("Hello");
}
// ---------------------------------------------------------------------------------------------------------------
#IBAction func greenAction(sender : AnyObject) {
let secondViewController = self.storyboard.instantiateViewControllerWithIdentifier("GreenViewController") as GreenViewController
secondViewController.delegate = self;
self.presentViewController(secondViewController, animated: true, completion: nil)
}
// ---------------------------------------------------------------------------------------------------------------
#IBAction func exitAction(sender : AnyObject) {
exit(0)
}
The pushed (second or 'green') UIViewController that is to be dismissed:
import UIKit
#class_protocol protocol ProtocolNameDelegate {
func hello()
}
class GreenViewController: UIViewController {
weak var delegate: ProtocolNameDelegate?
// ---------------------------------------------------------------------------------------------------------------
#IBAction func returnAction(sender : UIBarButtonItem) {
println("Inside returnAction")
delegate?.hello()
}
}
Revision: corrected delegate access to: delegate?.hello()...
And re-run the application. The following is via debugger:
(lldb) po delegate
Some
{
Some = {
payload_data_0 = 0x0d110b90 -> 0x00009a78 (void *)0x00009b70: OBJC_METACLASS_$__TtC9RicSwift218MainViewController
payload_data_1 = 0x0d110b90 -> 0x00009a78 (void *)0x00009b70: OBJC_METACLASS_$__TtC9RicSwift218MainViewController
payload_data_2 = 0x00000000
instance_type = 0x00000000
}
}
(lldb) po delegate.hello()
error: <REPL>:1:1: error: 'ProtocolNameDelegate?' does not have a member named 'hello'
delegate.hello()
(lldb) po delegate?.hello()
error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=2, address=0xb3145b0).
The process has been returned to the state before expression evaluation.
Question: What am I missing or doing wrong?
The issue here is that delegate is an Optional. So to access it you should do
delegate?.hello()
This makes it so you unwrap the optional, and call hello on it if it is non nil
ProtocolNameDelegate? is a distinct type from ProtocolNameDelegate, specifically Optional.
I had the same problem. I've been told that this is a bug in Swift (currently present in Xcode 6 beta 5). Strong protocols work well, but weak protocols not yet. The workaround is to declare the protocol as an Objective-C protocol with #objc protocol ProtocolNameDelegate instead of plain protocol ProtocolNameDelegate.
Since delegate is optional, you have to unwrap it, quickest way will be to use optional chaining:
delegate?.hello()
You'll also need to make sure that the delegate protocol is flagged as #objc (rather than #class_protocol in this case)
Related
I am currently trying to deploy a responsive website on mobile devices (iPhone & iPad) via a WebView.
Here are some details regarding the app :
the application's only purpose is to display the webView containing the website
using the WKWebView component since UIWebView is deprecated in my environment
any external links should load a new page within the webView
currently using Swift 3 and Xcode 9.2
made an Outlet between the WebKit WebView and the ViewController
edited the info.plist ; App Transport Security Setting > set NSAllowArbitraryLoads to true
source code :
ViewController.swift
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
// edit the url the app should load here
private var domain = "https://example.com/"
#IBOutlet weak var webView: WKWebView!
override func loadView() {
//delegates the navigation of other urls while still inside the webview
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = self
webView.allowsLinkPreview = false
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
// calls the website in a 'iframe' style
let url = URL(string: domain)
let request = URLRequest(url: url!)
webView.load(request)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping ((WKNavigationActionPolicy) -> Void)) {
// outputs are for debugging purpose
print("webView :\(webView)" + "\n")
print("decidePolicyForNavigationAction :\(navigationAction)" + "\n")
print("decisionHandler :\(decisionHandler)" + "\n")
switch navigationAction.navigationType {
case .linkActivated:
print("request : " + navigationAction.request.url!.absoluteString)
self.webView?.load(navigationAction.request)
return
default:
break
}
if let url = navigationAction.request.url {
print("url : " + url.absoluteString)
}
decisionHandler(.allow)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
AppDelegate.swift :
import UIKit
import WebKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
}
My trouble comes when trying to open external and internal links ; the app will throw a signal SIGABRT.
From what I understand, this error usually happens when there is an Outlet mismatch, but my project has only a single Outlet (WebView / ViewController). I tried deleting it and re-creating it to no avail.
If the issue is that no Outlet are made between the page loaded from an external link and my app I fail to see how to solve this since I have no control over the website.
Note : when I set webView.allowsLinkPreview to true (in func loadView()), the app loads the new page but takes a considerable amount of time. Also the function webView isn't called a second time when this happens.
Any help will be much appreciated.
Thank you.
Edit. as requested in a comment, here is what the debugger throws :
libc++abi.dylib: terminating with uncaught exception of type NSException (lldb)
Also, the Thread 1 SIGABRT signal appears to be in the AppDelegate class.
The interesting error message should be
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Completion handler passed to -[myAppName webView:decidePolicyForNavigationAction:decisionHandler:] was not called'
In webView(_ webView:, decidePolicyFor navigationAction:, decisionHandler:) in one particular case you don't do decisionHandler(someValue), that's why it crashes.
So in
case .linkActivated:
print("request : " + navigationAction.request.url!.absoluteString)
self.webView?.load(navigationAction.request)
return
You need to do decisionHandler(someValue) before you do the return.
I read quite a few questions and answers no this problem. Some are for Ojective C. Some are for iOS. The ones that were close to what I need didn't work.
I've set up a protocol for delegation. It doesn't work. The problem is that delegate variable isn't set. I need the reference to an active controller.
Delegator
protocol SwitchTabDelegate: class {
func selectTab(tab: Int)
}
class ViewController: NSViewController {
weak var delegate: SwitchTabDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func selectCompositions(_ sender: NSButton) {
if let delegate = self.delegate {
delegate.selectTab(tab: 2)
}
else {
print("self.delegate is nil")
}
print("delegate called")
}
}
Delegatee
class TabViewController: NSTabViewController, SwitchTabDelegate {
var viewController : ViewController?;
override func viewDidLoad() {
super.viewDidLoad()
//viewController = storyboard?.instantiateController(withIdentifier: "viewController") as? ViewController
// viewController?.delegate = self
// print(viewController)
}
func selectTab(tab: Int) {
print("In the delegate")
switchToDataTab()
}
func switchToDataTab() {
Timer.scheduledTimer(timeInterval: 0.2, target: self,
selector: #selector(switchToDataTabCont),
userInfo: nil, repeats: false)
}
func switchToDataTabCont(){
self.selectedTabViewItemIndex = 2
}
}
The delegatee is the main NSViewContoller. On the storyboard, it contains two buttons and a Container view controller. Embedded in the container view controller is the TabViewController, the delegatee. You can see in the delegatee where I tried to get a reference. It does get a reference, presumably to the newly instantiated instance. I need a reference to the original view controller that was spun up when the application started.
Answer
I added the following code to the delegator:
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
let controller = segue.destinationController as! TabViewController
self.delegate = controller as SwitchTabDelegate
}
That's not how it should work following the design pattern. The delegator should have no knowledge of the delegatee. I've spent way too much time on this issue so a hack is going to do.
When using storyboards, you want to "push" references to children when they are created vs. pulling them from an upstream controller. This is what -prepareForSegue:sender: is used for.
I am converting an app from Objective-C to Swift 2.0 and while converting an NSView-subclass, I am getting an error while trying to store a weak back-reference from a CALayer to the ActionViewItemInternal which is needed so that the layer drawing code can access the item data. I want to use a weak reference in order to avoid a retain-cycle (item object retains layer so layer needs a weak reference back to the item object).
class WeakReference {
weak var reference: AnyObject?
init(reference: AnyObject) {
self.reference = reference
}
}
class ActionView: NSView {
// Used to store an item in ActionView.items
private class ActionViewItemInternal {
var actionViewItem: ActionViewItem
var layer: CALayer
...
init(withItem actionViewItem: ActionViewItem) {
self.actionViewItem = actionViewItem
let layer = CALayer()
// !!!!!!!!!!!!!!!!!!!!!!!!!!
// other init and error here:
// !!!!!!!!!!!!!!!!!!!!!!!!!!
layer.setValue(value: WeakReference(reference: self), forKey: "ActionViewItem")
self.layer = layer
}
}
var items: [ActionViewItem] = [ActionViewItem]()
...
}
Where ActionItemView is defined as:
#objc protocol ActionViewItem {
var name: String { get set }
var icon: NSImage { get set }
var enabled: Bool { get set }
}
I am getting the error:
ActionView.swift:73:19: Cannot invoke 'setValue' with an argument list of type '(value: WeakReference, forKey: String)'
Xcode version: Xcode 7.0 beta 1. Deployment targets tried: 10.9, 10.10 and 10.11.
EDIT: I have tried using NSValue, as per #rickster's suggestion, but the error remains:
layer.setValue(value: NSValue(nonretainedObject: self), forKey: "ActionViewItem")
ActionView.swift:72:19: Cannot invoke 'setValue' with an argument list of type '(value: NSValue, forKey: String)'
The setValue(_:forKey:) method requires an ObjC object for the value — you're passing an instance of a pure Swift class. If you make your WeakReference class either annotated with #objc or inherit from NSObject that call should succeed.
However, you don't need your own class for this purpose anyway... The NSValue method valueWithNonretainedObject does the same thing.
Oh, and on closer inspection: you're using an extra argument label in your call. It should be like setValue(myvalue, forKey: mykey).
I am encountering a strange error:
2015-04-02 12:20:14.642 test[21167:257788] Failed to connect
(testApp) outlet from (test.AppDelegate) to (NSMenuItem): missing
setter or instance variable
inserted id: 122
I occured when a added a menuItem to a menu and connected a function to it.
I do not know what the Problem is. The app works fine but i don't think it is a smart idea to ignore the error.
What is meant by setter or instance variable? Why is it needed?
UPDATE: Here is the relevant code:
import Cocoa
import Foundation
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet weak var statusMenu: NSMenu!
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1)
func applicationDidFinishLaunching(aNotification: NSNotification) {
let icon = NSImage(named: "statusIcon")
statusItem.image = icon
statusItem.menu = statusMenu
// Time for constant repeat
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "timerRepeat", userInfo: nil, repeats: true)
}
// Method to call the tracking core
func timerRepeat() {
//....
}
#IBAction func frontEnd(sender: NSMenuItem) {
var targetURL : String = NSBundle.mainBundle().resourcePath!
targetURL = targetURL + "/" + "front.app"
let workspace = NSWorkspace()
workspace.launchApplication(targetURL)
}
#IBAction func menuClicked(sender: NSMenuItem) {
NSApplication.sharedApplication().terminate(self)
}
}
You have a broken outlet in your xib file. Usually it happens when you set up an outlet to ivar or property which is later deleted or renamed not using Xcode's rename feature.
Also make sure that your custom view or view controller class is added to your target. (Project => Target Name => Build Phases => Compile Sources). It's possible that a file is in your project but not your target.
This happens because you at one point created an #IBOutlet for a storyboard element. You then later removed the code (reference) from your swift file.
I created an example where I create two extra #IBOutlets (I named them 'correctField' and 'incorrectField'- both are incorrect though) and connected them from my storyboard to my swift file.
I then removed the code from my swift file. This generates the log as shown in the following figure :
To remove this kind of log message, you can do the following:
Go to the 'storyboard' and select the storyboard elements you created connections (#IBOutlets) from.
Open the 'connection inspector' as showed in the figure below
Remove the Referencing Outlets which are incorrect (in my case it is the 'correctField' and 'incorrectField')
Done
This was done in xCode 11
I'm trying to use a protocol / delegate in swift, and while I'm not getting any errors it seems that my delegate is not being created. It is returning nil on me, and I'm not sure why.
Here is my code
Class 1
import UIKit
protocol GameViewSliding{
func slideGameView()
}
class GameDetailsViewController: UIViewController {
var delegate:GameViewSliding?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func showOptions(sender: AnyObject) {
println("button pressed")
println(delegate)
delegate?.slideGameView()
}
}
Class 2 that conforms to the protocol
import UIKit
var currentHoleNumber:Int = 0
var parThree = false;
var parFive = false;
class GameViewController: UIViewController, GameViewSliding{
var gameDetailsVC:GameDetailsViewController = GameDetailsViewController()
override func viewDidLoad() {
super.viewDidLoad()
println("inside the game class")
gameDetailsVC.delegate = self
}
func slideGameView(){
println("this is from the root controller")
}
}
The problem is likely in the code you are not showing here. In prepareForSegue you usually want to set the delegate on the destination view controller.
Essentially you are setting it on an instance of the class that you are creating, but that isn't the instance that is actually shown. So the instance that is shown has no delegate.
Remove your local var of the second controller, and the setting of the delegate in the view did load and simply set it on the destination in prepare for segue and I bet it will work perfectly.