How to properly center UITabBar items vertically for UITabBar with larger height and no item titles? - swift

I'm working on a custom UITabBar that has a larger height, custom shadow and no item titles.
I am using the following code for my custom UITabBar class to achieve this like:
class CustomUITabBar: UITabBar {
#IBInspectable private var height: CGFloat = .zero
override func sizeThatFits(_ size: CGSize) -> CGSize {
guard let window = UIApplication.shared.windows.first else {
return super.sizeThatFits(size)
}
var sizeThatFits = super.sizeThatFits(size)
if #available(iOS 11.0, *) {
sizeThatFits.height = height + window.safeAreaInsets.bottom
} else {
sizeThatFits.height = height
}
return sizeThatFits
}
}
Here is the result: On iPhones that have notch (iPhone X, XS, 11, etc.) it looks fine, icons are centered properly, like so:
But on iPhones that have home button instead, tab bar items are not propery centered vertically. Is there a solution to this?

Now they are not centered as I see it on both devices. You can play around with this code:
guard let items = tabBar.items else { return }
for item in items {
item.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
it adjusts image of TabBarItem:)
With this settings they are going to be centered in each TabBarItem.

Related

Change Google's map SDK current location button possition [duplicate]

I am using google maps in my project i want to show my location button
a bit up how should i do that using swift
You can change the position of the MapControls by set the padding for your map view .
let mapView = GMSMapView()
mapView.isMyLocationEnabled = true
mapView.settings.myLocationButton = true
mapView.padding = UIEdgeInsets(top: 0, left: 0, bottom: 50, right: 50)
It will change the padding of the my location control. From bottom 50px and from right 50px
Refer the below screenshot which added the padding of 50px from bottom and right.
Update for swift 4 : just change the position of my location button only
for object in self.mapView.subviews{
if(object.theClassName == "GMSUISettingsPaddingView"){
for view in object.subviews{
if(view.theClassName == "GMSUISettingsView"){
for btn in view.subviews{
if(btn.theClassName == "GMSx_QTMButton"){
var frame = btn.frame
frame.origin.y = frame.origin.y - 100
btn.frame = frame
}
}
}
}
}
}
public extension NSObject {
public var theClassName: String {
return NSStringFromClass(type(of: self))
}
}
Like Subramanian, this works very well :
mapView.padding = UIEdgeInsets(top: 0, left: 0, bottom: 50, right: 0)
This is perfect if you have a navigation bar in your app.
Here's my function for Google Maps SDK 5.1.0, since the button's class was renamed to 'GMSx_MDCFloatingButton' in case someone needs an updated approach.
Also depending on your implementation you might want to call this on viewDidAppear().
func moveLocationButton(){
for object in self.googleMapsView!.subviews{
if(object.thisClassName == "GMSUISettingsPaddingView"){
for view in object.subviews{
if(view.thisClassName == "GMSUISettingsView"){
for btn in view.subviews{
if(btn.theClassName == "GMSx_MDCFloatingButton"){
var frame = btn.frame
frame.origin.x = frame.origin.x - 50
frame.origin.y = frame.origin.y - 150
btn.frame = frame
}
}
}
}
}
}
}
public extension NSObject {
var thisClassName: String {
return NSStringFromClass(type(of: self))
}
}
I would recommend to change button position by correcting constraint instead of changing frame.origin. I got it worked by changing frame.origin placing it into viewDidAppear only. Neither viewWillApper nor viewWillLayoutSubviews were correct methods to call from. However I managed it work perfectly calling constraint changing from viewDidLoad. And I used snapkit, but it is not a big deal to get it done natively
private func changeCurrentLocationButtonPosition(of mapView: GMSMapView) {
for object in mapView.subviews where object.theClassName == "GMSUISettingsPaddingView" {
for view in object.subviews where view.theClassName == "GMSUISettingsView" {
for btn in view.subviews where btn.theClassName == "GMSx_MDCFloatingButton" {
btn.snp.makeConstraints({ maker in
maker.left.equalToSuperview().offset(15)
})
btn.snp.makeConstraints({ maker in
maker.bottom.equalToSuperview().offset(-30)
})
return
}
}
}
}
Use this function and call this at viewDidLayoutSubviews()
func changeLocationBtnFrame(){
self.mapView.settings.myLocationButton = true
for obj in self.mapView.subviews{
if String(describing: obj.classForCoder) == "GMSUISettingsPaddingView"{
for viw in obj.subviews{
if String(describing: viw.classForCoder) == "GMSUISettingsView"{
for btn in viw.subviews{
if String(describing: btn.classForCoder) == "GMSx_MDCFloatingButton"{
var frame = btn.frame
btn.frame = CGRect(x: 316, y: 0, width: 56, height: 56)
//USE you want frame this line
break
}
}
break
}
}
break
}
}
}

How to make scrollview scroll separately to its content height in swift

I have view hierarchy like below in storyboard
here for content main constrains top = 0, leading = 0, trailing = 0, bottom = 0
here for scrollview constrains top = 0, leading = 0, trailing = 0, bottom = 0
here for View constrains top = 0, leading = 0, trailing = 0, bottom = 0
for ContentView constrains top = 0, leading = 0, trailing = 0
for TblReview constrains top = 0, leading = 0, trailing = 0, bottom = 20
for Productcollectionview constrains top = 0, leading = 0, trailing = 0, bottom = 20, height = 400
and i have Productcollectionview height outlet like below in swift file
and i don't want collectionview separate scrolling.. i want total view to scroll according to collectionview cells.. so for that i have written below code but with this code contentView and tblReview also scrolling upto productioncollectionview height i need contentView should scroll upto its content height and tblReview should scroll upto its rows
how to make scrolling separately to its height.
please help me to solve this issue
#IBOutlet weak var productCollHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
productCollectionView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
let collectionView = object as? UICollectionView
if collectionView == self.productCollectionView{
if(keyPath == "contentSize"){
if let newvalue = change?[.newKey]
{
let newsize = newvalue as! CGSize
self.productCollHeight.constant = newsize.height
}
}
}
}
#IBAction func aboutCompany(_ sender: UIButton){
self.productCollectionView.isHidden = true
self.tblReview.isHidden = true
self.contentView.isHidden = false
}
#IBAction func review(_ sender: UIButton){
self.productCollectionView.isHidden = true
self.tblReview.isHidden = false
self.contentView.isHidden = true
}
#IBAction func productOfSeller(_ sender: UIButton){
self.productCollectionView.isHidden = false
self.tblReview.isHidden = true
self.contentView.isHidden = true
}
and i am hiding and showing tblReview and contentView according to need
with the above code tblReview and contentView are also scrolling upto productCollectionView height.. please help me to solve this error
EDIT: share this seller is out of scrollview so here contentView height is not not so long but if i scroll the contentView also scrolling too long like productioncollectionview
this is contentView which is scrolling too long like productioncollectionview
if your all constraint are proper than just use it like this you don't need to count every time. UICollectionView has intrinsicContentSize it will count it properly.
final class ContentSizedCollectionView: UICollectionView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
put that code in controller or wherever you wan to put and assign ContentSizedCollectionView class to your collectionview.
NOTE: you can also use it with UITableView after creating for UITableView.

How to resize a NSTextView automatically as per its content?

I am making an app where a user can click anywhere on the window and a NSTextView is added programmatically at the mouse location. I have got it working with the below code but I want this NSTextView to horizontally expand until it reaches the edge of the screen and then grow vertically. It currently has a fixed width and when I add more characters, the text view grows vertically (as expected) but I also want it to grow horizontally. How can I achieve this?
I have tried setting isHorizontallyResizable and isVerticallyResizable to true but this doesn't work. After researching for a while, I came across this https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextStorageLayer/Tasks/TrackingSize.html but this didn't work for me either.
Code in my ViewController to add the NSTextView to its view:
private func addText(at point: NSPoint) {
let textView = MyTextView(frame: NSRect(origin: point, size: CGSize(width: 150.0, height: 40.0)))
view.addSubview(textView)
}
And, MyTextView class looks like below:
class MyTextView: NSTextView {
override func viewWillDraw() {
isHorizontallyResizable = true
isVerticallyResizable = true
isRichText = false
}
}
I have also seen this answer https://stackoverflow.com/a/54228147/1385441 but I am not fully sure how to implement it. I have added this code snippet in MyTextView and used it like:
override func didChangeText() {
frame.size = contentSize
}
However, I think I am using it incorrectly. Ergo, any help would be much appreciated.
I'm a bit puzzled, because you're adding NSTextView to a NSView which is part of the NSViewController and then you're talking about the screen width. Is this part of your Presentify - Screen Annotation application? If yes, you have a full screen overlay window and you can get the size from it (or from the view controller's view).
view.bounds.size // view controller's view size
view.window?.frame.size // window size
If not and you really need to know the screen size, check the NSWindow & NSScreen.
view.window?.screen?.frame.size // screen size
Growing NSTextView
There's no any window/view controller's view resizing behavior specified.
import Cocoa
class BorderedTextView: NSTextView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path = NSBezierPath(rect: bounds)
NSColor.red.setStroke()
path.stroke()
}
}
class ViewController: NSViewController {
override func mouseUp(with event: NSEvent) {
// Convert point to the view coordinates
let point = view.convert(event.locationInWindow, from: nil)
// Initial size
let size = CGSize(width: 100, height: 25)
// Maximum text view width
let maxWidth = view.bounds.size.width - point.x // <----
let textView = BorderedTextView(frame: NSRect(origin: point, size: size))
textView.insertionPointColor = .orange
textView.drawsBackground = false
textView.textColor = .white
textView.isRichText = false
textView.allowsUndo = false
textView.font = NSFont.systemFont(ofSize: 20.0)
textView.isVerticallyResizable = true
textView.isHorizontallyResizable = true
textView.textContainer?.widthTracksTextView = false
textView.textContainer?.heightTracksTextView = false
textView.textContainer?.size.width = maxWidth // <----
textView.maxSize = NSSize(width: maxWidth, height: 10000) // <----
view.addSubview(textView)
view.window?.makeFirstResponder(textView)
}
}
I finally got it to work (except for one minor thing). The link from Apple was the key here but they haven't described the code completely, unfortunately.
The below code work for me:
class MyTextView: NSTextView {
override func viewWillDraw() {
// for making the text view expand horizontally
textContainer?.heightTracksTextView = false
textContainer?.widthTracksTextView = false
textContainer?.size.width = 10000.0
maxSize = NSSize(width: 10000.0, height: 10000.0)
isHorizontallyResizable = true
isVerticallyResizable = true
isRichText = false
}
}
That one minor thing which I haven't been able to figure out yet is to limit expanding horizontally until the edge of the screen is reached. Right now it keeps on expanding even beyond the screen width and, in turn, the text is hidden after the screen width.
I think if I can somehow get the screen window width then I can replace 10000.0 with the screen width (minus the distance of text view from left edge) and I can limit the horizontal expansion until the edge of the screen. Having said that, keeping it 10000.0 won't impact performance as described in the Apple docs.

Identifying Objects in Firebase PreBuilt UI in Swift

FirebaseUI has a nice pre-buit UI for Swift. I'm trying to position an image view above the login buttons on the bottom. In the example below, the imageView is the "Hackathon" logo. Any logo should be able to show in this, if it's called "logo", since this shows the image as aspectFit.
According to the Firebase docs page:
https://firebase.google.com/docs/auth/ios/firebaseui
You can customize the signin screen with this function:
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return FUICustomAuthPickerViewController(nibName: "FUICustomAuthPickerViewController",
bundle: Bundle.main,
authUI: authUI)
}
Using this code & poking around with subviews in the debuggers, I've been able to identify and color code views in the image below. Unfortunately, I don't think that the "true" size of these subview frames is set until the view controller presents, so trying to access the frame size inside these functions won't give me dimensions that I can use for creating a new imageView to hold a log. Plus accessing the views with hard-coded index values like I've done below, seems like a pretty bad idea, esp. given that Google has already changed the Pre-Built UI once, adding a scroll view & breaking the code of anyone who set the pre-built UI's background color.
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
// Create an instance of the FirebaseAuth login view controller
let loginViewController = FUIAuthPickerViewController(authUI: authUI)
// Set background color to white
loginViewController.view.backgroundColor = UIColor.white
loginViewController.view.subviews[0].backgroundColor = UIColor.blue
loginViewController.view.subviews[0].subviews[0].backgroundColor = UIColor.red
loginViewController.view.subviews[0].subviews[0].tag = 999
return loginViewController
}
I did get this to work by adding a tag (999), then in the completion handler when presenting the loginViewController I hunt down tag 999 and call a function to add an imageView with a logo:
present(loginViewController, animated: true) {
if let foundView = loginViewController.view.viewWithTag(999) {
let height = foundView.frame.height
print("FOUND HEIGHT: \(height)")
self.addLogo(loginViewController: loginViewController, height: height)
}
}
func addLogo(loginViewController: UINavigationController, height: CGFloat) {
let logoFrame = CGRect(x: 0 + logoInsets, y: self.view.safeAreaInsets.top + logoInsets, width: loginViewController.view.frame.width - (logoInsets * 2), height: self.view.frame.height - height - (logoInsets * 2))
// Create the UIImageView using the frame created above & add the "logo" image
let logoImageView = UIImageView(frame: logoFrame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
// loginViewController.view.addSubview(logoImageView) // Add ImageView to the login controller's main view
loginViewController.view.addSubview(logoImageView)
}
But again, this doesn't seem safe. Is there a "safe" way to deconstruct this UI to identify the size of this button box at the bottom of the view controller (this size will vary if there are multiple login methods supported, such as Facebook, Apple, E-mail)? If I can do that in a way that avoids the hard-coding approach, above, then I think I can reliably use the dimensions of this button box to determine how much space is left in the rest of the view controller when adding an appropriately sized ImageView. Thanks!
John
This should address the issue - allowing a logo to be reliably placed above the prebuilt UI login buttons buttons + avoiding hard-coding the index values or subview locations. It should also allow for properly setting background color (also complicated when Firebase added the scroll view + login button subview).
To use: Create a subclass of FUIAuthDelegate to hold a custom view controller for the prebuilt Firebase UI.
The code will show the logo at full screen behind the buttons if there isn't a scroll view or if the class's private constant fullScreenLogo is set to false.
If both of these conditions aren't meant, the logo will show inset taking into account the class's private logoInsets constant and the safeAreaInsets. The scrollView views are set to clear so that a background image can be set, as well via the private let backgroundColor.
Call it in any signIn function you might have, after setting authUI.providers. Call would be something like this:
let loginViewController = CustomLoginScreen(authUI: authUI!)
let loginNavigationController = UINavigationController(rootViewController: loginViewController)
loginNavigationController.modalPresentationStyle = .fullScreen
present(loginNavigationController, animated: true, completion: nil)
And here's one version of the subclass:
class CustomLoginScreen: FUIAuthPickerViewController {
private var fullScreenLogo = false // false if you want logo just above login buttons
private var viewContainsButton = false
private var buttonViewHeight: CGFloat = 0.0
private let logoInsets: CGFloat = 16
private let backgroundColor = UIColor.white
private var scrollView: UIScrollView?
private var viewContainingButton: UIView?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// set color of scrollView and Button view inside scrollView to clear in viewWillAppear to avoid a "color flash" when the pre-built login UI first appears
self.view.backgroundColor = UIColor.white
guard let foundScrollView = returnScrollView() else {
print("😡 Couldn't get a scrollView.")
return
}
scrollView = foundScrollView
scrollView!.backgroundColor = UIColor.clear
guard let foundViewContainingButton = returnButtonView() else {
print("😡 No views in the scrollView contain buttons.")
return
}
viewContainingButton = foundViewContainingButton
viewContainingButton!.backgroundColor = UIColor.clear
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Create the UIImageView at full screen, considering logoInsets + safeAreaInsets
let x = logoInsets
let y = view.safeAreaInsets.top + logoInsets
let width = view.frame.width - (logoInsets * 2)
let height = view.frame.height - (view.safeAreaInsets.top + view.safeAreaInsets.bottom + (logoInsets * 2))
var frame = CGRect(x: x, y: y, width: width, height: height)
let logoImageView = UIImageView(frame: frame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
logoImageView.alpha = 0.0
// Only proceed with customizing the pre-built UI if you found a scrollView or you don't want a full-screen logo.
guard scrollView != nil && !fullScreenLogo else {
print("No scrollView found.")
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
self.view.addSubview(logoImageView)
self.view.sendSubviewToBack(logoImageView) // otherwise logo is on top of buttons
return
}
// update the logoImageView's frame height to subtract the height of the subview containing buttons. This way the buttons won't be on top of the logoImageView
frame = CGRect(x: x, y: y, width: width, height: height - (viewContainingButton?.frame.height ?? 0.0))
logoImageView.frame = frame
self.view.addSubview(logoImageView)
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
}
private func returnScrollView() -> UIScrollView? {
var scrollViewToReturn: UIScrollView?
if self.view.subviews.count > 0 {
for subview in self.view.subviews {
if subview is UIScrollView {
scrollViewToReturn = subview as? UIScrollView
}
}
}
return scrollViewToReturn
}
private func returnButtonView() -> UIView? {
var viewContainingButton: UIView?
for view in scrollView!.subviews {
viewHasButton(view)
if viewContainsButton {
viewContainingButton = view
break
}
}
return viewContainingButton
}
private func viewHasButton(_ view: UIView) {
if view is UIButton {
viewContainsButton = true
} else if view.subviews.count > 0 {
view.subviews.forEach({viewHasButton($0)})
}
}
}
Hope this helps any who have been frustrated trying to configure the Firebase pre-built UI in Swift.

Window animates width but not height

I have a preferences window with a NSTabViewController hooked up to the toolbar for selecting tabs. I want the window to be resizable, and to resize if necessary when switching tabs to fit the new tab's size.
I'm subclassing NSTabViewController with the following overload:
override var selectedTabViewItemIndex: Int
{
didSet
{
guard let view = tabViewItems[selectedTabViewItemIndex].view,
let window = view.window
else { return }
let minSize = view.fittingSize
let contentRect = NSWindow.contentRect(forFrameRect: window.frame,
styleMask: window.styleMask)
let minRect = NSRect(origin: contentRect.origin, size: minSize)
let newRect = minRect.union(contentRect)
let newFrame = NSWindow.frameRect(forContentRect: newRect,
styleMask: window.styleMask)
window.animator().setFrame(newFrame, display: true, animate: true)
}
}
The result is that it animates resizing horizontally, and at the end it suddenly resizes vertically as well. How do I get it to just animate both directions at once?
Do you have height constraints on any of the tabs? Those might prevent the window from getting larger (even while offscreen).