SwiftUI - Why doesn't my .alert window change my state variable? - swiftui

The incomprehensible thing is that if I set breakpoints when debugging, I get my desired result. But now to the problem:
I have a view, which on appearance should check if errors have appeared in previous calculations. If so, then the SelectionView() should be called when the error is confirmed. However, this does not happen. The alert window remains rigid, and you cannot continue using the app.
The function ErrorCheck() returns whether there is no error: 0 ; a warning: 1 ; or an error where you should jump back to the menu: 2.
So if there should be an error, then after confirming the Alert window, the window should close itself and you should be sent to the SelectionView.
If I set a breakpoint at the line where the state variable should be changed, then the alert window closes, and you are sent to the SelectionView.
import SwiftUI
struct NearfieldCalibrationView: View {
#StateObject var ErrorviewModel = ErrorViewModel()
var Speech = SpeechModel()
#State var showAlert = false
#State private var Menu: Int? = 0
#State var isActive = false
#ViewBuilder func getView() -> some View {
switch Menu {
case 1:
SelectionView().navigationBarTitle("").navigationBarHidden(true)
case 2:
WMLoadingMeasurementView(Flag: 5)
case 3:
WMLoadingMeasurementView(Flag: 7)
default:
Text("")
}
}
var body: some View {
ZStack {
BackgroundView()
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
VStack {
Spacer().frame(height: 20)
Text("Hello-StackOverFlow").font(Font.custom("Baro Plain", size: 20)).foregroundColor(.white)
Spacer().frame(height: 20)
Group {
NavigationLink(isActive: $isActive, destination: getView) {
Button(action: {
if SpeakerSetup.count != 2 {
self.Menu = 2
self.isActive = true
} else {
self.Menu = 3
self.isActive = true
}
}) {
Text("Start")
}.buttonStyle(OutlineButton())
.simultaneousGesture(TapGesture().onEnded { Speech.Speech(text: "", StopSpeak: true) })
}
Spacer().frame(height: 20)
}
}
}
.onAppear(perform: {
if ErrorviewModel.ErrorCheck() > 0 { showAlert = true }
else {
Speech.Speech(text:"Hello-StackOverflow",StopSpeak: false)
}
})
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
.alert(isPresented: $showAlert) { () -> Alert in
var button: Alert.Button
if ErrorviewModel.ErrorCheck() == 2 { button = Alert.Button.default(Text("zurück zum Menü")) {
self.Menu = 1; self.isActive = true }}
else { button = Alert.Button.default(Text("Ok")) {}}
return Alert(title: Text("Error"), message: Text(ErrorviewModel.ErrorList()), dismissButton: button)
}
}
}
}

Related

SwiftUI: fullScreenCover isPresented flag has unexpected value

I have a simplest of codes, which doesn't work as expected:
struct ContentView: View {
#State private var showSheet = false
var body: some View {
VStack {
Button("Press to show details") {
showSheet = true
}
}
.fullScreenCover(isPresented: $showSheet) {
Color.black.opacity(showSheet ? 0.5 : 1)
}
}
}
I expect the sheet to appear semi-transparent after I press the button, but it is black. The value of showSheet is false in debugger
If I add a close button, and keep pressing the buttons - it fixes itself after the second try - that is: a second time the cover is shown as semi-transparent
VStack {
Button("Press to show details") {
showSheet = true
}
}
.fullScreenCover(isPresented: $showSheet) {
Color.black.opacity(showSheet ? 0.5 : 1)
.overlay(
Button("Press to show details") {
showSheet = false
}
)
}
If I add an observer it also fixes itself. I'm lost completely
VStack {
Button("Press to show details") {
showSheet = true
}
}
.fullScreenCover(isPresented: $showSheet) {
Color.red.opacity(showSheet ? 0.5 : 1)
}
.onChange(of: showSheet) { newValue in
print(newValue)
}

Activity Indicator doesn't show when isAnimating set to TRUE

I've got several activity indicators. The one in the choose subscription screen works ok, but when I do a similar thing when user taps a button that writes some information to a database, the activity indicator doesn't show after the boolean value is set to true for the isAnimating parameter.
When I set the isLoading variable to true in its declaration, the activity indicator appears.
Below is the code that does the database work, and the activity indicator view.
import SwiftUI
struct LanguagePickerWheel: View {
#State private var selectedLanguage: String = ""
#State private var isLoading = false
var availableLanguages: [String] = []
#Environment(\.presentationMode) var presentation
#Environment(\.managedObjectContext) private var viewContext
private func dismiss() {
self.presentation.wrappedValue.dismiss()
}
var body: some View {
GeometryReader { geometry in
VStack {
HStack {
Button("Cancel") { self.dismiss() }
.padding(.top)
.padding(.trailing, 125)
Button("Select") {
DispatchQueue.main.async {
isLoading = true
}
let queue = DispatchQueue(label: "work-queue-1")
queue.async {
if selectedLanguage == "" {
selectedLanguage = availableLanguages[0]
}
// submit language to the addAndSaveLanguage method
let newLanguage = Language(context: viewContext)
newLanguage.name = selectedLanguage
newLanguage.setAsRevision = false
PersistenceController.shared.saveDB()
do {
// This solution assumes you've got the file in your bundle
if let path = Bundle.main.path(forResource: "\(selectedLanguage)_English_Over_2500_Words", ofType: "txt") {
let data = try String(contentsOfFile:path, encoding: String.Encoding.utf8)
var arrayOfStrings: [String]
arrayOfStrings = data.components(separatedBy: ";")
for string in arrayOfStrings {
let newCommonWord = CommonWordThing(context: viewContext)
newCommonWord.native = string.components(separatedBy: "_")[1]
newCommonWord.foreign = string.components(separatedBy: "_")[0]
newCommonWord.ckimage = false
newCommonWord.inUse = false
newCommonWord.typingTestCorrect = 0
newCommonWord.arrangeWordsCorrect = 0
newCommonWord.ckreference = newLanguage.ckrecordname
newCommonWord.attempts = 0
newCommonWord.image = nil
newCommonWord.repetitionInterval = 0
newCommonWord.testsUntilPresented = 0
newCommonWord.setAsRevision = false
newCommonWord.language = newLanguage
var stringNumber = string.split(separator: "_")[2]
if stringNumber.contains("\r\n") {
stringNumber.removeLast(1)
}
newCommonWord.count = NumberFormatter().number(from: String(stringNumber) as String)?.int64Value ?? 0
}
}
} catch let err as NSError {
// do something with Error
print("Couldn't save new language to database: \(err)")
}
PersistenceController.shared.saveDB()
}
isLoading = false
self.dismiss()
}
.padding(.top)
.padding(.leading, 125)
}
Text("Choose a language:")
.font(.title)
.padding(.top, 50)
Picker("Choose a language:", selection: $selectedLanguage, content: {
ForEach(Array(availableLanguages), id: \.self) { language in
Text(language)
}
})
.pickerStyle(WheelPickerStyle())
.padding(.leading)
.padding(.trailing)
}
ActivityIndicatorView(isAnimating: $isLoading, text: "Loading...")
.position(x: geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY)
}
}
}
Activity Indicator:
struct ActivityIndicatorView: View {
#Binding var isAnimating:Bool
var body: some View {
if isAnimating{
ZStack{
RoundedRectangle(cornerRadius: 15).fill(Color.lightGray)
ProgressView {
Text("Loading...")
.font(.title2)
}
}.frame(width: 120, height: 120, alignment: .center)
.background(RoundedRectangle(cornerRadius: 25).stroke(.gray,lineWidth: 2))
}
}
}
Everything that happens between isLoading = true and isLoading = false in your Button("Select") action is running on the main thread and is therefore blocking the UI. The blocked UI cannot be updated so the UI will only update again when it reaches isLoading = false but then it will be hidden again.
You need to look into async/await and/or threading to solve your issue.

How can you show multiple alert dialogs, one after the other, in SwiftUI?

The following code shows a "Delete file" button. When the user presses it, a confirmation alert dialog appears. When the user presses "Delete" on that first dialog, I want to show a second alert dialog confirming that the file has been deleted. However, it never shows up, even though debugging the code confirmed that the second "return Alert..." statement is actually being executed as expected. Is there a way to make the second alert show up too?
import SwiftUI
enum alertShownType {
case alertNone, alertDeleteFile,alertFileDeleted
}
struct ContentView: View {
#State var showingAlert = false
#State var alertShown: alertShownType = alertShownType.alertNone
var body: some View {
Button(action: {
self.alertShown = alertShownType.alertDeleteFile
self.showingAlert = true
})
{
Text("Delete file")
}.padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing:0))
.alert(isPresented: $showingAlert, content: {
if (alertShown == alertShownType.alertDeleteFile)
{
return Alert(title: Text("Delete file"),
message: Text("Are you sure?"),
primaryButton: .destructive(Text("Delete")) {
// Delete the file
....
// Show the next alert
alertShown = alertShownType.alertFileDeleted
showingAlert = true
},
secondaryButton: .cancel())
}
else // alertFileDeleted
{
return Alert(title: Text("File deleted"), message:
Text("Done!"),
dismissButton: .default(Text("OK")))
}
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You just need
public func alert<Item>(item: Binding<Item?>, content: (Item) -> Alert)
Not required #State var showingAlert = false flag.
Setup you code like this
enum alertShownType : Identifiable {
case alertNone,
alertDeleteFile,
alertFileDeleted
var id : Int { get {
hashValue
}}
}
struct ContentViewAlerts: View {
#State var alertShown: alertShownType?
var body: some View {
Button(action: {
self.alertShown = alertShownType.alertDeleteFile
})
{
Text("Delete file")
}.padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing:0))
.alert(item: $alertShown, content: { alertType in
if (alertShown == alertShownType.alertDeleteFile)
{
return Alert(title: Text("Delete file"),
message: Text("Are you sure?"),
primaryButton: .destructive(Text("Delete")) {
// Delete the file
// ....
// Show the next alert
alertShown = alertShownType.alertFileDeleted
},
secondaryButton: .cancel())
}
else // alertFileDeleted
{
return Alert(title: Text("File deleted"), message:
Text("Done!"),
dismissButton: .default(Text("OK")))
}
})
}
}

TabView disconnects when rotating to Landscape due to SwiftUI's re-render of parent-Views

Using Swift5.3.2, iOS14.4.1, XCode12.4,
As the following code shows, I am working with a quite complex TabView in Page-Mode in SwiftUI.
i.e. using iOS14's new possibility to show Pages:
.tabViewStyle(PageTabViewStyle())
Everything works.
Except, if I rotate my iPhone from Portrait to Landscape, the TabView disconnects and sets the selectedTab index to 0 (i.e. no matter where you scrolled to, rotating iPhone resets unwontedly to page 0).
The parent-View itself is in a complex View hierarchy. And one of the parent-View's of the TabView is updated during the TabView is shown (and swiped). And this might be the problem that the TabView gets re-rendered when rotating to Landscape.
What can I do to keep the TabView-Page during iPhone rotation ??
Here is the code:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
MediaTabView()
CloseButtonView()
}
}
}
And the MediaTabView at question:
struct MediaTabView: View {
#EnvironmentObject var appStateService: AppStateService
#EnvironmentObject var commService: CommunicationService
#State private var tagID = ""
#State private var selectedTab = 0
#State private var uniqueSelected = 0
#State private var IamInSwipingAction = false
var body: some View {
let myDragGesture = DragGesture(minimumDistance: 10)
.onChanged { _ in
IamInSwipingAction = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5000)) {
IamInSwipingAction = false // workaround: because onEnded does not work...
}
}
.onEnded { _ in
IamInSwipingAction = false
}
TabView(selection: self.$selectedTab) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
switch appStateService.appState {
case .content(let tagID):
return list.tagId == tagID
default:
return false
}
}) {
if list.paths.count > 0 {
ForEach(list.paths.indices, id: \.self) { index in
ZoomableScrollView {
if let url = URL(fileURLWithPath: list.paths[index]){
if url.containsImage {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
} else if url.containsVideo {
CustomPlayerView(url: url)
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
Text(LocalizedStringKey("MediaNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.onAppear() {
if uniqueSelected != selectedTab {
uniqueSelected = selectedTab
if IamInSwipingAction && (commService.communicationRole == .moderatorMode) {
commService.send(thCmd: THCmd(key: .swipeID, sender: "", content: URL(fileURLWithPath: list.paths[index]).lastPathComponent))
}
}
}
}
} else {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
}
} else {
if Constants.TrihowAlbum.tagIdArrayTrihowAlbum.contains(tagID) {
Text(LocalizedStringKey("EmptyOrNoTrihowAlbumKey"))
.multilineTextAlignment(.center)
.padding()
} else {
Text(LocalizedStringKey("TagNotRecognizedKey"))
.multilineTextAlignment(.center)
.padding()
}
}
}
.onAppear() {
switch appStateService.appState {
case .content(let tagID):
self.tagID = tagID
default:
self.tagID = ""
}
}
.tabViewStyle(PageTabViewStyle())
.onTHComm_ReceiveCmd(service: commService) { (thCmd) in
switch thCmd.key {
case .swipeID:
if (commService.communicationRole == .moderatorMode) || (commService.communicationRole == .discoveryMode) {
selectTabFromCmdID(fileName: thCmd.content)
} else {
break
}
default:
break
}
}
.simultaneousGesture(myDragGesture)
}
}
extension MediaTabView {
private func selectTabFromCmdID(fileName: String) {
if let list = appStateService.mediaViewModel.mediaList.first(where: { (list) -> Bool in
return list.tagId == tagID
}) {
if list.paths.count > 0 {
if let idx = list.paths.firstIndex(where: { (urlPath) -> Bool in
if let url = URL(string: urlPath) {
return url.lastPathComponent == fileName
} else { return false }
}) {
selectedTab = idx
}
}
}
}
}

ForEach(0...5,id: \.self) { n in --what is the scope of "n"?

See the following TestView. When I clicked "Button 5", I see the alert shows "Button 0 clicked".
struct TestView: View {
#State var showAlert = false
var body: some View {
VStack {
ForEach(0...5, id: \.self) { n in
Button(action: {
self.showAlert.toggle()
}) {
Text("Button \(n)")
}
.padding(20)
.border(Color.green, width: 4)
.padding(8)
.alert(isPresented: self.$showAlert) {
Alert(title: Text("Button \(n) clicked"))
}
}
}
}
}
Regardless of which button I click, the alert always show "Button 0 clicked". I was expecting each button should show its own button index. Wondering why is that?
I suspect this is happening because everything in your TestView re-renders while state variable changes. So your alert is displaying only for the first loop iteration. The decision is to change other variable, which should contain clicked button index:
struct TextView: View {
#State var showAlert = false
#State var clickedButtonIndex = 0
var body: some View {
VStack {
ForEach(0...5, id: \.self) { n in
Button(action: {
self.showAlert.toggle()
self.clickedButtonIndex = n
}) {
Text("Button \(n)")
}
.padding(20)
.border(Color.green, width: 4)
.padding(8)
.alert(isPresented: self.$showAlert) {
Alert(title: Text("Button \(self.clickedButtonIndex) clicked"))
}
}
}
}
}
And you'll have this result: