UIScene userActivity is nil - ios

I have a (Catalyst) app, that is able to have several windows. I created a button and if the user presses this button, the app creates a new window. To know, what View needs to be shown, I have different NSUserActivity activityTypes. In the new window there should be a button, that can close this new window. The problem is, that on looping through the open sessions, all the userActivities are nil (and I need the correct UISceneSession for the UIApplication.shared.requestSceneSessionDestruction.
This is my code:
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")
}
}
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
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()
}
}
...
struct Window1: View {
var body: some View {
VStack {
Text("Window1")
Button("show") {
for session in UIApplication.shared.openSessions {
// this prints two times nil !
print(session.scene?.userActivity?.activityType)
}
}
}
}
}
struct Window2: View {
var body: some View {
Text("Window2")
}
}

I ran into the same issue. I ended up assigning unique delegate classes to each scene and checking that instead.

Related

Forcing orientation between screens failing on iOS 16

Goal
Trying to set up my app so that some screens are forced into landscape mode and some portrait mode. When those screens become visible, (after appearance / re-appearance), the screen orients to the proper orientation.
Problem
In the below example, the screen auto-orients to the first screen correctly, and when the next sheet is presented, correctly updates. However, after dismissing the sheets, the app fails to re-orient the screen back to landscape mode. Giving the error:
Error Domain=UISceneErrorDomain Code=101 "None of the requested orientations are supported by the view controller. Requested: landscapeLeft, landscapeRight; Supported: portrait" UserInfo={NSLocalizedDescription=None of the requested orientations are supported by the view controller. Requested: landscapeLeft, landscapeRight; Supported: portrait}
This is unexpected, because the View.setOrientation() method should set the supported orientations to .all before attempting to re-orient the screen.
Code
import SwiftUI
#main
struct TestApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup("WindowGroup") {
VStack() {
LandscapeView()
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate,ObservableObject {
static var orientationLock = UIInterfaceOrientationMask.all
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
print("Application supportedInterfaceOrientations Ran providing: \(AppDelegate.orientationLock)")
return AppDelegate.orientationLock
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
if connectingSceneSession.role == .windowApplication {
configuration.delegateClass = SceneDelegate.self
}
return configuration
}
}
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
var window: UIWindow? // << contract of `UIWindowSceneDelegate`
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
self.window = windowScene.keyWindow // << store !!!
}
}
struct LandscapeView: View {
#EnvironmentObject var sceneDelegate: SceneDelegate
#State private var portraitSheetPresented = false
#State var isVisible:Bool = false
var body: some View {
HStack() {
Button("Forced Landscape") {
portraitSheetPresented = true
}
.sheet(isPresented: $portraitSheetPresented) {
PortraitModal(parentViewVisible: $isVisible)
}
}
.frame(maxWidth:.infinity,maxHeight:.infinity)
.onAppear() {
isVisible = true
}
.onChange(of: isVisible) { isVisible in
if isVisible {
print("is visibile again")
setOrientation(sceneDelegate, .landscape)
}
}
}
}
struct PortraitModal: View {
#Environment(\.dismiss) private var dismiss
#Binding var parentViewVisible: Bool
#State var isVisible:Bool = false
#EnvironmentObject var sceneDelegate: SceneDelegate
var body: some View {
VStack() {
Text("Forced Portrait")
.onTapGesture {
dismiss()
}
}
.onAppear() {
$parentViewVisible.wrappedValue = false
isVisible = true
}
.onChange(of: isVisible) { isVisible in
if isVisible {
setOrientation(sceneDelegate, .portrait)
}
}
.onDisappear() {
$parentViewVisible.wrappedValue = true
}
.frame(maxWidth:.infinity,maxHeight:.infinity)
}
}
extension View {
func setOrientation(_ sceneDelegate:SceneDelegate, _ orientation:UIInterfaceOrientationMask) {
// set supported orientations to the new orientation
AppDelegate.orientationLock = orientation
// get Window Scene
guard let windowScene = sceneDelegate.window?.windowScene else {return}
//force view into orientation
if #available(iOS 16.0, *) {
windowScene.requestGeometryUpdate(UIWindowScene.GeometryPreferences.iOS(interfaceOrientations: orientation)) { error in
print(error)
}
} else {
// Fallback to some earlier versions
}
}
}

Widget link works but does nothing

I have a widget with a link. I'm implementing the widget URL in SceneDelegate. The problem is when I tap on the widget I get a link, it works. But when I call View Controller function in SceneDelegate, it does nothing. Well, it can print something, but it doesn't change anything. SceneDelegate:
// App launched
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = ViewController()
self.window = window
window.makeKeyAndVisible()
getURL(urlContext: connectionOptions.urlContexts)
}
// App opened from background
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
getURL(urlContext: URLContexts)
}
// Get URL
private func getURL(urlContext: Set<UIOpenURLContext>) {
guard let urlContexts = urlContext.first(where: { $0.url.scheme == "app-deeplink" }) else { return }
print(urlContexts.url)
print("success")
let viewController = ViewController()
viewController.cameFromWidget()
}
Function in ViewController that changes labels text:
func cameFromWidget() {
label.text = "Hello"
print(label.text)
}
WidgetExtension:
var body: some View {
Text(entry.date, style: .time)
.widgetURL(URL(string: "app-deeplink://app"))
}
So ViewController func just prints the text but doesn't change it when I call it from SceneDelegate.
My widget link is only in WidgetExtension and SceneDelegate, I didn't add it to infoPlist.
My question: Why does it work but does nothing? Maybe I should add it to some file?
Thank you so much!
I found out. To make some changes in your viewController you need to make it with rootViewController:
private func getURL(urlContext: Set<UIOpenURLContext>) {
guard let urlContexts = urlContext.first(where: { $0.url.scheme == "app-deeplink" }) else { return }
print(urlContexts.url)
print("success")
let rootViewController = window?.rootViewController as? ViewController
rootViewController?.cameFromWidget()
}

How to auto-move/auto-adjust the save button for a note-taking app in swift?

This is what the app looks like when first opened:
I am content with this as the first screen the user sees.
The problem arises when the user hits the plus button to create a new note. The user touches the screen to add a new note and the keyboard covers up the save button at the bottom right corner. Is there any way to to have button appear above the keyboard on the top right side?
This is what it looks like:
I simply want the plus button above the keyboard on the right side so that the user can save the note. How would this be done?
Please help. Thank you!
Here is the entire code:
AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
SceneDelegate.swift:
import UIKit
import SwiftUI
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.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = Host(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
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 necessarily 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.
}
}
ContentView.swift:
import SwiftUI
import Firebase
struct ContentView: View {
var body: some View {
Home()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Home : View {
#ObservedObject var Notes = getNotes()
#State var show = false
#State var txt = ""
#State var docID = ""
#State var remove = false
var body : some View{
ZStack(alignment: .bottomTrailing) {
VStack(spacing: 0){
HStack{
Text("Notes").font(.title).foregroundColor(.white)
Spacer()
Button(action: {
self.remove.toggle()
}) {
Image(systemName: self.remove ? "xmark.circle" : "trash").resizable().frame(width: 23, height: 23).foregroundColor(.white)
}
}.padding()
.padding(.top,UIApplication.shared.windows.first?.safeAreaInsets.top)
.background(Color.red)
if self.Notes.data.isEmpty{
if self.Notes.noData{
Spacer()
Text("No Notes...")
Spacer()
}
else{
Spacer()
//Data is Loading ....
Indicator()
Spacer()
}
}
else{
ScrollView(.vertical, showsIndicators: false) {
VStack{
ForEach(self.Notes.data){i in
HStack(spacing: 15){
Button(action: {
self.docID = i.id
self.txt = i.note
self.show.toggle()
}) {
VStack(alignment: .leading, spacing: 12){
Text(i.date)
Text(i.note).lineLimit(1)
Divider()
}.padding(10)
.foregroundColor(.black)
}
if self.remove{
Button(action: {
let db = Firestore.firestore()
db.collection("notes").document(i.id).delete()
}) {
Image(systemName: "minus.circle.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.red)
}
}
}.padding(.horizontal)
}
}
}
}
}.edgesIgnoringSafeArea(.top)
Button(action: {
self.txt = ""
self.docID = ""
self.show.toggle()
}) {
Image(systemName: "plus").resizable().frame(width: 18, height: 18).foregroundColor(.white)
}.padding()
.background(Color.red)
.clipShape(Circle())
.padding()
}
.sheet(isPresented: self.$show) {
EditView(txt: self.$txt, docID: self.$docID, show: self.$show)
}
.animation(.default)
}
}
class Host : UIHostingController<ContentView>{
override var preferredStatusBarStyle: UIStatusBarStyle{
return .lightContent
}
}
class getNotes : ObservableObject{
#Published var data = [Note]()
#Published var noData = false
init() {
let db = Firestore.firestore()
db.collection("notes").order(by: "date", descending: false).addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
self.noData = true
return
}
if (snap?.documentChanges.isEmpty)!{
self.noData = true
return
}
for i in snap!.documentChanges{
if i.type == .added{
let id = i.document.documentID
let notes = i.document.get("notes") as! String
let date = i.document.get("date") as! Timestamp
let format = DateFormatter()
format.dateFormat = "MM/YY"
let dateString = format.string(from: date.dateValue())
self.data.append(Note(id: id, note: notes, date: dateString))
}
if i.type == .modified{
// when data is changed...
let id = i.document.documentID
let notes = i.document.get("notes") as! String
for i in 0..<self.data.count{
if self.data[i].id == id{
self.data[i].note = notes
}
}
}
if i.type == .removed{
// when data is removed...
let id = i.document.documentID
for i in 0..<self.data.count{
if self.data[i].id == id{
self.data.remove(at: i)
if self.data.isEmpty{
self.noData = true
}
return
}
}
}
}
}
}
}
struct Note : Identifiable {
var id : String
var note : String
var date : String
}
struct Indicator : UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<Indicator>) -> UIActivityIndicatorView {
let view = UIActivityIndicatorView(style: .medium)
view.startAnimating()
return view
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<Indicator>) {
}
}
struct EditView : View {
#Binding var txt : String
#Binding var docID : String
#Binding var show : Bool
var body : some View{
ZStack(alignment: .bottomTrailing) {
MultiLineTF(txt: self.$txt)
.padding()
.background(Color.black.opacity(0.05))
Button(action: {
self.show.toggle()
self.SaveData()
}) {
Text("Save").padding(.vertical).padding(.horizontal,25).foregroundColor(.white)
}.background(Color.red)
.clipShape(Capsule())
.padding()
}.edgesIgnoringSafeArea(.bottom)
}
func SaveData(){
let db = Firestore.firestore()
if self.docID != ""{
db.collection("notes").document(self.docID).updateData(["notes":self.txt]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
}
}
else{
db.collection("notes").document().setData(["notes":self.txt,"date":Date()]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
}
}
}
}
struct MultiLineTF : UIViewRepresentable {
func makeCoordinator() -> MultiLineTF.Coordinator {
return MultiLineTF.Coordinator(parent1: self)
}
#Binding var txt : String
func makeUIView(context: UIViewRepresentableContext<MultiLineTF>) -> UITextView{
let view = UITextView()
if self.txt != ""{
view.text = self.txt
view.textColor = .black
}
else{
view.text = "Type Something"
view.textColor = .gray
}
view.font = .systemFont(ofSize: 18)
view.isEditable = true
view.backgroundColor = .clear
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<MultiLineTF>) {
}
class Coordinator : NSObject,UITextViewDelegate{
var parent : MultiLineTF
init(parent1 : MultiLineTF) {
parent = parent1
}
func textViewDidBeginEditing(_ textView: UITextView) {
if self.parent.txt == ""{
textView.text = ""
textView.textColor = .black
}
}
func textViewDidChange(_ textView: UITextView) {
self.parent.txt = textView.text
}
}
}
Remove your .edgesIgnoringSafeArea(.bottom) attached to the ZStack.
The safe area includes the area taken up by the keyboard. Right now, because you've explicitly told it to ignore the safe area, it's not moving to accommodate the keyboard. If it respects the safe area, the button will move with the keyboard.

Hide Title Bar in SwiftUI App for macCatalyst

How can I hide the Title Bar in the new SwiftUI App Protocol?
Since the AppDelegate.swift and SceneDelegate.swift protocols are gone, I cant follow this documentation anymore:
https://developer.apple.com/documentation/uikit/mac_catalyst/removing_the_title_bar_in_your_mac_app_built_with_mac_catalyst
I can't implement this code:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
#if targetEnvironment(macCatalyst)
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
#endif
}
}
Hope it's still possible with the new AppProtocol..
Thanks in advance
This is how to hide the titlebar:
struct ContentView: View {
var body: some View {
ZStack {
Text("Example UI")
}
.withHostingWindow { window in
#if targetEnvironment(macCatalyst)
if let titlebar = window?.windowScene?.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
#endif
}
}
}
extension View {
fileprivate func withHostingWindow(_ callback: #escaping (UIWindow?) -> Void) -> some View {
self.background(HostingWindowFinder(callback: callback))
}
}
fileprivate struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
On your Scene, set .windowStyle(_:) to HiddenTitleBarWindowStyle().
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowStyle(HiddenTitleBarWindowStyle())
}
}
EDIT: Ah crap. While this API is supposedly available for Mac Catalyst according to the online documentation, it looks like it’s not actually marked as such in frameworks so you can’t use it.

How to push a new root view using SwiftUI without NavigationLink?

I have a login screen. After user fill up credential, I want to verify it then start a new root view so user won't be able to navigate back to the login view.
I currently have
Button(action: {
// launch new root view here
}, label: {Text("Login")}).padding()
Majority of answers I found online are using navigation link which I don't want to. Some other answers suggest to utilize AppDelegate by UIApplication.shared.delegate which isn't working for me because I have SceneDelegate
Here is possible alternate approach of how to replace root view completely... using notifications
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let loginRootViewNotification =
NSNotification.Name("loginRootViewNotification") // declare notification
private var observer: Any? // ... and observer
...
// in place of window creation ...
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
observer = NotificationCenter.default.addObserver(forName: loginRootViewNotification, object: nil, queue: nil, using: { _ in
let anotherRootView = AnotherRootView()
// create another view on notification and replace
window.rootViewController = UIHostingController(rootView: anotherRootView)
})
in your desired place post needed notification
Button(action: {
// launch new root view here
NotificationCenter.default.post(loginRootViewNotification)
}, label: {Text("Login")}).padding()
Another approach with Notifications is to place the observer inside your RootView and check for changes in a variable to decide which View should be presented, here is a very simplified example:
struct RootView: View {
#State var isLoggedIn: Bool = false
var body: some View {
Group {
VStack{
if isLoggedIn {
Text("View B")
Button(action: {
NotificationCenter.default.post(name: NSNotification.Name("changeLogin"), object: nil)
}) {
Text("Logout")
}
} else {
Text("View A")
Button(action: {
NotificationCenter.default.post(name: NSNotification.Name("changeLogin"), object: nil)
}) {
Text("Login")
}
}
}
}.onAppear {
NotificationCenter.default.addObserver(forName: NSNotification.Name("changeLogin"), object: nil, queue: .main) { (_) in
self.isLoggedIn.toggle()
}
}
}
And load RootView in your rootViewController as usual.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = RootView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}

Resources