Blur view when button is pressed swiftui - swift

I have a view with a toolbar on the bottom of the view. When clicked - two buttons are displayed. I am trying to achieve when the toolbar is pressed and the buttons are now displayed, the view (or background) becomes blurred/grayed out, except for the newly produced items.
I attached a screenshot of the desired effect I am aiming for.
struct UserDashController: View {
// #State private var showMealView = false
#State private var showSettingsView = false
#State private var showAddViews = false
#State private var angle: Double = 0
init(){
UIToolbar.appearance().barTintColor = UIColor.white
}
var body: some View {
NavigationView {
VStack{
Text("Blue me Please")
.frame(width: 400, height:600)
.background(.orange)
}
//sets setting bar top right
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
VStack{
Button(action: {
showSettingsView.toggle()
}) {
Image(systemName: "line.3.horizontal")
.font(.title3)
.foregroundColor(.black)
}
.sheet(isPresented: $showSettingsView){
JournalEntryMain()
}
}
}
// sets add meal option bottom/center
ToolbarItem(placement: .bottomBar) {
//displaying add meal and recipe icons when clicked
HStack{
Button(action: {
angle += 90
showAddViews.toggle()
}) {
if showAddViews {
VStack{
AddToolbar(showAddOptions: $showAddViews)
.offset(y:-50)
}
}
Image(systemName: "plus.square")
.opacity(showAddViews ? 0.5 : 1)
.font(.largeTitle)
.foregroundColor(.black)
.rotationEffect(.degrees(angle))
.animation(.easeIn(duration: 0.25), value: angle)
}
}
}
}
}
}
}
Buttons that appear when toolbar is pressed
struct AddToolbar: View {
#Binding var showAddOptions: Bool
#State var showMealView = false
var body: some View {
HStack{
VStack{
Button(action: {
showMealView.toggle()
}){
VStack{
Image(systemName: "square.and.pencil")
.font(.title)
.foregroundColor(.black)
.background(Circle()
.fill(.gray)
.frame(width:50, height:50))
.padding(3)
Text("Meal")
.foregroundColor(.black)
}
}.fullScreenCover(isPresented: $showMealView){
JournalEntryMain()
}
}
VStack{
Image(systemName: "text.book.closed")
.foregroundColor(.black)
.font(.title)
.background(Circle()
.fill(.gray)
.frame(width:50, height:50))
.padding(3)
Text("Recipe")
.foregroundColor(.black)
}
.offset(y: -50)
}
.frame(height:150)
}
}
Desired Effect

I'm a little confused by your desired effect example, partially because in the UI screenshot you attached, the background isn't blurred, it's just darkened. So, the following answer isn't tailored to your specific example but still should be able to help.
Let's say whatever variable you're using to determine whether or not to show the toolbar is showSettingsView. You could put the following modifiers on your background view:
To blur: .blur(showSettingsView ? 0.5 : 0.0)
To darken: .brightness(showSettingsView ? -0.5 : 0.0)
Obviously just replace "0.5" with whatever number feels best.

Related

List padding issue - elements have weird left margin

Experiencing a weird issue with my list. Looking at the screenshot provided below, my sub titles under "Eggs" all have a left margin issue, which pushes the sub headers away from there starting position. I don't think it's my padding causing this, honestly stumped, hoping you guys could help out.
Thanks!
struct FoodSearchResultsView: View {
#EnvironmentObject private var foodApi: FoodApiSearch
#EnvironmentObject var mealEntryObj: MealEntrys
//textfield input
#State private var searchResultsItem = ""
//if toggled, will display, binded to search bar
#Binding var userSearch: Bool
//when false, api results will not display
#Binding var isViewSearching:Bool //sending to searchboar
var body: some View {
if userSearch{
VStack{
Text(isViewSearching ? "Results" : "Searching..")
Spacer()
// delays showing api call
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
self.isViewSearching = true
}
}
//if user has completed searching for a food
if isViewSearching{
List(foodApi.userSearchResults){meal in
VStack{
HStack{
VStack(alignment: .leading){
Text(meal.mealName)
.padding(.leading,-50)
HStack{
Text(meal.calories + "cals, ")
.font(.caption)
.offset(y:8)
Text(meal.brand)
.font(.caption)
.offset(y:8)
}
.frame(maxWidth:150)
.padding(.leading,-50)
}
.foregroundColor(.black)
//Spacer()
Button(action: {
userSearch = false
isViewSearching = false //is user actively searching, communicates with journalEntryMain
//push meal to meal entry break fast
mealEntryObj.mealEntrysBreakfast.append(meal)
}){
Image(systemName: "plus.app")
.font(.title2)
.foregroundColor(.blue)
.offset(x: 70)
.frame(width:50)
}
}
}
.frame(width:220, height:40) //width of background
.padding([.leading, .trailing], 60)
.padding([.top, .bottom], 10)
.background(RoundedRectangle(
cornerRadius:20).fill(Color("LightWhite")))
.foregroundColor(.black)
}
.listStyle(.plain)
.listRowSeparator(.hidden)
//.frame(height:400)
}
}
}
}
}

How to align the an image on top of a button in swiftui?

I wish to add a 'trash' image on the top-right side of each button when 'Delete Button' is pressed, so that when user hits the trash image, the button will be removed from the vstack.
I think I should use zstack to position the trash image but I don't know how for now.
Below shows where the trash image should be located in each button.
Also, when I press the 'Delete Button', it seems that each button's text size and spacing with another button is changed slightly. How do I overcome this problem? The button position, spacing, textsize should be unchanged when 'Delete Button' is hit.
struct someButton: View {
#Environment(\.editMode) var mode
#ObservedObject var someData = SomeData()
#State var newButtonTitle = ""
#State var isEdit = false
var body: some View {
NavigationView{
// List{ // VStack
VStack{
VStack{
ForEach(Array(someData.buttonTitles.keys.enumerated()), id: \.element){ ind, buttonKeyName in
//
Button(action: {
self.someData.buttonTitles[buttonKeyName] = !self.someData.buttonTitles[buttonKeyName]!
print("Button pressed! buttonKeyName is: \(buttonKeyName) Index is \(ind)")
print("bool is \(self.someData.buttonTitles[buttonKeyName]!)")
}) {
HStack{ //HStack, ZStack
if self.isEdit{
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture{
print("buttonkey \(buttonKeyName) will be deleted")
self.deleteItem(ind: ind)
}
}
Text(buttonKeyName)
// .fontWeight(.semibold)
// .font(.title)
}
}
.buttonStyle(GradientBackgroundStyle(isTapped: self.someData.buttonTitles[buttonKeyName]!))
.padding(.bottom, 20)
}
}
HStack{
TextField("Enter new button name", text: $newButtonTitle){
self.someData.buttonTitles[self.newButtonTitle] = false
self.newButtonTitle = ""
}
}
}
.navigationBarItems(leading: Button(action: {self.isEdit.toggle()}){Text("Delete Button")},
trailing: EditButton())
// .navigationBarItems(leading: Button(action: {}){Text("ergheh")})
// }
}
}
func deleteItem(ind: Int) {
let key = Array(someData.buttonTitles.keys)[ind]
print(" deleting ind \(ind), key: \(key)")
self.someData.buttonTitles.removeValue(forKey: key)
}
}
struct GradientBackgroundStyle: ButtonStyle {
var isTapped: Bool
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity, maxHeight: 50)
.padding()
.foregroundColor(isTapped ? Color.blue : Color.black)
.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.overlay(RoundedRectangle(cornerRadius: 40)
.stroke(isTapped ? Color.blue : Color.black, lineWidth: 4))
.shadow(radius: 40)
.padding(.horizontal, 20)
.scaleEffect(configuration.isPressed ? 0.9 : 1.0)
//
}
}
class SomeData: ObservableObject{
#Published var buttonTitles: [String: Bool] = ["tag1": false, "tag2": false]
}
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4 (with some replicated code)
var body: some View {
Button(action: { }) {
Text("Name")
}
.buttonStyle(GradientBackgroundStyle(isTapped: tapped))
.overlay(Group {
if self.isEdit {
ZStack {
Button(action: {print(">> Trash Tapped")}) {
Image(systemName: "trash")
.foregroundColor(.red).font(.title)
}.padding(.trailing, 40)
.alignmentGuide(.top) { $0[.bottom] }
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
}
})
.padding(.bottom, 20)
}

Adding Segmented Style Picker to SwiftUI's NavigationView

The question is as simple as in the title. I am trying to put a Picker which has the style of SegmentedPickerStyle to NavigationBar in SwiftUI. It is just like the native Phone application's history page. The image is below
I have looked for Google and Github for example projects, libraries or any tutorials and no luck. I think if nativa apps and WhatsApp for example has it, then it should be possible. Any help would be appreciated.
SwiftUI 2 + toolbar:
struct DemoView: View {
#State private var mode: Int = 0
var body: some View {
Text("Hello, World!")
.toolbar {
ToolbarItem(placement: .principal) {
Picker("Color", selection: $mode) {
Text("Light").tag(0)
Text("Dark").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
}
You can put a Picker directly into .navigationBarItems.
The only trouble I'm having is getting the Picker to be centered. (Just to show that a Picker can indeed be in the Navigation Bar I put together a kind of hacky solution with frame and Geometry Reader. You'll need to find a proper solution to centering.)
struct ContentView: View {
#State private var choices = ["All", "Missed"]
#State private var choice = 0
#State private var contacts = [("Anna Lisa Moreno", "9:40 AM"), ("Justin Shumaker", "9:35 AM")]
var body: some View {
GeometryReader { geometry in
NavigationView {
List {
ForEach(self.contacts, id: \.self.0) { (contact, time) in
ContactView(name: contact, time: time)
}
.onDelete(perform: self.deleteItems)
}
.navigationBarTitle("Recents")
.navigationBarItems(
leading:
HStack {
Button("Clear") {
// do stuff
}
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.frame(width: 130)
.pickerStyle(SegmentedPickerStyle())
.padding(.leading, (geometry.size.width / 2.0) - 130)
},
trailing: EditButton())
}
}
}
func deleteItems(at offsets: IndexSet) {
contacts.remove(atOffsets: offsets)
}
}
struct ContactView: View {
var name: String
var time: String
var body: some View {
HStack {
VStack {
Image(systemName: "phone.fill.arrow.up.right")
.font(.headline)
.foregroundColor(.secondary)
Text("")
}
VStack(alignment: .leading) {
Text(self.name)
.font(.headline)
Text("iPhone")
.foregroundColor(.secondary)
}
Spacer()
Text(self.time)
.foregroundColor(.secondary)
}
}
}
For those who want to make it dead center, Just put two HStack to each side and made them width fixed and equal.
Add this method to View extension.
extension View {
func navigationBarItems<L, C, T>(leading: L, center: C, trailing: T) -> some View where L: View, C: View, T: View {
self.navigationBarItems(leading:
HStack{
HStack {
leading
}
.frame(width: 60, alignment: .leading)
Spacer()
HStack {
center
}
.frame(width: 300, alignment: .center)
Spacer()
HStack {
//Text("asdasd")
trailing
}
//.background(Color.blue)
.frame(width: 100, alignment: .trailing)
}
//.background(Color.yellow)
.frame(width: UIScreen.main.bounds.size.width-32)
)
}
}
Now you have a View modifier which has the same usage of navigationBatItems(:_). You can edit the code based on your needs.
Usage example:
.navigationBarItems(leading: EmptyView(), center:
Picker(selection: self.$choice, label: Text("Pick One")) {
ForEach(0 ..< self.choices.count) {
Text(self.choices[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}, trailing: EmptyView())
UPDATE
There was the issue of leading and trailing items were violating UINavigationBarContentView's safeArea. While I was searching through, I came across another solution in this answer. It is little helper library called SwiftUIX. If you do not want install whole library -like me- I created a gist just for navigationBarItems. Just add the file to your project.
But do not forget this, It was stretching the Picker to cover all the free space and forcing StatusView to be narrower. So I had to set frames like this;
.navigationBarItems(center:
Picker(...) {
...
}
.frame(width: 150)
, trailing:
StatusView()
.frame(width: 70)
)
If you need segmentcontroll to be in center you need to use GeometryReader, below code will provide picker as title, and trailing (right) button.
You set up two view on the sides left and right with the same width, and the middle view will take the rest.
5 is the magic number depends how width you need segment to be.
You can experiment and see the best fit for you.
GeometryReader {
Text("TEST")
.navigationBarItems(leading:
HStack {
Spacer().frame(width: geometry.size.width / 5)
Spacer()
picker
Spacer()
Button().frame(width: geometry.size.width / 5)
}.frame(width: geometry.size.width)
}
But better solution is if you save picker size and then calculate other frame sizes, so picker will be same on ipad & iphone
#State var segmentControllerWidth: CGFloat = 0
var body: some View {
HStack {
Spacer()
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
.background(Color.red)
segmentController
.fixedSize()
.background(PreferenceViewSetter())
profileButton
.frame(width: (geometry.size.width / 2) - (segmentControllerWidth / 2))
}
.onPreferenceChange(PreferenceViewKey.self) { preferences in
segmentControllerWidth = preferences.width
}
}
struct PreferenceViewSetter: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: PreferenceViewKey.self,
value: PreferenceViewData(width: geometry.size.width))
}
}
}
struct PreferenceViewData: Equatable {
let width: CGFloat
}
struct PreferenceViewKey: PreferenceKey {
typealias Value = PreferenceViewData
static var defaultValue = PreferenceViewData(width: 0)
static func reduce(value: inout PreferenceViewData, nextValue: () -> PreferenceViewData) {
value = nextValue()
}
}
Simple answer how to center segment controller and hide one of the buttons.
#State var showLeadingButton = true
var body: some View {
HStack {
Button(action: {}, label: {"leading"})
.opacity(showLeadingButton ? true : false)
Spacer()
Picker(selection: $selectedStatus,
label: Text("SEGMENT") {
segmentValues
}
.id(UUID())
.pickerStyle(SegmentedPickerStyle())
.fixedSize()
Spacer()
Button(action: {}, label: {"trailing"})
}
.frame(width: UIScreen.main.bounds.width)
}

Transition with a view with SwiftUI without animating the entire view

Using SwiftUI, I have a view that includes a slider that I'm using a transition to slide in from the bottom. All works well, until the slider is moved quickly back and forth. With that, the text field is being animated, and will show "..." when changing from 1 to two digits.
Here is my test code showing this:
struct TestSliderView: View {
#State private var val: Double = 0
#State private var showSlider: Bool = false
var body: some View {
VStack {
Button(action: {
self.showSlider.toggle()
}) {
Text("Show Slider")
}
Spacer()
if showSlider {
JustTheSlider(val: $val)
.padding()
.transition(.move(edge: .bottom))
.animation(.linear(duration: 0.4))
}
}
}
}
struct JustTheSlider: View {
#Binding var val: Double
var body: some View {
VStack {
Text("Slider")
.font(.title)
HStack {
Text("Value: ")
.frame(minWidth: 80, alignment: .leading)
Slider(value: $val, in: 0...30, step: 1)
Text("\(Int(val))")
.frame(minWidth: 20, alignment: .trailing)
.font(Font.body.monospacedDigit())
.padding(.horizontal)
}
}
}
}
One way around this would be to remove the .animation(.linear(duration: 0.4)) line and wrap the button action with an animation like so:
Button(action: {
withAnimation(.linear(duration: 0.4)) {
self.showSlider.toggle()
}
}) {
Text("Show Slider")
}
This stops the text from animating, but then the view only slides out, and just pops in without any slide animation.
Any ideas?
You need animate the state variable, not the View.
var body: some View {
VStack {
Button(action: {
withAnimation{
self.showSlider.toggle()}
}) {
Text("Show Slider")
}
Spacer()
if showSlider {
JustTheSlider(val: $val)
.padding()
.transition(.move(edge: .bottom))
}
}
}
As the last line shown.

What might be causing this animation bug with SwiftUI and NavigationView?

I've been experimenting with some SwiftUI layouts and one of the things that I wanted to try out was creating a simple circular progress ring. After playing around with the code for a while I managed to get everything working the way I was hoping for it to, at least for a prototype. The issue arrises when I embed this view inside a SwiftUI NavigationView. Now, every time I run the app in the canvas, simulator, or on a device, the initial loading of the progress ring has the entire view slowly sliding up into position.
This is a simple prototype, just messing around with the new SwiftUI tools. After some experimentation, I've found that if I remove the NavigationView the ring acts like it's meant to from the beginning. I'm not seeing an obvious reason for why this issue is occurring though.
import SwiftUI
struct ProgressRing_ContentView: View {
#State var progressToggle = false
#State var progressRingEndingValue: CGFloat = 0.75
var ringColor: Color = Color.green
var ringWidth: CGFloat = 20
var ringSize: CGFloat = 200
var body: some View {
TabView{
NavigationView{
VStack{
Spacer()
ZStack{
Circle()
.trim(from: 0, to: progressToggle ? progressRingEndingValue : 0)
.stroke(ringColor, style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.background(Circle().stroke(ringColor, lineWidth: ringWidth).opacity(0.2))
.frame(width: ringSize, height: ringSize)
.rotationEffect(.degrees(-90.0))
.animation(.easeInOut(duration: 1))
.onAppear() {
self.progressToggle.toggle()
}
Text("\(Int(progressRingEndingValue * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
}
Spacer()
Button(action: {
self.progressRingEndingValue = CGFloat.random(in: 0...1)
}) { Text("Randomize")
.font(.largeTitle)
.foregroundColor(ringColor)
}
Spacer()
}
.navigationBarTitle("ProgressRing", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
print("Refresh Button Tapped")
}) {
Image(systemName: "arrow.clockwise")
.foregroundColor(Color.green)
}, trailing:
Button(action: {
print("Share Button Tapped")
}) {
Image(systemName: "square.and.arrow.up")
.foregroundColor(Color.green)
}
)
}
}
}
}
#if DEBUG
struct ProgressRing_ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ProgressRing_ContentView()
.environment(\.colorScheme, .light)
.previewDisplayName("Light Mode")
}
}
#endif
Above is the exact code that I'm currently working with. The actual animation of the ring sliding seems to be working how I expected it to, I'm just not sure why the entire ring itself is moving when embedded in a NavigationView.
You need to use explicit animations, instead of implicit. With implicit animations, any animatable parameter that changes, the framework will animate. Whenever possible, you should use explicit animations. Below is the updated code. Notice I remove the .animation() call and added two withAnimation() closures.
If you would like to expand your knowledge on implicit vs. explicit animations, check this link: https://swiftui-lab.com/swiftui-animations-part1/
struct ContentView: View {
#State var progressToggle = false
#State var progressRingEndingValue: CGFloat = 0.75
var ringColor: Color = Color.green
var ringWidth: CGFloat = 20
var ringSize: CGFloat = 200
var body: some View {
TabView{
NavigationView{
VStack{
Spacer()
ZStack{
Circle()
.trim(from: 0, to: progressToggle ? progressRingEndingValue : 0)
.stroke(ringColor, style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.background(Circle().stroke(ringColor, lineWidth: ringWidth).opacity(0.2))
.frame(width: ringSize, height: ringSize)
.rotationEffect(.degrees(-90.0))
.onAppear() {
withAnimation(.easeInOut(duration: 1)) {
self.progressToggle.toggle()
}
}
Text("\(Int(progressRingEndingValue * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
}
Spacer()
Button(action: {
withAnimation(.easeInOut(duration: 1)) {
self.progressRingEndingValue = CGFloat.random(in: 0...1)
}
}) { Text("Randomize")
.font(.largeTitle)
.foregroundColor(ringColor)
}
Spacer()
}
.navigationBarTitle("ProgressRing", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
print("Refresh Button Tapped")
}) {
Image(systemName: "arrow.clockwise")
.foregroundColor(Color.green)
}, trailing:
Button(action: {
print("Share Button Tapped")
}) {
Image(systemName: "square.and.arrow.up")
.foregroundColor(Color.green)
}
)
}
}
}
}