How to fixed vertical positioning glitch in Mapbox callout - swift

So I have a custom AnnotationView class (what the pin itself looks like) and a custom CalloutView class (what the bubble that appears above it looks like) For some reason, I get this odd behavior where if I press the annotation, it selects and appears properly (as seen in the first picture), but then if I deselect the annotation and reselect it (without moving the map at all), the callout reappears in the wrong positioning. The problem appears to be the rect.origin.y value, but I'm not sure--I tried hardcoding it but the problem still persists. In the video, I click my mouse down and it causes the annotation to move up. Any help much appreciated!
class AnnotationView: MGLAnnotationView {
weak var delegate: MapAnnotationDelegate?
var pointAnnotation: MGLPointAnnotation?
required override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
if reuseIdentifier == "default" {
super.centerOffset = CGVector(dx: 0, dy: -17.5)
}
let rect = CGRect(x: 0, y: 0, width: 35, height: 35)
let button = UIButton(frame: rect)
let image = UIImage(named: reuseIdentifier ?? "default")?.resize(targetSize: CGSize(width: 35, height: 35))
button.setImage(image, for: .normal)
button.setImage(image, for: .selected)
button.setImage(image, for: .highlighted)
button.imageView?.contentMode = .scaleAspectFit
button.addTarget(self, action: #selector(annotationAction), for: .touchUpInside)
frame = rect
addSubview(button)
isEnabled = false
}
required init?(coder: NSCoder) {
return nil
}
#objc private func annotationAction() {
delegate?.tappedAnnotation(annotation: pointAnnotation!)
}
}
class CustomCalloutView: UIView, MGLCalloutView {
var representedObject: MGLAnnotation
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
required init(representedObject: MGLAnnotation) {
self.representedObject = representedObject
self.mainBody = UIButton(type: .system)
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
delegate?.calloutViewWillAppear?(self)
view.addSubview(self)
// Prepare title label.
mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.sizeToFit()
mainBody.backgroundColor = .red
if isCalloutTappable() {
// Handle taps and eventually try to send them to the delegate (usually the map view).
mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
} else {
// Disable tapping and highlighting.
mainBody.isUserInteractionEnabled = false
}
// Prepare our frame, adding extra space at the bottom for the tip.
let frameWidth = mainBody.bounds.size.width
let frameHeight = mainBody.bounds.size.height + tipHeight
let frameOriginX = rect.origin.x + (rect.size.width/2.0) - (frameWidth/2.0)
let frameOriginY = rect.origin.y - frameHeight
frame = CGRect(x: frameOriginX, y: frameOriginY, width: frameWidth, height: frameHeight)
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { \[weak self\] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { \[weak self\] in
self?.alpha = 0
}, completion: { \[weak self\] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
func isCalloutTappable() -> Bool {
if let delegate = delegate {
if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
return delegate.calloutViewShouldHighlight!(self)
}
}
return false
}
#objc func calloutTapped() {
if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
delegate!.calloutViewTapped!(self)
}
}
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .white
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
For context, the relevant Mapbox map delegate functions are:
func tappedAnnotation(annotation: MGLPointAnnotation) {
let selectedAnnotation = control.mapView.selectedAnnotations.first
guard control.mapView.annotations != nil, selectedAnnotation !== annotation else { return }
//control is the actual map being passed into this coordinator function
for annotation in control.mapView.annotations! {
if annotation.subtitle == "PrimaryAnno" {
control.mapView.removeAnnotation(annotation)
}
}
control.mapView.selectAnnotation(annotation, animated: false, completionHandler: nil)
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if annotation is MGLPointAnnotation {
var reuseid = ""
//I use the subtitles to indicate which image I use for my annotations
if annotation.subtitle! == "primaryAnno" {
reuseid = "default"
} else {
reuseid = annotation.subtitle!
}
if let reusable = mapView.dequeueReusableAnnotationView(withIdentifier: reuseid) as? AnnotationView {
reusable.pointAnnotation = annotation as? MGLPointAnnotation
return reusable
} else {
let new = AnnotationView(reuseIdentifier: reuseid)
new.delegate = self
new.pointAnnotation = annotation as? MGLPointAnnotation
return new
}
} else {
return nil
}
}
And this is a protocol I use to define the tapping on an annotation:
protocol MapAnnotationDelegate: AnyObject {
func tappedAnnotation(annotation: MGLPointAnnotation)
}
UPDATE: I figured out that the 'correction' happens when I click on the map or change the region, so the issue is with the initial tap on annotation. I've tried setting a breakpoint in the tappedAnnotation function and it seems like once that function ends (it subsequently redirects to the annotationAction() function), the callout just renders in the wrong position.
https://youtu.be/4x5o9T6p8aw

Related

Sizing UIButton depending on length of titleLabel

So I have a UIButton and I'm setting the title in it to a string that is dynamic in length. I want the width of the titleLabel to be half of the screen width. I've tried using .sizeToFit() but this causes the button to use the CGSize before the constraint was applied to the titleLabel. I tried using .sizeThatFits(button.titleLabel?.intrinsicContentSize) but this also didn't work. I think the important functions below are the init() & presentCallout(), but I'm showing the entire class just for a more complete understanding. The class I'm playing with looks like:
class CustomCalloutView: UIView, MGLCalloutView {
var representedObject: MGLAnnotation
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
required init(representedObject: MGLAnnotation) {
self.representedObject = representedObject
self.mainBody = UIButton(type: .system)
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
// I thought this would work, but it doesn't.
// mainBody.translatesAutoresizingMaskIntoConstraints = false
// mainBody.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
// mainBody.leftAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
delegate?.calloutViewWillAppear?(self)
view.addSubview(self)
// Prepare title label.
mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.titleLabel?.lineBreakMode = .byWordWrapping
mainBody.titleLabel?.numberOfLines = 0
mainBody.sizeToFit()
if isCalloutTappable() {
// Handle taps and eventually try to send them to the delegate (usually the map view).
mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
} else {
// Disable tapping and highlighting.
mainBody.isUserInteractionEnabled = false
}
// Prepare our frame, adding extra space at the bottom for the tip.
let frameWidth = mainBody.bounds.size.width
let frameHeight = mainBody.bounds.size.height + tipHeight
let frameOriginX = rect.origin.x + (rect.size.width/2.0) - (frameWidth/2.0)
let frameOriginY = rect.origin.y - frameHeight
frame = CGRect(x: frameOriginX, y: frameOriginY, width: frameWidth, height: frameHeight)
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.alpha = 0
}, completion: { [weak self] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
func isCalloutTappable() -> Bool {
if let delegate = delegate {
if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
return delegate.calloutViewShouldHighlight!(self)
}
}
return false
}
#objc func calloutTapped() {
if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
delegate!.calloutViewTapped!(self)
}
}
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .white
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
This is what it looks like for a short title and a long title. When the title gets too long, I want the text to wrap and the bubble to get a taller height. As you can see in the image set below, the first 'Short Name' works fine as a map annotation bubble. When the name gets super long though, it just widens the bubble to the point it goes off the screen.
https://imgur.com/a/I5z0zUd
Any help on how to fix is much appreciated. Thanks!
To enable word-wrapping to multiple lines in a UIButton, you need to create your own button subclass.
For example:
class MultilineTitleButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() -> Void {
self.titleLabel?.numberOfLines = 0
self.titleLabel?.textAlignment = .center
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .vertical)
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .horizontal)
}
override var intrinsicContentSize: CGSize {
let size = self.titleLabel!.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
That button will wrap the title onto multiple lines, cooperating with auto-layout / constraints.
I don't have any projects with MapBox, but here is an example using a modified version of your CustomCalloutView. I commented out any MapBox specific code. You may be able to un-comment those lines and use this as-is:
class CustomCalloutView: UIView { //}, MGLCalloutView {
//var representedObject: MGLAnnotation
var repTitle: String = ""
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
// NOTE: this causes a vertical shift when NOT using MapBox
// override var center: CGPoint {
// set {
// var newCenter = newValue
// newCenter.y -= bounds.midY
// super.center = newCenter
// }
// get {
// return super.center
// }
// }
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
//weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
var anchorView: UIView!
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
anchorView.removeFromSuperview()
}
}
//required init(representedObject: MGLAnnotation) {
required init(title: String) {
self.repTitle = title
self.mainBody = MultilineTitleButton()
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.setTitleColor(.black, for: [])
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
mainBody.translatesAutoresizingMaskIntoConstraints = false
let padding: CGFloat = 8.0
NSLayoutConstraint.activate([
mainBody.topAnchor.constraint(equalTo: self.topAnchor, constant: padding),
mainBody.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding),
mainBody.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding),
mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding),
])
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
//delegate?.calloutViewWillAppear?(self)
// since we'll be using auto-layout for the mutli-line button
// we'll add an "anchor view" to the superview
// it will be removed when self is removed
anchorView = UIView(frame: rect)
anchorView.isUserInteractionEnabled = false
anchorView.backgroundColor = .clear
view.addSubview(anchorView)
view.addSubview(self)
// Prepare title label.
//mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.setTitle(self.repTitle, for: .normal)
// if isCalloutTappable() {
// // Handle taps and eventually try to send them to the delegate (usually the map view).
// mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
// } else {
// // Disable tapping and highlighting.
// mainBody.isUserInteractionEnabled = false
// }
self.translatesAutoresizingMaskIntoConstraints = false
anchorView.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin]
NSLayoutConstraint.activate([
self.centerXAnchor.constraint(equalTo: anchorView.centerXAnchor),
self.bottomAnchor.constraint(equalTo: anchorView.topAnchor),
self.widthAnchor.constraint(lessThanOrEqualToConstant: constrainedRect.width),
])
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
//strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
//delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.alpha = 0
}, completion: { [weak self] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
// func isCalloutTappable() -> Bool {
// if let delegate = delegate {
// if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
// return delegate.calloutViewShouldHighlight!(self)
// }
// }
// return false
// }
//
// #objc func calloutTapped() {
// if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
// delegate!.calloutViewTapped!(self)
// }
// }
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
print(#function)
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .red
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
Here is a sample view controller showing that "Callout View" with various length titles, restricted to 70% of the width of the view:
class CalloutTestVC: UIViewController {
let sampleTitles: [String] = [
"Short Title",
"Slightly Longer Title",
"A ridiculously long title that will need to wrap!",
]
var idx: Int = -1
let tapView = UIView()
var ccv: CustomCalloutView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.8939146399, green: 0.8417750597, blue: 0.7458069921, alpha: 1)
tapView.backgroundColor = .systemBlue
tapView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tapView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tapView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
tapView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
tapView.widthAnchor.constraint(equalToConstant: 60),
tapView.heightAnchor.constraint(equalTo: tapView.widthAnchor),
])
// tap the Blue View to cycle through Sample Titles for the Callout View
// using the Blue view as the "anchor rect"
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap))
tapView.addGestureRecognizer(t)
}
#objc func gotTap() -> Void {
if ccv != nil {
ccv.removeFromSuperview()
}
// increment sampleTitles array index
// to cycle through the strings
idx += 1
let validIdx = idx % sampleTitles.count
let str = sampleTitles[validIdx]
// create a new Callout view
ccv = CustomCalloutView(title: str)
// to restrict the "callout view" width to less-than 1/2 the screen width
// use view.width * 0.5 for the constrainedTo width
// may look better restricting it to 70%
ccv.presentCallout(from: tapView.frame, in: self.view, constrainedTo: CGRect(x: 0, y: 0, width: view.frame.size.width * 0.7, height: 100), animated: false)
}
}
It looks like this:
The UIButton class owns the titleLabel and is going to position and set the constraints on that label itself. More likely than not you are going to have to create a subclass of UIButton and override its "updateConstraints" method to position the titleLabel where you want it to go.
Your code should probably not be basing the size of the button off the size of the screen. It might set the size of off some other view in your hierarchy that happens to be the size of the screen but grabbing the screen bounds in the middle of setting a view's size is unusual.

How to colour radio buttons?

You can see that Apple has coloured the radio buttons. I would like to do the same. I can't seems to find the option to change the colour in Interface Builder on storyboard.
As for doing it, programmatically, I tried enabling layer .wantsLayer = true and then tried to set the colour by .layer?.borderColour = NSColor.systemBlue.cgColor and tried .layer?.backgroundColor = NSColor.systemRed.cgColor and other similar properties but no avail.
Likewise, how do you add the colour rectangles on NSMenuItem on NSPopUpButton?
The following code demonstrates a group of custom radio buttons for MacOS made by subclassing NSButton. It may be run in an Xcode swift project by copy/pasting into a newly added file called ‘main.swift’ and deleting the original AppDelegate.
import Cocoa
class CustomButton: NSButton {
var circleColor: NSColor!
override func draw(_ rect: NSRect) {
let circle = NSBezierPath(ovalIn: bounds)
switch(self.tag) {
case 0:
circleColor = NSColor.red
case 1:
circleColor = NSColor.green
case 2:
circleColor = NSColor.yellow
case 3:
circleColor = NSColor.orange
default:
break
}
circleColor.set()
circle.fill()
if(self.state) == .on {
let dotRect = NSInsetRect(bounds, 18.0, 18.0);
let dot = NSBezierPath (ovalIn:dotRect)
let dotColor = NSColor.black
dotColor.set()
dot.fill()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
#objc func radioGrpAction(_ sender:NSButton) {
print("You selected: id = \(sender.tag)")
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
let _wndW : CGFloat = 300
let _wndH : CGFloat = 200
window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable], backing:.buffered, defer:false)
window.center()
window.title = "Radio Button Group"
window.makeKeyAndOrderFront(window)
// === Radio Grp Box === //
let grpBox = NSBox(frame: NSMakeRect( 50,_wndH - 100, 150, 60))
grpBox.title = "Radio Group"
window.contentView!.addSubview (grpBox)
// === Radio Horizontal Grid === //
let _btnW : CGFloat = 24
let _btnH : CGFloat = 24
let _left : CGFloat = 10 // left margin first button
let _YOffset : CGFloat = 5 // 0,0 at left, bottom of group box
let _spacing : CGFloat = 5 // spacing between buttons
for x in stride(from:0, through:3, by:1) {
let _XOffset = _left + CGFloat(x)*(_btnW + _spacing)
let btn = CustomButton(frame:NSMakeRect(_XOffset, _YOffset, _btnW, _btnH))
btn.setButtonType(.radio)
btn.tag = x
if(x == 0){btn.state = .on}
btn.action = #selector(self.radioGrpAction(_:))
grpBox.contentView!.addSubview(btn)
}
// === Quit btn === //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = AppDelegate()
// ***** main.swift ***** //
let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.delegate = appDelegate
app.activate(ignoringOtherApps:true)
app.run()
Ok the same but for AppKit:
import Cocoa
import AppKit
extension NSView {
func centerX(inView view: NSView, constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: constant).isActive = true
}
func centerY(inView view: NSView, constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: constant).isActive = true
}
func setDimensions(height: CGFloat, width: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: height).isActive = true
widthAnchor.constraint(equalToConstant: width).isActive = true
}
}
class CustomRadioButton: NSView {
private let containerSize: CGFloat = 60.0
private let selectorSize: CGFloat = 20.0
var containerColor: NSColor = .blue
var selectorColor: NSColor = .red
var selected: Bool = true {
didSet {
selectorView.isHidden = !selected
}
}
private lazy var containerView: NSView = {
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = containerColor.cgColor
return view
}()
private lazy var selectorView: NSView = {
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = selectorColor.cgColor
return view
}()
override init(frame: CGRect) {
super.init(frame: CGRect(x: 0, y: 0, width: containerSize, height: containerSize))
configureUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configureUI() {
addSubview(containerView)
containerView.setDimensions(height: containerSize, width: containerSize)
containerView.layer?.cornerRadius = containerSize / 2
containerView.centerX(inView: self)
containerView.centerY(inView: self)
addSubview(selectorView)
selectorView.setDimensions(height: selectorSize, width: selectorSize)
selectorView.layer?.cornerRadius = selectorSize / 2
selectorView.centerY(inView: containerView)
selectorView.centerX(inView: containerView)
}
}
let selector = CustomRadioButton()
selector.selected = false

UIButton System style selected state, keep image and background

When using the System style of UIButton (nope, i don't want to use the Custom style, as the system provides animations, etc...)
The selected state of system button adds a background and removes the image
This is the Default state
And i want to achieve a selected style like this, where the look when selected is the same as the custom button
ok, finally manged that one out, the key was not to allow the switch to selected state
class ControlButton: UIButton {
var sImage: UIImage?
var dImage: UIImage?
override func awakeFromNib() {
super.awakeFromNib()
sImage = image(for: .selected)
dImage = image(for: .normal)
}
override open var isSelected: Bool {
set {
if newValue {
setImage(sImage, for: .normal)
} else {
setImage(dImage, for: .normal)
}
}
get {
return false
}
}
}
Add this class and set it to your button class
class KButton: UIButton {
var view: UIButton!
#IBInspectable public var textPadding: CGFloat = 5.0 {
didSet {
layoutSubviews()
}
}
#IBInspectable public var circleRadius: CGFloat = 10 {
didSet {
layoutSubviews()
}
}
#IBInspectable public var circleWidth: CGFloat = 2.0 {
didSet {
layoutSubviews()
}
}
#IBInspectable public var currentState: Bool = false {
didSet {
layoutSubviews()
}
}
override func layoutSubviews() {
super.layoutSubviews()
if view != nil {
view?.removeFromSuperview()
}
view = UIButton(frame: CGRect(x: -(self.frame.height) - textPadding, y: 0, width: self.frame.height, height: self.frame.height))
view.backgroundColor = UIColor.clear
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.height/2, y: view.frame.height/2), radius: circleRadius, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = self.tintColor.cgColor
shapeLayer.lineWidth = circleWidth
view.layer.addSublayer(shapeLayer)
if currentState {
let circlePath1 = UIBezierPath(arcCenter: CGPoint(x: view.frame.height/2, y: view.frame.height/2), radius: (circleRadius - (circleWidth * 2)), startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer1 = CAShapeLayer()
shapeLayer1.path = circlePath1.cgPath
shapeLayer1.fillColor = self.tintColor.cgColor
shapeLayer1.strokeColor = self.tintColor.cgColor
shapeLayer1.lineWidth = circleWidth
view.layer.addSublayer(shapeLayer1)
}
self.addSubview(view)
}
}
Then in you click action
#IBAction func buttonClicked(_ sender: KButton) {
sender.currentState = !sender.currentState
}
Make sure to choose the type as KButton

Swift - detect touched coordinate of UIView

I am trying to make a custom jump bar which can be attached to UITableView.
What I want to achieve now is if user touches A and slides through Z, I want print out A, B, C, D..., Z. Currently, it only prints A, A, A...A. Is there anyway I can achieve it?
Each letter is UIButton subview of UIView.
class TableViewJumpBar{
let tableView: UITableView
let view: UIView
let jumpBar: UIView!
private var jumpIndexes: [Character]
init(tableView: UITableView, view: UIView){
self.view = view
self.tableView = tableView
jumpBar = UIView(frame: .zero)
jumpBar.backgroundColor = UIColor.gray
let aScalars = "A".unicodeScalars
let aCode = aScalars[aScalars.startIndex].value
jumpIndexes = (0..<26).map {
i in Character(UnicodeScalar(aCode + i)!)
}
}
func setFrame(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat){
guard jumpIndexes.count > 0 else{
print("Jump indexes cannot be empty")
return
}
//Remove jumpbar and remove all subviews
jumpBar.removeFromSuperview()
for subView in jumpBar.subviews{
subView.removeFromSuperview()
}
jumpBar.frame = CGRect(x: x, y: y, width: width, height: height)
let height = height/CGFloat(jumpIndexes.count)
for i in 0..<jumpIndexes.count{
let indexButton = UIButton(frame: CGRect(x:CGFloat(0.0), y: CGFloat(i)*height, width: width, height: height))
indexButton.setTitle(String(jumpIndexes[i]), for: .normal)
indexButton.addTarget(self, action: #selector(jumpIndexButtonTouched(_:)), for: .allEvents)
jumpBar.addSubview(indexButton)
}
self.view.addSubview(jumpBar)
}
///Touch has been begun
#objc private func jumpIndexButtonTouched(_ sender: UIButton!){
print(sender.titleLabel?.text)
}
this code works and print start and finish characters.
class ViewController: UIViewController {
#IBOutlet weak var myTableView: UITableView!
#IBOutlet weak var myView: UIView!
var startChar = ""
var finishChar = ""
override func viewDidLoad() {
super.viewDidLoad()
let table = TableViewJumpBar(tableView: myTableView, view: myView)
table.setFrame(x: 0, y: 0, width: 100, height: self.view.frame.size.height)
print(myView.subviews[0].subviews)
setPanGesture()
}
func setPanGesture() {
let pan = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
self.myView.addGestureRecognizer(pan)
}
#objc func panRecognized(sender: UIPanGestureRecognizer) {
let location = sender.location(in: myView)
if sender.state == .began {
for button in myView.subviews[0].subviews {
if let button = button as? UIButton, button.frame.contains(location), let startCharacter = button.titleLabel?.text {
self.startChar = startCharacter
}
}
} else if sender.state == .ended {
for button in myView.subviews[0].subviews {
if let button = button as? UIButton, button.frame.contains(location), let finishCharacter = button.titleLabel?.text {
self.finishChar = finishCharacter
}
}
print("start with \(startChar), finish with \(finishChar)")
}
}
}
In your code I delete Button action. you can use labels instead Buttons.
class TableViewJumpBar {
let tableView: UITableView
let view: UIView
let jumpBar: UIView!
private var jumpIndexes: [Character]
init(tableView: UITableView, view: UIView){
self.view = view
self.tableView = tableView
jumpBar = UIView(frame: .zero)
jumpBar.backgroundColor = UIColor.gray
let aScalars = "A".unicodeScalars
let aCode = aScalars[aScalars.startIndex].value
jumpIndexes = (0..<26).map {
i in Character(UnicodeScalar(aCode + i)!)
}
}
func setFrame(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat){
guard jumpIndexes.count > 0 else{
print("Jump indexes cannot be empty")
return
}
jumpBar.removeFromSuperview()
for subView in jumpBar.subviews{
subView.removeFromSuperview()
}
jumpBar.frame = CGRect(x: x, y: y, width: width, height: height)
let height = height/CGFloat(jumpIndexes.count)
for i in 0..<jumpIndexes.count{
let indexButton = UIButton(frame: CGRect(x:CGFloat(0.0), y: CGFloat(i)*height, width: width, height: height))
indexButton.setTitle(String(jumpIndexes[i]), for: .normal)
jumpBar.addSubview(indexButton)
}
self.view.addSubview(jumpBar)
}
}
Some output results:

Adding missing triangle(tooltip) to callout custom MKAnnotationView subclass

I Create a custom MKAnnotationView, that works fine but it pops as a rectangle, without the triangle (the same one you have in comics when someone is talking), that comes out of the pin.
Is there a way that the triangle will be added automatically?
here is my MKAnnotationView subclass:
class ShikmimAnnotationView: MKAnnotationView{
let selectedLabel:UILabel = UILabel.init(frame:CGRect(x: 0, y: 0, width: 140, height: 40))
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(false, animated: animated)
if(selected)
{
// Do customization, for example:
//let defaults = UserDefaults.standard
selectedLabel.font = UIFont(name: "Heebo-Regular.ttf", size: 18)
selectedLabel.text = "(גדליהו אלון 13, ירושלים\n (3 דק' הגעה"
selectedLabel.numberOfLines = 3
selectedLabel.sizeToFit()
selectedLabel.textColor = UIColor.white
selectedLabel.textAlignment = .center
selectedLabel.backgroundColor = UIColor.darkGray
selectedLabel.layer.borderColor = UIColor.white.cgColor
selectedLabel.layer.borderWidth = 2
selectedLabel.layer.cornerRadius = 5
selectedLabel.layer.masksToBounds = true
selectedLabel.center.x = 0.5 * self.frame.size.width;
selectedLabel.center.y = -0.5 * selectedLabel.frame.height;
self.addSubview(selectedLabel)
self.image = UIImage(named: "map_pin_next_stop-2.png")
}
else
{
selectedLabel.removeFromSuperview()
}
}
}
Here is how I initialize it in my VC:
internal func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? MyLocation {
let identifier = "reuse"
var view: ShikmimAnnotationView
if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier){
dequeuedView.annotation = annotation
view = dequeuedView as! ShikmimAnnotationView
} else {
view = ShikmimAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view.image = UIImage(named: "map_pin_next_stop-2.png")
view.canShowCallout = false
//view.calloutOffset = CGPoint(x: -5, y: 5)
//view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure) as UIView
}
return view
}
return nil
}
You can draw this triangle with BezierPath. Here's class for this view. You can setup size of view in storyboard or make it programmatically in your ShikmimAnnotationView class.
class TriangleView: UIView {
let shapeLayer = CAShapeLayer()
override func drawRect(rect: CGRect) {
// Get Height and Width
let layerHeight = self.layer.frame.height
let layerWidth = self.layer.frame.width
// Create Path
let bezierPath = UIBezierPath()
// Draw Points
bezierPath.moveToPoint(CGPoint(x: 0, y: 0))
bezierPath.addLineToPoint(CGPoint(x: layerWidth, y: 0))
bezierPath.addLineToPoint(CGPoint(x: layerWidth / 2, y: layerHeight))
bezierPath.addLineToPoint(CGPoint(x: 0, y: 0))
bezierPath.closePath()
// Apply Color
UIColor(red: (2/255), green: (35/255), blue: (73/255), alpha: 1).setFill()
bezierPath.fill()
// Mask to Path
shapeLayer.path = bezierPath.CGPath
self.layer.mask = shapeLayer
}
}