ScenePhase Change Not Firing - swift

For some reason, I'm not getting the .onChange(scenePhase) firing. No matter which View I place the modifier on there's no output:
import SwiftUI
struct TabViewDashboard: View {
#Environment(\.scenePhase) var scenePhase
var body: some View {
ZStack {
VStack {
//more code here....
}
.onChange(of: scenePhase) { newPhase in
debugPrint("NewPhase: ", newPhase as Any)
}
.onAppear {
// code here
}
.onDisappear {
// function here
}
}
}
I'm not getting any debug output for any scene changes.
I know this View is being called from a UIKit->SwiftUI UIHostingController. Could this be the reason I'm getting no output?
Min. OS: iOS 14.

Related

Changing scroll position from another view not working

I am working on a project where I need to reset a TabView's root view controllers (NavigationViews with Lists inside) when the TabViews selected item changes. This is pretty simple in UIKit, however in SwiftUI it doesn't seem that easy.
Let's say I have the following code:
class AppState: ObservableObject {
let objectWillChange = PassthroughSubject<AppState, Never>()
#Published var theScrollPosition: Int64? {
didSet {
print("Did set scroll position")
objectWillChange.send(self)
}
}
#Published var selectedTab: Tabs
{
didSet {
print("Tab switched, switching back to root view")
selectedItemID = nil
selectedRow = nil
theScrollPosition = -1
}
}
#Published var selectedItemID: Int64? {
didSet {
objectWillChange.send(self)
}
}
#Published var selectedRow: Int64? {
didSet {
objectWillChange.send(self)
}
}
}
struct ContentView: View {
#EnvironmentObject var state: AppState
var body: some View {
TabView {
View1().id(Tabs.Tab1)
View2().id(Tabs.Tab2)
View3().id(Tabs.Tab3)
}
}
}
struct View1: View {
#EnvironmentObject var state: AppState
#ObservedObject var viewModel: ListViewModel()
var body: some View {
ScrollViewReader { proxy in
List(viewModel.items) { item in
Section {
NavigationLink(destination:ListItemDetailView(item), tag: item.id, selection: state.selectedItemID) {
ListItemView(item)
}
}
}.onChange(of: self.state.selectedItemID) { newValue in
print("Scrolling to top")
proxy.scrollTo(0)
}
}
}
There may be some typos in the code as this is not the actual production code however the flow is this:
Selection state of TabView and NavigationLink are stored in a global EnvironmentObject. When the TabView selection changes, View1 should scroll back up to the top.
However, the onChange method is never called.
Next time, provide something runnable or at least a start...
But here is a example where view 1 will always scroll to the top, you are missing something like onAppear()
And you don't need to have AppState.theScrollPosition as a published, instead change to the right tab and have that view read the position or tag in the model.
import SwiftUI
struct ContentView: View {
var view: some View {
ScrollViewReader { proxy in
ScrollView {
VStack {
Text("TOP").id("topID")
Divider()
Spacer()
.frame(height:1000)
Text("BOTTOM").id("bottomID")
}
}
.onAppear {
proxy.scrollTo("topID")
}
}
}
var view2: some View {
VStack {
Text("View 2")
}
}
var body: some View {
TabView {
view.tag(0)
.tabItem {
Text("View 1")
}
view2.tag(1)
.tabItem {
Text("View 2")
}
}
}
}
Thanks, for some reason I was totally missing onAppear.

NavigationLink on macOS causing source view to appear twice

Overview:
On macOS when using NavigationLink, the source view is appearing twice.
It only happens on macOS (see print statements)
Questions:
How to resolve this?
Is there a workaround?
Code:
import SwiftUI
struct ContentView: View {
let names = ["aaa"]
var body: some View {
NavigationView {
List {
ForEach(names, id: \.self) { name in
NavigationLink(destination: Text("dest")) {
Text(name)
.onAppear {
print("\(name) appeared")
}
}
}
}
}
}
}
To resolve this, simply "attach" the .onAppear to your NavigationLink not the Text.
EDIT-1:
This is the test code I'm using to show that moving the .onAppear to the
NavigationLink as shown, prints only one on appear.
I'm using macos 12, Xcode 13.2, tested on iMac with macos 12.2.
import SwiftUI
#main
struct MacOnlyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
let names = ["aaa"]
var body: some View {
NavigationView {
List {
ForEach(names, id: \.self) { name in
NavigationLink(destination: Text("dest")) {
Text(name)
}
.onAppear {
print("----> \(name) appeared \n")
}
}
}
}
}
}

WatchOS Using ObservableObject in Conditional in View Causing Runtime Error

I use an ObservableObject to keep the state of whether a user is subscribed to my app or not, and based on the subscription status, show different views. This worked fine prior to Xcode 13 and WatchOS 8, but now this is causing a runtime error of runtime: SwiftUI: Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update. And, the binding does not update per the error. This occurs on both Xcode 13.1 and 13.2b2
This code below reproduces the error:
struct MultiPageView: View {
#ObservedObject var subscribed = SubscribedModel.shared
var body: some View {
if subscribed.value {
TabView {
ViewOne()
ViewTwo()
ViewThree()
ToggleView()
}
.tabViewStyle(PageTabViewStyle())
} else {
TabView {
ViewOne()
ToggleView()
}
.tabViewStyle(PageTabViewStyle())
}
}
}
struct ToggleView: View {
#ObservedObject var subscribed = SubscribedModel()
var body: some View {
Toggle(isOn: $subscribed.value) {
Text("Subscribed")
}
}
}
class SubscribedModel: ObservableObject {
public static let shared = SubscribedModel.shared
#Published var value: Bool = false
}
I am only listing ViewOne for brevity, but ViewTwo and ViewThree are the same with different text:
struct ViewOne: View {
var body: some View {
Text("View One")
.padding()
}
}
If you navigate to the ToggleView(), and switch the toggle, the error pops immediately. Any suggestions to fix this?
Update per #LoremIpsum comment:
struct MultiPageView: View {
#StateObject var subscribed = SubscribedModel()
var body: some View {
if subscribed.value {
TabView {
ViewOne()
ViewTwo()
ViewThree()
ToggleView(subscribed: $subscribed.value)
}
.tabViewStyle(PageTabViewStyle())
} else {
TabView {
ViewOne()
ToggleView(subscribed: $subscribed.value)
}
.tabViewStyle(PageTabViewStyle())
}
}
}
struct ToggleView: View {
#Binding var subscribed: Bool
var body: some View {
Toggle(isOn: $subscribed) {
Text("Subscribed")
}
}
}
It is now switching between the TabViews, but the error still remains, and is showing up immediately. Deleted DerivedData and cleaned build folder. Any thoughts?
I will add that this same basic code is running fine on iOS 15. It is just WatchOS that is popping the error.
I was having the same issue for a long time, and this is still happening on Xcode 13.2.1.
Seems to be an issue with TabView on watchOS, because if you replace the TabView for another View the error is gone.
The solution is to use the initialiser for TabView with a selection value: init(selection:content:)
1 Define a property for selection
#State private var selection = 0
2 Update TabView
From
TabView {
// content
}
To
TabView(selection: $selection) {
// content
}
Updating your code would look like this:
struct MultiPageView: View {
#StateObject var subscribed = SubscribedModel()
#State private var selection = 0
var body: some View {
if subscribed.value {
TabView(selection: $selection) {
ViewOne()
ViewTwo()
ViewThree()
ToggleView(subscribed: $subscribed.value)
}
.tabViewStyle(PageTabViewStyle())
} else {
TabView(selection: $selection) {
ViewOne()
ToggleView(subscribed: $subscribed.value)
}
.tabViewStyle(PageTabViewStyle())
}
}
}
Basically just defining a #State property for TabView.selection, and using it on both your TabViews (using separated properties would also work).

OnAppear and OnDisappear are not triggered on first view transition

I have a watchOS App which uses the following layout:
NavigationView {
if !HKHealthStore.isHealthDataAvailable() {
ContentNeedHealthKitView()
} else if !isAuthorized {
ContentUnauthorizedView()
} else {
TabView(selection: $selection) {
WeightView()
.navigationTitle("Weight")
.tag(1)
.onAppear {
print("Appear!")
}
.onDisappear {
print("Disappear!")
}
SettingsView()
.navigationTitle("Settings")
.tag(2)
}
}
}
Unfortunately, the OnAppear and OnDisappear actions are only executed after transitioning from on view to another the second time. When swiping right the first time, nothing happens.
You should provide a minimal reproducible example (see https://stackoverflow.com/help/minimal-reproducible-example).
Also your lines are producing compiler errors. The correct way to use onAppear is like this:
.onAppear {
}
Here is a working example, everything is working as expected. You should also place the onAppear ViewModifier to the child view.
import SwiftUI
struct WeightView: View {
var body: some View {
Text("WeightView")
.onAppear {
print("Appear!")
}
.onDisappear {
print("Disappear!")
}
}
}
struct SettingsView: View {
var body: some View {
Text("SettingsView")
}
}
struct ContentView: View {
#State var selection = 1
#State var isAuthorized = false
var body: some View {
NavigationView {
if !isAuthorized {
Button("authorize") {
isAuthorized.toggle()
}
} else {
TabView(selection: $selection) {
WeightView()
.navigationTitle("Weight")
.tag(1)
SettingsView()
.navigationTitle("Settings")
.tag(2)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to transition Views programmatically using SwiftUI?

I want to show the user another view when the login is successful, otherwise stay on that view. I've done that with UIKit by performing a segue. Is there such an alternative in SwiftUI?
The NavigationButton solution does not work as I need to validate the user input before transitioning to the other view.
Button(action: {
let authService = AuthorizationService()
let result = authService.isAuthorized(username: self.username, password: self.password)
if(result == true) {
print("Login successful.")
// TODO: ADD LOGIC
*** HERE I WANT TO PERFORM THE SEGUE ***
presentation(MainView)
} else {
print("Login failed.")
}
}) {
Text("Login")
}
Xcode 11 beta 5.
NavigationDestinationLink and NavigationButton have been deprecated and replaced by NavigationLink.
Here's a full working example of programatically pushing a view to a NavigationView.
import SwiftUI
import Combine
enum MyAppPage {
case Menu
case SecondPage
}
final class MyAppEnvironmentData: ObservableObject {
#Published var currentPage : MyAppPage? = .Menu
}
struct NavigationTest: View {
var body: some View {
NavigationView {
PageOne()
}
}
}
struct PageOne: View {
#EnvironmentObject var env : MyAppEnvironmentData
var body: some View {
let navlink = NavigationLink(destination: PageTwo(),
tag: .SecondPage,
selection: $env.currentPage,
label: { EmptyView() })
return VStack {
Text("Page One").font(.largeTitle).padding()
navlink
.frame(width:0, height:0)
Button("Button") {
self.env.currentPage = .SecondPage
}
.padding()
.border(Color.primary)
}
}
}
struct PageTwo: View {
#EnvironmentObject var env : MyAppEnvironmentData
var body: some View {
VStack {
Text("Page Two").font(.largeTitle).padding()
Text("Go Back")
.padding()
.border(Color.primary)
.onTapGesture {
self.env.currentPage = .Menu
}
}.navigationBarBackButtonHidden(true)
}
}
#if DEBUG
struct NavigationTest_Previews: PreviewProvider {
static var previews: some View {
NavigationTest().environmentObject(MyAppEnvironmentData())
}
}
#endif
Note that the NavigationLink entity has to be present inside the View body.
If you have a button that triggers the link, you'll use the label of the NavigationLink.
In this case, the NavigationLink is hidden by setting its frame to 0,0, which is kind of a hack but I'm not aware of a better method at this point. .hidden() doesn't have the same effect.
You could do it like bellow, based on this response (it's packed like a Playground for easy testing:
import SwiftUI
import Combine
import PlaygroundSupport
struct ContentView: View {
var body: some View {
NavigationView {
MainView().navigationBarTitle(Text("Main View"))
}
}
}
struct MainView: View {
let afterLoginView = DynamicNavigationDestinationLink(id: \String.self) { message in
AfterLoginView(msg: message)
}
var body: some View {
Button(action: {
print("Do the login logic here")
self.afterLoginView.presentedData?.value = "Login successful"
}) {
Text("Login")
}
}
}
struct AfterLoginView: View {
let msg: String
var body: some View {
Text(msg)
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
Although this will work, I think that, from an architectural perspective, you try to push an "imperative programming" paradigm into SwiftUI's reactive logic.
I mean, I would rather implement it with the login logic wrapped into an ObjectBinding class with an exposed isLoggedin property and make the UI react to the current state (represented by isLoggedin).
Here's a very high level example :
struct MainView: View {
#ObjectBinding private var loginManager = LoginManager()
var body: some View {
if loginManager.isLoggedin {
Text("After login content")
} else {
Button(action: {
self.loginManager.login()
}) {
Text("Login")
}
}
}
}
I used a Bool state for my login transition, it seems pretty fluid.
struct ContentView: View {
#State var loggedIn = false
var body: some View {
VStack{
if self.loggedIn {
Text("LoggedIn")
Button(action: {
self.loggedIn = false
}) {
Text("Log out")
}
} else {
LoginPage(loggedIn: $loggedIn)
}
}
}
}