Minimize or bring back sheet in SwiftUI - ios

I have a Voip calling app using CallKit and when call received, it will open a view call IncomingView in a sheet in my SwiftUI app. So far so good. But i want to minimize the sheet and can navigate to other pages and preferably shows a green bar at the navigation (similar to WhatApp) that indicates the call is going on and when i tap there, it should bring back my "IncomingView".
here is my code:
struct MainView: View {
let acceptPublishser = NotificationCenter.default
.publisher(for: Notification.Name.DidCallAccepted)
let endPublisher = NotificationCenter.default
.publisher(for: Notification.Name.DidCallEnd)
var body: some View {
NavigationView {
TabView {
TabListView()
.tabItem {
Label("Home", systemImage: "house.fill")
}
ContactListView()
.tabItem {
Label("Contacts", systemImage: "person.crop.circle")
}
ProfileView()
.tabItem {
Label("Profile", systemImage: "person.crop.circle")
}
}
.padding(0)
.onAppear(){
self.showModal = MyCallDelegate.shared.isIncomingCall
}
.sheet(isPresented: $showModal){
IncomingCallView() // -> Present IncomingCallview() as sheet
}
.accentColor(Color(.green))
}.onReceive(self.acceptPublishser, perform: { output in
showModal = true
})
.onReceive(self.endPublisher, perform: { output in
showModal = false
})
}
}
struct IncomingCallView: View {
var body: some View {
VStack{
Spacer()
Text("callerId").foregroundColor(.white)
Text("Timer").foregroundColor(.white)
}
}

Related

SwiftUI Alert does not dismiss when timer is running

Semi related question: SwiftUI ActionSheet does not dismiss when timer is running
I am currently experiencing an issue with alerts in a project that I am working on. Presented alerts will not dismiss when there is a timer running in the background. Most of the time it requires several clicks of the dismissal button to disappear. I have recreated this issue with as little overhead as possible in a sample project.
My primary project has this issue when trying to display an alert on a different view but I could not reproduce that issue in the sample project. The issue can be reliably replicated by toggling the alert on the same view that the timer is running. I have also tested by removing the binding from the text field to stop the text field view from updating. The alert still fails to dismiss on the first click. I am unsure if there is a way to work around this and am looking for any advice possible.
Xcode 13.0/iOS 15.0 and occurs in iOS 14.0 also
Timerview.swift
struct TimerView: View {
#ObservedObject var stopwatch = Stopwatch()
#State var isAlertPresented:Bool = false
var body: some View {
VStack{
Text(String(format: "%.1f", stopwatch.secondsElapsed))
.font(.system(size: 70.0))
.minimumScaleFactor(0.1)
.lineLimit(1)
Button(action:{
stopwatch.actionStartStop()
}){
Text("Toggle Timer")
}
Button(action:{
isAlertPresented.toggle()
}){
Text("Toggle Alert")
}
}
.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),message:Text("I am presented"))
}
}
}
Stopwatch.swift
class Stopwatch: ObservableObject{
#Published var secondsElapsed: TimeInterval = 0.0
#Published var mode: stopWatchMode = .stopped
func actionStartStop(){
if mode == .stopped{
start()
}else{
stop()
}
}
var timer = Timer()
func start() {
secondsElapsed = 0.0
mode = .running
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.secondsElapsed += 0.1
}
}
func stop() {
timer.invalidate()
mode = .stopped
}
enum stopWatchMode {
case running
case stopped
}
}
Edit:
Moving the button to a custom view solves the initial problem but is there a solution for when the button needs to interact with the Observable object?
Button(action:{
do{
try stopwatch.actionDoThis()
}catch{
isAlertPresented = true
}
}){
Text("Toggle Alert")
}.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),message:Text("I am presented"))
Every time timer runs UI will recreate, since "secondsElapsed" is an observable object. SwiftUI will automatically monitor for changes in "secondsElapsed", and re-invoke the body property of your view.
In order to avoid this we need to separate the button and Alert to another view like below.
struct TimerView: View {
#ObservedObject var stopwatch = Stopwatch()
#State var isAlertPresented:Bool = false
var body: some View {
VStack{
Text(String(format: "%.1f", stopwatch.secondsElapsed))
.font(.system(size: 70.0))
.minimumScaleFactor(0.1)
.lineLimit(1)
Button(action:{
stopwatch.actionStartStop()
}){
Text("Toggle Timer")
}
CustomAlertView(isAlertPresented: $isAlertPresented)
}
}
}
struct CustomAlertView: View {
#Binding var isAlertPresented: Bool
var body: some View {
Button(action:{
isAlertPresented.toggle()
}){
Text("Toggle Alert")
}.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),message:Text("I am presented"))
}
}
}
If you really need the ObservedObject or any attribute of it in order to perform any action in case of "OK" action of the alert, you can do a workaround.
struct TimerView: View {
#ObservedObject var stopwatch = Stopwatch()
#State var isResetAccepted: Bool = false
var body: some View {
VStack{
Text(String(format: "%.1f", stopwatch.secondsElapsed))
.font(.system(size: 70.0))
.minimumScaleFactor(0.1)
.lineLimit(1)
Button(action:{
stopwatch.actionStartStop()
}){
Text("Toggle Timer")
}
CustomAlertView(isResetAccepted: $isResetAccepted)
.onChange(of: isResetAccepted) { newValue in
if newValue {
isResetAccepetd = false
stopwatch.reset()
}
}
}
}
}
struct CustomAlertView: View {
#Binding var isResetAccepted: Bool
#State var isAlertPresented: Bool = false
var body: some View {
Button(action:{
isAlertPresented.toggle()
}){
Text("Toggle Alert")
}.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),
message:Text("I am presented"),
primaryButton: .destructive(Text("Cancel"), action: {
self.isResetAccepted = false
self.isAlertPresented = false
}),
secondaryButton: .default(Text("OK"), action: {
self.isResetAccepted = true
self.isAlertPresented = false
}))
}
}
}

How to Show Alert from Anywhere in app SwiftUI?

I have condition to show alert in a view which can able to show from anywhere in the app. Like I want to present it from root view so it can possibly display in all view. Currently what happens when I present from very first view it will display that alert until i flow the same Navigation View. Once any sheets open alert is not displayed on it. Have any solutions in SwiftUI to show alert from one place to entire app.
Here is my current Implementation of code.
This is my contentView where the sheet is presented and also alert added in it.
struct ContentView: View {
#State var showAlert: Bool = false
#State var showSheet: Bool = false
var body: some View {
NavigationView {
Button(action: {
showSheet = true
}, label: {
Text("Show Sheet")
}).padding()
.sheet(isPresented: $showSheet, content: {
SheetView(showAlert: $showAlert)
})
}
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Alert"))
})
}
}
Here from sheet I am toggle the alert and the alert is not displayed.
struct SheetView: View {
#Binding var showAlert: Bool
var body: some View {
Button(action: {
showAlert = true
}, label: {
Text("Show Alert")
})
}
}
here is the error in debug when we toggle button
AlertDemo[14187:3947182] [Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x109009c00> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x103908b50> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x103908b50>) which is already presenting <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x103d05f50>.
Any solution for that in SwiftUI? Thanks in Advance.
I was able to achieve this with this simplified version of what #workingdog suggested in their answer. It works as follows:
create the Alerter class that notifies the top-level and asks to display an alert
class Alerter: ObservableObject {
#Published var alert: Alert? {
didSet { isShowingAlert = alert != nil }
}
#Published var isShowingAlert = false
}
render the alert at the top-most level, for example in your #main struct or the ContentView
#main
struct MyApp: App {
#StateObject var alerter: Alerter = Alerter()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(alerter)
.alert(isPresented: $alerter.isShowingAlert) {
alerter.alert ?? Alert(title: Text(""))
}
}
}
}
set the alert that should be displayed from inside a child view
struct SomeChildView: View {
#EnvironmentObject var alerter: Alerter
var body: some View {
Button("show alert") {
alerter.alert = Alert(title: Text("Hello from SomeChildView!"))
}
}
}
Note on sheets
If you present views as sheets, each sheet needs to implement its own alert, just like MyApp does above.
If you have a NavigationView inside your sheet and present other views within this navigation view in the same sheet, the subsequent sheets can use the first sheet's alert, just like SomeChildView does in my example above.
Here is a possible example solution to show an Alert anywhere in the App.
It uses "Environment" and "ObservableObject".
import SwiftUI
#main
struct TestApp: App {
#StateObject var alerter = Alerter()
var body: some Scene {
WindowGroup {
ContentView().environment(\.alerterKey, alerter)
.alert(isPresented: $alerter.showAlert) {
Alert(title: Text("This is the global alert"),
message: Text("... alert alert alert ..."),
dismissButton: .default(Text("OK")))
}
}
}
}
struct AlerterKey: EnvironmentKey {
static let defaultValue = Alerter()
}
extension EnvironmentValues {
var alerterKey: Alerter {
get { return self[AlerterKey] }
set { self[AlerterKey] = newValue }
}
}
class Alerter: ObservableObject {
#Published var showAlert = false
}
struct ContentView: View {
#Environment(\.alerterKey) var theAlerter
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: SecondView()) {
Text("Click for second view")
}.padding(20)
Button(action: { theAlerter.showAlert.toggle()}) {
Text("Show alert here")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SecondView: View {
#Environment(\.alerterKey) var theAlerter
var body: some View {
VStack {
Button(action: { theAlerter.showAlert.toggle()}) {
Text("Show alert in second view")
}
}
}
}

TabView selection resets to first tab on sheet presentation

Update:
Thanks to Harshil & Sumit for pointing me out that I was simply too dumb to realize that I was using id() instead of tag(). If there is anything you can learn from this question, it is this:
When you work alone on a project, you tend to go blind. You don't see your own mistakes. Do code reviews. Ask friends and colleagues to look over it. It's a great idea. ;)
Original Question:
In my SwiftUI project I am using a TabView with a $selection Binding for being able to switch tabs programmatically.
The problem is: When I present a sheet on, for example, the second view contained in the TabView the selection is reset to the first tab.
For me this seems like a SwiftUI bug - but is there a workaround?
Below you find a working example that demonstrates the behaviour. (Tested with Xcode 12.4)
How to test: Go to the second tab, tap the button "Two" and you will see that you get back to the first tab. As soon as you remove the selection property from the TabView this does not happen any more.
Cheers
Orlando 🍻
enum TabPosition: Hashable {
case one
case two
case three
}
struct RootView: View {
#State private var selection: TabPosition = .one
var body: some View {
TabView(selection: $selection) {
One()
.tabItem { Label("One", systemImage: "1.circle") }
.id(TabPosition.one)
Two()
.tabItem { Label("Two", systemImage: "2.circle") }
.id(TabPosition.two)
Three()
.tabItem { Label("Three", systemImage: "3.circle") }
.id(TabPosition.three)
}
}
}
struct One: View {
var body: some View {
Text("One").padding()
}
}
struct Two: View {
#State var isPresented = false
var body: some View {
Button("Two") { isPresented.toggle() }
.sheet(isPresented: $isPresented, content: {
Three()
})
}
}
struct Three: View {
var body: some View {
Text("Three").padding()
}
}
Use .tag() like this:
struct ContentView: View {
#State private var selection = 1
var body: some View {
TabView(selection: $selection) {
One()
.tabItem { Label("One", systemImage: "1.circle") }
.tag(1)
Two()
.tabItem { Label("Two", systemImage: "2.circle") }
.tag(2)
Three()
.tabItem { Label("Three", systemImage: "3.circle") }
.tag(3)
}
}
}
Insted of id .id(TabPosition.one) assign tag like this .tag(TabPosition.one)

SwiftUI List NavigationView onDelete alert confirmation has ugly animation

The problem is that whenever you put the list in navigationView, the animation of the delete cancelation of the list row is not so nice. Am I doing something wrong in my body property?
var body: some View {
NavigationView {
VStack {
List {
ForEach(self.contacts){ contact in
ContactRow(contact: contact)
}.onDelete { self.setDeletIndex(at: $0) }
}
.alert(isPresented: $showConfirm) {
Alert(title: Text("Delete"), message: Text("Sure?"),
primaryButton: .cancel(),
secondaryButton: .destructive(Text("Delete")) {
self.delete()
})
}
.listStyle(PlainListStyle())
.navigationTitle("Contacts")
.navigationBarItems(trailing: HStack {
Button("Add", action: self.addItem)
})
}
}
}

App freezes when I change EnvironmentObject value

I want to login my user when they click a button. When they click the button, it calls self.auth.login() which changes loggedIn to true.
When this change happens, ContentView is meant to reload with HomeView() as the body View. However it just crashes the app.
Here's my code:
ContentView.swift
struct ContentView: View {
#EnvironmentObject var settings: UserSettings
#EnvironmentObject var auth: UserAuth
var body: some View {
if (!auth.loggedIn){
return AnyView(LoginView())
} else {
return AnyView(HomeView())
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserAuth())
}
}
class UserAuth: ObservableObject {
#Published var loggedIn: Bool = false
func login(){
self.loggedIn = true
}
}
struct LoginView: View {
#EnvironmentObject var auth: UserAuth
var body: some View {
ZStack {
Color.orange
.edgesIgnoringSafeArea(.all)
VStack {
Image("launcher_logo").resizable()
.scaledToFit()
.frame(height: 100)
.padding(.top, 100)
Spacer()
Button(action: {
self.auth.login()
print("loggedIn: ", self.auth.loggedIn) // prints "loggedIn: true" then doesn't print again when pressed
}) {
Text(String(auth.loggedIn))
}
...
Any idea what the problem is?
EDIT:
If I change my ContentView View to just LoginView() then it doesn't freeze. The app works. But I wan ContentView to be dynamic based on whether the user is loggedIn or not.
EDIT2: HomeView is causing it to crash:
struct HomeView: View {
// #EnvironmentObject var auth: UserAuth
var body: some View {
TabView {
HomeView()
.tabItem {
Image(systemName: "1.square.fill")
Text(String("4"))
}.onTapGesture {
// self.auth.login()
}
HomeView()
.tabItem {
Image(systemName: "3.square.fill")
}
Text("The Last Tab")
.tabItem {
Image(systemName: "3.square.fill")
Text("Profile")
}
}
.font(.headline)
}
func goOnline(){
print("went online")
}
}
console log:
[Firebase/Crashlytics][I-CLS000000] Failed to download settings Error Domain=FIRCLSNetworkError Code=-5 "(null)" UserInfo={status_code=404, type=2, request_id=, content_type=text/html; charset=utf-8}
2020-06-04 19:53:04.408869+1000 App[6718:4035694] Connection 5: received failure notification
2020-06-04 19:53:04.409001+1000 App[6718:4035694] Connection 5: failed to connect 3:-9816, reason -1
2020-06-04 19:53:04.409111+1000 App[6718:4035694] Connection 5: encountered error(3:-9816)
Your HomeView calls itself, causing an infinite loop.

Resources