How does initializing UIView subclass properties affect superview's center? - ios

I'm having a problem with the graphOrigin property in my UIView subclass. When I defined graphOrigin as a computed variable, it convert's the superview's center point to this view's center point and displays the graph in the center of the screen. This does not happen when the variable isn't computed. See code and screenshot for the working case:
class GraphX_YCoordinateView: UIView {
var graphOrigin: CGPoint {
return convertPoint(center, fromView: superview)
}
#IBInspectable var scale: CGFloat = 50 {
didSet {
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
// Draw X-Y axes in the view
let axesDrawer = AxesDrawer(contentScaleFactor: contentScaleFactor)
axesDrawer.drawAxesInRect(bounds, origin: graphOrigin, pointsPerUnit: scale)
}
}
AxesDrawer is a class that draws axes in the current view, here is the method signature for drawAxesInRect:
drawAxesInRect(bounds: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
And here is the code and screenshot for the case that doesn't work:
class GraphX_YCoordinateView: UIView {
var graphOrigin: CGPoint! {
didSet {
setNeedsDisplay()
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
graphOrigin = convertPoint(center, fromView: superview)
}
#IBInspectable var scale: CGFloat = 50 {
didSet {
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
// Draw X-Y axes
let axesDrawer = AxesDrawer(contentScaleFactor: contentScaleFactor)
axesDrawer.drawAxesInRect(bounds, origin: graphOrigin, pointsPerUnit: scale)
}
}
So literally all I changed was initializing the graphOrigin property in place and computing it in the initializer. I didn't touch the StoryBoard at all while editing this code.
I tried initializing the variable inline:
var graphOrigin = convertPoint(center, fromView: superview)
But this wasn't allowed because the implicit self is not initialized when properties are computed.
Can anyone explain why the superview's center seems to change location depending on how the variable is initialized?

This function
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
graphOrigin = convertPoint(center, fromView: superview)
}
means that you are loading view from xib, and at this time the view dimension is 600:600 (look at your xib). So that your graphOrigin = 300:300. This is why you see the second picture.
To fix that problem, you should compute the graphOrigin after the view finish layout in viewDidLayout.

Related

What is iOS's equivalent of Java/Swing's FlowLayout?

I'm trying to build a UIView that lays out other views using their intrinsic width, left to right, and then wraps if necessary. UICollectionView seems overly complicated for what I need. I also tried using the FlexLayout wrapper for Yoga, and couldn't seem to get the right incantation. It seems like it should be much easier than this -- what's the simple solution?
I ended up writing my own UIView to solve the problem. I doubt if it's very robust, but it solves my immediate needs. Please, comment if you have an improvements to suggest.
import UIKit
class FlowLayout: UIView {
private var hgapValue: CGFloat = 0.0
private var vgapValue: CGFloat = 0.0
var hgap: CGFloat {
set(newValue) {
hgapValue = (newValue < 0) ? -newValue : newValue
}
get {
return hgapValue
}
}
var vgap: CGFloat {
set(newValue) {
vgapValue = (newValue < 0) ? -newValue : newValue
}
get {
return vgapValue
}
}
//initWithFrame to init view from code
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
//initWithCode to init view from xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
override func didAddSubview(_ subview: UIView) {
refreshLayout()
}
//common func to init our view
private func setupView() {
// backgroundColor = .red
}
public func refreshLayout() {
var x: CGFloat = 0.0
var y: CGFloat = 0.0
for subview in subviews {
subview.frame.origin.x = x
subview.frame.origin.y = y
x += subview.frame.width + hgapValue
if x + subview.frame.width >= (subview.superview?.frame.width)! {
x = 0.0
y += subview.frame.height + vgapValue
}
}
setNeedsDisplay()
}
}
In interface builder, using UIStackView to achieve a simple alternative.
You can also turn on auto layout to make it naturally.

Add UIView rounding logic inside UIView class

I have a custom UIView and I add it to my ViewController like this:
let myCustomView = Bundle.main.loadNibNamed("MyCustomView", owner: nil, options: nil) as! MyCustomView
myCustomView.layer.cornerRadius = 10
myCustomView.layer.masksToBounds = true
I round the corners of the view. But I am wondering, is there a way to move this logic of rounding the corners inside the MyCustomView class?
As you use IB, you may find it more convenient to make an extension of UIView
extension UIView {
#IBInspectable var borderColor: UIColor? {
set {
layer.borderColor = newValue?.cgColor
}
get {
if let color = layer.borderColor {
return UIColor(cgColor:color)
} else {
return nil
}
}
}
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
clipsToBounds = newValue > 0
}
get {
return layer.cornerRadius
}
}
}
Then you can set those values from Attributes inspector.
Yes - If you're loading a nib with a custom view, that nib is very likely referring to another class. If that's the case, you can move the logic inside the class itself.
That said, I really like Lawliet's suggestion of making a UIView extension with IBInspectable properties. The downside to that approach is that every single view now has these properties, which creates a certain overhead and potential for clashes.
You can do something like this in your UIView subclass:
class RoundedView: UIView {
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
}
}
Also if you want to pass a custom value instead of '10' in the cornerRadius property, you can try to implement a convenience init by looking here:
Override Init method of UIView in Swift

UIPanGestureRecognnizer in UIView in Swift

I've implemented a UIPanGestureRecognizer in a UIView graph I have made, with the intent of moving it around in the frame of another UIView, which has the same dimensions. However when running it moves all over the frame of the ViewController view (the whole screen) and it not restricted to the smaller frame of its parent UIView I have set up. Here's some screen shots of the graph moving around:
Here's the code for the parent (container) UIView which houses the GraphView class I have made:
class GraphViewDynamic:UIView {
var graphView:GraphView?
init(frame: CGRect, _ backgroudColor:UIColor, _ delegate:GraphViewDelegate) {
super.init(frame:frame)
self.graphView = GraphView.init(frame: frame, backgroudColor, delegate)
self.addSubview(self.graphView!)
}
func graphData(dataSetName setNames: [String], _ lineColors:[UIColor], _ timeInterval:GraphViewTimeInterval) {
if let _ = graphView {
graphView?.graphData(dataSetName: setNames, lineColors, timeInterval)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and here's the code in my GraphView UIView for the pan gesture handler:
func handlePan(_ sender:UIPanGestureRecognizer) {
sender.view?.center = sender.location(in: sender.view?.superview)
}
Any ideas on how to restrict the panning to the specified frame of the parent UIView of my GraphView (which has the same dimensions)? Thanks for any help.

touchesBegan and touchesMoved not getting triggered on UIView

What I'm trying to achieve:
Trying to grab the coordinate of the currently touched area on the screen and draw the received coordinate on the screen. Simply put, a basic drawing app that's all written programmatically (for my own practice).
Problem
touchesBegan and touchesMoved for PaintingSubclass are not getting called at all.
Current setup
I have a PaintingViewController then I also have a PaintingSubclass.
PaintingViewController is a UIViewController and PaintingSubclass is a UIView.
PaintingViewController creates an instance of PaintingSubclass and adds it to the subview of PaintingViewController.
PaintingSubclass is where the actual drawing happens.
What I've tried so far
Put a breakpoint inside the touchesMoved and touchesBegan (didn't work)
Tried to add a UITapGestureRecognizer (didn't work)
Enabled self.isUserInteractionEnabled = true (didn't work)
Make PaintingSubclass inherit UIControl and call sendActions(for: .valueChanged) at the end of touchesMoved and touchesBegan (didn't work)
Current Code
(please ignore any the unnecessary variables)
import UIKit
class PaintingViewController: UIViewController{
var _paintView: PaintingSubclass? = nil
override func loadView() {
view = UILabel()
}
private var labelView: UILabel {
return view as! UILabel
}
override func viewDidLoad() {
_paintView = PaintingSubclass()
_paintView?.frame = CGRect(x: view.bounds.minX, y: view.bounds.minY, width: 400, height: 750)
_paintView?.backgroundColor = UIColor.white
_paintView?.setNeedsDisplay()
view.addSubview(_paintView!)
}
class PaintingSubclass: UIView{
private var context: CGContext? = nil
var styleSelection: String = "buttcap"
var linewidth: Float = 5
var lineCapStyle: CGLineCap? = nil
var lineJoinStyle: CGLineJoin? = nil
var lineWidthValue: CGFloat? = nil
var colorValue: UIColor? = nil
override init(frame: CGRect) {
super.init(frame: frame)
lineWidthValue = 0.5
colorValue = UIColor(cgColor: UIColor.black.cgColor)
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
context = UIGraphicsGetCurrentContext()!
context?.move(to: CGPoint(x: 0.0, y: 110.0))
context?.setStrokeColor((colorValue?.cgColor)!)
context?.drawPath(using: CGPathDrawingMode.stroke)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let touch: UITouch = touches.first!
let touchPoint: CGPoint = touch.location(in: self)
let _xValue = touchPoint.x
let _yValue = touchPoint.y
NSLog("coordinate: \(_xValue), \(_yValue)")
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
let touch: UITouch = touches.first!
let touchPoint: CGPoint = touch.location(in: self)
let _xValue = touchPoint.x
let _yValue = touchPoint.y
NSLog("coordinate: \(_xValue), \(_yValue)")
}
}
}
What am I doing wrong here? Been stuck in this state for hours not making any progress. Any help/feedbacks will be much appreciated.
I don't know why you are setting your default view to UILabel(), but your default isUserInteractionEnabled of your view is false. Set it to true
override func loadView() {
view = UILabel()
view.isUserInteractionEnabled = true
}
According to your code, your PaintingViewController's view is a UILabel, the property isUserInteractionEnabled default is false.Try to set this property to true may help you.
The problem lies on:
// override func loadView() {
// view = UILabel()
// }
//
// private var labelView: UILabel {
// return view as! UILabel
// }
You turn the view controller's self view object to a UILabel object.
It would be better to have your painting view as the main view in the view controller and the UILabel as a subview of the painting view. The UILabel needs to have userInteractionEnabled in your current layout, but instead of changing that switch the layout around.
Apart from anything else, presumably you don't want the painting view to draw over the label, but rather the label to be drawn on top, so the label should be the subview.

Why can't I draw on an image when I subclass UIView?

I am trying to draw on an image from the camera roll using the code from this tutorial. After selecting a photo from the camera roll, imageView is set to the photo. When the user presses the draw button, tempImageView is added over the photo, so the user can draw onto it. However, when I attempt to draw on the image, nothing happens. Why?
Code:
class EditViewController: UIViewController{
var tempImageView: DrawableView!
var imageView: UIImageView = UIImageView(image: UIImage(named: "ClearImage"))
func draw(sender: UIButton!) {
tempImageView = DrawableView(frame: imageView.bounds)
tempImageView.backgroundColor = UIColor.clearColor()
self.view.addSubview(tempImageView)
}
}
class DrawableView: UIView {
let path=UIBezierPath()
var previousPoint:CGPoint
var lineWidth:CGFloat=10.0
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override init(frame: CGRect) {
previousPoint=CGPoint.zero
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
previousPoint=CGPoint.zero
super.init(coder: aDecoder)
let panGestureRecognizer=UIPanGestureRecognizer(target: self, action: "pan:")
panGestureRecognizer.maximumNumberOfTouches=1
self.addGestureRecognizer(panGestureRecognizer)
}
override func drawRect(rect: CGRect) {
// Drawing code
UIColor.greenColor().setStroke()
path.stroke()
path.lineWidth=lineWidth
}
func pan(panGestureRecognizer:UIPanGestureRecognizer)->Void
{
let currentPoint=panGestureRecognizer.locationInView(self)
let midPoint=self.midPoint(previousPoint, p1: currentPoint)
if panGestureRecognizer.state == .Began
{
path.moveToPoint(currentPoint)
}
else if panGestureRecognizer.state == .Changed
{
path.addQuadCurveToPoint(midPoint,controlPoint: previousPoint)
}
previousPoint=currentPoint
self.setNeedsDisplay()
}
func midPoint(p0:CGPoint,p1:CGPoint)->CGPoint
{
let x=(p0.x+p1.x)/2
let y=(p0.y+p1.y)/2
return CGPoint(x: x, y: y)
}
}
make sure that tempImageView.userIteractionEnabled = YES. by default this property for UIImageView is NO.
after put breakpoint to you gestureRecogniser method, see if it's being called.

Resources