How to launch a tutorial on first app launch using SwiftUI? - swiftui

I would like to launch a tutorial when my SwiftUI app first launches. Where in the project should this code go and how do I launch the tutorial, which is just a SwiftUI View, when the app first launches?
I already know how to check if the app has launched before using UserDefaults. I am wanting to know how to launch the SwiftUI view and then how to launch the standard ContentView after the user completes the tutorial.
let hasLaunchedBefore = UserDefaults.standard.bool(forKey: "hasLaunchedBefore")
if hasLaunchedBefore {
// Not first launch
// Load ContentView here
} else {
// Is first launch
// Load tutorial SwiftUI view here
UserDefaults.standard.set(true, forKey: "hasLaunchedBefore") // Set hasLaunchedBefore key to true
}

Try put this in your sceneDelegate
let hasLaunchedBefore = UserDefaults.standard.bool(forKey: "hasLaunchedBefore")
let content = ContentView()
let tutorial = TutorialView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
if hasLaunchedBefore {
window.rootViewController = UIHostingController(rootView: content)
} else {
window.rootViewController = UIHostingController(rootView: tutorial)
UserDefaults.standard.set(true, forKey: "hasLaunchedBefore")
}
self.window = window
window.makeKeyAndVisible()
}

In your SampleApp which after SwiftUI 2.0:
import SwiftUI
#main
struct SampleApp: App {
#AppStorage("didLaunchBefore") var didLaunchBefore: Bool = true
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
if didLaunchBefore {
SplashView()
} else {
ContentView()
}
}
}
}
Then add a button with action like below in SplashView:
UserDefaults.standard.set(false, forKey: "didLaunchBefore")

Related

SwiftUI (macOS) how to pass a variable to a new window (WindowGroup)?

I'm trying to use the new WindowGroup to display a more complex view in a new window but somehow I can't figure out how to pass values into the view.
Until yet I was playing around with NSWindow but there I can't use the new toolbar with .toolbar{} and I'm somehow getting weird errors when using the latest swiftUI features.
In my old code I just could pass my values into the new view like usual:
.simultaneousGesture(TapGesture(count: 2).onEnded {
var window: NSWindow!
if nil == window {
let serverView = serverView(content: content) // parse my struct content(name: "XServe-Test", configFile: "/FileUrl", permissions: .rootPermissions, cluster: cluster(x12, CPUMax: .cores(28), ramMax: .gb(1200)))
window = NSWindow(
contentRect: NSRect(x: 20, y: 20, width: 580, height: 400),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false)
window.center()
window.setFrameAutosaveName("ServerMainView")
window.isReleasedWhenClosed = false
window.title = content.name
window.contentView = NSHostingView(rootView: serverView)
window.toolbar = NSToolbar()
window.toolbarStyle = .unifiedCompact
}
window.makeKeyAndOrderFront(nil)
}
Now I'm using in the app file:
import SwiftUI
#main
struct myApp: App {
var body: some Scene {
WindowGroup {
contentView()
}.windowStyle(HiddenTitleBarWindowStyle())
.commands {
SidebarCommands()
ToolbarCommands()
}
// the view that should be displayed in a new window
WindowGroup("serverView") {
let inputContent : content = content(name: "XServe-Test", configFile: "/FileUrl", permissions: .rootPermissions, cluster: cluster(x12, CPUMax: .cores(28), ramMax: .gb(1200)))
serverView(content: inputContent) // this is static now :(
}.handlesExternalEvents(matching: Set(arrayLiteral: "serverView"))
}
and the following code to open the view:
.simultaneousGesture(TapGesture(count: 2).onEnded {
guard let url = URL(string: "com-code-myApp://serverView") else { return }
NSWorkspace.shared.open(url)
}
How do I pass the input from the tap gesture into the new view using the WindowGroup logic?
I found two ways to solve this problem. I'm using the first one, because my application is file based. The second solution is based on the great Pulse git repo.
In both cases you need to register a custom URL in the Xcode project settings under:
Tragets -> yourApp -> Info -> URL Types, otherwise it won't work.
first solution:
import SwiftUI
#main
struct myApp: App {
var body: some Scene {
// 'default' view
WindowGroup { contentView() }
// the view that should open if someone opens your file
WindowGroup("fileView") { fileView() }
.handlesExternalEvents(matching: ["file"]) // does the magic
}
}
struct fileView: View {
var body: some View {
VStack{ /* your view content */}
.onOpenURL(perform: { url in
// get url and read e.g.: your info file
})
}
}
// you can open a file using:
Button("openMyFileinAnExtraWindow"){
let fileUrl = URL(fileURLWithPath: "~/Documents/myFile.yourExtension")
NSWorkspace.shared.open(fileUrl)
}
second solution:
Notice: I created this code snippet based on the great Pulse git repo.
import SwiftUI
#main
struct myApp: App {
var body: some Scene {
// 'default' view
WindowGroup { contentView() }
// the view that should open if someone opens your file
WindowGroup { DetailsView() }
.handlesExternalEvents(matching: Set(arrayLiteral: "newWindow")) // this url must be registerd
}
}
struct DetailsView: View {
var body: some View {
ExternalEvents.open
}
}
public struct ExternalEvents {
/// - warning: Don't use it, it's used internally.
public static var open: AnyView?
}
struct contentView: View {
var body: some View {
VStack {
// create a button that opens a new window
Button("open a new window") {
ExternalEvents.open = AnyView(
newWindow(id: 0, WindowName: "I am a new window!" )
)
guard let url = URL(string: "your-url://newWindow") else { return }
NSWorkspace.shared.open(url)
}
}
}
}
struct newWindow: View {
var id: Int
var WindowName: String
var body: some View{
VStack{
Text(WindowName + String(id))
}
}
}
I'm not sure if this is the best way to pass variables to a new window, but it does the job quite convincingly.
I'm happy about any solution approaches and ideas.

UIButton in SwiftUI Catalyst Mac app doesn't work when clicked second time

Here's the code I have:
private struct ShareButton: UIViewRepresentable {
func makeUIView(context: Context) -> UIButton {
let activityViewController = UIActivityViewController(activityItems: [URL(string: "https://www.apple.com/")!], applicationActivities: nil)
let action = UIAction(title: "Share") { _ in UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController?.present(activityViewController, animated: false) }
let button = UIButton(primaryAction: action)
activityViewController.popoverPresentationController?.sourceView = button
return button
}
func updateUIView(_ uiView: UIButton, context: Context) { }
}
Basically it's creating a UIButton with a UIAction, inside which there's a UIActivityViewController that set sourceView for the share menu to be the UIButton.
Here's a demo of the issue:
The UIButton is created when the SwiftUI view is created, and set as the sourceView. My guess is that the issue occur because the UIButton is somehow destroyed and recreated due to some SwiftUI mechanism? I can be entirely wrong though. Anyway to solve this?
Or any other way to do share button in a SwiftUI Catalyst Mac app?
"Or any other way to do share button in a SwiftUI Catalyst Mac app?"
You could try this approach, using the extension from:
How to get rid of message " 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead" with AdMob banner?
public extension UIApplication {
func currentUIWindow() -> UIWindow? {
let connectedScenes = UIApplication.shared.connectedScenes
.filter({
$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
let window = connectedScenes.first?
.windows
.first { $0.isKeyWindow }
return window
}
}
struct ContentView: View {
let holaTxt = "Hola 😀 "
var body: some View {
Button(action: {
let AV = UIActivityViewController(activityItems: [holaTxt], applicationActivities: nil)
UIApplication.shared.currentUIWindow()?.rootViewController?.present(AV, animated: true, completion: nil)
}) {
Text("Share")
}
}
}
Found an elegant solution for creating a share button in a SwiftUI Catalyst Mac app (in fact, all SwiftUI app), see https://github.com/SwiftUI-Plus/ActivityView

How to reload fetch data from coredata in Intent Handler when host App close?

I'm currently developing an application using SwiftUI.
I want to reload data from CoreData in IntentHandler to update newer data every when I close a host App.
In the case of Widget, we can use WidgetCenter.shared.reloadAllTimelines() to update, but how can we update data in the case of IntentHandler?
In my codes, data in a list of editing view in the widget receive data only when the host app boots for the first time.
Here are the codes:
TimerApp.swift (Host APP)
import SwiftUI
import WidgetKit
#main
struct TimerApp: App {
#Environment(\.scenePhase) private var scenePhase
let persistenceController = PersistenceController.shared.managedObjectContext
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController)
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .inactive {
// I want to update a data in IntentHandler here
}
}
}
}
}
IntentHandler.swift (IntentsExtension)
import WidgetKit
import SwiftUI
import CoreData
import Intents
class IntentHandler: INExtension, ConfigurationIntentHandling {
var moc = PersistenceController.shared.managedObjectContext
var timerEntity:TimerEntity?
func provideNameOptionsCollection(for intent: ConfigurationIntent, searchTerm: String?, with completion: #escaping (INObjectCollection<NSString>?, Error?) -> Void) {
let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
do{
let result = try moc.fetch(request)
timerEntity = result.first
}
catch let error as NSError{
print("Could not fetch.\(error.userInfo)")
}
let nameIdentifiers:[NSString] = [
NSString(string: timerEntity?.task ?? ""),
]
let allNameIdentifiers = INObjectCollection(items: nameIdentifiers)
completion(allNameIdentifiers,nil)
}
override func handler(for intent: INIntent) -> Any {
return self
}
}
Xcode: Version 12.0.1
iOS: 14.0
Life Cycle: SwiftUI App

Problem: How to call a SwiftUI View Struct Method from SceneDelegate

Currently I'm the one not getting it.
Problem:
Trying to connect to a View Method from a Scene Delegate e.g.:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
...
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
// guard let contentView = ???? as? ContentView else { return }
// contentView.callMethod(parameter: true)
}
}
struct ContentView: View {
var body: some View {
...
}
func callMethode(parameter: Bool) {
print("called")
}
}
Any clue how to connect to the View and call a method in-between?
thx
Jo
This is counter to the design of the SwiftUI framework. You should not have any persistent view around to call methods on. Instead, views are created and displayed as needed in response to your app's state changing.
For example, if your SceneDelegate had a reference to an instance of a model class, and your view depended on that model, you could modify the model in scene(_:continue:) and your view would update automatically.
If you can provide more specifics on what you're attempting to do, I may be able to give a more specific answer.
Many thanks. Fully understood... Will read the docs...
So is this a possible way as an example:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let activity = UserActivityManager()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(activity))
self.window = window
window.makeKeyAndVisible()
if let userActvity = connectionOptions.userActivities.first {
guard let title = userActvity.title else { return }
activity.doAction(action: title)
}
}
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard let title = userActivity.title else { return }
activity.doAction(action: title)
}
}

How can I launch a SwiftUI View without Navigation back tracking?

I want to launch a View as a standalone View without the navigation hierarchy. The reason that I don't want to use a NavigationButton is that I don't want the user to return to the calling form.
I have tried the following approach that is similar to how the first view is launched in ScenceDelegate but nothing happens:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let window = appDelegate.getNewWindow()
window.rootViewController = UIHostingController(rootView: NewView())
window.makeKeyAndVisible()
I have a legitimate reason not to use the navigation UI, I'm leaving the explanation out to keep this short. I'm avoiding Storyboards to keep this as a simple as possible.
Thank you for any solution suggestions.
This is how I accomplished loading a new root View.
I added the following to the AppDelegate code
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
...
func loadNewRootSwiftUIView(rootViewController: UIViewController)
{
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = rootViewController
self.window = window
window.makeKeyAndVisible()
}
}
And placed the following in my form:
struct LaunchView : View {
var body: some View {
VStack {
Button(
action: {
LaunchLoginView()
},
label: {
Text("Login")
}
)
}
}
}
func LaunchLoginView(){
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let vContoller = UIHostingController(rootView: LoginView())
appDelegate.loadNewRootSwiftUIView(rootViewController: vContoller)
}