What is the best way to align right bar button items with left title in UINavigationBar ?
(now I have the icons at the top right corner)
maybe there is a more correct and optimal solution, but it is suitable for me.
Do not be alarmed by the syntax, I use SnapKit
Create button or view
lazy private var settingsButton = UIButton().then {
$0.setImage(Image.settings, for: .normal)
}
override viewDidAppear(_ animated: Bool)
navigationController?.navigationBar.subviews.forEach { subview in
let stringFromClass = NSStringFromClass(subview.classForCoder)
guard stringFromClass.contains("UINavigationBarLargeTitleView") else { return }
subview.subviews.forEach { label in
guard label is UILabel else { return }
subview.addSubview(settingsButton)
settingsButton.snp.makeConstraints{
$0.top.equalTo(label)
$0.right.equalToSuperview().offset(-14)
$0.height.width.equalTo(35)
}
}
}
set UIScrollViewDelegate and in scrollViewDidScroll method get ahead of the navigationBar state and then hide / show the UIBarButtonItem
Result
screenshot
Related
I want to add a banner to the navigation bar, but by increasing the height of it. I want to copy the design and behaviour of an artist page in the Apple Music app:
It behaves just like a normal Large Title would, except for that it has been moved down, it has a sticky UIImageView behind it and it returns its background when the user scrolls down far enough. You can fire up Apple Music, search for an artist and go to their page to try it out yourself.
I've tried a bunch of things like setting the frame on the UINavigationBarLargeTitleView, and the code from this answer: https://stackoverflow.com/a/49326161/5544222
I already got a hold of the UINavigationBarLargeTitleView and its UILabel using the following code:
func setLargeTitleHeight() {
if let largeTitleView = self.getLargeTitleView() {
if let largeTitleLabel = self.getLargeTitleLabel(largeTitleView: largeTitleView) {
// Set largeTitleView height.
}
}
}
func getLargeTitleView() -> UIView? {
for subview in self.navigationBar.subviews {
if NSStringFromClass(subview.classForCoder).contains("UINavigationBarLargeTitleView") {
return subview
}
}
return nil
}
func getLargeTitleLabel(largeTitleView: UIView) -> UILabel? {
for subview in largeTitleView.subviews {
if subview.isMember(of: UILabel.self) {
return (subview as! UILabel)
}
}
return nil
}
Initially put a view with image and label and play button. Then clear the navigation bar it will show the image below it by
self.navigationController!.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController!.navigationBar.shadowImage = UIImage()
self.navigationController!.navigationBar.isTranslucent = true
later when you scroll up you have to handle it manually and again show the
navigation with title.
I'm using UIAlertController for some actions.
But I'm not a big fan of the Blurry View Effect in the actions group view (see screenshot below).
I'm trying to remove this blurry effect. I made some research online, and I couldn't find any API in UIAlertController that allows to remove this blurry effect. Also, according to their apple doc here :
The UIAlertController class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified.
I see that Instagram also removes this blurry view effect :
The only way I could find to remove it is to update the view hierarchy myself via an extension of UIAlertController.
extension UIAlertController {
#discardableResult private func findAndRemoveBlurEffect(currentView: UIView) -> Bool {
for childView in currentView.subviews {
if childView is UIVisualEffectView {
childView.removeFromSuperview()
return true
} else if String(describing: type(of: childView.self)) == "_UIInterfaceActionGroupHeaderScrollView" {
// One background view is broken, we need to make sure it's white.
if let brokenBackgroundView = childView.superview {
// Set broken brackground view to a darker white
brokenBackgroundView.backgroundColor = UIColor.colorRGB(red: 235, green: 235, blue: 235, alpha: 1)
}
}
findAndRemoveBlurEffect(currentView: childView)
}
return false
}
}
let actionSheetController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
actionSheetController.view.tintColor = .lightBlue
actionSheetController.removeBlurryView()
This worked fine, it removed my blurry view effect:
What I'm wondering... Is my solution the only way to accomplish that? Or there is something that I'm missing about the Alert Controller appearance?
Maybe there is a cleaner way to accomplish exactly that result? Any other ideas?
It is easier to subclass UIAlertController.
The idea is to traverse through view hierarchy each time viewDidLayoutSubviews gets called, remove effect for UIVisualEffectView's and update their backgroundColor:
class AlertController: UIAlertController {
/// Buttons background color.
var buttonBackgroundColor: UIColor = .darkGray {
didSet {
// Invalidate current colors on change.
view.setNeedsLayout()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Traverse view hierarchy.
view.allViews.forEach {
// If there was any non-clear background color, update to custom background.
if let color = $0.backgroundColor, color != .clear {
$0.backgroundColor = buttonBackgroundColor
}
// If view is UIVisualEffectView, remove it's effect and customise color.
if let visualEffectView = $0 as? UIVisualEffectView {
visualEffectView.effect = nil
visualEffectView.backgroundColor = buttonBackgroundColor
}
}
// Update background color of popoverPresentationController (for iPads).
popoverPresentationController?.backgroundColor = buttonBackgroundColor
}
}
extension UIView {
/// All child subviews in view hierarchy plus self.
fileprivate var allViews: [UIView] {
var views = [self]
subviews.forEach {
views.append(contentsOf: $0.allViews)
}
return views
}
}
Usage:
Create alert controller.
Set buttons background color:
alertController.buttonBackgroundColor = .darkGray
Customise and present controller.
Result:
Answer by Vadim works really well.
What I missed in it (testing on iOS 14.5) is lack of separators and invisible title and message values.
So I added setting correct textColor for labels and skipping separator visual effect views in order to get correct appearance. Also remember to override traitCollectionDidChange method if your app supports dark mode to update controls backgroundColor accordingly
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for subview in view.allViews {
if let label = subview as? UILabel, label.textColor == .white {
label.textColor = .secondaryLabel
}
if let color = subview.backgroundColor, color != .clear {
subview.backgroundColor = buttonBackgroundColor
}
if let visualEffectView = subview as? UIVisualEffectView,
String(describing: subview).contains("Separator") == false {
visualEffectView.effect = nil
visualEffectView.contentView.backgroundColor = buttonBackgroundColor
}
}
popoverPresentationController?.backgroundColor = buttonBackgroundColor
}
import UIKit
import Material
class MyVC: UITableViewController {
fileprivate var deleteButton: IconButton!
override func viewDidLoad() {
super.viewDidLoad()
prepareDeleteButton()
navigationItem.rightViews = [deleteButton]
}
// other delegates of UITableView
}
extension MyVC {
fileprivate func prepareDeleteButton() {
deleteButton = IconButton(image: UIImage(named: "Trash"))
deleteButton.tintColor = Color.red.base
deleteButton.addTarget(self, action: #selector(doSomething), for: .touchUpInside)
}
#objc
fileprivate func doSomething() {
print("delete accessory")
}
}
Note that MyVC is pushed from another ViewController.
In MyVC, I want my trash can icon to be at the upper right corner of the screen, which is navigationItem.rightViews = [deleteButton], and to have red base color deleteButton.tintColor = Color.red.base.
But it does not work in the code above, the trash can is still black. How can I change its tint color?
It seems that my trash icon is a little bigger then other Cosmic Mind icons at the same rightViews although it has the same size as Cosmic Mind icons (24x24 #1x). Is it true? How to make it a little smaller?
Regards,
The issue with your code is that you are not using the correct rendering mode. Try this:
deleteButton = IconButton(image: UIImage(named: "Trash")!.withRenderingMode(.alwaysOriginal))
All the best!
I'm trying to implement 6 lines high description label and I want it to be focusable. Ideally that would mean extending UILabel class to make a custom component. I tried that by implementing canBecomeFocused and didUpdateFocusInContext but my UILabel doesn't seem to get any focus.
I also tried replacing UILabel with UIButton, but buttons aren't really optimised for this sort of thing. Also that would mean I'd need to change buttonType on focus from custom to plain.. and buttonType seems to be a ready-only property.
In reality I'd like to have exact same text label implemented by Apple in Apple TV Movies app. For movie description they have a text label that displays a few lines of text and a "more". When focused it looks like a button (shadows around) and changed background color. When tapped - it opens up a modal window with entire movie description.
Any suggestions? Or maybe someone has already implemented this custom control for tvOS? Or event better - there is a component from Apple that does this and I'm missing something.
P.S: Swift solution would be welcome :)
Ok, answering my own question :)
So it appears that some some views are "focusable" on tvOS out-of-the-box, and other have to be instructed to do so.
I finally ended up using UITextView, which has a selectable property, but if not one of these focusable views by default. Editing of TextView has to be disabled to make it look like UILabel. Also, currently there is a bug which prevents you from using selectable property from Interface Builder but works from code.
Naturally, canBecomeFocused() and didUpdateFocusInContext had to be implemented too. You'll also need to pass a UIViewController because UITextView is not capable of presenting a modal view controller. Bellow is what I ended up creating.
class FocusableText: UITextView {
var data: String?
var parentView: UIViewController?
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: "tapped:")
tap.allowedPressTypes = [NSNumber(integer: UIPressType.Select.rawValue)]
self.addGestureRecognizer(tap)
}
func tapped(gesture: UITapGestureRecognizer) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let descriptionView = storyboard.instantiateViewControllerWithIdentifier("descriptionView") as? DescriptionViewController {
if let view = parentView {
if let show = show {
descriptionView.descriptionText = self.data
view.modalPresentationStyle = UIModalPresentationStyle.OverFullScreen
view.presentViewController(descriptionView, animated: true, completion: nil)
}
}
}
}
override func canBecomeFocused() -> Bool {
return true
}
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
if context.nextFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.2).CGColor
}, completion: nil)
} else if context.previouslyFocusedView == self {
coordinator.addCoordinatedAnimations({ () -> Void in
self.layer.backgroundColor = UIColor.clearColor().CGColor
}, completion: nil)
}
}
}
As for making a UILabel focusable:
class MyLabel: UILabel {
override var canBecomeFocused: Bool {
return true
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocus(in: context, with: coordinator)
backgroundColor = context.nextFocusedView == self ? .blue:.red
}
}
IMPORTANT!!!
As stated on the apple developer portal:
The value of this property is true if the view can become focused; false otherwise.
By default, the value of this property is false. This property informs the focus engine if a view is capable of being focused. Sometimes even if a view returns true, a view may not be focusable for the following reasons:
The view is hidden.
The view has alpha set to 0.
The view has userInteractionEnabled set to false.
The view is not currently in the view hierarchy.
Use a collection view with just one cell and add transform to cell and change cell background color in didUpdateFocusInContext when focus moves to cell.
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
coordinator.addCoordinatedAnimations({
if self.focused {
self.transform = CGAffineTransformMakeScale(1.01, 1.01)
self.backgroundColor = UIColor.whiteColor()
self.textLabel.textColor = .blackColor()
}
else {
self.transform = CGAffineTransformMakeScale(1, 1)
self.backgroundColor = UIColor.clearColor()
self.textLabel.textColor = .whiteColor()
}
}, completion: nil)
}
As an additional step you could try to extract the color of the image if you are using the image as background like iTunes and use that for Visual effect view behind the cell.
Also you can apply transform to the collectionView in the video controller to make it look like in focus
You can use system button, and set the background image in storyboard to an image that contains the color you would like
How would I loop through all UIButtons in my view in Swift? I would want to set all the titles to "", but my for-loop in Swift is giving an error.
for btns in self.view as [UIButton] {
// set the title to ""
}
This code should work:
for view in self.view.subviews as [UIView] {
if let btn = view as? UIButton {
btn.setTitleForAllStates("")
}
}
You need to iterate through the subViews array.
Shortened and updated for Swift 3 & 4
for case let button as UIButton in self.view.subviews {
button.setTitleForAllStates("")
}
Looping over subview works, but it's sometimes a little ugly, and has other issues.
If you need to loop over some specific buttons, for example to add corner radius or change tint or background color, you can use an array of IBOutlets and then loop over that.
var buttons = [SkipBtn, AllowBtn]
for button in buttons as! [UIButton] {
button.layer.cornerRadius = 5
}
Swift 4:
let subviewButtons = self.view.subviews.filter({$0.isKind(of: UIButton.self)})
for button in subviewButtons {
//do something
}
To add some context for a common use case, suppose the buttons were in a scroll view and you wanted to highlight the tapped button and de-highlight the other buttons. In this situation, you would direct all buttons to one action method:
#objc private func buttonAction(_ button: UIButton) {
for case let b as UIButton in view.scrollView.subviews {
if b == button {
b.setTitleColor(UIColor.green, for: []) // highlight
} else {
b.setTitleColor(UIColor.black, for: []) // de-highlight
}
}
}
This code seems to be quite useful for iterating over any object within a view, just change UIButton for any other subview type such as UIView or UIImageView, etc.
let filteredSubviews = self.view.subviews.filter({
$0.isKindOfClass(UIButton)})
for view in filteredSubviews {
//Do something
}
Used some of the offered questions out there and created my own. I believe is the most efficient when you want to programmatically set up the title of various UIButtons(in my case I am building a quiz)
By randomising my array list and with just a for loop I printing the item at index to the button title
for view in self.viewForButtons.subviews{
if view.isKindOfClass(UIButton)
{
let button : UIButton = view as! UIButton
button.setTitle("item[i]", forState: .Normal)
}
}
If you have UIView's within self.view then you need to loop through the subviews while searching for UIButton. Using the accepted answer, I made this little function to do so:
Swift 4 + :
func findButton(`in` view: UIView){
for view in view.subviews as [UIView] {
if let button = view as? UIButton {
// Do something with 'button'
}else{
// Loop through subview looking for buttons
findButton(in: view)
}
}
}
Usage:
override func viewDidLoad() {
findButton(in: self.view)
}
Hope this helps!
Here's a short way in Swift if you know the subview only has buttons:
myView.subviews.map {
($0 as? UIButton)!.enabled = false
}