Swift/SwiftUI Pass different functions into a view - swift

I am trying to pass in different functions into a custom styling View for a button and a NavigationLink.
struct commonButtonClass: View {
let buttonText: String
var body: some View {
Button(action: {
// a different function in each instance. it can be for example the login or register function passed in
}
}, label: {
Text(buttonText)
.font(.system(size:30))
})
}
}
However I can't find a way to pass through a function into the View. I was trying the CGFunction assignment for a variable and then passing in login() or register() in the call for this View but it does not work.
I tried a similar approach with the destination for the custom styling of NavigationLink but assigning View nor String allowed me to set it as the next destination like LoginView() or LandingView().
struct commonNavigationLinkClass: View {
let target: String
// the desired destination
let linkText: String
var body: some View {
NavigationLink(destination: View(target), label: {
Text(linkText)
.font(.system(size:30))
})
}
}
For the button, I tried using a CGFunction class for passing the function into the button but it gave me errors that the class and contents are not compatible.
For the NavigationLink, I tried using a String with a View(//string here) but it did not work. Nor did setting the target as a class of any View.
Maybe there is a better way to go about this without passing the contents down but I'm not quite sure how to achieve the styling for all these types of button and NavigationLink otherwise. there's other styling than just font but it's just colour and borders so I removed it for simplicity sake

You need to take a closure as a parameter…
struct CommonButtonView: View {
let buttonText: String
let action: () -> Void
var body: some View {
Button(action: action, label: {
Text(buttonText)
.font(.system(size:30))
})
}
}
Then use like:
struct ContentView: View {
var body: some View {
CommonButtonView(buttonText: "Press me") {
// some action
}
}
}
But you're probably better off just using the regular Button and defining your own custom ButtonStyle

Related

Is it possible to override SwiftUI modifiers?

Knowing that with SwiftUI view modifiers, order matters - because each modifier is a part of a chain of modifiers, I was wondering if it was possible to reset/overwrite/override a modifier (or the whole chain?
Specifically, I'm wondering about Styles (groupBoxStyle, buttonStyle, etc). I have default styles that I want to use in 90% of my app, and a few pages will have slightly different styles for those widgets.
For example:
// Renders a button with the "light" style
Button("Hello world") {
}
.buttonStyle(LightButtonStyle())
.buttonStyle(DarkButtonStyle())
// Renders a button with the "dark" style
Button("Hello world") {
}
.buttonStyle(DarkButtonStyle())
.buttonStyle(LightButtonStyle())
In those cases, I would actually like the 2nd modifier to be used, but the 1st takes over and subsequent styles don't work.
Note: In my actual app, none of my use cases are this trivial - this is just the simplest proof of concept.
The workaround(s) I have are that I create separate LightButton and DarkButton views, but that feels very inelegant (and becomes a mess when I have 5-6 variants of each component).
Alternatively, I have a custom MyButton(myStyle: ButtonStyle = .myDefaultStyle), but since this is a forms app, there are about 50-60 locations where something like that needs to be updated (instead of applying a modifier at a top level and letting that cascade through).
Edit: I should note, where I can set a top-level style and let it cascade, that works very well and as expected (closer to the View, the modifier takes over). But, there are just some weird use cases where it would be nice to flip the script.
Generally, buttonStyle propagates to child views, so ideally you would only need to set your “house style” once on the root view of your app.
The well-known place where this fails to work is the presentation modifiers like .sheet, which do not propagate styles to the presented view hierarchy. So you will need to write your own versions of the presentation modifiers that re-apply your house style.
For example, here's a custom ButtonStyle:
struct HouseButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(20)
.background {
Capsule(style: .continuous)
.foregroundColor(.pink)
}
.saturation(configuration.isPressed ? 1 : 0.5)
}
}
And here's a cover for sheet that applies the custom button style to the presented content:
extension View {
func houseSheet<Content: View>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
#ViewBuilder content: #escaping () -> Content
) -> some View {
return sheet(isPresented: isPresented, onDismiss: onDismiss) {
content()
.buttonStyle(HouseButtonStyle())
}
}
}
We can test out whether a NavigationLink, a sheet, and a houseSheet propagate the button style:
struct ContentView: View {
#State var showingHouseSheet = false
#State var showingStandardSheet = false
var body: some View {
NavigationView {
VStack {
NavigationLink("Navigation Push") {
ContentView()
}
Button("Standard Sheet") {
showingStandardSheet = true
}
Button("House Sheet") {
showingHouseSheet = true
}
}
.sheet(isPresented: $showingStandardSheet) {
ContentView()
}
.houseSheet(isPresented: $showingHouseSheet) {
ContentView()
}
}
}
}
Here's the root view that applies the house button style at the highest level:
struct RootView: View {
var body: some View {
ContentView()
.buttonStyle(HouseButtonStyle())
}
}
If you play with this, you'll find that both NavigationLink and houseSheet propagate the button style to the presented content, but sheet does not.

How to pass data from a modal view list to parent view in SwiftUI?

I have (probably) an easy question related to SwiftUI state management.
A have a modal view with a simple list of buttons:
struct ExerciseList: View {
var body: some View {
List {
ForEach(1..<30) { _ in
Button("yoga") {
}
}
}
}
}
The parent view is this one:
struct SelectExerciseView: View {
#State private var showingSheet = false
#State private var exercise = "select exercise"
var body: some View {
Button(exercise) {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet){
ExerciseList()
}
}
}
How can I do to pass the selected button text from the list to the parent view ?
I'm thinking that I need a Binding variable inside the modal and use that, but not really sure how in this example.
At its most basic, you need the selected exercise in your parent view (SelectExerciseView) as a state variable. You then pass that in to the child view (the modal) via a binding. Assuming exercise as a string holds the variable you want to change:
.sheet(isPresented: $showingSheet) {
ExerciseList(exercise: $exercise)
}
Your modal then needs to have a #Binding reference.
struct ExerciseList: View {
#Binding var exercise: Exercise
var body: some View {
List {
ForEach(1..<30) { _ in
Button("yoga") {
exercise = "yoga"
}
}
}
}
}
Im not sure what you're asking...
Are you trying to show a "Detail View" from the modal.
Meaning theres the parent view -> Modal View -> Detail View
In your case it would be the SelectExerciseView -> ExerciseListView -> DetailView which shows the text of the button that was pressed on the previous view (can be any view you want)
If thats what you're trying to do I would use a NavigationLink instead of a button on the modal. The destination of the NavigationLink would be the detail view

SwiftUI - Button - How to pass a function (with parameters) request to parent from child

I already know how to call a parent function from child but what I should do if my parent function has a parameter? I can't figure it out...
Working code without parameters:
struct ChildView: View {
var function: () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text("Button")
})
}
}
struct ContentView: View {
var body: some View {
ChildView(function: { self.setViewBackToNil() })
}
func setViewBackToNil() {
print("I am the parent")
}
}
And now I want to add a String parameter to setViewBackToNil(myStringParameter: String)
Ok, I managed to solve it.
You can use #State variable in a parent, and pass it to your child. Then, after changing it in the child view, call function, that was passed from the parent (without parameters), and in the parent get your #State inside the function.

#EnvironmentObject in SwiftUI - can't toggle bool

I have the class below to keep the state of the hamburger menu if it is or not displayed
class Menu: ObservableObject {
#Published var isActive: Bool = false
}
I instantiate it in Scene Delegate as such
let contentView = ContentView().environmentObject(Menu())
Then in a simple view i am trying to toggle the isActive bool, however i get the error below
struct Button: View {
#EnvironmentObject var menuState: Menu
var body: some View {
VStack{
Button(action: {
self.menuState.isActive.toggle()
}) {
Text("A")
}
}
}
}
This is the error i get: Cannot invoke initializer for type 'Button' with an argument list of type '(action: #escaping () -> (), #escaping () -> Text)'
The issue is that you named your custom View as Button, which is also the name of the existing SwiftUI button.
Simply rename your struct to something else and your code will compile just fine.
Unrelated to your question, but there's no point in wrapping a single View in a VStack, your body can simply contain the Button.
struct MyButton: View {
#EnvironmentObject var menuState: Menu
var body: some View {
Button(action: {
self.menuState.isActive.toggle()
}) {
Text("A")
}
}
}
You created a custom view named Button which is in conflict with the native SwiftUI Button (because they have the same name).
Rename your Button view and it will be okey I think.

Value from #State variable does not change

I have created a View that provides a convinient save button and a save method. Both can then be used inside a parent view.
The idea is to provide these so that the navigation bar items can be customized, but keep the original implementation.
Inside the view there is one Textfield which is bound to a #State variable. If the save method is called from within the same view everthing works as expected. If the parent view calls the save method on the child view, the changes to the #State variable are not applied.
Is this a bug in SwiftUI, or am I am missing something? I've created a simple playbook implementation that demonstrates the issue.
Thank you for your help.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
// Create the child view to make the save button available inside this view
var child = Child()
var body: some View {
NavigationView {
NavigationLink(
destination: child.navigationBarItems(
// Set the trailing button to the one from the child view.
// This is required as this view might be inside a modal
// sheet, and we need to add the cancel button as a leading
// button:
// leading: self.cancelButton
trailing: child.saveButton
)
) {
Text("Open")
}
}
}
}
struct Child: View {
// Store the value from the textfield
#State private var value = "default"
// Make this button available inside this view, and inside the parent view.
// This makes sure the visibility of this button is always the same.
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
// Simple textfield to allow a string to change.
TextField("Value", text: $value)
// Just for the playground to change the value easily.
// Usually it would be chnaged through the keyboard input.
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
}
func save() {
// This always displays the default value of the state variable.
// Even after the Update button was used and the value did change inside
// the textfield.
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
I think a more SwiftUi way of doing it:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
var body: some View {
return NavigationView {
// tell the child view where to render it's navigation item
// Instead of configuring navigation items.
NavigationLink(destination: Child(navigationSide: .left)) {
Text("Open")
}
}
}
}
struct Child: View {
enum NavigationSide { case left, right }
// If you really want to encapsulate all state in this view then #State
// is a good choice.
// If the parent view needs to read it, too, #Binding would be your friend here
#State private var value: String = "default"
// no need for #State as it's never changed from here.
var navigationSide = NavigationSide.right
// wrap in AnyView here to make ternary in ui code easier readable.
var saveButton: AnyView {
AnyView(Button(action: save) {
Text("Save")
})
}
var emptyAnyView: AnyView { AnyView(EmptyView()) }
var body: some View {
VStack {
TextField("Value", text: $value)
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
.navigationBarItems(leading: navigationSide == .left ? saveButton : emptyAnyView,
trailing: navigationSide == .right ? saveButton : emptyAnyView)
}
func save() {
print("\(value)")
}
}
TextField will only update your value binding when the return button is pressed. To get text changes that occur during editing, set up an observed object on Child with didSet. This was the playground I altered used from your example.
struct ContentView: View {
var child = Child()
var body: some View {
NavigationView {
NavigationLink(
destination: child.navigationBarItems(
trailing: child.saveButton
)
) {
Text("Open")
}
}
}
}
class TextChanges: ObservableObject {
var completion: (() -> ())?
#Published var text = "default" {
didSet {
print(text)
}
}
}
struct Child: View {
#ObservedObject var textChanges = TextChanges()
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
TextField("Value", text: $textChanges.text).multilineTextAlignment(.center)
Button(action: {
print(self.textChanges.text)
}) {
Text("Update")
}
}
}
func save() {
print("\(textChanges.text)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
Inside Child: value is mutable because it's wrapped with #State.
Inside ContentView: child is immutable because it's not wrapped with #State.
Your issue can be fixed with this line: #State var child = Child()
Good luck.
Child view needs to keep its state as a #Binding. This works:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var v = "default"
var body: some View {
let child = Child(value: $v)
return NavigationView {
NavigationLink(
destination: child.navigationBarItems(trailing: child.saveButton)
) {
Text("Open")
}
}
}
}
struct Child: View {
#Binding var value: String
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
TextField("Value", text: $value)
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
}
func save() {
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
Based on this commend from #nine-stones (thank you!) I implemented a more SwiftUI way so solve my problem. It does not allow the customization of the navigation items as I planned, but that was not the problem that needed to be solved. I wanted to use the Child view in a navigation link, as well as inside a modal sheet. The problem was how to perform custom cancel actions. This is why I removed the button implementation and replaced it with a cancelAction closure. Now I can display the child view wherever and however I want.
One thing I still do not know why SwiftUI is not applying the child context to the button inside the saveButton method.
Still, here is the code, maybe it helps someone in the future.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
NavigationLink(
destination: Child(
// Instead of defining the buttons here, I send an optional
// cancel action to the child. This will make it possible
// to use the child view on navigation links, as well as in
// modal dialogs.
cancelAction: {
self.presentationMode.wrappedValue.dismiss()
}
)
) {
Text("Open")
}
}
}
}
struct Child: View {
// Store the value from the textfield
#State private var value = "default"
#Environment(\.presentationMode) var presentationMode
var cancelAction: (() -> Void)?
// Make this button available inside this view, and inside the parent view.
// This makes sure the visibility of this button is always the same.
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
// Simple textfield to allow a string to change.
TextField("Value", text: $value)
// Just for the playground to change the value easily.
// Usually it would be chnaged through the keyboard input.
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
.navigationBarItems(
leading: self.cancelAction != nil ? Button(action: self.cancelAction!, label: {
Text("Cancel")
}) : nil,
trailing: self.saveButton
)
}
func save() {
// This always displays the default value of the state variable.
// Even after the Update button was used and the value did change inside
// the textfield.
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())