How to access content view's elements later in SwiftUI? - swift

Let's say that I have a content view like this one:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
CustomClass()
.tabItem {
VStack {
Image("First")
Text("First")
}
}
.tag(0)
Button(action: { EmployeeStorage.sharedInstance.reload() }) {
Text("Reload")
}
.tabItem {
VStack {
Image("Second")
Text("Second")
}
}
.tag(1)
}
}
}
// MARK: - SomeDelegateThatUpdatesMeLater
extension ContentView: SomeDelegateThatUpdatesMeLater {
func callback() {
// Here I want to update my content view's subviews
// The ContentClass instance needs to be updated
}
}
Let's say that I want to listen to a callback method and then update the content view's tab number 1 (CustomClass) later on. How to access the content view's subviews? I'd need something like UIKit's subviewWithTag(_:). Is there any equivalent in SwiftUI?

You should probably rethink your approach. What exactly is it you want to happen? You have some external data model type that fetches (or updates) data and you want your views to react to that? If that's the case, create an ObservableObject and pass that to your CustomClass.
struct CustomClass: View {
#ObservedObject var model: Model
var body: some View {
// base your view on your observed-object
}
}
Perhaps you want to be notified of events that originate from CustomClass?
struct CustomClass: View {
var onButtonPress: () -> Void = { }
var body: some View {
Button("Press me") { self.onButtonPress() }
}
}
struct ParentView: View {
var body: some View {
CustomClass(onButtonPress: { /* react to the press here */ })
}
}
Lastly, if you truly want some kind of tag on your views, you can leverage the Preferences system in SwiftUI. This is a more complicated topic so I will just point out what I have found to be a great resource here:
https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

Related

How to update one view from within another in SwiftUI

I am working on a SwiftUI view where I have it populates other subviews within the main view. My question is how can I call something in the main view from within a sub view?
Here is what the code looks like for my main view:
struct MovieList: View {
#ObservedObject var viewModel = MovieViewModel()
var body: some View {
VStack {
ScrollView(.vertical) {
VStack {
ForEach(self.viewModel.movie) { movie in
MovieView(movie: movie)
}
}
}
}
.navigationBarTitle("Movies")
.onAppear {
self.viewModel.fetchMovies() // Fetch all movies and cause entire view to refresh and populate movies
}
}
}
This main view populates a list of movies by adding multiple MovieView instances.
Here is some example code for the MovieView:
struct MovieView: View {
let movie: Movie
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(“Movie Title: \(movie.title)”)
}.padding([.top, .leading, .bottom])
Button("Do not show this movie") {
// Update user prefers to hide the movie.
// But also somehow from within here call viewModel.fetchMovies() in the other view to refresh the movies list
}
}
}
}
So for example, from within one of the MovieView views, how can I have fetchMovies() from within the main view called so that everything gets updated?
Essentially a list of items is being populated and I would like for any one of these to have the ability to refresh/ perform some action on the entire main view
In terms of updating the Movie itself, as was pointed out in the comments, you probably want to pass a Binding to it. See the changes to the ForEach.
In terms of calling fetchMovies again, you can either pass the entire ObservableObject to the child view or just pass a reference to the function you need (which I've shown below):
struct Movie : Identifiable {
var id = UUID()
var title : String
var isHidden: Bool
}
class MovieViewModel : ObservableObject {
#Published var movies = [Movie]()
func fetchMovies() {
//fetch
}
}
struct MovieList: View {
#ObservedObject var viewModel = MovieViewModel()
var body: some View {
VStack {
ScrollView(.vertical) {
VStack {
ForEach($viewModel.movies) { $movie in
MovieView(movie: $movie, fetchMovies: viewModel.fetchMovies)
}
}
}
}
.navigationBarTitle("Movies")
.onAppear {
self.viewModel.fetchMovies()
}
}
}
struct MovieView: View {
#Binding var movie: Movie
var fetchMovies : () -> Void
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Movie Title: \(movie.title)")
}.padding([.top, .leading, .bottom])
Button("Do not show this movie") {
movie.isHidden = true
fetchMovies()
}
}
}
}

Is it possible to perform an action on NavigationLink tap?

I have a simple View showing a list of 3 items. When the user taps on an item, it navigates to the next view. This works fine. However, I would like to also perform an action (set a variable in a View Model) when a list item is tapped.
Is this possible? Here's the code:
import SwiftUI
struct SportSelectionView: View {
#EnvironmentObject var workoutSession: WorkoutManager
let sports = ["Swim", "Bike", "Run"]
var body: some View {
List(sports, id: \.self) { sport in
NavigationLink(destination: ContentView().environmentObject(workoutSession)) {
Text(sport)
}
}.onAppear() {
// Request HealthKit store authorization.
self.workoutSession.requestAuthorization()
}
}
}
struct DisciplineSelectionView_Previews: PreviewProvider {
static var previews: some View {
SportSelectionView().environmentObject(WorkoutManager())
}
}
The easiest way I've found to get around this issue is to add an .onAppear call to the destination view of the NavigationLink. Technically, the action will happen when the ContentView() appears and not when the NavigationLink is clicked.. but the difference will be milliseconds and probably irrelevant.
NavigationLink(destination:
ContentView()
.environmentObject(workoutSession)
.onAppear {
// add action here
}
)
Here's a solution that is a little different than the onAppear approach. By creating your own Binding for isActive in the NavigationLink, you can introduce a side effect when it's set. I've implemented it here all within the view, but I would probably do this in an ObservableObject if I were really putting it into practice:
struct ContentView: View {
#State var _navLinkActive = false
var navLinkBinding : Binding<Bool> {
Binding<Bool> { () -> Bool in
return _navLinkActive
} set: { (newValue) in
if newValue {
print("Side effect")
}
_navLinkActive = newValue
}
}
var body: some View {
NavigationView {
NavigationLink(
destination: Text("Dest"),
isActive: navLinkBinding,
label: {
Text("Navigate")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}

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 = ""
}
}

SwiftUI - Xcode - Inferring type not possible for VStack

I am trying to create a simple master/detail app in Xcode.
I want that the detail view is
struct EditingView: View
{
var body: some View {
var mainVertical: VStack = VStack() //error here
{
var previewArea: HStack = HStack()
{
var editorButton: Button = Button()
//the same with return editorButton
// I have to add other controls, like a WKWebView
}
return previewArea
//this is a simple version, layout will have other stacks with controls inside
}
return mainVertical
}
}
but I get
Generic parameter 'Content' could not be inferred
The IDE offers me to fix but if I do that, it writes a generic type I have to fill but then other errors come, f.i. if I put AnyView o TupleView.
I would like that it infers everything, what is wrong that it cannot understand?
In SwiftUI you usually don't need to reference your controls. You can apply modifiers to them directly in the view.
This is the preferred way:
struct ContentView: View {
var body: some View {
VStack {
HStack {
Button("Click me") {
// some action
}
}
}
.background(Color.red) // modify your `VStack`
}
}
Alternatively if needed you can extract controls as separate variables:
struct ContentView: View {
var body: some View {
let hstack = HStack {
button
}
return VStack {
hstack
}
}
var button: some View {
Button("Click me") {
// some action
}
}
}
But in the end I definitely recommend you read Apple SwiftUI tutorials

SwiftUI: Dismiss View Within macOS NavigationView

As detailed here (on an iOS topic), the following code can be used to make a SwiftUI View dismiss itself:
#Environment(\.presentationMode) var presentationMode
// ...
presentationMode.wrappedValue.dismiss()
However, this approach doesn't work for a native (not Catalyst) macOS NavigationView setup (such as the below), where the selected view is displayed alongside the List.
Ideally, when any of these sub-views use the above, the list would go back to having nothing selected (like when it first launched); however, the dismiss function appears to do nothing: the view remains exactly the same.
Is this a bug, or expected macOS behaviour?
Is there another approach that can be used instead?
struct HelpView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination:
AboutAppView()
) {
Text("About this App")
}
NavigationLink(destination:
Text("Here’s a User Guide")
) {
Text("User Guide")
}
}
}
}
}
struct AboutAppView: View {
#Environment(\.presentationMode) var presentationMode
public var body: some View {
Button(action: {
self.dismissSelf()
}) {
Text("Dismiss Me!")
}
}
private func dismissSelf() {
presentationMode.wrappedValue.dismiss()
}
}
FYI: The real intent is for less direct scenarios (such as triggering from an Alert upon completion of a task); the button setup here is just for simplicity.
The solution here is simple. Do not use Navigation View where you need to dismiss the view.
Check the example given by Apple https://developer.apple.com/tutorials/swiftui/creating-a-macos-app
If you need dismissable view, there is 2 way.
Create a new modal window (This is more complicated)
Use sheet.
Following is implimenation fo sheet in macOS with SwiftUI
struct HelpView: View {
#State private var showModal = false
var body: some View {
NavigationView {
List {
NavigationLink(destination:
VStack {
Button("About"){ self.showModal.toggle() }
Text("Here’s a User Guide")
}
) {
Text("User Guide")
}
}
}
.sheet(isPresented: $showModal) {
AboutAppView(showModal: self.$showModal)
}
}
}
struct AboutAppView: View {
#Binding var showModal: Bool
public var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Dismiss Me!")
}
}
}
There is also a 3rd option to use ZStack to create a Modal Card in RootView and change opacity to show and hide with dynamic data.