Dismissing custom modal / overlay not updating contents of View In SwiftUI - ios

Update: I refactored it a bit to use #ObservableObject, but the issue still persists
In my SwiftUI app, I have a LazyVGrid populated with n cards and one New Event button. When I press the New Event button, I have a custom modal overlaying the View to let the user add a new event. However, after dismissing the view (by tapping a Create Event button), the modal disappears however the new event card isn't added to the LazyVGrid. It only appears once I force quit the app and re-open it.
Here is the main ContentView:
struct ContentView: View {
#State var progress: Double = 0.0
#State var editMode: Bool = false
#State var angle: Double = 0
#State var showModal: Bool = false
#ObservedObject var model: Model
var columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
func animate(while condition: Bool) {
self.angle = condition ? -0.6 : 0
withAnimation(Animation.linear(duration: 0.125).repeatForever(while: condition)) {
self.angle = 0.6
}
if !condition {
self.angle = 0
}
}
var body: some View {
ZStack {
NavigationView {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(model.events, id: \.self) { event in
SmallCardView(event: event, angle: $angle)
}
if !showModal {
AddEventButtonView(
editMode: $editMode,
showModal: $showModal,
namespace: namespace
)
} else {
Spacer().frame(height: 100)
}
}
.onLongPressGesture {
if !self.editMode {
self.editMode = true
animate(while: self.editMode)
}
}
}
if self.showModal {
AddEventView(namespace: namespace, events: self.$model.events, showModal: $showModal)
}
}
}
}
This is the View for the Add Event button:
struct AddEventButtonView: View {
#Binding var editMode: Bool
#Binding var showModal: Bool
var namespace: Namespace.ID
var body: some View {
Image(systemName: "calendar.badge.plus")
.frame(height: 100)
.frame(maxWidth: .infinity)
.opacity(self.editMode ? 0.0 : 1.0)
.onTapGesture {
withAnimation {
self.showModal = true
}
}
}
}
and this is my modal view:
struct AddEventView: View {
#State var eventName: String = ""
#State var endDate = Date()
#State var gradientIndex: Int = 0
#Binding var showModal: Bool
#Binding var events: [Event]
var namespace: Namespace.ID
init(namespace: Namespace.ID, events: Binding<[Event]>, showModal: Binding<Bool>) {
self.namespace = namespace
self._events = events
self._showModal = showModal
}
var body: some View {
VStack(spacing: 30.0) {
//...
Button(action: {
let event = Event(name: eventName, start: .init(), end: endDate, gradientIndex: gradientIndex)
self.events.append(event)
withAnimation {
self.showModal = false
}
}) {
RoundedRectangle(cornerRadius: 13)
.frame(maxWidth: .infinity)
.frame(height: 42)
.overlay(
Text("Add Event")
.foregroundColor(.white)
.bold()
)
}
.disabled(self.eventName.isEmpty)
}
}
}
If anyone has any idea of how to update the LazyVGrid to show the new event card when a new event is added that'd be awesome, thanks!
Here's the problem:

It is not clear how you did set up model, but it should be observed to update view, as (scratchy)
struct ContentView: View {
#State var progress: Double = 0.0
#State var editMode: Bool = false
#State var angle: Double = 0
#State var showModal: Bool = false
#ObservedObject var model: Model // << here !!
...
or use #EnvironmentObject pattern with injection via .environmentObject
or for Swift 2.0 use
#StateObject var model: Model = Model.shared

Related

How do I hide a view in response to a variable?

I have a view made up of 3 files. This view is a marketplace. There are two options to pick from in the filter tab bar at the top of the view. The view changes in response to these tabs successfully. When an item on the marketplace is tapped, a detailed view of the item is displayed.
However, when the detail view is displayed, the filter tab bar remains at the top of the view. I need this filter tab bar to hide when a detail view variable is set to true.
This is my code:
Marketplace code:
import SwiftUI
struct Marketplace: View {
#StateObject var MarketplaceModel = MarketplaceViewModel()
#State private var selectedMarketplaceFilter: MarketplaceFilterViewModel = .shoe
// MARK: For Smooth Sliding Effect
#Namespace var animation
// Shared Data...
#EnvironmentObject var sharedData: SharedDataModel
#State var shoeData : Shoe = Shoe()
#State var showDetailShoe: Bool = false
#State var sockData : Sock = Sock()
#State var showDetailSock: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
if !showDetailShoe && !showDetailSock {
marketplaceFilterBar
}
if selectedMarketplaceFilter == .shoes {
MarketplaceShoeView()
}
if selectedMarketplaceFilter == .socks {
MarketplaceSockView()
}
}
}
}
//Filter bar view
var marketplaceFilterBar: some View {
VStack {
HStack {
ForEach(MarketplaceFilterViewModel.allCases, id: \.rawValue) { item in
VStack {
Text(item.title)
.font(.headline)
.fontWeight(selectedMarketplaceFilter == item ? .semibold: .regular)
.foregroundColor(selectedMarketplaceFilter == item ? .black: .gray)
if selectedMarketplaceFilter == item {
Capsule()
.foregroundColor(Color("Blue"))
.frame(height: 3)
.matchedGeometryEffect(id: "filter", in: animation)
} else {
Capsule()
.foregroundColor(Color(.clear))
.frame(height: 3)
}
}
.onTapGesture {
withAnimation(.easeInOut) {
self.selectedMarketplaceFilter = item
}
}
}
}
}
}
}
MarketplaceShoeView(same exact code as MarketplaceSockView):
import SwiftUI
struct MarketplaceShoeView: View {
#StateObject var MarketplaceModel = MarketplaceViewModel()
#State private var selectedMarketplaceFilter: MarketplaceFilterViewModel = .shoe
#Namespace var animation
#State var showDetailShoe = false
#State var selectedShoe : Shoe!
// Shared Data...
#EnvironmentObject var sharedData: SharedDataModel
#State var string = ""
var body: some View {
var columns = Array(repeating: GridItem(.flexible()), count: 2)
ZStack{
VStack(spacing: 10){
HStack(spacing: 5) {
HStack(spacing: 12){
Image(systemName: "magnifyingglass")
TextField("Search", text: $MarketplaceModel.search)
}
}
if MarketplaceModel.shoes.isEmpty{
ProgressView()
}
else{
ScrollView(.vertical, showsIndicators: false, content: {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(),spacing: 10), count: 2),spacing: 20){
ForEach(MarketplaceModel.filteredShoe){shoe in
ShoeView(shoeData: shoe)
.onTapGesture {
withAnimation(.easeInOut){
selectedShoe = shoe
showDetailShoe.toggle()
}
}
}
}
})
}
}
if selectedShoe != nil && showDetailShoe{
ShoeDetailView(showDetailShoe: $showDetailShoe, shoeData: selectedShoe, shoe_id: string)
}
}
}
}
ShoeDetailView:
struct ShoeDetailView: View {
#StateObject var MarketplaceModel = MarketplaceViewModel()
#Binding var showDetailShoe: Bool
var shoeData : Shoe
var shoe_id: String
#Namespace var animation: Namespace.ID
#EnvironmentObject var sharedData: SharedDataModel
#EnvironmentObject var marketplaceData: MarketplaceViewModel
#State var string = ""
var body: some View {
NavigationView{
VStack{
VStack{
HStack {
Button(action: {
withAnimation(.easeOut){showDetailShoe.toggle()}
}) {
Image(systemName: "arrow.backward.circle.fill")
ForEach(MarketplaceModel.shoe_details_array){ items in
Text(items.shoe_name)
}
}
}
Image("ShoeImage")
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 15) {
Text(shoeData.shoe_name)
Text(shoeData.shoe_details)
}
}
}
}
}
}
SharedDataModel:
class SharedDataModel: ObservableObject {
#Published var detailSock : Sock?
#Published var showDetailSock : Bool = false
#Published var detailShoe : Shoe?
#Published var showDetailShoe : Bool = false
//Add something here??
}
marketplaceFilterBar needs to be hidden when a detailView is true.
You need to use the boolean values from EnvironmentObject instead of redefining them inside views. As follow:
import SwiftUI
struct Marketplace: View {
...
#EnvironmentObject var sharedData: SharedDataModel
...
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
// Use from env object
if !sharedData.showDetailShoe && !sharedData.showDetailSock {
marketplaceFilterBar
}
...
}
}
}
...
}
and so on...

Pass an #State variable to ContentView

Building my first SwiftUI app and I'm stuck on pass #State var to the ContentView. I have declared the #State variable in a struct, with an #Binding tag on the variable in the ContentView.
My intent is for multiple instances of NumberBlock to be called in ContentView, and be able to reset all of them to false (hide all of the images) with one button.
The new "App" struct that was added in Xcode 12 is giving an error for a missing parameter. I've tried everything I can think of to enter a parameter, but nothing seems to work. I was able to eliminate the error by using .constant(true), but that did not give me the functionality I need, which is to toggle the variable from the ContentView.
I appreciate any help eliminating the error or correcting my shallow understanding of #State and #Binding.
Here is where I create the #State reset_x var
import SwiftUI
struct NumberBlock: View {
#State var reset_x: Bool = true
#Binding var reset: Bool
var body: some View {
ZStack {
Text("test")
.onTapGesture(count: 1, perform: {
self.reset_x = false
})
Image("XMark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50, alignment: .center)
.onTapGesture(count: 1, perform: {
self.reset_x = true
print("reset_x is \(self.reset_x)")
})
.isHidden(reset_x ? true : false)
.isHidden(reset ? true : false)
}
}
}
The error occurs in this view:
import SwiftUI
#main
struct Quixx2App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Here is where I want to use the #Binding
import SwiftUI
struct ContentView: View {
#State var reset: Bool = false
#Binding var reset_x: Bool
var body: some View {
VStack {
HStack {
NumberBlock(reset: self.$reset)
NumberBlock(reset: self.$reset)
}
Button("Reset Score"){
self.scoreKeeper.redScore = 0
self.reset_x = false //this line is not doing anything
print("reset_x is \(self.reset_x)")
}
}
}
}
And the .isHidden extension
import Foundation
import SwiftUI
extension View {
#ViewBuilder func isHidden(_ hidden: Bool, remove: Bool = false) -> some View {
if hidden {
if !remove {
self.hidden()
}
} else {
self
}
}
}
So let's start with your NumberBlock view. You need one state within the View, which preserves if the image is hidden or not. By tapping the Text View, the the value is toggled and the view is rendered accordingly.
struct NumberBlock: View {
#State private var imageIsHidden: Bool
var body: some View {
VStack {
Text("Show")
.onTapGesture(count: 1, perform: {
self.imageIsHidden = false
})
Text("Image")
.onTapGesture(count: 1, perform: {
self.imageIsHidden = true
})
.isHidden(imageIsHidden ? true : false)
}
}
}
Now, you want a second feature: The ContentView should control this state. So you have to extract this state from the child view (NumberBlock) to the parent view (ContentView). By changing the property from #State to #Binding you are basically telling the child view that this data is being passed from a parent view.
My working example:
import SwiftUI
import Foundation
extension View {
#ViewBuilder func isHidden(_ hidden: Bool, remove: Bool = false) -> some View {
if hidden {
if !remove {
self.hidden()
}
} else {
self
}
}
}
struct NumberBlock: View {
#Binding var imageIsHidden: Bool
var body: some View {
VStack {
Text("Show")
.onTapGesture(count: 1, perform: {
self.imageIsHidden = false
})
Text("Image")
.onTapGesture(count: 1, perform: {
self.imageIsHidden = true
})
.isHidden(imageIsHidden ? true : false)
}
}
}
struct ContentView: View {
#State var imageOfBlock1IsHidden: Bool = true
#State var imageOfBlock2IsHidden: Bool = true
var body: some View {
VStack {
HStack {
NumberBlock(imageIsHidden: self.$imageOfBlock1IsHidden)
NumberBlock(imageIsHidden: self.$imageOfBlock2IsHidden)
}
Button("Reset Score") {
self.imageOfBlock1IsHidden = true
self.imageOfBlock2IsHidden = true
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI NavigationItem doesn't respond

When I click on the left arrow it should dismiss the view, but only the UI responds as the button being clicked and the view pushed by a NavigationLink is not dismissed...
The same view pushed by another NavigationLink in another view works perfectly, but when pushed by this NavigationLink, I click on the left arrow, only 1 in 20 times it dismisses the view. Is it bug in SwiftUI again? Is there a workaround?
import SwiftUI
import Firebase
import struct Kingfisher.KFImage
struct SearchView: View {
#Environment(\.presentationMode) var presentation
#State var typedSearchValue = ""
#State var createNewPost = false
#State var showMessaging = false
#EnvironmentObject var userInfo : UserData
#Binding var switchTab: Int
#State var text : String = ""
#State var foundUsers: [FetchedUser] = []
#State var showAccount = false
#State var fetchedUser = FetchedUser()
var body: some View {
NavigationView {
ZStack{
// navigate to that user's profile << the problem navlink
NavigationLink(destination:
UserAccountView(fetchedUser: self.$fetchedUser, showAccount: self.$showAccount)
.environmentObject(self.userInfo)
.environmentObject(FetchFFFObservable())
,isActive: self.$showAccount
){
EmptyView()
}.isDetailLink(false)
//...
NavigationLink(destination:
MessagingMainView(showMessaging: self.$showMessaging)
.environmentObject(UserData())
.environmentObject(MainObservable()),
isActive: self.$showMessaging
){
Text("")
}
VStack(spacing: 0) {
SearchBarMsg(text: $text, foundUsers: $foundUsers)
.environmentObject(userInfo)
.padding(.horizontal)
VStack{
if text != ""{
List(foundUsers, id: \.username){ user in
FetchedUserCellView(user: user)
.environmentObject(self.userInfo)
.onTapGesture {
self.fetchedUser = user
self.showAccount = true
}
}
}else{
//....
}
}
}
.navigationBarColor(.titleBarColor)
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
Navigates to this view, and the button in navigationItems doesn't work, although the UI responds:
struct UserAccountView: View {
#Environment(\.presentationMode) var presentation
//...
#Binding var showAccount : Bool
var body: some View {
VStack {
//.....
}
.navigationBarColor(.titleBarColor)
.navigationBarTitle(Text(fetchedUser.username), displayMode: .inline)
.navigationBarItems(leading: Button(action: {
//this button doesn't work!
self.showAccount = false
}, label: {
Image(systemName: "arrow.left")
.resizable()
.frame(width: 20, height: 15)
})).accentColor(.white)
)
}
}

How to select an item in ScrollView with SwiftUI?

what I am trying to accomplish is have a loop of items where I am able to tap one and it gets bigger programmatically once tapped
here is my code and my results so far:
struct ContentView: View {
#State var emojisArray = ["📚", "🎹", "🎯", "💻"]
#State var selectedIndex = 0
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(0..<emojisArray.count) { item in
emojiView(emoji: self.emojisArray[item],
isSelected: item == self.selectedIndex ? true : false)
.onTapGesture {
print (item)
self.selectedIndex = item
}
}
}
}
.onAppear()
.frame(height:160)
VStack{
Text("selcted item:")
Text("\(self.emojisArray[self.selectedIndex])")
}
}
}
}
where emojiView is :
struct emojiView: View {
var emoji : String
#State var isSelected : Bool
var body: some View {
Text(emoji)
.font(isSelected ? .system(size: 120) : .system(size: 45))
}
}
I guess the problem is that the ScrollView does't reload itself
Just remove #State in emojiView
struct emojiView: View {
var emoji : String
var isSelected : Bool // << here !!
var body: some View {
Text(emoji)
.font(isSelected ? .system(size: 120) : .system(size: 45))
}
}
Tested with Xcode 12 / iOS 14

SwiftUI programmatic navigation from within list

I have a navigation requirement that looks something like this:
Each detail screen can navigation to the next and previous detail screen. At the same time, the "back" button should always go back to the main list (not the previous detail screen).
I'm struggling with how to accomplish this in SwiftUI?
Here is what I have so far:
struct ListView: View {
#State private var currentDetailShown: Int?
#State private var listItems: [Int] = Array(repeating: 0, count: 10)
func goToNext() {
if let idx = self.currentDetailShown {
self.currentDetailShown = min(self.listItems.count - 1, idx + 1)
}
}
func goToPrev() {
if let idx = self.currentDetailShown {
self.currentDetailShown = max(0, idx - 1)
}
}
var body: some View {
List {
ForEach(0..<listItems.count) { index in
NavigationLink(destination: DetailView(goToNext: self.goToNext, goToPrev: self.goToPrev),
tag: index,
selection: self.$currentDetailShown) {
ListItem(score: listItems[index])
}
.isDetailLink(false)
.onTapGesture {
self.currentDetailShown = index
}
}
}
}
}
What happens with this code is that from the first detail view, it'll move to the to the next detail view and then immediately jump back to the list view.
I feel like I'm overthinking this or missing something obvious...
Instead of navigating to each detail from your list, you can navigate to a detailView that can show each detail individually by using a published variable in an observable object. Here is an example
struct MainView: View{
#EnvironmentObject var viewModel: ViewModel
var body: some View{
NavigationView{
VStack{
ForEach(self.viewModel.details, id:\.self){ detail in
NavigationLink(destination: DetailView(detail: self.viewModel.details.firstIndex(of: detail)!).environmentObject(ViewModel())){
Text(detail)
}
}
}
}
}
}
class ViewModel: ObservableObject{
#Published var showingView = 0
#Published var details = ["detail1", "detail2", "detail3", "detail4", "detail5", "detail6"]
}
struct DetailView: View{
#EnvironmentObject var viewModel: ViewModel
#State var detail: Int
var body: some View{
VStack{
IndivisualDetailView(title: viewModel.details[detail])
Button(action: {
self.viewModel.showingView -= 1
}, label: {
Image(systemName: "chevron.left")
})
Button(action: {
self.viewModel.showingView += 1
print(self.viewModel.showingView)
}, label: {
Image(systemName: "chevron.right")
})
}
}
}
struct IndivisualDetailView: View{
#State var title: String
var body: some View{
Text(title)
}
}

Resources