How to pass static values in SwiftUI from one View to another? - ios

I'm new to learning SwiftUI and XCode and am unable to figure out how to pass a variable from view to another. I read on #State and #Binding variables but from what I can tell that is for values that change. I have a static value that I calculate based on the date when the user opens the app.
The variable is the current moon phase and is stored locally in my main ContentView. I want to pass this variable to a second view that's accessed by clicking a NavigationLink.
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
let currentMoonPhaseArray = calculateMoonPhase()
let moonPhase = currentMoonPhaseArray[0]
NavigationView{
ScrollView(.vertical, showsIndicators:true) {
VStack(spacing:3){
NavigationLink(destination: MoonPhaseView()){
Text("Moon Phase - " + moonPhase)
}
}
}
.frame(maxWidth: .infinity)
.navigationTitle("MySky")
.navigationBarTitleDisplayMode(.inline)
}
}
}
MoonPhaseView.swift
import SwiftUI
struct MoonPhaseView: View {
var body: some View {
HStack{
Text("MoonPhaseView!")
}
}
}
struct MoonPhaseView_Previews: PreviewProvider {
static var previews: some View {
MoonPhaseView()
}
}
My goal is to have the calculated moon phase from ContentView.swift be passed to the MoonPhaseView.swift. I believe that bindings are the correct approach from what I've read, but all binding implementations seem to be for updating views often.
Thanks for any help or pointers!

You haven't shown what the type of moonPhase is, so I'm just going to use String as an example.
struct ContentView: View {
func calculateMoonPhase() -> [String] {
return ["Full","Waxing","Waning"]
}
var body: some View {
let currentMoonPhaseArray = calculateMoonPhase()
let moonPhase = currentMoonPhaseArray[0]
NavigationView{
ScrollView(.vertical, showsIndicators:true) {
VStack(spacing:3){
NavigationLink(destination: MoonPhaseView(phase: moonPhase)){
Text("Moon Phase - " + moonPhase)
}
}
}
.frame(maxWidth: .infinity)
.navigationTitle("MySky")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct MoonPhaseView: View {
var phase : String
var body: some View {
HStack{
Text("MoonPhaseView!")
Text(phase)
}
}
}
struct MoonPhaseView_Previews: PreviewProvider {
static var previews: some View {
MoonPhaseView(phase: "Full")
}
}
Note that every time MoonPhaseView is used, you must provide a phase parameter so that it has a value to fill var phase : String. You could provide a default value, but that doesn't seem like it would do much good here.
Not directly related to your question, but I might suggest that calculating the phase in the body might lead to undesirable results, especially if it's an expensive calculation or it has to contact an API or something. You might want to consider doing this in onAppear and keeping it in a #State variable in your ContentView, or perhaps even using an ObservableObject as a view model and storing the phase there.

You can use "Environment" to pass system-wide settings to views and child views.
For example:
#main
struct TestApp: App {
let moonPhaseValue = "Waxing" // <--- some value
var body: some Scene {
WindowGroup {
ContentView().environment(\.moonPhase, moonPhaseValue) // <--- pass it around
}
}
}
struct ContentView: View {
#Environment(\.moonPhase) var moonPhase // <--- the value again
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: MoonPhaseView()) {
Text("Moon Phase - " + moonPhase)
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct MoonPhaseView: View {
#Environment(\.moonPhase) var moonPhase // <--- the value again
var body: some View {
HStack{
Text("MoonPhaseView is \(moonPhase)")
}
}
}
// create your own EnvironmentKey
struct MoonPhaseKey: EnvironmentKey {
static let defaultValue: String = "Full"
}
// create your own EnvironmentValues
extension EnvironmentValues {
var moonPhase: String {
get { return self[MoonPhaseKey] }
set { self[MoonPhaseKey] = newValue }
}
}

Related

How to properly implement a global variable in SwiftUI

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()
}
}

cannot find $exercisePlan in scope (SwiftUI) [duplicate]

How do I generate a preview provider for a view which has a binding property?
struct AddContainer: View {
#Binding var isShowingAddContainer: Bool
var body: some View {
Button(action: {
self.isShowingAddContainer = false
}) {
Text("Pop")
}
}
}
struct AddContainer_Previews: PreviewProvider {
static var previews: some View {
// ERROR HERE <<<-----
AddContainer(isShowingAddContainer: Binding<Bool>()
}
}
In Code above, How to pass a Binding<Bool> property in an initialiser of a view?
Just create a local static var, mark it as #State and pass it as a Binding $
struct AddContainer_Previews: PreviewProvider {
#State static var isShowing = false
static var previews: some View {
AddContainer(isShowingAddContainer: $isShowing)
}
}
If you want to watch the binding:
Both other solutions [the "static var" variant AND the "constant(.false)"-variant work for just seeing a preview that is static. But you cannot not see/watch the changes of the value intended by the button action, because you get only a static preview with this solutions.
If you want to really watch (in the life preview) this changing, you could easily implement a nested view within the PreviewProvider, to - let's say - simulate the binding over two places (in one preview).
import SwiftUI
struct BoolButtonView: View {
#Binding var boolValue : Bool
var body: some View {
VStack {
Text("The boolValue in BoolButtonView = \(boolValue.string)")
.multilineTextAlignment(.center)
.padding()
Button("Toggle me") {
boolValue.toggle()
}
.padding()
}
}
}
struct BoolButtonView_Previews: PreviewProvider {
// we show the simulated view, not the BoolButtonView itself
static var previews: some View {
OtherView()
.preferredColorScheme(.dark)
}
// nested OTHER VIEW providing the one value for binding makes the trick
struct OtherView : View {
#State var providedValue : Bool = false
var body: some View {
BoolButtonView(boolValue: $providedValue)
}
}
}
Other way
struct AddContainer_Previews: PreviewProvider {
static var previews: some View {
AddContainer(isShowingAddContainer: .constant(false))
}
}

SwiftUI - "Argument passed to call that takes no arguments"?

I have an issue with the coding for my app, where I want to be able to scan a QR and bring it to the next page through navigation link. Right now I am able to scan a QR code and get a link but that is not a necessary function for me. Below I attached my code and got the issue "Argument passed to call that takes no arguments", any advice or help would be appreciated :)
struct QRCodeScannerExampleView: View {
#State private var isPresentingScanner = false
#State private var scannedCode: String?
var body: some View {
VStack(spacing: 10) {
if let code = scannedCode {
//error below
NavigationLink("Next page", destination: PageThree(scannedCode: code), isActive: .constant(true)).hidden()
}
Button("Scan Code") {
isPresentingScanner = true
}
Text("Scan a QR code to begin")
}
.sheet(isPresented: $isPresentingScanner) {
CodeScannerView(codeTypes: [.qr]) { response in
if case let .success(result) = response {
scannedCode = result.string
isPresentingScanner = false
}
}
}
}
}
Page Three Code
import SwiftUI
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
struct PageThree_Previews: PreviewProvider {
static var previews: some View {
PageThree()
}
}
You forgot property:
struct PageThree: View {
var scannedCode: String = "" // << here !!
var body: some View {
Text("Code: " + scannedCode)
}
}
You create your PageThree View in two ways, One with scannedCode as a parameter, one with no params.
PageThree(scannedCode: code)
PageThree()
Meanwhile, you defined your view with no initialize parameters
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
For your current definition, you only can use PageThree() to create your view. If you want to pass value while initializing, change your view implementation and consistently using one kind of initializing method.
struct PageThree: View {
var scannedCode: String
var body: some View {
Text(scannedCode)
}
}
or
struct PageThree: View {
private var scannedCode: String
init(code: String) {
scannedCode = code
}
var body: some View {
Text(scannedCode)
}
}
This is basic OOP, consider to learn it well before jump-in to development.
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

How to observe change in #StateObject in SwiftUI?

I am having property with #StateObject, I am trying to observe change in viewmodel, I am able to print correct result but can not able to show on screen as view is not refreshing.
Tried using binding but not worked because of #StateObject
import SwiftUI
struct AbcView: View {
#StateObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self._abcViewModel = StateObject(wrappedValue: abcViewModel)
}
var body: some View {
VStack(alignment: .leading) {
ZStack(alignment: .top) {
ScrollView {
Text("some txt")
}
.overlay(
VStack {
TopView(content: classViews(data: $abcViewModel.somedata, abcViewModel: abcViewModel))
Spacer()
}
)
}
}
}
}
func classViews(data: Binding<[SomeData]>, abcViewModel: AbcViewModel) -> [AnyView] {
var views: [AnyView] = []
for element in data {
views.append(
VStack(spacing: 0) {
HStack {
print("\(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? "")) )") // printing correct value
Text(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? ""))) // want to observe change here
}
}
.convertToAnyView())
}
return views
}
If you are injecting your AbcViewModel into AbcView you should use #ObserverdObject instead of #StateObject , full explanation here Also you should conform tour AbcViewModel to ObservableObject and make your desired property #Published if you want to trigger the change in View . Here is simplified code example:
Making AbcViewModel observable:
class AbcViewModel: ObservableObject {
#Published var dataID: String = "" //by changing the #Published proprty you trigger change in View using it
}
store AbcViewModel as #ObserverdObject:
struct AbcView: View {
#ObservedObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self.abcViewModel = abcViewModel
}
var body: some View {
//...
}
}
If you now use your AbcViewModel dataID property anywhere in the project, and you change its value, the property will publish the change and your View (struct) will be rebuilded. Use the same pattern for creating TopView and assigning AbcViewModel to it the same way.

Using #State resolves into 'self' used before all stored properties are initialized

I ran into an issue when using the #State property.
My ContentView.swift looks like this:
import SwiftUI
struct ContentView: View {
#State var showText: Bool = true
var Mod: Modifier
init() {
Mod = Modifier(showText: $showText) // Throws error -> 'self' used before all stored properties are initialized ('self.Mod' not initialized)
}
var body: some View {
VStack {
if showText == true {
Text("Hello, World!")
}
Mod
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And my Modifier.swift from which the Modifier view is called has following code:
import SwiftUI
struct Modifier: View {
#Binding var showText: Bool
var body: some View {
VStack {
Button("Hide Text") {
self.showText.toggle()
}
}
}
}
I created this simplified code from my actual project that my problem is easier to understand.
Problem
The problem is that the code in the init function results into an error and I don't know how to resolve it.
What I tried and what I would need
Because this is just a simplified version of my actual code there are some requirements I need to my code:
Mod can't be a computed variable
I somehow need the Modifier view as a variable called Mod in my ContentView
When I remove the #State property and the #Binding property and the $ the code works and results with 0 errors. But I need to use the #State property (which unfortunately results into errors with my code)
Also the button to hide and show the text should work
I would be very thankful if anyone could give me a hint. I really appreciate your help!
I did actually find a way to do this. I'm not sure whether it'll be suitable but here are the details.
The problem was that SwiftUI didn't seem to allow setting the Binding outside of body. So this solution returns a new instance of Modifier
struct Modifier: View {
#Binding var showText: Bool
var body: some View {
VStack {
Button("Hide Text") {
self.showText.toggle()
}
}
}
// this function returns a new instance with the binding
func bind(to binding: Binding<Bool>) -> Self {
return Modifier(showText: binding)
}
}
And the code for ContentView, where we can call this function from within body:
struct ContentView: View {
#State var showText: Bool = true
var Mod: Modifier
init() {
Mod = Modifier(showText: .constant(true)) // .constant() gives a placeholder Binding
}
var body: some View {
return VStack {
if showText == true {
Text("Hello, World!")
}
Mod.bind(to: $showText)
}
}
}
Tested and the text can be hidden/shown. Hope this can help.
Mod = Modifier(showText: _showText.projectedValue)
You can make it let instead of var if you'd like.
Use views inside body context
struct ContentView: View {
#State var showText: Bool = true
var body: some View {
VStack {
if showText == true {
Text("Hello, World!")
}
Modifier(showText: $showText)
}
}
}

Resources