How do I add NSButtons from the top instead of from the bottom? - swift

I am currently building a navigation left pane where buttons from the side are added dynamically. I have a NSView subclass that facilitates this
func addButton(_ service: String) {
let btn = NSButton()
btn.bezelStyle = .shadowlessSquare
btn.frame.size = NSSize(width: 48, height: 48)
btn.image = NSImage(named: service)
btn.imageScaling = .scaleAxesIndependently
btn.frame.origin = CGPoint(x: 0, y: self.currentY)
self.addSubview(btn)
self.currentY += 58
}
It works as desired but buttons are added from the bottom instead of the top:
Resulting App Screenshot
How do I make it so that new buttons are added from the top?
For reference, here's my storyboard with the "Servi" view containing the buttons.
Left Pane Storyboard

Y goes from bottom to top on macOS (the bottom is 0). So don't start with currentY at 0 or 10 or whatever you're starting with. Start with currentY set to one button height less than the view height, and then subtract 58 each time instead of adding.
So, you're getting something like this:
But if you do what I'm describing, you'll get something like this:

You could flip your view so that the coordinate system starts at the top left instead. Just create a new NSView subclass and override isFlipped
class FlippedView: NSView {
override var flipped:Bool {
get {
return true
}
}
}
Then add the new buttons to an instance of flippedview instead.

Related

How I can show/hide view inside another view with height changing of root swift?

I have two views one inside another, it looks like picture below:
when I press on orange arrow I would like to show/hide view above grey line and grey line too. I did it by such way:
#objc func showHide(tapGestureRecognizer: UITapGestureRecognizer)
{
let tappedImage = tapGestureRecognizer.view as! UIImageView
UIView.animate(withDuration: 0.2, animations: { () -> Void in
self.jobDataView.isHidden = !self.jobDataView.isHidden
self.view.layoutIfNeeded()
})
tappedImage.image = self.jobDataView.isHidden ? UIImage(systemName: "arrow.down"):UIImage(systemName: "arrow.up")
}
and my view above gray line can be hidden and shown. But root view doesn't change its' height and it has similar sizes before and after btn click. I tried to add constraint of height, but I it didn't solve my problem. Maybe someone knows how to solve my problem?
You need to use UIStackView either through Storyboard or from code. when you hide subview inside stackview it will automatically change stackview height. what you need to do is to make stackView distribution = fill and use vertical stack...
Hide view on button tap and when you need to show it .. add it in stack at. 0th index ..

Custom TabBarController with active circle

After reading a few articles about custom UITabBarControllers, I am left more confused than before I even started doing the research in the first place.
My goal is to create a custom TabBar with 3 important properties:
No text, just icons
The active icon is marked by a circle filled with a color behind it, and therefore needs a different icon color
Here's what I am trying to achieve:
I've been able to remove the text and center the icon by following another StackOverflow answer (Remove tab bar item text, show only image), although the solution seems like a hack to me.
How would I go about creating a circle behind the item and change the active item's color?
Also, would someone mind explaining the difference between the XCode inspector sections "Tab Bar Item" and "Bar Item", which appear directly under each other?
The first step is simple: leaving the title property of the UITabbarItem empty should hide the label.
Your second step can actually be broken down into two steps: changing the color of the icon and adding a circle behind it.
The first step here is simple again: you can set a different icon to use for the currently selected ViewController (I use Storyboards, this process is pretty straightforward). What you'd do is add a white version of the icon to be shown when that menu option is selected.
The final step is displaying the circle. To do this, we'll need the following information:
Which item is currently selected?
What is the position of the icon on the screen?
The first of these two is pretty easy to find out, but the second poses a problem: the icons in a UITabBar aren't spaced around the screen equally, so we can't just divide the width of the tabbar by the amount of items in it, and then take half of that to find the center of the icons. Instead, we will subclass UITabBarController.
Note: the tabBar property of a UITabBarController does have a .selectionIndicatorImage property. You can assign an image to this and it will be shown behind your icon. However, you can't easily control the placement of this image, and that is why we still resort to subclassing UITabBarController.
class CircledTabBarController: UITabBarController {
var circle: UIView?
override func viewDidLoad() {
super.viewDidLoad()
let numberOfItems = CGFloat(tabBar.items!.count)
let tabBarItemSize = CGSize(width: (tabBar.frame.width / numberOfItems) - 20, height: tabBar.frame.height)
circle = UIView(frame: CGRect(x: 0, y: 0, width: tabBarItemSize.height, height: tabBarItemSize.height))
circle?.backgroundColor = .darkGray
circle?.layer.cornerRadius = circle!.frame.width/2
circle?.alpha = 0
tabBar.addSubview(circle!)
tabBar.sendSubview(toBack: circle!)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let index = -(tabBar.items?.index(of: tabBar.selectedItem!)?.distance(to: 0))!
let frame = frameForTabAtIndex(index: index)
circle?.center.x = frame.origin.x + frame.width/2
circle?.alpha = 1
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let index = -(tabBar.items?.index(of: item)?.distance(to: 0))!
let frame = frameForTabAtIndex(index: index)
self.circle?.center.x = frame.origin.x + frame.width/2
}
func frameForTabAtIndex(index: Int) -> CGRect {
var frames = tabBar.subviews.compactMap { (view:UIView) -> CGRect? in
if let view = view as? UIControl {
for item in view.subviews {
if let image = item as? UIImageView {
return image.superview!.convert(image.frame, to: tabBar)
}
}
return view.frame
}
return nil
}
frames.sort { $0.origin.x < $1.origin.x }
if frames.count > index {
return frames[index]
}
return frames.last ?? CGRect.zero
}
}
Now use this subclass of UITabBarController instead of the base class.
So why this approach over simply changing the icon to a circled one? Because you can do many different things with this. I wrote an article about animating the UITabBarController in a similar manner, and if you like, you can easily use above implementation to add animation to yours too.
The easiest and actually cleanest way to do it is to design your icons and import them as images to the .xcassets folder. Then you can just set the different icons for the different states for each of the viewControllers with:
ViewController.tabBarItem = UITabBarItem(title: "", image: yourImage.withRenderingMode(.alwaysOriginal), selectedImage: yourImage)
your selected image will be the one with the circle and the image will be without. It is way easier than manipulating the images in xcode and it is also less expensive since the compiler only has to render the images and doesn't have to manipulate them.
About the other question UIBarItem is
An abstract superclass for items that can be added to a bar that appears at the bottom of the screen.
UITabBarItem is a subclass of UIBarItem to provide extra funtionality.

UIButton action is not triggered after constraint layouts changed

I have got an UIButton on a storyboard ViewController. When I load data into the form and the layout is significantly changing the button does not recognise the touch action.
I have figured out that when button is visible on the scrollview right after it if filled with data, the touch action works.
If the data too long and the button is not visible at first, just when it is scrolled into the display, the touch action does not work.
I was checking if something is above the button, but nothing. I have tried to change the zPosition of the button, not solved the problem.
What can be the issue?
I have made custom classes from the UIScrollView and the UIButton to check how the touches event triggered. It is showing the same behaviour, which is obvious. If the button is visible right at the beginning, the UIButton's touchesBegan event is triggered. If the button moves down and not visible at the beginning, it is never triggered, but the scrollview's touchesBegan is called instead.
Depending on the size of the data I load into the page sometimes the button is visible at the beginning, but the form can be still scrolled a bit. In this case the button still work, so it seems that this behaviour is not depending on if the scrollview is scrolled before or not, just on the initial visibility of the button.
Is there any layout or display refresh function which should be called to set back the behaviour to the button?
The code portion which ensures that the contentview is resized for the scroll if the filled data requires bigger space.
func fillFormWithData() {
dispDescription.text = jSonData[0]["advdescription"]
dispLongDescription.text = jSonData[0]["advlongdesc"]
priceandcurrency.text = jSonData[0]["advprice"]! + " " + jSonData[0]["advpricecur"]!
validitydate.text = jSonData[0]["advdate"]!
contentview.layoutIfNeeded()
let contentRect = CGRect(x: 0, y: 0, width: scrollview.frame.width, height: uzenetbutton.frame.origin.y+uzenetbutton.frame.height+50)
contentview.frame.size.height = contentRect.size.height
scrollview.contentSize = contentview.bounds.size
}
Ok, so another update. I have coloured the contentview background to blue and the scrollview background to white. When I load the data and resize the layout constraints, the contentview is resizing as expected, however now the scrollview is going to the bottom. After I scroll the view it is resizing to the original size which fits the screen. Now the button is only recognised when I touch the are which is blue behind. With the white background it is not recognised anymore, so it seems that the scrollview is hiding the button.
Let me get this clear the button is added in storyboard and it is a spritekit project?? If you are using zPosition?? Why don’t u connect the UIButton via the assistant editor as an IBAction then the action is always tied to the button.
You can also do it differently
Create an SKLabelNode and put it on the screen where you want to have the button and then set a name to it as myButton
override func touchesBegan(_ touches: Set<UITouch>, with event:
UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let tappedNodes = nodes(at: location)
for node in tappedNodes {
if node.name == "myButton" {
// call your action here
}
}
}
}
EDIT 1:
You could also try auto resizing your scrollView.content this works also if you are adding any views via the app or programmatically
private func resizeScrollView(){
print("RESIZING THE SCROLLVIEW from \(scrollView.contentSize)")
for view in scrollView.subviews {
contentRect = contentRect.union(view.frame)
}
scrollView.contentSize = CGSize(width: contentRect.size.width, height: contentRect.size.height + 150)
print("THE CONTENT SIZE AFTER RESIZING IS: \(scrollView.contentSize)")
}
EDIT 2: I think I found the issue with your project. You need to move the MessageButton(UzenetButton) above DispDescription label in the object inspector in that way it will always be above your message textView.
At the moment the UzeneButton is at the very far back in your view hierarchy so if your textView is resizing whilst editing it covers the button that is why you cannot click on it.
See #Endre Olah,
To make situation more clear do one more thing, set clipToBound property of contentview to true.
you will notice that after loading of data your button not fully visible, it means it is shifting out of bound of its parentView (ContentView)
And that's why button is not taking your touch. However, if you carefully touch upper part of button it still do its job. Because upper part is still in bound of ContentView
Solution :
After loading of data you have to make sure that you increase height of ContentView such that button should never go out of bound of its parentView(ContentView).
FOR EXAMPLE
#IBOutlet var heightConstraintOfContentView : NSLayoutConstraint!
After loading of data
let contentRect = CGRect(x: 0, y: 0, width: scrollview.frame.width, height: uzenetbutton.frame.origin.y+uzenetbutton.frame.height+50)
heightConstraintOfContentView.constant = contentRect.size.height
contentView.layoutIfNeeded()
I use following steps when I need to use scrollview with dynamic content:
1) Firstly add a scrollView with top, bottom, trailing and leading is 0 to super view.
2) Add a view to scrollView and view's trailing, leading bottom and top space to scrollView can be set to 0 (or you can add margin optionally).
3) Now, you should add UI elements like buttons, labels with appropriate top, bottom, trailing and leading margins to each other.
4) Lastly, add equal height and equal width constraint to view with Safe Area:
and change equal height priority of view to 250:
It should solve your problem with UIScrollView.
Finally, I have found the solution in another chain, once it became clear that the scrollview's contentview is resizing on scroll event to the original size. (Not clear why this is like this, but that is the fact.)
So I had to add a height constraint to the contentview in the storyboard and create an outlet to it and adjust this constraint when the content size is changing, like this:
#IBOutlet weak var ContentViewHeight: NSLayoutConstraint!
func fillFormWithData() {
dispDescription.text = jSonData[0]["advdescription"]
dispLongDescription.text = jSonData[0]["advlongdesc"]
priceandcurrency.text = jSonData[0]["advprice"]! + " " + jSonData[0]["advpricecur"]!
validitydate.text = jSonData[0]["advdate"]!
contentview.layoutIfNeeded()
let contentRect = CGRect(x: 0, y: 0, width: scrollview.frame.width, height: uzenetbutton.frame.origin.y+uzenetbutton.frame.height+50)
contentview.bounds = contentRect
scrollview.contentSize = contentRect.size
----------- This is the key line to the success ----------
ContentViewHeight.constant = contentRect.size.height
----------------------------------------------------------
}
After this is added, it works perfectly.

Change tab in tab bar controller using finger swipe

I have three tabs in my tabbar controller and I want to switch between these tabs just like tinder switches the tab using finger touch. I have done it using UISwipeGestureRecognizer but its not exactly same as that of Tinder (the dating app ) swiping.
I have added UISwipeGestureRecognizer on one of the Tabbar controller and then added the function to change the tabbar selected index. But the animations is not controlled by finger touch. I want the swiping should be controlled by finger touch.
I think the best way to do it is to put all your tab views in a UIScrollView. You place them next to each other.
Implement the scroll view delegate methods in your tabbarController.
You'll probably need scrollViewDidEndScrollingAnimation and scrollViewDidEndDecelerating to know on which view you are when the user stops scrolling, like this:
let page_width=UIScreen.main.bounds.width
let page=Int(floor((scrollView.contentOffset.x-page_width/2)/page_width)+1)
Here, I assume each of your tab view is the same size as the screen.
I am bit late but I found my ans -
I have created 4 UIviewcontrollers programatically and then
created an array of it.
var views = [CareTeamTableViewController(),VFCChatQViewController(), NewAccountViewController(), ShareViewController()]
Then I added a scrollview in my main UiViewController
private func initMainScroll() {
scrollView = UIScrollView.init()
scrollView?.delegate = self
scrollView?.showsHorizontalScrollIndicator = false
scrollView?.isPagingEnabled = true
self.view.addSubview(scrollView!)
}
and then added the views array like :
func setupScrollView(complete:()->()) {
scrollView?.frame = views.first!.view.frame
scrollView?.contentSize = CGSize(width: CGFloat(views.count) * UIScreen.main.bounds.width, height: 0)
_ = views.map({ addViewToScrollView($0) })
_ = views.map({ $0.view.frame.origin = CGPoint(x: CGFloat(views.index(of: $0)!) * UIScreen.main.bounds.width, y: 0) })
complete()
}
func addViewToScrollView(_ viewController: UIViewController) {
scrollView?.addSubview(viewController.view)
viewController.willMove(toParentViewController: self)
addChildViewController(viewController)
}

How to add NSTextFields to NSScrollView

I want to add multiple NSTextFields to NSScrollView dynamically.
Code:
func addLabel(data: String)
{
let offset = labelCount*20
let label = NSTextField(frame: NSMakeRect(0, CGFloat(offset), 200, 20))
label.stringValue = data
scrollView.documentView?.addSubview(label)
labelCount += 1
// scrollView is IBOutlet
// labelCount is var to keep record of number of labels
}
Problem:
It adds the NSTextFields correctly to the NSScrollView but they are only added till the visible part is available. After the visible part of NSScrollView is full, no more NSTextFields are added to it.
I need to add NSTextFields even though the visible part is full and scroll to find others.
How can I do it?
It's something like the following.
scrollView.documentView?.setFrameSize(CGSize(width: CGFloat(labelCount) * 200, height: 20))