Passing data from extension in SwiftUI - swift

I am building a complex interface in SwiftUI that I need to break into multiple extensions in order to be able to compile the code, but I can't figure out how to pass data between the extension and the body structure.
I made a simple code to explain it :
class Search: ObservableObject {
#Published var angle: Int = 10
}
struct ContentView: View {
#ObservedObject static var search = Search()
var body: some View {
VStack {
Text("\(ContentView.self.search.angle)")
aTest()
}
}
}
extension ContentView {
struct aTest: View {
var body: some View {
ZStack {
Button(action: { ContentView.search.angle = 11}) { Text("Button")}
}
}
}
}
When I press the button the text does not update, which is my issue. I really appreciate any help you can provide.

You can try the following:
struct ContentView: View {
#ObservedObject var search = Search()
var body: some View {
VStack {
Text("\(ContentView.self.search.angle)")
aTest // call as a computed property
}
}
}
extension ContentView {
var aTest: some View { // not a separate `struct` anymore
ZStack {
Button(action: { self.search.angle = 11 }) { Text("Button")}
}
}
}

Related

Is there an alternative way of displaying an image depends on my input?

I want to show the level of connectivity of the wifi depends on my input. I used custom wifi image in sfsymbol. The code result is not really good, so I am looking for a better way. Sorry about this if it's something that not possible in Xcode.
//please look my image atttachment
import SwiftUI
class ViewMode: ObservableObject {
#Published var rece = ""
}
struct MyApp: View {
#State private var passConnection: String = ""
var body: some View {
MyConnectionView(incomingConnection: $passConnection)
}
}
struct MyConnectionView: View {
#Binding var incomingConnection: String
var body: some View {
VStack {
VStack {
if incomingConnection == "low" {
CustomWifi1
}
else if incomingConnection == "medium" {
CustomWifi2
}
else {
CustomWifi3
}
}
}
}
var CustomWifi1: some View {
Image("custom1")
}
var CustomWifi2: some View {
Image("custom2")
}
var CustomWifi3: some View {
Image("custom3")
}
var CustomWifi4: some View {
Image("custom4")
}
var CustomWifi5: some View {
Image("custom5")
}
}
You can display a different level of connection with SwiftUI and the feature of SF Symbols by using "variableValue" modifier of the "Image".
Therefore, you don't have to create multiple customized wifi images for your app. Also, you should have included #swiftui tag in your question.
Try this code below:
import SwiftUI
struct TestView: View {
#State var connectionValue = 0.0
var body: some View {
VStack {
Slider(value: $connectionValue)
Image(systemName: "wifi", variableValue: connectionValue)
}
}
}

I'm trying to implement a view stack in swiftui and my #State objects are being reset for reasons that are unclear to me

I'm new to swiftui and doing an experiment with pushing and popping views with a stack. When I pop a view off the stack, the #State variable of the prior view has been reset and I don't understand why.
This demo code was tested on macos.
import SwiftUI
typealias Push = (AnyView) -> ()
typealias Pop = () -> ()
struct PushKey: EnvironmentKey {
static let defaultValue: Push = { _ in }
}
struct PopKey: EnvironmentKey {
static let defaultValue: Pop = {() in }
}
extension EnvironmentValues {
var push: Push {
get { self[PushKey.self] }
set { self[PushKey.self] = newValue }
}
var pop: Pop {
get { self[PopKey.self] }
set { self[PopKey.self] = newValue }
}
}
struct ContentView: View {
#State private var stack: [AnyView]
var body: some View {
currentView()
.environment(\.push, push)
.environment(\.pop, pop)
.frame(width: 600.0, height: 400.0)
}
public init() {
_stack = State(initialValue: [AnyView(AAA())])
}
private func currentView() -> AnyView {
if stack.count == 0 {
return AnyView(Text("stack empty"))
}
return stack.last!
}
public func push(_ content: AnyView) {
stack.append(content)
}
public func pop() {
stack.removeLast()
}
}
struct AAA : View {
#State private var data = "default text"
#Environment(\.push) var push
var body: some View {
VStack {
TextEditor(text: $data)
Button("Push") {
self.push(AnyView(BBB()))
}
}
}
}
struct BBB : View {
#Environment(\.pop) var pop
var body: some View {
VStack {
Button("Pop") {
self.pop()
}
}
}
}
If I type some text into the editor then hit Push, then Pop out of that view, I was expecting the text editor to maintain my changes but it reverts to the default text.
What am I missing?
Edit:
I guess this is really a question of how are NavigationView and NavigationLink implemented. This simple code does the what I'm trying to do:
import SwiftUI
struct MyView: View {
#State var text = "default text"
var body: some View {
VStack {
TextEditor(text: $text)
NavigationLink(destination: MyView()) {
Text("Push")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyView()
}
}
}
run that on iOS so you get a nav stack. edit the text, then push. Edit again if you want, then go back and see state is retained.
My code is trying to do the same thing in principle.
I'll share this attempt maybe it will help you create your version of this.
This all started with an attempt to create something like NavigationView and NavigationLink but being able to back track to a random View in the stack
I have a protocol where an object returns a View. Usually it is an enum. The view() references a View with a switch that provides the correct child View. The ContentView/MainView works almost like a storyboard and just presents whatever is designated in the current or path variables.
//To make the View options generic
protocol ViewOptionsProtocol: Equatable {
associatedtype V = View
#ViewBuilder func view() -> V
}
This is the basic navigation router that keep track of the main view and the NavigationLink/path. Which looks similar to what you want to do.
//A generic Navigation Router
class ViewNavigationRouter<T: ViewOptionsProtocol>: ObservableObject{
//MARK: Variables
var home: T
//Keep track of your current screen
#Published private (set) var current: T
//Keep track of the path
#Published private (set) var path: [T] = []
//MARK: init
init(home: T, current: T){
self.home = home
self.current = current
}
//MARK: Functions
//Control how you get to the screen
///Navigates to the nextScreen adding to the path/cookie crumb
func push(nextScreen: T){
//This is a basic setup just going forward
path.append(nextScreen)
}
///Goes back one step in the path/cookie crumb
func pop(){
//Use the stored path to go back
_ = path.popLast()
}
///clears the path/cookie crumb and goes to the home screen
func goHome(){
path.removeAll()
current = home
}
///Clears the path/cookie crumb array
///sets the current View to the desired screen
func show(nextScreen: T){
goHome()
current = nextScreen
}
///Searches in the path/cookie crumb for the desired View in the latest position
///Removes the later Views
///sets the nextScreen
func dismissTo(nextScreen: T){
while !path.isEmpty && path.last != nextScreen{
pop()
}
if path.isEmpty{
show(nextScreen: nextScreen)
}
}
}
It isn't an #Environment but it can easily be an #EnvrionmentObject and all the views have to be in the enum so the views are not completely unknown but it is the only way I have been able to circumvent AnyView and keep views in an #ViewBuilder.
I use something like this as the main portion in the main view body
router.path.last?.view() ?? router.current.view()
Here is a simple implementation of your sample
import SwiftUI
class MyViewModel: ViewNavigationRouter<MyViewModel.ViewOptions> {
//In some view router concepts the data that is /preserved/shared among the views is preserved in the router itself.
#Published var preservedData: String = "preserved"
init(){
super.init(home: .aaa ,current: .aaa)
}
enum ViewOptions: String, ViewOptionsProtocol, CaseIterable{
case aaa
case bbb
#ViewBuilder func view() -> some View{
ViewOptionsView(option: self)
}
}
struct ViewOptionsView: View{
let option: ViewOptions
var body: some View{
switch option {
case .aaa:
AAA()
case .bbb:
BBB()
}
}
}
}
struct MyView: View {
#StateObject var router: MyViewModel = .init()
var body: some View {
NavigationView{
ScrollView {
router.path.last?.view() ?? router.current.view()
}
.toolbar(content: {
//Custom back button
ToolbarItem(placement: .navigationBarLeading, content: {
if !router.path.isEmpty {
Button(action: {
router.pop()
}, label: {
HStack(alignment: .center, spacing: 2, content: {
Image(systemName: "chevron.backward")
if router.path.count >= 2{
Text(router.path[router.path.count - 2].rawValue)
}else{
Text(router.current.rawValue)
}
})
})
}
})
})
.navigationTitle(router.path.last?.rawValue ?? router.current.rawValue)
}.environmentObject(router)
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
struct AAA : View {
//This will reset because the view is cosmetic. the data needs to be preserved somehow via either persistence or in the router for sharing with other views.
#State private var data = "default text"
#EnvironmentObject var vm: MyViewModel
var body: some View {
VStack {
TextEditor(text: $data)
TextEditor(text: $vm.preservedData)
Button("Push") {
vm.push(nextScreen: .bbb)
}
}
}
}
struct BBB : View {
#EnvironmentObject var vm: MyViewModel
var body: some View {
VStack {
Button("Pop") {
vm.pop()
}
}
}
}

Why doesn't calling method of child view from parent view update the child view?

I'm trying to call a method of a child view which includes clearing some of its fields. When the method is called from a parent view, nothing happens. However, calling the method from the child view will clear its field. Here is some example code:
struct ChildView: View {
#State var response = ""
var body: some View {
TextField("", text: $response)
}
func clear() {
self.response = ""
}
}
struct ParentView: View {
private var child = ChildView()
var body: some View {
HStack {
self.child
Button(action: {
self.child.clear()
}) {
Text("Clear")
}
}
}
}
Can someone tell me why this happens and how to fix it/work around it? I can't directly access the child view's response because there are too many fields in my actual code and that would clutter it up too much.
SwiftUI view is not a reference-type, you cannot create it once, store in var, and then access it - SwiftUI view is a struct, value type, so storing it like did you work with copies it values, ie
struct ParentView: View {
private var child = ChildView() // << original value
var body: some View {
HStack {
self.child // created copy 1
Button(action: {
self.child.clear() // created copy 2
}) {
Here is a correct SwiftUI approach to construct parent/child view - everything about child view should be inside child view or injected in it via init arguments:
struct ChildView: View {
#State private var response = ""
var body: some View {
HStack {
TextField("", text: $response)
Button(action: {
self.clear()
}) {
Text("Clear")
}
}
}
func clear() {
self.response = ""
}
}
struct ParentView: View {
var body: some View {
ChildView()
}
}
Try using #Binding instead of #State. Bindings are a way of communicating state changes down to children.
Think of it this way: #State variables are used for View specific state. They are usually made private for this reason. If you need to communicate anything down, then #Binding is the way to do it.
struct ChildView: View {
#Binding var response: String
var body: some View {
TextField("", text: $response)
}
}
struct ParentView: View {
#State private var response = ""
var body: some View {
HStack {
ChildView(response: $response)
Button(action: {
self.clear()
}) {
Text("Clear")
}
}
}
private func clear() {
self.response = ""
}
}

How do I switch views with swiftUI MacOS?

I found this question - What is the best way to switch views in SwiftUI? - but I have not been able to get the answer to work for me.
struct view4x: View {
#State var goView: Bool = false
var body: some View {
if goView {
view5x(goView1: self.$goView)
} else {
Form {
/* ... */
}
}
}
}
and the button is inside the form:
Button(action: {
self.goView.toggle()
}) {
Text("Catalog")
}
and for my other view I have:
struct view5x: View {
#Binding var goView1: Bool
var body: some View {
Text("TEST")
Button(action: {
self.goView1.toggle()
}) {
Text("Return")
}
}
}
I just get errors that both bodies declare an opaque return type. It does not preview.
Ok, here are similar mistakes in your views. To understand them better to look at View protocol:
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
associatedtype Body : View
/// Declares the content and behavior of this view.
var body: Self.Body { get }
}
so, body is just a computed variable and it should return some View. Mistake in your view5x is that you put into it 2 different views instead 1. The solution here is to embed them into VStack for example:
struct view5x: View{
#Binding var goView1: Bool
var body: some View{
VStack {
Text("TEST")
Button(action: {
self.goView1.toggle()
}) {
Text("Return")
}
}
}
}
The problem it the view4x is similar - it's unclear what view returns body because of if...else statements, I think. You can fix it in the same way:
struct view4x: View {
#State var goView: Bool = false
var body: some View {
VStack {
if goView {
view5x(goView1: $goView)
} else {
Button(action: {
self.goView.toggle()
}) {
Text("Catalog")
}
}
}
}
}
The other way is to say what view should body return if you wrap each of them into AnyView and type return before. In this example changes of goView don't switch views, but you can see the other syntax:
struct view4x: View {
#State var goView: Bool = false
var body: some View {
if goView {
return AnyView(view5x(goView1: $goView))
} else {
return AnyView(Button(action: {
self.goView.toggle()
}) {
Text("Catalog")
})
}
}
}

SwiftUI - Using 'ObservableObject' and #EnvironmentObject to Conditionally Display View

I would like to conditionally display different views in my application - if a certain boolean is true, one view will be displayed. If it is false, a different view will be displayed. This boolean is within an ObservableObject class, and is changed from one of the views that will be displayed.
PracticeStatus.swift (the parent view)
import Foundation
import SwiftUI
import Combine
class PracticeStatus: ObservableObject {
#Published var showResults:Bool = false
}
PracticeView.swift (the parent view)
struct PracticeView: View {
#EnvironmentObject var practiceStatus: PracticeStatus
var body: some View {
VStack {
if practiceStatus.showResults {
ResultView()
} else {
QuestionView().environmentObject(PracticeStatus())
}
}
}
}
QuestionView.swift
struct QuestionView: View {
#EnvironmentObject var practiceStatus: PracticeStatus
var body: some View {
VStack {
...
Button(action: {
self.practiceStatus.showResults = true
}) { ... }
...
}
}
}
However, this code doesn't work. When the button within QuestionView is pressed, ResultView is not displayed. Does anybody have a solution? Thanks!
Have you tried to compile your code? There are several basic errors:
Variable practice does not exist in PracticeView. Did you mean practiceStatus?
Variable userData does not exist in QuestionView. Did you mean practiceStatus?
You are calling PracticeView from inside PracticeView! You'll definetely get a stack overflow ;-) Didn't you mean QuestionView?
Below is a working code:
import Foundation
import SwiftUI
import Combine
class PracticeStatus: ObservableObject {
#Published var showResults:Bool = false
}
struct ContentView: View {
#State private var flag = false
var body: some View {
PracticeView().environmentObject(PracticeStatus())
}
}
struct PracticeView: View {
#EnvironmentObject var practiceStatus: PracticeStatus
var body: some View {
VStack {
if practiceStatus.showResults {
ResultView()
} else {
QuestionView()
}
}
}
}
struct QuestionView: View {
#EnvironmentObject var practiceStatus: PracticeStatus
var body: some View {
VStack {
Button(action: {
self.practiceStatus.showResults = true
}) {
Text("button")
}
}
}
}
struct ResultView: View {
#State private var flag = false
var body: some View {
Text("RESULTS")
}
}