I'm learning RxSwift.
I added a border as CALayer to TextField. I would like to make this border reactively auto-resized as the textField is expanded (inputView changed to pickerView so I wish it could be resized by selecting titles)
I tried .rx.observe(CGRect.self, "frame") but it doesn't work properly.
extension UITextField {
func addBorderBottom(height: CGFloat, color: UIColor, width: CGFloat){
let border = CALayer()
border.frame = CGRect(x: 0, y: self.frame.height - height, width: width, height: height)
border.backgroundColor = color.cgColor
self.layer.addSublayer(border)
}
}
// viewContrller
func listDetailUISetup() {
let yellowColor = UIColor.hex(string: "FFB500", alpha: 1)
tagTextField.addBorderBottom(height: 1.0, color: yellowColor, width: tagTextField.frame.width)
}
//
picker.rx.modelSelected(String.self)
.subscribe(onNext: { tags in
self.tagTextField.text = tags[0]
})
.disposed(by: disposeBag)
tagTextField.rx.observe(CGRect.self, "frame")
.subscribe(onNext: { frame in
print("field frame changed")
let layer = self.tagTextField.layer.sublayers![0]
layer.frame = frame!
})
.disposed(by: disposeBag)
In a second way, I also tried to manually re-set a border after removing the previous one when a title in the pickerView is selected. But apparently, the width of the item selected at the previous time is reflected.
initial width
All's width
Favourite's width
icker.rx.modelSelected(String.self)
.subscribe(onNext: { tags in
self.tagTextField.text = tags[0]
self.tagTextField.layer.sublayers = nil
self.listDetailUISetup()
})
.disposed(by: disposeBag)
Hopefully, you could help to resolve this issue.
Thank you.
The frame property on a UIView is not KVO observable, so you can't observe it. The text field's layoutSubviews method is called whenever it changes size though so you can use that.
extension UIView {
func addDynamicBorderBottom(height: CGFloat, color: UIColor) -> Disposable {
let border = CALayer()
border.frame = CGRect(x: 0, y: frame.height - height, width: frame.width, height: height)
border.backgroundColor = color.cgColor
layer.addSublayer(border)
return rx.methodInvoked(#selector(UIView.layoutSubviews))
.bind(onNext: { [frame] _ in
border.frame = CGRect(x: 0, y: frame.height - height, width: frame.width, height: height)
})
}
}
But you would be better off making your border a UIView and setting up constraints to ensure it stays the right size.
Related
I want to know if there is a way to remove the border from view in swift after I have done this:
func addRightBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x: self.frame.size.width - width,y: 0, width:width,
height:self.frame.size.height)
self.layer.addSublayer(border)
}
I have tried this ->
func removeBorder(){
self.layer.sublayers?.removeAll()
}
but it doesn't work
You should simply save your own reference to this layer, rather than using sublayers. Using sublayers is not prudent as there might be layers other than your border layer.
Thus:
weak var borderLayer: CALayer?
func addRightBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x: bounds.maxX - width,
y: bounds.minY,
width: width,
height: bounds.height)
layer.addSublayer(border)
self.borderLayer = border
}
func removeBorder() {
borderLayer?.removeFromSuperlayer()
}
This obviously assumes that this was for a subclass of UIView and not an extension. If for the latter, you’d have to use an associated object to keep track of the border layer.
Note, one should not use frame when figuring out where to place the sublayer. Always use bounds.
Taking it a step further, you may want it to gracefully respond to frame changes, too:
override func layoutSubviews {
super.layoutSubviews()
borderLayer?.frame = CGRect(x: bounds.maxX - width,
y: bounds.minY,
width: width,
height: bounds.height)
}
I added the redView as a subView of the textField and used AutoLayout to constraint it to be as it appears in the image, but the problem here now is
I want to bring the red view to be on top of the border
I tried to use this method
bringSubviewToFront(floatingPlaceholderContainer)
but it didn't work, also tried to use the layer.zPosition = .greatestFiniteMagnitude and it also didn't work
Edit-
the code I used and didn't work as I expected
override func layoutSubviews() {
super.layoutSubviews()
self.redlayer.backgroundColor = UIColor.red.cgColor
self.redlayer.frame = CGRect(x: 10, y: -10, width: 100, height: 30)
self.layer.addSublayer(self.redlayer)
self.redlayer.zPosition = 10
}
and this is the result
You need to configure the timing of when you're adding this layer. This approach works fine for me:
class MyTextField : UITextField {
let redlayer = CALayer()
override func layoutSubviews() {
super.layoutSubviews()
self.redlayer.backgroundColor = UIColor.red.cgColor
self.redlayer.frame = CGRect(x: 10, y: -10, width: 100, height: 30)
self.layer.addSublayer(self.redlayer)
self.redlayer.zPosition = 10
}
}
I want to make a search field in my app, and I was wondering if it is possible to make it invisible to begin with and then make it appear with the click of a button, like with the magnifying glass in the video below.
I have tried making the width of a UITextField 0 to begin with and then make a button make the width larger, but I am doing something wrong, and I can not figure out what. Maybe you could make an example in a blank project and show/link the code?
I hope you can help :)
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: {
//change view width
}) { (completed) in
//you can use completion or you can delete I am using it like that
self.searchTextview.becomeFirstResponder()
}
I am using this for my view. You can change for your need. Change constraints or width with animation
Function for expand the SearchTextField
func setTextField(setExpand:Bool = false){
self.txtfldSearch.delegate = self
self.txtfldSearch.borderStyle = UITextField.BorderStyle.none
self.txtfldSearch.translatesAutoresizingMaskIntoConstraints = true
let bottomLine = CALayer()
bottomLine.backgroundColor = UIColor.red.cgColor
UIView.animate(withDuration: 0.5) {
if setExpand{
self.txtfldSearch.frame = CGRect(x:
self.viewContainer.frame.origin.x + 8, y:
self.txtfldSearch.frame.origin.y, width:
(self.btnSearch.frame.origin.x -
(self.viewContainer.frame.origin.x + 16)),
height: self.txtfldSearch.frame.size.height)
bottomLine.frame = CGRect(x: 0.0, y:
self.txtfldSearch.frame.size.height-2, width:
self.txtfldSearch.frame.size.width, height: 2.0)
}
else{
self.txtfldSearch.frame = CGRect(x:
self.btnSearch.frame.origin.x - 8,
y: self.txtfldSearch.frame.origin.y, width: 0,
height:self.txtfldSearch.frame.size.height)
bottomLine.frame = CGRect(x: 0.0, y:
self.txtfldSearch.frame.size.height-2, width:
self.txtfldSearch.frame.size.width, height: 2.0)
}
}
self.txtfldSearch.layer.addSublayer(bottomLine)
}
Use of Code For Expanding pass true and other case pass false
self.setTextField(setExpand: true)
I am using the following code in Swift 4.2 to have Textfield bottom border:
extension UITextField {
func useUnderline() {
let border = CALayer()
let borderWidth = CGFloat(1.0)
border.borderColor = UIColor.lightGray.cgColor
border.frame = CGRect(origin: CGPoint(x: 0,y :self.frame.size.height - borderWidth), size: CGSize(width: self.frame.size.width, height: self.frame.size.height))
border.borderWidth = borderWidth
self.layer.addSublayer(border)
self.layer.masksToBounds = true
}
}
but in different model of iPhones I get a different behaviors. for example:
in Iphone XR:
and with iPhone X or 8:
Any solution to this issue would be appreciated.
I'm sure this problem does not depend on an iPhone model, but from the place where you call useUnderline(). If you will call useUnderline() from viewDidAppear, then the line will draw correct. Because the size of UIControls not yet actual when calling viewDidLoad and viewWillAppear.
But keep in your mind you will have a problem with your extension because sublayers will be added to UITextField on every viewDidAppear execution.
This could be a prime example where you must use a UITextField subclass instead of an extension. The accepted answer will work, but as suggested there, your TextField will get many layers added to it as the user uses his application.
The below subclass could solve this issue:
class CustomTextField: UITextField{
let border = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
useUnderline()
}
func useUnderline(){
if layer.sublayers?.contains(border) ?? false{
border.removeFromSuperlayer()
}
border.path = UIBezierPath.init(rect: CGRect.init(x: 0, y: self.bounds.height - 2, width: self.bounds.width, height: 2)).cgPath
border.fillColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.3).cgColor
self.layer.insertSublayer(border, at: 10)
}
}
I'm creating a bare-bones window programmatically and I just need to draw some lines in its content view. The size of the area the lines fall in (worldBounds) needs to be scaled down and I assume I need the contentView bounds origin to be the same as worldBounds origin so it can draw everything. The approach is to set the frame to 1/8 the size, then use setBoundsOrigin and setBoundsSize on the contentView, which I understand scales the coordinate system.
The problem is no drawing appears. The window appears with the correct size. I have a function drawTestLines set up just to test it. If I remove the calls to setBounds, everything draws fine, but of course then the bounds don't match worldBounds. Any help is much appreciated!
var worldBounds = NSRect()
let scale: CGFloat = 0.125
func drawMap() {
boundLineStore(lineStore, &worldBounds)
worldBounds.origin.x -= 8
worldBounds.origin.y -= 8
worldBounds.size.width += 16
worldBounds.size.height += 16
if !draw {
return
}
let scaled = NSRect(x: 300.0, // to set the window size/position
y: 80,
width: worldBounds.size.width*scale, // 575
height: worldBounds.size.height*scale) // 355
window = NSWindow(contentRect: scaled,
styleMask: .titled,
backing: .buffered,
defer: false)
window.display()
window.orderFront(nil)
window.contentView!.setBoundsSize(worldBounds.size) // (4593, 2833)
window.contentView!.setBoundsOrigin(worldBounds.origin) // (-776, -4872)
// Draw map lines
window.contentView!.lockFocus()
drawTestLines(in: window.contentView!)
window.contentView!.unlockFocus()
}
// for testing
func drawTestLines(in view: NSView) {
let bottom = view.bounds.minY
let top = view.bounds.maxY
let left = view.bounds.minX
let right = view.bounds.maxX
NSColor.black.setStroke()
for i in Int(left)..<Int(right) { // draw vertical lines across the view just to see if it works!
if i%64 == 0 {
NSBezierPath.strokeLine(from: NSPoint(x: CGFloat(i), y: bottom), to: NSPoint(x: CGFloat(i), y: top))
}
}
NSBezierPath.strokeLine(from: NSPoint(x: left, y: bottom), to: NSPoint(x: right, y: top))
NSBezierPath.strokeLine(from: NSPoint.zero, to: NSPoint(x: 50.0, y: 50.0))
}
Experiment: Create a new Xcode project. Change applicationDidFinishLaunching:
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let contentBounds = window.contentView?.bounds {
let scale: CGFloat = 0.5
let worldBounds = CGRect(x: 0.0, y: 0.0, width: contentBounds.size.width * scale, height: contentBounds.size.height * scale)
window.contentView!.bounds = worldBounds
}
}
Change the class of the content view of the window in IB to MyView. Add a new class MyView, subclass of NSView. Change draw to:
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let bottom = self.bounds.minY
let top = self.bounds.maxY
let left = self.bounds.minX
let right = self.bounds.maxX
NSColor.black.setStroke()
for i in Int(left)..<Int(right) { // draw vertical lines across the view just to see if it works!
if i%64 == 0 {
NSBezierPath.strokeLine(from: NSPoint(x: CGFloat(i), y: bottom), to: NSPoint(x: CGFloat(i), y: top))
}
}
NSBezierPath.strokeLine(from: NSPoint(x: left, y: bottom), to: NSPoint(x: right, y: top))
NSBezierPath.strokeLine(from: NSPoint.zero, to: NSPoint(x: 50.0, y: 50.0))
}
See what happens when you change scale or the origin of worldBounds.