Override menu button label text color (MacOS SwiftUI) - macos

Can I override a Menu button label's "post-set dimmed" color?
The GIF below shows a legibly bright menu item that dims after new selection. (Default behavior for this system style (e.g., in trackpad prefs).
But it fails accessibility standards, such as WCAG's requirement for > 4.5 : 1 luminance contrast for that font size in an active control (system default is ~ 2).
I've tried:
setting accentColor and foregroundColor everywhere
using onChange to update an #State color fed into the aforementioned modifiers
using onReceive for NSMenu.didSendActionNotification or from calls to the delegate menuDidClose method (was hunting for some appearance setting)
using the DefaultMenuStyle, not borderless
ramping up brightness or contrast, but that introduces aliasing issues
Any suggestions? Maybe I'm just supposed to be using a Picker? Should I flag this as an accessibility issue with Apple?
struct BareExample: View {
var body: some View {
Menu { menuContents }
label: { menuLabel }
.menuStyle(BorderlessButtonMenuStyle())
.fixedSize()
}
var menuContents: some View {
ForEach(sortOptions) { option in
Button(option.rawValue) { setSortOrder(option) }
.accentColor(labelColor)
.foregroundColor(labelColor)
}
}
var menuLabel: some View {
HStack {
Image(systemName: sortIcon)
Text(sortOrderLabel)
}
.accentColor(labelColor)
.foregroundColor(labelColor)
}
#State var sortOrder: PaletteSortOrder = .sequential
var sortOrderLabel: String { sortOrder.rawValue }
let sortIcon = "arrow.up.arrow.down"
let sortOptions = PaletteSortOrder.allCases
#State var labelColor = Color.white
func setSortOrder(_ order: PaletteSortOrder) {
sortOrder = order
labelColor = Color.white
}
}
public enum PaletteSortOrder: String, CaseIterable, Identifiable {
case sequential = "Sequential"
case byLuminance = "Luminance"
case byWorstPairwiseContrast = "Pairwise Contrast"
public var id: String { rawValue }
}

Related

How to create pages for each random card?

(I am a total beginner in Xcode, so try to simplify the answer if you can...)
I have created a random card generator. More specifically I have four cards, when I click a button, a random card among the four cards appears.
The problem is I want to create four pages for each card. For instance, if a random card(image1) appears, I can click the card and go to a page(image1 page), whereas if a random card(image2) appears, I am also able to click the card and go to a page(image2 page)etc....
private var imgs = ["image1", "image2", "image3", "image4"]
#State private var imgsnumbers = [0, 1, 2, 3]
#State var buttonTapped = false
var body: some View {
VStack {
Image(imgs[imgsnumbers[1]])
.resizable()
.frame(width:150 , height:212.63)
Button(action: {
self.imgsnumbers[1] = Int.random(in:0...self.imgs.count - 1)
self.buttonTapped.toggle()
}){
Text("DEAL")
}
.disabled(buttonTapped)
}
}
Depending on how you want the page to be presented, you need to use either a sheet, fullScreenCover or NavigationLink. I'm going to focus on NavigationLink.
First, you need to add a new property #State var randomImage: Int?. This property will help you track what number was generated instead of altering imgsnumbers. You will be assigning the generated number to randomImage in your Button:
Button(action: {
self.randomImage = Int.random(in:0...self.imgs.count - 1)
self.buttonTapped.toggle()
}){
Text("DEAL")
}.disabled(buttonTapped) //note: You can use randomImage != nil instead of using a separate property to check if the button was tapped.
Next, you need to check for whether a number was generated or not using an if let. You'll notice I wrapped the image inside a NavigationLink, this is a button that will present the specific page (destination) for the number generated:
if let randomImage = randomImage {
NavigationLink(destination: PageToNavigateTo()) {
Image(imgs[randomImage])
.resizable()
.frame(width:150 , height:212.63)
}
}else {
//provide a default view if no number was generated
}
If you want to present the page using a different style, checkout fullScreenCover & sheet.
This is best done in two steps , first steps you already did a lot of work but here it is , with some changes, you can disable the button "Show Card" if you want,
The need for an extra #State variable imgsnumber is not there as we can use the random number for storing value and providing to your card view
The way you can create pages is by having another view called CardView of swiftui view type and move to that view with the image you click
This image can be identified by the index number
In a perfect world you would use an ObservableObject class to share the images you have ,but here if its simple need you can type in the array of images again in CardView…
Now you get a small app where you get random card and on clicking on the card you get a exclusive page for that card
You need #Binding property wrapper(in card view) , a facility by compiler that says , my value will be provided by some other View, in this case provided by ContentView, via the Navigation Link code
struct ContentView: View {
private var imgs = ["image1", "image2", "image3", "image4"]
#State private var randomNumber: Int = 0
#State var buttonTapped = false
#State var cardToggle = false
var body: some View {
NavigationView {
VStack {
Image(imgs[randomNumber])
.resizable()
.frame(width:150 , height:212.63)
.onTapGesture {
cardToggle = true
}
Button("Show Card") {
buttonTapped = true
randomNumber = Int.random(in: 0...3)
}
NavigationLink("", destination: CardView(pageNumber: $randomNumber), isActive: $cardToggle)
}
}
}
}
CardView
import SwiftUI
struct CardView: View {
#Binding var pageNumber: Int
var imgs = ["image1", "image2", "image3", "image4"]
var body: some View {
VStack {
Image(imgs[pageNumber])
.resizable()
.frame(width:150 , height:212.63)
}
}
}

How to show/hide column in MacOS multi-column Navigation View?

I would like to show/hide the 2nd column in a 3 column NavigationView layout on macOS. The toggle button does hide the content of the 2nd column – but I would like it to vanish completely, as if the user drags it away.
Any ideas?
struct ContentView: View {
#State private var showSecondColumn = true
var body: some View {
NavigationView {
// 1. Column / sidebar
List {
ForEach(0..<10) { i in
Text("Sidebar \(i)")
}
}
.listStyle(.sidebar)
// 2. Column - conditional
if showSecondColumn {
List {
ForEach(0..<10) { i in
Text("Second \(i)")
}
}
// .frame(width: showSecondColumn ? 150 : 0) // does not work either
}
// 3. Column / Content
Button(showSecondColumn ? "Hide second" : "Show second") {
withAnimation {
showSecondColumn.toggle()
}
}
}
}
}
I ran into this same issue for anyone else that comes across this. I found this article basically saying the amount of columns is fixed at compile time and cannot be changed dynamically.
A bit disappointing, but for what it's worth Apple's own notes app just has a blank third column when nothing is selected so I guess it's part of the design.

SwiftUI animation - toggled Boolean always ends up as true

I'm trying to create an animation in my app when a particular action happens which will essentially make the background of a given element change colour and back x number of times to create a kind of 'pulse' effect. The application itself is quite large, but I've managed to re-create the issue in a very basic app.
So the ContentView is as follows:
struct ContentView: View {
struct Constants {
static let animationDuration = 1.0
static let backgroundAlpha: CGFloat = 0.6
}
#State var isAnimating = false
#ObservedObject var viewModel = ContentViewViewModel()
private let animation = Animation.easeInOut(duration: Constants.animationDuration).repeatCount(6, autoreverses: false)
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button(action: {
animate()
}) {
Text("Button")
.foregroundColor(Color.white)
}
}
.background(isAnimating ? Color.red : Color.blue)
.onReceive(viewModel.$shouldAnimate, perform: { _ in
if viewModel.shouldAnimate {
withAnimation(self.animation, {
self.isAnimating.toggle()
})
}
})
}
func animate() {
self.viewModel.isNew = true
}
}
And then my viewModel is:
import Combine
import SwiftUI
class ContentViewViewModel: ObservableObject {
#Published var shouldAnimate = false
#Published var isNew = false
var cancellables = Set<AnyCancellable>()
init() {
$isNew
.sink { result in
if result {
self.shouldAnimate = true
}
}
.store(in: &cancellables)
}
}
So the logic I am following is that when the button is tapped, we set 'isNew' to true. This in turn is a publisher which, when set to true, sets 'shouldAnimate' to true. In the ContentView, when shouldAnimate is received and is true, we toggle the background colour of the VStack x number of times.
The reason I am using this 'shouldAnimate' published property is because in the actual app, there are several different actions which may need to trigger the animation, and so it feels simpler to have this tied to one variable which we can listen for in the ContentView.
So in the code above, we should be toggling the isAnimating bool 6 times. So, we start with false then toggle as follows:
1: true, 2: false, 3: true, 4: false, 5: true, 6: false
So I would expect to end up on false and therefore have the background white. However, this is what I am getting:
I tried changing the repeatCount (in case I was misunderstanding how the count works):
private let animation = Animation.easeInOut(duration: Constants.animationDuration).repeatCount(7, autoreverses: false)
And I get the following:
No matter the count, I always end on true.
Update:
I have now managed to get the effect I am looking for by using the following loop:
for i in 0...5 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i), execute: {
withAnimation(self.animation, {
self.isAnimating.toggle()
})
})
}
Not sure this is the best way to go though....
To understand what is going on, it would help to understand CALayer property animations.
When you define an animation the system captures the state of a Layer and watches for changes in the animatable properties of that layer. It records property changes for playback during the animation. To present the animation, it create a copy of the layer in its initial state (the presentationLayer). It then substitutes the copy in place of the actual layers on screen and runs the animation by manipulating the animatable properties of the presentation layer.
I this case, when you begin the animation, the system watches what happens to the CALayer that backs your view and captures the changes to any animatable properties (in this case the background color). It then creates a presentationLayer and replays those property changes repeatedly. It's not running your code repeatedly - it's changing the properties of the presentation Layer.
In other words the animation the system knows the layer's background color property should toggle back and forth because of the example you set in your animation block, but the animation toggles the background color back and forth without running your code again.

SwiftUI is there any way of getting a disabled TextField to be grayed out on Mac OS

When a SwitfUI TextField is disabled, on Mac OS, there is no visual feedback that the field is not enterable (apart from not accepting focus click). I have searched high and low, it looks like simply setting .background(Color.whatever) works for IOS (from all the "how tos" that I have encountered). However for a Mac OS app, it only changes the color of the thin boundary of the textfield. I have futzed around and found that I can add opaque overlays to simulate the effect, but that seems overly complex for what I always took to be conventional standard of greying out of disabled fields. Which makes me think that I am missing something bleedingly obvious somewhere.
Has anyone a sample of a MacOS SwiftUI struct that greys the background of a disabled TextField ? My minimal example of what I am doing to see the issue is below.
struct ContentView: View {
#State var nameEditDisabled = true
#State var myText = "Fred"
var body: some View {
VStack {
Button("Change Name") {
nameEditDisabled.toggle()
}
TextField("hello", text: $myText)
.background(nameEditDisabled ? Color.gray: Color.yellow)
.disabled(nameEditDisabled)
}
}
}
it seems to be "fixed' in swiftUI 3.0, macos 12. I get a slightly darker shade of gray when disabled. When in focus, I get a blue border.
Edit:
struct ContentView: View {
#State var nameEditDisabled = false
#State var myText = "Fred"
var body: some View {
VStack {
Button("Change disabling") {
nameEditDisabled.toggle()
}
TextField("hello", text: $myText)
.colorMultiply(nameEditDisabled ? .gray: .yellow)
.disabled(nameEditDisabled)
}.frame(width: 444, height: 444)
}
}

Animating Text in SwiftUI

SwiftUI has wonderful animation features, but the way it handles changes in Text View content is problematic. It animates the change of the text frame but changes the text immediately without animation. As a result, when the content of a Text View is made longer, animating the transition causes an ellipsis (…) to appear until the text frame reaches its full width. For example, in this little app, pressing the Toggle button switches between shorter and longer text:
Here's the code:
import SwiftUI
struct ContentView: View {
#State var shortString = true
var body: some View {
VStack {
Text(shortString ? "This is short." : "This is considerably longer.").font(.title)
.animation(.easeInOut(duration:1.0))
Button(action: {self.shortString.toggle()}) {
Text("Toggle").padding()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The question is: how to avoid the ellipsis? When animating a one character string into a two character string the situation is even worse, because the short string is completely replaced by the ellipsis while it animates into the longer string.
One possibility is to assign a separate id to the view in one state or another by adding the modifier, for instance, .id(self.shortString ? 0 : 1) and then adding a .transition() modifier. That will treat the Text as two different Views, before and after. Unfortunately, in my case I need to move text location during the change, and different ids makes animating that impossible.
I guess the solution is a creative use of AnimatableData. Any ideas?
Here is a demo of possible approach (scratchy - you can redesign it to extension, modifier, or separate view)
Tested with Xcode 11.4 / iOS 13.4
struct ContentView: View {
#State var shortString = true
var body: some View {
VStack {
if shortString {
Text("This is short.").font(.title).fixedSize()
.transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
}
if !shortString {
Text("This is considerably longer.").font(.title).fixedSize()
.transition(AnyTransition.opacity.animation(.easeInOut(duration:1.0)))
}
Button(action: {self.shortString.toggle()}) {
Text("Toggle").padding()
}
}
}
}
Any suggestions for shrinking an animated gif's dimensions?
I use this way:
- decrease zoom of Preview to 75% (or resize window of Simulator)
- use QuickTimePlayer region-based Screen Recording
- use https://ezgif.com/video-to-gif for converting to GIF
If you add .animation(nil) to the Text object definition then the contents will change directly between values, avoiding ellipsis.
However, this may prevent the animation of the text location, which you also mention wanting to do simultaneously.
You can add one by one character into a string with animation after 0.1 seconds additional, but remember to disable the button toggle while the characters being added, like below:
Code:
public struct TextAnimation: View {
public init(){ }
#State var text: String = ""
#State var toggle = false
public var body: some View {
VStack{
Text(text).animation(.spring())
HStack {
Button {
toggle.toggle()
} label: {
Text("Toggle")
}
}.padding()
}.onChange(of: toggle) { toggle in
if toggle {
text = ""
"This is considerably longer.".enumerated().forEach { index, character in
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) {
text += String(character)
}
}
} else {
text = "This is short."
}
}
}
}

Resources