UICollectionView - draw a line between cells - ios

How do I draw a line between cells in a UICollectionView that crosses over the spaces?
The intended output is something like this.:
The best that I've done is to add lines inside each cell. How do I connect the lines through the spaces?

I made an extension that you can use like this:
collectionView.drawLineFrom(indexPathA, to: indexPathB, color: UIColor.greenColor())
Here is the extension:
extension UICollectionView {
func drawLineFrom(
from: NSIndexPath,
to: NSIndexPath,
lineWidth: CGFloat = 2,
strokeColor: UIColor = UIColor.blueColor()
) {
guard
let fromPoint = cellForItemAtIndexPath(from)?.center,
let toPoint = cellForItemAtIndexPath(to)?.center
else {
return
}
let path = UIBezierPath()
path.moveToPoint(convertPoint(fromPoint, toView: self))
path.addLineToPoint(convertPoint(toPoint, toView: self))
let layer = CAShapeLayer()
layer.path = path.CGPath
layer.lineWidth = lineWidth
layer.strokeColor = strokeColor.CGColor
self.layer.addSublayer(layer)
}
}
The result looks like this:

I could achieve this with below code, might have different approach too.
Ok, so I am creating a custom UIView with custom frame and just providing the frame blindly, you can calculate based on your adjacent cells.
let cell1 = collectionView.cellForItemAtIndexPath(NSIndexPath(forRow: 0, inSection: 0))
let myView = UIView.init(frame: CGRectMake((cell1?.frame.origin.x)!, ((cell1?.frame.origin.y)! + 50.0), (cell1?.frame.size.width)!*4, 10))
myView.backgroundColor = UIColor.blueColor()
collectionView.addSubview(myView)
collectionView.bringSubviewToFront(myView)
This would draw a line of height 10.0. Let me know if this helps you.

Updated #Callam's answer for Swift 5 and added a few minor modifications
Uses dequeueReusableCell instead cellForItemAtIndexPath
Uses a custom collection view cell that you'll need to create elsewhere. You'll also need to give it a reuse identifier name.
extension UICollectionView {
func drawLineFrom(
from: IndexPath,
to: IndexPath,
lineWidth: CGFloat = 2,
strokeColor: UIColor = UIColor.blue
) {
let fromPoint = self.dequeueReusableCell(withReuseIdentifier: "fooReuseIdentifier", for: from) as? FooCustomCollectionViewCell
let toPoint = self.dequeueReusableCell(withReuseIdentifier: "fooReuseIdentifier", for: to) as? FooCustomCollectionViewCell
let path = UIBezierPath()
guard let fromCenter = fromPoint?.center else { return }
guard let toCenter = toPoint?.center else { return }
path.move(to: convert(fromCenter, to: self))
path.addLine(to: convert(toCenter, to: self))
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.lineWidth = lineWidth
layer.strokeColor = strokeColor.cgColor
self.layer.addSublayer(layer)
}
}

Related

CGContext Drawing two adjacent rhomboids produce a very thin gap, how to reduce it?

The two adjacent rectangle is ok as image below.
override func draw(_ rect: CGRect) {
// Drawing code
let leftTop = CGPoint(x:50,y:50)
let rightTop = CGPoint(x:150,y:100)
let leftMiddle = CGPoint(x:50,y:300)
let rightMiddle = CGPoint(x:150,y:300)
let leftDown = CGPoint(x:50,y:600)
let rightDown = CGPoint(x:150,y:650)
let context = UIGraphicsGetCurrentContext()
context?.addLines(between: [leftTop,rightTop,rightMiddle,leftMiddle])
UIColor.black.setFill()
context?.fillPath()
context?.addLines(between: [leftMiddle,rightMiddle,rightDown,leftDown])
context?.fillPath()
let leftTop1 = CGPoint(x:200,y:50)
let rightTop1 = CGPoint(x:300,y:100)
let leftMiddle1 = CGPoint(x:200,y:300)
let rightMiddle1 = CGPoint(x:300,y:350)
let leftDown1 = CGPoint(x:200,y:600)
let rightDown1 = CGPoint(x:300,y:650)
context?.addLines(between: [leftTop1,rightTop1,rightMiddle1,leftMiddle1])
UIColor.black.setFill()
context?.fillPath()
context?.addLines(between: [leftMiddle1,rightMiddle1,rightDown1,leftDown1])
context?.fillPath()
}
You may need to zoom in to see the gap. If I draw a thin line to cover the gap, then any width may overlap if the color has an alpha channel.
Change shapeColor to let shapeColor = UIColor(white: 0.0, alpha: 0.5) and add context?.setShouldAntialias(false)
I have the same result using the CGContext even if I use setShouldAntialias(true) or if I try to call strokePath() on the context. But it works fine with sublayers, CGPath and strokeColor
class RhombView: UIView {
let shapeColor: UIColor = .black
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupView()
}
func setupView() {
let leftTop1 = CGPoint(x:200.0,y:50.0)
let rightTop1 = CGPoint(x:300.0,y:100.0)
let leftMiddle1 = CGPoint(x:200.0,y:300.0)
let rightMiddle1 = CGPoint(x:300.0,y:350.0)
let leftDown1 = CGPoint(x:200.0,y:600.0)
let rightDown1 = CGPoint(x:300.0,y:650.0)
var path = UIBezierPath()
path.move(to: leftTop1)
path.addLine(to: rightTop1)
path.addLine(to: rightMiddle1)
path.addLine(to: leftMiddle1)
path.close()
let subLayer1 = CAShapeLayer()
subLayer1.path = path.cgPath
subLayer1.frame = self.layer.frame
subLayer1.fillColor = shapeColor.cgColor
subLayer1.strokeColor = shapeColor.cgColor
self.layer.addSublayer(subLayer1)
path = UIBezierPath()
path.move(to: leftMiddle1)
path.addLine(to: rightMiddle1)
path.addLine(to: rightDown1)
path.addLine(to: leftDown1)
path.close()
let subLayer2 = CAShapeLayer()
subLayer2.path = path.cgPath
subLayer2.frame = self.layer.frame
subLayer2.fillColor = shapeColor.cgColor
subLayer2.strokeColor = shapeColor.cgColor
self.layer.addSublayer(subLayer2)
self.layer.backgroundColor = UIColor.white.cgColor
}
}
If you remove both subLayer.strokeColor you will see the gap.
This is related to screen resolution. On devices with high resolution one point actually has 2 or 3 pixels. That's why to draw an inclined line iOS draws some edge pixels with some opacity, when 2 lines are next to each other these edge pixels overlap. In your case I was able to fix the issue by making the following change
let leftTop1 = CGPoint(x:200,y:50)
let rightTop1 = CGPoint(x:300,y:100)
let leftMiddle1 = CGPoint(x:200,y:300)
let rightMiddle1 = CGPoint(x:300,y:350)
let leftMiddle2 = CGPoint(x:200,y:300.333)
let rightMiddle2 = CGPoint(x:300,y:350.333)
let leftDown1 = CGPoint(x:200,y:600)
let rightDown1 = CGPoint(x:300,y:650)
context?.addLines(between: [leftTop1,rightTop1,rightMiddle1,leftMiddle1])
UIColor.black.setFill()
context?.fillPath()
context?.addLines(between: [leftMiddle2,rightMiddle2,rightDown1,leftDown1])
context?.fillPath()
Basically moving down the second rhomboid by 1 pixel (1/3 = 0.333...), I have tested on a screen with 1:3 pixel density, for the solution to work on all devices you'll need to check the scaleFactor of the screen.

Efficient off-screen UIView rendering and mirroring

I have a "off-screen" UIView hierarchy which I want render in different locations of my screen. In addition it should be possible to show only parts of this view hierarchy and should reflect all changes made to this hierarchy.
The difficulties:
The UIView method drawHierarchy(in:afterScreenUpdates:) always calls draw(_ rect:) and is therefore very inefficient for large hierarchies if you want to incorporate all changes to the view hierarchy. You would have to redraw it every screen update or observe all changing properties of all views. Draw view hierarchy documentation
The UIView method snapshotView(afterScreenUpdates:) also does not help much since I have not found a way to get a correct view hierarchy drawing if this hierarchy is "off-screen". Snapshot view documentation
"Off-Screen": The root view of this view hierarchy is not part of the UI of the app. It has no superview.
Below you can see a visual representation of my idea:
Here's how I would go about doing it. First, I would duplicate the view you are trying to duplicate. I wrote a little extension for this:
extension UIView {
func duplicate<T: UIView>() -> T {
return NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: self)) as! T
}
func copyProperties(fromView: UIView, recursive: Bool = true) {
contentMode = fromView.contentMode
tag = fromView.tag
backgroundColor = fromView.backgroundColor
tintColor = fromView.tintColor
layer.cornerRadius = fromView.layer.cornerRadius
layer.maskedCorners = fromView.layer.maskedCorners
layer.borderColor = fromView.layer.borderColor
layer.borderWidth = fromView.layer.borderWidth
layer.shadowOpacity = fromView.layer.shadowOpacity
layer.shadowRadius = fromView.layer.shadowRadius
layer.shadowPath = fromView.layer.shadowPath
layer.shadowColor = fromView.layer.shadowColor
layer.shadowOffset = fromView.layer.shadowOffset
clipsToBounds = fromView.clipsToBounds
layer.masksToBounds = fromView.layer.masksToBounds
mask = fromView.mask
layer.mask = fromView.layer.mask
alpha = fromView.alpha
isHidden = fromView.isHidden
if let gradientLayer = layer as? CAGradientLayer, let fromGradientLayer = fromView.layer as? CAGradientLayer {
gradientLayer.colors = fromGradientLayer.colors
gradientLayer.startPoint = fromGradientLayer.startPoint
gradientLayer.endPoint = fromGradientLayer.endPoint
gradientLayer.locations = fromGradientLayer.locations
gradientLayer.type = fromGradientLayer.type
}
if let imgView = self as? UIImageView, let fromImgView = fromView as? UIImageView {
imgView.tintColor = .clear
imgView.image = fromImgView.image?.withRenderingMode(fromImgView.image?.renderingMode ?? .automatic)
imgView.tintColor = fromImgView.tintColor
}
if let btn = self as? UIButton, let fromBtn = fromView as? UIButton {
btn.setImage(fromBtn.image(for: fromBtn.state), for: fromBtn.state)
}
if let textField = self as? UITextField, let fromTextField = fromView as? UITextField {
if let leftView = fromTextField.leftView {
textField.leftView = leftView.duplicate()
textField.leftView?.copyProperties(fromView: leftView)
}
if let rightView = fromTextField.rightView {
textField.rightView = rightView.duplicate()
textField.rightView?.copyProperties(fromView: rightView)
}
textField.attributedText = fromTextField.attributedText
textField.attributedPlaceholder = fromTextField.attributedPlaceholder
}
if let lbl = self as? UILabel, let fromLbl = fromView as? UILabel {
lbl.attributedText = fromLbl.attributedText
lbl.textAlignment = fromLbl.textAlignment
lbl.font = fromLbl.font
lbl.bounds = fromLbl.bounds
}
if recursive {
for (i, view) in subviews.enumerated() {
if i >= fromView.subviews.count {
break
}
view.copyProperties(fromView: fromView.subviews[i])
}
}
}
}
to use this extension, simply do
let duplicateView = originalView.duplicate()
duplicateView.copyProperties(fromView: originalView)
parentView.addSubview(duplicateView)
Then I would mask the duplicate view to only get the particular section that you want
let mask = UIView(frame: CGRect(x: 0, y: 0, width: yourNewWidth, height: yourNewHeight))
mask.backgroundColor = .black
duplicateView.mask = mask
finally, I would scale it to whatever size you want using CGAffineTransform
duplicateView.transform = CGAffineTransform(scaleX: xScale, y: yScale)
the copyProperties function should work well but you can change it if necessary to copy even more things from one view to another.
Good luck, let me know how it goes :)
I'd duplicate the content I wish to display and crop it as I want.
Let's say I have a ContentViewController which carries the view hierarchy I wish to replicate. I would encapsule all the changes that can be made to the hierarchy inside a ContentViewModel. Something like:
struct ContentViewModel {
let actionTitle: String?
let contentMessage: String?
// ...
}
class ContentViewController: UIViewController {
func display(_ viewModel: ContentViewModel) { /* ... */ }
}
With a ClippingView (or a simple UIScrollView) :
class ClippingView: UIView {
var contentOffset: CGPoint = .zero // a way to specify the part of the view you wish to display
var contentFrame: CGRect = .zero // the actual size of the clipped view
var clippedView: UIView?
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
clippedView?.frame = contentFrame
clippedView?.frame.origin = contentOffset
}
}
And a view controller container, I would crop each instance of my content and update all of them each time something happens :
class ContainerViewController: UIViewController {
let contentViewControllers: [ContentViewController] = // 3 in your case
override func viewDidLoad() {
super.viewDidLoad()
contentViewControllers.forEach { viewController in
addChil(viewController)
let clippingView = ClippingView()
clippingView.clippedView = viewController.view
clippingView.contentOffset = // ...
viewController.didMove(to: self)
}
}
func somethingChange() {
let newViewModel = ContentViewModel(...)
contentViewControllers.forEach { $0.display(newViewModel) }
}
}
Could this scenario work in your case ?

How to place UIView on top of other UIView?

Right now I have a collectionView for which each cell contains a horizontal stackView. The stackView gets populated with a series of UIViews (rectangles), one for each day of a month - each cell corresponds to a month. I fill the stack views like so:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == self.collectionView {
...
return cell
} else if collectionView == self.timeline {
let index = indexPath.row
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM"
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: timelineMonthCellReuseIdentifier, for: indexPath) as! SNTimelineMonthViewCell
let firstPost = posts.first?.timeStamp
let month = Calendar.current.date(byAdding: .month, value: index, to: firstPost!)
print(dateFormatter.string(from: month!),dateFormatter.string(from: firstPost!),"month diff")
for post in posts {
print(post.timeStamp, "month diff")
}
cell.monthLabel.text = dateFormatter.string(from: month!)
cell.monthLabel.textAlignment = .center
if let start = month?.startOfMonth(), let end = month?.endOfMonth(), let stackView = cell.dayTicks {
var date = start
while date <= end {
let line = UIView()
if posts.contains(where: { Calendar.current.isDate(date, inSameDayAs: $0.timeStamp) }) {
line.backgroundColor = UIColor(red:0.15, green:0.67, blue:0.93, alpha:1.0)
let tapGuesture = UITapGestureRecognizer(target: self, action: #selector (self.tapBar (_:)))
line.isUserInteractionEnabled = true
line.addGestureRecognizer(tapGuesture)
self.dayTicks[date] = line
} else {
line.backgroundColor = UIColor.clear
}
stackView.addArrangedSubview(line)
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
}
}
return cell
} else {
preconditionFailure("Unknown collection view!")
}
}
Then, when the user stops scrolling a different collection view, I want to add a subview called arrowView ontop of the dayTick (see how self.dayTicks gets populated with the subviews of the stackView above).
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width
let post = posts[Int(currentIndex)]
for (_,tick) in self.dayTicks {
tick.subviews.forEach({ $0.removeFromSuperview() })
}
let day = Calendar.current.startOfDay(for: post.timeStamp)
let tick = self.dayTicks[day]
let arrow = UIImage(named:"Tracer Pin")
let arrowView = UIImageView(image: arrow)
// arrowView.clipsToBounds = false
print((tick?.frame.origin)!,"tick origin")
// arrowView.frame.origin = (tick?.frame.origin)!
// arrowView.frame.size.width = 100
// arrowView.frame.size.height = 100
tick?.addSubview(arrowView)
}
This kind of works and it looks like this:
The red rectangle is added but it appears to the right of the dayTick, and it appears as a long thin rectangle. In actuality, the Tracer Pin image referenced looks like this:
Thats at least where the red color comes from but as you can see its stretching it weird and clipping everything thats not in a rectangular UIView space.
Now note that I commented out the 4 lines that set the size and origin of the arrowView as well as setting clipToBounds to false. When I uncomment these lines - the arrowView simply doesn't show up at all so I must be doing this wrong. What I want is to show something like this:
How can I put it directly on top like that?
Another perspective might be to do this with CALayer. Here are some clues (cut from another project) to help you discover a solution:
#IBInspectable open var slideIndicatorThickness: CGFloat = 5.0 {
didSet {
if slideIndicator != nil { slideIndicator.removeFromSuperlayer() }
let slideLayer = CALayer()
let theOrigin = CGPoint(x: bounds.origin.x, y: bounds.origin.y)
let theSize = CGSize(width: CGFloat(3.0), height: CGFloat(10.0)
slideLayer.frame = CGRect(origin: theOrigin, size: theSize)
slideLayer.backgroundColor = UIColor.orange.cgColor
slideIndicator = slideLayer
layer.addSublayer(slideIndicator)
}
}
fileprivate var slideIndicator: CALayer!
fileprivate func updateIndicator() {
// ..
// Somehow figure out new frame, based on stack view's frame.
slideIndicator.frame.origin.x = newOrigin
}
You may have to implement this on a subclass of UIStackView, or your own custom view that is a wrapper around UIStackView.
It looks like you have a fixed height on the arrowView. Could it be that the red triangle portion is under another view?
Debug View Hierarchy
Click the debug view hierarchy which is the second from right icon - it looks like 3 rectangles. Check to see if the whole image is there.
I ended up using CALayer - thanks to #ouni's suggestion For some reason the CALayer seemed to draw directly on top of the view whereas a subview didn't. One key was unchecking the "Clip to bounds box on the collectionView cell itself (as opposed to the subview) - so that I could draw the flared base of the arrow outside of the collection view cell:
My code looks like this:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == self.collectionView {
print("is collection view")
print(scrollView,"collection view")
let currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width
let post = posts[Int(currentIndex)]
for (_,tick) in self.dayTicks {
tick.layer.sublayers = nil
}
let day = Calendar.current.startOfDay(for: post.timeStamp)
let tick = self.dayTicks[day]
let arrowLayer = CAShapeLayer()
let path = UIBezierPath()
let start_x = (tick?.bounds.origin.x)!
let start_y = (tick?.bounds.minY)!
let top_width = (tick?.bounds.width)!
let tick_height = (tick?.bounds.height)!
let tip_height = CGFloat(10)
let tip_flare = CGFloat(10)
path.move(to: CGPoint(x: start_x, y: start_y))
path.addLine(to: CGPoint(x: start_x + top_width,y: start_y))
path.addLine(to: CGPoint(x: start_x + top_width,y: start_y + tick_height))
path.addLine(to: CGPoint(x: start_x + top_width + tip_flare,y: start_y+tick_height+tip_height))
path.addLine(to: CGPoint(x: start_x - tip_flare,y: start_y + tick_height + tip_height))
path.addLine(to: CGPoint(x: start_x,y: start_y+tick_height))
path.close()
arrowLayer.path = path.cgPath
arrowLayer.fillColor = UIColor(red:0.99, green:0.13, blue:0.25, alpha:1.0).cgColor
tick?.layer.addSublayer(arrowLayer)
} else {
print(scrollView, "timeline collection view")
}
}
This draws the arrow on top of the subview beautifully.

How to draw UIBezierPath identical to MKPolyline in a UIView

Currently I am tracking my location on an MKMapView. My objective is to draw a bezier path identical to an MKPolyline created from tracked locations.
What I have attempted is: Store all location coordinates in a CLLocation array. Iterate over that array and store the lat/lng coordinates in a CLLocationCoordinate2D array. Then ensure the polyline is in the view of the screen to then convert all the location coordinates in CGPoints.
Current attempt:
#IBOutlet weak var bezierPathView: UIView!
var locations = [CLLocation]() // values from didUpdateLocation(_:)
func createBezierPath() {
bezierPathView.isHidden = false
var coordinates = [CLLocationCoordinate2D]()
for location in locations {
coordinates.append(location.coordinate)
}
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
fitPolylineInView(polyline: polyline)
let mapPoints = polyline.points()
var points = [CGPoint]()
for point in 0...polyline.pointCount
{
let coordinate = MKCoordinateForMapPoint(mapPoints[point])
points.append(mapView.convert(coordinate, toPointTo: polylineView))
}
print(points)
let path = UIBezierPath(points: points)
path.lineWidth = 2.0
path.lineJoinStyle = .round
let layer = CAShapeLayer(path: path, lineColor: UIColor.red, fillColor: UIColor.black)
bezierPathView.layer.addSublayer(layer)
}
extension UIBezierPath {
convenience init(points:[CGPoint])
{
self.init()
//connect every points by line.
//the first point is start point
for (index,aPoint) in points.enumerated()
{
if index == 0 {
self.move(to: aPoint)
}
else {
self.addLine(to: aPoint)
}
}
}
}
extension CAShapeLayer
{
convenience init(path:UIBezierPath, lineColor:UIColor, fillColor:UIColor)
{
self.init()
self.path = path.cgPath
self.strokeColor = lineColor.cgColor
self.fillColor = fillColor.cgColor
self.lineWidth = path.lineWidth
self.opacity = 1
self.frame = path.bounds
}
}
I am able to output the points to the console that stored from the convert(_:) method( not sure if they are correct ). Yet the there is not output on the bezierPathView-resulting in an empty-white background-view controller.
Your extensions work fine. The problem may be in the code that adds the layer to the view (which you do not show).
I'd suggest that you simplify your project, for example use predefined array of points that definitely fit to your view. For example, for a view that is 500 pixels wide and 300 pixels high, you could use something like:
let points = [
CGPoint(x: 10, y: 10),
CGPoint(x: 490, y: 10),
CGPoint(x: 490, y: 290),
CGPoint(x: 10, y: 290),
CGPoint(x: 10, y: 10)
]
Use colors that are clearly visible, like black and yellow for your stroke and fill.
Make sure that your path is correctly added to the view, for example:
let path = UIBezierPath(points: points)
let shapeLayer = CAShapeLayer(path: path, lineColor: UIColor.blue, fillColor: UIColor.lightGray)
view.layer.addSublayer(shapeLayer)
Inspect the controller that contains the view in Xcode's Interface Builder. In the debug view hierarchy function:
this might help you, in case you haven't solved it yet.
I wanted the shape of an MKPolyline as an image without any background.
I used the code above as an inspiration and had the same troubles as you had, the route was not shown.
In fact it was kind a scaling problem I think. At least it looked like that in the playground.
Anyway, with this methods I get an image of the polylines shape.
private func createPolylineShapeAsImage() -> UIImage? {
let vw = UIView(frame: mapView.bounds)
var image : UIImage?
if let polyline = viewModel.tourPolyline {
let path = createBezierPath(mapView, polyline, to: mapView)
let layer = getShapeLayer(path: path, lineColor: UIColor.white, fillColor: .clear)
vw.layer.addSublayer(layer)
image = vw.asImage()
}
return image
}
func createBezierPath(_ mapView : MKMapView, _ polyline : MKPolyline, to view : UIView) -> UIBezierPath {
let mapPoints = polyline.points()
var points = [CGPoint]()
let max = polyline.pointCount - 1
for point in 0...max {
let coordinate = mapPoints[point].coordinate
points.append(mapView.convert(coordinate, toPointTo: view))
}
let path = UIBezierPath(points: points)
path.lineWidth = 5.0
return path
}
private func getShapeLayer(path:UIBezierPath, lineColor:UIColor, fillColor:UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = lineColor.cgColor
layer.fillColor = fillColor.cgColor
layer.lineWidth = path.lineWidth
layer.opacity = 1
layer.frame = path.bounds
return layer
}
And to get the image of the view use this extension
import UIKit
extension UIView {
// Using a function since `var image` might conflict with an existing variable
// (like on `UIImageView`)
func asImage() -> UIImage {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(self.frame.size)
self.layer.render(in:UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
}

How to pass arguments from one class into a UIView class? Swift

I have a UIView class in my app which plots a line graph. In there, I assign my graphPoints variables like so :
var graphPoints:[Int] = [1,2,3,5,7,9]
var graphPoints2:[Int] = [1,2,3,5,7,9]
What I want to do is pass an array of Int from another class and assign those variables, but I am not sure how to do it. Initially i put all my code into one func with array [Int] as parameters and called it from another class but it stopped plotting the graph altogether. How do i do this?
Here is my UIVIew GraphPlotter class code :
import UIKit
#IBDesignable class GraphPlotter: UIView {
var graphPoints:[Int] = [1,2,3,5,7,9]
var graphPoints2:[Int] = [1,2,3,5,7,9]
//1 - the properties for the gradient
var startColor: UIColor = UIColor.redColor()
var endColor: UIColor = UIColor.greenColor()
override func drawRect(rect: CGRect) {
let width = rect.width
let height = rect.height
//set up background clipping area
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: UIRectCorner.AllCorners,
cornerRadii: CGSize(width: 8.0, height: 8.0))
path.addClip()
//2 - get the current context
let context = UIGraphicsGetCurrentContext()
let colors = [startColor.CGColor, endColor.CGColor]
//3 - set up the color space
let colorSpace = CGColorSpaceCreateDeviceRGB()
//4 - set up the color stops
let colorLocations:[CGFloat] = [0.0, 1.0]
//5 - create the gradient
let gradient = CGGradientCreateWithColors(colorSpace,
colors,
colorLocations)
//6 - draw the gradient
var startPoint = CGPoint.zero
var endPoint = CGPoint(x:0, y:self.bounds.height)
CGContextDrawLinearGradient(context,
gradient,
startPoint,
endPoint,
[])
//calculate the x point
let margin:CGFloat = 40.0
let columnXPoint = { (column:Int) -> CGFloat in
//Calculate gap between points
let spacer = (width - margin*2 - 4) /
CGFloat((self.graphPoints.count - 1))
var x:CGFloat = CGFloat(column) * spacer
x += margin + 2
return x
}
// calculate the y point
let topBorder:CGFloat = 60
let bottomBorder:CGFloat = 50
let graphHeight = height - topBorder - bottomBorder
let maxValue = graphPoints2.maxElement()!
let columnYPoint = { (graphPoint2:Int) -> CGFloat in
var y:CGFloat = CGFloat(graphPoint2) /
CGFloat(maxValue) * graphHeight
y = graphHeight + topBorder - y // Flip the graph
return y
}
// draw the line graph
UIColor.flatTealColor().setFill()
UIColor.flatTealColor().setStroke()
//set up the points line
let graphPath = UIBezierPath()
//go to start of line
graphPath.moveToPoint(CGPoint(x:columnXPoint(0),
y:columnYPoint(graphPoints2[0])))
//add points for each item in the graphPoints array
//at the correct (x, y) for the point
for i in 1..<graphPoints.count {
let nextPoint = CGPoint(x:columnXPoint(i),
y:columnYPoint(graphPoints2[i]))
graphPath.addLineToPoint(nextPoint)
}
//Create the clipping path for the graph gradient
//1 - save the state of the context (commented out for now)
CGContextSaveGState(context)
//2 - make a copy of the path
let clippingPath = graphPath.copy() as! UIBezierPath
//3 - add lines to the copied path to complete the clip area
clippingPath.addLineToPoint(CGPoint(
x: columnXPoint(graphPoints.count - 1),
y:height))
clippingPath.addLineToPoint(CGPoint(
x:columnXPoint(0),
y:height))
clippingPath.closePath()
//4 - add the clipping path to the context
clippingPath.addClip()
let highestYPoint = columnYPoint(maxValue)
startPoint = CGPoint(x:margin, y: highestYPoint)
endPoint = CGPoint(x:margin, y:self.bounds.height)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, [])
CGContextRestoreGState(context)
//draw the line on top of the clipped gradient
graphPath.lineWidth = 2.0
graphPath.stroke()
//Draw the circles on top of graph stroke
for i in 0..<graphPoints.count {
var point = CGPoint(x:columnXPoint(i), y:columnYPoint(graphPoints2[i]))
point.x -= 5.0/2
point.y -= 5.0/2
let circle = UIBezierPath(ovalInRect:
CGRect(origin: point,
size: CGSize(width: 5.0, height: 5.0)))
circle.fill()
let label = UILabel(frame: CGRectMake(0, 0, 200, 21))
label.center = CGPointMake(160, 284)
label.textAlignment = NSTextAlignment.Center
// label.text = "I'am a test label"
self.addSubview(label)
}
//Draw horizontal graph lines on the top of everything
let linePath = UIBezierPath()
//top line
linePath.moveToPoint(CGPoint(x:margin, y: topBorder))
linePath.addLineToPoint(CGPoint(x: width - margin,
y:topBorder))
//center line
linePath.moveToPoint(CGPoint(x:margin,
y: graphHeight/2 + topBorder))
linePath.addLineToPoint(CGPoint(x:width - margin,
y:graphHeight/2 + topBorder))
//bottom line
linePath.moveToPoint(CGPoint(x:margin,
y:height - bottomBorder))
linePath.addLineToPoint(CGPoint(x:width - margin,
y:height - bottomBorder))
let color = UIColor.flatTealColor()
color.setStroke()
linePath.lineWidth = 1.0
linePath.stroke()
}
}
DBController, func dosmth where I pass the array :
func dosmth(metadata: DBMetadata!) {
let documentsDirectoryPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let localFilePath = (documentsDirectoryPath as NSString).stringByAppendingPathComponent(metadata.filename)
var newarray = [Int]()
do{
let data = try String(contentsOfFile: localFilePath as String,
encoding: NSASCIIStringEncoding)
print(data)
newarray = data.characters.split(){$0 == ","}.map{
Int(String.init($0).stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()))!}
print(newarray)
}
catch let error { print(error) }
//Probably wrong
GraphPlotter().graphPoints = newarray
GraphPlotter().graphPoints2 = newarray
}
So your drawRect method is based on the two variables graphPoints and graphPoints2. Create a method whose job is to update the arrays of these two variables, and then invoke setNeedsDisplay - which will go on to redraw the view.
func plotGraphPoints(gpArray1 : [Int], andMorePoints gpArray2: [Int] ) {
print("Old Values", self.graphPoints)
self.graphPoints = gpArray1
self.graphPoints2 = gpArray2
print("New values", self.graphPoints)
self.setNeedsDisplay()
}
First, I'd set these up so that any update will redraw the view:
var graphPoints:[Int]? { didSet { setNeedsDisplay() } }
var graphPoints2:[Int]? { didSet { setNeedsDisplay() } }
Note, I made those optionals, because you generally want it to handle the absence of data with nil values rather than dummy values. This does assume, though, that you'll tweak your implementation to detect and handle these nil values, e.g., before you start drawing the lines, do a
guard graphPoints != nil && graphPoints2 != nil else { return }
But, I notice that this whole class is IBDesignable, in which case, you probably want a prepareForInterfaceBuilder that provides sample data:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
graphPoints = [1,2,3,5,7,9]
graphPoints2 = [1,2,3,5,7,9]
}
Second, your other class needs to have a reference to this custom view.
If this "other" class is the view controller and you added the custom view via IB, you would just add a #IBOutlet for the custom view to this view controller. If you added this custom view programmatically, you'd just keep a reference to it in some property after adding it to the view hierarchy. But, however you added a reference to that view, say graphView, you'd just set these properties:
graphView.graphPoints = ...
graphView.graphPoints2 = ...
If this "other" class is something other than a view controller (and in discussion, it sounds like the class in question is a controller for processing of asynchronous DropBox API), you also need to give that class some mechanism to reference the view controller (and thus the custom view). You can accomplish this by either implementing a "completion handler pattern" or a "delegate-protocol" pattern.

Resources