SwiftUI Transition + LinearGradient + Opacity weird behavior - swiftui

I'm trying to animate opacity on appear/disappear of Text view with a simple linear gradient.
Here's the "minimum" code I have right now:
import SwiftUI
struct ContentView: View {
#State var shown: Bool = true
var body: some View {
VStack {
ZStack {
if shown {
TextView()
}
}
.frame(height: 100)
Button("Show Toggle") {
shown.toggle()
}
}
}
}
struct TextView: View {
var body: some View {
text
.overlay(gradient)
.mask(text)
.transition(transition)
}
var gradient: LinearGradient {
LinearGradient(
colors: [Color.white, Color.blue],
startPoint: .leading, endPoint: .trailing
)
}
var transition: AnyTransition {
.asymmetric(
insertion: .opacity.animation(.linear(duration: 0.500)),
removal: .opacity.animation(.linear(duration: 0.500))
)
}
var text: some View {
Text("Hello World")
.fontWeight(.bold)
.font(.largeTitle)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Notice: I'm using the view.overlay(gradient).mask(view) pattern in order for the gradient view to not be greedy.
As a result, the animation looks like this:
Notice how the view doesn't "just" fade out -- it first turns black and then fades out.
I can fix opacity issues by just doing gradient.mask(text) instead of the other pattern, but then I run into other issues with the gradient view being greedy

You are drawing a black text, then overlay the gradient, then clip it with the text. Just omit the first black text, e.g.by setting its opacity to 0.
text.opacity(0)
.overlay(gradient)
.mask(text)
.transition(transition)
or use .drawingGroup which renders the whole view offscreen before displaying:
text
.overlay(gradient)
.mask(text)
.drawingGroup() // here
.transition(transition)

Related

Why is Text onTapGesture not working for widened frame in SwiftUI?

I have this testing file that is not working how I expect it to.
import SwiftUI
struct SwiftUIView: View {
#State var boolTest = false
var nums = ["1","2","3","4","5","6","7"]
var body: some View {
VStack {
ForEach(nums, id: \.self) { num in
Text("\(num)")
.frame(width: 400)
.font(.system(size: 70))
.foregroundColor(boolTest ? .red : .green)
.onTapGesture {
boolTest.toggle()
}
}
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
When I tap on a number, the foreground color changes as expected. However, I want to be able to tap on the areas left and right of the Text("\(num)") so I expanded the frame modifier to test. However the color only changes when I tap directly on the number or text.
How do I tap on the space left or right of the number and have it change colors instead of it doing nothing?
The system treats the "invisible" (ie doesn't have visible drawn content) part of the view as unresponsive unless you set a contentShape on it.
//...
.frame(width: 400)
.contentShape(Rectangle())
//...

SwiftUI changing navigation bar background color for inline navigationBarTitleDisplayMode

I just started coding in SwiftUI and came across a problem. I need to give different colors to the background of the navigation bar (NavigationView). The colors will change as I go from one view to the next. I need to have this working for navigationBarTitleDisplayMode being "inline".
I tried the solutions presented in:
SwiftUI update navigation bar title color
but none of these solutions work fully for what I need.
The solution in this reply to that post works for inline:
Using UIViewControllerRepresentable. Nevertheless, when we first open the view it will show the color of the previous view for one second, before changing to the new color. I would like to avoid this and have the color displayed as soon as everything appears on screen. Is there a way to do this?
This other solution will not work either: Changing UINavigation's appearance in init(), because when I set the background in init(), it will change the background of all the views in the app. Again, I need the views to have different background colors.
I tried something similar to this solution: Modifying Toolbar, but it does not allow me to change the color of the navigation bar.
The other solution I tried was this: Creating navigationBarColor function, which is based on: NAVIGATIONVIEW DYNAMIC BACKGROUND COLOR IN SWIFTUI. This solution works for navigationBarTitleDisplayMode "large", but when setting navigationBarTitleDisplayMode to "inline", it will show the background color of the navigation bar in a different color, as if it was covered by a gray/transparent layer. For example, the color it shows in "large" mode is:
Red color in large mode
But instead, it shows this color:
Red color in inline mode
Finally, I tried this solution: Subclassing UIViewController and configuring viewDidLayoutSubviews(), but it did not work for what I want it either.
The closest solutions for what I need are 1. and 4., but they still do not work 100%.
Would anybody know how to make any of these solutions work for navigationBarTitleDisplayMode inline, being able to change the background color of the navigation bar in different layouts, and showing the new color once the view is shown (without delays)?
Thank you!
By the way, I am using XCode 12.5.
Here is the sample code that I am using, taking example 4. as a model:
FirstView.swift
import SwiftUI
struct FirstView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
GeometryReader { metrics in
VStack {
Text("This is the first view")
NavigationLink(destination: SecondView(), tag: "SecondView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "SecondView"
print("Go to second view")
}) {
Text("Go to second view")
}
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
On this screen, if I use
.navigationBarTitleDisplayMode(.large)
the color will be displayed properly: Navigation bar with red color
But using
.navigationBarTitleDisplayMode(.inline)
there is a blur on it: Navigation bar with some sort of blur over red color
import SwiftUI
struct SecondView: View {
#State private var selection: String? = nil
var body: some View {
GeometryReader { metrics in
VStack {
Text("This is the second view")
NavigationLink(destination: ThirdView(), tag: "ThirdView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "ThirdView"
print("Go to third view")
}) {
Text("Go to third view")
}
}
}
.navigationBarColor(backgroundColor: Color.red, titleColor: .black)
.navigationBarTitleDisplayMode(.inline)
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
ThirdView.swift
This view displays the color properly as it is using
.navigationBarTitleDisplayMode(.large)
But if changed to
.navigationBarTitleDisplayMode(.inline)
it will show the blur on top of the color as well.
import SwiftUI
struct ThirdView: View {
var body: some View {
GeometryReader { metrics in
Text("This is the third view")
}
.navigationBarColor(backgroundColor: Color.blue, titleColor: .black)
.navigationBarTitleDisplayMode(.large)
}
}
struct ThirdView_Previews: PreviewProvider {
static var previews: some View {
ThirdView()
}
}
NavigationBarModifierView.swift
import SwiftUI
struct NavigationBarModifier: ViewModifier {
var backgroundColor: UIColor?
var titleColor: UIColor?
init(backgroundColor: Color, titleColor: UIColor?) {
self.backgroundColor = UIColor(backgroundColor)
let coloredAppearance = UINavigationBarAppearance()
coloredAppearance.configureWithTransparentBackground()
coloredAppearance.backgroundColor = UIColor(backgroundColor)
coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.shadowColor = .clear
UINavigationBar.appearance().standardAppearance = coloredAppearance
UINavigationBar.appearance().compactAppearance = coloredAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
UINavigationBar.appearance().tintColor = titleColor
}
func body(content: Content) -> some View {
ZStack{
content
VStack {
GeometryReader { geometry in
Color(self.backgroundColor ?? .clear)
.frame(height: geometry.safeAreaInsets.top)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}
}
extension View {
func navigationBarColor(backgroundColor: Color, titleColor: UIColor?) -> some View {
self.modifier(NavigationBarModifier(backgroundColor: backgroundColor, titleColor: titleColor))
}
}
NOTE TO THE MODERATORS: Please, do not delete this post. I know similar questions were asked before, but I need an answer to this in particular which was not addressed. Please read before deleting indiscriminately, I need this for work. Also, I cannot ask questions inline in each of those solutions because I do not have the minimum 50 points in stackoverflow required to write there.
I think I have what you want. It is VERY touchy... It is a hack, and not terribly robust, so take as is...
I got it to work by having your modifier return a clear NavBar, and then the solution from this answer works for you. I even added a ScrollView to ThirdView() to make sure that scrolling under didn't affect in. Also note, you lose all of the other built in effects of the bar like translucency, etc.
Edit: I went over the code. The .navigationViewStyle was in the wrong spot. It likes to be outside of the NavigaionView(), where everything else needs to be inside. Also, I removed the part of the code setting the bar color in FirstView() as it was redundant and ugly. I hadn't meant to leave that in there.
struct NavigationBarModifier: ViewModifier {
var backgroundColor: UIColor?
var titleColor: UIColor?
init(backgroundColor: Color, titleColor: UIColor?) {
self.backgroundColor = UIColor(backgroundColor)
let coloredAppearance = UINavigationBarAppearance()
coloredAppearance.configureWithTransparentBackground()
coloredAppearance.backgroundColor = .clear // The key is here. Change the actual bar to clear.
coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.shadowColor = .clear
UINavigationBar.appearance().standardAppearance = coloredAppearance
UINavigationBar.appearance().compactAppearance = coloredAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
UINavigationBar.appearance().tintColor = titleColor
}
func body(content: Content) -> some View {
ZStack{
content
VStack {
GeometryReader { geometry in
Color(self.backgroundColor ?? .clear)
.frame(height: geometry.safeAreaInsets.top)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}
}
extension View {
func navigationBarColor(backgroundColor: Color, titleColor: UIColor?) -> some View {
self.modifier(NavigationBarModifier(backgroundColor: backgroundColor, titleColor: titleColor))
}
}
struct FirstView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
GeometryReader { _ in
VStack {
Text("This is the first view")
NavigationLink(destination: SecondView(), tag: "SecondView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "SecondView"
print("Go to second view")
}) {
Text("Go to second view")
}
}
.navigationTitle("First")
.navigationBarTitleDisplayMode(.inline)
.navigationBarColor(backgroundColor: .red, titleColor: .black)
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SecondView: View {
#State private var selection: String? = nil
var body: some View {
VStack {
Text("This is the second view")
NavigationLink(destination: ThirdView(), tag: "ThirdView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "ThirdView"
print("Go to third view")
}) {
Text("Go to third view")
}
}
.navigationTitle("Second")
.navigationBarTitleDisplayMode(.inline)
.navigationBarColor(backgroundColor: .blue, titleColor: .black)
}
}
struct ThirdView: View {
var body: some View {
ScrollView {
ForEach(0..<50) { _ in
Text("This is the third view")
}
}
.navigationTitle("Third")
.navigationBarTitleDisplayMode(.inline)
.navigationBarColor(backgroundColor: .green, titleColor: .black)
}
}
iOS 16
Since this version of SwiftUI, there is a dedicated modifier for setting any toolbar background color (including the navigation bar):
Xcode 14 beta 5 (Not working 🤦🏻‍♂️, waiting for beta 6...)
.toolbarBackground(.red, for: .navigationBar)
Xcode 14 beta 1,2,3,4
.toolbarBackground(.red, in: .navigationBar)
It works perfectly in in inline mode and also animates between modes.
For my custom view the following code worked well.
struct HomeView: View {
init() {
//Use this if NavigationBarTitle is with Large Font
UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.systemIndigo]
//Use this if NavigationBarTitle is with displayMode = .inline
UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.systemIndigo]
UINavigationBar.appearance().backgroundColor = UIColor.clear
UINavigationBar.appearance().barTintColor = UIColor(Color(red: 32 / 255, green: 72 / 255, blue: 63 / 255))
}
var body: some View {
NavigationView {
ZStack {
...
...
...
}
.padding(.zero)
.navigationTitle("Feedbacks")
}
}
}
and result is like that:
Here is a bit hacky solution, but it works for me (as of iOS 15) both for .large and .inline display modes.
import SwiftUI
enum Kind: String, CaseIterable {
case checking
case savings
case investment
}
struct PaddedList: View {
#Binding var name: String
#Binding var kind: Kind
var body: some View {
NavigationView {
List {
TextField("Account name", text: $name)
Picker("Kind", selection: $kind) {
ForEach(Kind.allCases, id: \.self) { kind in
Text(kind.rawValue).tag(kind)
}
}
.listRowSeparatorTint(.red)
Spacer()
}
.padding(.top, 1) // note top 1 padding!
.background(.green) // the color "bleeds" through
.navigationBarTitle("Navigation Bar")
}
}
}
struct PaddedList_Previews: PreviewProvider {
static var previews: some View {
PaddedList(name: .constant(""), kind: .constant(.checking))
}
}

How to add shade to Image swiftui?

I want to take the top image and add shade so it looks like the bottom.
I've tried;
.renderingMode(.template)
.accentColor(.black.opacity(0.5))
.foreGroundColor(.black.opacity(0.2)
and other things.
I prefer a negative brightness:
struct ContentView: View {
var body: some View {
Image("green-phone-pocket 2")
.resizable()
.aspectRatio(contentMode: .fit)
.brightness(-0.3)
}
}
No brightness modifier
-0.1
-0.3
It is simpler by adding an overlay:
Image(...)
.modifiers(...)
.overlay(Color.black.opacity(0.1)
You can animate it too:
struct HomeView: View {
#State private var hover = false
var body: some View {
Image(...)
.modifiers(...)
.onHover { hover in withAnimation { self.hover = hover } }
.overlay(Color.black.opacity(hover ? 0.2 : 0))
}
}
Here is one way, though I would prefer a predefined way.
ZStack {
Image("green-phone-pocket 2")
Color.black.opacity(0.5)
}

How do I set a background view's size equal to a foreground view's size?

I have a foreground view and a background view. The foreground view contains some text, images, and a couple of stack views. It automatically sizes itself and looks perfect. I want to add a background view that matches the automatic size of my foreground view. My background view is a View composed of some shapes.
Things I've tried:
If I use a ZStack, my background view sizes itself (filling the container).
If I overlay the background on top of the foreground, the background view has the correct size but it obscures the foreground view.
If I overlay the foreground view on top of the background view, the background view determines it's own size.
Code Sample
Foreground View
struct ForegroundView: View {
var body: some View {
HStack {
Image(systemName: "photo")
VStack(alignment: .leading) {
Text("Title")
Text("Subtitle")
}
Spacer()
Text("42")
}
}
}
struct ForegroundView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Background View
The background view is a progress bar.
struct ProgressBar: View {
#Binding var value: Float
var body: some View {
GeometryReader { gr in
ZStack(alignment: .leading) {
Rectangle()
.frame(width: gr.size.width,
height: gr.size.height)
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
Rectangle()
.frame(width: min(CGFloat(self.value)*gr.size.width, gr.size.width),
height: gr.size.height)
.foregroundColor(Color(UIColor.systemBlue))
.animation(.linear)
}
}
}
}
struct ProgressBar_Previews: PreviewProvider {
static var previews: some View {
ProgressBar(value: .constant(0.5))
}
}
Things I Tried
struct Demo_Preview: PreviewProvider {
static var previews: some View {
Group {
// Attempt 1
ZStack {
ProgressBar(value: .constant(0.5))
ForegroundView()
}
// Attempt 2
ForegroundView().overlay(ProgressBar(value: .constant(0.5)))
// Attempt 3
ProgressBar(value: .constant(0.5)).overlay(ForegroundView())
}
}
}
The answer, it turns out, is quite simple: the background modifier.
struct Demo_Preview: PreviewProvider {
static var previews: some View {
Group {
ForegroundView().background(ProgressBar(value: .constant(0.5)))
}
}
}

SwiftUI popover background color

With UIKit, I could customize the background color of a popover using UIPopoverPresentationController's backgroundColor.
This would change the color including the arrow.
How can I accomplish this with SwiftUI? When I change the the popover content's background color, it doesn't include the arrow:
You can use .scaleEffect(…) to make your background view take more space. When it’s bigger than the content of the popover, it will fill the arrow.
struct ContentView: View {
#State var popoverVisible = false
var body: some View {
VStack {
Button("Open Popover") {
self.popoverVisible = true
}
.popover(isPresented: $popoverVisible) {
ZStack {
// Scaled-up background
Color.blue
.scaleEffect(1.5)
Text("Hello world")
.foregroundColor(.white)
.padding()
}
}
}
}
}
Use i.e. a VStack to fill the view, and then background will be extrapolated from the view at the edge:
struct ContentView: View {
#State var popoverVisible = false
var body: some View {
VStack {
Button("Open Popover") {
self.popoverVisible = true
}
.popover(isPresented: $popoverVisible) {
VStack {
Text("Hello world")
.foregroundColor(.white)
.padding(8)
Spacer()
Text("Bye world")
.foregroundColor(.white)
.background(Color.blue)
}
}
}
}
}