SwiftUI, changing a view from scene(:continue - ios

In a SwiftUI app, I am facing a new challenge and hope someone can give me some hints or guidance. The mechanisms I have seen up to now to communicate between the parts of an app don't seem to quite fit here. But it is probably due to my still limited experience with SwiftUI.
First here is the relevant code:
class SceneDelegate {
... lot of code irrelevant to the question ...
func scene(_ scene: UIScene,
continue userActivity: NSUserActivity) {
... useful things happening for the app ...
// Now the view should change ... by some mechanism.
// This is the point of the question.
}
}
and:
struct ContentView: View {
... lot of code irrelevant to the question ...
var body: some View {
VStack {
... code to draw the view ...
}
... more code to draw the view ...
}
}
Second, my question is: how do I make my view to redraw itself, after processing has been performed inside scene(:continue ?
I had in mind some ideas, to do things in the scene(:continue function which would influence the drawing of the view.
Unfortunately, when trying to implement, I realized the code drawing the view was executed before the scene(:continue function. Therefore I need some other mechanism (notification like, bindings like, or ??) to have the view redrawn.
Is there a good practice or standard way of doing that?

It would be appropriate to use EnvironmentObject in this scenario
class AppState: ObservableObject
#Published var someVar: Sometype
}
class SceneDelegate {
let appState = AppState()
func scene(_ scene: UIScene,
continue userActivity: NSUserActivity) {
// ... other code
appState.someVar = ... // modify
}
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// .. other code
let contentView = ContentView()
.environmentObject(appState)
//
}
}
struct ContentView: View {
#EnvironmentObject var appState
var body: some View {
VStack {
// ... other code
appState.someVar // use here as needed
}
}
}

Related

Detect app launch from WidgetKit widget extension

Tapping a WidgetKit widget automatically launches its parent application. How can I detect if my application was launched from its WidgetKit widget extension?
I'm unable to find any documentation on capturing this in the applications AppDelegate and/or SceneDelegate.
To detect an app launch from a WidgetKit widget extension where the parent application supports scenes you'll need to implement scene(_:openURLContexts:), for launching from a background state, and scene(_:willConnectTo:options:), for launching from a cold state, in your parent application's SceneDelegate. Also, add widgetURL(_:) to your widget's view.
Widget's View:
struct WidgetEntryView: View {
var entry: SimpleEntry
private static let deeplinkURL: URL = URL(string: "widget-deeplink://")!
var body: some View {
Text(entry.date, style: .time)
.widgetURL(WidgetEntryView.deeplinkURL)
}
}
Parent application's SceneDelegate:
// App launched
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _: UIWindowScene = scene as? UIWindowScene else { return }
maybeOpenedFromWidget(urlContexts: connectionOptions.urlContexts)
}
// App opened from background
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
maybeOpenedFromWidget(urlContexts: URLContexts)
}
private func maybeOpenedFromWidget(urlContexts: Set<UIOpenURLContext>) {
guard let _: UIOpenURLContext = urlContexts.first(where: { $0.url.scheme == "widget-deeplink" }) else { return }
print("🚀 Launched from widget")
}
If you are setting widgetURL or Link control for Widget UI then containing app opens with application(_:open:options:). You can set additional data in URL to know source.
If you are not using widgetUrl or link control then containing app opens with application(_:continue:restorationHandler:) and userInfo has WidgetCenter.UserInfoKey. That should tell you App opened from widget and information about user's interaction.
SwiftUI 2 life cycle
Add widgetURL to your Widget view:
struct SimpleWidgetEntryView: View {
var entry: SimpleProvider.Entry
private static let deeplinkURL = URL(string: "widget-deeplink://")!
var body: some View {
Text("Widget")
.widgetURL(Self.deeplinkURL)
}
}
Detect if the app is opened with a deeplink in onOpenURL:
#main
struct WidgetTestApp: App {
#State var linkActive = false
var body: some Scene {
WindowGroup {
NavigationView {
VStack {
NavigationLink("", destination: Text("Opened from Widget"), isActive: $linkActive).hidden()
Text("Opened from App")
}
}
.onOpenURL { url in
guard url.scheme == "widget-deeplink" else { return }
linkActive = true
}
}
}
}
Here is a GitHub repository with different Widget examples including the DeepLink Widget.

Update state of a screen struct from SceneDelegate

I'm coming from react-native and a beginner at Swift and SwiftUI and I was curious how to perform an action and update state on a specific screen when the app comes back into the foreground. I want to check the status of the notifications("allowed, "denied" etc.) and update the UI.
This is some example code - Here is the view I want to update:
struct Test: View {
#State var isNotificationsEnabled : Bool
var body : some View {
Toggle(isOn: self.isNotificationsEnabled) {
Text("Notifications")
}
}
}
So far what I've been reading is that you need to do edit the func sceneWillEnterForeground(_ scene: UIScene) inside SceneDelegate.swift but how on earth do I update the state of my Test struct from there? I'm thinking we need some kind of global state but that's just a guess.
Any advice?
Here is simplest approach
struct Test: View {
#State private var isNotificationsEnabled : Bool
private let foregroundPublisher = NotificationCenter.default.publisher(for: UIScene.willEnterForegroundNotification)
var body : some View {
Toggle(isOn: self.$isNotificationsEnabled) {
Text("Notifications")
}
.onReceive(foregroundPublisher) { notification in
// do anything needed here
}
}
}
Of course, if your application can have multiple scenes and you need to differentiate them somehow then more complicated variant of this approach will be needed to differentiate which scene generates this notification.

Open a new window in Mac Catalyst

I am porting an iPad app using Mac Catalyst. I am trying to open a View Controller in a new window.
If I were using strictly AppKit I could do something as described in this post. However, since I am using UIKit, there is no showWindow() method available.
This article states that this is possible by adding AppKit in a new bundle in the project (which I did), however it doesn't explain the specifics on how to actually present the new window. It reads...
Another thing you cannot quite do is spawn a new NSWindow with a UIKit view hierarchy. However, your UIKit code has the ability to spawn a new window scene, and your AppKit code has the ability to take the resulting NSWindow it's presented in and hijack it to do whatever you want with it, so in that sense you could spawn UIKit windows for auxiliary palettes and all kinds of other features.
Anyone know how to implement what is explained in this article?
TL;DR: How do I open a UIViewController as a new separate NSWindow with Mac Catalyst?
EDIT : ADDED INFO ON HOW TO HAVE ADDITIONAL DIFFERENT WINDOWS LIKE PANELS
In order to support multiple windows on the mac, all you need to do is to follow supporting multiple windows on the iPad.
You can find all the information you need in this WWDC session starting minute 22:28, but to sum it up what you need to do is to support the new Scene lifecycle model.
Start by editing your target and checking the support multiple window checkmark
Once you do that, click the configure option which should take you to your info.plist.
Make sure you have the proper entry for Application Scene Manifest
Create a new swift file called SceneDelegate.swift and just paste into it the following boilerplate code
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
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).
// Create the SwiftUI view that provides the window contents.
guard let _ = (scene as? UIWindowScene) else { return }
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
And you're basically done. Run your app, and hit command + N to create as many new windows as you want.
If you want to create a new window in code you can use this:
#IBAction func newWindow(_ sender: Any) {
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: nil, options: nil) { (error) in
//
}
}
And now we get to the big mystery of how to create additional windows
The key to this is to create multiple scene types in the app. You can do it in info.plist which I couldn't get to work properly or in the AppDelegate.
Lets change the function to create a new window to:
#IBAction func newWindow(_ sender: Any) {
var activity = NSUserActivity(activityType: "panel")
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil) { (error) in
}
}
Create a new storyboard for your new scene, create at least one viewcontroller and make sure to set in as the initalviewcontroller in the storyboard.
Lets add to the appdelegate the following function:
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
if options.userActivities.first?.activityType == "panel" {
let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
configuration.delegateClass = CustomSceneDelegate.self
configuration.storyboard = UIStoryboard(name: "CustomScene", bundle: Bundle.main)
return configuration
} else {
let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
configuration.delegateClass = SceneDelegate.self
configuration.storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
return configuration
}
}
By setting the userActivity when requesting a scene we can know which scene to create and create the configuration for it accordingly. New Window from the menu or CMD+N will still create your default new window, but the new window button will now create the UI from your new storyboard.
And tada:
With SwiftUI you can do it like this (thanks to Ron Sebro):
1. Activate multiple window support:
2. Request a new Scene:
struct ContentView: View {
var body: some View {
VStack {
// Open window type 1
Button(action: {
UIApplication.shared.requestSceneSessionActivation(nil,
userActivity: NSUserActivity(activityType: "window1"),
options: nil,
errorHandler: nil)
}) {
Text("Open new window - Type 1")
}
// Open window type 2
Button(action: {
UIApplication.shared.requestSceneSessionActivation(nil,
userActivity: NSUserActivity(activityType: "window2"),
options: nil,
errorHandler: nil)
}) {
Text("Open new window - Type 2")
}
}
}
}
3. Create your new window views:
struct Window1: View {
var body: some View {
Text("Window1")
}
}
struct Window2: View {
var body: some View {
Text("Window2")
}
}
4. Change SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
if connectionOptions.userActivities.first?.activityType == "window1" {
window.rootViewController = UIHostingController(rootView: Window1())
} else if connectionOptions.userActivities.first?.activityType == "window2" {
window.rootViewController = UIHostingController(rootView: Window2())
} else {
window.rootViewController = UIHostingController(rootView: ContentView())
}
self.window = window
window.makeKeyAndVisible()
}
}

SwiftUI - How to trigger an Alert from a seperate class

I have a class called Startup. In this class, I have a bunch of functions that I call from SceneDelegate when the app starts.
for example:
class Startup {
func doThis() {
print("I'm doing this")
}
}
At one point, it may download data, if its necessary. When this happens, I'd like to trigger an Alert w/ Activity Indicator to block the UI from user input until things are completed.
I have the Activity Indicator Alert all set up in ContentView, which triggers on an EnvironmentObject var Bool. So all I need to do to activate this UI blocking alert is to toggle() my env var.
The problem I am having is that I can not trigger this env var from within my class. I have tried the following:
When I put the #EnvironmentObject var dataBusy: DataBusy
and call it from within a function: dataBusy.isBusy = true, I get the error message:
Fatal error: No ObservableObject of type DataBusy found.
Which indicates that I need to shove the env object into the environment of the class when it is instantiated, however, when I try to do that, I get:
Value of tuple type '()' has no member 'environmentObject'
So, I can not add this env var into this class object.
Trying to use:
#ObservedObject var dataBusy = DataBusy()
In my class seems to not error out, but toggling this does not do anything to trigger my event.
I can't think of any other way to communicate with my View from this startup class.
Any ideas?
The following code works, you many find what you miss or misunderstanding.
class Env: NSObject, ObservableObject{
#Published var isEnabled = false
}
struct AlertTest: View {
#EnvironmentObject var envObject: Env
var body: some View {
Text("board").alert(isPresented: $envObject.isEnabled) { () -> Alert in
return Alert(title: Text("hello"))
}
}
}
//Scene Delegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let m = Env()
let contentView = AlertTest().environmentObject(m)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { (Timer) in
DispatchQueue.main.async {
m.isEnabled = true
}
}
}
Got it..
E.Coms answer is very good, however, I was calling the func which would be responsible for this action from within another function, within another function from this other class, not directly from the scene delegate.
To get it, I had to pass the DataBusy object dataBusy from my scene delegate to my class func as a parameter:
//Scene Delegate:
let dataBusy = DataBusy() //my Observed Object Class
let myClass = MyClass(); //instantiate my class
myClass.myFunc(data: dataBusy) //pass the object through to the class function
This way, when the time arises within my class function, I can toggle the variable and it all works perfectly.
Thank you for your input.

swiftUI: Navigate to Home screen after Login Completed. Navigating Views by Button click

I'm doing a sign-in screen. after the sign-in button click, I'll do an API call on the response I will redirect the user to home screen.
Q - How to navigate from one View to Another by a button click with some action.
I've tried this,
Programatically navigate to new view in SwiftUI
But I'm getting an error like,
"Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type"
Please help me to resolve it.
There is an issue with some View ATM (hope it will be fixed soon).
In order to use code posted, you will need to write something like:
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
#ViewBuilder
var body: some View {
if !userAuth.isLoggedin {
return LoginView()
} else {
return NextView()
}
}
}
At the list, at the moment of writing, this was the only thing working for me - both for body and for Group.
Reference for future: date 24 Oct 2019.
Alternatively look at SceneDelgate.swift where you can set the root view of the key window to whatever you want.
In your situation when there is a successful login you can signal the change of state to the SceneDelegate (such as by Notification). Then have the app set the root view controller to your main View (as a UIHostingController).
For example:
In your SceneDelegate class add:
var currentScene: UIScene? // need to keep reference
Then inside the func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
Save a reference to the scene in the variable declared above.
self.currentScene = scene
Next, add a listener to when you want to change the key window with the new view:
var keyWindow: UIWindow?
NotificationCenter.default.addObserver(forName: .newUser, object: nil, queue: .main) { [weak self] (notification) in
guard let windowScene = self?.currentScene as? UIWindowScene else { return }
keyWindow = UIWindow(windowScene: windowScene)
keyWindow?.rootViewController = UIHostingController(rootView: Text("hello"))
keyWindow?.makeKeyAndVisible()
}
Just set the Notification to post whenever you need and replace the Text with whatever View you want.

Resources