IBInpectable properties for different states when customizing UIButton - ios

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.

Related

How to create a Collision Detection between a label and an Image

INTRUDUCTION
I want to create a simple game where you should be able to drag a label and if you drag it in the correct place, you win. To be more specific: This is a game to help children with autism. In this game they have to create the correct sequence of numbers from one to ten dragging the label with the number in the correct place (which is an image actually). now you will understand better:
PROBLEM
I have already created the code to drag the labels (with a pan gesture recognizer) but I don't know how to create a Collision detection: When the card "1" is dragged and it collides with the blue image "1" something happens,
my mentally code is:
if LB_1 *collides with* IMG_1 {
self.IMG_1?.backgroundColor = UIColor.green
}
I hope the question it's clear.
Ah I don't use SpriteKit. I used "SingleViewApplication" as Template not "Game".
You want to use contains(). If you use intersects() then it will return true as soon as the two rectangles touch. With contains(), it won't return true until the dragged view is fully inside the target view, which seems much more intuitive.
I just wrote a sample app that implements this and it works perfectly.
I created a simple subclass of UIView I called BoxedView that just sets a border around it's layer so you can see it.
I set up a view controller with 2 boxed views, a larger "targetView", and a smaller view that the user could drag.
The target/action for my gesture recognizer moves the dragged view's frame as the user drags, and if target view contains the dragged view, it sets a Bool highlightTargetView, which causes the box around the target view to get thicker.
The entire view controller class' code looks like this:
class ViewController: UIViewController {
#IBOutlet weak var targetView: BoxedView!
var viewStartingFrame: CGRect = CGRect.zero
var highlightTargetView: Bool = false {
didSet {
targetView.layer.borderWidth = highlightTargetView ? 5 : 1
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func userDraggedView(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
viewStartingFrame = gesture.view?.frame ?? CGRect.zero
case .changed:
let offset = gesture.translation(in: view)
gesture.view?.frame = viewStartingFrame.offsetBy(dx: offset.x, dy: offset.y)
highlightTargetView = targetView.frame.contains(gesture.view?.frame ?? CGRect.zero)
case .ended:
gesture.view?.frame = viewStartingFrame
highlightTargetView = false
default:
break
}
}
}
In order for the math to work, I use the frame property of both views, which is in the coordinate system of the parent view (The parent view is the view controller's content view in this case, but the key thing is that we compare 2 rectangles using the same coordinate system for both.) If you used the bounds property of either view your math wouldn't work because bounds of a view is in the local coordinate system of that view.
Here's what that program looks like when running:
For comparison, I modified the program to also show what it looks like using the intersects() function, and created a video of what that looks like:
You can check if their frames intersect.
CGRect has method intersects.
So you're if statement should be the following:
if LB_1.frame.intersects(IMG_1.frame) {
self.IMG_1?.backgroundColor = UIColor.green
}
If that's not enough for you, you can calculate area of intersection rect.

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.)

Custom UIInputView presented on inputAccessoryView not respecting height constraint on iPhone X

I am building a chat application that has a floating text input field at the bottom of the screen (similar to the iMessage app).
I've achieved this by creating a ChatInputViewController which is a subclass of UIInputViewController, which contains a UIInputView with a textView for input.
In my main ChatViewController, I then override the following properties to present my custom UIInputViewController:
class ChatViewController: UIViewController {
override var inputAccessoryViewController: UIInputViewController? {
return chatInputViewController
}
override var canBecomeFirstResponder: Bool {
return true
}
...
}
Here's the issue: I also have a custom input view which is presented when the user taps on the help icon. The custom input view is laid out using Autolayout and has an explicit height constraint set to be equal to the height of the keyboard. When presented, this view swaps out with the keyboard in place. I am doing this by setting the inputView property of the textView and then calling textView.reloadInputViews(). This works as intended on all devices except the iPhone X, where instead the inputView is expanded to cover the entire screen.
There are no autolayout constraints logged in the console. Both the ChatInputViewController and HelpInputViewController have allowSelfSizing enabled on their inputViews and translatesAutoresizingMaskIntoConstraints disabled. Any ideas would be greatly appreciated.

UILabel text property when set to nil or "" makes UILabel disappear from view (Swift / Autolayout/ iOS9.1)

I am going through the Stanford Winter 2015 Swift/iOS course and while doing the assignments I run into a behavior I'd like to change.
I use Autolayout as described in the videos (making the display pin to leading and trailing view edges) and the Calculator app "Display" UILabel is fine with an initial value of 0 and whenever the value used to set it (a String) is non-nil and non "".
If it is either nil or "", the entire UILabel disappears. What I am trying to do is to "clear" the display whenever there is no value to display or an incorrect calculation resulted in nil.
Any tips on who to deal with this in general? "Clearing" a UILabel without changing it's on-screen dimensions?
Edit (thanks Rob)
The UILabel has the following constraints
1. Option-click drag-left to containing UIView, selected "leading" something (on commute to work can't check yet for exact wording.
2. Same method as (1) except that the drag is to the right edge and selecting "trailing"
3. Option click-drag up to top of view, select "vertical" menu option.
4. Same as (3) except that drag is to a UIButton underneath the UILabel on the GUI.
With those settings, the label when it contains a number is always visible and (if understand, will color it to verify) stretches across the screen even if the text doesn't.
The layout looks correct in profile and landscape as long as content of UILabel is not empty. If empty, it seems to "shrink to fit" so much that the buttons below get moved up towards the top.
I'm a C++ dev since mid 90s but I have little UI experience and no more than a couple weeks experience in iOS/Swift development.
Thanks!
You can always give the UILabel a min width and min height or constraints that holds the left and right side of the label. That should keep the label from changing it's dimensions to zero.
Use a custom UILabel class assigned in Interface Builder >> Identity inspector >> Custom Class >> Class to override UILabel intrinsic content size.
No need to create any superfluous auto-layout constraints.
Swift:
class UILabelNonCompressible: UILabel
{
private static let NonCompressibleInvisibleContent = " "
override var intrinsicContentSize: CGSize
{
if /* zero-width */ text == nil ? true : text!.isEmpty
{
// prefer mirror-and-calculate over modify-calculate-restore due to KVO
let doppelganger = createCopy()
// calculate for any non-zero -height content
doppelganger.text = UILabelNonCompressible.NonCompressibleInvisibleContent
// override
return doppelganger.intrinsicContentSize
}
else
{
return super.intrinsicContentSize
}
}
}
You will also need "How do copy for UILabel?":
extension UILabel
{
func createCopy() -> UILabel
{
let archivedData = NSKeyedArchiver.archivedData(withRootObject: self)
return NSKeyedUnarchiver.unarchiveObject(with: archivedData) as! UILabel
}
}

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

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.

Resources