Is there a way to conditionally use #StateObject while targeting iOS 13? - swiftui

I have a SwiftUI app that is targeting iOS 13 and higher. I would like to conditionally use the #StateObject property wrapper if I am running on iOS 14, as it would fix so many bugs I've had to workaround on iOS 13.
Is there any way to conditionally do this via #ifdef or #if available(iOS 14, *) calls or similar? I'm happy to have a completely separate implementation of a given view for iOS 14.
I've tried this but it of course doesn't compile.
struct MyView: View {
if #available(iOS 14, *) {
#StateObject var viewModel: ViewModel()
} else {
#ObservedObject var viewModel: ViewModel
}

This can be done using the #available attribute like so:
class ViewModel: ObservableObject {}
#available(iOS 14, *)
struct FourteenView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Text("iOS 14")
}
}
struct ThirteenView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Text("iOS 13")
}
}
struct ContentView: View {
var body: some View {
if #available(iOS 14, *) {
AnyView(FourteenView())
} else {
ThirteenView()
}
}
}
This only works in Xcode 12 of course. I'm using 12.0 beta 3 (12A8169g).
The AnyView() in the ContentView is because using if #available(iOS 14, *) in a #ViewBuilder doesn't seem to work properly. See this Apple Developer Forum thread.

Alternatively you can use this StateObject that works on iOS 13:
https://gist.github.com/Amzd/8f0d4d94fcbb6c9548e7cf0c1493eaff

Related

EnvironmentObject causes unrelated ObservedObject to reset

I am not quite sure I understand what is going on here as I am experimenting with an EnvironmentObject in SwiftUI.
I recreated my problem with a small example below, but to summarize: I have a ContentView, ContentViewModel, and a StateController. The ContentView holds a TextField that binds with the ContentViewModel. This works as expected. However, if I update a value in the StateController (which to me should be completely unrelated to the ContentViewModel) the text in the TextField is rest.
Can someone explain to me why this is happening, and how you could update a state on an EnvironmentObject without having SwiftUI redraw unrelated parts?
App.swift
#main
struct EnvironmentTestApp: App {
#ObservedObject var stateController = StateController()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(stateController)
}
}
}
ContentView.swift
struct ContentView: View {
#ObservedObject private var viewModel = ContentViewModel()
#EnvironmentObject private var stateController: StateController
var body: some View {
HStack {
TextField("Username", text: $viewModel.username)
Button("Update state") {
stateController.validated = true
}
}
}
}
ContentViewModel.swift
class ContentViewModel: ObservableObject {
#Published var username = ""
}
StateController.swift
class StateController: ObservableObject {
#Published var validated = false
}
Like lorem-ipsum pointed out, you should use #StateObject.
A good rule of thumb is to use #StateObject every time you init a viewModel, but use #ObservedObject when you are passing in a viewModel that has already been init.

How to avoid Fatal error: No ObservableObject of type ... in SwiftUI?

I trimmed things down to this very simple example (a vanilla SwiftUI project, edit only ContentView and one line in SceneDelegate). Sets up a trivial ObservableObject and a couple of Views for screens. With latest Xcode 11.4 and simulator crashes very easily.
import SwiftUI
// NB In SceneDelegate added: let contentView = ContentView().environmentObject(EO())
class EO: ObservableObject {
#Published var n = 2
}
struct ContentView: View {
#EnvironmentObject var eo: EO
var body: some View {
NavigationView {
VStack {
Text("A: \(eo.n)")
NavigationLink(destination: ContentViewB()) {
Text("Go to B")
}
}
}
}
}
struct ContentViewB: View {
#EnvironmentObject var eo: EO
var body: some View {
VStack {
Text("B: \(eo.n)")
}.onAppear {
self.eo.n += 1
}
}
}
Am I doing something wrong? Or is this a SwiftUI bug? It seems to work fine initially then on re-navigating to child view it crashes. Though the exact behaviour is non deterministic (might crash on 2nd or 3rd navigation!)

A View.environmentObject(_:) for may be missing as an ancestor of this view

I just updated to Xcode 11.4 and it's broken my code. I am storing some user settings in an ObservableObject as follows:
class UserSettings: ObservableObject {
#Published var cardOrder = UserDefaults.standard.integer(forKey: "Card Order")
#Published var cardTheme = UserDefaults.standard.integer(forKey: "Card Theme")
#Published var translation = UserDefaults.standard.integer(forKey: "Translation")
#Published var overdueFirst = UserDefaults.standard.bool(forKey: "Overdue First")
#Published var randomNum = 0
}
This is my main menu, the settings environment object is successfully passed down to the Settings view where I'm able to save and retrieve user selections.
struct ContentView: View {
#State var settings = UserSettings()
var body: some View {
SubView().environmentObject(settings)
}
}
struct SubView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
List {
NavigationLink (destination: Flashcard()){
HStack {
Image(systemName: "rectangle.on.rectangle.angled")
Text(verbatim: "Study")
}
}
NavigationLink (destination: Settings()) {
HStack {
Image(systemName: "gear")
Text(verbatim: "Settings")
}
}
}
}
}
But in my flashcard view, I am getting an error: Fatal error: No ObservableObject of type UserSettings found. A View.environmentObject(_:) for UserSettings may be missing as an ancestor of this view.: file SwiftUI, line 0
The error is on line 13 where I initiate Frontside. In the original code, I just called the Frontside subview, but I thought to solve the error I had to add .environmentObject(settings), but even after adding it my app compiles but crashes as soon I go to the Flashcard view.
struct Flashcard: View {
#EnvironmentObject var settings: UserSettings
#State var colour = UserDefaults.standard.integer(forKey: "Card Theme") * 6
#State private var showResults: Bool = false
#State private var fullRotation: Bool = false
#State private var showNextCard: Bool = false
var body: some View {
let zstack = ZStack {
Frontside(id: $settings.randomNum, sheet: $showingSheet, rotate: $fullRotation, invis: $showNextCard, col: $colour).environmentObject(self.settings)
//
Backside(id: $settings.randomNum, sheet: $showingSheet, bookmark: $bookmarked, results: $showResults, rotate: $fullRotation, invis: $showNextCard, col: $colour, trans: $translation).environmentObject(self.settings)
//
}
}
Does anyone know what I'm doing wrong? This code compiled and ran fine in the previous Xcode.
I think you should pass settings object to FlashCard and Settings as well.
try this:
struct ContentView: View {
#State var settings = UserSettings()
var body: some View {
SubView().environmentObject(settings)
}
}
struct SubView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
List {
NavigationLink (destination: Flashcard().environmentObject(settings)){
HStack {
Image(systemName: "rectangle.on.rectangle.angled")
Text(verbatim: "Study")
}
}
NavigationLink (destination: Settings().environmentObject(settings)) {
HStack {
Image(systemName: "gear")
Text(verbatim: "Settings")
}
}
}
}
}
An #EnvironmentObject has to be filled with an #StateObject, an #ObservedObject or an ObservableObject directly NOT an #State
struct ContentView: View {
//#ObservedObject
#StateObject var settings = UserSettings()
var body: some View {
SubView().environmentObject(settings)
}
}
Note: UserSettings has to be an ObservableObject
Apple documentation on managing model data
struct BookReader: App {
#StateObject var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environmentObject(library)
}
}
}
struct LibraryView: View {
#EnvironmentObject var library: Library
// ...
}
I'm running iOS 14.3 in the simulator and in my case the error was about my environmentObject NavigationController. It was resolved by modifying ContentView() with .environmentObject(NavigationController()) in the SceneDelegate and, if you want the preview to work, also in ContentView_Previews.
import SwiftUI
#main
// there is a file with the name of your "projectApp" (JuegosSwiftUIApp in my case)
struct JuegosSwiftUIApp: App {
var body: some Scene {
WindowGroup {
DatosIniciales() // any view
.environmentObject(Datos()) // this solved it (Datos() is class type Observableobject)
}
}
}

SwiftUI GeometryReader causes memory leak

Consider the following code:
import SwiftUI
class ViewModel: ObservableObject {
}
struct TestView: View {
#ObservedObject var vm = ViewModel()
var body: some View {
// self.sample
GeometryReader { _ in
self.sample
}
}
var sample: some View {
Text("Hello, World!")
}
}
struct Tabs : View {
#State var selection: Int = 0
var body: some View {
TabView(selection: $selection) {
TestView().tabItem {
Text("First Tab")
}
.tag(0)
Text(String(selection))
.tabItem {
Text("Second Tab")
}
.tag(1)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
There are two tabs and selection is referenced in body therefore body will be called when selection is changed.
TestView is using GeometryReader.
When I switch from "First Tab" to "Second Tab" ViewModel is created again and never dereferenced. This is unexpected.
If I switch 100 times I will have 100 ViewModels referenced from SwiftUI internals.
Though if i remove GeometryReader it works as expected.
Did someone experience it? Are there any workarounds?
I simply want this ViewModel lifetime to be bound to TestView lifetime.
UPDATE:
XCode 11.3.1 iOS 13.3
Ok, let's make the following changes in ViewModel
class ViewModel: ObservableObject {
init() {
print(">> inited") // you can put breakpoint here in Debug Preview
}
}
so now it seen that because View is value type
struct TestView: View {
#ObservedObject var vm = ViewModel() // << new instance on each creation
...
and it is originated from
var body: some View {
TabView(selection: $selection) {
TestView().tabItem { // << created on each tab switch
...
so, the solution would be to ViewModel creation out of TestView and inject outer instance either via .environmentObject or via constructor arguments.
Btw, it does not depend on GeometryReader. Tested with Xcode 11.2.1 / iOS 13.2

Xcode 11 beta 3 crashing when using NavigationLink, #EnvironmentObject and List together

I've got a strange crash in SwiftUI / Xcode 11 beta 3 with code like the one below (I've kept only the bare minimum to show the behavior):
import SwiftUI
import Combine
final class AppData: BindableObject {
let didChange = PassthroughSubject<AppData, Never>()
init() { }
}
struct ContentView : View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView() ) {
Text("link")
}
}
}
}
struct DetailView : View {
#EnvironmentObject var appData: AppData
// #ObjectBinding var appData = AppData() -> Works
var body: some View {
List {
Text("A")
Text("B")
Text("C")
}
}
}
The BindableObject is injected in SceneDelegate.swift like this:
....
// 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(AppData()))
self.window = window
window.makeKeyAndVisible()
}
....
When following the NavigationLink it crashes with
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
If I remove the List view from the detail view it works OK. The same if I use #ObjectBinding instead (like in the commented line in my code).
The same code used to work in previous betas.
It's a bug in Xcode 11 beta 3. The old behaviour will likely return.
From https://developer.apple.com/tutorials/swiftui/handling-user-input as of July 4th 2019:
Step 4
In Xcode 11 beta 3, the LandmarkDetail view doesn’t automatically access the UserData object in the view hierarchy’s environment. The workaround for this is to add the environmentObject(_:) modifier to the LandmarkDetail view.
I think that this is by design. When you create DetailView(), it is disconnected from the hierarchy, and hence it does not inherit the same environment.
If you change your ContentView to the following, it won't crash. I think I remember having a similar problem with modals:
struct ContentView : View {
#EnvironmentObject var appData: AppData
var body: some View {
NavigationView {
NavigationLink(destination: DetailView().environmentObject(appData) ) {
Text("link")
}
}
}
}