SwiftUI spring timing doesn't appear to match UIKit spring timing - swift

With identical parameters, the SwiftUI spring timing function doesn't appear to match the UIKit spring timing function (specifically, UIViewPropertyAnimator).
In the video below, we have two labels, one in SwiftUI and one in UIKit, with positions being driven by spring functions with identical parameters, but the computed positions don't line up throughout the animation. What am I missing here?
Video
Code
import SwiftUI
import UIKit
class SwiftUIViewModel: ObservableObject {
#Published var position: CGPoint = .zero
}
struct SwiftUIView: View {
#ObservedObject var model: SwiftUIViewModel
var body: some View {
GeometryReader { geo in
Text("Hello")
.font(.system(size: 100))
.foregroundColor(.blue)
.position(model.position)
.animation(.interpolatingSpring(mass: 1, stiffness: 39.47841760435743, damping: 12.566370614359172, initialVelocity: .zero))
}
}
}
class UIKitView: UIView {
let referenceViewModel = SwiftUIViewModel()
let referenceView: UIHostingController<SwiftUIView>
let label = UILabel()
var animator: UIViewPropertyAnimator?
let tapGesture = UITapGestureRecognizer()
var previousBounds: CGRect = .zero
override func layoutSubviews() {
guard bounds != previousBounds else {
return
}
previousBounds = bounds
label.sizeToFit()
label.center = .init(x: bounds.width / 2, y: bounds.height / 2)
referenceView.view.frame = bounds
}
init() {
referenceView = UIHostingController(rootView: SwiftUIView(model: referenceViewModel))
super.init(frame: .zero)
tapGesture.addTarget(self, action: #selector(tapped))
addGestureRecognizer(tapGesture)
addSubview(referenceView.view)
label.text = "Hello"
label.textColor = .black
label.adjustsFontSizeToFitWidth = true
label.font = .systemFont(ofSize: 100.0)
label.layer.opacity = 1.0
addSubview(label)
}
#objc func tapped(_ event: UIEvent) {
let location = tapGesture.location(in: self)
// Forward to reference view.
referenceViewModel.position = location
let timing = UISpringTimingParameters(mass: 1, stiffness: 39.47841760435743, damping: 12.566370614359172, initialVelocity: .zero)
animator = UIViewPropertyAnimator(duration: 0.0, timingParameters: timing)
animator.addAnimations { [weak self] in
self?.label.center = location
}
animator.startAnimation()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

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 fixed vertical positioning glitch in Mapbox callout

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

After a sheet is presented in SwiftUI, the text color of interactive UI elements changes to systemBlue?

I have a sheet that is presented when a button is pressed:
struct Button: View {
#State isPresented = false
var body: some View {
Button(action: { self.isPresented.toggle() },
label: { Text("Text Label") }
).sheet(isPresented: $isPresented, content: {
//sheet contents in here
})
}
}
The class seen below is essentially the view in UIKit (I removed some of the code from the functions because it doesn't really have anything to do with the problem, but I kept the function names in there with descriptions so you can interpret what it's doing)
class CustomCalloutView: UIView, MGLCalloutView {
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()
weak var delegate: MGLCalloutViewDelegate?
//MARK: Subviews -
let personImg: UIImageView = {
let img = UIImage(systemName: "person.fill")
var imgView = UIImageView(image: img)
//imgView.tintColor = UIColor(ciColor: .black)
imgView.translatesAutoresizingMaskIntoConstraints = false
return imgView
}()
let clockImg: UIImageView = {
let img = UIImage(systemName: "clock")
var imgView = UIImageView(image: img)
//imgView.tintColor = UIColor(ciColor: .black)
imgView.translatesAutoresizingMaskIntoConstraints = false
return imgView
}()
//Initialization of the view
required init() {
super.init(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: UIScreen.main.bounds.width * 0.75, height: 130)))
setup()
}
//other initializer
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//essentially just positioning the view
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
//setting it up
func setup() {
// setup this view's properties
self.backgroundColor = UIColor.clear
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(CustomCalloutView.calloutTapped)))
// And the subviews
self.addSubview(personImg)
self.addSubview(clockImg)
// Add Constraints to subviews
//Positioning the clock image
clockImg.topAnchor.constraint(equalTo: self.separatorLine.bottomAnchor, constant: spacing).isActive = true
clockImg.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
clockImg.rightAnchor.constraint(equalTo: self.timeLabel.leftAnchor, constant: -spacing / 2).isActive = true
clockImg.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
//Positioning the person image
personImg.topAnchor.constraint(equalTo: self.timeLabel.bottomAnchor, constant: spacing / 2).isActive = true
personImg.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
personImg.rightAnchor.constraint(equalTo: self.peopleLabel.leftAnchor, constant: -spacing / 2).isActive = true
personImg.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
}
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool)
//presenting the view
}
func dismissCallout(animated: Bool) {
//dismissing the view
}
#objc func calloutTapped() {
//respond to the view being tapped
}
}
When the sheet is presented and then dismissed, it causes other UI elements that are coded in UIKit (like text buttons, for example) to have their text turn color to the systemBlue UIColor...any idea how to fix this?
There is not enough code provided to test, but the reason might be in
struct Button: View { // << this custom view named as standard Button !!!
#State isPresented = false
it is named the same as standard SwiftUI component Button so this can confuse rendering engine.
Try to rename this (and others if you practice this) to something unique to your app, like MyButton or CustomButton, SheetButton, etc.

How can I position these UIView elements from the right using CGRect to position

I have a UIView sub class that allows me to create a group of 'tags' for the footer of some content. At the moment however they are position aligned to the left edge, I would like them to be positioned from the right.
I have included a playground below that should run the screen shot you can see.
The position is set within the layoutSubviews method of CloudTagView.
I tried to play around with their position but have not been able to start them from the right however.
import UIKit
import PlaygroundSupport
// CLOUD VIEW WRAPPER - THIS IS THE CONTAINER FOR THE TAGS AND SETS UP THEIR FRAME
class CloudTagView: UIView {
weak var delegate: TagViewDelegate?
override var intrinsicContentSize: CGSize {
return frame.size
}
var removeOnDismiss = true
var resizeToFit = true
var tags = [TagView]() {
didSet {
layoutSubviews()
}
}
var padding = 5 {
didSet {
layoutSubviews()
}
}
var maxLengthPerTag = 0 {
didSet {
layoutSubviews()
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
clipsToBounds = true
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isUserInteractionEnabled = true
clipsToBounds = true
}
override func layoutSubviews() {
for tag in subviews {
tag.removeFromSuperview()
}
var xAxis = padding
var yAxis = padding
var maxHeight = 0
for (index, tag) in tags.enumerated() {
setMaxLengthIfNeededIn(tag)
tag.delegate = self
if index == 0 {
maxHeight = Int(tag.frame.height)
}else{
let expectedWidth = xAxis + Int(tag.frame.width) + padding
if expectedWidth > Int(frame.width) {
yAxis += maxHeight + padding
xAxis = padding
maxHeight = Int(tag.frame.height)
}
if Int(tag.frame.height) > maxHeight {
maxHeight = Int(tag.frame.height)
}
}
tag.frame = CGRect(x: xAxis, y: yAxis, width: Int(tag.frame.size.width), height: Int(tag.frame.size.height))
addSubview(tag)
tag.layoutIfNeeded()
xAxis += Int(tag.frame.width) + padding
}
if resizeToFit {
frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.size.width, height: CGFloat(yAxis + maxHeight + padding))
}
}
// MARK: Methods
fileprivate func setMaxLengthIfNeededIn(_ tag: TagView) {
if maxLengthPerTag > 0 && tag.maxLength != maxLengthPerTag {
tag.maxLength = maxLengthPerTag
}
}
}
// EVERYTHING BELOW HERE IS JUST SETUP / REQUIRED TO RUN IN PLAYGROUND
class ViewController:UIViewController{
let cloudView: CloudTagView = {
let view = CloudTagView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
let tags = ["these", "are", "my", "tags"]
tags.forEach { tag in
let t = TagView(text: tag)
t.backgroundColor = .darkGray
t.tintColor = .white
cloudView.tags.append(t)
}
view.backgroundColor = .white
view.addSubview(cloudView)
NSLayoutConstraint.activate([
cloudView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
cloudView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
cloudView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
cloudView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])
}
}
// Tag View
class TagView: UIView {
weak var delegate: TagViewDelegate?
var text = "" {
didSet {
layoutSubviews()
}
}
var marginTop = 5 {
didSet {
layoutSubviews()
}
}
var marginLeft = 10 {
didSet {
layoutSubviews()
}
}
var iconImage = UIImage(named: "close_tag_2", in: Bundle(for: CloudTagView.self), compatibleWith: nil) {
didSet {
layoutSubviews()
}
}
var maxLength = 0 {
didSet {
layoutSubviews()
}
}
override var backgroundColor: UIColor? {
didSet {
layoutSubviews()
}
}
override var tintColor: UIColor? {
didSet {
layoutSubviews()
}
}
var font: UIFont = UIFont.systemFont(ofSize: 12) {
didSet {
layoutSubviews()
}
}
fileprivate let dismissView: UIView
fileprivate let icon: UIImageView
fileprivate let textLabel: UILabel
public override init(frame: CGRect) {
dismissView = UIView()
icon = UIImageView()
textLabel = UILabel()
super.init(frame: frame)
isUserInteractionEnabled = true
addSubview(textLabel)
addSubview(icon)
addSubview(dismissView)
dismissView.isUserInteractionEnabled = true
textLabel.isUserInteractionEnabled = true
dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.iconTapped)))
textLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.labelTapped)))
backgroundColor = UIColor(white: 0.0, alpha: 0.6)
tintColor = UIColor.white
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public init(text: String) {
dismissView = UIView()
icon = UIImageView()
textLabel = UILabel()
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
isUserInteractionEnabled = true
addSubview(textLabel)
addSubview(icon)
addSubview(dismissView)
dismissView.isUserInteractionEnabled = true
textLabel.isUserInteractionEnabled = true
dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.iconTapped)))
textLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.labelTapped)))
self.text = text
backgroundColor = UIColor(white: 0.0, alpha: 0.6)
tintColor = UIColor.white
}
override func layoutSubviews() {
icon.frame = CGRect(x: marginLeft, y: marginTop + 4, width: 8, height: 8)
icon.image = iconImage?.withRenderingMode(.alwaysTemplate)
icon.tintColor = tintColor
let textLeft: Int
if icon.image != nil {
dismissView.isUserInteractionEnabled = true
textLeft = marginLeft + Int(icon.frame.width ) + marginLeft / 2
} else {
dismissView.isUserInteractionEnabled = false
textLeft = marginLeft
}
textLabel.frame = CGRect(x: textLeft, y: marginTop, width: 100, height: 20)
textLabel.backgroundColor = UIColor(white: 0, alpha: 0.0)
if maxLength > 0 && text.count > maxLength {
textLabel.text = text.prefix(maxLength)+"..."
}else{
textLabel.text = text
}
textLabel.textAlignment = .center
textLabel.font = font
textLabel.textColor = tintColor
textLabel.sizeToFit()
let tagHeight = Int(max(textLabel.frame.height,14)) + marginTop * 2
let tagWidth = textLeft + Int(max(textLabel.frame.width,14)) + marginLeft
let dismissLeft = Int(icon.frame.origin.x) + Int(icon.frame.width) + marginLeft / 2
dismissView.frame = CGRect(x: 0, y: 0, width: dismissLeft, height: tagHeight)
frame = CGRect(x: Int(frame.origin.x), y: Int(frame.origin.y), width: tagWidth, height: tagHeight)
layer.cornerRadius = bounds.height / 2
}
// MARK: Actions
#objc func iconTapped(){
delegate?.tagDismissed?(self)
}
#objc func labelTapped(){
delegate?.tagTouched?(self)
}
}
// MARK: TagViewDelegate
#objc protocol TagViewDelegate {
#objc optional func tagTouched(_ tag: TagView)
#objc optional func tagDismissed(_ tag: TagView)
}
extension CloudTagView: TagViewDelegate {
public func tagDismissed(_ tag: TagView) {
delegate?.tagDismissed?(tag)
if removeOnDismiss {
if let index = tags.firstIndex(of: tag) {
tags.remove(at: index)
}
}
}
public func tagTouched(_ tag: TagView) {
delegate?.tagTouched?(tag)
}
}
let viewController = ViewController()
PlaygroundPage.current.liveView = viewController
PlaygroundPage.current.needsIndefiniteExecution = true
UIStackView can line subviews up in a row for you, including with trailing alignment. Here is a playground example:
import SwiftUI
import PlaygroundSupport
class V: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let tags = ["test", "testing", "test more"].map { word -> UIView in
let label = UILabel()
label.text = word
label.translatesAutoresizingMaskIntoConstraints = false
let background = UIView()
background.backgroundColor = .cyan
background.layer.cornerRadius = 8
background.clipsToBounds = true
background.addSubview(label)
NSLayoutConstraint.activate([
background.centerXAnchor.constraint(equalTo: label.centerXAnchor),
background.centerYAnchor.constraint(equalTo: label.centerYAnchor),
background.widthAnchor.constraint(equalTo: label.widthAnchor, constant: 16),
background.heightAnchor.constraint(equalTo: label.heightAnchor, constant: 16),
])
return background
}
let stack = UIStackView.init(arrangedSubviews: [UIView()] + tags)
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .horizontal
stack.alignment = .trailing
stack.spacing = 12
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: view.topAnchor),
stack.widthAnchor.constraint(equalTo: view.widthAnchor),
])
view.backgroundColor = .white
}
}
PlaygroundPage.current.liveView = V()

how can I implement a singleton spinner (activityindicator)?

i want to implement a singleton spinner, because any app that has a api needs a spinner. well I can code a spinner custom, but my problem is that I should code the next line ever if I want to show it.
let rosetaGIF = UIImage(named: "wheel.png")
let ind = MyIndicator(frame: CGRect(x: 0, y: 0, width: (rosetaGIF?.size.width)!/2, height: (rosetaGIF?.size.height)!/2), image: rosetaGIF!)
view.addSubview(ind)
view.alpha = 0.5
ind.startAnimating()
that's not good, because I must put this lines every time that I want to show the spinner, well, my spinner class is the next. I'm using swift 4.2
import UIKit
class MyIndicator: UIActivityIndicatorView {
let loadingView = UIView(frame: (UIApplication.shared.delegate?.window??.bounds)!)
let imageView = UIImageView()
let sizeView = UIViewController()
init(frame: CGRect, image: UIImage) {
super.init(frame: frame)
imageView.frame = bounds
imageView.image = image
imageView.contentMode = .scaleAspectFit
imageView.center = CGPoint(x: sizeView.view.frame.width/2, y: sizeView.view.frame.height/2)
imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(imageView)
}
required init(coder: NSCoder) {
fatalError()
}
override func startAnimating()
{
isHidden = false
rotate()
}
override func stopAnimating()
{
isHidden = true
removeRotation()
}
private func rotate() {
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.toValue = NSNumber(value: Double.pi * 1)
rotation.duration = 1
rotation.isCumulative = true
rotation.repeatCount = Float.greatestFiniteMagnitude
self.imageView.layer.add(rotation, forKey: "rotationAnimation")
}
private func removeRotation() {
self.imageView.layer.removeAnimation(forKey: "rotationAnimation")
}
}
what should I do for that the spinner will be singleton?
thanks
I suggest you create a util class and make that class Singleton.
import UIKit
class Util {
static let shared = Util()
private init(){}
var loader: MyIndicator?
func showLoader(view: UIView){
hideLoader()
let rosetaGIF = UIImage(named: "wheel.png")
loader = MyIndicator(frame: CGRect(x: 0, y: 0, width: (rosetaGIF?.size.width)!/2, height: (rosetaGIF?.size.height)!/2), image: rosetaGIF!)
view.addSubview(loader!)
view.alpha = 0.5
loader?.startAnimating()
}
func hideLoader(){
loader?.stopAnimating()
loader?.removeFromSuperview()
}
}
How to use this class
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Util.shared.showLoader(view: view) // for showing the loader
Util.shared.hideLoader() // for hiding the loader
}
}
Create a Base Controller with your spinner, then inherit other Controllers from you Base Controller and show the spinner whenever you want.
class BaseViewController: UIViewController {
var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
activityIndicator = UIActivityIndicatorView(frame: CGRect(x: (UIScreen.main.bounds.width-40)/2, y: (UIScreen.main.bounds.height-40)/2, width: 40, height: 40))
activityIndicator.transform = CGAffineTransform(scaleX: 2, y: 2)
activityIndicator.color = .darkGray
view.addSubview(activityIndicator)
}
}
class ViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
activityIndicator.startAnimating() // you can show the spinner wherever you want.
}
}
hope this will help to you.