In my content View, I am initializing the views "ringView" and "ringNumberView" which are shown on screen. However, the data shown by these views is constantly changing and I would like the user to be able to refresh them at any point via the "refresh" button. With the button I am attempting to reset the views with current data by recreating them but they do not changed when "refresh" is clicked.
content View:
struct ContentView: View {
#State var WeekElevation : FlightsClimbed = FlightsClimbed() //ignore this
#State var ringView : RingView = RingView() //creating them here
#State var ringNumberView = RingNumbersView()
var body: some View {
Button("refresh") { //trying to recreate them here
ringView = RingView()
ringNumberView = RingNumbersView()
}
ringView.frame(width: 45, height: 45)
ringNumberView
VStack { // ignore this
Text("\(Int(WeekElevation.getFlights())) ") + Text(Image(systemName: "figure.stairs")) + Text(" in past week")
Text("\(WeekElevation.calculatePercentEverest())% of Mt. Everest")
}
}
}
RingView:
import Foundation
import HealthKit
import SwiftUI
struct RingView : WKInterfaceObjectRepresentable {
#StateObject var fitness = main()
func makeWKInterfaceObject(context: Context) -> some WKInterfaceObject {
let ringObject = WKInterfaceActivityRing()
fitness.makeQuery() { summary in
ringObject.setActivitySummary(summary, animated: true)
}
return ringObject
}
func updateWKInterfaceObject(_ wkInterfaceObject: WKInterfaceObjectType, context: Context) {
}
}
RingNumbersView:
struct RingNumbersView: View {
#StateObject var fitness = main()
#State var ActivitySummary : HKActivitySummary = HKActivitySummary()
var body: some View {
let red = Int(ActivitySummary.activeEnergyBurned.doubleValue(for: .largeCalorie()))
let green = Int(ActivitySummary.appleExerciseTime.doubleValue(for: .minute()))
let blue = Int(ActivitySummary.appleStandHours.doubleValue(for: .count()))
HStack {
Text("\(red)").foregroundColor(.red)
Text("\(green)").foregroundColor(.green)
Text("\(blue)").foregroundColor(.blue)
}.padding().onAppear(){
fitness.authorizeHealthkit()
fitness.makeQuery() { summary in
ActivitySummary = summary
}
}
}
}
Thanks very much for any help
Every time you call
main()
You create a different instance. One does not know about the other.
Use
#ObservedObject
Or
#EnvironmentObject
For the child Views.
Also, Views should not be in an
#State
Only in a
body
Or
#ViewBuilder
Related
I am fairly new to SwiftUI and I am trying to build an app where you can favorite items in a list. It works in the ContentView but I would like to have the option to favorite and unfavorite an item in its DetailView.
I know that vm is not in the scope but how do I fix it?
Here is some of the code in the views. The file is long so I am just showing the relevant code
struct ContentView: View {
#StateObject private var vm = ViewModel()
//NavigationView with a List {
//This is the code I call for showing the icon. The index is the item in the list
Image(systemName: vm.contains(index) ? "heart.fill" : "heart")
.onTapGesture{
vm.toggleFav(item: index)
}
}
struct DetailView: View {
Hstack{
Image(systemName: vm.contains(entry) ? "heart.fill" : "heart") //Error is "Cannot find 'vm' in scope"
}
}
Here is the code that that vm is referring to
import Foundation
import SwiftUI
extension ContentView {
final class ViewModel: ObservableObject{
#Published var items = [Biase]()
#Published var showingFavs = false
#Published var savedItems: Set<Int> = [1, 7]
// Filter saved items
var filteredItems: [Biase] {
if showingFavs {
return items.filter { savedItems.contains($0.id) }
}
return items
}
private var BiasStruct: BiasData = BiasData.allBias
private var db = Database()
init() {
self.savedItems = db.load()
self.items = BiasStruct.biases
}
func sortFavs(){
withAnimation() {
showingFavs.toggle()
}
}
func contains(_ item: Biase) -> Bool {
savedItems.contains(item.id)
}
// Toggle saved items
func toggleFav(item: Biase) {
if contains(item) {
savedItems.remove(item.id)
} else {
savedItems.insert(item.id)
}
db.save(items: savedItems)
}
}
}
This is the list view...
enter image description here
Detail view...
enter image description here
I tried adding this code under the List(){} in the ContentView .environmentObject(vm)
And adding this under the DetailView #EnvironmentObject var vm = ViewModel() but it said it couldn't find ViewModel.
To put the view model inside the ContentView struct is wrong. Delete the enclosing extension.
If the view model is supposed to be accessed from everywhere it must be on the top level.
In the #main struct create the instance of the view model and inject it into the environment
#main
struct MyGreatApp: App {
#StateObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
And in any struct you want to use it add
#EnvironmentObject var vm : ViewModel
without parentheses.
I can't update the color of my Text base on the current status of my object.
The text should change color base on the variable status true or false.
I try below to simplify the code of where the data come from.
My contentview:
struct ContentView: View {
#StateObject var gm = GameManager()
#State var openSetting = false
var body: some View {
Button {
openSetting.toggle()
} label: {
Text("Setting")
}
}
}
ContentView has a SettingView where I'm selecting setting and where I want to update my textColor based on the status of object
struct SettingView: View {
#StateObject var gm : GameManager
var body: some View {
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 20) {
ForEach(gm.cockpit.ecamManager.door.doorarray) { doorName in
Button {
gm.close(door: doorName.doorName)
} label: {
Text(doorName.doorName)
// Here where I want to change color
.foregroundColor(doorName.isopen ? .orange : .green)
}
}
}
}
}
}
The data come from GameManager which inside has a variable called cockpit:
class GameManager: NSObject, ObservableObject, ARSessionDelegate, ARSCNViewDelegate {
#Published var cockpit = MakeCockpit() // create the cockpit
// do other stuff
}
MakeCockpit :
class MakeCockpit: SCNNode, ObservableObject {
#Published var ecamManager = ECAMManager()
// do other stuff
ECAMManager:
class ECAMManager: ObservableObject {
#Published var door = ECAMDoor()
#Published var stanby = ECAMsby()
}
And Finally... the Array I want to watch is in ECAMDoor class:
class ECAMDoor: ObservableObject {
#Published var doorarray : [Door] = [] // MODEL
}
Now everything work fine as expected but the #Publish of the door array not update my color in the setting view. I need to close the view and open again to se the color update.
Is someone can tell me where I mistake? I probably missed something .. hope I been clear (to many instance of class inside other class)
I am kind of a SwiftUI newbe but my question is essentially this:
I have a view that looks like this:
struct myView: View {
var label = Text("label")
var subLabel = Text("sublabel")
var body: some View {
VStack {
label
subLabel
}
}
public func primaryColor(color: Color) -> some View {
var view = self
view.label = view.label.foregroundColor(color)
return view.id(UUID())
}
public func secondaryColor(color: Color) -> some View {
var view = self
view.subLabel = view.subLabel.foregroundColor(color)
return view.id(UUID())
}
}
And here is my problem:
In my parent view, I would like to call myView as follows
struct parentView: View {
var body: some View {
myView()
.primaryColor(color: .red)
.secondaryColor(color: .blue)
}
}
Using only one of these modifiers works fine but stacking them won't work (since they return some View ?).
I don't think that I can use standard modifiers since I have to access myView variables, which (I think) wouldn't be possible by using a ViewModifier.
Is there any way to achieve my goal or am I going on the wrong direction ?
Here is a solution for you - remove .id (it is really not needed, result will be a copy anyway):
struct myView: View {
var label = Text("label")
var subLabel = Text("sublabel")
var body: some View {
VStack {
label
subLabel
}
}
public func primaryColor(color: Color) -> Self {
var view = self
view.label = view.label.foregroundColor(color)
return view
}
public func secondaryColor(color: Color) -> Self {
var view = self
view.subLabel = view.subLabel.foregroundColor(color)
return view
}
}
and no changes in parent view
Demo prepared with Xcode 13 / iOS 15
I'd suggest a refactor where primaryColor and secondaryColor can be passed as optional parameters to your MyView (in Swift, it is common practice to capitalize type names):
struct MyView: View {
var primaryColor : Color = .primary
var secondaryColor : Color = .secondary
var body: some View {
VStack {
Text("label")
.foregroundColor(primaryColor)
Text("sublabel")
.foregroundColor(secondaryColor)
}
}
}
struct ParentView: View {
var body: some View {
MyView(primaryColor: .red, secondaryColor: .blue)
}
}
This way, the code is much shorter and more simple, and follows the general form/practices of SwiftUI.
You could also use Environment keys/values to pass the properties down, but this takes more code (shown here for just the primary color, but you could expand it to secondary as well):
private struct PrimaryColorKey: EnvironmentKey {
static let defaultValue: Color = .primary
}
extension EnvironmentValues {
var primaryColor: Color {
get { self[PrimaryColorKey.self] }
set { self[PrimaryColorKey.self] = newValue }
}
}
extension View {
func primaryColor(_ primary: Color) -> some View {
environment(\.primaryColor, primary)
}
}
struct MyView: View {
#Environment(\.primaryColor) var primaryColor : Color
var body: some View {
VStack {
Text("label")
.foregroundColor(primaryColor)
}
}
}
struct ParentView: View {
var body: some View {
MyView()
.primaryColor(.red)
}
}
I know that State wrappers are for View and they designed for this goal, but I wanted to try build and test some code if it is possible, my goal is just for learning purpose,
I have 2 big issues with my code!
Xcode is unable to find T.
How can I initialize my state?
import SwiftUI
var state: State<T> where T: StringProtocol = State(get: { state }, set: { newValue in state = newValue })
struct ContentView: View {
var body: some View {
Text(state)
}
}
Update: I could do samething for Binding here, Now I want do it for State as well with up code
import SwiftUI
var state2: String = String() { didSet { print(state2) } }
var binding: Binding = Binding.init(get: { state2 }, set: { newValue in state2 = newValue })
struct ContentView: View {
var body: some View {
TextField("Enter your text", text: binding)
}
}
If I could find the answer of my issue then, i can define my State and Binding both outside of View, 50% of this work done and it need another 50% for State Wrapper.
New Update:
import SwiftUI
var state: State<String> = State.init(initialValue: "Hello") { didSet { print(state.wrappedValue) } }
var binding: Binding = Binding.init(get: { state.wrappedValue }, set: { newValue in state = State(wrappedValue: newValue) })
struct ContentView: View {
var body: some View {
Text(state) // <<: Here is the issue!
TextField("Enter your text", text: binding)
}
}
Even if you create a State wrapper outside a view, how will the view know when to refresh its body?
Without a way to notify the view, your code will do the same as:
struct ContentView: View {
var body: some View {
Text("Hello")
}
}
What you can do next depends on what you want to achieve.
If all you need is a way to replicate the State behaviour outside the view, I recommend you take a closer look at the Combine framework.
An interesting example is CurrentValueSubject:
var state = CurrentValueSubject<String, Never>("state1")
It stores the current value and also acts as a Publisher.
What will happen if we use it in a view that doesn't observe anything?
struct ContentView: View {
var body: some View {
Text(state.value)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state.value = "state2"
}
}
}
}
The answer is: nothing. The view is drawn once and, even if the state changes, the view won't be re-drawn.
You need a way to notify the view about the changes. In theory you could do something like:
var state = CurrentValueSubject<String, Never>("state1")
struct ContentView: View {
#State var internalState = ""
var body: some View {
Text(internalState)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state.value = "state2"
}
}
.onReceive(state) {
internalState = $0
}
}
}
But this is neither elegant nor clean. In these cases we should probably use #State:
struct ContentView: View {
#State var state = "state1"
var body: some View {
Text(state)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state = "state2"
}
}
}
}
To sum up, if you need a view to be refreshed, just use the native SwiftUI property wrappers (like #State). And if you need to declare state values outside the view, use ObservableObject + #Published.
Otherwise there is a huge Combine framework which does exactly what you want. I recommend you take a look at these links:
Combine: Getting Started
Using Combine
In the code below after running I see the text in the first row to be "xxxxxxxx" and not "Initial Value we Want". It appears that the "$strValue.wrappedValue = tempStr" line in the gcRow initialiser is not working?
Question - how to correct so I can correctly pass the initial value for the child view to it, and it uses this correctly?
Playgrounds Code:
import SwiftUI
import PlaygroundSupport
struct gcRow : View {
#State var strValue : String = "xxxxxxxx"
init(tempStr : String) {
$strValue.wrappedValue = tempStr // <== DOESN'T SEEM TO WORK
}
var body : some View {
HStack {
Text(strValue)
}
}
}
struct GCParentView: View {
var body: some View {
VStack {
List {
gcRow(tempStr: "Initial Value we Want")
}
}
}
}
let gcParentView = GCParentView()
PlaygroundPage.current.liveView = UIHostingController(rootView: gcParentView)
Image/Snapshop of what I see after Startup:
In swiftUI its not allowed to change #State variables in the initializer. The correct way is to remove the default value and initialize it inside the initializer.
Fixed Playground Code
import SwiftUI
import PlaygroundSupport
struct gcRow : View {
#State var strValue: String
init(tempStr: String) {
_strValue = State(initialValue: tempStr)
}
var body : some View {
HStack {
Text(strValue)
}
}
}
struct GCParentView: View {
var body: some View {
VStack {
List {
gcRow(tempStr: "Initial Value we Want")
}
}
}
}
let gcParentView = GCParentView()
PlaygroundPage.current.liveView = UIHostingController(rootView: gcParentView)
you have to use this:
init(tempStr: String) {
_strValue = State(initialValue: tempStr)
}