SwiftUI access passed view data in sctruct header - view

How can I pass data to a view and use it directly in the "header"? All tutorials I made are accessing the data in the view body - which works fine - but I want to call a graphlql method from the UpdateAccountView and than render a view based on the result.
My class for passing data:
class Account {
var tel: Int
init(tel: Int) {
self.tel = tel
}
}
My main view where the class is initialised (simplified - normally the "tel" will come from an input)
struct ContentView: View {
var account: Account = Account(tel: 123)
var body: some View {
NavigationView {
NavigationLink(
destination: UpdateAccountView(account: account),
label: {
Text("Navigate")
})
}
}
}
The view I call to do the request and call the next view based on the result
UpdateAccount is taking tel:Int as a parameter.
And here is the problem. I cannot access account.tel from the passed data.
struct UpdateAccountView: View {
var account: Account
#ObservedObject private var updateAccount: UpdateAccount = UpdateAccount(tel: account.tel)
#ViewBuilder
var body: some View {
if updateAccount.success {
AccountVerifyView()
} else {
ContentView()
}
}
}
The error:
Cannot use instance member 'account' within property initializer; property initializers run before 'self' is available
Update method (GraphQL):
class UpdateAccount: ObservableObject {
#Published var success: Bool
init(tel: Int){
self.success = false
update(tel: tel)
}
func update(tel: Int){
Network.shared.apollo.perform(mutation: UpdateAccountMutation(tel: tel)) { result in
switch result {
case .success(let graphQLResult):
self.success = graphQLResult.data!.updateAccount.success
case .failure(let error):
print("Failure! Error: \(error)")
self.success = false
}
}
}
I saw that there is an EnvironmentObject but than the variable become available globally as far as I understood, which is not necessary here.
Thank you for your help.

You can make it in explicit init, like
struct UpdateAccountView: View {
var account: Account
#ObservedObject private var updateAccount: UpdateAccount // << declare
init(account: Account) {
self.account = account
self.updateAccount = UpdateAccount(tel: account.tel) // << here !!
}
// ... other code
}

Related

Swiftui TextEditor seems not to respond to state changes

I am having trouble getting the SwiftUI TextEditor to work when it is in a Child View.
This is a small example that demonstrates the issue for me:
import SwiftUI
struct ContentView: View {
#State private var someText: String = "Hello World"
var body: some View {
VStack {
HStack {
Button("Text 1", action: {someText = "hello"})
Button("Text 2", action: {someText = "world"})
}
ViewWithEditor(entry: $someText)
}
}
}
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
When I click the buttons I expected the Editor text to change, however it remains with its initial value.
The print statement show that the "localString" variable is being updated.
Is TextEditor broken or am I missing something fundamental ??
If you move the buttons into the same view as the TextEditor, directly changing local state var it works as expected.
This is being run under MacOS in case it makes a difference.
TIA Alan.
Ok. So a proxy Binding does the job for me. See updated editor view below:
struct ViewWithEditor: View {
#Binding var entry: String
var body: some View
{
let localString = Binding<String> (
get: {
entry
},
set: {
entry = $0
}
)
return VStack {
Text(entry)
TextEditor(text: localString)
}
}
A bit more ugly (proxy bindings just seem clutter), but in some ways simpler..
It allows for the result of the edit to be reviewed / rejected before being pushed into the bound var.
This is occurring because the binding entry var is not actually being used after initialization of ViewWithEditor. In order to make this work without using the proxy binding add onChange to the ViewWithEditor as below:
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
The problem here is that now entry is not updating if localString changes. One could just use the same approach as before:
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
.onChange(of: localString) {
entry = $0
}
}
but why not just use $entry as the binding string for TextEditor?

SwiftUI Button compiler bug or mine?

I have the following code:
import SwiftUI
enum OptionButtonRole {
case normal
case destructive
case cancel
}
struct OptionButton {
var title: String
var role: OptionButtonRole
let id = UUID()
var action: () -> Void
}
struct OptionSheet: ViewModifier {
#State var isPresented = true
var title: String
var buttons: [OptionButton]
init(title: String, buttons: OptionButton...) {
self.title = title
self.buttons = buttons
}
func body(content: Content) -> some View {
content
.confirmationDialog(title,
isPresented: $isPresented,
titleVisibility: .visible) {
ForEach(buttons, id: \.title) { button in
let role: ButtonRole? = button.role == .normal ? nil : button.role == .destructive ? .destructive : .cancel
Button(button.title, role: role, action: button.action)
}
}
}
}
It builds and my app shows the option sheet with the specified buttons.
However, if I use an alternative Button.init, i.e. if I replace the body with the following code:
func body(content: Content) -> some View {
content
.confirmationDialog(title,
isPresented: $isPresented,
titleVisibility: .visible) {
ForEach(buttons, id: \.title) { button in
let role: ButtonRole? = button.role == .normal ? nil : button.role == .destructive ? .destructive : .cancel
Button(role: role, action: button.action) {
Text(button.title)
}
}
}
}
Then, Xcode hangs on build with the following activity:
Is there an error in my code or is this a compiler bug (Xcode Version 14.1 (14B47b))?
While your code is technically correct, the ability of view logic to evaluate variable values can get quite compiler-intensive, especially when you have multiple chained ternaries and logic inside a ForEach (which seems to make a bigger difference than one would probably think).
I'd be tempted to move the conditional logic outside the loop altogether, so that you're calling a method rather than needing to evaluate and store a local variable. You could make this a private func in your view, or as an extension to your custom enum. For example:
extension OptionButtonRole {
var buttonRole: ButtonRole? {
switch self {
case .destructive: return .destructive
case .cancel: return .cancel
default: return nil
}
}
}
// in your view
ForEach(buttons, id: \.title) { button in
Button(role: button.role.buttonRole, action: button.action) {
Text(button.title)
}
}

How to link child View state to menus using FocusedValues

SwiftUI newbie here. 👋 I'm struggling to understand how #FocusedBinding and FocusedValues work when building menus for a MacOS app. I'm trying to build the Apple HIG UI pattern with buttons in the window toolbar for changing the list view, and matching menu items in the View menu. Much like Finder windows have the four different view modes.
I have gone through the Apple's Landmarks tutorial, the Frameworks Engineer's example code in Apple dev forum, and Majid's tutorial.
The Apple documentation says FocusedValues is "a collection of state exported by the focused view and its ancestors." I assume the collection is global and I can set a focusedValue in any child View, and read or bind to any of the FocusedValues from anywhere in my code.
Therefore I don't understand why my first example below works, but the second one doesn't?
This works:
import SwiftUI
#main
struct TestiApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.frame(minWidth: 200, minHeight: 300)
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}
.windowStyle(HiddenTitleBarWindowStyle())
.commands {
MenuCommands()
}
}
}
struct ContentView: View {
// In the working version of the code selectedView is defined here
// in the ContentView, which is a direct child of the WindowGroup
// that has the .commands modifier.
#State private var selectedView: Int = 0
// For demonstration purposes I have simplified the authorization
// code to a hardcoded boolean.
private var isAuthorized: Bool = true
var body: some View {
switch isAuthorized {
case true:
// focusedValue is set here to the selectedView binding.
// I don't really understand why do it here, but it works.
AuthorizedView(selectedView: $selectedView)
.focusedValue(\.selectedViewBinding, $selectedView)
default:
NotYetAuthorizedView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AuthorizedView: View {
// selectedView is passed to this view as an argument and
// bound from ContentView.
#Binding var selectedView: Int
var body: some View {
List {
Text("Something here")
Text("Something more")
Text("Even more")
}
.toolbar {
// The Picker element sets and gets the bound selectedView value
Picker("View", selection: $selectedView) {
Text("View 1").tag(0)
Text("View 2").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
struct NotYetAuthorizedView: View {
var body: some View {
VStack {
Text("You need permission to access this 😭")
.padding()
}
}
}
struct MenuCommands: Commands {
private struct MenuContent: View {
// Command menu binds the selectedView value through focusedValues.
// MenuContent is a View, because otherwise the binding doesn't
// work (I read there's a bug in SwiftUI...).
#FocusedBinding(\.selectedViewBinding) var selectedView: Int?
var body: some View {
Button("View 1") {
selectedView = 0
}
.keyboardShortcut("1")
Button("View 2") {
selectedView = 1
}
.keyboardShortcut("2")
}
}
var body: some Commands {
CommandGroup(before: .toolbar) {
MenuContent()
}
}
}
struct SelectedViewBinding: FocusedValueKey {
typealias Value = Binding<Int>
}
extension FocusedValues {
var selectedViewBinding: SelectedViewBinding.Value? {
get { self[SelectedViewBinding.self] }
set { self[SelectedViewBinding.self] = newValue }
}
}
But if I make the following changes to ContentView and AuthorizedView, the project compiles fine but the binding between selectedView and the command menus no longer works:
struct ContentView: View {
// selectedView definition has been removed from ContentView
// and moved to AuthorizedView.
private var isAuthorized: Bool = true
var body: some View {
switch isAuthorized {
case true:
AuthorizedView()
// Also setting the focusedValue here has been removed
default:
NotYetAuthorizedView()
}
}
}
struct AuthorizedView: View {
// Moved selectedView definition here
#State private var selectedView: Int = 0
var body: some View {
List {
Text("Something here")
Text("Something more")
Text("Even more")
}
.toolbar {
Picker("View", selection: $selectedView) {
Text("View 1").tag(0)
Text("View 2").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.focusedValue(\.selectedViewBinding, $selectedView)
// I am now setting the focusedValue here, which seems
// more logical to me...
}
}
}
I like the second example better, because the selectedView state is encapsulated in the AuthorizedView where it belongs.

Display subview from an array in SwiftUI

I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.

No ObservableObject of type User found

I am trying to understand #EnvironmentObject better so I wrote sample code below to replicate the issue i am facing
This is the class where i declare the array which needs to be accessed in multiple locations and be displayed and updated in ContentView
class User: ObservableObject {
#Published var array = [String]()
func diplayName(name: String){
self.array.append(name)
}
}
I want to be able to append my array in another class. Something like the below code
class myTests: ObservableObject {
#EnvironmentObject var user:User
func diplayMyName(name: String){
self.user.array.append(name)
}
}
When I call displayMyName function in myTests class i get an Error message as below
Fatal error: No ObservableObject of type User found.
A View.environmentObject(_:) for User may be missing as an ancestor of this view.
This is how my contentView looks like
struct ContentView: View {
#EnvironmentObject var user:User
var testing = myTests()
var body: some View {
VStack {
List(user.array, id: \.self){ x in
Text(x)
}
Button(action: {
self.user.diplayName(name: "Name1")
// self.testing.diplayMyName(name: "Name2")
}){
Text("Call Function")
}
}
}
}
This is how i declare my environment object in scene delegate
let contentView = ContentView().environmentObject(User())
I would really appreciate if someone can help me understand why am i getting the error when i append the published array from myTests class. Thank you.
UPDATE
To work around my issue i did the following adjustments
I returned an array in myTests class
class myTests {
var ar = [String]()
func displayMyName() -> [String] {
ar.removeAll()
ar.append(contentsOf: ["Name2", "Name3"])
return ar
}
}
And added it to the array in ContentView
struct ContentView: View {
#EnvironmentObject var user : User
var testing = myTests()
var body: some View {
VStack {
List(user.array, id: \.self){ x in
Text(x)
}
Button(action: {
self.user.array.append(contentsOf: self.testing.displayMyName())
}){
Text("Call Function")
}
}
}
}

Resources