EXC_BAD_ACCESS when an NSMenuItem's target/selector is set to an object - swift

I'm abstracting the NSMenuItem using this class:
class MenuItem{
let title: String
let iconName: String
let action: Action
init(title: String, iconName: String, _ action: #escaping Action) {
self.title = title
self.iconName = iconName
self.action = action
}
#objc func doAction(sender: NSMenuItem){
action()
}
}
And here's the static function that builds the menu:
static func getMenu(items: [MenuItem]) -> NSMenu{
let m = NSMenu()
for x in items{
let item = NSMenuItem()
item.title = x.title
item.target = x // If I remove this line or the line below, there won't be any crash
item.action = #selector(MenuItem.doAction)
item.image = x.iconName.toImage()
m.addItem(item)
}
return m
}
Now my problem is whenever the contextual menu is shown, the program crashes with EXC_BAD_ACCESS error.
However, when I comment out the line that sets the target or the action, then the problem will be gone (the menu then won't be clickable of course).
So how do I fix this? Thanks.
EDIT:
I should have stated that I already tried these things:
Using #selector(x.doAction) rather than #selector(MenuItem.doAction)
Using #selector(x.doAction(sender:))
Also, there is nothing in the output window. That's why I'm seeking help here. Worse, it involves EXC_BAD_ACCESS which I can hardly grasp given that memory is supposed to be managed by the system.

So the problem is that the items inside the array I'm passing to the static getMenu function are being deallocated after the getMenu function is complete (which is very soon followed by the popUpMenuWithEvent:forView).
I solved it by having a strong reference to that array.

Related

MVVM project. How to connect Model View to the View

I am trying to make an app that will show the weather in my city. I am using MVVM architecture and I have my Model, ModelView and View as follows. I have a variable inside the WeatherModelView class that I want to use in my view controller:
label.text = "(weatherViewModel.res?.main.temp ?? -123)"
but it does not work.
(https://i.stack.imgur.com/8BTEJ.png)
View Controller](https://i.stack.imgur.com/qW54n.png)
It does not give an error, it simply prints -123.0 on the label, which is the nil case after unwrapping. I would like it to print the actual weather. I don't think there are problems with the URL or the JSON decoding.
This is what is wrongfully shown when I run it: simulator
In "viewdidload" "fetchWeather" is not complete.
You need set "label.text" after it completed
change res in your view model
var res = WeatherModel? {
didSet {
resHasData?()
}
}
var resHasData?: (() -> Void)?
add my code in the last line "viewDidLoad"
weatherViewModel.resHasData = { [weak sekf] in
guard let self = self else { return }
self.label.text = "\(self.weatherViewModel.res?.main.temp ?? -123)"
}
good luck.

How do I execute functions from tableViewCell?

I currently have a custom type Cell that I use to populate cells info, style, etc... with. Here's the code
struct Cell {
var opened = Bool()
var isChecked = Bool()
var body = String()
var icon = UIImage()
var cellStyle: UITableViewCell.CellStyle?
var detailTextLabel: String?
var accessoryType: UITableViewCell.AccessoryType?
var action: CellDelegate?
}
I would like to add a property that contains a specific method for each cell. Most solutions I came across suggest using cell's tag and go from there but I can't imagine this being a sustainable solution. Most of those functions are literally to transition from one view to the other i.e. push views. The rest are used to toggle switches, update texts, etc...
I'm also open to other ideas on how I can do this.
Thanks
Edit: don't get distracted by CellDelegate type, it was basically one of my attempts in trying to get this to work
If your method has always the same signature then:
Swift solution:
add a property to the Cell
var action: (() -> Void)?
Objective-C solution:
add a property to the Cell with selector
var action: Selector? // action = #selector(customMethod) and cell perform a selector
If a method signature varies then you can use Any type
var action: Any?
and cell which calls the action must know the signature. This is done with casting:
class CellView: UITableViewCell {
private var cell: Cell!
fun configureWith(_ cell: Cell) {
self.cell = cell
}
override func select(_ sender: Any?) {
if let action = cell.action as? (String) -> Int {
action()
}
}
}

Can not insert items into NSOutlineView

I've been fighting with this NSOutlineView that should insert new items to the end of a group. My code is being called/hitting breakpoints but the new items don't appear in the NSOutlineView until I kill and restart the app.Not sure what I'm missing.
#objc func didReceaveNewFeeds(aNotification: Notification) {
guard let userinfo: [AnyHashable : Any] = aNotification.userInfo,
let newFeed: ManagedFeed = userinfo[Notification.Name.newFeedKey] as? ManagedFeed else {
return
}
let index: Int = sidebarDataSource?.allFeeds.count ?? 0
unowned let unownedSelf: MainViewController = self
DispatchQueue.main.async {
unownedSelf.sidebarDataSource?.allFeeds.append(newFeed)
unownedSelf.outlineview.insertItems(at: IndexSet([index]),
inParent: unownedSelf.sidebarDataSource?.allFeeds,
withAnimation: .slideRight)
}
}
I suspect the trouble you are having is because of this line:
unownedSelf.outlineview.insertItems(at: IndexSet([index]),
inParent: unownedSelf.sidebarDataSource?.allFeeds,
withAnimation: .slideRight)
I assume allFeeds is an array of data objects, and inParent argument expects an item (row) in the outline view's tree, or nil which would mean the root item.
You can get a reference to an outline view item with this method:
func item(atRow row: Int) -> Any?
You are correct in adding the new data to your data source with this line:
unownedSelf.sidebarDataSource?.allFeeds.append(newFeed)
So when you reload the application you are likely calling reloadData somewhere on the outline view and seeing the new data appear.
Because you are missing this sentence
unownedSelf.outlineview.expandItem(unownedSelf.sidebarDataSource?.allFeeds, expandChildren: true)

How to invoke a func passing it as argument between controllers

I'm trying to create a dynamic menu where I send the text and the action of a button from one controller to the next controller and then I want to generate the button that runs the action, but I'm having trouble with the syntax.
So as an example in the first controller i have:
let vc = storyboard.instantiateViewController(withIdentifier:
"CompetitiveKPIChart") as! CompetitiveKPIChartViewController
vc.Menu = [["Menu1":ClickBack()],["Menu2":ClickBack()],["Menu3":ClickBack()]]
func ClickBack() {
self.dismiss(animated: true, completion: {});
}
and in the second controller:
var Menu : [Dictionary<String,()>] = []
override func viewDidLoad() {
let gesture2 = UITapGestureRecognizer(target: self,action: Menu["Menu1"])
btnBack.addGestureRecognizer(gesture2)
}
How can I call the ClickBack() from the first controller in the second controller GestureRecognizer?
The syntax of your Menu declaration is wrong - it should be () -> () for a void function, not ().
This works in Playground, your syntax does not...
var menu : [Dictionary<String,() -> ()>] = [] // Don't use capitals for variables...
func aFunc() {
print("Hello aFunc")
}
menu = [["aFunc": aFunc]]
menu.first!["aFunc"]!() // Prints "Hello aFunc"
However, I think there is a second problem with your code. Your line
UITapGestureRecognizer(target: self,action: Menu["Menu1"])
cannot compile, as Menu is an array, not a dictionary. I think you probably meant
var menu : Dictionary<String,() -> ()> = [:]
in which case your set-up code should say
vc.menu = ["Menu1":ClickBack, "Menu2":ClickBack, "Menu3":ClickBack]
However, there is a final, and probably insurmountable problem, which is that your code
override func viewDidLoad() {
let gesture2 = UITapGestureRecognizer(target: self, action: menu["Menu1"])
btnBack.addGestureRecognizer(gesture2)
}
will still give an error, since action: needs to be a Selector, which is an #objc message identifier, not a Swift function.
I'm afraid I can't see a good way to make this (rather clever) idea work. Time to refactor?

Trigger UIAlertAction on UIAlertController programmatically?

There are a couple of existing questions on this topic but they aren't quite what I'm after. I've written a little Swift app rating prompt for my app which presents two UIAlertController instances, one triggered by the other.
I'm now trying to unit test this, and trying to reach that second alert in the tests. I've written a simple spy to check the first controller, but I'd like a way to trigger one of the actions on the first alert, which in turn shows the second.
I've already tried alert.actions.first?.accessibilityActivate(), but it didn't seem to break inside the handler of that action – that's what I'm after.
A solution that doesn't involve changing the production code to allow programmatic tapping of UIAlertActions in unit tests, which I found in this SO answer.
Posting it here as well as this question popped up for me when Googling for an answer, and the following solution took me way more time to find.
Put below extension in your test target:
extension UIAlertController {
typealias AlertHandler = #convention(block) (UIAlertAction) -> Void
func tapButton(atIndex index: Int) {
guard let block = actions[index].value(forKey: "handler") else { return }
let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self)
handler(actions[index])
}
}
Here's roughly what I did:
Created a mocked version of my class that would present the alert controller, and in my unit tests, used this mock.
Overrode the following method that I'd created in the non-mocked version:
func alertActionWithTitle(title: String?, style: UIAlertActionStyle, handler: Handler) -> UIAlertAction
In the overridden implementation, stored all the details about the actions in some properties (Handler is just a typealias'd () -> (UIAlertAction))
var didCreateAlert = false
var createdTitles: [String?] = []
var createdStyles: [UIAlertActionStyle?] = []
var createdHandlers: [Handler?] = []
var createdActions: [UIAlertAction?] = []
Then, when running my tests, to traverse the path through the alerts, I implemented a callHandlerAtIndex method to iterate through my handlers and execute the right one.
This means that my tests look something like this:
feedback.start()
feedback.callHandlerAtIndex(1) // First alert, second action
feedback.callHandlerAtIndex(2) // Second alert, third action
XCTAssertTrue(mockMailer.didCallMail)
I took a slightly different approach based on a tactic I took for testing UIContextualAction—it's very similar to UIAction but exposes its handler as a property (not sure why Apple wouldn't have done the same for UIAction). I injected an alert actions provider (encapsulated by a protocol) into my view controller. In production code, the former just vends the actions. In unit tests, I use a subclass of this provider which stores the action and the handler in two dictionaries—these can be queried and then triggered in tests.
typealias UIAlertActionHandler = (UIAlertAction) -> Void
protocol UIAlertActionProviderType {
func makeAlertAction(type: UIAlertActionProvider.ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction
}
Concrete object (has typed titles for easy retrieval later):
class UIAlertActionProvider: UIAlertActionProviderType {
enum ActionTitle: String {
case proceed = "Proceed"
case cancel = "Cancel"
}
func makeAlertAction(title: ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction {
let style: UIAlertAction.Style
switch title {
case .proceed: style = .destructive
case .cancel: style = .cancel
}
return UIAlertAction(title: title.rawValue, style: style, handler: handler)
}
}
Unit testing subclass (stores actions and handlers keyed by ActionTitle enum):
class MockUIAlertActionProvider: UIAlertActionProvider {
var handlers: [ActionTitle: UIAlertActionHandler] = [:]
var actions: [ActionTitle: UIAlertAction] = [:]
override func makeAlertAction(title: ActionTitle, handler: UIAlertActionHandler?) -> UIAlertAction {
handlers[title] = handler
let action = super.makeAlertAction(title: title, handler: handler)
actions[title] = action
return action
}
}
Extension on UIAlertAction to enable typed action title lookup in tests:
extension UIAlertAction {
var typedTitle: UIAlertActionProvider.ActionTitle? {
guard let title = title else { return nil }
return UIAlertActionProvider.ActionTitle(rawValue: title)
}
}
Sample test demonstrating usage:
func testDeleteHandlerActionSideEffectTakesPlace() throws {
let alertActionProvider = MockUIAlertActionProvider()
let sut = MyViewController(alertActionProvider: alertActionProvider)
// Do whatever you need to do to get alert presented, then retrieve action and handler
let action = try XCTUnwrap(alertActionProvider.actions[.proceed])
let handler = try XCTUnwrap(alertActionProvider.handlers[.proceed])
handler(action)
// Assert whatever side effects are triggered in your code by triggering handler
}
I used Luke's guidance above to create a subclass of UIAlertAction that saves its completion block so it can be called during tests:
class BSAlertAction: UIAlertAction {
var completionHandler: ((UIAlertAction) -> Swift.Void)?
class func handlerSavingAlertAction(title: String?,
style: UIAlertActionStyle,
completionHandler: #escaping ((UIAlertAction) -> Swift.Void)) -> BSAlertAction {
let alertAction = self.init(title: title, style: style, handler: completionHandler)
alertAction.completionHandler = completionHandler
return alertAction
}
}
You could customize this to save more information (like the title and the style) if you like. Here's an example of an XCTest that then uses this implementation:
func testThatMyMethodGetsCalled() {
if let alert = self.viewController?.presentedViewController as? UIAlertController,
let action = alert.actions[0] as? BSAlertAction,
let handler = action.completionHandler {
handler(action)
let calledMyMethod = self.presenter?.callTrace.contains(.myMethod) ?? false
XCTAssertTrue(calledMyMethod)
} else {
XCTFail("Got wrong kind of alert when verifying that my method got called“)
}
}