How to properly implement a global variable in SwiftUI - ios

I am going to create a SwiftUI application where I want to be able to swap between 3 modes. I am trying EnvironmentObject without success. I am able to change the view displayed locally, but from another View (in the end will be a class) I get a
fatal error: No ObservableObject of type DisplayView found. A View.environmentObject(_:) for DisplayView may be missing as an ancestor of this view.
Here is my code. The first line of the ContentView if/else fails.
enum ViewMode {
case Connect, Loading, ModeSelection
}
class DisplayView: ObservableObject {
#Published var displayMode: ViewMode = .Connect
}
struct ContentView: View {
#EnvironmentObject var viewMode: DisplayView
var body: some View {
VStack {
if viewMode.displayMode == .Connect {
ConnectView()
} else if viewMode.displayMode == .Loading {
LoadingView()
} else if viewMode.displayMode == .ModeSelection {
ModeSelectView()
} else {
Text("Error.")
}
TestView() //Want this to update the var & change UI.
}
.environmentObject(viewMode)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(DisplayView())
}
}
//FAILS
struct TestView: View {
#EnvironmentObject var showView: DisplayView
var body: some View {
HStack {
Button("-> load") {
self.showView.displayMode = .Loading
}
}
}
}
struct ConnectView: View {
var body: some View {
Text("Connect...")
}
}
struct LoadingView: View {
var body: some View {
Text("Loading...")
}
}
struct ModeSelectView: View {
var body: some View {
Text("Select Mode")
}
}
I would like to be able to update DisplayView from anywhere and have the ContentView UI adapt accordingly. I can update from within ContentView but I want to be able update from anywhere and have my view change.

I needed to inject BEFORE - so this fixed things up:
#main
struct fooApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DisplayView()) //super key!
}
}
}

I also tried a Singleton class to store some properties - and thus they are available from anywhere and can be updated anywhere - without having to declare EnvironmentObject. It's just another way that can work in different circumstances.
class PropContainerModel {
public var foo = "Hello"
static let shared = PropContainerModel()
private override init(){}
}
And then somewhere else
let thisFoo = PropContainerModel.shared.foo
//
PropContainerModel.shared.foo = "There"
Update here (Singleton but changes reflect in the SwiftUI UI).
class PropContainerModel: ObservableObject
{
#Published var foo: String = "Foo"
static let shared = PropContainerModel()
private init(){}
}
struct ContentView: View
{
#ObservedObject var propertyModel = PropContainerModel.shared
var body: some View {
VStack {
Text("foo = \(propertyModel.foo)")
.padding()
Button {
tapped(value: "Car")
} label: {
Image(systemName:"car")
.font(.system(size: 24))
.foregroundColor(.black)
}
Spacer()
.frame(height:20)
Button {
tapped(value: "Star")
} label: {
Image(systemName:"star")
.font(.system(size: 24))
.foregroundColor(.black)
}
}
}
func tapped(value: String)
{
PropContainerModel.shared.foo = value
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

Dismissing a view with SwiftUI

I want to show an instruction page the first time a user opens my app. I have gotten this to work, but I cannot understand how to get back to the contentView (without restarting the app).
Just to clarify: I want the "Dismiss this view" button on InstructionView to set the view to be shown to "ContentView" and to dismiss the InstructionView. As it is now the viewRouter.setToContentView() makes the App crash, and I cannot get around it.
Thanks in advance
TestDismissApp
import SwiftUI
#main
struct TestDismissApp: App {
var body: some Scene {
WindowGroup {
MotherView()
}
}
}
MotherView
import SwiftUI
struct MotherView : View {
#ObservedObject var viewRouter = ViewRouter()
var body: some View {
VStack {
if viewRouter.currentPage == "InstructionView" {
InstructionView()
} else if viewRouter.currentPage == "ContentView" {
ContentView()
}
}
}
}
ViewRouter
import Foundation
class ViewRouter: ObservableObject {
init() {
//UserDefaults.standard.set(false, forKey: "didLaunchBefore") remove // if you want to show the instructions again for test reasons
if !UserDefaults.standard.bool(forKey: "didLaunchBefore") {
UserDefaults.standard.set(true, forKey: "didLaunchBefore")
currentPage = "InstructionView"
} else {
currentPage = "ContentView"
}
}
func setToContentView () {
currentPage = "ContentView"
}
#Published var currentPage: String
}
ContentView
import SwiftUI
struct ContentView: View {
var body: some View {
Text("My wonderful content")
.padding()
}
}
InstructionView
import SwiftUI
struct InstructionView: View {
#Environment(\.dismiss) var dismiss
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Text("Instruction: This app shows a wonderful sentence")
Button {
viewRouter.setToContentView()
dismiss()
} label: {
Text("Dismiss this view")
}
}
}
}
try this:
in MotherView use #StateObject var viewRouter = ViewRouter() and
InstructionView().environmentObject(viewRouter)

Value of Selected Option From a SwiftUI Picker does not Update the View

I have the following in a SwiftUI app. Basically I have some settings (Settings class) that I would like to use throughout the app. I have a Settings view that shows a picker to select the value of one of the settings. And other views of the app would only use the current set value of the settings. The following setup works in the sense that in ContentView I see the correct value of firstLevel setting. But the problem is that in SettingsView, I think since selectedFirstLevel is not a #State, its correct value is not shown on the picker I navigate to select either even or odd (oddly, the first time it's correct). This selection is carried correctly to ContentView, but it's not shown correctly on SettingsView. How can I fix this issue?
Settings.swift
import Foundation
class Settings: ObservableObject {
static let shared: Settings = Settings()
#Published var firstLevel: FirstLevel = .even
}
enum FirstLevel: String, CaseIterable, Identifiable {
case even
case odd
var id: String { self.rawValue }
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var showSettings: Bool = false
#ObservedObject var settings = Settings.shared
var body: some View {
VStack {
SettingsButton(showSettings: $showSettings, settings: settings)
Text(settings.firstLevel.id)
.padding()
}
}
}
struct SettingsButton: View {
#Binding var showSettings: Bool
var settings: Settings
var firstLevel: Binding<FirstLevel> {
return Binding<FirstLevel>(
get: {
return self.settings.firstLevel
}) { newFirstLevel in
self.settings.firstLevel = newFirstLevel
}
}
var body: some View {
Button(action: { self.showSettings = true }) {
Image(systemName: "gear").imageScale(.large)
}
.sheet(isPresented: $showSettings) {
SettingsView(selectedFirstLevel: self.firstLevel)
}
}
}
SettingsView.swift
import SwiftUI
struct SettingsView: View {
#Binding var selectedFirstLevel: FirstLevel
var body: some View {
NavigationView {
Form {
Picker("First Level", selection: $selectedFirstLevel) {
ForEach(FirstLevel.allCases) { level in
Text(level.rawValue).tag(level)
}
}
}
.navigationBarTitle("Settings", displayMode: .inline)
}
}
}
It looks overcomplicated, moreover Binding is unreliable as communication between different view hierarchies (which is sheet in your case).
Here is simplified and worked variant. Tested with Xcode 12 / iOS 14.
struct ContentView: View {
#ObservedObject var settings = FLevelSettings.shared
var body: some View {
VStack {
SettingsButton(settings: settings)
Text(settings.firstLevel.id)
.padding()
}
}
}
struct SettingsButton: View {
#State private var showSettings: Bool = false
var settings: FLevelSettings
var body: some View {
Button(action: { self.showSettings = true }) {
Image(systemName: "gear").imageScale(.large)
}
.sheet(isPresented: $showSettings) {
FLevelSettingsView(settings: self.settings)
}
}
}
struct FLevelSettingsView: View {
#ObservedObject var settings: FLevelSettings
var body: some View {
NavigationView {
Form {
Picker("First Level", selection: $settings.firstLevel) {
ForEach(FirstLevel.allCases) { level in
Text(level.rawValue).tag(level)
}
}
}
.navigationBarTitle("Settings", displayMode: .inline)
}
}
}
Note: it can be even more simplified, if you want, due to presence of FLevelSettings.shared, so you can use it inside FLevelSettingsView directly. Just in case.

Making a combine passthrough publisher less global

Swift 5, iOS 13
I want to use passthroughSubject publisher; but I my gut tells me its a global variable and as such very poor practice. How can make this global variable less global, while still being usable. Here's some code to show what I talking about.
I know there are a dozen other ways to do this, but I wanted to create some simple code to illustrate the issue.
import SwiftUI
import Combine
let switcher = PassthroughSubject<Void,Never>()
struct SwiftUIViewF: View {
#State var nextPage = false
var body: some View {
VStack {
Text("Switcher")
.onReceive(switcher) { (_) in
self.nextPage.toggle()
}
if nextPage {
Page1ViewF()
} else {
Page2ViewF()
}
}
}
}
struct Page1ViewF: View {
var body: some View {
Text("Page 1")
.onTapGesture {
switcher.send()
}
}
}
struct Page2ViewF: View {
var body: some View {
Text("Page 2")
.onTapGesture {
switcher.send()
}
}
}
struct SwiftUIViewF_Previews: PreviewProvider {
static var previews: some View {
SwiftUIViewF()
}
}
Here is possible solution - to hold it in parent and inject into child views:
struct SwiftUIViewF: View {
let switcher = PassthroughSubject<Void,Never>()
#State var nextPage = false
var body: some View {
VStack {
Text("Switcher")
.onReceive(switcher) { (_) in
self.nextPage.toggle()
}
if nextPage {
Page1ViewF(switcher: switcher)
} else {
Page2ViewF(switcher: switcher)
}
}
}
}
struct Page1ViewF: View {
let switcher: PassthroughSubject<Void,Never>
var body: some View {
Text("Page 1")
.onTapGesture {
self.switcher.send()
}
}
}
struct Page2ViewF: View {
let switcher: PassthroughSubject<Void,Never>
var body: some View {
Text("Page 2")
.onTapGesture {
self.switcher.send()
}
}
}
An example using #EnvironmentObject.
Let SDK take care of observing / passing things for you, rather than setting up yourself.
Especially when your usage is a simple toggle.
import SwiftUI
import Combine
final class EnvState: ObservableObject { #Published var nextPage = false }
struct SwiftUIViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
VStack {
Text("Switcher")
if env.nextPage {
Page1ViewF()
} else {
Page2ViewF()
}
}
}
}
struct Page1ViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
Text("Page 1")
.onTapGesture {
env.nextPage.toggle()
}
}
}
struct Page2ViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
Text("Page 2")
.onTapGesture {
env.nextPage.toggle()
}
}
}
struct SwiftUIViewF_Previews: PreviewProvider {
static var previews: some View {
SwiftUIViewF().environmentObject(EnvState())
}
}

How do I update a text label in SwiftUI?

I have a SwiftUI text label and i want to write something in it after I press a button.
Here is my code:
Button(action: {
registerRequest() // here is where the variable message changes its value
}) {
Text("SignUp")
}
Text(message) // this is the label that I want to change
How do I do this?
With only the code you shared it is hard to say exactly how you should do it but here are a couple good ways:
The first way is to put the string in a #State variable so that it can be mutated and any change to it will cause an update to the view. Here is an example that you can test with Live Previews:
import SwiftUI
struct UpdateTextView: View {
#State var textToUpdate = "Update me!"
var body: some View {
VStack {
Button(action: {
self.textToUpdate = "I've been udpated!"
}) {
Text("SignUp")
}
Text(textToUpdate)
}
}
}
struct UpdateTextView_Previews: PreviewProvider {
static var previews: some View {
UpdateTextView()
}
}
If your string is stored in a class that is external to the view you can use implement the ObservableObject protocol on your class and make the string variable #Published so that any change to it will cause an update to the view. In the view you need to make your class variable an #ObservedObject to finish hooking it all up. Here is an example you can play with in Live Previews:
import SwiftUI
class ExternalModel: ObservableObject {
#Published var textToUpdate: String = "Update me!"
func registerRequest() {
// other functionality
textToUpdate = "I've been updated!"
}
}
struct UpdateTextViewExternal: View {
#ObservedObject var viewModel: ExternalModel
var body: some View {
VStack {
Button(action: {
self.viewModel.registerRequest()
}) {
Text("SignUp")
}
Text(self.viewModel.textToUpdate)
}
}
}
struct UpdateTextViewExternal_Previews: PreviewProvider {
static var previews: some View {
UpdateTextViewExternal(viewModel: ExternalModel())
}
}

Displaying activity indicator while loading async request in SwiftUI

I have a basic view that displays a list that fetches data from an API. I want to implement an activity indicator while the data is being retrieved from the API. In MVC we could use a delegate and protocol and make the view controller inherit the protocol, and after the model has finished fetching the data we call the delegate to tell the view controller that the data has finished retrieving (now hide the activity indicator, etc.). How to achieve a similar thing in SwiftUI and it's MVVM style?
I have tried implementing an activity indicator from this question, I just don't get how and when to stop it: Activity indicator in SwiftUI
My SourcesViewModel (it fetches news article sources from newsapi.org)
import UIKit
class SourcesViewModel: Identifiable {
let id = UUID()
let source: Sources
init(source: Sources) {
self.source = source
}
var name: String {
return self.source.sourceName
}
var description: String {
return self.source.sourceDescription
}
}
My SourcesListViewModel:
import Combine
class SourcesListViewModel: ObservableObject {
init() {
fetchSources()
}
#Published var sources = [SourcesViewModel]()
private func fetchSources() {
NetworkManager.shared.getSourceData { (sources) in
self.sources = sources.map(SourcesViewModel.init)
}
}
}
Lastly, my SourcesView:
import SwiftUI
struct SourcesView: View {
#ObservedObject var model = SourcesListViewModel()
var body: some View {
ActivityIndicatorView(isShowing: .constant(true)) {
NavigationView {
List(self.model.sources) { source in
VStack(alignment: .center) {
Text(source.name)
Text(source.description)
.foregroundColor(.secondary)
.lineLimit(3)
}
.navigationBarTitle(Text("Sources"), displayMode: .inline)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
SourcesView()
}
}
The result:
Your view model should have loading state, like the following
#Published var loading = false
private func fetchSources() {
self.loading = true
NetworkManager.shared.getSourceData { (sources) in
self.sources = sources.map(SourcesViewModel.init)
self.loading = false
}
}
and activity indicator should be bound to it, like
ActivityIndicatorView(isShowing: $model.loading) {
import Combine
class SourcesListViewModel: ObservableObject {
#Published var loading = true
#Published var sources = [SourcesViewModel]()
init() {
fetchSources()
}
private func fetchSources() {
NetworkManager.shared.getSourceData { (sources) in
self.sources = sources.map(SourcesViewModel.init)
self.loading=false
}
}
}
import SwiftUI
struct SourcesView: View {
#ObservedObject var model = SourcesListViewModel()
var body: some View {
ActivityIndicatorView(isShowing: .constant(self.model.loading)) {
NavigationView {
List(self.model.sources) { source in
VStack(alignment: .center) {
Text(source.name)
Text(source.description)
.foregroundColor(.secondary)
.lineLimit(3)
}
.navigationBarTitle(Text("Sources"), displayMode: .inline)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
SourcesView()
}
}

Resources