Get frame from certain UISegmentedControl index? - ios

In my app, I use a UIPopoverController and I use the presentPopoverFromRect API. What I am doing now is just setting it to the frame of my whole UISegmentedControl. However I want to be more precise than this. Is there any way to get the frame of a specific index in the UISegmentedControl?
Thanks!

For our project we needed the actual frames of each segment, frame division wasn't enough. Here's a function I wrote that calculates the exact frame for every segment. Be aware that it accesses the segmented control actual subviews, so it might break in any iOS update.
- (CGRect)segmentFrameForIndex:(NSInteger)index inSegmentedControl:(UISegmentedControl *)control
{
// WARNING: This function gets frame from UISegment objects, undocumented subviews of UISegmentedControl.
// May break in iOS updates.
NSMutableArray *segments = [NSMutableArray arrayWithCapacity:self.numberOfSegments];
for (UIView *view in control.subviews) {
if ([NSStringFromClass([view class]) isEqualToString:#"UISegment"]) {
[segments addObject:view];
}
}
NSArray *sorted = [segments sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
if (a.frame.origin.x < b.frame.origin.x) {
return NSOrderedAscending;
} else if (a.frame.origin.x > b.frame.origin.x) {
return NSOrderedDescending;
}
return NSOrderedSame;
}];
return [[sorted objectAtIndex:index] frame];
}

If the segments are equal, why not just divide the width of the control by the number of the selected segment (+1 because numbering starts at 0)?
EDIT: Like this
-(void)showPopover:(id)sender {
if ((UISegmentedControl*)sender.selectedSegmentIndex == 0)
[self.popover presentPopoverFromRect:CGRectMake(self.segmentedControl.frame.size.width/6, self.segmentedControl.frame.origin.y, aWidth, aHeight)]
}
It's over 6 (I'm assuming a 3 segment implementation), because you have to get the center of the segment, and 3 would put it on the lines. And if you do some simple math here (let's assume the whole control is 60 px wide), then 60/3 yeilds 20. Because each segment is 20 px wide, the width of 60 over six yields the correct answer 10.

This can be done much easier without matching class names, providing that you use no custom segmented control (Swift version)
extension NSSegmentedControl {
func frameForSegment(segment: Int) -> NSRect {
var left : CGFloat = 0
for i in 0..<segmentCount {
let w = widthForSegment(i)
if i == segment {
let off = CGFloat(i) + 2 // Account for separators and border.
return NSRect(x: left + off, y: bounds.minY, width: w, height: bounds.height)
}
left += w
}
return NSZeroRect
}
}

If somebody is searching for a solution for NSSegmentedControl, I've slightly modified #erik-aigner's answer. For UISegmentedControl it should work similarly.
This version improves the geometry computation for spacing and the code in general. Disclaimer: It was tested only for a segmented control placed in a toolbar.
import AppKit
public extension NSSegmentedControl {
/// The width of a single horizontal border.
public static let horizontalBorderWidth: CGFloat = 3
/// The height of a single vertical border.
public static let verticalBorderWidth: CGFloat = 2
/// The horizontal spacing between segments.
public static let horizontalSegmentSpacing: CGFloat = 1
/// Returns the frame of the specified segment.
///
/// - Parameter segment: The index of the segment whose frame should be computed.
/// - Returns: The frame of the segment or `.zero` if an invalid segment index is passed in.
public func frame(forSegment segment: Int) -> NSRect {
let y = bounds.minY - NSSegmentedControl.verticalBorderWidth
let height = bounds.height
var left = NSSegmentedControl.horizontalBorderWidth
for index in 0..<segmentCount {
let width = self.width(forSegment: index)
if index == segment {
return NSRect(x: left, y: y, width: width, height: height)
}
left += NSSegmentedControl.horizontalSegmentSpacing
left += width
}
return .zero
}
}

Swift 5.3 version of #CodaFi's answer:
func showPopover(_ sender: Any?) {
if sender?.selectedSegmentIndex as? UISegmentedControl == 0 {
popover.presentPopover(fromRect: CGRect(x: segmentedControl.frame.size.width / 6, y: segmentedControl.frame.origin.y, width: aWidth, height: aHeight))
}
}

For me widthForSegment returns 0, so the other answers always fail to produce frame. I think a better solution is to just get the frame of a subview:
public extension UISegmentedControl {
func frame(forSegment segment: Int) -> CGRect {
subviews[segment].frame
}
}

Related

How do you get correct caret size AND position when using NSMutableParagraphStyle and paragraphSpacingBefore in Swift 4

I've been playing around with attributed text in a UITextView (Swift 4.2 and noticed that once I introduced "paragraphSpacingBefore" into my design, the Caret becmae too large on the first line of each new paragraph.
I found this suggested fix on Stackoverflow which seemed to work ok to fix the caret size. The problem I found was the caret itself floats above the target line when that line was the start of a new paragraph.
UITextView lineSpacing make cursor height not same
Caret Floats above the target line
I tried solving it, maintaining the core idea of the original solution and adding some offset logic. During debugging I noticed that the original answer for caret size always adjusts the size even when not required so I added a variance filter (only adjust if variance > 10%). Did this because I think adjusting every time will interfere with my soln. to the floating caret problem.
If someone can take a look at my proposed approach, suggest improvements or a better way etc i'd be grateful:
override func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else {
return superRect
}
let proposedHeight: CGFloat = isFont.pointSize - isFont.descender
var delta: CGFloat = superRect.size.height - proposedHeight
delta = (delta * delta).squareRoot()
//If the delta is < 10% of the original height just return the original rect
if delta / superRect.size.height < 0.1 {
return superRect
}
superRect.size.height = isFont.pointSize - isFont.descender
// "descender" is expressed as a negative value,
// so to add its height you must subtract its value
superRect.origin.y = superRect.origin.y + delta
// delta is used to correct for resized caret floating above the target line
return superRect
}
I got a solution:
// Fix long cursor height when at the end of paragraph with paragraphspacing and wrong cursor position in titles with paragraph spacing before
override public func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else { return superRect }
let location = self.offset(from: self.beginningOfDocument, to: position)
if let paragrahStyle = self.storage.attribute(.paragraphStyle, at: location, effectiveRange: nil) as? NSParagraphStyle {
superRect.origin.y += paragrahStyle.paragraphSpacingBefore
}
superRect.size.height = isFont.pointSize - isFont.descender
return superRect
}
The real problem paragraphSpacingBefore. So all you have to do is to get the paragraph styling attributes, get the spacing and move the cursor by that spacing. This works well with all the text.

Swift: How do I arrange programatically added checkboxes?

I'm creating an iOS app using Swift, one with checkboxes. Currently, I've placed them inside a view (constrained and all), in the hopes that they would stay there and not mess up the rest of my app. Here's my code so far:
// UI
let lCheckboxHeight: CGFloat = 44.0;
let lCheckboxWidth: CGFloat = 180.0;
let waterSampleTreatmentTitles = ["i - Untreated", "ii - Acidified", "iii - Airfree", "iv - Filtered, Untreated","v - Filtered, Acidified","Stable Isotopes","Others"];
let lNumberOfCheckboxes = waterSampleTreatmentTitles.count
var lFrame = CGRectMake(0, 0, self.view.frame.size.width, lCheckboxHeight);
for (var counter = 0; counter < lNumberOfCheckboxes; counter++) {
let lCheckbox = Checkbox(frame: lFrame, title: waterSampleTreatmentTitles[counter], selected: false);
lCheckbox.mDelegate = self;
lCheckbox.tag = counter;
if (waterSampleTreatmentTitles[counter] == "i - Untreated" && self.flagUntreated == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "ii - Acidified" && self.flagAcidified == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "iii - Airfree" && self.flagAirfree == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "iv - Filtered, Untreated" && self.flagFilterUntreat == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "v - Filtered, Acidified" && self.flagFilterAcid == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "Stable Isotopes" && self.flagStabIso == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "Others" && self.flagOthers == true){
lCheckbox.selected = true
}
self.chemistryProductionWell.viewSampTreat.addSubview(lCheckbox);
lFrame.origin.y += lFrame.size.height;
}
Currently, this creates a list of checkboxes, which spills past the view I made, and just generally makes a mess of the app. The view is long enough for maybe two checkboxes vertically, but not eight.
How do I make it such that the checkboxes are arranged horizontally? I've tried replacing the following code:
lFrame.origin.y += lFrame.size.height;
With this:
lFrame.origin.x += lCheckboxWidth
But that doesn't take into account that the text for the checkboxes aren't the same length, and of course ignores the width restriction as well.
How do I make it such that if the checkbox length exceeds the view, it would drop down to the next line?
Thanks.
If the checkbox has no way to determine its own intrinsic size, you'll have to determine the width that each title would take, then factor that value into that checkbox's frame width.
However, wrapping a row of checkboxes will lead to a couple issues. First, the layout would need to be updated upon autorotation. Second, each row's checkboxes would not be vertically aligned with the previous row's checkboxes.
What you preferably want is a checkbox that can determine its own intrinsic size. Now you'd be able to take advantage of the Auto Layout system, and both the checkboxes, and their containing view could size themselves. This would have avoided the problem where the checkboxes are located outside their container's bounds (as well as having to hard code frames).
At that point, you could benefit from an easier solution like UIStackView, instead of having to code their layout on your own.
If you are using Storyboard, the ideal option would be be a checkbox that could not only self-size, but supported IBDesignable. You could then just setup outlets to IB checkboxes, instead of doing any of this in code.
If you aren't able to find a better control, then the easiest way to do what you ask would be as follows.
let lCheckboxHeight: CGFloat = 44.0
// Make the width as wide as necessary to accommodate the largest title
let lCheckboxWidth: CGFloat = 180.0
// Spacing between checkboxes or rows
let xSpacing: CGFloat = 10
let ySpacing: CGFloat = 10
// Current offset for next checkbox
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
for counter in 0..<lNumberOfCheckboxes {
var lFrame = CGRect(x: xOffset, y: yOffset, width: lCheckboxWidth, height: lCheckboxHeight);
// Advance offset for upcoming checkbox
xOffset += lCheckboxWidth + xSpacing
// Determine if there is enough room for another horizontal checkbox
let maxX = xOffset + lCheckboxWidth + xSpacing
if maxX >= chemistryProductionWell.viewSampTreat.bounds.maxX {
// Move to start of next row
xOffset = 0
yOffset += lCheckboxHeight + ySpacing
}
// ... Other code here
}
This still doesn't do anything to support autorotation, which is why you should look into an Auto Layout/Adaptive UI approach, and not directly work with frames.
As an aside, although you placed them in a viewSampTreat container, you had based their width on something other than their container, i.e. self.view.frame.size.width. This would have led to a clipping problem.

Update position of UIImageView Swift

I'm trying to make a draggable image (monkey) takes the place of another draggable image (banana) when I'm pushing a button.
So far, I've been able to get the coordinates of the banana, and store them as the new x,y coordinates of the monkey. But, the view is not updating and the monkey does not moves to it's new place.
I'm sure that I need to update the view, but can't figure it out.
I've tried to add a new subview (self.view.addSubview(monkey)) but when I do that, it's the entire view that is reset and both monkey and banana are going back to their original coordinates.
Here's the code of the action button :
#IBAction func getCoordinates (sender : AnyObject) {
var bananax = banana.frame.origin.x
var bananay = banana.frame.origin.y
var monkeyx = monkey.frame.origin.x
var monkeyy = monkey.frame.origin.y
println("Banana : \(bananax) et \(bananay)")
println("Monkey's default coordinates : \(monkeyx) et \(monkeyy)")
monkeyx = bananax
monkeyy = bananay
println("Monkey's new coordinates : \(monkeyx) et \(monkeyy)")
}
Any idea on how to update only the monkey and not reseting the entire view ?
Thanks :)
These values are just thrown away when you set monkeyx and monkeyy below.
var monkeyx = monkey.frame.origin.x
var monkeyy = monkey.frame.origin.y
Assigning the values below just assigns the values from one local variable to another. It does not actually change the coordinates for monkey.
monkeyx = bananax
monkeyy = bananay
You must set the new coordinates on monkey by constructing a new CGRect and assigning monkey's frame:
monkey.frame = CGRect(x: bananax, y: bananay, width: monkey.frame.size.width, height: monkey.frame.size.height)
As you are just setting monkey's origin to banana's origin, you can remove all the var declarations and reduce this to one line by constructing monkey's new frame with monkey's size and banana's origin:
monkey.frame = CGRect(origin: banana.frame.origin, size: monkey.frame.size)
Hi create this extends if you want. For Swift
Create File Extends.Swift and add this code
/**
Extension UIView
by DaRk-_-D0G
*/
extension UIView {
/**
Set x Position
:param: x CGFloat
by DaRk-_-D0G
*/
func setX(#x:CGFloat) {
var frame:CGRect = self.frame
frame.origin.x = x
self.frame = frame
}
/**
Set y Position
:param: y CGFloat
by DaRk-_-D0G
*/
func setY(#y:CGFloat) {
var frame:CGRect = self.frame
frame.origin.y = y
self.frame = frame
}
/**
Set Width
:param: width CGFloat
by DaRk-_-D0G
*/
func setWidth(#width:CGFloat) {
var frame:CGRect = self.frame
frame.size.width = width
self.frame = frame
}
/**
Set Height
:param: height CGFloat
by DaRk-_-D0G
*/
func setHeight(#height:CGFloat) {
var frame:CGRect = self.frame
frame.size.height = height
self.frame = frame
}
}
For Use (inherits Of UIView)
inheritsOfUIView.setX(x: 100)
button.setX(x: 100)
view.setY(y: 100)
Now you can do
monkey.frame.origin = banana.frame.origin
If you are using a UIImageView for monkey and banana, you can simplify this by doing the following
monkey.center = banana.center
Understand that there is a difference between origin and center. See more here:
UIView's frame, bounds, center, origin, when to use what?
but based on question, I think center should work fine and it is simpler.

X axis label formatting in Shinobi charts

I have a chart of name versus age, where name is on the x axis and value on Y axis.
The problem is the names are getting overlapped on the x axis which do not look good.
I could not find any formatting for showing truncated name values on x axis and complete as we zoom in.
Is there any way to show names with ellipses or other formatting where the names will not overlap?
The alterTickMark: method on SChartDelegate allows you to modify a tickMark (and its corresponding tickLabel) before they are added to the axis.
You could potentially check the axisRange in this step, and decide if the range.span is small enough that you could display labels truncated or in full.
E.g.
-(void)sChart:(ShinobiChart *)chart alterTickMark:(SChartTickMark *)tickMark beforeAddingToAxis:(SChartAxis *)axis
{
if (!axis.isXAxis)
return;
if ([axis.axisRange.span doubleValue] > 5)
{
NSString *shortText = [tickMark.tickLabel.text substringToIndex:3];
tickMark.tickLabel.text = [NSString stringWithFormat:#"%#...", shortText];
//Resize, but maintain centering
CGPoint center = tickMark.tickLabel.center;
[tickMark.tickLabel sizeToFit];
tickMark.tickLabel.center = center;
}
}
As full disclosure, I work for ShinobiControls.
If you need to adjust tick label width in column series and zooming is enabled (which makes more space for label after zoom gesture), just implement SChart delegate method:
- (void)sChart:(ShinobiChart *)chart alterTickMark:(SChartTickMark *)tickMark beforeAddingToAxis:(SChartAxis *)axis
{
if (axis.isXAxis) {
// Adjusting tickmark labels
UILabel *label = [tickMark tickLabel];
CGFloat maxLabelWidth = axis.axisFrame.size.width / [axis.axisRange.span floatValue];
CGRect labelFrame = label.frame;
labelFrame.size.width = maxLabelWidth;
[label setFrame:labelFrame];
}
}
Look at the longestLabelStringOn delegate method. Example:
func sChart(_ chart: ShinobiChart, longestLabelStringOn axis: SChartAxis) -> String? {
if axis == chart.xAxis{
return "Longest Possible String"
}
else{
return ""
}
}

Understanding CGRectDivide()

I am trying to experiment with UIViews on screen and using pan gestures. So I got some open source code from another project that I am looking at - and trying to learn a few things from it.
-(BOOL)isPointContainedWithinBezelRect:(CGPoint)point {
CGRect leftBezelRect;
CGRect tempRect;
CGFloat bezelWidth = 20;
CGRectDivide(self.view.bounds, &leftBezelRect, &tempRect, bezelWidth, CGRectMinXEdge);
return CGRectContainsPoint(leftBezelRect, point);
}
I understand that CGRectDivide function "Slices up a rect", but thats as far as I can make out.
I hope to get more clarification regarding the function. Also, how does the function return value vide a false / true value?
void CGRectDivide(
CGRect rect,
CGRect *slice,
CGRect *remainder,
CGFloat amount,
CGRectEdge edge
)
The CGRectDivide method splits a CGRect into two CGRects based on the CGRectEdge and distance from the rectangle side amount provided to the method.
Source
You should check
https://developer.apple.com/library/ios/documentation/graphicsimaging/reference/CGGeometry/Reference/reference.html#//apple_ref/c/func/CGRectDivide
and
http://nshipster.com/cggeometry/
But it seems that this method could be simplified to
-(BOOL)isPointContainedWithinBezelRect:(CGPoint)point {
CGRect leftBezelRect = self.view.bounds;
leftBezelRect.size.width = 20;
return CGRectContainsPoint(leftBezelRect, point);
}
or even to
-(BOOL)isPointContainedWithinBezelRect:(CGPoint)point {
return CGRectContainsPoint(self.view.bounds, point) && (point.x <= 20);
}

Resources