Passing UIView control events through UIView extension - ios

This might be a weird question, but i'm trying to code like a pro which obviously i am not.
Right now i have an extension which uses UIView and my concept is making it like an alert
For example, i coded the following:
extension UIView {
typealias completionHandler = (_ success:Bool) -> Void
private var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
public func showLuna(title messageTitle:String, message messageDescription:String, dissmiss dissmissDuration: TimeInterval) {
let luna = UIView()
luna.frame = CGRect(x: 16, y: 30, width: screenWidth - 30, height: 60)
luna.center.x = self.center.x
luna.backgroundColor = .white
luna.addShadow(radius: 11, opacity: 0.2)
luna.layer.cornerRadius = 10
}
}
And on my other ViewController, I use this to present Luna
#IBAction func presentLuna(_ sender: Any) {
self.view.showLuna(title: "Oooh", message: "Oops, something went horribly wrong!", dissmiss: 2.5);
}
At this very specific moment, I've been digging StackOverFlow for a day to find an answer. How do i attach a gesture recognizer or a function WITH a code block so the user can perform another task when luna gets tapped, or is that even possible with Extensions??

This is maybe what #rmaddy means by subclassing UIView:
First, create a subview so you can use it to respond to touch events:
class LunaView: UIView {
typealias LunaViewCompletionBlock = (_ isSuccessful: Bool) -> Void
var label: UILabel
//other things you need
var completionHandler: LunaViewCompletionBlock?
init(frame: CGRect, /* other properties such as title and colour */, completionHandler: LunaViewCompletionBlock?) {
self.completionHandler = completionHandler
self.label = UILabel()
super.init(frame: frame)
isUserInteractionEnabled = true
//this is how we handle touch
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTouch)))
addSubview(label)
// set up frame or constraints(if you use autolayout) for label and other views
// configure label.text and other things
}
func handleTouch() {
completionHandler(/*true or false*/)
}
/*other methods needed in this class*/
}
Then, to use it, simply do this in the extension:
extension UIView {
private var screenWidth: CGFloat {
return UIScreen.main.bounds.width
}
public func showLuna(title messageTitle:String, message messageDescription:String, dissmiss dissmissDuration: TimeInterval, completionHandler: LunaView.LunaViewCompletionBlock) {
let luna = LunaView(frame: /*size*/, /*other things like title*/, completionHandler: completionHandler)
// set luna's center, shadow, auto-dismiss time, colour.....
}
}

Answering your question:
How do i attach a gesture recognizer or a function WITH a code block so the user can perform another task when luna gets tapped, or is that even possible with Extensions??
You can't detect a touch event on just a general UIView you need a UIControl. Since the UIControl type inherits from UIView you will be able to do any of your typical drawing or view hierarchy stuff but you can also setup touch actions and callbacks using addTarget(:action:for:) or other UIControl mechanisms.

Related

iOS 14 Context Menu from UIView (Not from UIButton or UIBarButtonItem)

There is an easy way to present a context menu in iOS 13/14 via UIContextMenuInteraction:
anyUIView.addInteraction(UIContextMenuInteraction(delegate: self))
The problem for me with this is that it blurs out the whole user interface. Also, this only gets invoked via a long-press/Haptic Touch.
If I do not want the blur, there are action menus. As shown here
https://developer.apple.com/documentation/uikit/menus_and_shortcuts/adopting_menus_and_uiactions_in_your_user_interface
This seems to present without a blur, yet it only seems to attach to a UIButton or a UIBarButtonItem.
let infoButton = UIButton()
infoButton.showsMenuAsPrimaryAction = true
infoButton.menu = UIMenu(options: .displayInline, children: [])
infoButton.addAction(UIAction { [weak infoButton] (action) in
infoButton?.menu = infoButton?.menu?.replacingChildren([new items go here...])
}, for: .menuActionTriggered)
Is there a way to attach a context menu to a UIView that invokes on long press and does not present with blur?
After some experimentation I was able to remove the dimming blur, like this. You will need a utility method:
extension UIView {
func subviews<T:UIView>(ofType WhatType:T.Type,
recursing:Bool = true) -> [T] {
var result = self.subviews.compactMap {$0 as? T}
guard recursing else { return result }
for sub in self.subviews {
result.append(contentsOf: sub.subviews(ofType:WhatType))
}
return result
}
}
Now we use a context menu interaction delegate method to find the UIVisualEffectView that is responsible for the blurring and eliminate it:
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
DispatchQueue.main.async {
let v = self.view.window!.subviews(ofType:UIVisualEffectView.self)
if let v = v.first {
v.alpha = 0
}
}
}
Typical result:
Unfortunately there is now zero shadow at all behind the menu, but it's better than the big blur.
And of course it’s still a long press gesture. I doubt anything can be done about that! If this were a normal UILongPressGestureRecognizer you could probably locate it and shorten its minimumPressDuration, but it isn't; you have to subject yourself to the UIContextMenuInteraction rules of the road.
However, having said all that, I can think of a much better way to do this, if possible: make this UIView be a UIControl! Now it behaves like a UIControl. So for example:
class MyControl : UIControl {
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
let config = UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in
let act = UIAction(title: "Red") { action in }
let act2 = UIAction(title: "Green") { action in }
let act3 = UIAction(title: "Blue") { action in }
let men = UIMenu(children: [act, act2, act3])
return men
})
return config
}
}
And:
let v = MyControl()
v.isContextMenuInteractionEnabled = true
v.showsMenuAsPrimaryAction = true
v.frame = CGRect(x: 100, y: 100, width: 200, height: 100)
v.backgroundColor = .red
self.view.addSubview(v)
And the result is that a simple tap summons the menu, which looks like this:
So if you can get away with that approach, I think it's much nicer.
I can only follow up on Matt's answer – using UIControl is much easier. Although there is no native menu property, there is an easy way how to ease the contextMenuInteraction setup, just create a subclass of UIControl and pass your menu there!
class MenuControl: UIControl {
var customMenu: UIMenu
// MARK: Initialization
init(menu: UIMenu) {
self.customMenu = menu
super.init(frame: .zero)
isContextMenuInteractionEnabled = true
showsMenuAsPrimaryAction = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: ContextMenu
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [weak self] _ in
self?.customMenu
})
}
}
Then you only need to provide UIMenu with UIActions like this:
let control = MenuControl(menu: customMenu)

Adding a UIView/UIGestureRecognizer to the presented view in a UIPresentationController

I am trying to recreate the bottom drawer functionality seen in Maps or Siri Shortcuts by using a UIPresentationController by having it recognise user input and updating the frameOfPresentedViewInContainerView accordingly. However I want this mechanism to work independently of the presented UIViewController as much as possible so I'm trying to have the presentation controller add a handle area above the view. Ideally the view of the presented controller and the handle are should both recognise user input.
This works for the presented view, however any view I add to it responds to no UIGestureRecognizer at all. Am I missing something?
class PresentationController: UIPresentationController {
private let handleArea: UIView = UIView()
override var frameOfPresentedViewInContainerView: CGRect {
// Return some frame for now
return CGRect(x: 0, y: 250, width: containerView!.frame.width, height: 500)
}
override func presentationTransitionWillBegin() {
// Unwrap presented view
guard let presentedView = self.presentedView else {
return
}
// Set color
self.handleArea.backgroundColor = UIColor.green
// Add to view hierachy
presentedView.addSubview(self.handleArea)
// Set constraints
self.handleArea.trailingAnchor.constraint(equalTo: presentedView.trailingAnchor).isActive = true
self.handleArea.leadingAnchor.constraint(equalTo: presentedView.leadingAnchor).isActive = true
self.handleArea.bottomAnchor.constraint(equalTo: presentedView.topAnchor).isActive = true
self.handleArea.heightAnchor.constraint(equalToConstant: 56).isActive = true
self.handleArea.translatesAutoresizingMaskIntoConstraints = false
// These don't help
self.handleArea.isUserInteractionEnabled = true
presentedView.isUserInteractionEnabled = true
presentedView.bringSubviewToFront(self.handleArea)
}
override func presentationTransitionDidEnd(_ completed: Bool) {
if completed {
// Add gesture recognizer
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onHandleAreaTapped(sender:)))
self.handleArea.addGestureRecognizer(tapGestureRecognizer)
}
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
// Remove subview
self.handleArea.removeFromSuperview()
}
// MARK: - Responder
#objc private func onHandleAreaTapped(sender: UITapGestureRecognizer) {
print("tap") // No output
}
}
I managed to solve it by adding both the handle area and the view of the presentedViewController to a custom view and then overriding the presentedView property and returning my custom view.

Either wait X seconds or for a user input

I'm very new to swift so even this is a bit complicated to me at this point; sorry for sounding dumb if I do
Preface:
I have a main view controller(lets call it viewA) and a UIView which gets its functionality from a .xib file, let's call this viewB
This is all in the single main view controller page
The problem:
So i want my ViewController to execute a bunch of methods in sequence one of which is to call the result from a function in this viewB(its a subview so i cant use segues)
So in the function i want to return the result only when either--
A button to be pressed
30 seconds have passed
Whats the most efficient way to tackle this problem?
EDIT:
In a nutshell i want to make my main queue execution wait till there is an input from the player or 30 seconds have passed
Code structure:
ViewController:
class ViewController{
var viewB:CustomView
//methods
function to execute{
viewB.executeFunction()
}
}
CustomView:
class CustomView:UIView{
//functions of initializing buttons and text boxes
func executeFunction(){
//wait for a user input to complete then return from this function. i cant figure out how this works
}
}
Image of the UI idea
I think Option 1 is more suitable for your scenario. You can call the block when the button in your nib is pressed. Following is the code how you can use block in swift:
// Code for your UIViewController
func presentPopup() {
let popup = NibClassName.instantiateViewFromNib()
popup.btnPressBlock = { [unowned self] (success) -> () in
// Code after button of UIView pressed
}
popup.frame = CGRect(x: 100, y: 100, width: 300, height: 300)
self.view.addSubview(popup)
}
// Code of your UIView class file
var btnPressBlock: ((Bool) -> ())?
class func instantiateViewFromNib() -> NibClassName {
let obj = Bundle.main.loadNibNamed("NibName", owner: nil, options: nil)![0] as! NibClassName
return obj
}
#IBAction func btnPressed(sender: UIButton) {
btnPressBlock!(true)
removeFromSuperview()
}

Updating UILabel text when variable changes

I've got a UIControl class and need to do some calculation based on UIImageView location which can be moved with touchesBegan and touchesMoved (everything inside this class).
Than I would like to display it as a UILabel which I've created programmatically.
class control : UIControl{
...
let leftControl: UIImageView = UIImageView(image: UIImage(named: "left-control"))
...
func leftValue() -> String{
var leftValue : String = "0.0"
leftValue = "\(leftControl.center.x)"
return leftValue
}
}
and my ViewController.swift
class ViewController: UIViewController {
let ctrl : Control = Control()
let leftLabel : UILabel = UILabel(frame: CGRect(x: 40, y: 300, width: 150, height: 30))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
ctrl.frame.origin = CGPoint(x: 40, y: 400)
leftLabel.text = "\(ctrl.leftValue())" //displays only starting value
view.addSubview(slider)
view.addSubview(leftLabel)
view.addSubview(rightLabel)
}
I know that it's inside the viewDidLoad so it's not updating properly. I was wondering about scheduledTimer but don't know if it's good solution.
You can achieve this using protocols and delegation - in the file for your Control add this :
protocol ControlDelegate: class {
func controlPositionDidChange(leftValue: String)
}
And add a weak var delegate: ControlDelegate? inside Control class.
In the file for view controller make following changes :
class ViewController: UIViewController, ControllDelegate {
let ctrl : Control = Control()
let leftLabel : UILabel = UILabel(frame: CGRect(x: 40, y: 300, width: 150, height: 30))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
ctrl.frame.origin = CGPoint(x: 40, y: 400)
ctrl.delegate = self
leftLabel.text = "\(ctrl.leftValue())" //displays only starting value
view.addSubview(slider)
view.addSubview(leftLabel)
view.addSubview(rightLabel)
}
func controlPositionDidChange(leftValue: String) {
leftLabel.text = leftValue
}
}
Now, whenever you want to inform the delegate that your control has changed the position, simply call self.delegate?.controlPositionDidChange(self.leftValue()) in appropriate places.
As usually, there is more in the docs. I highly suggest reading through them as delegation and protocols are widely used in CocoaTouch.
The answer of #Losiowaty describes the solution that most developers choose. But there are (in my opinion) much better ways to achieve it. I prefer the object oriented solution. It might look like more code, but its a much better maintainable way with more reusable code.
A real object oriented solution of your problem might look like that:
// reusable protocol set
protocol OOString: class {
var value: String { get set }
}
// reusable functional objects
final class RuntimeString: OOString {
init(initialValue: String = "") {
self.value = initialValue
}
var value: String
}
final class ViewUpdatingString: OOString {
init(_ decorated: OOString, view: UIView) {
self.decorated = decorated
self.view = view
}
var value: String {
get {
return decorated.value
}
set(newValue) {
decorated.value = newValue
view.setNeedsLayout()
}
}
private let decorated: OOString
private let view: UIView
}
// reusable ui objects
final class MyLabel : UILabel {
init(frame: CGRect, imageXCenter: OOString) {
self.imageXCenter = imageXCenter
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Not supported")
}
override func layoutSubviews() {
super.layoutSubviews()
text = imageXCenter.value
}
private let imageXCenter: OOString
}
final class MyControl : UIControl {
init(frame: CGRect, imageXCenter: OOString) {
self.imageXCenter = imageXCenter
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Not supported")
}
private let imageXCenter: OOString
private let leftControl = UIImageView(image: UIImage(named: "left-control"))
// call this at change
private func updateValue() {
imageXCenter.value = "\(leftControl.center.x)"
}
}
// non reusable business logic
final class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let dependency = RuntimeString(initialValue: "unset")
let leftLabel = MyLabel(
frame: CGRect(x: 40, y: 300, width: 150, height: 30),
imageXCenter: dependency
)
let control = MyControl(
frame: CGRect(x: 40, y: 400, width: 400, height: 400),
imageXCenter: ViewUpdatingString(dependency, view: leftLabel)
)
view.addSubview(leftLabel)
view.addSubview(control)
}
}
The main idea is to extract the dependency of both objects into another object and use a decorator then to automatically update the ui on every set value.
Note:
this approach follows the rules of object oriented coding (clean coding, elegant objects, decorator pattern, ...)
reusable classes are very simple constructed and fullfill exactly one task
classes communicate by protocols with each other
dependencies are given by dependency injection as far as possible
internal object functionality is private (loose coupling)
everything (except the business logic) is designed for reuse -> if you code like that the portfolio of reusable code grows with every day you code
the business logic of your app is more concentrated in one place (in my real coding its even outside the UIViewController)
unittesting of reusable objects is very simple when using fake implementations for the protocols (even mocking is not needed in most cases)
lesser problems with retain cycles, in most cases you do not need weak properties any more
avoiding Null, nil and Optionals (they pollute your code)
...

App Crashes when trying to make view draggable with protocol

So I wrote a protocol to make what ever UIView that conforms it draggable. However when I test this in the simulator it crashes when I try to drag the view. and displays this in the log
libc++abi.dylib: terminating with uncaught exception of type NSException
The protocol:
protocol Draggable {}
extension Draggable where Self: UIView {
func wasDragged (gestrue: UIPanGestureRecognizer) {
let translation = gestrue.translation(in: UIScreen.main.focusedView)
if let label = gestrue.view {
label.center = CGPoint(x: label.center.x + translation.x, y: label.center.y + translation.y)
}
gestrue.setTranslation(CGPoint.zero, in: UIScreen.main.focusedView)
}
func setGesture () {
let gesture = UIPanGestureRecognizer(target: UIScreen.main, action: Selector(("wasDragged:")))
self.addGestureRecognizer(gesture)
self.isUserInteractionEnabled = true
}
}
and in a custom label class I conformed it:
class DraggableLabel: UILabel, Draggable {
}
Then I called the setGesutre function in viewDidLoad of the view controller:
override func viewDidLoad() {
super.viewDidLoad()
draggableLabel.setGesture()
}
OK I admit I don't really know what I'm doing.
The action wasDragged(gesture:) needs to be accessible for message-dispatch via the Objective-C runtime. Use the #objc annotation to make a method available for message dispatch. Methods of NSObject subclasses are automatically #objc methods.
The bad news is that this will only work for Objective-C-compatible classes or extensions. Protocol extensions like yours are not compatible, so you cannot put action methods into those extensions.
Your options are to add this functionality to a subclass or a plain class extension:
extension DraggableLabel {
func wasDragged (gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: UIScreen.main.focusedView)
center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
gesture.setTranslation(CGPoint.zero, in: UIScreen.main.focusedView)
}
func setGesture () {
let gesture = UIPanGestureRecognizer(target: self,
action: #selector(wasDragged(sender:)))
self.addGestureRecognizer(gesture)
self.isUserInteractionEnabled = true
}
}
(Notice that I also changed the target of the gesture recognizer to the view instead of the main screen. Did you intend to use the responder chain to propagate the event to the right view?)
The obvious disadvantage is the reduced flexibility compared to the protocol oriented approach. If that's a problem I would look into class composition. Create a class that encapsulates the gesture recognizer and its action method. Give it a view property and configure everything when that property is set:
class DraggingBehavior: NSObject {
#IBOutlet var view: UIView? {
didSet {
guard let view = view else { return }
let gesture = UIPanGestureRecognizer(target: self, action: #selector(wasDragged(sender:)))
view.addGestureRecognizer(gesture)
view.isUserInteractionEnabled = true
}
}
func wasDragged(sender: UIGestureRecognizer) {
print("Was dragged")
// put the view translation code here.
}
}
The #IBOutlet makes this class compatible with Interface Builder. Drag in a Custom Object, set its class to DraggingBehavior, connect the view outlet to the view you would like to make draggable.

Resources