I am looking to structure a SwiftUI component in a similar way as some of Apple's implementations (E.g. Button). I am looking to have a label associated to this component (which is just another View) or have the ability to construct using just a string (and default to using a Text view).
This is what I have:
struct ExampleComponent<Label> : View where Label : View {
let label: () -> Label
let action: () -> ()
init(_ title: String, action: #escaping () -> ()) {
self.init(label: {
Text(title)
}, action: action)
}
init(#ViewBuilder label: #escaping () -> Label, action: #escaping () -> ()) {
self.label = label
self.action = action
}
var body: some View {
label()
}
}
However, this cannot compile due to the error:
Cannot convert value of type 'Text' to closure result type 'Label'.
What's going wrong?
If you want this to be a default, then you need to make sure it only applies in cases where Label == Text. In other situations, you can't use it. I generally do this with a constrained extension:
extension ExampleComponent where Label == Text {
init(_ title: String, action: #escaping () -> ()) {
self.init(label: {
Text(title)
}, action: action)
}
}
But you can also add the constraint on the init directly:
init(_ title: String, action: #escaping () -> ()) where Label == Text {
self.init(label: {
Text(title)
}, action: action)
}
Related
I created a custom implementation of onTapGesture(_:) for a custom view, however, it doesn't work as intended. I have an #State property called action which gets called at the end of the button press. It's supposed to function as "additional functionality" to whatever the button does. For some strange reason, when I update the action property it isn't reflected.
ShowAlertButton()
.onTapGestureCustom {
print("*silent alarm*")
// I expect the view to print "*silent alarm*" when I press "sound alarm"
// However, it continues to print "ALERT!!!" which is the default value
}
struct ShowAlertButton: View {
#State private var action: () -> Void = { print("ALERT!!!") } // << default value
var body: some View {
Button {
// ... some code
action() // << additional functionality
} label: {
Text("Sound alarm")
}
}
// Custom implementation of onTapGesture(_:)
func onTapGestureCustom(_ action: #escaping () -> Void) -> some View {
self.action = action
return self
}
}
Does anyone know why the action variable is not being updated?
Thanks.
By returning a newly created View and changing the action variable into a normal instance property makes this work as intended. (Thanks to #Cora for the tip!!)
struct ShowAlertButton: View {
private var action: () -> Void = { print("ALERT!!!") }
var body: some View {
Button {
// ... some code
action()
} label: {
Text("Sound alarm")
}
}
func onTapGestureCustom(_ newValue: #escaping () -> Void) -> some View {
return ShowAlertButton(action: newValue)
// or Self(action: newValue)
}
}
I have my audioPlayer setup already
In addition to the current functionality of the stepper, I want to also play separate sounds for onIncrement & onDecrement.
This project uses Core Data to persist. $estimatorData.qty listens to published var my View Model when the qty changes the new qty is saved in my view model estimatorData.save()
Here is a link to docs Stepper
I am trying to wrap my head around if one of the initializers would fit with what I am trying to accomplish
Stepper("", value: $estimatorData.qty.onChange { qty in
estimatorData.save()
}, in: 0...10000)
.frame(width: 100, height: 35)
.offset(x: -4)
.background(colorScheme == .dark ? Color.blue : Color.blue)
.cornerRadius(8)
Here are my players
func incrementTaped() {
playSound(sound: "plus", type: "mp3")
}
func decrementTaped() {
playSound(sound: "minus", type: "m4a")
}
Problem
Currently there is no initialiser which combines both onIncrement / onDecrement functions and value / bounds / step parameters.
You can either have onIncrement and onDecrement:
public init(onIncrement: (() -> Void)?, onDecrement: (() -> Void)?, onEditingChanged: #escaping (Bool) -> Void = { _ in }, #ViewBuilder label: () -> Label)
or value, bounds and step:
public init<V>(value: Binding<V>, in bounds: ClosedRange<V>, step: V.Stride = 1, onEditingChanged: #escaping (Bool) -> Void = { _ in }, #ViewBuilder label: () -> Label) where V : Strideable
Solution
Instead, you can create a custom Stepper to combine both initialisers:
struct CustomStepper<Label, Value>: View where Label: View, Value: Strideable {
#Binding private var value: Value
private let bounds: ClosedRange<Value>
private let step: Value.Stride
private let onIncrement: (() -> Void)?
private let onDecrement: (() -> Void)?
private let label: () -> Label
#State private var previousValue: Value
public init(
value: Binding<Value>,
in bounds: ClosedRange<Value>,
step: Value.Stride = 1,
onIncrement: (() -> Void)? = nil,
onDecrement: (() -> Void)? = nil,
#ViewBuilder label: #escaping () -> Label
) {
self._value = value
self.bounds = bounds
self.step = step
self.onIncrement = onIncrement
self.onDecrement = onDecrement
self.label = label
self._previousValue = .init(initialValue: value.wrappedValue)
}
var body: some View {
Stepper(
value: $value,
in: bounds,
step: step,
onEditingChanged: onEditingChanged,
label: label
)
}
func onEditingChanged(isEditing: Bool) {
guard !isEditing else {
previousValue = value
return
}
if previousValue < value {
onIncrement?()
} else if previousValue > value {
onDecrement?()
}
}
}
Demo
struct ContentView: View {
#State private var value = 1
var body: some View {
CustomStepper(value: $value, in: 1...10, onIncrement: onIncrement, onDecrement: onDecrement) {
Text(String(value))
}
.onChange(of: value) {
print("onChange value: \($0)")
}
}
func onIncrement() {
print("onIncrement")
}
func onDecrement() {
print("onDecrement")
}
}
Generally speaking the business logic should not go in your view since views are values that get created and destroyed all the time. Move it into a ViewModel. You can watch any published property by using its projectedValue to get a publisher. For example:
import SwiftUI
import PlaygroundSupport
import Combine
final class ViewModel: ObservableObject {
#Published var steps: Int = 0
#Published var title: String = ""
init() {
$steps
.handleEvents(receiveOutput: { value in
// Perform your audio side effect for this value here
})
.map {"The current value is \($0)" }
.assign(to: &$title)
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Stepper(viewModel.title, value: $viewModel.steps)
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
I have custom ActionView with two buttons: Car and Bike. When these buttons tapped I need that in the MainView modifiers onCarTap/onBikeTap will be triggered.
With my current implementation here is error:
Argument passed to call that takes no arguments
Value of tuple type 'Void' has no member 'onBikeTap'
Source code:
struct ActionView: View {
// Callback for button taps
var onCarTap: (() -> Void)?
var onBikeTap: (() -> Void)?
var body: some View {
HStack {
Button(action: {
onCarTap?()
}, label: {
Text("Car")
})
Button(action: {
onBikeTap?()
}, label: {
Text("Bike")
})
}
}
}
I am looking for solution like this:
struct MainView: View {
var body: some View {
ActionView()
.onCarTap({})
.onBikeTap({ })
}
}
It is possible to implement in this way:
ActionView(onCarTap: {
print("on car tap")
}, onBikeTap: {
print("on bike tap")
})
Assuming you have the following View:
struct ActionView: View {
var onCarTapAction: (() -> Void)?
var onBikeTapAction: (() -> Void)?
var body: some View {
HStack {
Button(action: {
onCarTapAction?()
}, label: {
Text("Car")
})
Button(action: {
onBikeTapAction?()
}, label: {
Text("Bike")
})
}
}
}
You can create an extension:
extension ActionView {
func onCarTap(action: #escaping (() -> Void)) -> ActionView {
ActionView(onCarTapAction: action, onBikeTapAction: onBikeTapAction)
}
func onBikeTap(action: #escaping (() -> Void)) -> ActionView {
ActionView(onCarTapAction: onCarTapAction, onBikeTapAction: action)
}
}
and use it like this:
struct ContentView: View {
var body: some View {
ActionView()
.onCarTap {
print("onCarTap")
}
.onBikeTap {
print("onBikeTap")
}
}
}
You can declare a modifier for your purpose like the following.
extension ActionView {
func onCarTap(_ handler: #escaping () -> Void) -> ActionView {
var new = self
new.onCarTap = handler
return new
}
}
In addition, if you prefer to hide the handler property with private or fileprivate to prevent to be accessed directly, have to declare a designated init which accepts parameters for its properties except one for the handler.
I have a PersonView like following
struct PersonView<Content>: View where Content: View {
let text: Text
let detailText: Text
var content: (() -> Content)? = nil
var body: some View {
VStack(alignment: .leading, spacing: 10) {
text
detailText
content?()
}
}
}
Also My sample model looks like:
struct Person {
let name: String
let age: Int
}
To make life easy I have made an extension of PersonView
extension PersonView {
init<Content: View>(person: Person, content: (() -> Content)? = nil) {
self.init(text: Text(person.name),
detailText: Text("\(person.age)"), content: content)
}
}
but here I am getting error like
"Cannot convert value of type '(() -> Content)?' to expected argument type 'Optional<(() -> _)>'"
I am not sure where I am getting wrong
You should not declare an extra generic parameter Content in the initialiser. The initialiser should not be generic, and instead just use the Content generic parameter from PersonView:
extension PersonView {
init(person: Person, content: (() -> Content)? = nil) {
self.init(text: Text(person.name),
detailText: Text("\(person.age)"), content: content)
}
}
The extra Content that you declared is a different generic parameter from the Content in PersonView<Content>, which is why the compiler says it can't convert the types.
I'm trying to understand how the below initialization of the TextField struct works in Swift UI.
TextField("Name", text: $name)
When using Xcode's "Jump to definition" feature, I see the below extension on TextField defining what seems to be all of TextFields init methods. In all of the init methods, onEditingChanged and onCommit seem to be required arguments. So, how is the above usage working?
extension TextField where Label == Text {
public init(_ titleKey: LocalizedStringKey, text: Binding<String>, onEditingChanged: #escaping (Bool) -> Void = { _ in }, onCommit: #escaping () -> Void = {})
public init<S>(_ title: S, text: Binding<String>, onEditingChanged: #escaping (Bool) -> Void = { _ in }, onCommit: #escaping () -> Void = {}) where S : StringProtocol
public init<T>(_ titleKey: LocalizedStringKey, value: Binding<T>, formatter: Formatter, onEditingChanged: #escaping (Bool) -> Void = { _ in }, onCommit: #escaping () -> Void = {})
public init<S, T>(_ title: S, value: Binding<T>, formatter: Formatter, onEditingChanged: #escaping (Bool) -> Void = { _ in }, onCommit: #escaping () -> Void = {}) where S : StringProtocol
}
When you open the documentation in Xcode you will be able to see that onEditingChanged and onCommit have got empty closures provided as default:
init(_ titleKey: LocalizedStringKey,
text: Binding<String>,
onEditingChanged: #escaping (Bool) -> Void = { _ in },
onCommit: #escaping () -> Void = {})