SwiftUI Bizarre Picker Behavior with Modal Sheets - ios

I'm experiencing a truly bizarre behavior with an app that has pickers in a view that
calls a map as a sheet modal. The pickers are not directly involved in calling the map -
but set conditions for the annotations that will be displayed. And in the simple example
I have included below, it is clear the issue has nothing to do with the map - the
problem behavior is present with a simple text view in the modal.
The issue: if the user swipes to dismiss the modal it appears to always work as expected.
If the user taps a button to dismiss the modal with either the environment dismiss or
through a binding to the #State that calls the view, then after the second showing of the
modal, you can no longer raise the picker - the displayed value just turns color - as if
a color toggle on tap. I've also tried showing the modal as full screen, as an animation
and as a transition. I get the same result every time. I'm at the point where I think this
must be an Xcode bug, but I hope someone can show me a solution. The same behavior exists
in Preview, Simulator and a real Device.
Here is a very stripped down example which demonstrates the issue:
enum StorageKeys: String {
case appChosenFuel, appChosenState
}//enum
struct ContentView: View {
#State private var showGroupMapView: Bool = false
#State private var showTooBigAlert: Bool = false
#State private var justANumber: Int = 500
var body: some View {
NavigationView {
VStack {
Text("Picker plus Sheet Test")
.padding()
FuelPickerView()
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
if justANumber > 1000 {
showTooBigAlert = true
} else {
withAnimation {
showGroupMapView.toggle()
}
}
} label: {
Image(systemName: "map")
.font(.system(size: 20))
.frame(width: 40, height: 40)
}
.sheet(isPresented: $showGroupMapView, onDismiss: {
print("you dismissed me")
}) {
GroupMapView()
}
}//toolbar group
}//toolbar
}//nav
}//body
}//sruct
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct GroupMapView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Text("This is the map view")
VStack {
HStack {
Spacer()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "gear")
.padding(.trailing, 20)
.padding(.top, 20)
}
}
Spacer()
}
}//z
}
}//struct
class FuelPickerViewModel: ObservableObject {
struct FuelItem: Identifiable, Hashable {
let id = UUID()
let name: String
let initials: String
}
#Published var fuelItems = [
FuelItem(name: "Biodiesel (B20 and above)", initials: "BD"),
FuelItem(name: "Compressed Natural Gas", initials: "CNG"),
FuelItem(name: "Ethanol (E85)", initials: "E85"),
FuelItem(name: "Electric", initials: "ELEC"),
FuelItem(name: "Hydrogen", initials: "HY"),
FuelItem(name: "Liquified Natural Gas", initials: "LNG"),
FuelItem(name: "Liquified Petroleum Gas (Propane)", initials: "LPG")
]
}//class
struct FuelPickerView: View {
#AppStorage(StorageKeys.appChosenFuel.rawValue) var appChosenFuel = "ELEC"
#AppStorage(StorageKeys.appChosenState.rawValue) var appChosenState = "CO"
#StateObject var fuelPickerVM = FuelPickerViewModel()
var body: some View {
return VStack {
Picker("Fuel", selection: $appChosenFuel) {
ForEach(fuelPickerVM.fuelItems, id: \.self) {
Text($0.initials)
}
}
}
}//body
}//struct
And after the second modal display/dismiss with the button, tapping the picker does
nothing except change the background color:
Any guidance would be appreciated: Xcode 13.2.1 iOS 15.2

Related

Keyboard in a sheet's

I have a basic window with an input field (page 1), on top of it appears a popup window (page 2), inside of which there is also an input fields and buttons, which, when clicked, will bring up a small window with an input field (page 3). If there is no "Done" on the keyboard, the interface functions normally. If you add a "Done" button, it turns out that its color changes from system color blue to gray when moving from page 2 to page 3. Experimenting and wondering why this is so, I found that the toolbar on page 1 is responsible for the color of the button on page 3... If you change the color of the button on the toolbar on page 1 - it will change on the toolbar on page 3, and page 2 will not be affected. Also, adding buttons causes error: "[LayoutConstraints] Unable to simultaneously satisfy constraints." I want to understand why setting the button on the keyboard for Page 1, I also get a button when I type on Page 3? And why is it grayed out and not working? Why if I change the color for the button on Page 1, does it also change for that gray button on Page 3?
A small representative sample:
ContentView
import SwiftUI
struct ContentView: View {
#State private var bloodClucoseLvl: String = ""
#State private var isSheetShown: Bool = false
#FocusState private var focusField: Bool
var body: some View {
NavigationView {
List {
Section("Add your current blood glucose lvl") {
TextField("5,0 mmol/l", text: $bloodClucoseLvl)
.focused($focusField)
}
Section("Add food or drink") {
Button(action:{
isSheetShown.toggle()
}, label:{
HStack{
Text("Add")
Image(systemName: "folder.badge.plus")
}
})
.sheet(isPresented: $isSheetShown) {
addFoodButton()
}
}
}
.navigationTitle("Page 1 - General")
.toolbar{
ToolbarItem(placement: .keyboard) {
HStack {
Spacer()
Button(action: {
focusField = false
}) {
Text("Done")
}
}
}
}
.ignoresSafeArea(.keyboard)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
addFoodButton
import SwiftUI
struct addFoodButton: View {
#State private var selectedFood: String = ""
#State public var addScreen: Bool = false
var body: some View {
ZStack {
NavigationView {
List {
Section("or choose from category"){
NavigationLink(destination: Alcohol(addScreen: $addScreen)){
Text("Alcohol")
}
}
}
.listStyle(.insetGrouped)
.searchable(text: $selectedFood, prompt: "Search by word")
.navigationTitle("Page 2 - Search in DB")
}
if addScreen{
addScreenView(addScreen: $addScreen)
}
}
}
}
struct Alcohol: View {
#State private var searchInsideCategory: String = ""
#Binding var addScreen: Bool
var body: some View {
List {
Button(action: {addScreen.toggle()}){
Text("Light beer")
}
}
.navigationTitle("Page 2 - Choose beer")
.searchable(text: $searchInsideCategory, prompt: "Search inside a category")
}
}
struct addFoodButton_Previews: PreviewProvider {
static var previews: some View {
addFoodButton()
}
}
addScreenView
import SwiftUI
struct addScreenView: View {
#Binding var addScreen: Bool
#State private var gram: String = ""
var body: some View {
ZStack{
Color.black.opacity(0.2).ignoresSafeArea()
VStack(spacing:0){
Text("Page 3 - Add an item")
.bold()
.padding()
Divider()
VStack(){
TextField("gram", text: $gram)
.padding(.leading, 16)
.padding(.trailing, 16)
.keyboardType(.numberPad)
Rectangle()
.frame(height: 1)
.padding(.leading, 16)
.padding(.trailing, 16)
}.padding()
Divider()
HStack(){
Button(action: {
addScreen.toggle()
}){
Text("Cancel").frame(minWidth:0 , maxWidth: .infinity)
}
Divider()
Button(action: {
addScreen.toggle()
}){
Text("Save").frame(minWidth:0 , maxWidth: .infinity)
}
}.frame(height: 50)
}
.background(Color.white.cornerRadius(10))
.padding([.leading, .trailing], 15)
}
}
}
struct addScreenView_Previews: PreviewProvider {
static var previews: some View {
addScreenView(addScreen: .constant(true))
}
}

SwiftUI updating #Binding value in NavigationLink(destination:label) causes destination to reappear

I have a weird issue, where I navigate to EditView and edit a state variable from ContentView. After that I dismiss EditView with presentation.wrappedValue.dismiss(). The problem is, as soon as the view is dismissed, it reappears again.
I'm using XCode 12.4 and my iOS deployment target is set to 14.4
Observations:
EditView doesn't reappear if the value isn't edited
removing the changed value Text("Value: \(title)") >> Text("Value") from ContentView tree resolves the issue, but that obviously isn't a solution.
moving the NavigationLink from .toolbar e.g.
VStack {
Text("Value: \(title)")
NavigationLink(destination: EditView(title: $title)){
Text("Edit")
}
}
also resolves the issue, but that seems like a hack. Besides, I'd like to keep using .toolbar because I like the .navigationTitle animation and I can't have a button in the upper-right corner of the screen if I have a navigation title without the toolbar.
Here's the full code:
import SwiftUI
struct ContentView: View {
#State var title: String = "Title"
#State var isActive: Bool = false
var body: some View {
NavigationView {
Text("Value: \(title)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: EditView(title: $title, isActive: $isActive), isActive: $isActive){
Text("Edit")
}
}
}
}
}
}
struct EditView: View {
#Environment(\.presentationMode) var presentation
#Binding var title: String
#Binding var isActive: Bool
var body: some View {
Button(action: {
title = "\(Date().timeIntervalSince1970)"
// presentation.wrappedValue.dismiss()
isActive = false
}){
Text("Done")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As far as I can tell this is a .toolbar bug, and if it turns out that way I'll report it to Apple, but in the meantime, does anyone have a better solution and/or explanation for this?
Cheers!
EDIT:
I updated the code with isActive value for the NavigationLink. It doesn't work when written like that, but uncommenting the commented out line makes it work. But that's quite hacky.
You are mixing up 2 things NavigationLink will push the view on stack, just like NavigationController in swift. That’s why you can see back button after navigating to second view. When you hit back button, it will pop top most view out of stack. presentationMode is not needed , dismissing presented view will not pop it of the stack.
To present a view and dismiss it you can check below code.
import SwiftUI
struct ContentViewsss: View {
#State var title: String = "Title"
#State var isPresented = false
var body: some View {
NavigationView {
Text("Value: \(title)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isPresented.toggle()
}){
Text("Edit")
}
}
}
}.sheet(isPresented: $isPresented, content: {
EditView(title: $title, state: $isPresented)
})
}
}
struct EditView: View {
#Binding var title: String
#Binding var state: Bool
var body: some View {
Button(action: {
title = "\(Date().timeIntervalSince1970)"
state.toggle()
}){
Text("Done")
}
}
}
If you want NavigationLink functionality, you can just remove presentationMode code from second view, and keep ContentView as it is -:
struct EditView: View {
//#Environment(\.presentationMode) var presentation
#Binding var title: String
var body: some View {
Button(action: {
title = "\(Date().timeIntervalSince1970)"
// presentation.wrappedValue.dismiss()
}){
Text("Done")
}
}
}

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)
)
}
}

Cannot add something to an Array

i have made an app in SwiftUI where you can create different classes. The app saves this in an array. I have a textfield and a button in the same view as the scrollview that displays the array. This works perfectly fine and I can easily add new classes. Now I made a new view with a text field and a button. This view can be viewed by pressing a button in the nav bar. It uses the exact same function as the other view, but in the other view adding a item to the array works, in this view it doesn't work. I hope you understand my problem and can help me.
Thank You.
This is the file where I store the array:
import Foundation
import SwiftUI
import Combine
struct Class: Identifiable {
var name: String
var id = UUID()
}
class ClassStore: ObservableObject {
#Published var classes = [Class]()
}
This is the view with the button + textfield that works and the scrollview that displays the array:
import SwiftUI
struct ContentView: View {
#State var showNewClass = false
#ObservedObject var classStore = ClassStore()
#State var newClass: String = ""
#State var toDoColor: Color = Color.pink
func addNewClass() {
classStore.classes.append(
Class(name: newClass)
)
newClass = ""
}
var body: some View {
NavigationView {
VStack {
HStack {
TextField("New Todo", text: $newClass)
Image(systemName: "app.fill")
.foregroundColor(Color.pink)
.padding(.horizontal, 3)
Image(systemName: "books.vertical")
.padding(.horizontal, 3)
if newClass == "" {
Text("Add!")
.foregroundColor(Color.gray)
} else {
Button(action: {
addNewClass()
}) {
Text("Add!")
}
}
}.padding()
ScrollView {
ForEach(self.classStore.classes) { name in
Text(name.name)
}
}
.navigationBarTitle(Text("Schulnoten"))
.navigationBarItems(trailing:
Button(action: {
self.showNewClass.toggle()
}) {
Text("New Class")
}
.sheet(isPresented: $showNewClass) {
NewClass(isPresented: $showNewClass)
})
}
}
}
}
And this is the new view I created, the button and the textfield have the exact same code, but somehow this doesn't work:
import SwiftUI
struct NewClass: View {
#Binding var isPresented: Bool
#State var className: String = ""
#ObservedObject var classStore = ClassStore()
func addNewClass() {
classStore.classes.append(
Class(name: className)
)
}
var body: some View {
VStack {
HStack {
Text("New Class")
.font(.title)
.fontWeight(.semibold)
Spacer()
}
TextField("Name of the class", text: $className)
.padding()
.background(Color.gray)
.cornerRadius(5)
.padding(.vertical)
Button(action: {
addNewClass()
self.isPresented.toggle()
}) {
HStack {
Text("Safe")
.foregroundColor(Color.white)
.fontWeight(.bold)
.font(.system(size: 20))
}
.padding()
.frame(width: 380, height: 60)
.background(Color.blue)
.cornerRadius(5)
}
Spacer()
}.padding()
}
}
Sorry if my English is not that good. I'm not a native speaker.
I assume you should pass same class store from parent view into sheet, ie
struct NewClass: View {
#Binding var isPresented: Bool
#State var className: String = ""
#ObservedObject var classStore: ClassStore // << expect external
and in ContentView
.sheet(isPresented: $showNewClass) {
NewClass(isPresented: $showNewClass, classStore: self.classStore) // << here !!
})

SwiftUI Programmatically Select List Item

I have a SwiftUI app with a basic List/Detail structure. A new item is created from
a modal sheet. When I create a new item and save it I want THAT list item to be
selected. As it is, if no item is selected before an add, no item is selected after
an add. If an item is selected before an add, that same item is selected after the
add.
I'll include code for the ContentView, but this is really the simplest example of
List/Detail.
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addNewResort = false
#State private var coverDeletedDetail = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
List {
ForEach(resortStore.resorts) { resort in
NavigationLink(destination: ResortView(resort: resort)) {
HStack(spacing: 20) {
Image("FlatheadLake1")
//bunch of modifiers
VStack(alignment: .leading, spacing: 10) {
//the cell contents
}
}
}
}
.onDelete { indexSet in
self.removeItems(at: [indexSet.first!])
self.coverDeletedDetail.toggle()
}
if UIDevice.current.userInterfaceIdiom == .pad {
NavigationLink(destination: WelcomeView(), isActive: self.$coverDeletedDetail) {
Text("")
}
}
}//list
.onAppear(perform: self.selectARow)
.navigationBarTitle("Resorts")
.navigationBarItems(leading:
//buttons
}//body
func removeItems(at offsets: IndexSet) {
resortStore.resorts.remove(atOffsets: offsets)
}
func selectARow() {
//nothing that I have tried works here
print("selectARow")
}
}//struct
And again - the add item modal is extremely basic:
struct AddNewResort: View {
//bunch of properties
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName, country: self.resortCountry, description: self.resortDescription, imageCredit: "Credit", price: Int(self.resortPriceString) ?? 0, size: Int(self.resortSizeString) ?? 0, snowDepth: 20, elevation: 3000, runs: 40, facilities: ["bar", "garage"])
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
To show the issue - The list with a selection:
The list after a new item created showing the previous selection:
Any guidance would be appreciated. Xcode 11.4
I tried to reconstitute your code as closely as could so that it builds. Here is what I have in the end. We have a list of resorts and when a new resort is saved in the AddNewResort sheet, if we are currently in split view (horizontalSizeClass is regular), we will select the new resort, otherwise just dismiss the sheet.
import SwiftUI
class ResortStore: ObservableObject {
#Published var resorts = [Resort(id: UUID(), name: "Resort 1")]
}
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addingNewResort = false
#State var selectedResortId: UUID? = nil
var navigationLink: NavigationLink<EmptyView, ResortView>? {
guard let selectedResortId = selectedResortId,
let selectedResort = resortStore.resorts.first(where: {$0.id == selectedResortId}) else {
return nil
}
return NavigationLink(
destination: ResortView(resort: selectedResort),
tag: selectedResortId,
selection: $selectedResortId
) {
EmptyView()
}
}
var body: some View {
NavigationView {
ZStack {
navigationLink
List {
ForEach(resortStore.resorts, id: \.self.id) { resort in
Button(action: {
self.selectedResortId = resort.id
}) {
Text(resort.name)
}
.listRowBackground(self.selectedResortId == resort.id ? Color.gray : Color(UIColor.systemBackground))
}
}
}
.navigationBarTitle("Resorts")
.navigationBarItems(trailing: Button("Add Resort") {
self.addingNewResort = true
})
.sheet(isPresented: $addingNewResort) {
AddNewResort(selectedResortId: self.$selectedResortId)
.environmentObject(self.resortStore)
}
WelcomeView()
}
}
}
struct ResortView: View {
let resort: Resort
var body: some View {
Text("Resort View for resort name: \(resort.name).")
}
}
struct AddNewResort: View {
//bunch of properties
#Binding var selectedResortId: UUID?
#State var resortName = ""
#Environment(\.presentationMode) var presentationMode
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#EnvironmentObject var resortStore: ResortStore
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName)
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
if self.horizontalSizeClass == .regular {
self.selectedResortId = newResort.id
}
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
struct WelcomeView: View {
var body: some View {
Text("Welcome View")
}
}
struct Resort {
var id: UUID
var name: String
}
We need to keep track of the selectedResortId
We create an invisible NavigationLink that will programmatically navigate to the selected resort
We make our list row a Button, so that the user can select a resort by tapping on the row
I started writing a series of articles about navigation in SwiftUI List view, there are a lot of points to consider while implementing programmatic navigation.
Here is the one that describes this solution that I'm suggesting: SwiftUI Navigation in List View: Programmatic Navigation. This solution works at the moment on iOS 13.4.1. SwiftUI is changing rapidly, so we have to keep on checking.
And here is my previous article that explains why a more simple solution of adding a NavigationLink to each List row has some problems at the moment SwiftUI Navigation in List View: Exploring Available Options
Let me know if you have questions, I'd be happy to help where I can.

Resources