Code for the Custom UIView:
Please check the video too here: https://drive.google.com/open?id=1kbrOxXWcJIi4vkiqMNer3exBr5cOWgDz
import UIKit
protocol PostAttachmentFullScreenViewDelegate: class {
func closeAttachmentFullView()
}
class PostAttachmentFullScreenView: UIView {
weak var delegate: PostAttachmentFullScreenViewDelegate?
#IBOutlet var backgroundView: UIImageView!
#IBOutlet var closeButton: UIButton!
#IBAction func closeViewAction(_ sender: Any) {
print("will call delegate to put it off")
self.delegate?.closeAttachmentFullView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let _ = commonInitialization()
backgroundView.image = UIImage(named: "ScrollImageTop1")
closeButton.isUserInteractionEnabled = true
}
override init(frame: CGRect) {
super.init(frame: frame)
let _ = commonInitialization()
backgroundView.image = UIImage(named: "ScrollImageTop1")
closeButton.isUserInteractionEnabled = true
}
func commonInitialization() -> UIView
{
let bundle = Bundle.init(for: type(of: self))
let nib = UINib(nibName: "PostAttachmentFullScreenView", bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
view.frame = bounds
view.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
addSubview(view)
return view
}
}
usage in ViewController (I am defining an instance of the custom view and putting it inside the Scroll View):
var frame = CGRect(x:0, y:0, width:0, height:0)
let blue = PostAttachmentFullScreenView()
blue.delegate = self
blue.isUserInteractionEnabled = true
blue.backgroundColor = UIColor.blue
blue.backgroundView.image = fileAttachments[1]
frame.origin.x = attachmentsScrollView.frame.size.width * CGFloat (0)
frame.size = attachmentsScrollView.frame.size
blue.frame = frame
attachmentsScrollView.addSubview(blue)
extension NewPostViewController : PostAttachmentFullScreenViewDelegate
{
func closeAttachmentFullView() {
print("hiding attachments view")
attachmentSuperView.isHidden = true
}
}
To my surprise it doesn't even print - "will call delegate to put it off".
I am not able to understand what's wrong here. Please help me understand the issue and correct it. Thank you.
You are mixing programmatic approach and xib approach.
As you have added IBOultet and IBAction that means you are using xib for the UIView.
In that scenario you have to load the UIView xib when initialising the view.
Add an extension for UIView in your project:
extension UIView {
class func fromNib<T: UIView>() -> T {
return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
}
}
when you are initialising your view add it like this :
let blue : PostAttachmentFullScreenView = UIView.fromNib()
blue.delegate = self
blue.isUserInteractionEnabled = true
blue.backgroundColor = UIColor.blue
blue.backgroundView.image = fileAttachments[1]
frame.origin.x = attachmentsScrollView.frame.size.width * CGFloat (0)
frame.size = attachmentsScrollView.frame.size
blue.frame = frame
attachmentsScrollView.addSubview(blue)
and the delegate and button action methods will work.
you missed this :
You never set the target/action on your button. Somewhere you need to call addTarget(_:action:for:) to set the target/action on the button. Also, what connects the button to your PostAttachmentFullScreenView as an outlet?
This might be an obvious one but for me (Xcode 10.1) adding all missing UI constraints to the UIButton in question (at least 4 constraints) fixed the error for me in my custom view:
Make sure you add enough constraints (typically 4 constraints) or enough to have all warnings regarding missing constraints removed. After doing this and attaching the button with Ctrl + drag from View to corresponding swift code, the click was being detected and working properly.
Hope this helps.
Related
Swift 5/Xcode 12.4
I created an xib file for my custom MarkerView - the layout's pretty simple:
- View
-- StackView
--- DotLabel
--- NameLabel
View and StackView are both set to "User Interaction Enabled" in the inspector by default. DotLabel and NameLabel aren't but ticking their boxes doesn't seem to actually change anything.
At runtime I create MarkerViews (for testing purposes only one atm) in my ViewController and add them to a ScrollView that already contains an image (this works):
override func viewDidAppear(_ animated: Bool) {
createMarkers()
setUpMarkers()
}
private func createMarkers() {
let marker = MarkerView()
marker.setUp("Some Text")
markers.append(marker)
scrollView.addSubview(marker)
marker.alpha = 0.0
}
private func setUpMarkers() {
for (i,m) in markers.enumerated() {
m.frame.origin = CGPoint(x: (i+1)*100,y: (i+1)*100)
m.alpha = 1.0
}
}
These MarkerViews should be clickable, so I added a UITapGestureRecognizer but the linked function is never called. This is my full MarkerView:
class MarkerView: UIView {
#IBOutlet weak var stackView: UIStackView!
#IBOutlet weak var dotLabel: UILabel!
#IBOutlet weak var nameLabel: UILabel!
let nibName = "MarkerView"
var contentView:UIView?
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
contentView = view
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickedMarker))
self.addGestureRecognizer(gesture)
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
func setUp(_ name:String) {
nameLabel.text = name
nameLabel.sizeToFit()
stackView.sizeToFit()
}
#objc private func clickedMarker() {
print("Clicked Marker!")
}
}
Other questions recommend adding self.isUserInteractionEnabled = true before the gesture recognizer is added. I even enabled it for the StackView and both labels and also tried to add the gesture recognizer to the ScrollView but none of it helped.
Why is the gesture recognizer not working and how do I fix the problem?
Edit: With Sweeper's suggestion my code now looks like this:
class MarkerView: UIView, UIGestureRecognizerDelegate {
.....
func commonInit() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickedMarker))
gesture.delegate = self
self.isUserInteractionEnabled = true
self.addGestureRecognizer(gesture)
self.addSubview(view)
contentView = view
}
.....
func gestureRecognizer(_: UIGestureRecognizer, _ otherGestureRecognizer: UIGestureRecognizer) -> Bool {
print("gestureRecognizer")
return true
}
}
"gestureRecognizer" is never printed to console.
One of the comments below the accepted answer for the question #Sweeper linked gives a good hint:
What is the size of the content view (log it)?
print(self.frame.size)
print(contentView.frame.size)
both printed (0.0, 0.0) in my app. The UITapGestureRecognizer is attached to self, so even if it worked, there was simply no area that you could tap in for it to recognize the tap.
Solution: Set the size of the UIView the UITapGestureRecognizer is attached to:
stackView.layoutIfNeeded()
stackView.sizeToFit()
self.layoutIfNeeded()
self.sizeToFit()
didn't work for me, the size was still (0.0, 0.0) afterwards. Instead I set the size of self directly:
self.frame.size = stackView.frame.size
This also sets the size of the contentView (because it's the MarkerView's child) but of course requires the size of stackView to be set properly too. You could add up the childrens' widths/heights (including spacing/margins) yourself or simply call:
stackView.layoutIfNeeded()
stackView.sizeToFit()
The finished code:
func setUp(_ name:String) {
nameLabel.text = name
//nameLabel.sizeToFit() //Not needed anymore
stackView.layoutIfNeeded() //Makes sure that the labels already use the proper size after updating the text
stackView.sizeToFit()
self.frame.size = stackView.frame.size
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.onClickStackView))
self.addGestureRecognizer(gesture)
}
Specifics:
There's no need for a delegate and overriding func gestureRecognizer(_: UIGestureRecognizer, _ otherGestureRecognizer: UIGestureRecognizer) -> Bool to always return true, which might be an alternative but I didn't manage to get that version to work - the function was never called. If someone knows how to do it, please feel free to post it as an alternative solution.
Attaching the UITapGestureRecognizer has to be done once the parent view knows its size, which it doesn't in commonInit because the text of the labels and the size of everything isn't set there yet. It's possible to add more text after the recognizer is already attached but everything that exceeds the original length (at the time the recognizer was attached) won't be clickable if using the above code.
The parent view's background color is used (the stackView's is ignored) but it's possible to simply set it to a transparent color.
Attaching the gesture recognizer to stackView instead of self works the same way. You can even use a UILabel but their "User Interaction Enabled" is off by default - enable it first, either in the "Attributes Inspector" (tick box in the "View" section) or in code (myLabel.isUserInteractionEnabled = true).
I have a Controller with these items
In stack view I add one or many xib file as UiView dynamically, this is my xib file Code:
class PassengerInfoItem: UIView {
#IBOutlet weak var View_Content: UIView!
#IBOutlet weak var Label_AddPassenger: UILabel!
override init(frame: CGRect)
{
super.init(frame: frame)
self.commenInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commenInit()
}
func commenInit()
{
Bundle.main.loadNibNamed("PassengerInfoItem", owner: self, options: nil)
self.View_Content.frame = self.bounds
self.View_Content.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(self.View_Content)
}}
and this is my controller code:
for i in 1...(UserSearchModel.Passengers.AdultNumber)!
{
let passengerInfoItem = PassengerInfoItem()
passengerInfoItem.tag = i
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTapGesture_PassengerInfo))
tapGestureRecognizer.delegate = self
passengerInfoItem.addGestureRecognizer(tapGestureRecognizer)
self.StackView_AddPassengerInfo.addArrangedSubview(passengerInfoItem)
}
In stack view all passengerInfoItem added successfully but when touch one of those nothing happen and my problem is UITapGestureRecognizer do not work correctly , I checked all isUserInteractionEnabled = true for back View, Scroll View, Card, Stack View but UITapGesture not work
Because you add another instance here
Bundle.main.loadNibNamed("PassengerInfoItem", owner: self, options: nil)
self.View_Content.frame = self.bounds
self.View_Content.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(self.View_Content)
so the gesture is applied to the back-most view , you may need
class func getInstance() -> PassengerInfoItem {
let vv = Bundle.main.loadNibNamed("PassengerInfoItem", owner: self, options: nil)?.first! as! PassengerInfoItem
return vv
}
Then replace
let passengerInfoItem = PassengerInfoItem()
with
let passengerInfoItem = PassengerInfoItem.getInstance()
I have 1 MainViewController from storyboard and 1 ModalUIView from xib.
In ModalUIView has present function for displaying modal and dismiss function for closing modal.
Step:
MainViewController -> OpenModal -> ModalUIView -> CloseModal
Here are my code:
UIViewUtil.swift
import Foundation
import UIKit
extension UIView {
// Load xib as the same name of CustomView that want to use xib
func loadXib() -> UIView{
let bundle = Bundle(for: type(of: self))
let nibName = type(of: self).description().components(separatedBy: ".").last!
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as! UIView
}
}
MainViewController.swift is subclass of UIViewController
#IBAction func guideButton(_ sender: Any) {
let modal = ModalUIView()
modal.present(targetView: self.view)
}
ModalUIView.swift is subclass of UIView
var view : UIView?
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
setup()
}
func setup() {
view = loadXib()
}
func present(targetView: UIView) {
view!.layer.cornerRadius = 10.0
view!.clipsToBounds = true
targetView.addSubview(view!)
// Set size
let popupWidth: CGFloat = targetView.frame.width - (targetView.frame.width * 0.04)
let popupHeight: CGFloat = targetView.frame.height - (targetView.frame.height * 0.08)
view!.frame = CGRect(x: targetView.frame.origin.x, y: targetView.frame.origin.y,
width: popupWidth, height: popupHeight)
view!.center = targetView.center
view!.transform = CGAffineTransform.init(scaleX: 1.3, y: 1.3)
view!.alpha = 0
UIView.animate(withDuration: 0.4){
self.view!.alpha = 1
self.view!.transform = CGAffineTransform.identity
}
}
#objc func dismiss(sender: UIButton!) {
print("dismiss")
}
My problem is mainViewController when I called present of modalUIView and then I tab on closeButton in modalUIView is not fired the action
I try with #IBAction but it not work:
#IBAction func CloseButtonAction(_ sender: UIButton) {
}
I also try with manual add action by programmatically but it not work too:
let closeButton: UIButton? = view?.viewWithTag(10) as! UIButton
closeButton!.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
Note:
I can see and tap on closeButton on modal.
I already add ModalUIView to xib File's Owner
OK, I have a solution now but not sure that is the best answer.
My solution is to add an action to closeButton of modalUIView via mainViewController not in modalUIView
If anyone have another best solution than this please suggest me.
You can also get an action in your viewcontroller following.
yourSubView.button.addTarget(self, action: #selector(buttonPress(sender:)), for: .touchUpInside)
and it will call your method.
func buttonPress(sender:UIButton){
print("Button pressed")
}
I am trying to build (programmatically) a new view and use IBDesignable attribute to simplify this process and to show views in storyboard instead of white rectangles.
Here is a class with two subviews: UILabel and UIImageView. I am adding them dynamically to the parent view and set a couple of constraints for them:
import UIKit
#IBDesignable
class ChoiceView: UIView {
enum ChoiceState {
case empty, chosen
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupViewForState(.empty)
}
override init(frame: CGRect) {
super.init(frame: frame)
setupViewForState(.empty)
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func layoutSubviews() {
super.layoutSubviews()
}
private func setupViewForState(_ state: ChoiceState) {
guard case .empty = state else { return } // single case for now
let placeholder = UILabel()
let choiceImage = UIImageView(image: UIImage(named: "plus_small"))
placeholder.text = "None"
placeholder.textAlignment = .right
choiceImage.contentMode = .center
let constraintFormats = [
"H:|-0-[placeholder]-10-[choice(50)]-0-|",
"V:|-0-[placeholder]-0-|",
"V:|-0-[choice]-0-|"
]
let views = ["placeholder": placeholder, "choice": choiceImage]
translatesAutoresizingMaskIntoConstraints = false
placeholder.translatesAutoresizingMaskIntoConstraints = false
choiceImage.translatesAutoresizingMaskIntoConstraints = false
addSubview(placeholder)
addSubview(choiceImage)
let constraints = constraintFormats.flatMap {
NSLayoutConstraint.constraints(
withVisualFormat: $0,
options: .directionLeadingToTrailing,
metrics: nil,
views: views)
}
NSLayoutConstraint.activate(constraints)
}
}
These are not perfect yet, but at least - shown in simulator:
But when I reload views in storyboard builder, I see this:
As you can see, the constraints are not enforced in storyboard and the image is not shown. Do you know how to fix that? Or, is it even possible to get the same picture in both storyboard and simulator?
Solution: Please look at #DonMag answer. It allows to see result I was expecting (see picture attached).
Do not set translatesAutoresizingMaskIntoConstraints = false for your Designable view itself.
let views = ["placeholder": placeholder, "choice": choiceImage]
// don't do this one
//translatesAutoresizingMaskIntoConstraints = false
placeholder.translatesAutoresizingMaskIntoConstraints = false
choiceImage.translatesAutoresizingMaskIntoConstraints = false
addSubview(placeholder)
addSubview(choiceImage)
Also, to see your "choice" image in IB:
let theBundle = Bundle(for: type(of: self))
let choiceImage = UIImageView(image: UIImage(named: "plus_small", in: theBundle, compatibleWith: self.traitCollection))
//
//let choiceImage = UIImageView(image: UIImage(named: "plus_small"))
...
I may be doing something really stupid, but I don't seem to be able to use Interface Builder to connect IBOutlet variables to custom views, but only in Swift.
I've created a class called MyView, which extends from UIView. In my controller, I've got a MyView variable (declared as #IBOutlet var newView: MyView). I go into IB and drag a UIView onto the window and give it a class of MyView.
Whenever I've done similar in Objective C, I'm then able to click on the View Controller button at the top of the app window, select the variable and drag it down to the control to link the two together. When I try it in Swift, it refuses to recognise that the view is there.
If I change the class of the variable in the controller to UIView, it works fine. But not with my custom view.
Has anyone else got this problem? And is it a feature, or just my idiocy?
Code for Controller
import UIKit
class ViewController: UIViewController {
#IBOutlet var newView:MyView
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.
}
}
Code for view
import UIKit
class MyView: UIView {
init(frame: CGRect) {
super.init(frame: frame)
// Initialization code
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect)
{
// Drawing code
}
*/
}
I've had a similar problem, and I think it's partially a caching issue and partially just an Xcode6/Swift issue. The first step I found was required was to make sure that the view controller .swift file would be loaded in the Assistant Editor when choosing "automatic".
With Xcode finding that both the files are linked I could sometimes control-drag from the view/button/etc. from the IB to the .swift file, but often had to drag from the empty circle in the gutter of the #IBOutlet var newView:MyView line to the view I wanted it to match up to.
If you can't get the file to load in the Assistant Editor then I found that doing the following would often work:
Remove the custom class from the IB view
Clean the project (cmd + K)
Close/reopen Xcode
Possibly clean again?
Add the custom class back to the view
Hope it works :)
If that seems to get you half way/nowhere add a comment and I'll see if it triggers anything else I did
In my case import UIKit was missing, after adding this line I could create an IBOutlet from Storyboard again.
I've had a similar problem to the one described in this thread. Maybe you found a solution maybe not but anybody who encounters this in the future. I've found the key is to use the "required init" function as follows:
required init(coder aDecoder: NSCoder) {
print("DrawerView: required init")
super.init(coder: aDecoder)!
screenSize = UIScreen.mainScreen().bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
self.userInteractionEnabled = true
addCustomGestureRecognizer()
}
This is the complete class of my custom view:
import UIKit
import Foundation
class DrawerView: UIView {
var screenSize: CGRect!
var screenWidth: CGFloat!
var screenHeight: CGFloat!
var drawerState: Int = 0
override init (frame : CGRect) {
print("DrawerView: main init")
super.init(frame : frame)
}
override func layoutSubviews() {
print("DrawerView: layoutSubviews")
super.layoutSubviews()
}
convenience init () {
self.init(frame:CGRect.zero)
}
required init(coder aDecoder: NSCoder) {
print("DrawerView: required init")
super.init(coder: aDecoder)!
screenSize = UIScreen.mainScreen().bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
self.userInteractionEnabled = true
addCustomGestureRecognizer()
}
func addCustomGestureRecognizer (){
print("DrawerView: addCustomGestureRecognizer")
let swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(self.handleDrawerSwipeGesture(_:)))
swipeDown.direction = UISwipeGestureRecognizerDirection.Down
self.addGestureRecognizer(swipeDown)
let swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(self.handleDrawerSwipeGesture(_:)))
swipeUp.direction = UISwipeGestureRecognizerDirection.Up
self.addGestureRecognizer(swipeUp)
print("DrawerView self: \(self)")
}
func minimizeDrawer(){
UIView.animateWithDuration(0.25, delay: 0.0, options: .CurveEaseOut, animations: {
// let height = self.bookButton.frame.size.height
// let newPosY = (self.screenHeight-64)*0.89
// print("newPosY: \(newPosY)")
self.setY(self.screenHeight*0.86)
}, completion: { finished in
self.drawerState = 0
for view in self.subviews {
if let _ = view as? UIButton {
let currentButton = view as! UIButton
currentButton.highlighted = false
} else if let _ = view as? UILabel {
let currentButton = view as! UILabel
if self.tag == 99 {
currentButton.text = "hisotry"
} else if self.tag == 999 {
currentButton.text = "results"
}
}
}
})
}
func handleDrawerSwipeGesture(gesture: UIGestureRecognizer) {
print("handleDrawerSwipeGesture: \(self.drawerState)")
if let swipeGesture = gesture as? UISwipeGestureRecognizer {
switch self.drawerState{
case 0:
if swipeGesture.direction == UISwipeGestureRecognizerDirection.Down {
// nothing to be done, mini and swiping down
print("mini: !")
} else {
// mini and swiping up, should go to underneath city box
UIView.animateWithDuration(0.25, delay: 0.0, options: .CurveEaseOut, animations: {
let toYPos:CGFloat = 128 + 64 + 8
self.setY(toYPos)
}, completion: { finished in
self.drawerState = 1
for view in self.subviews {
if let _ = view as? UIButton {
let currentButton = view as! UIButton
currentButton.highlighted = true
} else if let _ = view as? UILabel {
let currentLabel = view as! UILabel
currentLabel.text = "close"
}
}
})
}
break;
case 1:
if swipeGesture.direction == UISwipeGestureRecognizerDirection.Down {
// open and swiping down
self.minimizeDrawer()
} else {
// open and swiping up, nothing to be done
}
break;
default:
break;
}
}
}
}
Hope this helps...