Is there a way to pass functions into views as optional parameters? - swift

I am making an application in SwiftUI that involves answering yes or no questions. Because of this I have created a subview call YesOrNoView. This subview has two buttons, one labeled yes and the other labeled no. The view accepts a binding variable of question that returns true or false based on the users answer. It also accepts a binding variable of questionsAnswered and increments it whenever either button is pressed. This behavior works for most questions in the game, but for the final question I want it to execute custom logic to take the user to a different view. To do this I am trying to make the view accept a custom action/method that I can call from either buttons action logic. Ideally, the action would also be optional so that I don't have to pass it in the 99% percent of the time when I'm not using it.
How do I pass a function as an optional parameter into a view and then run that action when a button within said view is pressed?
I tried adding actions using
struct YesOrNoView<Content: Action>: View {
...
but it couldn't find action or Action within it's scope.
I also tried using
struct YesOrNoView<Content>: View {
But I got an error saying that the Content variables type could not be inferred.
Any help would be greatly appreciated.

It return clickAction back to the view
struct GenericButton<Title: StringProtocol>: View {
let title: Title
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
}.frame(width: 200, height: 40)
}
}
}
Usage:
GenericButton("Button") {
// Button tapped action
}

I managed to figure it out with help from #JoakimDanielson. The trick is to pass in an empty function as the default.
var myFunction: () -> Void = {}
Thanks for the help everyone.

Related

SwiftUI Animation Bindings

struct ContentView: View {
#State private var animationAmount = 1.0
var body: some View {
VStack
{
Stepper("Scale amount", value: $animationAmount.animation(.linear), in: 1...10)
Spacer()
Button("Tap Me")
{
animationAmount += 1
}
.padding(50)
.background(.red)
.foregroundColor(.white)
.clipShape(Circle())
.scaleEffect(animationAmount)
}
}
}
So I have a tiny question, here I made a Stepper view with value being some way two binding of a variable and then I called the .animation method on that binding which from what I understood, if any changes happen to that binding they simply get animated.
My question is, is it specifically only changes that relate to the binding value that get animated? Or if some other changes happen to this view but coincidentally they happened a bit before the binding changed would those changes get animated too?
And another super super tiny question, why is it exactly that I can't put an if statement in this VStack that will increment animationAmount?
like
if animationAmount > 1.0
{
animationAmount += 0.25
}
Just says that () doesn't conform to View.
By definition, .animation(_:value:):
Applies the given animation to this view when the specified value changes.
So yes, the animation applies only when the value you passed changes, see more: .animation(_:value:)
To answer your second question,VStack, HStack,... & body are ViewBuilders. Meaning they require you to pass in a View, whereas animationAmount += 0.25 is of type Void != View. So what you should do is use .onChange:
.onChange(of: animationAmount) { newValue in
if animationAmount > 1.0 {
animationAmount += 0.25
}
}
First question: .animation works by animating a specific component whenever the value of this component changes.
eg: foregroundColor(color.animation) meaning that if the value of variable color changes, animation will happen BUT only to this foregroundColor component, unless you give another component color.animation too. Any other changes in view/variable beside this variable color will not apply animation to this foregroundColor. Any change even a very small change of variable color will animate the foregroundColor.
There is another different type of animation called withAnimation.
eg: withAnimation { isViewed = true }, now all the views or modifiers that relate to this variable isViewed will be animated.
Second question: To make it easy to understand, you simply can’t make any calculation/logic process inside a SwiftUI View Bracket. An ifelse{} statement inside a VStack or var body expects Views, so you can’t make any calculation there because it is not a view. Any code inside this ifelse bracket {. . .} shall be views only.
However, an ifelse{} statement inside a Button Action or onTapGesture is an opposite story. You can make any calculation/logic process there except displaying views. To understand formally about these concepts you should explore more about the basic of SwiftUI.

How does this SwiftUI binding + state example manage to work without re-invoking body?

A coworker came up with the following SwiftUI example which looks like it works just as expected (you can enter some text and it gets mirrored below), but how it works is surprising to me!
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
TextField("Change the string", text: $text)
WrappedText(text: $text)
}
}
}
struct WrappedText: View {
#Binding var text: String
var body: some View {
Text(text)
}
}
My newbie mental model of SwiftUI led me to think that typing in the TextField would change the $text binding, which would in turn mutate the text #State var. This would then invalidate the ContentView, triggering a fresh invocation of body. But interestingly, that's not what happens! Setting a breakpoint in ContentView's body only gets hit once, while WrappedText's body gets run every time the binding changes. And yet, as far as I can tell, the text state really is changing.
So, what's going on here? Why doesn't SwiftUI re-invoke ContentView's body on every change to text?
On State change SwiftUI rendering engine at first checks for equality of views inside body and, if some of them not equal, calls body to rebuild, but only those non-equal views. In your case no one view depends (as value) on text value (Binding is like a reference - it is the same), so nothing to rebuild at this level. But inside WrappedText it is detected that Text with new text is not equal to one with old text, so body of WrappedText is called to re-render this part.
This is declared rendering optimisation of SwiftUI - by checking & validating exact changed view by equality.
By default this mechanism works by View struct properties, but we can be involved in it by confirming our view to Eqatable protocol and marking it .equatable() modifier to give some more complicated logic for detecting if View should be (or not be) re-rendered.

Pass image name to custom Image subclass in SwiftUI

I am trying to accomplish something that looks pretty simple: I have a custom Image subclass in SwiftUI which I’m using for convenience, and I would like to use it in my app. My end goal is to use the OnboardingThumbnailImage in other views and simply pass an image name to the constructor.
import SwiftUI
struct OnboardingThumbnailImage: View {
var imageName: String
var body: some View {
Image(imageName)
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundColor(Colors.tintColor)
}
}
struct OnboardingThumbnailImage_Previews: PreviewProvider {
static var previews: some View {
OnboardingThumbnailImage(imageName: "?????")
}
}
How can I accomplish this? The compiler requires me to specify a value inside OnboardingThumbnailImage_Previews so I have no clue. I have looked into Bindings but I don't need a 'two-way street' between the views, so I'm not sure.
Can I instead just perhaps leave Image() with no arguments inside, in order to inherit the default Image constructor? If I leave Image() I get an error: Cannot invoke initializer for type 'Image' with no arguments.
This is SwiftUI's way of asking you
What image do you want to show when this View is previewed?
You can only preview SwiftUI views on macOS Catalina, so if you are not using Catalina (like me), then this feature is not very relevant to you.
You are supposed to put the image that you want to see in the previews in the ???? bit. If you are not using Catalina, or you just don't want to preview it, you can just delete the whole OnboardingThumbnailImage_Previews struct.
Also note that you can't "subclass" another view in SwiftUI. All you can do is composition, which is what you have done here. SwiftUI's design favours composition over inheritance. You can find explanations of this concept in these pages: 1, 2.
In your struct view, the variable imageName needs value for initialization. as it should be string, because you define a string variable and use it in your Image. To ignore the error you can set an empty value to your variable to ignore the requirement to init in view construction
var imageName: String = ""

SwiftUI - NavigationView Error message - Argument passed to call that takes no arguments

I am trying to implement a really basic NavigationView in SwiftUI. When I try the sample code that I have seen on other websites it generates an error message in Xcode. I am not sure why or how to fix this.
I have tried to clean the project, quit Xcode-Beta and restart it but that did not work.
struct ContentView : View {
var body: some View {
NavigationView {
Text("This is a great app")
}
}
}
I thought the code above should work but the error I get says:
"Argument passed to call that takes no arguments."
Any ideas or suggestions?
VStack can only take 10 argument.
If more, there will be error, so you should make it nested.
from
VStack{
}
to
VStack{
VStack{
}
VStack{
}
}
I had this same error message too and figured out what I did wrong and then kind of felt like an idiot. Ha ha.
Take a look:
It took me a while to figure out that my struct was the same name as a previously defined struct VStack. Whoops!
So I'm wondering if you had a file in your project that did this too.
check-in your app is there any Swifui class with the name NavigationView.
also when you jump to the definition from NavigationView it should refer to:
#available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 7.0, *)
public struct NavigationView<Content> : View where Content : View {
public init(#ViewBuilder content: () -> Content)
/// 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.
public typealias Body = Never
}
Testing Xcode 11.2.1 and it's still buggy. I noticed when I keep adding primitive views to my ContentView, I start getting errors like
"Argument passed to call that takes no arguments",
"Type of expression is ambiguous without more context" etc. on primitive views which worked before.
When I replaced, for example,
ScrollView {
VStack {
Text
Button
Image
Text
Button
Image
Text
Button
Image
...
}
}
with
ScrollView {
VStack {
VStack {
Text
Button
Image
}
VStack {
Text
Button
Image
}
VStack {
Text
Button
Image
}
...
}
my code started to compile and run again.
I found this problem when I accidentally try to redefine Text struct. Check if you naming your custom class the same as those in SwiftUI.
#Matteo Pacini helped me find the answer. When I started a new Xcode Project just to test the code above everything worked. I had a lot of files and was testing a lot of different code while experimenting with SwiftUI in my other project and for some reason XCode was always generating this error.
When I tried everything in a new project it worked. Something to be aware of while testing. Hope this helps others avoid similar problems.
Embed the 'Text("This is a great app")' in a VStack
struct ContentView : View {
var body: some View {
NavigationView {
Stack {
Text("This is a great app")
}
}
}
}

Eureka PushRow on a modal, view won't close after item selection

I am using a SplitViewController and on the detail page (which is set to "defines context"), the user can select "+" in the navbar and I present the next view controller "modally over current context" using a segue. On that view controller I am using Eureka and one of the rows I want to use is PushRow. The issue I am running into is when I select an option on the PushRow, the view (table of options to choose that Eureka generated) never closes. The list of options stays full screen. I can see that PushRow.onChange is called and it has the correct value. For some reason that topmost view will not close.
I dug deeper and it seems like I need to modify the PushRow presentationMode to be "presentModally" since I am presenting it from a modal. However, I am not sure what to put for the controllerProvider. Is this the right path? If so, what would the correct syntax be? I also tried doing a reload in the onChange but that didn't make a difference.
private func getGroupPushRow() -> PushRow<String> {
return PushRow<String>() {
$0.title = "Group"
$0.selectorTitle = "What is the Group?"
$0.noValueDisplayText = "Select a Group..."
$0.options = self.getGroups()
$0.presentationMode = PresentationMode.presentModally(controllerProvider: ControllerProvider<VCType>, onDismiss: { () in
})
$0.onChange({ (row) in
print("in onchange \(row.value)")
// row.reload()
// self.tableView.reloadData()
})
}
}
I eventually figured out a solution so I'm posting it here to hopefully help out somebody else. Going off of the example above, replace presentationMode & onChange with this code. Note that if you are using another object in your PushRow besides String, then the type in PushSelectorCell should be that type instead.
$0.presentationMode = PresentationMode.presentModally(
controllerProvider: ControllerProvider.callback {
return SelectorViewController<SelectorRow<PushSelectorCell<String>>> { _ in }
},
onDismiss: { vc in
vc.dismiss(animated: true)
})
$0.onChange({ (row) in
row.reload()
})