How do I scale down a navigationTitle in SwiftUI? - ios

I have the same problem as described in navigationtitle too long in swiftui -- I want to scale down my navigation bar title to fit the available space. Here's the problem:
Here's my code, which incorporates the suggestion made in the answer to the other referenced question:
struct AboutView: View {
#Binding var showAboutView: Bool
var body: some View {
NavigationView {
Form {
Section() {
Text("A placeholder for information about this app.\n\nDetails forthcoming....")
}
if let url = URL(string: "mailto:sarjo#sarjosoftware.com?subject=My+Wonderful+App") {
Section() {
Link("Contact the Author", destination: url)
}
}
}
// *** suggested solution ***
.navigationTitle(Text("About My Wonderful App").minimumScaleFactor(0.5).lineLimit(1))
.navigationBarItems(trailing: Button(action: {
showAboutView = false
}) {
Text("Done").bold()
})
}
}
}
But this code fails to build with the error:
Instance method 'navigationTitle' requires that 'some View' conform to 'StringProtocol' on the line Form {.
If I move the modifiers on the title text outside to apply to the navigationTitle modifier itself, as shown here:
.navigationTitle(Text("About My Wonderful App")).minimumScaleFactor(0.5).lineLimit(1)
this code builds and runs, but the modifiers are not applied to the title text but to the body text beginning "A placeholder for :
Thanks for any suggestions.

You can try .toolbar especially because .navigationBarItems is deprecated
struct AboutView: View {
#Binding var showAboutView: Bool
var body: some View {
NavigationView {
Form {
Section() {
Text("A placeholder for information about this app.\n\nDetails forthcoming....")
}
if let url = URL(string: "mailto:sarjo#sarjosoftware.com?subject=My+Wonderful+App") {
Section() {
Link("Contact the Author", destination: url)
}
}
}
.toolbar(content: {
ToolbarItem(placement: .principal, content: {
Text("About My Wonderful App")
.font(.title2).fontWeight(.bold)
})
ToolbarItem(placement: .navigationBarTrailing, content: {
Button(action: {showAboutView = false}) {
Text("Done").bold()
}
})
})
}
}
}

I read your problem.
my suggestion is to reduce the font size of NavigationTitle
you can reduce your NavigationTitle by this Code :
init() {
// this is not the same as manipulating the proxy directly
let appearance = UINavigationBarAppearance()
// this overrides everything you have set up earlier.
appearance.configureWithTransparentBackground()
// this only applies to big titles
appearance.largeTitleTextAttributes = [
.font : UIFont.systemFont(ofSize: 30),
NSAttributedString.Key.foregroundColor : UIColor.black
]
// this only applies to small titles
appearance.titleTextAttributes = [
.font : UIFont.systemFont(ofSize: 20),
NSAttributedString.Key.foregroundColor : UIColor.black
]
//In the following two lines you make sure that you apply the style for good
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().standardAppearance = appearance
// This property is not present on the UINavigationBarAppearance
// object for some reason and you have to leave it til the end
UINavigationBar.appearance().tintColor = .black
}
you should add this code upper the body.
also, I attached two links for helping you out of NavigationTitle.
I hope these are useful for you.
Apple
hackingwithswift

Tested on SwiftUI's NavigationView and NavigationStack.
This answer is derived from Amirreza Jolani's answer which is also similar to the code provided from "Customizing SwiftUI Navigation Bar". I thought some additional explanation might help.
The following function formats the singleton UINavigationBar and takes in the title and large titles' desired font sizes:
func formatNavTitle(_ fontSize: CGFloat, _ largeFontSize: CGFloat) {
let appearance = UINavigationBarAppearance()
appearance.largeTitleTextAttributes = [
.font : UIFont.systemFont(ofSize: largeFontSize),
NSAttributedString.Key.foregroundColor : UIColor.label
]
appearance.titleTextAttributes = [
.font : UIFont.systemFont(ofSize: fontSize),
NSAttributedString.Key.foregroundColor : UIColor.label
]
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().tintColor = .label
}
Usage: formatNavTitle(20, 30)
Features that shrink text such as minimumScaleFactor and tightening are limited to UILabel and SwiftUI's Text. Unfortunately those features don't appear to be available with UINavigationBar which uses NSAttributedString. And SwiftUI's navigation title appears to be part of a wrapper around UINavigationBar.
But the font size can be adjusted for the regular title and the large title using the function above. After adding the function to the app, an init statement in this example was also added to the main entrypoint of the app although could be added to AboutView instead:
#main
struct myExampleApp: App {
init() { formatNavTitle(20, 30) }
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This results in the large title being 30 points and the regular title being 20 points:

Related

Update UIKit View From Hosted SwiftUI Selection

Have a UIKit Navigation Controller that is the root view controller in the hierarchy of the storyboards. One of the button items navigates to a UIHostedViewController that produces a SwiftUI View. That SwiftUI View is in and of itself a Tab View with tabs - and all of it works fine.
One of the crux of our development is displaying large data models for the user, and we could regain a bit of screen real estate if we utilize the Navigation bar of the UIKit Navigation.
A defining attribute of our individual views is the color coding of the header and footer, we have been able to change these colors easily as the SwiftUI Tab View just calls different views with different modifiers.
What we would like to do is have the selection of the SwiftUI Tab View set a shared property with the UIKit view that would then change the navigation bar color - however the way we setup a bound variable and reload the NavigationBar setup with a didSet on that variable does not seem to be working.
The code is almost exactly as follows, less a couple of views.
class SomeViewController: UIHostingController< MySwiftUITabView > {
var headerColor: Binding<UIColor> = .constant(UIColor.blue) {
didSet {
DispatchQueue.main.async(qos: .userInteractive) {
self.setupUI()
}
}
}
var defaultColor: UIColor!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder, rootView: MySwiftUITabView(headerColor: headerColor))
}
override func viewDidLoad() {
super.viewDidLoad()
defaultColor = self.navigationController?.navigationBar.backgroundColor
setupUI()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
teardownUI()
}
func setupUI() {
if let naviController = self.navigationController {
let newNavBarAppearance = UINavigationBarAppearance()
newNavBarAppearance.backgroundColor = headerColor.wrappedValue
naviController.navigationBar.scrollEdgeAppearance = newNavBarAppearance
naviController.navigationBar.compactAppearance = newNavBarAppearance
naviController.navigationBar.standardAppearance = newNavBarAppearance
}
}
func teardownUI() {
if let naviController = self.navigationController {
let oldNavBarAppearance = UINavigationBarAppearance()
oldNavBarAppearance.backgroundColor = defaultColor
naviController.navigationBar.scrollEdgeAppearance = oldNavBarAppearance
naviController.navigationBar.compactAppearance = oldNavBarAppearance
naviController.navigationBar.standardAppearance = oldNavBarAppearance
}
}
}
And the SwiftUI Tab View:
struct MySwiftUITabView: View {
#Binding var headerColor: UIColor
var body: some View {
TabView(selection: $selectedTab){
TestView()
.tag(0)
.onAppear {
headerColor = .orange
}
.tabItem {
Label("Test", image: "circle.fill")
}
TestView()
.tag(1)
.onAppear {
headerColor = .red
}
.tabItem {
Label("Test", image: "square.fill")
}
}
.accentColor(.white)
}
}
Would a callback be more appropriate for this? We wrapped the didSet function call in a DispatchGroup as a final attempt since it is updating the UI, but the was not effective still for what we are looking for.
In this example, the navigation bar remains the same color as the original set value (.blue) and never gets updated to the color set to the binding by the .onAppear of the swiftUI tab items.
Any help would be greatly appreciated as always!

SwiftUI - How to add attributed accessibility label for VoiceOver, to control spoken pitch?

I want to have VoiceOver speak a View's accessibility label in multiple pitches. For example, for "Raised string. Normal string.", I want "Raised string" to have a 1.5 pitch, and "Normal string." to have the default 1.0 pitch.
With UIKit, I can set the element's accessibilityAttributedLabel with a NSAttributedString and NSAttributedString.Key.accessibilitySpeechPitch. Something like this:
let pitchAttribute = [NSAttributedString.Key.accessibilitySpeechPitch: 1.5]
let string = NSMutableAttributedString()
let raisedString = NSAttributedString(string: "Raised string.", attributes: pitchAttribute)
string.append(raisedString)
let normalString = NSAttributedString(string: "Normal string.")
string.append(normalString)
squareView.isAccessibilityElement = true
squareView.accessibilityAttributedLabel = string
The result, which is exactly I want (Audio link):
However, with SwiftUI, there only seems to be a .accessibility(label: Text) modifier. This is my code:
struct ContentView: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.accessibility(label: Text("Raised string. Normal string."))
}
}
And this is the result (Audio link):
As you can hear, "Raised string." and "Normal string." are spoken in the same pitch. This is as expected, because I passed in a solitary Text for the label.
But is there any way I can set the spoken pitch in SwiftUI? I can't find a way to set just one pitch, never mind two.
Love to see developers working to include accessibility!
In iOS 15, you can have your function return an AttributedString and then set the view's accessibility label with that AttributedString from your function:
.accessibility(label: Text(getAccessibilityAttributedLabel()))
A sample function with one pitch value:
func getAccessibilityAttributedLabel() -> AttributedString {
var pitchSpeech = AttributedString("Raised pitch")
pitchSpeech.accessibilitySpeechAdjustedPitch = 1.5
return pitchSpeech
}
Well, I guess it's UIViewRepresentable time (Yay 😔). Unless someone has a better answer, this is what I came up with:
struct ContentView: View {
var body: some View {
RectangleView(
accessibilityAttributedLabel: getAccessibilityAttributedLabel(),
fill: UIColor.blue /// pass color into initializer
)
.frame(width: 100, height: 100)
}
/// make the attributed string
func getAccessibilityAttributedLabel() -> NSAttributedString {
let pitchAttribute = [NSAttributedString.Key.accessibilitySpeechPitch: 1.5]
let string = NSMutableAttributedString()
let raisedString = NSAttributedString(string: "Raised string.", attributes: pitchAttribute)
string.append(raisedString)
let normalString = NSAttributedString(string: "Normal string.")
string.append(normalString)
return string
}
}
struct RectangleView: UIViewRepresentable {
var accessibilityAttributedLabel: NSAttributedString
var fill: UIColor
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIView {
let uiView = UIView()
uiView.backgroundColor = fill
uiView.isAccessibilityElement = true
uiView.accessibilityAttributedLabel = accessibilityAttributedLabel /// set the attributed label here
return uiView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<Self>) {}
}

Size of UIViewRepresentable in SwiftUI [duplicate]

Setting lineBreakMode to byWordWrapping and set numberOfLines to 0 does not seem to be sufficient:
struct MyTextView: UIViewRepresentable {
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
label.text = "Here's a lot of text for you to display. It won't fit on the screen."
return label
}
func updateUIView(_ view: UILabel, context: Context) {
}
}
struct MyTextView_Previews: PreviewProvider {
static var previews: some View {
MyTextView()
.previewLayout(.fixed(width: 300, height: 200))
}
}
The text does not wrap, regardless of which setting I use for lineBreakMode. The canvas preview and live preview both look like this:
The closest I've gotten is setting preferredMaxLayoutWidth, which does cause the text to wrap, but there doesn't seem to be a value that means "whatever size the View is".
Possible solution is to declare the width as a variable on MyTextView:
struct MyTextView: UIViewRepresentable {
var width: CGFloat
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
label.preferredMaxLayoutWidth = width
label.text = "Here's a lot of text for you to display. It won't fit on the screen."
return label
}
func updateUIView(_ view: UILabel, context: Context) {
}
}
and then use GeometryReader to findout how much space there is avaible and pass it into the intializer:
struct ExampleView: View {
var body: some View {
GeometryReader { geometry in
MyTextView(width: geometry.size.width)
}
}
}
Try to use this magic line in makeUIView() func
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
I found a somehow "nasty" approach that allows a UILabel to properly wrap when used as a UIViewRepresentable (even when inside a ScrollView), without the need for GeometryReader:
Whenever creating your UILabel:
label.setContentCompressionResistancePriority(.defaultLow,
for: .horizontal)
label.setContentHuggingPriority(.defaultHigh,
for: .vertical)
This ensures that:
the label will break line and not have an infinite width
the label will not add grow unnecessarily in height, which may happen in some circumstances.
Then...
Add a width property to your UIViewRepresentable that will be used to set the preferredMaxLayoutWidth
Use your UIViewRepresentable into a vanilla SwiftUI.View
Add a GeometryReader as an overlay to prevent expansion
Trigger the measurement after a soft delay, modifying some state to trigger a new pass.
i.e.:
public var body: some View {
MyRepresentable(width: $width,
separator: separator,
content: fragments)
.overlay(geometryOverlay)
.onAppear { shouldReadGeometry = true }
}
// MARK: - Private Props
#State private var width: CGFloat?
#State private var shouldReadGeometry = false
#ViewBuilder
var geometryOverlay: some View {
if shouldReadGeometry {
GeometryReader { g in
SwiftUI.Color.clear.onAppear {
self.width = g.size.width
}
}
}
}
OLD ANSWER:
...
In your updateUIView(_:context:):
if let sv = uiView.superview, sv.bounds.width != 0 {
let shouldReloadState = uiView.preferredMaxLayoutWidth != sv.bounds.width
uiView.preferredMaxLayoutWidth = sv.bounds.width
if shouldReloadState {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
self.stateToggle.toggle() // a Bool #State you can add in your struct
}
}
}
Disclaimer: I'm not a huge fan of main.async calls, particularly when they come in combination with some arbitrary delay, but this seems to get the work done in a consistent way.

Placement of UIViewRepresentable within list cells in SwiftUI

When adding a custom UILabel to a List in SwiftUI, I get errors with cell reuse, where the label on some cells isn't visible at all, and on some cells it is placed in top-left without any regard for the cell's padding. It always renders perfectly on the initial cells.
The problem doesn't occur when using a ScrollView. Is this a known bug, and are there good workarounds?
GeometryReader { geometry in
List {
ForEach(self.testdata, id: \.self) { text in
Group {
AttributedLabel(attributedText: NSAttributedString(string: text), maxWidth: geometry.size.width - 40)
}.padding(.vertical, 20)
}
}
}
struct AttributedLabel: UIViewRepresentable {
let attributedText: NSAttributedString
let maxWidth: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel {
let label = UILabel()
label.preferredMaxLayoutWidth = maxWidth
label.attributedText = attributedText
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
label.backgroundColor = UIColor.red
return label
}
func updateUIView(_ label: UILabel, context: UIViewRepresentableContext<Self>) {}
}
It is not related to ScrollView or SwiftUI bug.
I think You have a issue with your AttributedLabel Class. I tried using normal Text and it is working fine.
List {
ForEach(self.testdata, id: \.self) { text in
Group {
Text(student.name)
.background(Color.red)
}.padding(.vertical, 20)
}
}
There does seem to be a workaround for this.
The first step is to get the model to first return an empty array of items and later return the actual update. This will force the view to update. And then, after a short pause, it can be followed by the actual update. For this case, this isn't quite enough. Doing that alone still leads to the layout issues. Somehow the list (presumably backed by a UITableView that is aggressively recycling its cells) still manages to keep the state the is somehow causing the trouble. And so...
The second step is to get the view to offer something other than the List when there are no items. This is done using the SwiftUI if and else to use a different view depending on whether there are any items. With the changes to the model, as per step 1, this happens every update.
Doing steps (1) and (2) appears to workaround the issue. The sample code below also includes an .animation(.none) method on the View. This was necessary in my code, but in the example code below it doesn't seem to be needed.
A downside of this workaround is that you will lose animations. And clearly it is something of a hack that, if Apple make changes in the future, might not continue to work. (Still, maybe by then the bug will have been fixed.)
import SwiftUI
struct ContentView: View {
#ObservedObject var model = TestData()
var body: some View {
VStack() {
GeometryReader { geometry in
// handle the no items case by offering up a different view
// this appears to be necessary to workaround the issues
// where table cells are re-used and the layout goes wrong
// Don't show the "No Data" message unless there really is no data,
// i.e. skip case where we're just delaying to workaround the issue.
if self.model.sampleList.isEmpty {
Text("No Data")
.foregroundColor(self.model.isModelUpdating ? Color.clear : Color.secondary)
.frame(width: geometry.size.width, height: geometry.size.height) // centre the text
}
else {
List(self.model.sampleList, id:\.self) { attributedString in
AttributedLabel(attributedText: attributedString, maxWidth: geometry.size.width - 40)
}
}
}.animation(.none) // this MAY not be necessary for all cases
Spacer()
Button(action: { self.model.shuffle()} ) { Text("Shuffle") }.padding(20)
}
}
}
struct AttributedLabel: UIViewRepresentable {
let attributedText: NSAttributedString
let maxWidth: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel {
let label = UILabel()
label.preferredMaxLayoutWidth = maxWidth
label.attributedText = attributedText
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
label.backgroundColor = UIColor.red
return label
}
func updateUIView(_ label: UILabel, context: UIViewRepresentableContext<Self>) {
// function required by protoocol - NO OP
}
}
class TestData : ObservableObject {
#Published var sampleList = [NSAttributedString]()
#Published var isModelUpdating = false
private var allSamples = [NSAttributedString]()
func shuffle() {
let filtered = allSamples.filter{ _ in Bool.random() }
let shuffled = filtered.shuffled()
// empty the sampleList - this will trigger the View that is
// observing the model to update and handle the no items case
self.sampleList = [NSAttributedString]()
self.isModelUpdating = true
// after a short delay update the sampleList - this will trigger
// the view that is observing the model to update
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
self.sampleList = shuffled
self.isModelUpdating = false
}
}
init() {
generateSamples()
shuffle()
}
func generateSamples() {
DispatchQueue.main.async {
var samples = [NSAttributedString]()
samples.append("The <em>quick</em> brown fox <strong>boldly</strong> jumped over the <em>lazy</em> dog.".fromHTML)
samples.append("<h1>SwiftUI</h1><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p>".fromHTML)
samples.append("<h1>Test Cells</h1><p>Include cells that have different heights to demonstrate what is going on. Make some of them really quite long. If they are all showing the list is going to need to scroll at least on smaller devices.</p><p>Include cells that have different heights to demonstrate what is going on. Make some of them really quite long. If they are all showing the list is going to need to scroll at least on smaller devices.</p><p>Include cells that have different heights to demonstrate what is going on. Make some of them really quite long. If they are all showing the list is going to need to scroll at least on smaller devices.</p> ".fromHTML)
samples.append("<h3>List of the day</h3><p>And he said:<ul><li>Expect the unexpected</li><li>The sheep is not a creature of the air</li><li>Chance favours the prepared observer</li></ul>And now, maybe, some commentary on that quote.".fromHTML)
samples.append("Something that is quite short but that is more than just one line long on a phone maybe. This might do it.".fromHTML)
self.allSamples = samples
}
}
}
extension String {
var fromHTML : NSAttributedString {
do {
return try NSAttributedString(data: Data(self.utf8), options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue
], documentAttributes: nil)
}
catch {
return NSAttributedString(string: self)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I had the similar problem and solved it by adding frame to UIViewRepresentable with getTextFrame(text),
GeometryReader { geometry in
List {
ForEach(self.testdata, id: \.self) { text in
Group {
AttributedLabel(attributedText: NSAttributedString(string: text), maxWidth: geometry.size.width - 40)
// add this frame
.frame(width: getTextFrame(text).width height: getTextFrame(text).height)
}.padding(.vertical, 20)
}
}
}
func getTextFrame(for text: String, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> CGSize {
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.preferredFont(forTextStyle: .body)
]
let attributedText = NSAttributedString(string: text, attributes: attributes)
let width = maxWidth != nil ? min(maxWidth!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
let height = maxHeight != nil ? min(maxHeight!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
let constraintBox = CGSize(width: width, height: height)
let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral
return rect.size
}

UIAlertController Action Sheet without Blurry View Effect

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
}

Resources