Change Swipe Button Title After Completion - swift

I have the following code:
cell.rightButtons = [MGSwipeButton(title: "Save", backgroundColor:UIColor.redColor(),callback: {
(sender: MGSwipeTableCell!) -> Bool in
print("Saved")
return true
}))]
cell.rightSwipeSettings.transition = MGSwipeTransition.Rotate3D
return cell
This works fine but I want to change the title from "Save" to remove afterward. Tried it many different ways but doesn't seem to be working.

As explained in the official project: MGSwipeTableCell
you can obtain the button pressed by calling the delegate method:
-(BOOL) swipeTableCell:(MGSwipeTableCell*) cell tappedButtonAtIndex:(NSInteger) index direction:(MGSwipeDirection)direction fromExpansion:(BOOL) fromExpansion;
/**
* Delegate method to setup the swipe buttons and swipe/expansion settings
* Buttons can be any kind of UIView but it's recommended to use the convenience MGSwipeButton class
* Setting up buttons with this delegate instead of using cell properties improves memory usage because buttons are only created in demand
* #param swipeTableCell the UITableVieCel to configure. You can get the indexPath using [tableView indexPathForCell:cell]
* #param direction The swipe direction (left to right or right to left)
* #param swipeSettings instance to configure the swipe transition and setting (optional)
* #param expansionSettings instance to configure button expansions (optional)
* #return Buttons array
**/
In Swift you can write:
func swipeTableCell(cell: MGSwipeTableCell!, tappedButtonAtIndex index: Int, direction: MGSwipeDirection, fromExpansion: Bool) -> Bool {
if let button = cell.rightbuttons[index] {
print(button.titleLabel.text)
button.setTitle("Button Title", forState: UIControlState.Normal)
... (do whatever you want with your tapped button..)
}
}
To help you can launch the MGSwipeDemo project included in the bundle source, and add these lines in objective c like in this screenshots:
-(BOOL) swipeTableCell:(MGSwipeTableCell*) cell tappedButtonAtIndex:(NSInteger) index direction:(MGSwipeDirection)direction fromExpansion:(BOOL) fromExpansion
{
...
if (direction == MGSwipeDirectionRightToLeft && index == 1) {
NSLog(#"more pressed");
id button = cell.rightButtons[index];
UIButton *pressedBtn = button;
[pressedBtn setTitle:#"Less" forState:UIControlStateNormal];
NSLog(#"pressedBtn title: %#",pressedBtn.titleLabel.text);
}
...
}
P.S.: update: this part was added to help during comments:
About your personal project you must add to your datasource any boolean var, i make an example (in Swift code):
Class rowData{
var title: String! = String()
var subTitle:String! = String()
var button1Title : String! = String()
var button2Title : String! = String()
var isSaved : Bool! = false
}
var myDataSource : [rowData]! = [rowData]()
So, when you had populate your datasource you can use the delegate:
func swipeTableCell(cell: MGSwipeTableCell!, tappedButtonAtIndex index: Int, direction: MGSwipeDirection, fromExpansion: Bool) -> Bool {
...
if (direction == MGSwipeDirection.RightToLeft && index == 2){
if let _= cell?.rightButtons[index]{
let cellData:rowData = myDataSource[index]
if cellData.isSaved {
cellData.buttonTitle1 = "Just saved"
cellData.isSaved = true
} else {
cellData.buttonTitle1 = "Save"
cellData.isSaved = false
}
myDataSource[index] = cellData
// datasource is modified so launch reload data to refresh layout and call cellForRow..
// There are other softly methods than reloadData to update tableView layout but I use it just for example..
self.tableView.reloadData()
}
}
...
}

Related

Cocoa customise NSView's tooltips Swift

I am trying to create a tooltip with bold text. Some apple apps on macOS use this behaviour. How do I achieve this?
My code currently
btn.tooltip = "Open Options"
//tooltip doesn't accept attributed strings.
Here is an example (screenshot of Xcode using this behaviour) of what I'm trying to achieve.
It seems there is no built-in default behavior for tooltips with NSAttributedStrings. As a solution, one could implement a floating NSPanel.
As long as the mouse is within the button bounds for at least a certain period of time, you could show a popover with an NSAttributedString. You can use the mouseEntered and mouseExited events for this purpose. Unfortunately, this requires that you subclass the NSButton.
Complete, Self-contained Swift Program
From a ViewController we would most likely to call it like this:
import Cocoa
class ViewController: NSViewController {
private let button = ToolTipButton()
override func viewDidLoad() {
super.viewDidLoad()
button.title = "Hoover over me"
let headline = "isEnabled"
let body = "A Boolean value that determines whether the label draws its text in an enabled state."
button.setToolTip(headline: headline, body: body)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
The ToolTipButton class could look like this:
import Cocoa
class ToolTipButton: NSButton {
private var toolTipHandler: ToolTipHandler?
func setToolTip(headline: String, body: String) {
toolTipHandler = ToolTipHandler(headline: headline, body: body)
}
override func mouseEntered(with event: NSEvent) {
toolTipHandler?.mouseEntered(into: self)
}
override func mouseExited(with event: NSEvent) {
toolTipHandler?.mouseExited()
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
toolTipHandler?.updateTrackingAreas(for: self)
}
}
Finally the ToolTipHandler could look like this:
import Cocoa
final class ToolTipHandler {
private var headline: String
private var body: String
private var mouseStillInside = false
private var panel: NSPanel?
init(headline: String, body: String) {
self.headline = headline
self.body = body
}
func setToolTip(headline: String, body: String) {
self.headline = headline
self.body = body
}
func mouseEntered(into view: NSView) {
mouseStillInside = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.showToolTipIfMouseStillInside(for: view)
}
}
func mouseExited() {
mouseStillInside = false
panel?.close()
panel = nil
}
func updateTrackingAreas(for view: NSView) {
for trackingArea in view.trackingAreas {
view.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways]
let trackingArea = NSTrackingArea(rect: view.bounds, options: options, owner: view, userInfo: nil)
view.addTrackingArea(trackingArea)
}
private func showToolTipIfMouseStillInside(for view: NSView) {
guard mouseStillInside && panel == nil else { return }
panel = Self.showToolTip(sender: view, headline: headline, body: body)
}
private static func showToolTip(sender: NSView, headline: String, body: String) -> NSPanel {
let panel = NSPanel()
panel.styleMask = [NSWindow.StyleMask.borderless]
panel.level = .floating
let attributedToolTip = Self.attributedToolTip(headline: headline, body: body)
panel.contentViewController = ToolTipViewController(attributedToolTip: attributedToolTip, width: 200.0)
let lowerLeftOfSender = sender.convert(NSPoint(x: sender.bounds.minX + 4.0, y: sender.bounds.maxY + 10.0), to: nil)
let newOrigin = sender.window?.convertToScreen(NSRect(origin: lowerLeftOfSender, size: .zero)).origin ?? .zero
panel.setFrameOrigin(newOrigin)
panel.orderFrontRegardless()
return panel
}
private static func attributedToolTip(headline: String, body: String) -> NSAttributedString {
let headlineAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: NSColor.controlTextColor,
.font: NSFont.boldSystemFont(ofSize: 11)
]
let bodyAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: NSColor.controlTextColor,
.font: NSFont.systemFont(ofSize: 11)
]
let tooltip = NSMutableAttributedString(string: headline, attributes: headlineAttributes)
tooltip.append(NSAttributedString(string: "\n" + body , attributes: bodyAttributes))
return tooltip
}
}
Finally the ToolTipViewController:
import Cocoa
final class ToolTipViewController: NSViewController {
private let attributedToolTip: NSAttributedString
private let width: CGFloat
init(attributedToolTip: NSAttributedString, width: CGFloat) {
self.attributedToolTip = attributedToolTip
self.width = width
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
let label = NSTextField()
label.isEditable = false
label.isBezeled = false
label.attributedStringValue = attributedToolTip
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 1.0),
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 1.0),
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -1.0),
label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -1.0),
label.widthAnchor.constraint(equalToConstant: width)
])
}
}
Depending on the actual requirements, adjustments are probably necessary. But it should at least be a starting point.
Demo
The source code and full-length version of this answer are at this GitHub repo.
Separately from that repo I also extracted the code into a Swift Package, so I could use it in other projects. The dependency to add to your project is "https://github.com/chipjarred/CustomToolTip.git". Use "from" version 1.0.0 or branch "main".
What follows is the version trimmed down to a length SO would let me post.
Stephan's answer prompted me to do my own implementation of tool tips. My solution produces tool tips that look like the standard tool tips, except you can put any view you like inside them, so not just styled text, but images... you could even use a WebKit view, if you wanted to.
Obviously it doesn't make sense to put some kinds of views in it. Anything that only makes sense with user interaction would be meaningless since the tool tip would disappear as soon as they move the mouse cursor to interact with it... though that would be good April Fools joke.
Before I get to my solution, I want to mention that there is another way to make Stephan's solution a little easier to use, which is to use the "decorator" pattern by subclassing NSView to wrap another view. Your wrapper is the part that hooks into to the tool tips, and handles the tracking areas. Just make sure you forward those calls to the wrapped view too, in case it also has tracking areas (perhaps it changes the cursor or something, like NSTextView does.) Using a decorator means you don't subclass every view... just put the view you want to add a tool tip inside of a ToolTippableView or whatever you decide to call it. I don't think you'll need to override all NSView methods as long as you wrap the view by adding it to your subviews. The view heirarchy and responder chain should take care of dispatching the events and messages you're not interested in to the subview. You should only need to forward the ones you handle for the tool tips (mouseEntered, mouseExited, etc...)
My solution
However, I went to an evil extreme... and spent way more time on it than I probably should have, but it seemed like something I might want to use at some point. I swizzled ("monkey patched") NSView methods to handle custom tool tips, which combined with an extension on NSView means I don't have subclass anything to add custom tool tips, I can just write:
myView.customToolTip = myCustomToolTipContent
where myCustomToolTipContent is whatever NSView I want to display in the tool tip.
The Tool Tip itself
The main thing is the tool tip itself. It's just a window. It sizes itself to whatever content you put in it, so make sure you've set your tip content's view frame to the size you want before setting customToolTip. Here's the tool tip window code:
// -------------------------------------
/**
Window for displaying custom tool tips.
*/
class CustomToolTipWindow: NSWindow
{
// -------------------------------------
static func makeAndShow(
toolTipView: NSView,
for owner: NSView) -> CustomToolTipWindow
{
let window = CustomToolTipWindow(toolTipView: toolTipView, for: owner)
window.orderFront(self)
return window
}
// -------------------------------------
init(toolTipView: NSView, for toolTipOwner: NSView)
{
super.init(
contentRect: toolTipView.bounds,
styleMask: [.borderless],
backing: .buffered,
defer: false
)
self.backgroundColor = NSColor.windowBackgroundColor
let border = BorderedView.init(frame: toolTipView.frame)
border.addSubview(toolTipView)
contentView = border
contentView?.isHidden = false
reposition(relativeTo: toolTipOwner)
}
// -------------------------------------
deinit { orderOut(nil) }
// -------------------------------------
/**
Place the tool tip window's frame in a sensible place relative to the
tool tip's owner view on the screen.
If the current layout direction is left-to-right, the preferred location is
below and shifted to the right relative to the owner. If the layout
direction is right-to-left, the preferred location is below and shift to
the left relative to the owner.
The preferred location is overridden when any part of the tool tip would be
drawn off of the screen. For conflicts with horizontal edges, it is moved
to be some "safety" distance within the screen bounds. For conflicts with
the bottom edge, the tool tip is positioned above the owning view.
Non-flipped coordinates (y = 0 at bottom) are assumed.
*/
func reposition(relativeTo toolTipOwner: NSView)
{
guard let ownerRect =
toolTipOwner.window?.convertToScreen(toolTipOwner.frame),
let screenRect = toolTipOwner.window?.screen?.visibleFrame
else { return }
let hPadding: CGFloat = ownerRect.width / 2
let hSafetyPadding: CGFloat = 20
let vPadding: CGFloat = 0
var newRect = frame
newRect.origin = ownerRect.origin
// Position tool tip window slightly below the onwer on the screen
newRect.origin.y -= newRect.height + vPadding
if NSApp.userInterfaceLayoutDirection == .leftToRight
{
/*
Position the tool tip window to the right relative to the owner on
the screen.
*/
newRect.origin.x += hPadding
// Make sure we're not drawing off the right edge
newRect.origin.x = min(
newRect.origin.x,
screenRect.maxX - newRect.width - hSafetyPadding
)
}
else
{
/*
Position the tool tip window to the left relative to the owner on
the screen.
*/
newRect.origin.x -= hPadding
// Make sure we're not drawing off the left edge
newRect.origin.x =
max(newRect.origin.x, screenRect.minX + hSafetyPadding)
}
/*
Make sure we're not drawing off the bottom edge of the visible area.
Non-flipped coordinates (y = 0 at bottom) are assumed.
If we are, move the tool tip above the onwer.
*/
if newRect.minY < screenRect.minY {
newRect.origin.y = ownerRect.maxY + vPadding
}
self.setFrameOrigin(newRect.origin)
}
// -------------------------------------
/// Provides thin border around the tool tip.
private class BorderedView: NSView
{
override func draw(_ dirtyRect: NSRect)
{
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current?.cgContext else {
return
}
context.setStrokeColor(NSColor.black.cgColor)
context.stroke(self.frame, width: 2)
}
}
}
The tool tip window is the easy part. This implementation positions the window relative to its owner (the view to which the tool tip is attached) while also avoiding drawing offscreen. I don't handle the pathalogical case where the tool tip is so large that it can't fit onto screen without obscuring the thing it's a tool tip for. Nor do I handle the case where the thing you're attaching the tool tip to is so large that even though the tool tip itself is a reasonable size, it can't go outside of the area occupied by the view to which it's attached. That case shouldn't be too hard to handle. I just didn't do it. I do handle responding to the currently set layout direction.
If you want to incorporate it into another solution, the code to show the tool tip is
let toolTipWindow = CustomToolTipWindow.makeAndShow(toolTipView: toolTipView, for: ownerView)
where toolTipView is the view to be displayed in the tool tip. ownerView is the view to which you're attaching the tool tip. You'll need to store toolTipWindow somewhere, for example in Stephan's ToolTipHandler.
To hide the tool tip:
toolTipWindow.orderOut(self)
or just set the last reference you keep to it to nil.
I think that gives you everything you need to incorporate it into another solution if you like.
Tool Tip handling code
As a small convenience, I use this extension on NSTrackingArea
// -------------------------------------
/*
Convenice extension for updating a tracking area's `rect` property.
*/
fileprivate extension NSTrackingArea
{
func updateRect(with newRect: NSRect) -> NSTrackingArea
{
return NSTrackingArea(
rect: newRect,
options: options,
owner: owner,
userInfo: nil
)
}
}
Since I'm swizzling NSVew (actually its subclasses as you add tool tips), I don't have a ToolTipHandler-like object. I just put it all in an extension on NSView and use global storage. To do that I have a ToolTipControl struct and a ToolTipControls wrapper around an array of them:
// -------------------------------------
/**
Data structure to hold information used for holding the tool tip and for
controlling when to show or hide it.
*/
fileprivate struct ToolTipControl
{
/**
`Date` when mouse was last moved within the tracking area. Should be
`nil` when the mouse is not in the tracking area.
*/
var mouseEntered: Date?
/// View to which the custom tool tip is attached
weak var onwerView: NSView?
/// The content view of the tool tip
var toolTipView: NSView?
/// `true` when the tool tip is currently displayed. `false` otherwise.
var isVisible: Bool = false
/**
The tool tip's window. Should be `nil` when the tool tip is not being
shown.
*/
var toolTipWindow: NSWindow? = nil
init(
mouseEntered: Date? = nil,
hostView: NSView,
toolTipView: NSView? = nil)
{
self.mouseEntered = mouseEntered
self.onwerView = hostView
self.toolTipView = toolTipView
}
}
// -------------------------------------
/**
Data structure for holding `ToolTipControl` instances. Since we only need
one collection of them for the application, all its methods and properties
are `static`.
*/
fileprivate struct ToolTipControls
{
private static var controlsLock = os_unfair_lock()
private static var controls: [ToolTipControl] = []
// -------------------------------------
static func getControl(for hostView: NSView) -> ToolTipControl? {
withLock { return controls.first { $0.onwerView === hostView } }
}
// -------------------------------------
static func setControl(for hostView: NSView, to control: ToolTipControl)
{
withLock
{
if let i = index(for: hostView) { controls[i] = control }
else { controls.append(control) }
}
}
// -------------------------------------
static func removeControl(for hostView: NSView)
{
withLock
{
controls.removeAll {
$0.onwerView == nil || $0.onwerView === hostView
}
}
}
// -------------------------------------
private static func index(for hostView: NSView) -> Int? {
controls.firstIndex { $0.onwerView == hostView }
}
// -------------------------------------
private static func withLock<R>(_ block: () -> R) -> R
{
os_unfair_lock_lock(&controlsLock)
defer { os_unfair_lock_unlock(&controlsLock) }
return block()
}
// -------------------------------------
private init() { } // prevent instances
}
These are fileprivate in the same file as my extension on NSView. I also have to have a way to differentiate between my tracking areas and others the view might have. They have a userInfo dictionary that I use for that. I don't need to store different individualized information in each one, so I just make a global one I reuse.
fileprivate let bundleID = Bundle.main.bundleIdentifier ?? "com.CustomToolTips"
fileprivate let toolTipKeyTag = bundleID + "CustomToolTips"
fileprivate let customToolTipTag = [toolTipKeyTag: true]
And I need a dispatch queue:
fileprivate let dispatchQueue = DispatchQueue(
label: toolTipKeyTag,
qos: .background
)
NSView extension
My NSView extension has a lot in it, the vast majority of which is private, including swizzled methods, so I'll break it into pieces
In order to be able to attach a custom tool tip as easily as you do for a standard tool tip, I provide a computed property. In addition to actually setting the tool tip view, it also checks to see if Self (that is the particular subclass of NSView) has already been swizzled, and does that if it hasn't been, and it's adds the mouse tracking area.
// -------------------------------------
/**
Adds a custom tool tip to the receiver. If set to `nil`, the custom tool
tip is removed.
This view's `frame.size` will determine the size of the tool tip window
*/
public var customToolTip: NSView?
{
get { toolTipControl?.toolTipView }
set
{
Self.initializeCustomToolTips()
if let newValue = newValue
{
addCustomToolTipTrackingArea()
var current = toolTipControl ?? ToolTipControl(hostView: self)
current.toolTipView = newValue
toolTipControl = current
}
else { toolTipControl = nil }
}
}
// -------------------------------------
/**
Adds a tracking area encompassing the receiver's bounds that will be used
for tracking the mouse for determining when to show the tool tip. If a
tacking area already exists for the receiver, it is removed before the
new tracking area is set. This method should only be called when a new
tool tip is attached to the receiver.
*/
private func addCustomToolTipTrackingArea()
{
if let ta = trackingAreaForCustomToolTip {
removeTrackingArea(ta)
}
addTrackingArea(
NSTrackingArea(
rect: self.bounds,
options:
[.activeInActiveApp, .mouseMoved, .mouseEnteredAndExited],
owner: self,
userInfo: customToolTipTag
)
)
}
// -------------------------------------
/**
Returns the custom tool tip tracking area for the receiver.
*/
private var trackingAreaForCustomToolTip: NSTrackingArea?
{
trackingAreas.first {
$0.owner === self && $0.userInfo?[toolTipKeyTag] != nil
}
}
trackingAreaForCustomToolTip is where I use the global tag to sort my tracking area from any others that the view might have.
Of course, I also have to implement updateTrackingAreas and this where we start to see some of evidence of swizzling.
// -------------------------------------
/**
Updates the custom tooltip tracking aread when `updateTrackingAreas` is
called.
*/
#objc private func updateTrackingAreas_CustomToolTip()
{
if let ta = trackingAreaForCustomToolTip
{
removeTrackingArea(ta)
addTrackingArea(ta.updateRect(with: self.bounds))
}
else { addCustomToolTipTrackingArea() }
callReplacedMethod(for: #selector(self.updateTrackingAreas))
}
The method isn't called updateTrackingAreas because I'm not overriding it in the usual sense. I actually replace the implementation of the current class's updateTrackingAreas with the implementation of my updateTrackingAreas_CustomToolTip, saving off the original implementation so I can forward to it. callReplacedMethod where I do that forwarding. If you look into swizzling, you find lots of examples where people call what looks like an infinite recursion, but isn't because they exchange method implementations. That works most of the time, but it can subtly mess up the underlying Objective-C messaging because the selector used to call the old method is no longer the original selector. The way I've done it preserves the selector, which makes it less fragile when something depends on the actual selector remaining the same. There's more on swizzling in the full answer on GitHub I linked to above. For now, think of callReplacedMethod as similar to calling super if I were doing this by subclassing.
Then there's scheduling to show the tool tip. I do this kind of similarly to Stephan, but I wanted the behavior that the tool tip isn't shown until the mouse stops moving for a certain delay (1 second is what I currently use).
As I'm writing this, I just noticed that I do deviate from the standard behavior once the tool tip is displayed. The standard behavior is that once the tool tip is shown it continues to show the tool tip even if the mouse is moved as long as it remains in the tracking area. So once shown, the standard behavior doesn't hide the tool tip until the mouse leaves the tracking area. I hide it as soon as you move the mouse. Doing it the standard way is actually simpler, but the way I do it would allow for the tool tip to be shown over large views (for example a NSTextView for a large document) where it has to actually in the same area of the screen that it's owner occupies. I don't currently position the tool tip that way, but if I were to, you'd want any mouse movement to hide the tool tip, otherwise the tool tip would obscure part of what you need to interact with.
Anyway, here's what that scheduling code looks like
// -------------------------------------
/**
Controls how many seconds the mouse must be motionless within the tracking
area in order to show the tool tip.
*/
private var customToolTipDelay: TimeInterval { 1 /* seconds */ }
// -------------------------------------
/**
Schedules to potentially show the tool tip after `delay` seconds.
The tool tip is not *necessarily* shown as a result of calling this method,
but rather this method begins a sequence of chained asynchronous calls that
determine whether or not to display the tool tip based on whether the tool
tip is already visible, and how long it's been since the mouse was moved
withn the tracking area.
- Parameters:
- delay: Number of seconds to wait until determining whether or not to
display the tool tip
- mouseEntered: Set to `true` when calling from `mouseEntered`,
otherwise set to `false`
*/
private func scheduleShowToolTip(delay: TimeInterval, mouseEntered: Bool)
{
guard var control = toolTipControl else { return }
if mouseEntered
{
control.mouseEntered = Date()
toolTipControl = control
}
let asyncDelay: DispatchTimeInterval = .milliseconds(Int(delay * 1000))
dispatchQueue.asyncAfter(deadline: .now() + asyncDelay) {
[weak self] in self?.scheduledShowToolTip()
}
}
// -------------------------------------
/**
Display the tool tip now, *if* the mouse is in the tracking area and has
not moved for at least `customToolTipDelay` seconds. Otherwise, schedule
to check again after a short delay.
*/
private func scheduledShowToolTip()
{
let repeatDelay: TimeInterval = 0.1
/*
control.mouseEntered is set to nil when exiting the tracking area,
so this guard terminates the async chain
*/
guard let control = self.toolTipControl,
let mouseEntered = control.mouseEntered
else { return }
if control.isVisible {
scheduleShowToolTip(delay: repeatDelay, mouseEntered: false)
}
else if Date().timeIntervalSince(mouseEntered) >= customToolTipDelay
{
DispatchQueue.main.async
{ [weak self] in
if let self = self
{
self.showToolTip()
self.scheduleShowToolTip(
delay: repeatDelay,
mouseEntered: false
)
}
}
}
else { scheduleShowToolTip(delay: repeatDelay, mouseEntered: false) }
}
Earlier I gave the code for how to show and hide the tool tip window. Here are the functions where that code lives with its interaction with toolTipControl to control the corresponding loop.
// -------------------------------------
/**
Displays the tool tip now.
*/
private func showToolTip()
{
guard var control = toolTipControl else { return }
defer
{
control.mouseEntered = Date.distantPast
toolTipControl = control
}
guard let toolTipView = control.toolTipView else
{
control.isVisible = false
return
}
if !control.isVisible
{
control.isVisible = true
control.toolTipWindow = CustomToolTipWindow.makeAndShow(
toolTipView: toolTipView,
for: self
)
}
}
// -------------------------------------
/**
Hides the tool tip now.
*/
private func hideToolTip(exitTracking: Bool)
{
guard var control = toolTipControl else { return }
control.mouseEntered = exitTracking ? nil : Date()
control.isVisible = false
let window = control.toolTipWindow
control.toolTipWindow = nil
window?.orderOut(self)
control.toolTipWindow = nil
toolTipControl = control
print("Hiding tool tip")
}
The only thing that's left before getting to the actual swizzling is handling the mouse movements. I do this with mouseEntered, mouseExited and mouseMoved, or rather, their swizzled implementations:
// -------------------------------------
/**
Schedules potentially showing the tool tip when the `mouseEntered` is
called.
*/
#objc private func mouseEntered_CustomToolTip(with event: NSEvent)
{
scheduleShowToolTip(delay: customToolTipDelay, mouseEntered: true)
callReplacedEventMethod(
for: #selector(self.mouseEntered(with:)),
with: event
)
}
// -------------------------------------
/**
Hides the tool tip if it's visible when `mouseExited` is called, cancelling
further `async` chaining that checks to show it.
*/
#objc private func mouseExited_CustomToolTip(with event: NSEvent)
{
hideToolTip(exitTracking: true)
callReplacedEventMethod(
for: #selector(self.mouseExited(with:)),
with: event
)
}
// -------------------------------------
/**
Hides the tool tip if it's visible when `mousedMoved` is called, and
resets the time for it to be displayed again.
*/
#objc private func mouseMoved_CustomToolTip(with event: NSEvent)
{
hideToolTip(exitTracking: false)
callReplacedEventMethod(
for: #selector(self.mouseMoved(with:)),
with: event
)
}
Sadly my original version of this post was too long, so I had to cut out the swizzling details, however, I put the whole thing on GitHub, with the complete source code, so you can look at it more in depth. I've never reached the length limit before.
So skipping to the end...
That puts everything in place (or would do if I could have posted the whole thing here), so now you just have to use it.
I was just using Xcode's default Cocoa App template to implement, so it uses a Storyboard (which normally I prefer not to). I just added an ordinary NSButton in the Storyboard. That means I don't start with a reference to it anywhere in the source code, so in ViewController, for the sake of building an example I just do a quick recursive search through the view hierarchy looking for an NSButton.
func findPushButton(in view: NSView) -> NSButton?
{
if let button = view as? NSButton { return button }
for subview in view.subviews
{
if let button = findPushButton(in: subview) {
return button
}
}
return nil
}
And I need to make a tool tip view. I wanted to demonstrate using more than just text, so I hacked this together
func makeCustomToolTip() -> NSView
{
let titleText = "Custom Tool Tip"
let bodyText = "\n\tThis demonstrates that its possible,\n\tand if I can do it, so you can you"
let titleFont = NSFont.systemFont(ofSize: 14, weight: .bold)
let title = NSAttributedString(
string: titleText,
attributes: [.font: titleFont]
)
let bodyFont = NSFont.systemFont(ofSize: 10)
let body = NSAttributedString(
string: bodyText,
attributes: [.font: bodyFont]
)
let attrStr = NSMutableAttributedString(attributedString: title)
attrStr.append(body)
let label = NSTextField(labelWithAttributedString: attrStr)
let imageView = NSImageView(frame: CGRect(origin: .zero, size: CGSize(width: label.frame.height, height: label.frame.height)))
imageView.image = #imageLiteral(resourceName: "Swift_logo")
let toolTipView = NSView(
frame: CGRect(
origin: .zero,
size: CGSize(
width: imageView.frame.width + label.frame.width + 15,
height: imageView.frame.height + 10
)
)
)
imageView.frame.origin.x += 5
imageView.frame.origin.y += 5
toolTipView.addSubview(imageView)
label.frame.origin.x += imageView.frame.maxX + 5
label.frame.origin.y += 5
toolTipView.addSubview(label)
return toolTipView
}
And then in viewDidLoad()
override func viewDidLoad()
{
super.viewDidLoad()
findPushButton(in: view)?.customToolTip = makeCustomToolTip()
}

Touch Gesture Recognizer to clear screen of random function

I am building a collage in Xcode with a set background image and a random int function to display an image.
I tried using a tapGestureRecognizer to 1) display the random image and 2) clear the screen after using self.clearScreen. The images are displaying but they do not disappear when the next image is displayed.
import UIKit
import C4
class WorkSpace: CanvasController {
override func setup() {
var myRandomNumber: Int?
//Background Image
let background = Image("background")
background?.constrainsProportions = true
background?.width = self.canvas.width
background?.height = self.canvas.height
self.canvas.add(background)
canvas.addTapGestureRecognizer { (locations, center, state) in
self.clearScreen
myRandomNumber = random(below: 4)
}
}
if myRandomNumber == 1 {
let tree = Image("Tree")
tree?.constrainsProportions = true
tree?.width = self.canvas.width
tree?.origin = Point(0.0, self.canvas.height/2.0)
self.canvas.add(tree)
print("number1")
}
if myRandomNumber == 2 {
let boy = Image("boy")
boy?.constrainsProportions = true
boy?.width = self.canvas.width
boy?.origin = Point(0.0, self.canvas.height/2.0)
self.canvas.add(boy)
}
if myRandomNumber == 3 {
let woman = Image("woman")
woman?.constrainsProportions = true
woman?.width = self.canvas.width
woman?.height = self.canvas.height
self.canvas.add(woman)
}
}
}
I would like the screen to clear the pictures and replace with another.
func clearScreen() {
for view in self.view.subView {
view.removeFromSuperView()
}
}
After looking at the C4 pod docs:
Check the view hierarchy after adding to canvas
/// Adds a view to the end of the receiver’s list of subviews.
/// When working with C4, use this method to add views because it handles the addition of both UIView and View.
public func add<T>(_ subview: T?) {...}
And then try removing subviews with their supplied methods:
/// Unlinks the view from the receiver and its window, and removes it from the responder chain.
/// Calling this method removes any constraints that refer to the view you are removing, or that refer to any view in the
/// subtree of the view you are removing.
/// When working with C4, use this method to add views because it handles the removal of both UIView and View.
/// ````
/// let v = View(frame: Rect(0,0,100,100))
/// let subv = View(frame: Rect(25,25,50,50))
/// v.add(subv)
/// v.remove(subv)
/// ````
/// - parameter subview: The view to be removed.
public func remove<T>(_ subview: T?) {
if let v = subview as? UIView {
v.removeFromSuperview()
} else if let v = subview as? View {
v.view.removeFromSuperview()
} else {
fatalError("Can't remove subview of class `\(type(of: subview))`")
}
}

No-show menuController and can't figure out how to make the View return True to calling .becomeFirstResponder

I am having a no-show menuController and I have checked all of the suggestions in previous questions. It turns out the imageView I have implemented a UILongPressGestureRecognizer on, to show the menu, is returning False on calling .becomeFirstResponder just before setting up the menu controller.
I am coding in swift 4 and can't figure out how to make the imageView return True to calling .becomeFirstResponder. Help!
/*********************************************************/
override func viewDidLoad() {
super.viewDidLoad()
// long tap to show menu that enables deletion of the image.
imageView_1.isUserInteractionEnabled = true
let longPressRecogniser = UILongPressGestureRecognizer(target: self, action: #selector(longPressOnImage(_:)))
//longPressRecogniser.numberOfTapsRequired = 1
//longPressRecogniser.numberOfTouchesRequired = 1
longPressRecogniser.minimumPressDuration = 0.5
imageView_1.addGestureRecognizer(longPressRecogniser)
imageView_1.image = placeHolderImage_1
imageView_2.image = placeHolderImage_2
}
/*********************************************************/
#IBAction func longPressOnImage(_ gestureRecognizer: UILongPressGestureRecognizer) {
print(#function)
if gestureRecognizer.state == .began {
//print("gestureRecognizer.state == .began")
self.tappedView = gestureRecognizer.view!
if tappedView.canResignFirstResponder {
print("can resign first responder")
}
if tappedView.becomeFirstResponder() {
print("returned TRUE to becomeFirstResponder")
} else {
print("returned FALSE to becomeFirstResponder")
}
// Configure the shared menu controller
let menuController = UIMenuController.shared
// Configure the menu item to display
// Create a "delete" menu item
let deleteImage = UIMenuItem(title: "Delete", action: #selector(deleteImage_1))
menuController.menuItems = [deleteImage]
// Set the location of the menu in the view.
let location = gestureRecognizer.location(in: tappedView)
print("location = ", location)
let menuLocation = CGRect(x: location.x, y: location.y, width: 2, height: 2)
menuController.setTargetRect(menuLocation, in: tappedView)
//update the menu settings to force it to display my custom items
menuController.update()
// Show the menu.
menuController.setMenuVisible(true, animated: true)
print("menu should be visible now")
}
}
/*********************************************************/
#objc func deleteImage_1() {
print(#function)
}
My caveman debugging print statements output:
longPressOnImage
can resign first responder
returned FALSE to becomeFirstResponder
location = (207.0, 82.0)
menu should be visible now
Create a custom imageView class and override "canBecomeFirstResponder" property like this:
class ResponsiveImage : UIImageView{
override var canBecomeFirstResponder: Bool{
return true
}
}
Use this ResponsiveImage type and your code will work :)
Thank you to adri. Your answer is the solution to my problem.
I had read in other posts to similar questions about overriding var canBecomeFirstResponder but either overlooked it or it wasn't made explicit that a custom UIImageView class needs to be created.
Just to make it clear to newbies like me, the class of the imageView in storyBoard and its #IBOutlet in its viewController must typed as ResponsiveImage. If only one of these is changed a type casting error is reported.
Many thanks for ending my hours of frustration! :-)

Programmatically Adding/Removing Images to Subview in Swift

Using code found in another post on here, I was able to programmatically draw and erase a subview, including location, width, and height, as shown in func addLoadButton and func removeSubview below. I have also figured out how to programmatically draw a picture on the main View Controller, as shown in func trailerLoadImage below. However, after many hours and attempts, I have tried to programmatically add and remove images into that subview without success.
My end goal is to be able to press three different trailer load type buttons to insert three different images (button 1 loads image 1, button 2 loads image 2, etc.) in a subview located in a specific location on the screen, and to be able to remove the images one at a time (may not be in order put on screen) by tapping on the images with a finger. The subview can be permanent or can be created and removed programmatically (as used below).
What code would I use to insert an image or multiple different images into a subview that has already been created, to remove the image(s) in the reverse order added, and to clear all images out of the subview? If this can’t be done, an acceptable alternative would be the ability to remove the image from the main VC by either tapping on it or pressing a button to clear all added images.
//Class declaration
class ViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate {
//Boolean to include load type one in calculations
var trailerLoad : Bool = false
var trailerLoadDistanceFromFront = 20
//Boolean to include load type two in calculations
var trailerLoadTwo : Bool = false
var trailerLoadTwoDistanceFromFront = 80
//Boolean to include load type three in calculations
var trailerLoadThree : Bool = false
var trailerLoadThreeDistanceFromFront = 120
var trailerLoadWidth : Int = 0
var trailerLoadX : Int = 0
//Boolean true only when subView on trailer is active
var subViewActive : Bool = false
override func viewDidLoad() {
super.viewDidLoad()
//Picker view data sources and delegates included in here and work fine
}
//Adds subview for loads
#IBAction func addLoadButton(_ sender: Any) {
let trailerLoadView: UIView = UIView(frame: CGRect(x: 252, y: 233, width: 378, height: 100))
trailerLoadView.backgroundColor = .blue
trailerLoadView.alpha = 0.5
trailerLoadView.tag = 100
trailerLoadView.isUserInteractionEnabled = true
self.view.addSubview(trailerLoadView)
subViewActive = true
}
//If subViewActive is true, calls alert to get distance load type one is from front, moves on to insert and position image, changes trailerLoad bool to true
#IBAction func trailerLoadOneButton(_ sender: Any) {
//If subViewActive is true:
//Calls alert to get distance load type one is from front, puts in var trailerLoadDistanceFromFront
//Calls trailerLoadImage() to insert and position load type one image
//Changes bool trailerLoad to true
//If subViewActive is false:
//Calls alert to tell user that they need to click Add Load button (create subview) before adding load types one, two, or three
}
//Add trailer load type one image, scales and positions it relatively accurately in view.
//To be duplicated and modified for load types two and three in the future, with different images (trailerLoadTypeTwoPic and trailerLoadTypeThreePic)
func trailerLoadImage() {
trailerLoadWidth = 378 * 60 / trailerTotalLength
trailerLoadX = 378 * trailerLoadDistanceFromFront / trailerTotalLength
let imageView = UIImageView(frame: CGRect(x: (252 + trailerLoadX), y: (333 - trailerLoadWidth), width: trailerLoadWidth, height: trailerLoadWidth));
let image = UIImage(named: “trailerLoadTypeOnePic”);
imageView.image = image;
self.view.addSubview(imageView)
}
//Calls func removeSubview to remove subview
#IBAction func resetButton(_ sender: Any) {
removeSubview()
}
//Removes subview for loads
#objc func removeSubview(){
subViewActive = false
if let viewWithTag = self.view.viewWithTag(100) {
viewWithTag.removeFromSuperview()
}else{
print("No!")
}
}
}
Thank you very much to anybody that offers assistance or advice.
Don't use tags! Just create variables in global scope for your views
var imageViews = [UIImageView]()
then when you need to add them first append them to your array and then add them to view
imageViews.append(imageView)
view.addSubview(imageView)
Then when you need to remove your all views from their superview, use method removeFromSuperview() for each view in array
imageViews.forEach { $0.removeFromSuperview() }
imageViews.removeAll()
or if you need to remove just one view at specific index
imageViews[index].removeFromSuperview()
imageViews.remove(at: index)

Getting notified when an NSView comes to the foreground

I have a NSTableView inside the second tab of a NSTabView. The table uses some custom event code that takes care of buttons in the table being clicked are recognised. The problem is that even if I have the first or third tab of the tab view open (i.e. table view is not visible), these events are being intercepted (so if I have buttons on 1./3. tab they don't work).
To solve this I added a flag to my custom table view class to only execute the custom event code when the flag is true and if not just return the original event.
Now I'm wondering how do I tell if the table view is in the front (i.e. tab no. 2 is open)? Is there any elegant way to check for this or do I need to get this from the tab bar directly?
In case it matters, here's my custom table code...
class ButtonTableView : NSTableView
{
var isAtForeground:Bool = false;
override init(frame frameRect:NSRect) {
super.init(frame: frameRect);
}
required init?(coder:NSCoder) {
super.init(coder: coder);
addEventInterception();
}
/// Get the row number (if any) that coincides with a specific point - where the point is in window coordinates.
func rowContainingWindowPoint(windowPoint:CGPoint) -> Int? {
var rowNum:Int?;
var tableRectInWindowCoords = convertRect(bounds, toView: nil);
if (CGRectContainsPoint(tableRectInWindowCoords, windowPoint))
{
let tabPt = convertPoint(windowPoint, fromView: nil);
let indexOfClickedRow = rowAtPoint(tabPt);
if (indexOfClickedRow > -1 && indexOfClickedRow < numberOfRows)
{
rowNum = indexOfClickedRow;
}
}
return rowNum;
}
func addEventInterception() {
NSEvent.addLocalMonitorForEventsMatchingMask(.LeftMouseDownMask, handler:
{
(theEvent) -> NSEvent! in
/* Don't bother if the table view is not in the foreground! */
if (!self.isAtForeground) { return theEvent; }
var e:NSEvent? = theEvent;
let p:NSPoint = theEvent.locationInWindow;
/* Did the mouse-down event take place on a row in the table? */
if let row = self.rowContainingWindowPoint(p)
{
let localPoint = self.convertPoint(p, fromView: nil);
/* Get the column index that was clicked in. */
let col = self.columnAtPoint(localPoint);
/* Get the table cell view on which the mouse down event took place. */
let tcv = self.viewAtColumn(col, row: row, makeIfNecessary: false) as! NSTableCellView;
/* Did the click occur on the table view? */
let tableBoundsInWindowCoords:NSRect = self.convertRect(self.bounds, toView: nil);
if (CGRectContainsPoint(tableBoundsInWindowCoords, p))
{
/* If clicked anywhere in the table cell view, only allow button areas to pass. */
/* subView is the NSTableCellView's last subview. */
var subView = tcv.subviews.last! as! NSView;
let subViewBoundsInWindowCoords:NSRect = subView.convertRect(subView.bounds, toView: nil);
/* Did the click occur on the subview part of the view? */
if (CGRectContainsPoint(subViewBoundsInWindowCoords, p))
{
/* Only allow button subviews to be clicked. */
if let button = subView as? NSButton
{
// Create a modified event, where the <location> property has been
// altered so that it looks like the click took place in the
// NSTableCellView itself.
let newLocation = tcv.convertPoint(tcv.bounds.origin, toView: nil);
e = theEvent.cloneEventButUseAdjustedWindowLocation(newLocation);
return e;
}
}
/* If we've reched here, the user clicked anywhere on the table but not one of the buttons. */
return nil;
}
}
return e;
});
}
}