How can I use lifecycle methods when a particular view is selected? - swiftui

I'm currently developing an application using SwiftUI.
This app has 2 Views controlled a Tab View.
I want to use these methods sceneDidBecomeActive and sceneWillEnterForeground in SceneDelegate.swift only when a particular view is selected.
These methods work irrespective of which view is selected.
How can I do this request?
SceneDelegate.swift
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
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)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
}
func sceneDidBecomeActive(_ scene: UIScene) {
// I want use this print method only when FirstView is selected
print("selected FirstVIew")
}
func sceneWillResignActive(_ scene: UIScene) {
}
func sceneWillEnterForeground(_ scene: UIScene) {
// I want use this print method only when FirstView is selected
print("selected FirstVIew")
}
func sceneDidEnterBackground(_ scene: UIScene) {
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
FirstView()
.tabItem {
Text("First")
}.tag(1)
SecondView()
.tabItem {
Text("Second")
}.tag(2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
FirstView.swift
import SwiftUI
struct FirstView: View {
var body: some View {
Text("FirstView")
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
import SwiftUI
struct SecondView: View {
var body: some View {
Text("SecondView")
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
Xcode: Version 11.7
Swift: Swift 5

SceneDelegate methods deal with App's life cycle, not a view's. Therefore you cannot "run" them when a view is selected.
What you can do though is use UserDefaults.
// When first view selected
UserDefaults.standard.set("First View", forKey: "selectedView")
// In SceneDelegate
func sceneDidBecomeActive(_ scene: UIScene) {
if let selected = UserDefaults.standard.string(forKey: "selectedView"),
selected == "First View" {
print("selected FirstVIew")
}
}
func sceneWillEnterForeground(_ scene: UIScene) {
if let selected = UserDefaults.standard.string(forKey: "selectedView"),
selected == "First View" {
print("selected FirstVIew")
}
}

Related

Requesting access with AVCaptureDevice causes a switch in selected tab in tab view

I am trying to add a QR scanner to my app that has a Tab View at the root. It sort of works except for one thing; When the dialog appears to ask the user for permission to use the camera, it also automatically switches the selected tab from the second tab to the first one that appears in the code. If I switch places in the code between CameraView and AnyView it all works as expected, but I don't want the tab with the CameraView to be the first tab.
I've reproduced this in the following example:
ContentView:
import SwiftUI
enum Tab: Int {
case first = 1, second
}
struct ContentView: View {
#SceneStorage("selectedTab") var selectedTab: Int?
init() {
selectedTab = Tab.first.rawValue
}
var body: some View {
TabView {
TabView(selection: $selectedTab) {
AnyView()
.tabItem {
Label("Calendar", systemImage: "calendar")
}
.tag(Tab.first)
CameraView()
.tabItem {
Label("Camera", systemImage: "person.fill")
}
.tag(Tab.second)
}
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
CameraView:
import SwiftUI
import UIKit
import AVFoundation
struct CameraView: UIViewRepresentable {
typealias UIViewType = CameraPreview
private let session = AVCaptureSession()
private let metadataOutput = AVCaptureMetadataOutput()
func setupCamera(_ uiView: CameraPreview) {
if let backCamera = AVCaptureDevice.default(for: AVMediaType.video) {
if let input = try? AVCaptureDeviceInput(device: backCamera) {
session.sessionPreset = .photo
if session.canAddInput(input) {
session.addInput(input)
}
if session.canAddOutput(metadataOutput) {
session.addOutput(metadataOutput)
}
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
uiView.backgroundColor = UIColor.gray
previewLayer.videoGravity = .resizeAspectFill
uiView.layer.addSublayer(previewLayer)
uiView.previewLayer = previewLayer
session.startRunning()
}
}
}
func makeUIView(context: UIViewRepresentableContext<CameraView>) -> CameraView.UIViewType {
let cameraView = CameraPreview(session: session)
checkCameraAuthorizationStatus(cameraView)
return cameraView
}
static func dismantleUIView(_ uiView: CameraPreview, coordinator: ()) {
uiView.session.stopRunning()
}
private func checkCameraAuthorizationStatus(_ uiView: CameraPreview) {
let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
if cameraAuthorizationStatus == .authorized {
setupCamera(uiView)
} else {
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.sync {
if granted {
self.setupCamera(uiView)
}
}
}
}
}
func updateUIView(_ uiView: CameraPreview, context: UIViewRepresentableContext<CameraView>) {
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
}
CameraPreview:
import UIKit
import AVFoundation
class CameraPreview: UIView {
private var label:UILabel?
var previewLayer: AVCaptureVideoPreviewLayer?
var session = AVCaptureSession()
init(session: AVCaptureSession) {
super.init(frame: .zero)
self.session = session
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
previewLayer?.frame = self.bounds
}
}
Any ideas on why this may be happening or how I can fix this or work around it?
After reading a bit about SceneStorage I started to wonder if that was what was creating the issue. I switched #SceneStorage("selectedTab") var selectedTab: Int? to #State var selectedTab: Int? but that did not work.
After looking at Apple's own example for how to create a tab view I realized that maybe the syntax was off just a little bit.
After switching to:
enum Tab {
case first <-- this is different
case second
}
struct ContentView: View {
#State var selectedTab: Tab = .first <-- this is different
var body: some View {
TabView {
TabView(selection: $selectedTab) {
AnyView()
.tabItem {
Label("Calendar", systemImage: "calendar")
}
.tag(Tab.first)
AddFriend()
.tabItem {
Label("Camera", systemImage: "person.fill")
}
.tag(Tab.second)
}
.padding()
}
}
}
the problem no longer persisted. I guess the way I was initializing the default tab made it behave in unexpected ways.

Popping views using SceneDelegate.toView()

I have a UI where you can navigate/push views like this:
AView -> BView -> CView -> DView
I want to pop a couple of views (get to BView from DView) instead of placing a new view (BView) on top of existing stack and I found the way to do this:
(UIApplication.shared.connectedScenes.first?.delegate as?
SceneDelegate)?.toBView()
What I don't understand is why does it call CView's init() when I try to return from DView to BView?
Here's the output i get in debug console:
AView init
BView init
CView init
DView init
contract.
button pressed
BView init
**CView init**
Why does it call CView's init() and how to avoid this behaviour?
AView.swift:
import SwiftUI
struct AView: View {
init() {
print("AView init")
}
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: BView()) {
Text("This is View A, now go to View B.")
}
}
}
}
}
struct BView: View {
init() {
print("BView init")
}
var body: some View {
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
struct CView: View {
init() {
print("CView init")
}
var body: some View {
NavigationLink(destination: DView()) {
Text("This is View C, now go to View D.")
}
}
}
struct DView: View {
init() {
print("DView init")
}
var body: some View {
Button(action: {
print("button pressed")
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.toBView()
},
label: {
Text("Back!")
})
}
}
SceneDelegate.swift:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let aView = AView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: AView)
self.window = window
window.makeKeyAndVisible()
}
}
func toBView() {
let bView = BView()
window?.rootViewController = UIHostingController(rootView: bView)
}
}
This behavior is not wrong. If you start your app and display AView you will see in the console that it prints init from A and B.
It's because BView() is already initialized in your AView, even though you haven't activated the NavigationLink.
NavigationLink(destination: BView()) {
So this behavior is not specific to your pop back action, happens already at the beginning. See this solution from Asperi regarding multiple init() aswell, if you are concerned about calling init() more than once

NavigationLink is not clickable after returning to the view from it's child view

My app has a multilevel layout
AView -> BView -> CView -> DView.
I change
window.rootViewController
to BView in order to "pop" 2 top views but for some reason when i come back to BView it's NavigationLink is not clickable.
Any ideas on how to fix this? It seems like BView doesn't know that it became visible..
AView.swift:
import SwiftUI
struct AView: View {
init() {
print("AView init")
}
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: BView()) {
Text("This is View A, now go to View B.")
}
}
}
}
}
struct BView: View {
init() {
print("BView init")
}
var body: some View {
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
struct CView: View {
init() {
print("CView init")
}
var body: some View {
NavigationLink(destination: DView()) {
Text("This is View C, now go to View D.")
}
}
}
struct DView: View {
init() {
print("DView init")
}
var body: some View {
Button(action: {
print("button pressed")
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.toBView()
},
label: {
Text("Back!")
})
}
}
SceneDelegate.swift:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let aView = AView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: aView)
self.window = window
window.makeKeyAndVisible()
}
}
func toBView() {
let bView = BView()
window?.rootViewController = UIHostingController(rootView: bView)
}
}
Asperi already stated the problem.. you are setting BView as root level. Actually changing that, shouldn't be a big problem.
But back to your question why does the NavigationLink won't work anymore. Thats because ViewA, which contains the NavigationView is not in the RootLevel anymore. Hence you will need to provide a new NavigationView, but only and only when BView is the root view.
So inside BView, add a parameter isRootView, which you will set only to true when you call it from your SceneDelegate
struct BView: View {
init(isRootView: Bool = false) {
print("BView init")
self.isRootView = isRootView
}
var isRootView : Bool = false
var body: some View {
if isRootView {
NavigationView {
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
else
{
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
}
And here the call from SceneDelegate
func toBView() {
let bView = BView(isRootView: true)
window?.rootViewController = UIHostingController(rootView: bView)
}

EnvironmentVariables not working when passing variable from one view to another in SwiftUI

I have found a few similar examples of how to pass variables among multiple views in SwiftUI:
Hacking with Swift - How to use #EnvironmentObject to share data between views
How to pass variable from one view to another in SwiftUI
I am trying to follow the examples and use EnvironmentVariables and modify the ContentView where it's first defined in the SceneDelegate. However, when trying both examples, I get the error "Compiling failed: 'ContentView_Previews' is not a member type of 'Environment'". I am using Xcode Version 11.3.1.
Following the example given in How to pass variable from one view to another in SwiftUI, here is code contained in ContentView:
class SourceOfTruth: ObservableObject{
#Published var count = 0
}
struct ContentView: View {
#EnvironmentObject var truth: SourceOfTruth
var body: some View {
VStack {
FirstView()
SecondView()
}
}
}
struct FirstView: View {
#EnvironmentObject var truth: SourceOfTruth
var body: some View {
VStack{
Text("\(self.truth.count)")
Button(action:
{self.truth.count = self.truth.count-10})
{
Text("-")
}
}
}
}
struct SecondView: View {
#EnvironmentObject var truth: SourceOfTruth
var body: some View {
Button(action: {self.truth.count = 0}) {
Text("Reset")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(SourceOfTruth())
}
}
... and here is the contents of SceneDelegate:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var truth = SourceOfTruth() // <- Added
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(SourceOfTruth())) // <- Modified
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
}
func sceneDidBecomeActive(_ scene: UIScene) {
}
func sceneWillResignActive(_ scene: UIScene) {
}
func sceneWillEnterForeground(_ scene: UIScene) {
}
func sceneDidEnterBackground(_ scene: UIScene) {
}
}
I does not depend on Xcode version and it is not an issue. You have to set up ContentView in ContentView_Previews in the same way as you did in SceneDelegate, provide .environmentObject, as in below example
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(_Your_object_here())
}
}

Unable to share environment between Views

Environment: Xcode Version 11.0 beta 4 (11M374r)
I'm unable to share the 'environment' with a second view.
I've instantiate the environment BindableObject in the SceneDelegate:
SceneDelegate.swift:
I'm using #EnvironmentObject in both the base (ContentView) and the detail view.
The environment has already been set up in the SceneDelegate so it should be available to all views.
The ContentView does see the environment.
But DetailView blows up:
Here's the complete code:
import Combine
import SwiftUI
struct UserInfo {
var name: String
var message: String
init(name: String, msg: String) {
self.name = name; self.message = msg
}
}
// A BindableObject is always a class; NOT a struct.
class UserSettings: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var userInfo = UserInfo(name: "Ric", msg: "Mother had a feeling, I might be too appealing.") {
didSet {
willChange.send()
}
}
}
// =====================================================================================================
struct DetailView: View {
#Binding var dismissFlag: Bool
#EnvironmentObject var settings: UserSettings // ...<error source>
var body: some View {
VStack {
Spacer()
Button(action: dismiss) {
Text("Dismiss")
.foregroundColor(.white)
}
.padding()
.background(Color.green)
.cornerRadius(10)
.shadow(radius: 10)
Text("Hello")
Spacer()
}
}
private func dismiss() {
settings.userInfo.message = "Rubber baby buggy bumpers."
dismissFlag = false
}
}
// ---------------------------------------------------------------------------
// Base View:
struct ContentView: View {
#State var shown = false
#EnvironmentObject var settings: UserSettings
var body: some View {
VStack {
Spacer()
Button(action: {
self.settings.userInfo.name = "Troglodyte"
self.settings.userInfo.message = "Top Secret"
self.shown.toggle()
}) {
Text("Present")
.foregroundColor(.white)
}.sheet(isPresented: $shown) { DetailView(dismissFlag: self.$shown) }
.padding()
.background(Color.red)
.cornerRadius(10)
.shadow(radius: 10)
Text(self.settings.userInfo.message)
Spacer()
}
}
}
// =====================================================================================================
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
What am I missing?
What am I doing wrong?
Revision per suggestion:
import SwiftUI
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var userSettings = UserSettings()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use a UIHostingController as window root view controller
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(userSettings))
self.window = window
window.makeKeyAndVisible()
}
}
}
His the runtime-error message after modifying the SceneDelegate:
Here's a clue:
In SceneDelegate you need to declare an instance of your variable:
var userSettings = UserSettings() // <<--- ADD THIS
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView()
.environmentObject(userSettings) <<-- CHANGE THIS
)
self.window = window
window.makeKeyAndVisible()
}
}
That creates a global/environment instance of userSettings.
EDIT:
There's a second error happening, related to exposing your #EnvironmentObject to the preview. Per this answer by #MScottWaller, you need to create an separate instance in both SceneDelegate and PreviewProvider.
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView().environmentObject(UserSettings())
}
}
#endif