I'm creating an darken overlay on top of my UITableView, I would like to highlight one of the row. I'm able to highlight it, but how can I enable user interaction (e.g. tap on the cell)?
func addFullScreenDarkOverlay(){
let viewDarkOverlay = UIView()
viewDarkOverlay.alpha = 0.5
viewDarkOverlay.backgroundColor = .black
viewDarkOverlay.frame = UIApplication.shared.keyWindow!.frame
// add dummy hole (will change it to the frame of the particular cell)
let holeFrame = CGRect(x: 0, y: 0, width: viewDarkOverlay.frame.width/2, height: viewDarkOverlay.frame.height/2)
// Do any additional setup after loading the view.
let path = CGMutablePath()
// Dark background
path.addRect(CGRect(x: 0, y: 0, width: viewDarkOverlay.frame.width, height: viewDarkOverlay.frame.height))
// White/spotlight path
path.addRect(holeFrame)
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = kCAFillRuleEvenOdd
viewDarkOverlay.layer.mask = maskLayer
viewDarkOverlay.clipsToBounds = true
UIApplication.shared.keyWindow!.addSubview(viewDarkOverlay)
}
Basically, you can disable the user interaction of the mask view, that will pass the all the touch events to the view below... however, if you just want the highlighted area to be intractable (i.e. the cell), you can create a custom UIView subclass, override the hitTest method, like so:
import UIKit
class MaskView: UIView {
var hitableArea: CGRect {
return CGRect(x: 0, y: 0, width: bounds.width * 0.5, height: bounds.height * 0.5)
}
override init(frame: CGRect) {
super.init(frame: frame)
installMaskLayer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
installMaskLayer()
}
private func installMaskLayer() {
backgroundColor = UIColor.black.withAlphaComponent(0.5)
let path = CGMutablePath()
// Dark background
path.addRect(bounds)
// White/spotlight path
path.addRect(hitableArea)
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
layer.mask = maskLayer
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if (hitableArea.contains(point)) {
return nil
}
return self
}
}
Just create this custom view and add it to your window, and now you can only touch the cell now.
Remove the user interaction in your overlay view and you will be able to select the rows in the view below it:
viewDarkOverlay.isUserInteractionEnabled = false
If you want the selection only inside the whole you can transfer the touch from the overlay view to your parent view using a delegate like so:
protocol OverlaySelection: class{
func selected(with touch: UITouch?)
}
class OverlayView: UIView{
weak var delegate : OverlaySelection?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
delegate?.selected(with: touches.first)
}
}
then in your parent view:
func addFullScreenDarkOverlay(){
let viewDarkOverlay = OverlayView()
viewDarkOverlay.delegate = self
//other config..
}
//Delegate
func selected(with touch: UITouch?) {
if let location = touch?.location(in: self.view){
//Here you check if this point is inside the whole and if it is you select
//the row, if not just return
let indexpath = tableview.indexPathForRow(at: location)
tableview.selectRow(at: indexpath, animated: true, scrollPosition: .bottom)
}
}
Related
I'm pretty new to Swift and I'm having some trouble displaying UITextField in SpriteKit. Init() in SceneMenu will not display the UITextField, while using function showTextField() with touchesBegan() works. How can I display UITextField without the user clicking the button Show TextField?
GameViewController.swift:
class GameViewController: UIViewController {
override func viewDidLoad() {
let scene = SceneMenu(size: view.frame.size)
scene.scaleMode = .aspectFill
scene.backgroundColor = .white
let view = view as! SKView
view.presentScene(scene)
}
}
SceneMenu.swift:
class SceneMenu: SKScene {
override init(size: CGSize) {
super.init(size: size)
let btnAlert = SKLabelNode(text: "Show TextField")
btnAlert.name = "btn_text"
btnAlert.fontSize = 20
btnAlert.fontColor = SKColor.blue
btnAlert.fontName = "Avenir"
btnAlert.position = CGPoint(x: size.width / 2, y: size.height / 2)
addChild(btnAlert)
let textFieldFrame = CGRect(origin: CGPoint(x: 100, y: 300), size: CGSize(width: 200, height: 30))
let textField = UITextField(frame: textFieldFrame)
textField.backgroundColor = SKColor.blue
textField.placeholder = "Type name"
view?.addSubview(textField)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showTextField() {
let textFieldFrame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 200, height: 30))
let textField = UITextField(frame: textFieldFrame)
textField.backgroundColor = SKColor.blue
textField.placeholder = "Type name"
view?.addSubview(textField)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let action = atPoint(location)
if action.name == "btn_text" {
showTextField()
}
}
}
}
SKScene has a method that lets you setup the scene when you move to it. So, instead of using the custom class's initializer, override and use SKScene's didMove(to view: SKView) method. An example:
override func didMove(to view: SKView) {
showTextField()
}
I am trying to create an overlay view, that has a "cutout" part that comes from a frame that is passed to the view, that size and position of that passed frame will change upon creation of the view. And in that "cutout" part I am expecting to see the content that is under that overlay view. Tried to set border to a rounded rectangle that is added to a CGMutablePath, but no luck.
The expected result is something like this:
The code I currently have in my UIView class, without tried solutions as I can't seem to get them to work properly. This current code displays the expected result, but without the red border:
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.blue.setFill()
UIRectFill(rect)
let shapeLayer = CAShapeLayer()
let path = CGMutablePath()
// frame that will change position on the screen
if let frame = changingFrame {
path.addRoundedRect(in: frame, cornerWidth: 16, cornerHeight: 16)
}
path.addRect(bounds)
shapeLayer.path = path
shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
self.layer.mask = shapeLayer
}
I have tried solutions from here, here, but no luck as CAShapeLayer for border just overlays existing one.
What can I do differently to achieve the expected result? Thanks!
try this ⭐️
If all you want to do is create a rounded rectangle, then you can simply use.
let rectangle = CGRect(x: 0, y: 0, width: 100, height: 100)
let path = UIBezierPath(roundedRect: rectangle, cornerRadius: 20)
view.clipsToBounds = true
view.layer.cornerRadius = 10.0
let border = CAShapeLayer()
border.path = UIBezierPath(roundedRect:view.bounds, cornerRadius:10.0).cgPath
border.frame = view.bounds
border.fillColor = nil
border.strokeColor = UIColor.purple.cgColor
border.lineWidth = borderWidth * 2.0 // doubled since half will be clipped
border.lineDashPattern = [1.0]
view.layer.addSublayer(border)
One approach is to use two sublayers... a "cutout" layer and a "border" layer.
Use the same path for the cutout and the border shape, setting the line width and stroke color for the "outline".
Here's an example -- including making it #IBDesignable with a few #IBInspectable properties:
#IBDesignable
class BorderedCutoutView: UIView {
#IBInspectable
var bkgColor: UIColor = .systemBlue {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var brdColor: UIColor = .white {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var brdWidth: CGFloat = 1 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var radius: CGFloat = 20 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var horizInset: CGFloat = 40.0 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var vertInset: CGFloat = 60.0 {
didSet {
setNeedsLayout()
}
}
private let cutoutLayer = CAShapeLayer()
private let borderLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
backgroundColor = .clear
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(cutoutLayer)
layer.addSublayer(borderLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(rect: bounds)
let cp = UIBezierPath(roundedRect: bounds.insetBy(dx: horizInset, dy: vertInset), cornerRadius: radius)
path.append(cp)
path.usesEvenOddFillRule = true
cutoutLayer.path = path.cgPath
cutoutLayer.fillRule = .evenOdd
cutoutLayer.fillColor = bkgColor.cgColor
borderLayer.path = cp.cgPath
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.lineWidth = brdWidth
borderLayer.strokeColor = brdColor.cgColor
}
}
This example uses horizontal and vertical "inset" values to center the cutout in the view.
Result:
The video: https://www.youtube.com/watch?v=8_xCTJAld9Y&feature=youtu.be
. Here you can see my issue, and I do not know what exactly is wrong. I cant move it like I want.
I have a draggable UIView, which has a custom class:
import UIKit
class UIViewDraggable: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isUserInteractionEnabled = true
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first;
let location = touch?.location(in: self.superview);
if(location != nil)
{
self.frame.origin = CGPoint(x: location!.x-self.frame.size.width/2, y: location!.y-self.frame.size.height/2);
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
}
I create the UIView with a button tap:
#IBAction func buttonAddSubviewTextTapped(_ sender: UIButton) {
let label = UIView()
label.frame = CGRect(x: 0, y: 0, width: 200, height: 21)
label.center = CGPoint(x: 160, y: 285)
label.backgroundColor = .red
self.viewImagePreview.addSubview(label)
self.viewImagePreview.bringSubviewToFront(label)
}
How can I implement a fully smoothly draggable UIView?
Final Solution to make button created view draggable within its parentview:
#IBAction func buttonAddSubviewTextTapped(_ sender: UIButton) {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
let label = UIView()
label.frame = CGRect(x: 0, y: 0, width: 200, height: 21)
label.center = CGPoint(x: 160, y: 285)
label.backgroundColor = .red
label.isUserInteractionEnabled = true
label.addGestureRecognizer(panGesture)
self.viewImagePreview.addSubview(label)
self.viewImagePreview.bringSubviewToFront(label)
}
// Function to drag a view
#objc func draggedView(_ sender: UIPanGestureRecognizer) {
guard let createdView = sender.view else {return}
createdView.bringSubviewToFront(viewImagePreview)
if sender.state == .began || sender.state == .changed {
let point = sender.location(in: self.viewImagePreview)
if let parentView = self.viewImagePreview {
let superBounds = CGRect(x: parentView.bounds.origin.x, y: parentView.bounds.origin.y, width: parentView.bounds.size.width - 2, height: parentView.bounds.size.height - 2)
if (superBounds.contains(point)) {
let translation = sender.translation(in: parentView)
createdView.center = CGPoint(x: createdView.center.x + translation.x, y: createdView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: parentView)
}
}
}
}
Add a panGesture to your View and then based on the touch location, updated your view.center
let pan = UIPanGestureRecognizer(target: self, action: #selector(panView))
self.addGestureRecognizer(pan)
then use it as:
#objc func panView(pan: UIPanGestureRecognizer) {
let location = pan.location(in: self.superView) // get pan location
switch pan.state {
case .began:
print("began")
case .changed:
self.center = location
// updated your frames here
default:
print("nothing")
}
}
Also for your case,
try replacing
self.frame.origin = CGPoint(x: location!.x-self.frame.size.width/2, y: location!.y-self.frame.size.height/2);
to
self.center = location
I have a UIView. My UIView has a layer. I added another layer to it. I need to cut a circle in this layer when the user moves his finger to create a drawing effect. I will leave my code, but it is not correct, as it creates a circle once.
class CustomImageView: UIImageView {
private var recognizer: UIPanGestureRecognizer!
private var maskLayer: CALayer!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
self.isUserInteractionEnabled = true
recognizer = UIPanGestureRecognizer(target: self, action: #selector(swipeGesture))
self.addGestureRecognizer(recognizer)
maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.black.withAlphaComponent(0.6).cgColor
maskLayer.frame = self.bounds
self.layer.insertSublayer(maskLayer, at: 0)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func crateMask(location: CGPoint, layer: CALayer) {
let maskFrame = CGRect(origin: CGPoint(x: location.x - 25 , y: location.y - 25 ), size: CGSize(width: 50, height: 50))
layer.createMask(maskRect: maskFrame, invert: true)
}
#objc func swipeGesture(sender: UIPanGestureRecognizer) {
let location = sender.location(in: self)
print(location)
crateMask(location: location, layer: self.maskLayer )
}
}
extension CALayer {
func createMask(maskRect: CGRect, invert: Bool = false) {
print(maskRect)
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(self.bounds)
}
let cornerRadius = maskRect.height / 2
path.addRoundedRect(in: maskRect, cornerWidth: cornerRadius, cornerHeight: cornerRadius)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
self.mask = maskLayer
}
}
Here is the screen shot of the UI: https://prnt.sc/ozki6s
Any help will be appreciated! Thanks in advance.
I am trying to add a UITextView to my view. I have this class:
import UIKit
class TextAnnotation: UITextView {
var lastLocation:CGPoint?
var panRecognizer:UIPanGestureRecognizer?
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
self.panRecognizer = UIPanGestureRecognizer(target:self, action: #selector(StampAnnotation.detectPan))
self.gestureRecognizers = [panRecognizer!]
self.text = "Text Field"
self.backgroundColor = UIColor.brownColor()
self.userInteractionEnabled = true
self.editable = true
self.frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: 200, height: 50)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func detectPan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self.superview!)
self.center = CGPointMake(lastLocation!.x + translation.x, lastLocation!.y + translation.y)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
// Promote the touched view
self.superview?.bringSubviewToFront(self)
// Remember original location
lastLocation = self.center
}
}
The background color will change to brown, but the text does not get added and when I tap on the UITextView nothing happens when I set to editable, what am I doing wrong?
Here is how I am calling this class:
let textCenter = touches.first!.locationInView(view)
let textView = TextAnnotation(frame: CGRect(x: textCenter.x, y: textCenter.y, width: 200, height: 50), textContainer: NSTextContainer())
view.addSubview(textView)
You are the one who has wantonly stripped off the real gesture recognizers of this text view and replaced them with something else, effectively crippling the text view. Don't do that. Don't mess with the gesture recognizers of a text view! If interaction is enabled and editable is true, this text view needs to be able to do its own work.