Live Xcode's Interface Builder UIView subclass (IB_DESIGNABLE) without drawRect: - ios

Is it possible to create a UIView subclass that renders live in Xcode (by adding the IB_DESIGNABLE attribute as explained here) but doesn't have a custom drawRect: method?
We have a custom UIView subclass that uses some CAShapeLayers which are added to self.layer for drawing (hence, there's no need to override drawRect:). This class works fine on the App, but won't render on Xcode.
If we replicate the code in drawRect: it works, but we'd prefer to keep the drawing to happen automatically on the layers.
Can this be done?
I also tried doing
- (void)drawRect:(CGRect)rect
{
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextMoveToPoint(currentContext, self.myLayer1.frame.origin.x, self.myLayer1.frame.origin.y);
[self.myLayer1 renderInContext:currentContext];
CGContextMoveToPoint(currentContext, self.myLayer1.frame.origin.x, self.myLayer1.frame.origin.y);
[self.myLayer2 renderInContext:currentContext];
}
which seems to work on the device but not on Xcode's IB.

You can preview UIView subclasses in IB (with the IB_DESIGNABLE macro) even if the drawRect: isn't overridden.
I added your code in XCode 6.1 and added a OEProgressIndicator into a xib file. Then I debugged it (using menu Editor / Debug Selected View ) by setting a breakpoint in your commonProgressIndicatorInit selector.
Here's why you don't see anything in the preview with your current code: when the commonProgressIndicatorInit is invoked (from the initWithFrame:(CGRect)frame constructor), the frame is equal to CGRectZero (x:0 y:0 width:0 height:0) so that your center variable is actually equal to (0, 0) and radius is -1.
On the device, depending on the way the class is used, you may be directly invoked with the proper frame, that's why it may work on the device but not in IB.
To fix this, I would implement the layoutSubviews selector (override it from UIView) to organise properly the sublayers. This selector is going to be invoked when the frame is going to change from CGRectZero to the proper values set in Interface Builder.

I've been using the method - (void)prepareForInterfaceBuilder in order to tell IB to live render a view.
See here: Creating a Live View of a Custom Object
Also, you guys are right that this feature is also available for Objective-C.
You don't necessarily need to use drawRect, you can try using - (void)layoutSubviews, it seems to work. The problem with leaving code in places like - (void)layoutSubviews just for the sake of live rendering is that it may be less performant, etc (for instance you can do a lot of stuff in - (void)awakeFromNib, but that method does not get called from Live Rendering, so just make sure that you do all your set up in - (void)prepareForInterfaceBuilder as well.

Without seeing all your code, it's hard to see what the source of the problem is, but to answer your question, yes, you can use IBInspectable / IBDesignable without needing to implement any other specific method. I have done this for a view that uses many layers and does not do any drawing (uses the nested layers for that).
For a quick test example snippet with rounded corners:
#IBDesignable
class MyView : UIView {
#IBInspectable var cornerRadius:CGFloat {
get { return self.layer.cornerRadius }
set { self.layer.cornerRadius = newValue }
}
#IBInspectable var borderWidth:CGFloat {
get { return self.layer.borderWidth }
set { self.layer.borderWidth = newValue }
}
#IBInspectable var borderColor:UIColor {
get { return UIColor(CGColor: self.layer.borderColor) }
set { self.layer.borderColor = newValue.CGColor }
}
}
For a simple example that does gradients, see this post.
For an explanation on how to debug the live views, refer to WWDC §411 at about the 22 minute mark.
The only limitation that I have seen so far is that you can add inspectable properties in class extensions, but they only render properly on the device.

Related

Swift: Rounded corners appear different upon first appearance and reappearing

class ApplyCorners: UIButton {
override func didMoveToWindow() {
self.layer.cornerRadius = self.frame.height / 2
}
}
I apply this class to the buttons in my application and it is working great, but when I apply it to a button used in every cell in a table view the button corners are not round upon entering the view, but if I click one of the buttons I get segued to another view. If I then segue back the corners are "fixed" / round.
The green is the button when returning and the red is upon first entering the view.
Anyone know how to fix this?
I'd suggest layoutSubviews, which captures whenever the frame of the button changes:
class ApplyCorners: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.height / 2
}
}
This takes care of both the original appearance and any subsequent appearance. It also avoids all sorts of problems related to not only whether the frame was known when the view appeared, but also if you do anything that might change the size of the button (e.g. anything related to constraints, rotation events, etc.).
This sort of thing is likely to be a timing problem. Consider the phrase self.frame.height. At the time didMoveToWindow is called, we may not yet know our frame. If you are going to call a method that depends upon layout, do so when layout has actually occurred.
Gonna propose another alternative: listen to any bounds changes. This avoids the problem of wondering "is my frame set yet when this is called?"
class ApplyCorners: UIButton {
override var bounds: CGRect {
didSet {
layer.cornerRadius = bounds.height / 2
}
}
}
Edited frame to bounds because as #Rob points out, listening for frame changes will cause you to miss the initial load sometimes.
Putting your code in didMoveToWindow() does not make sense to me. I'd suggest implementing layoutSubviews() instead. That method gets called any time a view object's layout changes, so it should update if you resize your view.
(Changed my suggestion based on comments from TNguyen and and Rob.)

I can not get this UIView round

This is the final result of the code below:
This is my code:
view.layer.cornerRadius = 0.5 * view.bounds.size.width
view.layer.masksToBounds = true
view.clipsToBounds = true
view is the red UIView. This code is placed in viewWillLayoutSubviews and viewDidLayoutSubviews (not together, but I both tested them).
Besides this SO is not clear about using masksToBounds OR clipsToBounds. I am not sure what to use and when to use one of them above. I also tested it apart.
The green UIView has also clipsToBounds active and rounded corners, but if I remove them both, I still get the same effect... I hope someone can help. This only occurs when I place a UIView, inside another UIView. If the UIView does not has a parent, it works (I get the UIView rounded).
Edit: GitHub link: https://github.com/Jasperav/tests
The problem lies with when the auto-layout engine actually finishes all the layout calculations.
If you move your code to viewDidAppear you will see properly "rounded" corners.
This is a very common case for using a custom view though. In fact, it's easily used as an "IBDesignable" class, so you can see it rendered in Interface Builder.
Create a new file named "RoundedView.swift" with this code:
import UIKit
#IBDesignable
class RoundView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = 0.5 * self.bounds.size.height
}
}
Then assign that class to the View you want to be round. Xcode will build your new class, and it will show up in Interface Builder like so:

IBInpectable properties for different states when customizing UIButton

I have customised UI button and create some properties with IBInspectable. However, I also need the same property for selected or highlighted state and can be inspected in Interface Builder. I want to know if it can be achieved?
Here is the customized button I created
#IBDesignable
class ImageLabelButton: UIButton{
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
let buttonImgView = UIImageView()
let buttonLabel = UILabel()
// let stackView = UIStackView()
// Override property observors
#IBInspectable
var textColor:UIColor? {
get {
return buttonLabel.textColor
}
set(newValue) {
self.buttonLabel.textColor = newValue
}
}
}
I want to create a IBInspectable property for other states as well. Can it be done? Thanks!
Short answer? No. Interface Builder cannot "process" code.
I have a need to know when my app is in portrait or landscape orientation (various slider controls are on the bottom or right depending on this).
Can I use IB for this? Not if I need to know on an iPad... it's size class is (wR hR) unless the slide out or split screen is there. I can "design" something for each orientation, but even Apple - WWDC'16, Making Apps Adaptive, Part 2 - ended up putting code into viewWillLayoutSubviews() for this.
Put a UIButton on your storyboard. Can you process a tap? Put a UISlider on the storyboard. Can you pan it left or right?
You are asking a design time tool to process run time actions.
So again, you can't make certain button states IBDesignable.

Can you add IBDesignable properties to UIView using categories/extensions?

For those that don't know what I'm talking about, Xcode 6.0 added new features, IBDesignable and IBInspectable.
When you tag your custom views with IBInspectable properties, those properties show up in the Attributes Inspector in IB.
Likewise, when you tag a custom UIView subclass with IBDesignable, Xcode compiles your views and invokes the code to render your view objects right in the Xcode window so you can see what they look like.
The technique for adding IBDesignable and IBInspectable attributes to custom views is pretty much identical in Swift and Objective-C. IBInspectable properties appear in the Interface Builder Attributes Inspector regardless of which language you use to define them.
I've created a category of UIView in Objective-C and an extension of UIView in Swift that promote the borderWidth, cornerRadius, borderColor, and layerBackgroundColor properties of the view's underlying layer as properties of the view. If you change the property, the extension/category does type conversion as required and forwards the change to the layer.
The IBInspectable part works great. I see and can set the new properties in the IB attributes inspector.
I could have sworn that last week, the IBDesignable attribute on my view category/extension was working too, and I could see my custom UIView category rendering in IB with it's changed layer attributes. This week it isn't working.
Was I hallucinating?
Can categories/extensions of existing system classes draw their custom UI in Interface Builder when they are set up with IBDesignable?
Since posting this question I've learned that #IBDesignable does not work for class extensions. You can add the tag, but it has no effect.
I was able to make it work with code below, but the side effect is that some times IB agent in storyboard crashes because it has to refresh too many UI elements. Restarting Xcode fixes problem temporarily until next crash. Maybe that's the problem OP is facing
#IBDesignable
extension UIView
{
#IBInspectable
public var cornerRadius: CGFloat
{
set (radius) {
self.layer.cornerRadius = radius
self.layer.masksToBounds = radius > 0
}
get {
return self.layer.cornerRadius
}
}
#IBInspectable
public var borderWidth: CGFloat
{
set (borderWidth) {
self.layer.borderWidth = borderWidth
}
get {
return self.layer.borderWidth
}
}
#IBInspectable
public var borderColor:UIColor?
{
set (color) {
self.layer.borderColor = color?.cgColor
}
get {
if let color = self.layer.borderColor
{
return UIColor(cgColor: color)
} else {
return nil
}
}
}
}
That's why I am trying to add where clause to reduce subclasses which should extend this functionality:
Generic IBDesginables UIView extension
#IBDesignable work with UIView extension only in custom class.For example. UILabel is a default sub-class of UIView. It won't work there, but if you make a custom class called MyUILabel subclassing UILabel. assign the MyUILabel class to the Label your are working on. Then your corner radius in UIView extension will work of this MyUILabel.
( I guess the first week it work for you is because you are dealing with some custom class.)
I've made this work for my use case by having one #IBDesignable UIView that I set as the top view in my view controller. My particular use case is making ClassyKit styling visible in Interface Builder on the default UIKit views without have to subclass just for that and it's working great.
Here's an example of how you could set it up:
// in Interface Builder set the class of your top view controller view to this
#IBDesignable class DesignableView: UIView {
}
extension UIView {
open override func prepareForInterfaceBuilder() {
subviews.forEach {
$0.prepareForInterfaceBuilder()
}
}
}
extension UILabel {
// just an example of doing something
open override func prepareForInterfaceBuilder() {
layer.cornerRadius = 8
layer.masksToBounds = true
backgroundColor = .red
textColor = .green
}
}
This code block is working well for me.
import UIKit
public extension UIView {
#IBInspectable public var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
}
NOTE It might not work when being imported from a framework. I am trying to find out the reason now.

How to change UIView's position in code?

I created a custom ViewCell class attached with a xib layout file, and I added some labels in xib. But when I want to change the labels' position in code, it doesn't work, can you tell me why?
Here are the the relative codes:
public override void LayoutSubviews ()
{
base.LayoutSubviews ();
var bounds = ContentView.Bounds;
UILabel label_period = RetriveViewByID ("label_period") as UILabel;
label_period.ContentMode = UIViewContentMode.Redraw;
label_period.Text = string.Format ("{0}天之旅", m_travel_data.period); \\Works
label_period.Frame = new RectangleF (20, label_period.Frame.Y, label_period.Frame.Width, label_period.Frame.Height);
}
It is weird, because the Text is actually changed, but Frame not.
There are a few things that you could try here.
First of all try calling
label_period.SetNeedsLayout();
or possibly (but rather unlikely)
label_period.SetNeedsDisplay();
If neither of those work I would check to see that you do not have any unnecessary Layout Constraints declared in your .xib.
Although this was directly part of your question, I recommend instead of using RetrieveViewByID(...) just reference the view by its name.
(If you declared the UIViewController in the .xib you should be able to access the view inside the UIViewController with this.label_period )
Finally, although I do not know the specifics of your situation, I recommend switching over to Storyboards, you will run into issues like these less.

Resources