SwiftUI Image Opacity Animation affecting Other Views - swiftui

I'm trying to build a list with progress bar and status icon. I want to animate the icon to indicate that some download activity is in progress. However, when I apply the animation, it also affects the progress bar and causes it to animate.
Here's the code for the list view:
List(syncProgres.downloadQueue) { mediaFile in
VStack(alignment: .leading) {
Text(mediaFile.path).font(.callout).lineLimit(2).truncationMode(.middle)
Spacer()
ProgressView(value: Double(mediaFile.downloadedBytes) / Double(mediaFile.downloadTotalBytes))
HStack {
Text("\(MiscUtils.toHumanReadable(bytes: mediaFile.downloadedBytes)) of \(MiscUtils.toHumanReadable(bytes: mediaFile.downloadTotalBytes))").font(.footnote)
Spacer()
Image(systemName: "arrow.down.doc").opacity(opacity)
.onAppear {
let baseAnimation = Animation.linear(duration: 1)
let repeated = baseAnimation.repeatForever(autoreverses: true)
withAnimation(repeated) {
opacity = 0.2
}
}
}
}.frame(height: 80, alignment: .center)
}

So swiftUI view modifiers work bottom to top, if the progress bar is getting the animation from the view below it add '.animation(.default)' below the view that you don't want affected.
List(syncProgres.downloadQueue) { mediaFile in
VStack(alignment: .leading) {
Text(mediaFile.path).font(.callout).lineLimit(2).truncationMode(.middle)
Spacer()
ProgressView(value: Double(mediaFile.downloadedBytes) / Double(mediaFile.downloadTotalBytes))
.animation(.default)
HStack {
Text("\(MiscUtils.toHumanReadable(bytes: mediaFile.downloadedBytes)) of \(MiscUtils.toHumanReadable(bytes: mediaFile.downloadTotalBytes))").font(.footnote)
Spacer()
Image(systemName: "arrow.down.doc").opacity(opacity)
.onAppear {
let baseAnimation = Animation.linear(duration: 1)
let repeated = baseAnimation.repeatForever(autoreverses: true)
withAnimation(repeated) {
opacity = 0.2
}
}
}
}.frame(height: 80, alignment: .center)
}

Related

Recrete animated "tab" buttons from iOS Photos

I want to recreate the animated buttons found in the Photos app and shown below. My goal is to use this type of buttons in a TabView(or something similar) instead of the default ones. Does these type of buttons exist in swiftUI? Or what is a good way to create these buttons?
I have written some stupid code to illustrate the problem, but it feels like the wrong approach.
struct ContentView: View {
#State private var selection = 0
var body: some View {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.frame(width: 100, height: 50)
.offset(x: CGFloat(selection * 100), y: 0)
HStack {
Button("Tap Me") {
withAnimation {
selection = 0
}
}
Spacer()
Button("Tap Me") {
withAnimation {
selection = 1
}
}
Spacer()
Button("Tap Me") {
withAnimation {
selection = 2
}
}
}
.frame(width: 300)
}
.padding()
.background(.green)
}
}
Output:

SwiftUI image with borders overlaying button

Thanks to a previous question I asked, I am using Color.black.overlay and .clipped() to show an image with letterbox borders above and below it.
But when I attempt to put a button on the top border, it can't be tapped on (I assume because the image (unclipped) is in that space, and is intercepting the tap gesture).
Here is what the layout looks like:
Here is the code:
var body: some View {
ZStack {
Color.black
VStack {
topBorder
imageMiddle
bottomBorder
}
}
.ignoresSafeArea()
}
var topBorder: some View {
return Group {
ZStack {
Rectangle()
.fill(.green)
.frame(minHeight: borderHeight, maxHeight: borderHeight)
Button {
print("tap")
} label: {
Image(systemName: "hand.tap.fill")
.foregroundColor(.black)
}
}
}
}
var bottomBorder: some View {
return Group {
Rectangle()
.fill(.green)
.frame(minHeight: borderHeight, maxHeight: borderHeight)
}
}
var imageMiddle: some View {
return Group {
Color.black.overlay(
Image("cat")
.scaledToFill()
)
.clipped()
}
}
How can I expose that button to a user's tap?
Adding .allowsHitTesting(false) to your image view will fix it. However, it seems like the wrong approach.
VStack {
topBorder
imageMiddle
.allowsHitTesting(false) // <- This will fix your problem.
bottomBorder
}
I would recommend using another approach to add your borders on top of the image instead. Something like this:
ZStack {
imageMiddle
VStack {
topBorder
.overlay(alignment: .bottom) {
Rectangle().frame(minHeight: 0, maxHeight: 10)
}
Spacer()
bottomBorder
.overlay(alignment: .top) {
Rectangle().frame(minHeight: 0, maxHeight: 10)
}
}
}
.ignoresSafeArea()

Set View Picker Header Color?

Is it possible to add background color to the top of a picker view? I'm specifically interested in the area from the top of the view down to just below the navigation bar. I have made all my app tab and navigation views with the same header appearance but haven't had any luck with the pickers.
I would like to set the picker backgrounds to look like this:
Here is a picker view that I would like to add background color.
Here is what happens when adding call to DrawNavBox() just after the Picker (See code below). I need help pushing the green block to the top of the view.
The code below is what generates the Home Currency green header above
var body: some View {
GeometryReader { g in
VStack {
DrawNavBox(g: g) // draw green header
Text("Your home currency is:")
.padding(.top, g.size.height * 0.10)
Text("\(base.baseCur.baseCurrency) \(base.baseCur.baseCountry)")
.font(.body)
.fontWeight(.bold)
.foregroundColor(.white)
.padding(15)
.background(Color("Dark Green"))
.cornerRadius(20)
.padding(.trailing, g.size.width * 0.19)
.padding(.leading, g.size.width * 0.22)
Form {
Section(header: Text("Select Home Currency")) {
Picker("Home Currency", selection: $gotBase) {
DrawNavBox(g: g)
ForEach(0 ..< currCountry.count, id: \.self) { item in
VStack(alignment: .leading) {
Text(currCountry[item])
.font(.subheadline)
.padding(.leading, 25)
HStack {
Text(currSymbol[item])
Text(currName[item])
}
.font(.caption)
.padding(.leading, 25)
}
}
}
}
}
}
.navigationBarTitle(Text("Home Currency"), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
// search for new base currency
self.changeBaseCurrency()
}) {
Text ("Save").bold()
}
)}
}
}
This is the code I have been using to set the navigation bar area green
struct DrawNavBox: View {
var g: GeometryProxy
var body: some View {
ZStack (alignment: .top) {
Color(.systemGreen)
.frame(height: (g.safeAreaInsets.top), alignment: .top)
.ignoresSafeArea()
}
}
}

Transparent background SwiftUI

I have a list in swiftUI and I want the background to be somewhat see through. However there seems to be a white background applied to both scrollview and List. I was wondering if anyone had a work around or way to change it so when .background(Color.white.opacity(0.7)) is applied to the list, it can be translucent and not have the same affect as .background(Color.white).
Here is my code for the view that I am trying to implement the desired effect. The wordPosts is simply just a custom data struct used to populate the list item data. I've already trying changing the cell and tableview appearance using UIkit on init which did not work.
struct ProfileWordsView: View {
init(){
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
}
var body: some View {
List {
ForEach(wordPosts) { post in
Group {
VStack(alignment: .leading) {
HStack(alignment: .top) {
RoundedRectangle(cornerRadius: 5)
.fill()
.frame(width: 4)
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 5) {
Text(post.title).font(.custom("Gilroy-SemiBold", size: 20)).foregroundColor(.black).lineLimit(1)
Text("• \(post.timestamp)").font(.custom("Gilroy-SemiBold", size: 19)).foregroundColor(.gray)
}
HStack {
Text(post.text).font(.custom("Gilroy-Regular", size: 16)).foregroundColor(Color.black.opacity(0.7)).padding(.top, 5)
}
}
Spacer()
VStack {
Button(action: {}) {
Image(systemName: "chevron.up")
.resizable()
.frame(width: 20, height: 15)
.font(Font.title3)
.foregroundColor(.gray)
.padding(.bottom, 6)
}
Text("\(post.rizz)").font(.custom("Gilroy-SemiBold", size: 18)).foregroundColor(.gray)
Button(action: {}) {
Image(systemName: "chevron.down")
.resizable()
.frame(width: 20, height: 15)
.font(Font.title3)
.foregroundColor(.gray)
}
}
}
}.listRowInsets(EdgeInsets(top: 10, leading: 5, bottom: 10, trailing: 10))
}.listRowBackground(Color.clear)
}
.listRowSeparator(.hidden)
}.edgesIgnoringSafeArea(.all).listStyle(.plain)
}
}
I'm not sure if it is possible for your code but I'd recommend using a scrollview instead. They are transparent by default, so that should help. You can always add a frame with a semi-transparent color if you want to have a semi-transparent background instead.

SwiftUI Button tap only on text portion

The background area of my button is not detecting user interaction. Only way to interact with said button is to tap on the Text/ Label area of the button. How to make entire Button tappable?
struct ScheduleEditorButtonSwiftUIView: View {
#Binding var buttonTagForAction : ScheduleButtonType
#Binding var buttonTitle : String
#Binding var buttonBackgroundColor : Color
let buttonCornerRadius = CGFloat(12)
var body: some View {
Button(buttonTitle) {
buttonActionForTag(self.buttonTagForAction)
}.frame(minWidth: (UIScreen.main.bounds.size.width / 2) - 25, maxWidth: .infinity, minHeight: 44)
.buttonStyle(DefaultButtonStyle())
.lineLimit(2)
.multilineTextAlignment(.center)
.font(Font.subheadline.weight(.bold))
.foregroundColor(Color.white)
.border(Color("AppHighlightedColour"), width: 2)
.background(buttonBackgroundColor).opacity(0.8)
.tag(self.buttonTagForAction)
.padding([.leading,.trailing], 5)
.cornerRadius(buttonCornerRadius)
}
}
The proper solution is to use the .contentShape() API.
Button(action: action) {
HStack {
Spacer()
Text("My button")
Spacer()
}
}
.contentShape(Rectangle())
You can change the provided shape to match the shape of your button; if your button is a RoundedRectangle, you can provide that instead.
I think this is a better solution, add the .frame values to the Text() and the button will cover the whole area 😉
Button(action: {
//code
}) {
Text("Click Me")
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .center)
.foregroundColor(Color.white)
.background(Color.accentColor)
.cornerRadius(7)
}
You can define content Shape for hit testing by adding modifier: contentShape(_:eoFill:)
And important thing is you have to apply inside the content of Button.
Button(action: {}) {
Text("Select file")
.frame(width: 300)
.padding(100.0)
.foregroundColor(Color.black)
.contentShape(Rectangle()) // Add this line
}
.background(Color.green)
.cornerRadius(4)
.buttonStyle(PlainButtonStyle())
Another
Button(action: {}) {
VStack {
Text("Select file")
.frame(width: 100)
Text("Select file")
.frame(width: 200)
}
.contentShape(Rectangle()) // Add this inside Button.
}
.background(Color.green)
.cornerRadius(4)
.buttonStyle(PlainButtonStyle())
This fixes the issue on my end:
var body: some View {
GeometryReader { geometry in
Button(action: {
// Action
}) {
Text("Button Title")
.frame(
minWidth: (geometry.size.width / 2) - 25,
maxWidth: .infinity, minHeight: 44
)
.font(Font.subheadline.weight(.bold))
.background(Color.yellow).opacity(0.8)
.foregroundColor(Color.white)
.cornerRadius(12)
}
.lineLimit(2)
.multilineTextAlignment(.center)
.padding([.leading,.trailing], 5)
}
}
Is there a reason why you are using UIScreen instead of GeometryReader?
Short Answer
Make sure the Text (or button content) spans the length of the touch area, AND use .contentShape(Rectangle()).
Button(action:{}) {
HStack {
Text("Hello")
Spacer()
}
.contentShape(Rectangle())
}
Long Answer
There are two parts:
The content (ex. Text) of the Button needs to be stretched
The content needs to be considered for hit testing
To stretch the content (ex. Text):
// Solution 1 for stretching content
HStack {
Text("Hello")
Spacer()
}
// Solution 2 for stretching content
Text("Hello")
.frame(maxWidth: .infinity, alignment: .leading)
// Alternatively, you could specify a specific frame for the button.
To consider content for hit testing use .contentShape(Rectangle()):
// Solution 1
Button(action:{}) {
HStack {
Text("Hello")
Spacer()
}
.contentShape(Rectangle())
}
// Solution 2
Button(action:{}) {
Text("Hello")
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
You might be doing this:
Button { /*to do something on button click*/}
label: { Text("button text").foregroundColor(Color.white)}
.frame(width: 45, height: 45, alignment: .center)
.background(Color.black)
Solution:
Button(action: {/*to do something on button click*/ })
{
HStack {
Spacer()
Text("Buttton Text")
Spacer() } }
.frame(width: 45, height: 45, alignment: .center)
.foregroundColor(Color.white)
.background(Color.black).contentShape(Rectangle())
A bit late to the answer, but I found two ways to do this —
Option 1: Using Geometry Reader
Button(action: {
}) {
GeometryReader { geometryProxy in
Text("Button Title")
.font(Font.custom("SFProDisplay-Semibold", size: 19))
.foregroundColor(Color.white)
.frame(width: geometryProxy.size.width - 20 * 2) // horizontal margin
.padding([.top, .bottom], 10) // vertical padding
.background(Color.yellow)
.cornerRadius(6)
}
}
Option 2: Using HStack with Spacers
HStack {
Spacer(minLength: 20) // horizontal margin
Button(action: {
}) {
Text("Hello World")
.font(Font.custom("SFProDisplay-Semibold", size: 19))
.frame(maxWidth:.infinity)
.padding([.top, .bottom], 10) // vertical padding
.background(Color.yellow)
.foregroundColor(Color.white)
.cornerRadius(6)
}
Spacer(minLength: 20)
}.frame(maxWidth:.infinity)
My thought process here is that although option 1 is more succinct, I would choose option 2 since it's less coupled to its parent's size (through GeometryReader) and more in line of how I think SwiftUI is meant to use HStack, VStack, etc.
I was working with buttons and texts that need user interaction when I faced this same issue. After looking and testing many answers (including some from this post) I ended up making it works in the following way:
For buttons:
/* WITH IMAGE */
Button {
print("TAppeD")
} label: {
Image(systemName: "plus")
.frame(width: 40, height: 40)
}
/* WITH TEXT */
Button {
print("TAppeD")
} label: {
Text("My button")
.frame(height: 80)
}
For Texts:
Text("PP")
.frame(width: 40, height: 40)
.contentShape(Rectangle())
.onTapGesture {
print("TAppeD")
}
In the case of the texts, I only need the .contentShape(Rectangle()) modifier when the Text doesn't have a .background in order to make the entire Text frame responsive to tap gesture, while with buttons I use my Text or Image view with a frame and neither a .background nor a .contentShape is needed.
Image of the following code in preview (I'm not allowed to include pictures yet )
import SwiftUI
struct ContentView: View {
#State var tapped: Bool = true
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 19)
.frame(width: 40, height: 40)
.foregroundColor(tapped ? .red : .green)
Spacer()
HStack (spacing: 0) {
Text("PP")
.frame(width: 40, height: 40)
.contentShape(Rectangle())
.onTapGesture {
tapped.toggle()
}
Button {
print("TAppeD")
tapped.toggle()
} label: {
Image(systemName: "plus")
.frame(width: 40, height: 40)
}
.background(Color.red)
Button {
print("TAppeD")
tapped.toggle()
} label: {
Text("My button")
.frame(height: 80)
}
.background(Color.yellow)
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
this way makes the button area expand properly
but if the color is .clear, it dosen't work🤷‍♂️
Button(action: {
doSomething()
}, label: {
ZStack {
Color(.white)
Text("some texts")
}
})
When I used HStack then it worked for button whole width that's fine, But I was facing issue with whole button height tap not working at corners and I fixed it in below code:
Button(action:{
print("Tapped Button")
}) {
VStack {
//Vertical whole area covered
Text("")
Spacer()
HStack {
//Horizontal whole area covered
Text("")
Spacer()
}
}
}
If your app needs to support both iOS/iPadOS and macOS, you may want to reference my code!
Xcode 14.1 / iOS 14.1 / macOS 13.0 / 12-09-2022
Button(action: {
print("Saved to CoreData")
}) {
Text("Submit")
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 60, alignment: .center)
.foregroundColor(Color.white)
#if !os(macOS)
.background(Color.accentColor)
#endif
}
#if os(macOS)
.background(Color.accentColor)
#endif
.cornerRadius(7)
Easier work around is to add .frame(maxWidth: .infinity) modifier.
and wrap your button inside a ContainerView. you can always change the size of the button where it's being used.
Button(action: tapped) {
HStack {
if let icon = icon {
icon
}
Text(title)
}
.frame(maxWidth: .infinity) // This one
}