trouble creating a scrollable UIView (swift + Interface Builder) - ios

Please do not immediately discredit this a duplicate, I have been on stackoverflow for like 2 days now trying any and every way to do this, read a lot of blog articles (which might have been slightly dated) and still no luck.
Basically, I have a custom UIView which is meant to draw a lot of circles (it's just the starting point) and I cannot get the view to scroll down to the ones apart from the visible circles on initial load. I made the for loop for about 200 of them to be sure there is something to scroll to.
here is my view code:
import UIKit
class Draw2D: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder);
}
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2.0);
let colorSpace = CGColorSpaceCreateDeviceRGB();
let components: [CGFloat] = [0.0, 0.0, 1.0, 1.0];
let color = CGColorCreate(colorSpace, components);
CGContextSetStrokeColorWithColor(context, color);
var ctr = 0;
var ctr2 = 0;
for(var i:Int = 1; i<200; i++){
var ii: CGFloat=CGFloat(10+ctr);
var iii: CGFloat = CGFloat(30+ctr2);
CGContextStrokeEllipseInRect(context, CGRectMake(ii, iii, 33, 33));
if(ctr<200)
{
ctr+=160;
}else{
ctr=0;
ctr2+=50;
}
}
CGContextStrokePath(context);
}
}
In interface builder, I set the view class to Draw2D and the controller to my custom ViewController class. The view controller class is currently a standard one with no additional options added.
I tried dragging a Scroll View on the screen, I tried deleting the standard view, adding the scroll view and then putting my Draw2D view on top of that and it didn't work. I tried changing the size of the scroll view to make it smaller than my content (draw2d) view and nothing. I tried unchecking auto-layout and also changing simulated size to freeform as per certain tutorials I found online.
I also at one point tried creating a scroll view programmatically in the UIView and adding it as a subview which didn't help, maybe I did it wrong, I don't know but I'm really running out of options and sources to generate ideas from.

First, you can inherit from UIScrollView directly:
class Draw2D: UIScrollView {
Next, you need to set the content size of the UIScrollView to the height of the elements you inserted. For instance, let's say you have 4 circles:
let radius: CGFloat = 200
var contentHeight: CGFloat = 0
for i in 0...4 {
// initialize circle here
var circle = UIView()
// customize the view
circle.layer.cornerRadius = radius/2
circle.backgroundColor = UIColor.redColor()
// set the frame of the circle
circle.frame = CGRect(x: 0, y: CGFloat(i)*radius, width: radius, height: radius)
// add to scroll view
self.addSubview(circle)
// keep track of the height of the content
contentHeight += radius
}
At this point you have 4 circles one after the other, summing up to 800 px in height. Thus, you would set the content size of the scroll view as follows:
self.contentSize = CGSize(width: self.frame.width, height: contentHeight)
As far as contentHeight > device screen height then your view will scroll.
Full working code below:
import UIKit
import Foundation
class Draw2D: UIScrollView {
override init(frame: CGRect) {
super.init(frame: frame)
let radius: CGFloat = 200
var contentHeight: CGFloat = 0
for i in 0...4 {
// initialize circle here
var circle = UIView()
// customize the view
circle.layer.cornerRadius = radius/2
circle.backgroundColor = UIColor.redColor()
// set the frame of the circle
circle.frame = CGRect(x: 0, y: CGFloat(i)*radius, width: radius, height: radius)
// add to scroll view
self.addSubview(circle)
// keep track of the height of the content
contentHeight += radius
}
self.contentSize = CGSize(width: self.frame.width, height: contentHeight)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Related

How can I get the coordinates of a Label inside a view?

What I am trying to do is to get the position of my label (timerLabel) in order to pass those coordinates to UIBezierPath (so that the center of the shape and the center of the label coincide).
Here's my code so far, inside the viewDidLoad method, using Xcode 13.2.1:
// getting the center of the label
let center = CGPoint.init(x: timerLabel.frame.midX , y: timerLabel.frame.midY)
// drawing the shape
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
and this is what I have when I run my app:
link
What I don't understand is why I get (0,0) as coordinates even though I access the label's property (timerLabel.frame.midX).
The coordinates of your label may vary depending on current layout. You need to track all changes and reposition your circle when changes occur. In view controller that uses constraints you would override
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// recreate your circle here
}
this alone does not explain why your circle is so far out. First of all, looking at your image you do not get (0, 0) but some other value which may be relative position of your label within the blue bubble. The frame is always relative to its superview so you need to convert that into your own coordinate system:
let targetView = self.view!
let sourceView = timerLabel!
let centerOfSourceViewInTargetView: CGPoint = targetView.convert(CGPoint(x: sourceView.bounds.midX, y: sourceView.bounds.midY), to: targetView)
// Use centerOfSourceViewInTargetView as center
but I suggest using neither of the two. If you are using constraints (which you should) then rather create more views than adding layers to your existing views.
For instance you could try something like this:
#IBDesignable class CircleView: UIView {
#IBInspectable var lineWidth: CGFloat = 10 { didSet { refresh() } }
#IBInspectable var strokeColor: UIColor = .lightGray { didSet { refresh() } }
override var frame: CGRect { didSet { refresh() } }
override func layoutSubviews() {
super.layoutSubviews()
refresh()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let fillRadius: CGFloat = min(bounds.width, bounds.height)*0.5
let strokeRadius: CGFloat = fillRadius - lineWidth*0.5
let path = UIBezierPath(ovalIn: .init(x: bounds.midX-strokeRadius, y: bounds.midY-strokeRadius, width: strokeRadius*2.0, height: strokeRadius*2.0))
path.lineWidth = lineWidth
strokeColor.setStroke()
UIColor.clear.setFill() // Probably not needed
path.stroke()
}
private func refresh() {
setNeedsDisplay() // This is to force redraw
}
}
this view should draw your circle within itself by overriding draw rect method. You can easily use it in your storyboard (first time it might not draw in storyboard because Xcode. Simply close your project and reopen it and you should see the circle even in storyboard).
Also in storyboard you can directly modify both line width and stroke color which is very convenient.
About the code:
Using #IBDesignable to see drawing in storyboard
Using #IBInspectable to be able to set values in storyboard
Refreshing on any value change to force redraw (sometimes needed)
When frame changes forcing a redraw (Needed when setting frame from code)
A method layoutSubviews is called when resized from constraints. Again redrawing.
Path is computed so that it fits within the size of view.

IntrinsicContentSize on a custom UIView streches the content

I would like to create a custom UIView which uses/offers a IntrinsicContentSize, since its height depends on its content (just like a label where the height depends on the text).
While I found a lot of information on how to work with IntrinsicContentSize offered by existing Views, I found just a few bits on how to use IntrinsicContentSize on a custom UIView:
#IBDesignable class MyIntrinsicView: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.gray.cgColor)
context?.fill(CGRect(x: 0, y: 0, width: frame.width, height: 25))
height = 300
invalidateIntrinsicContentSize()
}
#IBInspectable var height: CGFloat = 50
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
invalidateIntrinsicContentSize()
}
}
The initial high is set to 50
The draw methods draws a gray rect with a size 25
height is changed to 300 and invalidateIntrinsicContentSize() is called
Placing a MyView instance in InterfaceBuilder works without any problem. The view does not need a height constraint
However, in IB the initial height of 50 is used. Why? IB draws the gray rect, thus the draw method is called. So why is the height not changed to 300?
What is also strange: When setting a background color it is drawn as well, also super.draw(...) is not called. Is this intended?
I would expect a view with height of 300 and a gray rect at the top with a height of 25. However, when running the project in simulator the result is different:
height of the view = 300 - OK
height of content (= gray rect) = 150 (half view height) - NOT OK
It seems that the content was stretched from its original height of 25 to keep its relative height to the view. Why is this?
Trying to change the view's height from inside draw() is probably a really bad idea.
First, as you've seen, changing the intrinsic content size does not trigger a redraw. Second, if it did, your code would go into an infinite recursion loop.
Take a look at this edit to your class:
#IBDesignable class MyIntrinsicView: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.gray.cgColor)
context?.fill(CGRect(x: 0, y: 0, width: frame.width, height: 25))
// probably a really bad idea to do this inside draw()
//height = 300
//invalidateIntrinsicContentSize()
}
#IBInspectable var height: CGFloat = 50 {
didSet {
// call when height var is set
invalidateIntrinsicContentSize()
// we need to trigger draw()
setNeedsDisplay()
}
}
override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: height)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// not needed
//invalidateIntrinsicContentSize()
}
}
Now, when you change the intrinsic height in IB via the IBDesignable property, it will update in your Storyboard properly.
Here's a quick look at using it at run-time. Each tap (anywhere) will increase the height property by 50 (until we get over 300, when it will be reset to 50), which then invalidates the intrinsic content size and forces a call to draw():
class QuickTestVC: UIViewController {
#IBOutlet var testView: MyIntrinsicView!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var h: CGFloat = testView.intrinsicContentSize.height
h += 50
if h > 300 {
h = 50
}
testView.height = h
}
}

Bottom Border Width on Swift TextField in TableView

i builded a static tableview with more Rowes than the screen has, so the user has to scroll to see all cell.
Every cell has a textfield with the following class to add a bottom border:
class TextFieldWithBottomBorder: UITextField {
let border = CALayer()
let width = CGFloat(1.0)
func addBottomBorder(color: UIColor){
self.border.borderColor = color.cgColor
self.border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height:self.frame.size.height)
self.border.borderWidth = self.width
self.layer.addSublayer(self.border)
self.layer.masksToBounds = true
}
func changeBorderColor(color: UIColor){
self.border.borderColor = color.cgColor
}
}
And i call the method after receiving some data from the server e. g.
self.firstnameTextField.text = firstNameFromDB
self.firstnameTextField.addBottomBorder(color: .blue)
This works fine for every cell is currently displayed. But the cells which are out of the current view the with is shorter than the textfield.
See this screenshot, for "Vorname", means firstName everything looks good, but for email, password etc. the border is to short.
http://share-your-photo.com/34b5e80253
Looks like the size of the UITextField is being resized after you have called addBottomBorder and so the UIView being used at the line is now not wide enough. It's difficult to say why this would be without seeing more code but there are several methods you could use to overcome it.
1) Switch to a UIView instead of a CALayer and use auto layout to keep the view in the correction position.
2) Override layoutSubviews to update the frame of the bottom line.
The simplest for you is probably option 2 (although I would go option 1) and it would look like this:
override func layoutSubviews() {
super.layoutSubviews()
self.border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height:self.frame.size.height)
}
Now whenever the frame/size of the text field changes the frame/size of the border line CALayer will be updated appropriately.
Use this class for bottom line text field
#IBDesignable class BottomTextField: UITextField {
var lineView = UIView()
#IBInspectable var lineViewBgColor:UIColor = UIColor.gray{
didSet {
if !isFirstResponder {
lineView.backgroundColor = lineViewBgColor
}
}
}
required init?(coder aDecoder:NSCoder) {
super.init(coder:aDecoder)!
setup()
}
override init(frame:CGRect) {
super.init(frame:frame)
setup()
}
// MARK:- Private Methods
private func setup() {
lineView.frame = CGRect(x:CGFloat(0), y:self.frame.size.height-2, width:self.frame.size.width, height:CGFloat(1))
lineView.backgroundColor = lineViewBgColor
self.addSubview(lineView)
}
}

Swift: Producing a circle UIView is giving a diamond like shape

I created an extension on UIView so that I can create circle views easily without writing the code in each custom component. My code looks as:
extension UIView {
func createCircleView(targetView: UIView) {
let square = CGSize(width: min(targetView.frame.width, targetView.frame.height), height: min(targetView.frame.width, targetView.frame.height))
targetView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: square)
targetView.layer.cornerRadius = square.width / 2.0
}
}
the purpose of the property square is to always compute a perfect square based on the smallest property of width or height from the target view, this stops rectangles from trying to become squares, as that could obviously never produce a circle.
Inside my custom component I call this method with:
// machineCircle is a child view of my cell
#IBOutlet weak var machineCircle: UIView!
// Whenever data is set, update the cell with an observer
var machineData: MachineData? {
didSet {
createCircleView(machineCircle)
}
}
The problem I am having is that my circles are rendering to the screen like this:
When debugging, I inspected the square variable, it consistently prints width: 95, height: 95, which would lead me to believe that a perfect circle should be rendered each time.
Why am I seeing these strange shapes?
UPDATE I have found why perfect circles aren't being formed but I am not sure how to go about it.
In my storyboard I set the default size of my machineCircle view to be 95x95, however when my view loads, the collection cells width and height are computed dynamically with this method:
override func viewDidLoad() {
super.viewDidLoad()
let width = CGRectGetWidth(collectionView!.frame) / 3
let layout = collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = CGSize(width: width, height: width + width / 2)
}
This resizes the collection view cells so that they can fit in cols of 3 accross the screen, but it does not seem to change the base scale of the inner machineCircle view. The machineCircle view still retains its size of 95x95 but seems to scale down inside the view causing the effect to be caused (thats what I have observed thus far). Any ideas?
Taking Matt's advice, I created a method to draw a circle within a UIView using CALayers.
For any who are interested, here is my implementation:
func drawCircleInView(parentView: UIView, targetView: UIView, color: UIColor, diameter: CGFloat)
{
let square = CGSize(width: min(parentView.bounds.width, parentView.bounds.height), height: min(parentView.bounds.width, parentView.bounds.height))
let center = CGPointMake(square.width / 2 - diameter, square.height / 2 - diameter)
let circlePath = UIBezierPath(arcCenter: center, radius: CGFloat(diameter), startAngle: CGFloat(0), endAngle: CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
print(targetView.center)
shapeLayer.path = circlePath.CGPath
shapeLayer.fillColor = color.CGColor
shapeLayer.strokeColor = color.CGColor
shapeLayer.lineWidth = 1.0
targetView.backgroundColor = UIColor.clearColor()
targetView.layer.addSublayer(shapeLayer)
}
And it is called with:
drawCircleInView(self, machineCircle, color: UIColor.redColor(), radius: 30)
Here is the result:
The white box behind is for demonstration purposes only, it shows the parent view that the circle is drawn into, this will be set to transparent in production.

Changing UISwitch width and height

I am trying to change the default height and width of a UISwitch element in iOS, but unsuccessfully.
Can you change the default height and width of a UISwitch element?
Should the element be created programmatically?
I tested the theory and it appears that you can use a scale transform to increase the size of the UISwitch
UISwitch *aSwitch = [[UISwitch alloc] initWithFrame:CGRectMake(120, 120, 51, 31)];
aSwitch.transform = CGAffineTransformMakeScale(2.0, 2.0);
[self.view addSubview:aSwitch];
Swift 4
#IBOutlet weak var switchDemo: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
switchDemo.transform = CGAffineTransform(scaleX: 0.75, y: 0.75)
}
Swift 5:
import UIKit
extension UISwitch {
func set(width: CGFloat, height: CGFloat) {
let standardHeight: CGFloat = 31
let standardWidth: CGFloat = 51
let heightRatio = height / standardHeight
let widthRatio = width / standardWidth
transform = CGAffineTransform(scaleX: widthRatio, y: heightRatio)
}
}
Not possible. A UISwitch has a locked intrinsic height of 51 x 31 .
You can force constraints on the switch at design time in the xib...
but come runtime it will snap back to its intrinsic size.
You can supply another image via the .onImage / .offImage properties but again from the docs.
The size of this image must be less than or equal to 77 points wide
and 27 points tall. If you specify larger images, the edges may be
clipped.
You are going to have to bake your own custom one if you want another size.
here is a nice UISwitch subclass that i wrote for this purpose, its also IBDesignable so you can control it from your Storyboard / xib
#IBDesignable class BigSwitch: UISwitch {
#IBInspectable var scale : CGFloat = 1{
didSet{
setup()
}
}
//from storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
//from code
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup(){
self.transform = CGAffineTransform(scaleX: scale, y: scale)
}
override func prepareForInterfaceBuilder() {
setup()
super.prepareForInterfaceBuilder()
}
}
import UIKit
extension UISwitch {
static let standardHeight: CGFloat = 31
static let standardWidth: CGFloat = 51
#IBInspectable var width: CGFloat {
set {
set(width: newValue, height: height)
}
get {
frame.width
}
}
#IBInspectable var height: CGFloat {
set {
set(width: width, height: newValue)
}
get {
frame.height
}
}
func set(width: CGFloat, height: CGFloat) {
let heightRatio = height / UISwitch.standardHeight
let widthRatio = width / UISwitch.standardWidth
transform = CGAffineTransform(scaleX: widthRatio, y: heightRatio)
}
}
Even if it’s possible to make a UISwitch smaller, this would negatively effect the user experience. Apple's Human Interface Guidelines recommend a minimum size of 44 points for touch targets.
Provide ample touch targets for interactive elements. Try to maintain a minimum tappable area of 44pt x 44pt for all controls
By scaling this to smaller than the standard size, it will become more difficult for users to tap, and also introduce accessibility concerns. Please consider users with less than perfect vision or motor control before making UI elements small.
Finally, here’s an excerpt from a great article about touch target sizes illustrating what can happen when controls are too small.
Interviewer — “I noticed, you had some trouble submitting your email address on this screen, can you tell me how that felt?”
User — “Oh yeah, I’m not very good at technology.”
Interviewer — “What do you think was causing you to struggle at that point?”
User — “The buttons were hard to tap, and I just kept stuffing it up.”

Resources