Push SwifuUIscreen from AppDelegate - swift

How can i push a SwiftUI screen from AppDelegate, e.x. when i click on a Remote notification in willPresent Method, i want the user to redirect to my TabBarView() with tab notification selected. I also need to have initialized my rootViewModel since i have logic there.
MainApp:
#main
struct MainApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
private var rootViewModel: MainViewModel
init() {
self.rootViewModel = MainViewModel()
}
var body: some Scene {
WindowGroup {
if authenticated {
Tabbar()
.environmentObject(rootViewModel)
} else {
SecondScreen()
.environmentObject(rootViewModel)
}
}
}
}
AppDelegate:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
print("willPresent notification")
let info = notification.request.content.userInfo
completionHandler([.badge, .sound, .banner])
}

If you want to control the tab selection programmatically from AppDelegate:
You have to use the TabBar initializer that takes a Binding to the selected tab, and
You have to be able to set the binding's value from AppDelegate.
Here's one solution.
First, define an enum representing the tab selection:
enum TabSelection: Hashable {
case firstTab
case secondTab
case thirdTab
}
Then, make your AppDelegate conform to ObservableObject and give it a Published property that holds the tab selection:
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
#Published var tabSelection: TabSelection = .firstTab
}
Finally, update your view to use tabSelection to control the selected tab:
Get a Binding<TabSelection> from appDelegate,
pass it to the TabView initializer, and
use the tag modifier on each tab subview.
struct MainApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
private var rootViewModel: MainViewModel
init() {
self.rootViewModel = MainViewModel()
}
var body: some Scene {
WindowGroup {
if rootViewModel.authenticated {
TabView(selection: _appDelegate.projectedValue.tabSelection) {
FirstTab()
.tag(TabSelection.firstTab)
SecondTab()
.tag(TabSelection.secondTab)
ThirdTab()
.tag(TabSelection.thirdTab)
}
.environmentObject(rootViewModel)
} else {
SecondScreen()
.environmentObject(rootViewModel)
}
}
}
}
You should be able to say $appDelegate.tabSelection to get the Binding. But I'm writing this answer on my iPad and the Swift Playgrounds app is complaining about $appDelegate, so I'm using _appDelegate.projectedValue.tabSelection to get the Binding.

Related

Deinit not called when using UIImagePickerController in SwiftUI

The deinit block inside Custom is not called. I also tried the onDismiss variant instead of the isPresent Binding, but both do not run the deinit block for type Custom.
To reproduce my problem either clone the app and run it, or check out the code below. The deinit block is called when directly subclassing UIViewController, but it goes wrong for UIImagePickerController.
Clone: https://github.com/Jasperav/MemoryLeak
Code:
import Combine
import SwiftUI
struct ContentView: View {
#State var present = false
var body: some View {
Button("click me") {
present = true
}
.sheet(isPresented: $present) {
MediaPickerViewWrapperTest(isPresented: $present)
}
}
}
class Custom: UIImagePickerController {
deinit {
print("DEINIT")
}
}
struct MediaPickerViewWrapperTest: UIViewControllerRepresentable {
let isPresented: Binding<Bool>
func makeUIViewController(context: Context) -> Custom {
let c = Custom()
c.delegate = context.coordinator
return c
}
func updateUIViewController(_: Custom, context _: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(
isPresented: isPresented
)
}
}
final class Coordinator: NSObject, UINavigationControllerDelegate,
UIImagePickerControllerDelegate
{
#Binding var isPresented: Bool
init(
isPresented: Binding<Bool>
) {
_isPresented = isPresented
}
func imagePickerController(
_: UIImagePickerController,
didFinishPickingMediaWithInfo _: [
UIImagePickerController
.InfoKey: Any
]
) {
isPresented = false
}
func imagePickerControllerDidCancel(_: UIImagePickerController) {
isPresented = false
}
}
A possible workaround is to use view representable instead.
Tested with Xcode 13.2 / iOS 15.2
Below is modified part only, everything else is the same:
struct MediaPickerViewWrapperTest: UIViewRepresentable {
let isPresented: Binding<Bool>
func makeUIView(context: Context) -> UIView {
let c = Custom()
c.delegate = context.coordinator
return c.view
}
func updateUIView(_: UIView, context _: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(
isPresented: isPresented
)
}
}
When do de-initializers run? When the reference of the object reaches zero.
Thus, you expect the reference count to become zero when the picker is dismissed or is removed from the UI hierarchy. And while that might well happen, it's not guaranteed to.
Reference counting is not that simple, especially once you've handed your object to another framework (UIKit in this case). Once you do it, you no longer have full control over the lifecycle of that object. The internal implementation details of the other framework can keep the object alive more than you assume it would, thus the deinit code might not be called with the timing you expect.
Recommeding to instead rely on UIViewController's didMove(toParent:) method, and write the cleanup logic there.
And even if you're not handing your custom class instance to another framework, relying on the object's lifecycle for other side effects is not always reliable, as the object can end up being retained by unexpected new owners.
Bottom line - deinit should be used to clean up stuff related to that particular object.

iOS: Alert is only triggered once SwiftUI

I have an app that required some alerts. I have implemented the alert trigger with observableObject so that it can be triggered from the my coordinator class. The problem I am having is that the first trigger work and the alert shows. But after the first time no matter how many times I trigger the button the alert does not show.
The view has a webview (UIRepresentable) where when a button is pressed on the webpage the JS triggers the alert in the coordinator class
Relevant parts of code below
MainView with showAlerts class:
import SwiftUI
import WebKit
struct MainContentView: View {
#ObservedObject public var viewAlertsInstance = ShowAlerts()
var body: some View {
ZStack {
SubscriptionViewController(showAlert: viewAlertsInstance)
}
.alert(isPresented: $viewAlertsInstance.showAlert) {
Alert(
title: Text("Important message"),
message: Text("Something"),
dismissButton: .default(Text("Got it!"))
)
}
}
}
class ShowAlerts: ObservableObject {
#Published var showAlert:Bool = false
func displayAlert() {
self.showAlert.toggle()
print(showAlert)
}
}
SubscriptionView including the coordinator:
import SwiftUI
import WebKit
struct SubscriptionViewController: UIViewRepresentable {
#StateObject var showAlert: ShowAlerts
func makeUIView(context: UIViewRepresentableContext<SubscriptionViewController>) -> WKWebView {
let view = WKWebView()
view.navigationDelegate = context.coordinator
DispatchQueue.main.async {
let url = URL(string:"https")!
let request = URLRequest(url: url)
view.load(request)
}
context.coordinator.webView = view
return view
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<SubscriptionViewController>) {
}
func makeCoordinator() -> CoordinatorSubscription {
CoordinatorSubscription(self, showAlert: self.showAlert)
}
typealias UIViewType = WKWebView
}
class CoordinatorSubscription: NSObject, WKNavigationDelegate, WKScriptMessageHandler{
var control: SubscriptionViewController
var showAlert: ShowAlerts
var webView : WKWebView?
init(_ control: SubscriptionViewController, showAlert: ShowAlerts) {
self.control = control
self.showAlert = showAlert
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
processReturnedJS(body: message.body as! String)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
}
func processReturnedJS(body: String) {
//Here should toggle the alert
showAlert.displayAlert()
}
}
In the processReturnedJS function the showAlert.displayAlert() should show the alert every-time the JS is received
Would appreciate any help
Thanks
Swap your
#StateObject and #ObservedObject wrappers
StateObject is for initializing and ObservedObject takes its initial value as a parameter in the initializer per apple documentation.
Then remove
var showAlert: ShowAlerts
In your coordinator and access the variable like this
control.showAlert.displayAlert()
I think you flipped the use of #ObservedObject and #StateObject.
When you find yourself creating a default or a starting value for your object, like you're doing in MainContentView, use #StateObject.
When you are creating your object elsewhere up the view hierarchy and just want your view to observe the object, use #ObservedObject.

Redirecting to sheet or presentation when local notification touched in swiftui, ios14

I have little app, similar to system Alarm. When alarm is triggered, notification comes up and when touched it suppose to take user to particular view (modal, full view, doesn't really matter). Touching notification works but it only takes user back to app not to specific view. All UI is in SwiftUI, however I dont know how to change state of "triggered" variable which should open fullScreenCover when true.
Code:
NotifcationCenter:
class NotificationCenter: NSObject, ObservableObject {
#Published var isTouched: Bool = false
override init() {
super.init()
UNUserNotificationCenter.current().delegate = self
}
}
extension NotificationCenter: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) { }
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
isTouched = true
}
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { }}
AlarmView:
struct AlarmView: View {
// MARK: - PROPERTIES
#State var isAlarmOn: Bool = true
#State var editAlarmSheet: Bool = false
#EnvironmentObject var alarmVM: AlarmVM
#StateObject var localNotification = LocalNotification()
#State var triggered: Bool = false
#ObservedObject var notificationCenter: NotificationCenter
// MARK: - BODY
var body: some View {
NavigationView {
List {
ForEach(alarmVM.alarms, id:\.id) { alarm in
AlarmItem(alarm: alarm)
} // FOREACH
.onDelete(perform: { indexSet in
removeRow(at: indexSet)
})
} // LIST
.padding(.top, 40)
.navigationTitle("Alarms")
.navigationBarItems(trailing: Button( action: {
editAlarmSheet.toggle()
} ) {
Image(systemName: "plus")
}) // BUTTON
.sheet(isPresented: $editAlarmSheet, content: {
EditAlarmSheet().environmentObject(alarmVM)
}) // SHEET
} // NAVIGATION VIEW
.fullScreenCover(isPresented: $triggered, content: FullScreenModalView.init)
.onAppear(perform: {
localNotification.sendNotification()
triggered = notificationCenter.isTouched
})
.navigationViewStyle(StackNavigationViewStyle())
} // VIEW
I simply made State "triggered" equals "NotificationCenter.isTouched" in .onAppear() but I think it's too naive and simply doesn't work.
Any help would be much appreciated.
I fixed it, was really simple. Instead of using #State to present fullScreenCover I use NotificationCenter.isTouch which is published. Don't know I missed this simple solution.

Implement delegates within SwiftUI Views

I am trying to implement a functionality that requires a delegate method (like NSUserActivity). Therefore I need a UIViewController that conforms to NSUserActivityDelegate (or similar other delegates), handles and hold all the required information. My problem is that I am using SwiftUI for my interface and therefore I am not using UIViewControllers. So how can I implement this functionality and still use SwiftUI for the UI. What I tried: view1 is just a normal SwiftUI View that can present (via NavigationLink) view2 which is the view where in want to implement this functionality. So I tried instead of linking view1 and view2, linking view1 to a UIViewControllerRepresentable which then handles the implementation of this functionality and adds UIHostingController(rootView: view2) as a child view controller.
struct view1: View {
var body: some View {
NavigationLink(destination: VCRepresentable()) {
Text("Some Label")
}
}
}
struct view2: View {
var body: some View {
Text("Hello World!")
}
}
struct VCRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return implementationVC()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}
class implementationVC: UIViewController, SomeDelegate for functionality {
// does implementation stuff in delegate methods
...
override func viewDidLoad() {
super.viewDidLoad()
attachChild(UIHostingController(rootView: view2()))
}
private func attachChild(_ viewController: UIViewController) {
addChild(viewController)
if let subview = viewController.view {
subview.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(subview)
subview.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
subview.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
subview.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
subview.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
viewController.didMove(toParent: self)
}
}
I am having trouble with transferring the data between my VC and my view2. So I'm wondering if there is a better way to implement such a functionality within a SwiftUI View.
You need to create a view that conforms to UIViewControllerRepresentable and has a Coordinator that handles all of the delegate functionality.
For example, with your example view controller and delegates:
struct SomeDelegateObserver: UIViewControllerRepresentable {
let vc = SomeViewController()
var foo: (Data) -> Void
func makeUIViewController(context: Context) -> SomeViewController {
return vc
}
func updateUIViewController(_ uiViewController: SomeViewController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(vc: vc, foo: foo)
}
class Coordinator: NSObject, SomeDelegate {
var foo: (Data) -> Void
init(vc: SomeViewController, foo: #escaping (Data) -> Void) {
self.foo = foo
super.init()
vc.delegate = self
}
func someDelegateFunction(data: Data) {
foo(data)
}
}
}
Usage:
struct ContentView: View {
var dataModel: DataModel
var body: some View {
NavigationLink(destination: CustomView(numberFromPreviousView: 10)) {
Text("Go to VCRepresentable")
}
}
}
struct CustomView: View {
#State var instanceData1: String = ""
#State var instanceData2: Data?
var numberFromPreviousView: Int // example of data passed from the previous view to this view, the one that can react to the delegate's functions
var body: some View {
ZStack {
SomeDelegateObserver { data in
print("Some delegate function was executed.")
self.instanceData1 = "Executed!"
self.instanceData2 = data
}
VStack {
Text("This is the UI")
Text("That, in UIKit, you would have in the UIViewController")
Text("That conforms to whatever delegate")
Text("SomeDelegateObserver is observing.")
Spacer()
Text(instanceData1)
}
}
}
}
Note: I renamed VCRepresentable to SomeDelegateObserver to be more indicative of what it does: Its sole purpose is to wait for delegate functions to execute and then run the closures (i.e foo in this example) you provide it. You can use this pattern to create as many functions as you need to "observe" whatever delegate functions you care about, and then execute code that can update the UI, your data model, etc. In my example, when SomeDelegate fires someDelegateFunction(data:), the view will display "Excuted" and update the data instance variable.

Programmatically navigate to new view in SwiftUI

Descriptive example:
login screen, user taps "Login" button, request is performed, UI shows waiting indicator, then after successful response I'd like to automatically navigate user to the next screen.
How can I achieve such automatic transition in SwiftUI?
You can replace the next view with your login view after a successful login. For example:
struct LoginView: View {
var body: some View {
...
}
}
struct NextView: View {
var body: some View {
...
}
}
// Your starting view
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
if !userAuth.isLoggedin {
LoginView()
} else {
NextView()
}
}
}
You should handle your login process in your data model and use bindings such as #EnvironmentObject to pass isLoggedin to your view.
Note: In Xcode Version 11.0 beta 4, to conform to protocol 'BindableObject' the willChange property has to be added
import Combine
class UserAuth: ObservableObject {
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
self.isLoggedin = true
}
var isLoggedin = false {
didSet {
didChange.send(self)
}
// willSet {
// willChange.send(self)
// }
}
}
For future reference, as a number of users have reported getting the error "Function declares an opaque return type", to implement the above code from #MoRezaFarahani requires the following syntax:
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
if !userAuth.isLoggedin {
return AnyView(LoginView())
} else {
return AnyView(NextView())
}
}
}
This is working with Xcode 11.4 and Swift 5
struct LoginView: View {
#State var isActive = false
#State var attemptingLogin = false
var body: some View {
ZStack {
NavigationLink(destination: HomePage(), isActive: $isActive) {
Button(action: {
attlempinglogin = true
// Your login function will most likely have a closure in
// which you change the state of isActive to true in order
// to trigger a transition
loginFunction() { response in
if response == .success {
self.isActive = true
} else {
self.attemptingLogin = false
}
}
}) {
Text("login")
}
}
WaitingIndicator()
.opacity(attemptingLogin ? 1.0 : 0.0)
}
}
}
Use Navigation link with the $isActive binding variable
To expound what others have elaborated above based on changes on combine as of Swift Version 5.2 it could be simplified using publishers.
Create a class names UserAuth as shown below don't forget to import import Combine.
class UserAuth: ObservableObject {
#Published var isLoggedin:Bool = false
func login() {
self.isLoggedin = true
}
}
Update SceneDelegate.Swift with
let contentView = ContentView().environmentObject(UserAuth())
Your authentication view
struct LoginView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
...
if ... {
self.userAuth.login()
} else {
...
}
}
}
Your dashboard after successful authentication, if the authentication userAuth.isLoggedin = true then it will be loaded.
struct NextView: View {
var body: some View {
...
}
}
Lastly, the initial view to be loaded once the application is launched.
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
if !userAuth.isLoggedin {
LoginView()
} else {
NextView()
}
}
}
Here is an extension on UINavigationController that has simple push/pop with SwiftUI views that gets the right animations. The problem I had with most custom navigations above was that the push/pop animations were off. Using NavigationLink with an isActive binding is the correct way of doing it, but it's not flexible or scalable. So below extension did the trick for me:
/**
* Since SwiftUI doesn't have a scalable programmatic navigation, this could be used as
* replacement. It just adds push/pop methods that host SwiftUI views in UIHostingController.
*/
extension UINavigationController: UINavigationControllerDelegate {
convenience init(rootView: AnyView) {
let hostingView = UIHostingController(rootView: rootView)
self.init(rootViewController: hostingView)
// Doing this to hide the nav bar since I am expecting SwiftUI
// views to be wrapped in NavigationViews in case they need nav.
self.delegate = self
}
public func pushView(view:AnyView) {
let hostingView = UIHostingController(rootView: view)
self.pushViewController(hostingView, animated: true)
}
public func popView() {
self.popViewController(animated: true)
}
public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
navigationController.navigationBar.isHidden = true
}
}
Here is one quick example using this for the window.rootViewController.
var appNavigationController = UINavigationController.init(rootView: rootView)
window.rootViewController = appNavigationController
window.makeKeyAndVisible()
// Now you can use appNavigationController like any UINavigationController, but with SwiftUI views i.e.
appNavigationController.pushView(view: AnyView(MySwiftUILoginView()))
I followed Gene's answer but there are two issues with it that I fixed below. The first is that the variable isLoggedIn must have the property #Published in order to work as intended. The second is how to actually use environmental objects.
For the first, update UserAuth.isLoggedIn to the below:
#Published var isLoggedin = false {
didSet {
didChange.send(self)
}
The second is how to actually use Environmental objects. This isn't really wrong in Gene's answer, I just noticed a lot of questions about it in the comments and I don't have enough karma to respond to them. Add this to your SceneDelegate view:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
var userAuth = UserAuth()
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(userAuth)
Now you need to just simply create an instance of the new View you want to navigate to and put that in NavigationButton:
NavigationButton(destination: NextView(), isDetail: true, onTrigger: { () -> Bool in
return self.done
}) {
Text("Login")
}
If you return true onTrigger means you successfully signed user in.