SwiftUI List - selected element flashes white before drawn as planned - swift

I have this code for a List populated by Core Data:
List {
ForEach(items.filter {
self.searchText.isEmpty ? true : $0.term!.contains(self.searchText)
}, id: \.self) { item in
Button(action: {
globalVariables.selectedItem = item
}) {
Text(item.term!)
.font(fontItems)
.disabled(true)
.foregroundColor(globalVariables.selectedItem == item ? .black : .white)
}
.listRowBackground(
Group {
if ((globalVariables.selectedItem == nil) || (item != globalVariables.selectedItem)) {
Color(UIColor.clear)
} else if item == globalVariables.selectedItem {
Color("corListaSelecao").mask(RoundedRectangle(cornerRadius: 20))
}
}
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
)
}
.onDelete(perform: deleteItems)
.onAppear{
globalVariables.selectedItem = items.first
}
.cornerRadius(20)
}
.background(Color("fundoControles"))
.cornerRadius(10)
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
This produces a list like the following picture:
Every time I select a new item on the list, the new selected element background is drawn as a white hard edge rectangle momentarily then it is drawn as is is supposed to be, orange with round corners.
I am not setting this white color anywhere.
I have this init on my ContentView:
init() {
UITextView.appearance().backgroundColor = .clear
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
}
Where is this white color coming from?
How do I get rid of this undesired flash?

This might be default List highlight for Button, try to remove Button and use instead .onTapGesture directly to Text, like
Text(item.term!)
.font(fontItems)
.disabled(true)
.foregroundColor(globalVariables.selectedItem == item ? .black : .white)
// replacements for button to make whole row tappable
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
globalVariables.selectedItem = item
}

Related

How to make SwiftUI Buttons a fixed size?

I am trying to create 2 buttons of equal size, but the size is determined by the text inside the button. The two buttons are "login" and "create account", so the "create account" button is larger. I tried to use .frame() to adjust the size, but that just made it have extra padding around it.
Here is my code:
HStack {
Button(action: {
print("Login button pressed")
}) {
Text("Login")
.padding()
.foregroundColor(Color.white)
.background(Color(UIColor.buttonColor))
.cornerRadius(15)
}
Button(action: {
print("Create Account button pressed")
}) {
Text("Create Account")
.padding()
.foregroundColor(Color.white)
.background(Color(UIColor.buttonColor))
.cornerRadius(15)
}
}
And here is what that displays
Best to make a button style you can use across your app which will also ensure any future stack is proportionally filled. Otherwise you'll find your UIs will get messy making Buttons with Text()s like that.
Laying out the buttons with a custom Style in the HStack:
HStack(spacing: 20) {
Button("Login", action: { print("login")})
.buttonStyle(PrimaryButtonStyle())
Button("Create Account", action: { print("create account")})
.buttonStyle(PrimaryButtonStyle())
}.padding([.leading, .trailing], 10)
PrimaryButtonStyle:
struct PrimaryButtonStyle: ButtonStyle {
var backgroundColor: Color = .black
var textColor: Color = Color.white
var height: CGFloat = 46
var cornerRadius: CGFloat = 15
var fontSize: CGFloat = 15
var disabled: Bool = false
var textSidePadding: CGFloat = 30
var weight: Font.Weight = .semibold
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding([.leading, .trailing], textSidePadding)
.frame(maxWidth: .infinity, maxHeight: height)
.background(disabled ? .gray : backgroundColor)
.foregroundColor(textColor)
.cornerRadius(cornerRadius)
.font(.system(size: fontSize, weight: weight, design: .default))
.scaleEffect(configuration.isPressed ? 1.2 : 1)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
}
}
Result:
:
.frame and set width to UI screen - blank should work

Animating a flashing bell in SwiftUI

I am having problems with making a simple systemIcon flash in SwiftUI.
I got the animation working, but it has a silly behaviour if the layout of
a LazyGridView changes or adapts. Below is a video of its erroneous behaviour.
The flashing bell stays in place but when the layout rearranges the bell
starts transitioning in from the bottom of the parent view thats not there anymore.
Has someone got a suggestion how to get around this?
Here is a working example which is similar to my problem
import SwiftUI
struct FlashingBellLazyVGrid: View {
#State var isAnimating = false
#State var showChart = true
var body: some View {
let columns = [GridItem(.adaptive(minimum: 300), spacing: 50, alignment: .center)]
VStack {
Button(action: {
showChart.toggle()
}) {
VStack {
Circle()
.fill(showChart ? Color.green : Color.red)
.shadow(color: Color.gray, radius: 5, x: 2, y: 2)
Text("Charts")
.foregroundColor(Color.primary)
}.frame(width: 150, height: 50)
}
ScrollView {
LazyVGrid (
columns: columns, spacing: 50
) {
ForEach(0 ..< 25) { item in
ZStack {
Rectangle()
.fill(Color.red)
.cornerRadius(15)
VStack {
HStack {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Spacer()
Image(systemName: "bell.fill")
.foregroundColor(Color.yellow)
.opacity(self.isAnimating ? 1 : 0)
.animation(Animation.easeInOut(duration: 0.66).repeatForever(autoreverses: false))
.onAppear{ self.isAnimating = true }
}.padding(50)
if showChart {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
}
}
}
}
}
}
}
struct FlashingBellLazyVGrid_Previews: PreviewProvider {
static var previews: some View {
FlashingBellLazyVGrid()
}
}
how it looks like before you click the showChart button at the top
After you toggle the button it looks like the bells are erroneously moving into place from the bottom of the screen. and toggling it back to its original state doesn't resolve this bug subsequently.
[
Looks like the animation is basing itself off of the original size of the view. In order to trick it into recognizing the new view size, I used .id(UUID()) on the outside of the grid. In a real world application, you'd probably want to be careful to store this ID somewhere and only refresh it when needed -- not on every re-render like I'm doing:
struct FlashingBellLazyVGrid: View {
#State var showChart = true
let columns = [GridItem(.adaptive(minimum: 300), spacing: 50, alignment: .center)]
var body: some View {
VStack {
Button(action: {
showChart.toggle()
}) {
VStack {
Circle()
.fill(showChart ? Color.green : Color.red)
.shadow(color: Color.gray, radius: 5, x: 2, y: 2)
Text("Charts")
.foregroundColor(Color.primary)
}.frame(width: 150, height: 50)
}
ScrollView {
LazyVGrid (
columns: columns, spacing: 50
) {
ForEach(0 ..< 25) { item in
ZStack {
Rectangle()
.fill(Color.red)
.cornerRadius(15)
VStack {
SeparateComponent()
if showChart {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
}
}
}
.id(UUID()) //<-- Here
}
}
}
}
struct SeparateComponent : View {
#State var isAnimating : Bool = false
var body: some View {
HStack {
Text("Hello, World!")
Spacer()
Image(systemName: "bell.fill")
.foregroundColor(Color.yellow)
.opacity(self.isAnimating ? 1 : 0)
.animation(Animation.easeInOut(duration: 0.66).repeatForever(autoreverses: false))
.onAppear{
self.isAnimating = true
}
}
.padding(50)
}
}
I also separated out the blinking component into its own view, since there were already problematic things happening with the existing logic with onAppear, which wouldn't affect newly-scrolled-to items correctly. This may need refactoring for your particular case as well, but this should get you started.

Add shadow above SwiftUI's TabView

I'm trying to implement TabView in SwiftUI that has the same color as screen background but also has a shadow above it like in this picture:
So far I've been able to properly display color, but I don't know how to add the shadow. This is my code:
struct ContentView: View {
init() {
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
UITabBar.appearance().standardAppearance = appearance
}
var body: some View {
TabView {
Text("First View")
.tabItem {
Image(systemName: "square.and.arrow.down")
Text("First")
}
Text("Second View")
.tabItem {
Image(systemName: "square.and.arrow.up")
Text("Second")
}
}
}
}
Does anyone know how to do this? I would appreciate your help :)
Short answer
I found a solution. You can create your own shadow image and add it to the UITabBar appearance like this:
// load your custom shadow image
let shadowImage: UIImage = ...
//you also need to set backgroundImage, without it shadowImage is ignored
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().shadowImage = shadowImage
More detailed answer
Setting backgroundImage
Note that by setting
UITabBar.appearance().backgroundImage = UIImage()
you make your TabView transparent, so it is not ideal if you have content that can scroll below it. To overcome this, you can set TabView's color.
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor.systemGray6
UITabBar.appearance().standardAppearance = appearance
Setting shadowImage
I wanted to generate shadow image programatically. For that I've created an extension of UIImage. (code taken from here)
extension UIImage {
static func gradientImageWithBounds(bounds: CGRect, colors: [CGColor]) -> UIImage {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
And finally I styled my TabView like this:
let image = UIImage.gradientImageWithBounds(
bounds: CGRect( x: 0, y: 0, width: UIScreen.main.scale, height: 8),
colors: [
UIColor.clear.cgColor,
UIColor.black.withAlphaComponent(0.1).cgColor
]
)
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor.systemGray6
appearance.backgroundImage = UIImage()
appearance.shadowImage = image
UITabBar.appearance().standardAppearance = appearance
Result
Your best bet in order to achieve pretty much exactly what you wish is to create a custom TabView.
In fact, in SwiftUI you could use UITabBarAppearance().shadowColor, but that won't do much apart from drawing a 2px line on top of the TabView itself.
Instead, with the below code, you could create a custom TabView and achieve the desired graphical effect.
import SwiftUI
enum Tab {
case borrow,ret,device,you
}
struct TabView: View {
#Binding var tabIdx: Tab
var body: some View {
HStack {
Group {
Spacer()
Button (action: {
self.tabIdx = .borrow
}) {
VStack{
Image(systemName: "arrow.down.circle")
Text("Borrow")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .borrow ? .purple : .secondary)
Spacer()
Button (action: {
self.tabIdx = .ret
}) {
VStack{
Image(systemName: "arrow.up.circle")
Text("Return")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .ret ? .purple : .secondary)
Spacer()
Button (action: {
self.tabIdx = .device
}) {
VStack{
Image(systemName: "safari")
Text("Device")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .device ? .purple : .secondary)
Spacer()
Button (action: {
self.tabIdx = .you
}) {
VStack{
Image(systemName: "person.circle")
Text("You")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .you ? .purple : .secondary)
Spacer()
}
}
.padding(.bottom, 30)
.padding(.top, 10)
.background(Color(red: 0.95, green: 0.95, blue: 0.95))
.font(.system(size: 30))
.frame(height: 80)
}
}
struct ContentView: View {
#State var tabIdx: Tab = .borrow
var body: some View {
NavigationView {
VStack(spacing: 20) {
Spacer()
if tabIdx == .borrow {
Text("Borrow")
} else if tabIdx == .ret {
Text("Return")
} else if tabIdx == .device {
Text("Device")
} else if tabIdx == .you {
Text("You")
}
Spacer(minLength: 0)
TabView(tabIdx: self.$tabIdx)
.shadow(radius: 10)
}
.ignoresSafeArea()
}
}
}
Remember that when you do this, all your tabs are specified as cases within enum Tab {}, and the TabView() contains some Button elements, which will change the tab by using the #State var tabIdx. So you can adjust your actions by modifying the self.tabIdx = <yourtab> statement.
The active color is set with this statement after each button:
.foregroundColor(self.tabIdx == .borrow ? .purple : .secondary)
Here you can just change .purple to whatever suits you.
You can see that on the ContentView, the if-else if block catches the SubView. Here I have placed some Text() elements like Text("Borrow"), so you can replace them by calling something like BorrowView() or whatever you have.
I have added the shadow when I call the TabView from within the ContentView, but you could even add the .shadow(radius: 10) after the HStack of the TabView itself.
This would be the final output:
                 

SwiftUI : Apply different animation style for Button using enum(s)?

I have a ForEach loop that displays all of the Buttons with icons from enum Icons. Every time I tap a button it will stay active until I tap another button ( same animation for every button case ).
enum Icons: String,CaseIterable, Hashable {
case overlayText = "Text"
case image = "Image"
case rotate = "Rotate"
case audio = "Audio"
case merge = "Merge"
case split = "Split"
case duplicate = "Duplicate"
case none
}
struct EffectPanel: View {
#State var currentIconSelected: Icons = .none
#State var listIcons = [Bool](repeating: false, count: Icons.allCases.count)
var body: some View {
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(Icons.allCases, id: \.self) { i in
if i != .none {
EffectIcon(icon: Icons(rawValue: i.rawValue)!, currentIconSelected: $currentIconSelected)
}
}
}
}
.background(Color.black)
}
}
}
Now I want to add a second animation style for 3 selected buttons, for example for the cases "rotate, audio, merge". Right now, I manually apply the second animation style to the case "rotate" by using
if currentIconSelected.rawValue == "Rotate"
this is my EffectIcon View:
struct EffectIcon: View {
var icon: Icons = .none
#Binding var currentIconSelected: Icons
var isSelected: Bool {
if icon == currentIconSelected { return true}
return false
}
#State var buttonFlash: Bool = true
var body: some View {
HStack(spacing: 0){
Button(action: {
self.currentIconSelected = self.icon
}){
ZStack {
if isSelected {
// ---- begin manually applying animation ---- //
if currentIconSelected.rawValue == "Rotate" {
HStack {
if buttonFlash {
Image("\(Icons.allCases[1].rawValue)")
.font(.title2)
.frame(width: 65, height: 70)
} else {
Image("\(Icons.allCases[1].rawValue)Active")
.font(.title2)
.frame(width: 65, height: 70)
}
}
.onTapGesture {
self.colorChange()
}
// ---- end manually applying animation ---- //
} else {
Image("\(icon.rawValue)Active")
.font(.title2)
.frame(width: 65, height: 70)
Text("\(icon.rawValue)")
.fontWeight(.bold)
.font(.system(size: 12))
.foregroundColor(Color("orange"))
.frame(alignment: .leading)
.lineLimit(1)
.padding(.top, 56)
}
} else {
Image("\(icon.rawValue)")
.font(.title2)
.frame(width: 65, height: 70)
}
}
}
}
}
private func colorChange() {
self.buttonFlash.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(Int(0.1)), execute: {
withAnimation(.easeIn){
self.buttonFlash.toggle()
}
})
}
}
I know there are better ways to implement ( using another enum + init... ) but I have not figured it out yet.
Thank you so much for your help!
Here's some code to get you started the right path. I don't have your custom icons, so I just used a systemIcon for the example.
Notes:
In SwiftUI it's extremely powerful to use ternary operators to manage the state of a view. This way, for example, you only need to write out Image() and set its font/frame/etc. one time. They basically let you write "If true do x otherwise do y."
By setting an image's renderingMode to Template, you can change it's color (if it's a compatible format)
I added a bunch of comments within the code too. Your question talked about adding a 'second animation'. In SwiftUI you can basically animate as many modifiers as you want to add multiple animations to a view.
I used a .onTapGesture() instead of a Button(). It's just a personal preference for this use case, but you can change it back if you prefer.
Code:
enum Icons: String,CaseIterable, Hashable {
case overlayText = "Text"
case image = "Image"
case rotate = "Rotate"
case audio = "Audio"
case merge = "Merge"
case split = "Split"
case duplicate = "Duplicate"
case none
}
struct EffectPanel: View {
#State var currentIconSelected: Icons = .none
#State var listIcons = [Bool](repeating: false, count: Icons.allCases.count)
var body: some View {
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(Icons.allCases, id: \.self) { icon in
if icon != .none {
// You are looping on the icon already
// Don't need to init a new Icon()
EffectIcon2(icon: icon, currentIconSelected: $currentIconSelected)
}
}
}
.background(Color.black)
}
}
}
}
struct EffectIcon2: View {
var icon: Icons
#Binding var currentIconSelected: Icons
var body: some View {
Image(systemName:
currentIconSelected == icon ?
"heart.fill" : // selected image
"heart" // not selected image
)
.renderingMode(.template)
.font(.title2)
.frame(width: 65, height: 70)
// animate color when selected
.foregroundColor(
icon == currentIconSelected ? .orange : .white
)
// animate opacity for .audio
.opacity(
(icon == .audio && icon == currentIconSelected) ?
0.1 :
1.0
)
// animate rotation for .rotate
.rotationEffect(
Angle(degrees:
(icon == .rotate && icon == currentIconSelected) ?
360 :
0
)
)
// animate scale for .merge
.scaleEffect(
(icon == .merge && icon == currentIconSelected) ?
1.5 :
1.0
)
// When icon is selected, Animation is set to .repeatForever
// When not selected, Animation returns to .default
// Autoreverses set to false for .rotate animation only (optional)
// - You can change .linear(duration) for different animation timing
// - You can remove .repeatForever(autoreverses) for only 1 loop
.animation(
currentIconSelected == icon ?
Animation.linear(duration: 1.0)
.repeatForever(autoreverses: (icon == .rotate) ? false : true) :
.default
)
// Add a background layer with the Text
// This is similar to a ZStack (a matter of coding preference)
// The order which you add modifers is important in SwiftUI
// By adding .background after the .animation, it will NOT animate with the previous
// Like ZStack, Background layers have alignment abilities (currently .bottom)
// Animate opacity so that Text only appears when selected
.background(
Text("\(icon.rawValue)")
.fontWeight(.bold)
.font(.caption)
.foregroundColor(.orange)
.lineLimit(1)
.opacity(
icon == currentIconSelected ?
1.0 :
0.0
)
.padding(.bottom, 4)
.animation(.easeInOut)
, alignment: .bottom
)
.onTapGesture {
currentIconSelected = icon
}
}
}

Spacer Breaks Button Appearance, Button Still Toggles

I have created an HStack that houses a collection of buttons that change appearance when selected and deselected. Only one button can be selected at a time. When adding a Spacer() between items, the button toggle works, however the appearance no longer changes, even though the index selected is changing as it's supposed to.
struct GenericFilterButton: View {
var category: String
var textColor: Color
var buttonColor: Color
var body: some View {
Text(category)
.font(.custom("Avenir Heavy", size: 14))
.foregroundColor(textColor)
.padding(.vertical, 10)
.padding(.horizontal, 14)
.background(buttonColor)
.cornerRadius(50)
.lineLimit(1)
}
}
struct FilterViewCompletionPercent: View {
var completionPercent = ["Any", "25%", "50%", "75%", "100%"]
#State var percentSelected = 0
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Completion")
.font(.custom("Avenir Heavy", size: 18))
.foregroundColor(Color.black.opacity(0.5))
.padding(.bottom, 20)
HStack(alignment: .center) {
ForEach(0..<completionPercent.count) { i in
Button(action: {
self.percentSelected = i
print(self.percentSelected)
}) {
GenericFilterButton(category: self.completionPercent[i], textColor: self.percentSelected == i ? Color.white : Color.black.opacity(0.5), buttonColor: self.percentSelected == i ? Color.blue : Color.white.opacity(0.0))
}
if i != 4 {
Spacer()
}
}
}.padding(.bottom, 20)
Divider()
.background(Color.black.opacity(0.1))
}.padding(.horizontal, 25)
.padding(.top, 30)
}
}
Why may this be happening? I can't seem to think of a plausible reason why adding a Spacer() between items doesn't work, but when adding a Spacer() without the if statement does.
Dynamic content of ForEach should be single view, so embed your Button-Spacer pair into another HStack.
Tested with Xcode 11.4 / iOS 13.4
HStack(alignment: .center) {
ForEach(0..<completionPercent.count, id: \.self) { i in
HStack { // << this one !!
Button(action: {
self.percentSelected = i
print(self.percentSelected)
}) {
GenericFilterButton(category: self.completionPercent[i], textColor: self.percentSelected == i ? Color.white : Color.black.opacity(0.5), buttonColor: self.percentSelected == i ? Color.blue : Color.white.opacity(0.0))
}
if i != 4 {
Spacer()
}
}
}
}.padding(.bottom, 20)