UIScrollView with Horizontal Zoom - Bezier Paths Drawn within Subviews Are Blurry - ios

I have a scroll view that zooms only horizontally on pinch (based on this answer)
The content view has several children that are evenly spaced horizontally, and placed at different heights. Basically, I'm plotting a graph with dots.
Each marker is a custom UIView subclass that has a label as subview and draws a red circle inside drawRect(_:).
(The scroll view and its only child the content view use Autolayout, but all subviews of the content view are placed with frames calculated at runtime; no constraints except the label positioning respect to the marker)
I have modified the answer linked above, so that -when zooming- the dots get more spaced horizontally, but stay the same size.
This is part of the code for my content view:
override var transform: CGAffineTransform {
get {
return super.transform
}
set {
if let unzoomedViewHeight = unzoomedViewHeight {
// ^Initial height of the content view, captured
// on layoutSubviews() and used to calculate a
// a zoom transform that preserves the height
// (see linked answer for details)
// 1. Ignore vertical zooming for this view:
var modified = newValue
modified.d = 1.0
modified.ty = (1.0 - modified.a) * unzoomedViewHeight/2
super.transform = modified
// 2. Ignore zooming altogether in marker subviews:
var markerTransform = CGAffineTransformInvert(newValue)
markerTransform.d = 1.0 // No y-scale
for subview in subviews where subview is MarkerView {
subview.transform = markerTransform
}
}
}
}
Next, I want to connect my dots with straight line segments. The problem is, when I zoom the scroll view the segments become blurry.
In addition to the blurring, and because I am only zooming horizontally, the segments "shear" a bit (depending on their their slope), and thus the line width becomes uneven:
(Right now I am placing an intermediate view between each pair of dots, that draws a single segment - but the result is the same with a single path)
I have tried making the segment-drawing views aware of the transform that is applied to them by their parent, revert it, and instead modify their frame accordingly (and redraw the path within the new bounds); however it doesn't seem to work.
What is the best way to draw zoom-resistant bezier paths inside a scroll view?
Update:
I got rid of the pixelation by following this answer to a similar question (almost duplicate?); in my case the code translates to:
func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView?, atScale scale: CGFloat)
{
for segment in contentView.subviews where segment is SegmentView {
segment.layer.contentsScale = scale
segment.setNeedsDisplay()
}
}
...However, I still have the uneven line width issue due to stretching the (diagonal) segments only in the horizontal direction.

My initial thought, having not done this before exactly, is to store the graph as a bezier path rather than as a set of view line and point views, and based on a single unit axis. You could convert your data into this form just for drawing.
So, all points on the bezier path are normalised into the range 0 to 1.
Once you've done that you can apply a transform to the bezier path to translate (move) and scale (zoom) to the part you want and then draw the bezier path at full resolution.
This will work and is different to your current situation because your current code draws the views and then scales then so you get artifacts. The above option scales the path and then draws it.

Related

Is a view's frame always set by the time `draw(_:)` is called?

I use the below code to curve the edge of a custom view.
/// Curves `self's` trailing edge.
override func draw(_ rect: CGRect) {
super.draw(rect)
let path=UIBezierPath(ovalIn: .init( // The path defining `self's` curved edge.
x: rect.width - Self.ovalWidth, // Align the oval's trailing edge with `self's`.
y: (rect.height - Self.ovalHeight) / 2, // Center oval vertically within `self`.
width: Self.ovalWidth,
height: Self.ovalHeight
))
path.stroke() // Draws the border of the curved edge.
self.layer.mask={ $0.path=path.cgPath; return $0 }(CAShapeLayer())
}
My concern is that as far as I can see there is no guarantee that rect will be equal to self.frame; which would result in undefined behavior as far as drawing the mask goes. Is it possible that if I use self.frame instead of rect that draw(_:) could be called while self.frame has yet to be set, or has a value that is not up-to-date? — this view is laid out using AutoLayout.
Your concerns are only half correct. According to the docs:
The portion of the view’s bounds that needs to be updated. The first
time your view is drawn, this rectangle is typically the entire
visible bounds of your view. However, during subsequent drawing
operations, the rectangle may specify only part of your view.
Also in the docs:
You should limit any drawing to the rectangle specified in the rect
parameter
draw(_:) is typically only called when the view is first displayed (not when it is added as a subview) and when setNeedsDisplay() or setNeedsDisplay(_:) is called.
As far as I know there are only two causes for concern. The first is that your view is displayed before all of you constraints are set. The second is if you call setNeedsDisplay(_:) with a specific rectangle.

How do i get the vertical distance from an UIView to another one?

As the title suggests, i struggle to find a way to calculate the vertical distance between UIViews.
Say i have two buttons in a View Controller. How would I go about getting a CGFloat indicative of the distance between button1's bottom and button2's top?
As already pointed out by #lobstah, the difference can be calculated by accessing the frames of the views in question.
However, there's an important detail that must be considered: This only works in that simple way, if the views share the same parent view. Because only then, the frames will be expressed with regards to the same coordinate space.
A general approach with doesn't come with this restriction and will always work as long as the views are part of the same view hierarchy (don't appear on different windows) and are not scaled / rotated by an affine transform is the following:
func distanceBetween(bottomOf view1: UIView, andTopOf view2: UIView) -> CGFloat {
let frame2 = view1.convert(view2.bounds, from: view2)
return frame2.minY - view1.bounds.maxY
}
The distance would be positive if the top of view2 is below the bottom of view1.
You can use maxY of the top and minY of bottom view’s frames properties:
bottomView.frame.minY - topView.frame.maxY

Best way to add multiple diagonal connection lines between TableViewCells

I'm creating an app that needs to show a tableview like below image
Similar colored circles are to be matched with a line.
Which view i can add the lines?
Or need to create a new view above tableview? But still my tableview needs to be scrolled.
How can i achieve this?
Update for Bounty
I want to implement the same with incliend lines between neighbouring circles. How to achieve the same?
Demonstration below:
create design like this
Based on your requirement just hide upper line and lower line of circle
You need to create collection view in tableview cell. In collection view you create one cell. Design the same user interface like your design. Show and hide the view with matching of rule. It will not affect tableview scrolling. and with this approach you can also provide scroll in collection view cell. i can provide you coded solution if you able to provide me more information. Thanks
You can use this Third Party LIb
You need to use a combination of collection view and a table view to give support for all devices.
1.Create one collection view cell with following layout
Hide upper and lower lines as per your need
Add collection view in table view cell and managed a number of cells in collection view depending upon the current device width and item's in between spacing.
You can create a vertical label without text, set the background color with black and place it behind the circle in view hierarchy and set a width of the label as per your requirement. Then you can hide unhide the label whenever you want.
P.S.: Make sure to hide your cell separator.
I have created a demo project. You can find it here. I tried to match your requirements. You can update the collection view settings to handle the hide and show of labels.
Let me know if you have any questions.
Hope this help.
Thanks!
To connect any circle with any other circle in the cell above / below, it will be easier and cleaner to create the connection lines dynamically rather than building them into the asset as before. This part is simple. The question now is where to add them.
You could have the connection lines between every two cells be contained in the top or bottom cell of each pair, since views can show content beyond their bounds.
There's a problem with this though, regardless of which cell contains the lines. For example, if the top cell contains them, then as soon as it is scrolled up off screen, the lines will disappear when didEndDisplayingCell is called, even though the bottom cell is still completely on screen. And then scrolling slightly such that cellForRow is called, the lines will suddenly appear again.
If you want to avoid that problem, then here is one approach:
One Approach
Give your table view and cells a clear background color, and have another table view underneath to display a new cell which will contain the connection lines.
So you now have a background TVC, with a back cell, and a foreground TVC with a fore cell. You add these TVC's as children in a parent view controller (of which you can set whatever background color you like), disable user interaction on the background TVC, and peg the background TVC's content offset to the foreground TVC's content offset in an observation block, so they will stay in sync when scrolling. I've done this before; it works well. Use the same row height, and give the background TVC a top inset of half the row height.
We can make the connection lines in the back cell hug the top and bottom edges of the cell. This way circles will be connected at their centre.
Perhaps define a method in your model that calculates what connections there are, and returns them, making that a model concern.
extension Array where Element == MyModel {
/**
A connection is a (Int, Int).
(0, 0) means the 0th circle in element i is connected to the 0th circle in element j
For each pair of elements i, j, there is an array of such connections, called a mesh.
Returns n - 1 meshes.
*/
func getMeshes() -> [[(Int, Int)]] {
// Your code here
}
}
Then in your parent VC, do something like this:
class Parent_VC: UIViewController {
var observation: NSKeyValueObservation!
var b: Background_TVC!
override func viewDidLoad() {
super.viewDidLoad()
let b = Background_TVC(model.getMeshes())
let f = Foreground_TVC(model)
for each in [b, f] {
self.addChild(each)
each.view.frame = self.view.bounds
self.view.addSubview(each.view)
each.didMove(toParent: self)
}
let insets = UIEdgeInsets(top: b.tableView.rowHeight / 2, left: 0, bottom: 0, right: 0)
b.tableView.isUserInteractionEnabled = false
b.tableView.contentInset = insets
self.b = b
self.observation = f.tableView.observe(\.contentOffset, options: [.new]) { (_, change) in
let y = change.newValue!.y
self.b.tableView.contentOffset.y = y // + or - half the row height
}
}
}
Then of course there's your drawing code. You could make it a method of your back cell class (a custom cell), which will take in a mesh data structure and then draw the lines that represent it. Something like this:
class Back_Cell: UITableViewCell {
/**
Returns an image with all the connection lines drawn for the given mesh.
*/
func createMeshImage(for mesh: [(Int, Int)]) -> UIImage {
let canvasSize = self.contentView.bounds.size
// Create a new canvas
UIGraphicsBeginImageContextWithOptions(canvasSize, false, 0)
// Grab that canvas
let canvas = UIGraphicsGetCurrentContext()!
let spacing: CGFloat = 10.0 // whatever the spacing between your circles is
// Draw the lines
for each in mesh {
canvas.move(to: CGPoint(x: CGFloat(each.0) * spacing, y: 0))
canvas.addLine(to: CGPoint(x: CGFloat(each.1) * spacing, y: self.contentView.bounds.height))
}
canvas.setStrokeColor(UIColor.black.cgColor)
canvas.setLineWidth(3)
canvas.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
You'd probably want to create a Mesh class and store the images in that model, to avoid redrawing.

Setting size of JTCalendar cell

I am working through the tutorial of JTCalendar (version 6.1.5). When I run on smaller phones such as the iPhone SE, one side of the circle in the Selection View gets clipped. This because the cells are about 45x45 points, but the Selection View's size is 50x50 points and is therefore too large to fully fit into the cell.
How can I make my selection view fit properly into date cells of varying sizes?
How can I get the proper value of cornerRadius for the selection view circle?
I was able to resolve this issue. The problem is that the tutorial set the size of the Selected View and left it at that. What I did was
Made Outlets for both the width and height constraints in CellView.swift
In ViewContoller.swift, I modified cell selection as follows:
if cellState.isSelected {
var parentMinDimension = min(view.frame.width, view.frame.height)
parentMinDimension = round(parentMinDimension - 0.5)
myCustomCell.widthConstraint.constant = parentMinDimension
myCustomCell.heightConstraint.constant = parentMinDimension
myCustomCell.selectedView.layer.cornerRadius = parentMinDimension / 2
myCustomCell.selectedView.isHidden = false
} else {
myCustomCell.selectedView.isHidden = true
}
This gets the parent view and determines the smaller dimension. This needs to be rounded down. I then uses this parent view dimension to set the width and height of the CellView as well as for determining the corner radius.

How can I tap on a UICollectionView cell of a collection view in UIAutomation?

How can I tap on a UICollectionView cell of a collection view in UIAutomation?
I tried this
var iconsCollView = window.collectionViews()[0];
var iconRect = iconsCollView.cells()[0].rect();
var iconX = iconRect.origin.x/100;
var iconY = iconRect.origin.y/100;
iconsCollView.tapWithOptions({tapOffset:{x: iconX, y: iconY}});
but it taps another cell in the collection view, a wrong cell other than the cell I specified its offset.
Can you please help me? is there another way?
From the UIAElement class reference:
Your script should treat the rect object as a generic JavaScript object whose properties for origin, x, y, size, width, and height correspond to those of the analogous CGRect Cocoa structure. The rect object has the form {origin:{x:xposition,y:yposition}, size:{width:widthvalue, height:heightvalue}}. The relevant coordinates are screen-relative and are adjusted to account for device orientation.
From the same source under tapWithOptions method:
You can use offsets to achieve finer precision in specifying the hitpoint within the rect for the specified element. The offset comprises a pair of x and y values, each ranging from 0.0 to 1.0. These values represent, respectively, relative horizontal and vertical positions within the rect, with {x:0.0, y:0.0} as the top left and {x:1.0, y:1.0} as the bottom right. Thus, {x:0.3, y:0.6} specifies a position just below and to the left of center, and {x:1.0, y:0.5} specifies a position centered vertically at the far right.
From the source you provided, you're trying to tap inside of collection view passing some weirdly scaled offset coordinates of one of its cells (instead of expected relative horizontal and vertical positions within the rect of collection view).
If you want to tap the cell simply locate it and call the tap method on it:
var iconsCollView = window.collectionViews()[0];
var iconCellToTap = iconsCollView.cells()[0].tap();

Resources