Setting tick marks for UISlider - iphone

Is there any method for setting tick marks for UISlider. NSSlider has something called
- (void)setNumberOfTickMarks:(NSInteger)numberOfTickMarks. But UISlider does not appear to have one.
What i want to do is get the slider values as 0,1,2,3,4 etc if say i set the min as 0 and max as 5. Now as such UISLider returns something like 0.0,0.1,0.2,0.3 etc 4.8,4.9,5.0 based on the example i gave above.
Any help??

UISlider doesn't have any tickmarks.
To get the behaviour you specify you'll have to round the number returned by the slider to the nearest integer.
So when you start sliding, and the slider reports a value of 0.1 or 0.2, round that to 0. Until it hits 0.5, when you round up to 1.
Then when the slider stops sliding, you can set it's value to the number you rounded to, which will make the slider "snap" to that position, kind like snapping to a tick mark.
If you want visual tickmarks, you'll have to implement that yourself.

Yeah I think that subclassing is the way to go. This is what I did:
TickSlider.h…
/**
* A custom slider that allows the user to specify the number of tick marks.
* It behaves just like the NSSlider with tick marks for a MacOS app.
**/
#interface TickSlider : UISlider
/**
* The number of tickmarks for the slider. No graphics will be draw in this
* sublcass but the value of the slider will be rounded to the nearest tick
* mark in the same way/behaviour as the NSSlider on a MacOS app.
* #discussion This value can be set as a UserDefinedAttribute in the xib file.
**/
#property (nonatomic) NSInteger numberOfTickMarks;
#end
TickSlider.m…
#import "TickSlider.h"
#implementation TickSlider
- (void)setValue:(float)value animated:(BOOL)animated {
float updatedValue = value;
if (self.numberOfTickMarks > 0) {
updatedValue = MAX(self.minimumValue, MIN(value, self.maximumValue));
if (self.numberOfTickMarks == 1) {
updatedValue = (self.minimumValue + self.maximumValue) * 0.5f;
}
else {
const int kNumberOfDivisions = (self.numberOfTickMarks - 1);
const float kSliderRange = ([self maximumValue] - [self minimumValue]);
const float kRangePerDivision = (kSliderRange / kNumberOfDivisions);
const float currentTickMark = roundf(value / kRangePerDivision);
updatedValue = (currentTickMark * kRangePerDivision);
}
}
[super setValue:updatedValue animated:animated];
}
#end

As above, casting to int makes the slider behave as if you where sliding next to the thumb. The above ticker code is interesting, allowing you to specify more "stop values" (ticks) as the maximum value setting.
If you need a simple "integer slider", rounding (value + 0.25) actually makes the slider behave quite nicely. This is the code I use:
#interface IntegerSlider : UISlider
#end
#implementation IntegerSlider
- (void) setValue: (float) value
animated: (BOOL) animated
{
value = roundf(value + 0.25);
[super setValue: value
animated: animated];
}
#end

I just cast [slider value] to int to achieve this.

For anyone needing the Swift 5 version, here it is:
class CAUISlider: UISlider {
// MARK: - Properties
public var numberOfTickMarks: Int = 0
// MARK: - Public
override func setValue(_ value: Float, animated: Bool) {
var updatedValue: Float = value
if numberOfTickMarks > 0 {
updatedValue = max(minimumValue, maximumValue)
}
if numberOfTickMarks == 1 {
updatedValue = (minimumValue + maximumValue) * 0.5
} else {
let kNumberOfDivisions = numberOfTickMarks - 1
let kSliderRange = maximumValue - minimumValue
let kRangePerDivision = kSliderRange / Float(kNumberOfDivisions)
let currentTickMark = roundf(value / kRangePerDivision)
updatedValue = currentTickMark * kRangePerDivision
}
super.setValue(updatedValue, animated: animated)
}
}

Related

How to change the highlight sytle of selected slice in PieChart of ios-charts

ios-charts is a cool library I'm using recently.
This is the expected highlighting slice:
This is my current highlighting effect:
The difference is the highlighted effect is too much than I expected.
I wonder which method can I use to adjust the highlighting effect?
Thanks for #Wingzero's answer.
However I do not want to change the source code or override some methods of it.
Finally, I found the property for the highlight effect of selected slice.
It is a property of #interface PieChartDataSet : ChartDataSet.
/// indicates the selection distance of a pie slice
#property (nonatomic) CGFloat selectionShift;
By setting it as dataSet.selectionShift = 7.0;, it works for my design.
Here attaches the object of dataSet:
PieChartDataSet *dataSet = [[PieChartDataSet alloc] initWithYVals:yVals1 label:NULL];
dataSet.selectionShift = 7.0;
PieChartData *data = [[PieChartData alloc] initWithXVals:nil dataSet:dataSet];
self.pieChartView.data = data;
This is the effect what I have expected.
You can change yourself:
In PieChartRenderer.swift:
public override func drawHighlighted(#context: CGContext, indices: [ChartHighlight])
{
if (_chart.data === nil)
{
return
}
CGContextSaveGState(context)
var rotationAngle = _chart.rotationAngle
var angle = CGFloat(0.0)
var drawAngles = _chart.drawAngles
var absoluteAngles = _chart.absoluteAngles
var innerRadius = drawHoleEnabled && holeTransparent ? _chart.radius * holeRadiusPercent : 0.0
...
}
You can change innerRadius or holeTransparent whatever you like to try.

iOS how to make slider stop at discrete points

I would like to make a slider stop at discrete points that represent integers on a timeline. What's the best way to do this? I don't want any values in between. It would be great if the slider could "snap" to position at each discrete point as well.
The steps that I took here were pretty much the same as stated in jrturton's answer but I found that the slider would sort of lag behind my movements quite noticeably. Here is how I did this:
Put the slider into the view in Interface Builder.
Set the min/max values of the slider. (I used 0 and 5)
In the .h file:
#property (strong, nonatomic) IBOutlet UISlider *mySlider;
- (IBAction)sliderChanged:(id)sender;
In the .m file:
- (IBAction)sliderChanged:(id)sender
{
int sliderValue;
sliderValue = lroundf(mySlider.value);
[mySlider setValue:sliderValue animated:YES];
}
After this in Interface Builder I hooked up the 'Touch Up Inside' event for the slider to File's Owner, rather than 'Value Changed'. Now it allows me to smoothly move the slider and snaps to each whole number when my finger is lifted.
Thanks #jrturton!
UPDATE - Swift:
#IBOutlet var mySlider: UISlider!
#IBAction func sliderMoved(sender: UISlider) {
sender.setValue(Float(lroundf(mySlider.value)), animated: true)
}
Also if there is any confusion on hooking things up in the storyboard I have uploaded a quick example to github: https://github.com/nathandries/StickySlider
To make the slider "stick" at specific points, your viewcontroller should, in the valueChanged method linked to from the slider, determine the appropriate rounded from the slider's value and then use setValue: animated: to move the slider to the appropriate place. So, if your slider goes from 0 to 2, and the user changes it to 0.75, you assume this should be 1 and set the slider value to that.
What I did for this is first set an "output" variable of the current slider value to an integer (its a float by default). Then set the output number as the current value of the slider:
int output = (int)mySlider.value;
mySlider.value = output;
This will set it to move in increments of 1 integers. To make it move in a specific range of numbers, say for example, in 5s, modify your output value with the following formula. Add this between the first two lines above:
int output = (int)mySlider.value;
int newValue = 5 * floor((output/5)+0.5);
mySlider.value = newValue;
Now your slider "jumps" to multiples of 5 as you move it.
I did as Nathan suggested, but I also want to update an associated UILabel displaying the current value in real time, so here is what I did:
- (IBAction) countdownSliderChanged:(id)sender
{
// Action Hooked to 'Value Changed' (continuous)
// Update label (to rounded value)
CGFloat value = [_countdownSlider value];
CGFloat roundValue = roundf(value);
[_countdownLabel setText:[NSString stringWithFormat:#" %2.0f 秒", roundValue]];
}
- (IBAction) countdownSliderFinishedEditing:(id)sender
{
// Action Hooked to 'Touch Up Inside' (when user releases knob)
// Adjust knob (to rounded value)
CGFloat value = [_countdownSlider value];
CGFloat roundValue = roundf(value);
if (value != roundValue) {
// Almost 100% of the time - Adjust:
[_countdownSlider setValue:roundValue];
}
}
The drawback, of course, is that it takes two actions (methods) per slider.
Swift version with ValueChanged and TouchUpInside.
EDIT: Actually you should hook up 3 events in this case:
yourSlider.addTarget(self, action: #selector(presetNumberSliderTouchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel])
I've just pasted my code, but you can easily see how it's done.
private func setSizesSliderValue(pn: Int, slider: UISlider, setSliderValue: Bool)
{
if setSliderValue
{
slider.setValue(Float(pn), animated: true)
}
masterPresetInfoLabel.text = String(
format: TexturingViewController.createAndSendPresetNumberNofiticationTemplate,
self.currentPresetNumber.name.uppercased(),
String(self.currentPresetNumber.currentUserIndexHumanFriendly)
)
}
#objc func presetNumberSliderTouchUp(_ sender: Any)
{
guard let slider = sender as? NormalSlider else{
return
}
setupSliderChangedValuesGeneric(slider: slider, setSliderValue: true)
}
private func setupSliderChangedValuesGeneric(slider: NormalSlider, setSliderValue: Bool)
{
let rounded = roundf((Float(slider.value) / Float(presetNumberStep))) * Float(presetNumberStep)
// Set new preset number value
self.currentPresetNumber.current = Int(rounded)
setSizesSliderValue(pn: Int(rounded), slider: slider, setSliderValue: setSliderValue)
}
#IBAction func presetNumberChanged(_ sender: Any)
{
guard let slider = sender as? NormalSlider else{
return
}
setupSliderChangedValuesGeneric(slider: slider, setSliderValue: false)
}

iPhone UIScrollView Speed Check

I know how to get the contentOffset on movement for a UIScrollView, can someone explain to me how I can get an actual number that represents the current speed of a UIScrollView while it is tracking, or decelerating?
There's an easier way: check the UISCrollview's pan gesture recognizer. With it, you can get the velocity like so:
CGPoint scrollVelocity = [[_scrollView panGestureRecognizer] velocityInView:self];
Have these properties on your UIScrollViewDelegate
CGPoint lastOffset;
NSTimeInterval lastOffsetCapture;
BOOL isScrollingFast;
Then have this code for your scrollViewDidScroll:
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint currentOffset = scrollView.contentOffset;
NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval timeDiff = currentTime - lastOffsetCapture;
if(timeDiff > 0.1) {
CGFloat distance = currentOffset.y - lastOffset.y;
//The multiply by 10, / 1000 isn't really necessary.......
CGFloat scrollSpeedNotAbs = (distance * 10) / 1000; //in pixels per millisecond
CGFloat scrollSpeed = fabsf(scrollSpeedNotAbs);
if (scrollSpeed > 0.5) {
isScrollingFast = YES;
NSLog(#"Fast");
} else {
isScrollingFast = NO;
NSLog(#"Slow");
}
lastOffset = currentOffset;
lastOffsetCapture = currentTime;
}
}
And from this i'm getting pixels per millisecond, which if is greater than 0.5, i've logged as fast, and anything below is logged as slow.
I use this for loading some cells on a table view animated. It doesn't scroll so well if I load them when the user is scrolling fast.
Converted #bandejapaisa answer to Swift 5:
Properties used by UIScrollViewDelegate:
var lastOffset: CGPoint = .zero
var lastOffsetCapture: TimeInterval = .zero
var isScrollingFast: Bool = false
And the scrollViewDidScroll function:
func scrollViewDidScroll(scrollView: UIScrollView) {
let currentOffset = scrollView.contentOffset
let currentTime = Date.timeIntervalSinceReferenceDate
let timeDiff = currentTime - lastOffsetCapture
let captureInterval = 0.1
if timeDiff > captureInterval {
let distance = currentOffset.y - lastOffset.y // calc distance
let scrollSpeedNotAbs = (distance * 10) / 1000 // pixels per ms*10
let scrollSpeed = fabsf(Float(scrollSpeedNotAbs)) // absolute value
if scrollSpeed > 0.5 {
isScrollingFast = true
print("Fast")
} else {
isScrollingFast = false
print("Slow")
}
lastOffset = currentOffset
lastOffsetCapture = currentTime
}
}
For a simple speed calculation (All the other answers are more complicated):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat scrollSpeed = scrollView.contentOffset.y - previousScrollViewYOffset;
previousTableViewYOffset = scrollView.contentOffset.y;
}
2017...
It's very easy to do this with modern Swift/iOS:
var previousScrollMoment: Date = Date()
var previousScrollX: CGFloat = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let d = Date()
let x = scrollView.contentOffset.x
let elapsed = Date().timeIntervalSince(previousScrollMoment)
let distance = (x - previousScrollX)
let velocity = (elapsed == 0) ? 0 : fabs(distance / CGFloat(elapsed))
previousScrollMoment = d
previousScrollX = x
print("vel \(velocity)")
Of course you want the velocity in points per second, which is what that is.
Humans drag at say 200 - 400 pps (on 2017 devices).
1000 - 3000 is a fast throw.
As it slows down to a stop, 20 - 30 is common.
So very often you will see code like this ..
if velocity > 300 {
// the display is >skimming<
some_global_doNotMakeDatabaseCalls = true
some_global_doNotRenderDiagrams = true
}
else {
// we are not skimming, ok to do calculations
some_global_doNotMakeDatabaseCalls = false
some_global_doNotRenderDiagrams = false
}
This is the basis for "skimming engineering" on mobiles. (Which is a large and difficult topic.)
Note that that is not a complete skimming solution; you also have to care for unusual cases like "it has stopped" "the screen just closed" etc etc.
May be this would be helpful
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
You can see PageControl sample code about how to get the contentOffset of scrollview.
The contentOffset on movement can be obtained from UIScrollViewDelegate method, named - (void)scrollViewDidScroll:(UIScrollView *)scrollView, by querying scrollView.contentOffset. Current speed can be calculated by delta_offset and delta_time.
Delta_offset = current_offset - pre_offset;
Delta_time = current_time - pre_time;
Here is another smart way to do this in SWIFT :-
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if velocity.y > 1.0 || velocity.y < -1.0 && self.sendMessageView.isFirstResponder() {
// Somthing you want to do when scrollin fast.
// Generally fast Vertical scrolling.
}
}
So if you scrolling vertically you should use velocity.y and also if you are scrolling horizontally you should use velocity.x . Generally if value is more than 1 and less than -1, it represent generally fast scrolling. So you can change the speed as you want. +value means scrolling up and -value means scrolling down.

Is there way to limit MKMapView maximum zoom level?

the question is - is there a way to limit maximum zoom level for MKMapView? Or is there a way to track when user zooms to the level where there's no map image available?
If you're working with iOS 7+ only, there's a new camera.altitude property that you can get/set to enforce a zoom level. Its equivalent to azdev's solution, but no external code is required.
In testing, I also discovered that it was possible to enter an infinite loop if you repeatedly tried to zoom in at detail, so I have a var to prevent that in my code below.
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
// enforce maximum zoom level
if (_mapView.camera.altitude < 120.00 && !_modifyingMap) {
_modifyingMap = YES; // prevents strange infinite loop case
_mapView.camera.altitude = 120.00;
_modifyingMap = NO;
}
}
You could use the mapView:regionWillChangeAnimated: delegate method to listen for region change events, and if the region is wider than your maximum region, set it back to the max region with setRegion:animated: to indicate to your user that they can't zoom out that far. Here's the methods:
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
I just spent some time working on this for an app i'm building. Here's what I came up with:
I started with Troy Brant's script on this page which is a nicer way to set the map view I think.
I added a method to return the current zoom level.
In MKMapView+ZoomLevel.h:
- (double)getZoomLevel;
In MKMapView+ZoomLevel.m:
// Return the current map zoomLevel equivalent, just like above but in reverse
- (double)getZoomLevel{
MKCoordinateRegion reg=self.region; // the current visible region
MKCoordinateSpan span=reg.span; // the deltas
CLLocationCoordinate2D centerCoordinate=reg.center; // the center in degrees
// Get the left and right most lonitudes
CLLocationDegrees leftLongitude=(centerCoordinate.longitude-(span.longitudeDelta/2));
CLLocationDegrees rightLongitude=(centerCoordinate.longitude+(span.longitudeDelta/2));
CGSize mapSizeInPixels = self.bounds.size; // the size of the display window
// Get the left and right side of the screen in fully zoomed-in pixels
double leftPixel=[self longitudeToPixelSpaceX:leftLongitude];
double rightPixel=[self longitudeToPixelSpaceX:rightLongitude];
// The span of the screen width in fully zoomed-in pixels
double pixelDelta=abs(rightPixel-leftPixel);
// The ratio of the pixels to what we're actually showing
double zoomScale= mapSizeInPixels.width /pixelDelta;
// Inverse exponent
double zoomExponent=log2(zoomScale);
// Adjust our scale
double zoomLevel=zoomExponent+20;
return zoomLevel;
}
This method relies on a few private methods in the code linked above.
I added this in to my MKMapView delegate (as #vladimir recommended above)
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
NSLog(#"%f",[mapView getZoomLevel]);
if([mapView getZoomLevel]<10) {
[mapView setCenterCoordinate:[mapView centerCoordinate] zoomLevel:10 animated:TRUE];
}
}
This has the effect of re-zooming if the user gets too far out. You can use regionWillChangeAnimated to prevent the map from 'bouncing' back in.
Regarding the looping comments above, it looks like this method only iterates once.
Yes, this is doable. First, extend MKMapView by using MKMapView+ZoomLevel.
Then, implement this in your MKMapViewDelegate:
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
// Constrain zoom level to 8.
if( [mapView zoomLevel] < 8 )
{
[mapView setCenterCoordinate:mapView.centerCoordinate
zoomLevel:8
animated:NO];
}
}
Here is code rewritten in Swift 3 using MKMapView+ZoomLevel and #T.Markle answer:
import Foundation
import MapKit
fileprivate let MERCATOR_OFFSET: Double = 268435456
fileprivate let MERCATOR_RADIUS: Double = 85445659.44705395
extension MKMapView {
func getZoomLevel() -> Double {
let reg = self.region
let span = reg.span
let centerCoordinate = reg.center
// Get the left and right most lonitudes
let leftLongitude = centerCoordinate.longitude - (span.longitudeDelta / 2)
let rightLongitude = centerCoordinate.longitude + (span.longitudeDelta / 2)
let mapSizeInPixels = self.bounds.size
// Get the left and right side of the screen in fully zoomed-in pixels
let leftPixel = self.longitudeToPixelSpaceX(longitude: leftLongitude)
let rightPixel = self.longitudeToPixelSpaceX(longitude: rightLongitude)
let pixelDelta = abs(rightPixel - leftPixel)
let zoomScale = Double(mapSizeInPixels.width) / pixelDelta
let zoomExponent = log2(zoomScale)
let zoomLevel = zoomExponent + 20
return zoomLevel
}
func setCenter(coordinate: CLLocationCoordinate2D, zoomLevel: Int, animated: Bool) {
let zoom = min(zoomLevel, 28)
let span = self.coordinateSpan(centerCoordinate: coordinate, zoomLevel: zoom)
let region = MKCoordinateRegion(center: coordinate, span: span)
self.setRegion(region, animated: true)
}
// MARK: - Private func
private func coordinateSpan(centerCoordinate: CLLocationCoordinate2D, zoomLevel: Int) -> MKCoordinateSpan {
// Convert center coordiate to pixel space
let centerPixelX = self.longitudeToPixelSpaceX(longitude: centerCoordinate.longitude)
let centerPixelY = self.latitudeToPixelSpaceY(latitude: centerCoordinate.latitude)
// Determine the scale value from the zoom level
let zoomExponent = 20 - zoomLevel
let zoomScale = NSDecimalNumber(decimal: pow(2, zoomExponent)).doubleValue
// Scale the map’s size in pixel space
let mapSizeInPixels = self.bounds.size
let scaledMapWidth = Double(mapSizeInPixels.width) * zoomScale
let scaledMapHeight = Double(mapSizeInPixels.height) * zoomScale
// Figure out the position of the top-left pixel
let topLeftPixelX = centerPixelX - (scaledMapWidth / 2)
let topLeftPixelY = centerPixelY - (scaledMapHeight / 2)
// Find delta between left and right longitudes
let minLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX)
let maxLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX + scaledMapWidth)
let longitudeDelta: CLLocationDegrees = maxLng - minLng
// Find delta between top and bottom latitudes
let minLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY)
let maxLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY + scaledMapHeight)
let latitudeDelta: CLLocationDegrees = -1 * (maxLat - minLat)
return MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta)
}
private func longitudeToPixelSpaceX(longitude: Double) -> Double {
return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0)
}
private func latitudeToPixelSpaceY(latitude: Double) -> Double {
if latitude == 90.0 {
return 0
} else if latitude == -90.0 {
return MERCATOR_OFFSET * 2
} else {
return round(MERCATOR_OFFSET - MERCATOR_RADIUS * Double(logf((1 + sinf(Float(latitude * M_PI) / 180.0)) / (1 - sinf(Float(latitude * M_PI) / 180.0))) / 2.0))
}
}
private func pixelSpaceXToLongitude(pixelX: Double) -> Double {
return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI
}
private func pixelSpaceYToLatitude(pixelY: Double) -> Double {
return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI
}
}
Example of use in your view controller:
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
print("Zoom: \(mapView.getZoomLevel())")
if mapView.getZoomLevel() > 6 {
mapView.setCenter(coordinate: mapView.centerCoordinate, zoomLevel: 6, animated: true)
}
}
Use this example to lock the maximum zoom range, also equally you can limit the minimum
map.cameraZoomRange = MKMapView.CameraZoomRange(maxCenterCoordinateDistance: 1200000)
If you are targeting iOS 13+, use the MKMapView setCameraZoomRange method. Simply provide the min and max center coordinate distances (measured in meters).
See Apple's Documentation here: https://developer.apple.com/documentation/mapkit/mkmapview/3114302-setcamerazoomrange
Don't use regionWillChangeAnimated. Use regionDidChangeAnimated
we can also use setRegion(region, animated: true). Normally it will freeze MKMapView if we use regionWillChangeAnimated, but with regionDidChangeAnimated it works perfectly
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
mapView.checkSpan()
}
extension MKMapView {
func zoom() {
let region = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, 2000, 2000)
setRegion(region, animated: true)
}
func checkSpan() {
let rect = visibleMapRect
let westMapPoint = MKMapPointMake(MKMapRectGetMinX(rect), MKMapRectGetMidY(rect))
let eastMapPoint = MKMapPointMake(MKMapRectGetMaxX(rect), MKMapRectGetMidY(rect))
let distanceInMeter = MKMetersBetweenMapPoints(westMapPoint, eastMapPoint)
if distanceInMeter > 2100 {
zoom()
}
}
}
The MKMapView has, inside of it, a MKScrollView (private API), that is a subclass of UIScrollView. The delegate of this MKScrollView is its own mapView.
So, in order to control the max zoom do the following:
Create a subclass of MKMapView:
MapView.h
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#interface MapView : MKMapView <UIScrollViewDelegate>
#end
MapView.m
#import "MapView.h"
#implementation MapView
-(void)scrollViewDidZoom:(UIScrollView *)scrollView {
UIScrollView * scroll = [[[[self subviews] objectAtIndex:0] subviews] objectAtIndex:0];
if (scroll.zoomScale > 0.09) {
[scroll setZoomScale:0.09 animated:NO];
}
}
#end
Then, access the scroll subview and see the zoomScale property. When the zoom is greater than a number, set your max zoom.
The post by Raphael Petegrosso with the extended MKMapView works great with some small modifications.
The version below is also much more "user friendly", as it gracefully "snaps" back to the defined zoom level as soon as the user lets go of the screen, being similar in feel to Apple's own bouncy scrolling.
Edit: This solution is not optimal and will break/damage the map view, I found a much better solution here: How to detect any tap inside an MKMapView. This allows you to intercept pinching and other motions.
MyMapView.h
#import <MapKit/MapKit.h>
#interface MyMapView : MKMapView <UIScrollViewDelegate>
#end
MyMapView.m
#import "MyMapView.h"
#implementation MyMapView
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
{
if (scale > 0.001)
{
[scrollView setZoomScale:0.001 animated:YES];
}
}
#end
For a hard limit, use this:
#import "MyMapView.h"
#implementation MyMapView
-(void)scrollViewDidZoom:(UIScrollView *)scrollView
{
if (scrollView.zoomScale > 0.001)
{
[scrollView setZoomScale:0.001 animated:NO];
}
}
#end
The following code worked for me and is conceptually easy to use because it sets the region based on a distance in meters.
The code is derived from the answer posted by: #nevan-king and the comment posted by #Awais-Fayyaz to use regionDidChangeAnimated
Add the following extension to your MapViewDelegate
var currentLocation: CLLocationCoordinate2D?
extension MyMapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if self.currentLocation != nil, mapView.region.longitudinalMeters > 1000 {
let initialLocation = CLLocation(latitude: (self.currentLocation?.latitude)!,
longitude: (self.currentLocation?.longitude)!)
let coordinateRegion = MKCoordinateRegionMakeWithDistance(initialLocation.coordinate,
regionRadius, regionRadius)
mapView.setRegion(coordinateRegion, animated: true)
}
}
}
Then define an extension for MKCoordinateRegion as follows.
extension MKCoordinateRegion {
/// middle of the south edge
var south: CLLocation {
return CLLocation(latitude: center.latitude - span.latitudeDelta / 2, longitude: center.longitude)
}
/// middle of the north edge
var north: CLLocation {
return CLLocation(latitude: center.latitude + span.latitudeDelta / 2, longitude: center.longitude)
}
/// middle of the east edge
var east: CLLocation {
return CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta / 2)
}
/// middle of the west edge
var west: CLLocation {
return CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta / 2)
}
/// distance between south and north in meters. Reverse function for MKCoordinateRegionMakeWithDistance
var latitudinalMeters: CLLocationDistance {
return south.distance(from: north)
}
/// distance between east and west in meters. Reverse function for MKCoordinateRegionMakeWithDistance
var longitudinalMeters: CLLocationDistance {
return east.distance(from: west)
}
}
The above snippet for MKCoordinateRegion was posted by #Gerd-Castan on this question:
Reverse function of MKCoordinateRegionMakeWithDistance?
I've run into this very issue at work and have created something that works fairly well without setting a global limit.
The MapView delegates that I leverage are:
- mapViewDidFinishRendering
- mapViewRegionDidChange
The premise behind my solution is that since a satellite view renders an area with no data it is always the same thing. This dreaded image (http://imgur.com/cm4ou5g) If we can comfortably rely on that fail case we can use it as a key for determining wha the user is seeing. After the map renders, I take a screenshot of the rendered map bounds and determing an average RGB value. Based off of that RGB value, I assume that the area in question has no data. If that's the case I pop the map back out to the last span that was rendered correctly.
The only global check I have is when it starts to check the map, you can increase or decrease that setting based on your needs. Below is the raw code that will accomplish this and will be putting together a sample project for contribution. Any optimizations you can offer would be appreciated and hope it helps.
#property (assign, nonatomic) BOOL isMaxed;
#property (assign, nonatomic) MKCoordinateSpan lastDelta;
self.lastDelta = MKCoordinateSpanMake(0.006, 0.006);
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
if (mapView.mapType != MKMapTypeStandard && self.isMaxed) {
[self checkRegionWithDelta:self.lastDelta.longitudeDelta];
}
}
- (void)checkRegionWithDelta:(float)delta {
if (self.mapView.region.span.longitudeDelta < delta) {
MKCoordinateRegion region = self.mapView.region;
region.span = self.lastDelta;
[self.mapView setRegion:region animated:NO];
} else if (self.mapView.region.span.longitudeDelta > delta) {
self.isMaxed = NO;
}
}
- (void)mapViewDidFinishRenderingMap:(MKMapView *)mapView fullyRendered:(BOOL)fullyRendered {
if (mapView.mapType != MKMapTypeStandard && !self.isMaxed) {
[self checkToProcess:self.lastDelta.longitudeDelta];
}
}
- (void)checkToProcess:(float)delta {
if (self.mapView.region.span.longitudeDelta < delta) {
UIGraphicsBeginImageContext(self.mapView.bounds.size);
[self.mapView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *mapImage = UIGraphicsGetImageFromCurrentImageContext();
[self processImage:mapImage];
}
}
- (void)processImage:(UIImage *)image {
self.mapColor = [self averageColor:image];
const CGFloat* colors = CGColorGetComponents( self.mapColor.CGColor );
[self handleColorCorrection:colors[0]];
}
- (void)handleColorCorrection:(float)redColor {
if (redColor < 0.29) {
self.isMaxed = YES;
[self.mapView setRegion:MKCoordinateRegionMake(self.mapView.centerCoordinate, self.lastDelta) animated:YES];
} else {
self.lastDelta = self.mapView.region.span;
}
}
- (UIColor *)averageColor:(UIImage *)image {
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
unsigned char rgba[4];
CGContextRef context = CGBitmapContextCreate(rgba, 1, 1, 8, 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), image.CGImage);
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
if(rgba[3] > 0) {
CGFloat alpha = ((CGFloat)rgba[3])/255.0;
CGFloat multiplier = alpha/255.0;
return [UIColor colorWithRed:((CGFloat)rgba[0])*multiplier
green:((CGFloat)rgba[1])*multiplier
blue:((CGFloat)rgba[2])*multiplier
alpha:alpha];
}
else {
return [UIColor colorWithRed:((CGFloat)rgba[0])/255.0
green:((CGFloat)rgba[1])/255.0
blue:((CGFloat)rgba[2])/255.0
alpha:((CGFloat)rgba[3])/255.0];
}
}

How to get the center of the thumb image of UISlider

I'm creating a custom UISlider to test out some interface ideas. Mostly based around making the thumb image larger.
I found out how to do that, like so:
UIImage *thumb = [UIImage imageNamed:#"newThumbImage_64px.png"];
[self.slider setThumbImage:thumb forState:UIControlStateNormal];
[self.slider setThumbImage:thumb forState:UIControlStateHighlighted];
[thumb release];
To calculate a related value I need to know where the center point of the thumb image falls when it's being manipulated. And the point should be in it's superview's coordinates.
Looking at the UISlider docs, I didn't see any property that tracked this.
Is there some easy way to calculate this or can it be derived from some existing value(s)?
This will return the correct X position of center of thumb image of UISlider in view coordinates:
- (float)xPositionFromSliderValue:(UISlider *)aSlider {
float sliderRange = aSlider.frame.size.width - aSlider.currentThumbImage.size.width;
float sliderOrigin = aSlider.frame.origin.x + (aSlider.currentThumbImage.size.width / 2.0);
float sliderValueToPixels = (((aSlider.value - aSlider.minimumValue)/(aSlider.maximumValue - aSlider.minimumValue)) * sliderRange) + sliderOrigin;
return sliderValueToPixels;
}
Put it in your view controller and use it like this: (assumes property named slider)
float x = [self xPositionFromSliderValue:self.slider];
I tried this after reading the above suggestion -
yourLabel = [[UILabel alloc]initWithFrame:....];
//Call this method on Slider value change event
-(void)sliderValueChanged{
CGRect trackRect = [self.slider trackRectForBounds:self.slider.bounds];
CGRect thumbRect = [self.slider thumbRectForBounds:self.slider.bounds
trackRect:trackRect
value:self.slider.value];
yourLabel.center = CGPointMake(thumbRect.origin.x + self.slider.frame.origin.x, self.slider.frame.origin.y - 20);
}
For Swift version
func sliderValueChanged() -> Void {
let trackRect = self.slider.trackRect(forBounds: self.slider.bounds)
let thumbRect = self.slider.thumbRect(forBounds: self.slider.bounds, trackRect: trackRect, value: self.slider.value)
yourLabel.center = CGPoint(x: thumbRect.origin.x + self.slider.frame.origin.x + 30, y: self.slider.frame.origin.y - 60)
}
I could get most accurate value by using this snippet.
Swift 3
extension UISlider {
var thumbCenterX: CGFloat {
let trackRect = self.trackRect(forBounds: frame)
let thumbRect = self.thumbRect(forBounds: bounds, trackRect: trackRect, value: value)
return thumbRect.midX
}
}
I would like to know why none of you provide the simplest answer which consist in reading the manual. You can compute all these values accurately and also MAKING SURE THEY STAY THAT WAY, by simply using the methods:
- (CGRect)trackRectForBounds:(CGRect)bounds
- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value
which you can easily find in the developer documentation.
If thumb image changes and you want to change how it's positioned, you subclass and override these methods. The first one gives you the rectangle in which the thumb can move the second one the position of the thumb itself.
It's better to use -[UIView convertRect:fromView:] method instead. It's cleaner and easier without any complicated calculations:
- (IBAction)scrub:(UISlider *)sender
{
CGRect _thumbRect = [sender thumbRectForBounds:sender.bounds
trackRect:[sender trackRectForBounds:sender.bounds]
value:sender.value];
CGRect thumbRect = [self.view convertRect:_thumbRect fromView:sender];
// Use the rect to display a popover (pre iOS 8 code)
[self.popover dismissPopoverAnimated:NO];
self.popover = [[UIPopoverController alloc] initWithContentViewController:[UIViewController new]];
[self.popover presentPopoverFromRect:thumbRect inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionDown|UIPopoverArrowDirectionUp animated:YES];
}
I approached it by first mapping the UISlider's value interval in percents and then taking the same percent of the slider's size minus the percent of the thumb's size, a value to which I added half of the thumb's size to obtain its center.
- (float)mapValueInIntervalInPercents: (float)value min: (float)minimum max: (float)maximum
{
return (100 / (maximum - minimum)) * value -
(100 * minimum)/(maximum - minimum);
}
- (float)xPositionFromSliderValue:(UISlider *)aSlider
{
float percent = [self mapValueInIntervalInPercents: aSlider.value
min: aSlider.minimumValue
max: aSlider.maximumValue] / 100.0;
return percent * aSlider.frame.size.width -
percent * aSlider.currentThumbImage.size.width +
aSlider.currentThumbImage.size.width / 2;
}
Swift 3.0
Please refer if you like.
import UIKit
extension UISlider {
var trackBounds: CGRect {
return trackRect(forBounds: bounds)
}
var trackFrame: CGRect {
guard let superView = superview else { return CGRect.zero }
return self.convert(trackBounds, to: superView)
}
var thumbBounds: CGRect {
return thumbRect(forBounds: frame, trackRect: trackBounds, value: value)
}
var thumbFrame: CGRect {
return thumbRect(forBounds: bounds, trackRect: trackFrame, value: value)
}
}
AFter a little playing with IB and a 1px wide thumb image, the position of the thumb is exactly where you'd expect it:
UIImage *thumb = [UIImage imageNamed:#"newThumbImage_64px.png"];
CGRect sliderFrame = self.slider.frame;
CGFloat x = sliderFrame.origin.x + slideFrame.size.width * slider.value + thumb.size.width / 2;
CGFloat y = sliderFrame.origin.y + sliderFrame.size.height / 2;
return CGPointMake(x, y);
Here is a Swift 2.2 solution, I created an extension for it. I have only tried this with the default image.
import UIKit
extension UISlider {
var thumbImageCenterX: CGFloat {
let trackRect = trackRectForBounds(bounds)
let thumbRect = thumbRectForBounds(bounds, trackRect: trackRect, value: value)
return thumbRect.origin.x + thumbRect.width / 2 - frame.size.width / 2
}
}
Above solution is useful when UISlider is horizontal. In a recent project,we need to use UISlider with angle. So I need to get both x and y position. Using below to calculate the x,y axis:
- (CGPoint)xyPositionFromSliderValue:(UISlider *)aSlider WithAngle:(double)aangle{
//aangle means the dextrorotation angle compare to horizontal.
float xOrigin = 0.0;
float yOrigin = 0.0;
float xValueToaXis=0.0;
float yValueToaXis=0.0;
float sliderRange = slider_width-aSlider.currentThumbImage.size.width;
xOrigin = aSlider.frame.origin.x+slider_width*fabs(cos(aangle/180.0*M_PI));
yOrigin = aSlider.frame.origin.y;
xValueToaXis = xOrigin + ((((((aSlider.value-aSlider.minimumValue)/(aSlider.maximumValue-aSlider.minimumValue)) * sliderRange))+(aSlider.currentThumbImage.size.width / 2.0))*cos(aangle/180.0*M_PI)) ;
yValueToaXis = yOrigin + ((((((aSlider.value-aSlider.minimumValue)/(aSlider.maximumValue-aSlider.minimumValue)) * sliderRange))+(aSlider.currentThumbImage.size.width / 2.0))*sin(aangle/180.0*M_PI));
CGPoint xyPoint=CGPointMake(xValueToaXis, yValueToaXis);
return xyPoint;
}
Besides, can I Create a Ranger Slider based on UISlider? Thanks.
This will work for the UISlider being placed anywhere on the screen. Most of the other solutions will only work when the UISlider is aligned with the left edge of the screen. Note, I used frame rather than bounds for the thumbRect, to achieve that. And I show two variations, based on using frame or bounds for the trackRect
extension UISlider {
//this version will return the x coordinate in relation to the UISlider frame
var thumbCenterX: CGFloat {
return thumbRect(forBounds: frame, trackRect: trackRect(forBounds: bounds), value: value).midX
}
//this version will return the x coordinate in relation to the UISlider's containing view
var thumbCenterX: CGFloat {
return thumbRect(forBounds: frame, trackRect: trackRect(forBounds: frame), value: value).midX
}
}
step 1 :get View for detect position (use same extension top commet of# Ovi Bortas)
#IBOutlet weak var sliderView: UIView!
step 2 : set label frame for add sub view
func setLabelThumb(slider:UISlider,value:Float){
slider.value = value
let label = UILabel(frame: CGRect(x: slider.thumbCenterX - 20, y: slider.frame.origin.y - 25, width: 50, height: 30))
label.font = UIFont.systemFont(ofSize: 10.0)
label.textColor = UIColor.red
label.textAlignment = .center
label.text = "\(value) kg."
sliderView.addSubview(label)
}