How do I add closures to Viewmodifiers in SwiftUI? - swift

I am using the ClearButton ViewModifier here
.modifier(ClearButton(text: $someBinding))
But I want to run a function after clear textfield. Like this or similar
.modifier(ClearButton(text: $someBinding)) {
print("")
}
Is it possible?

You can pass a function as a parameter like this:
struct ClearButton: ViewModifier {
#Binding var text: String
var action: () -> Void = {} // pass the function here
public func body(content: Content) -> some View {
ZStack(alignment: .trailing) {
content
if !text.isEmpty {
Button(action: {
self.text = ""
action() // call the `action` here
}) {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
.padding(.trailing, 8)
}
}
}
}
and apply this modifier to your TextField:
struct ContentView: View {
#State private var text = ""
var body: some View {
TextField("Some Text", text: $text)
.modifier(
ClearButton(text: $text) {
print("TextField cleared")
}
)
}
}
Note that if you want to skip the label for the trailing closure, the action parameter must come last.

Related

Remove padding on TextEditor

My custom text editor below once you click on the pen to edit, a new space appears so the text from before is not on the same line as the new one. How can I fix this? Here's a simple reproducible example:
struct SwiftUIView: View {
#State var name: String = "test"
#State var showEdit: Bool = true
var body: some View {
HStack {
HStack {
if(showEdit) {
CustomTextEditor.init(placeholder: "My unique name", text: $name)
.font(.headline)
} else {
Text(name)
.font(.headline)
}
}
Spacer()
Button(action: {
showEdit.toggle()
}) {
Image(systemName: "pencil")
.foregroundColor(.secondary)
}
}
}
}
struct CustomTextEditor: View {
let placeholder: String
#Binding var text: String
var body: some View {
ZStack {
if text.isEmpty {
Text(placeholder)
.foregroundColor(Color.primary.opacity(0.25))
}
TextEditor(text: $text)
}.onAppear() {
UITextView.appearance().backgroundColor = .clear
}.onDisappear() {
UITextView.appearance().backgroundColor = nil
}
}
}
I want it to have the same padding properies as inserting a simple Text("") so when I switch between Text("xyz") and TextEditor(text: $xyz) it has the same padding alignment. Right now TextEditor has a weird padding.
You will drive yourself insane trying to line up a Text and a TextEditor (or a TextField, for that matter), so don't try. Use another, disabled, TextEditor instead, and control the .opacity() on the top one depending upon whether the bound variable is empty or not. Like this:
struct CustomTextEditor: View {
#Binding var text: String
#State private var placeholder: String
init(placeholder: String, text: Binding<String>) {
_text = text
_placeholder = State(initialValue: placeholder)
}
var body: some View {
ZStack {
TextEditor(text: $placeholder)
.disabled(true)
TextEditor(text: $text)
.opacity(text == "" ? 0.7 : 1)
}
}
}
This view will show the placeholder if there is no text, and hide the placeholder as soon as there is text.
Edit:
You don't need the button, etc. in your other view. It becomes simply:
struct SwiftUIView: View {
#State var name: String = ""
var body: some View {
CustomTextEditor.init(placeholder: "My unique name", text: $name)
.font(.headline)
.padding()
}
}
and if you need a "Done" button on the keyboard, change your CustomTextEditor() to this:
struct CustomTextEditor: View {
#Binding var text: String
#State private var placeholder: String
#FocusState var isFocused: Bool
init(placeholder: String, text: Binding<String>) {
_text = text
_placeholder = State(initialValue: placeholder)
}
var body: some View {
ZStack {
TextEditor(text: $placeholder)
.disabled(true)
TextEditor(text: $text)
.opacity(text == "" ? 0.7 : 1)
.focused($isFocused)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button {
isFocused = false
} label: {
Text("Done")
.foregroundColor(.accentColor)
.padding(.trailing)
}
}
}
}
}

SwiftUI - Adding a keyboard toolbar button for only one TextField adds it for all TextFields

Background
I have two TextFields, one of which has a keyboard type of .decimalPad.
Given that there is no 'Done' button when using a decimal pad keyboard to close it, rather like the return key of the standard keyboard, I would like to add a 'Done' button within a toolbar above they keypad only for the decimal keyboard in SwiftUI.
Problem
Adding a .toolbar to any TextField for some reason adds it to all of the TextFields instead! I have tried conditional modifiers, using focussed states and checking for the Field value (but for some reason it is not set when checking, maybe an ordering thing?) and it still adds the toolbar above the keyboard for both TextFields.
How can I only have a .toolbar for my single TextField that accepts digits, and not for the other TextField that accepts a string?
Code
Please note that I've tried to make a minimal example that you can just copy and paste into Xcode and run it for yourself. With Xcode 13.2 there are some issues with displaying a keyboard for TextFields for me, especially within a sheet, so maybe simulator is required to run it properly and bring up the keyboard with cmd+K.
import SwiftUI
struct TestKeyboard: View {
#State var str: String = ""
#State var num: Float = 1.2
#FocusState private var focusedField: Field?
private enum Field: Int, CaseIterable {
case amount
case str
}
var body: some View {
VStack {
Spacer()
// I'm not adding .toolbar here...
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
// I'm only adding .toolbar here, but it still shows for the one above..
TextField("", value: $num, formatter: FloatNumberFormatter())
.keyboardType(.decimalPad)
.focused($focusedField, equals: .amount)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
Spacer()
}
}
}
class FloatNumberFormatter: NumberFormatter {
override init() {
super.init()
self.numberStyle = .currency
self.currencySymbol = "€"
self.minimumFractionDigits = 2
self.maximumFractionDigits = 2
self.locale = Locale.current
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
// So you can preview it quickly
struct TestKeyboard_Previews: PreviewProvider {
static var previews: some View {
TestKeyboard()
}
}
Try to make toolbar content conditional and move toolbar outside, like below. (No possibility to test now - just idea)
Note: test on real device
var body: some View {
VStack {
Spacer()
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
TextField("", value: $num, formatter: FloatNumberFormatter())
.focused($focusedField, equals: .amount)
.keyboardType(.decimalPad)
Spacer()
}
.toolbar { // << here !!
ToolbarItem(placement: .keyboard) {
if field == .amount { // << here !!
Button("Done") {
focusedField = nil
}
}
}
}
}
Using introspect you can do something like this in any part in your View:
.introspectTextField { textField in
textField.inputAccessoryView = UIView.getKeyboardToolbar {
textField.resignFirstResponder()
}
}
and for the getKeyboardToolbar:
extension UIView {
static func getKeyboardToolbar( _ callback: #escaping (()->()) ) -> UIToolbar {
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44))
let doneButton = CustomBarButtonItem(title: "Done".localized, style: .done) { _ in
callback()
}
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
toolBar.items = [space, doneButton]
return toolBar
}
}
and for the CustomBarButtonItem this is a bar button item that takes a closure
import UIKit
class CustomBarButtonItem: UIBarButtonItem {
typealias ActionHandler = (UIBarButtonItem) -> Void
private var actionHandler: ActionHandler?
convenience init(image: UIImage?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
self.init(image: image, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
convenience init(title: String?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
self.init(title: title, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, actionHandler: ActionHandler?) {
self.init(barButtonSystemItem: systemItem, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
#objc func barButtonItemPressed(sender: UIBarButtonItem) {
actionHandler?(sender)
}
}
I tried it a lot but I ended up in the below one.
.focused($focusedField, equals: .zip)
.toolbar{
ToolbarItem(placement: .keyboard) {
switch focusedField{
case .zip:
HStack{
Spacer()
Button("Done"){
focusedField = nil
}
}
default:
Text("")
}
}
}
This is my solution:
func textFieldSection(title: String,
text: Binding<String>,
keyboardType: UIKeyboardType,
focused: FocusState<Bool>.Binding,
required: Bool) -> some View {
TextField(
vm.placeholderText(isRequired: required),
text: text
)
.focused(focused)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if focused.wrappedValue {
Spacer()
Button {
focused.wrappedValue = false
} label: {
Text("Done")
}
}
}
}
}
For my project I have five TextField views on one View, so I created this method in the View's extension.
I pass the unique FocusState<Bool>.Binding value and use it in the ToolbarItemGroup closure to determine if we should display the content (Spacer, Button). If the particular TextField is focused, we display the toolbar content (all other unfocused TextFields won't).
Number Pad return solution in SwiftUI,
Tool bar button over keyboard,
Focused Field
struct NumberOfBagsView:View{
#FocusState var isInputActive: Bool
#State var phoneNumber:String = ""
TextField("Place holder",
text: $phoneNumber,
onEditingChanged: { _ in
//do actions while writing something in text field like text limit
})
.keyboardType(.numberPad)
.focused($isInputActive)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
print("done clicked")
isInputActive = false
}
}
}
}
I've found wrapping each TextField in its own NavigationView gives each its own context and thus a unique toolbar. It feels not right and I've seen constraint warnings in the console. Use something like this:
var body: some View {
VStack {
Spacer()
// I'm not adding .toolbar here...
NavigationView {
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
}
// I'm only adding .toolbar here, but it still shows for the one above..
NavigationView {
TextField("", value: $num, formatter: FloatNumberFormatter())
.keyboardType(.decimalPad)
.focused($focusedField, equals: .amount)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
}
Spacer()
}
}
There is work. But the other TextField will still display toolbar.
--- update ---
Hi, I updated the code to use ViewModifier to make the code easier to use and this time the code does compile and run >_<
struct ToolbarItemWithShow<Toolbar>: ViewModifier where Toolbar: View {
var show: Bool
let toolbar: Toolbar
let placement: ToolbarItemPlacement
func body(content: Content) -> some View {
content.toolbar {
ToolbarItemGroup(placement: placement) {
ZStack(alignment: .leading) {
if show {
HStack { toolbar }
.frame(width: UIScreen.main.bounds.size.width - 12)
}
}
}
}
}
}
extension View {
func keyboardToolbar<ToolBar>(_ show: Bool, #ViewBuilder toolbar: () -> ToolBar) -> some View where ToolBar: View {
modifier(ToolbarItemWithShow(show: show, toolbar: toolbar(), placement: .keyboard))
}
}
struct ContentView: View {
private enum Field: Hashable {
case name
case age
case gender
}
#State var name = "Ye"
#State var age = "14"
#State var gender = "man"
#FocusState private var focused: Field?
var body: some View {
VStack {
TextField("Name", text: $name)
.focused($focused, equals: .name)
.keyboardToolbar(focused == .name) {
Text("Input Name")
}
TextField("Age", text: $age)
.focused($focused, equals: .age)
.keyboardToolbar(focused == .age) {
Text("Input Age")
}
TextField("Gender", text: $gender)
.focused($focused, equals: .gender)
.keyboardToolbar(focused == .gender) {
Text("Input Sex")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
--- old ---
struct TextFieldWithToolBar<Label, Toolbar>: View where Label: View, Toolbar: View {
#Binding public var text: String
public let toolbar: Toolbar?
#FocusState private var focus: Bool
var body: some View {
TextField(text: $text, label: { label })
.focused($focus)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
ZStack(alignment: .leading) {
if focus {
HStack {
toolbar
Spacer()
Button("Done") {
focus = false
}
}
.frame(width: UIScreen.main.bounds.size.width - 12)
}
}
}
}
}
}
TextFieldWithToolBar("Name", text: $name)
TextFieldWithToolBar("Name", text: $name){
Text("Only Brand")
}
TextField("Name", "Set The Name", text: $name)
with Done with Toolbar without

SwiftUI passing ViewModifier as parameter

I am creating a custom TextField view that consists of multiple adornment views. I want to be able to set up the inner TextField with view modifiers such as keyboard, capitalization, etc. that apply just to that sub-view.
Rather than creating properties for each of these I figured the best way would be to pass in a single optional ViewModifier parameter and use it something like this:
struct MySuperTextField: View {
var vm: ViewModifier?
var body: some View {
TextField(...)
.modifier( vm ?? EmptyModifier() )
// ... more views here
}
}
This doesn't work due to the associatedType in ViewModifier. Alas there is no such thing as AnyViewModifier either (and I could't figure out how to make one that worked).
Anyone manage to do something like this? I couldn't find anything searching the web.
An example would be
struct LastNameModifier: ViewModifier {
func body(content: Content) -> some View {
content
.autocapitalization(.words)
.textContentType(.familyName)
.backgroundColor(.green)
// ... anything else specific to names
}
}
struct EmailModifier: ViewModifier {
func body(content: Content) -> some View {
content
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.backgroundColor(.yellow)
// ... anything else specific to emails
}
}
and then use them with my MySuperTextField like this:
VStack {
MySuperTextField("Last Name", $lastName, vm: LastNameModifier())
MySuperTextField("Email", $email, vm: EmailModifier())
}
If I understood correctly, you can make your MySuperTextField accept a generic parameter:
struct MySuperTextField<V>: View where V: ViewModifier {
private let placeholder: String
#Binding private var text: String
private let vm: V
init(_ placeholder: String, text: Binding<String>, vm: V) {
self.placeholder = placeholder
self._text = text
self.vm = vm
}
var body: some View {
TextField(placeholder, text: $text)
.modifier(vm)
}
}
Then, you can pass some ViewModifier as the parameter:
struct ContentView: View {
#State private var text: String = "Test"
var body: some View {
MySuperTextField("Last Name", text: $text, vm: LastNameModifier())
}
}
If you need a way to skip the vm parameter when creating MySuperTextField:
MySuperTextField("Last Name", text: $text)
you can create an extension:
extension MySuperTextField where V == EmptyModifier {
init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
self.vm = EmptyModifier()
}
}
Set the ViewModifier up as a struct and just call it on the view. You just have to be specific as to the type, such as this buttonStyle modifier I used:
struct PurchaseButtonStyle: ButtonStyle {
let geometry: GeometryProxy
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(minWidth: 0, idealWidth: 300, maxWidth: .infinity)
.padding(.all, geometry.size.height / 30)
.foregroundColor(.black)
.background(.orange)
.cornerRadius(30)
}
}
The use is:
Button(...)
.buttonStyle(PurchaseButtonStyle(geometry: Geometry))
You just have to write it to be the specific modifier. I just used this because it was handy in an app I was working on.
since ViewModifier has an associated type that you cannot use as an instance variable but I propose you another way of use:
MySuperTextField does not have to maintain instances of the ViewModifier type.
struct MySuperTextField: View {
#State var textValue = ""
var body: some View {
TextField("", text: $textValue)
}
}
For each structure that conforms to the ViewModifier protocol you create an extension:
extension MySuperTextField {
func lastNameModifier() -> some View {
modifier(LastNameModifier())
}
func emailModifier() -> some View {
modifier(EmailModifier())
}
}
and you can use it this way:
struct ContentView : View {
var body: some View {
VStack {
MySuperTextField(textValue: "Last Name")
.lastNameModifier()
MySuperTextField(textValue: "Email")
.emailModifier()
}
}
}

How to pass one SwiftUI View as a variable to another View struct

I'm implementing a very custom NavigationLink called MenuItem and would like to reuse it across the project. It's a struct that conforms to View and implements var body : some View which contains a NavigationLink.
I need to somehow store the view that shall be presented by NavigationLink in the body of MenuItem but have yet failed to do so.
I have defined destinationView in MenuItem's body as some View and tried two initializers:
This seemed too easy:
struct MenuItem: View {
private var destinationView: some View
init(destinationView: View) {
self.destinationView = destinationView
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Error: Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
2nd try:
struct MenuItem: View {
private var destinationView: some View
init<V>(destinationView: V) where V: View {
self.destinationView = destinationView
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Error: Cannot assign value of type 'V' to type 'some View'.
Final try:
struct MenuItem: View {
private var destinationView: some View
init<V>(destinationView: V) where V: View {
self.destinationView = destinationView as View
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Error: Cannot assign value of type 'View' to type 'some View'.
I hope someone can help me. There must be a way if NavigationLink can accept some View as an argument.
Thanks ;D
To sum up everything I read here and the solution which worked for me:
struct ContainerView<Content: View>: View {
#ViewBuilder var content: Content
var body: some View {
content
}
}
This not only allows you to put simple Views inside, but also, thanks to #ViewBuilder, use if-else and switch-case blocks:
struct SimpleView: View {
var body: some View {
ContainerView {
Text("SimpleView Text")
}
}
}
struct IfElseView: View {
var flag = true
var body: some View {
ContainerView {
if flag {
Text("True text")
} else {
Text("False text")
}
}
}
}
struct SwitchCaseView: View {
var condition = 1
var body: some View {
ContainerView {
switch condition {
case 1:
Text("One")
case 2:
Text("Two")
default:
Text("Default")
}
}
}
}
Bonus:
If you want a greedy container, which will claim all the possible space (in contrary to the container above which claims only the space needed for its subviews) here it is:
struct GreedyContainerView<Content: View>: View {
#ViewBuilder let content: Content
var body: some View {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
If you need an initializer in your view then you can use #ViewBuilder for the parameter too. Even for multiple parameters if you will:
init(#ViewBuilder content: () -> Content) {…}
The way Apple does it is using function builders. There is a predefined one called ViewBuilder. Make it the last argument, or only argument, of your init method for MenuItem, like so:
..., #ViewBuilder builder: #escaping () -> Content)
Assign it to a property defined something like this:
let viewBuilder: () -> Content
Then, where you want to diplay your passed-in views, just call the function like this:
HStack {
viewBuilder()
}
You will be able to use your new view like this:
MenuItem {
Image("myImage")
Text("My Text")
}
This will let you pass up to 10 views and use if conditions etc. though if you want it to be more restrictive you will have to define your own function builder. I haven't done that so you will have to google that.
You should make the generic parameter part of MenuItem:
struct MenuItem<Content: View>: View {
private var destinationView: Content
init(destinationView: Content) {
self.destinationView = destinationView
}
var body : some View {
// ...
}
}
You can create your custom view like this:
struct ENavigationView<Content: View>: View {
let viewBuilder: () -> Content
var body: some View {
NavigationView {
VStack {
viewBuilder()
.navigationBarTitle("My App")
}
}
}
}
struct ENavigationView_Previews: PreviewProvider {
static var previews: some View {
ENavigationView {
Text("Preview")
}
}
}
Using:
struct ContentView: View {
var body: some View {
ENavigationView {
Text("My Text")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can pass a NavigationLink (or any other view widget) as a variable to a subview as follows:
import SwiftUI
struct ParentView: View {
var body: some View {
NavigationView{
VStack(spacing: 8){
ChildView(destinationView: Text("View1"), title: "1st")
ChildView(destinationView: Text("View2"), title: "2nd")
ChildView(destinationView: ThirdView(), title: "3rd")
Spacer()
}
.padding(.all)
.navigationBarTitle("NavigationLinks")
}
}
}
struct ChildView<Content: View>: View {
var destinationView: Content
var title: String
init(destinationView: Content, title: String) {
self.destinationView = destinationView
self.title = title
}
var body: some View {
NavigationLink(destination: destinationView){
Text("This item opens the \(title) view").foregroundColor(Color.black)
}
}
}
struct ThirdView: View {
var body: some View {
VStack(spacing: 8){
ChildView(destinationView: Text("View1"), title: "1st")
ChildView(destinationView: Text("View2"), title: "2nd")
ChildView(destinationView: ThirdView(), title: "3rd")
Spacer()
}
.padding(.all)
.navigationBarTitle("NavigationLinks")
}
}
The accepted answer is nice and simple. The syntax got even cleaner with iOS 14 + macOS 11:
struct ContainerView<Content: View>: View {
#ViewBuilder var content: Content
var body: some View {
content
}
}
Then continue to use it like this:
ContainerView{
...
}
I really struggled to make mine work for an extension of View. Full details about how to call it are seen here.
The extension for View (using generics) - remember to import SwiftUI:
extension View {
/// Navigate to a new view.
/// - Parameters:
/// - view: View to navigate to.
/// - binding: Only navigates when this condition is `true`.
func navigate<SomeView: View>(to view: SomeView, when binding: Binding<Bool>) -> some View {
modifier(NavigateModifier(destination: view, binding: binding))
}
}
// MARK: - NavigateModifier
fileprivate struct NavigateModifier<SomeView: View>: ViewModifier {
// MARK: Private properties
fileprivate let destination: SomeView
#Binding fileprivate var binding: Bool
// MARK: - View body
fileprivate func body(content: Content) -> some View {
NavigationView {
ZStack {
content
.navigationBarTitle("")
.navigationBarHidden(true)
NavigationLink(destination: destination
.navigationBarTitle("")
.navigationBarHidden(true),
isActive: $binding) {
EmptyView()
}
}
}
}
}
Alternatively you can use a static function extension. For example, I make a titleBar extension to Text. This makes it very easy to reuse code.
In this case you can pass a #Viewbuilder wrapper with the view closure returning a custom type that conforms to view. For example:
import SwiftUI
extension Text{
static func titleBar<Content:View>(
titleString:String,
#ViewBuilder customIcon: ()-> Content
)->some View {
HStack{
customIcon()
Spacer()
Text(titleString)
.font(.title)
Spacer()
}
}
}
struct Text_Title_swift_Previews: PreviewProvider {
static var previews: some View {
Text.titleBar(titleString: "title",customIcon: {
Image(systemName: "arrowshape.turn.up.backward")
})
.previewLayout(.sizeThatFits)
}
}
If anyone is trying to pass two different views to other view, and can't do it because of this error:
Failed to produce diagnostic for expression; please submit a bug report...
Because we are using <Content: View>, the first view you passed, the view is going to store its type, and expect the second view you are passing be the same type, this way, if you want to pass a Text and an Image, you will not be able to.
The solution is simple, add another content view, and name it differently.
Example:
struct Collapsible<Title: View, Content: View>: View {
#State var title: () -> Title
#State var content: () -> Content
#State private var collapsed: Bool = true
var body: some View {
VStack {
Button(
action: { self.collapsed.toggle() },
label: {
HStack {
self.title()
Spacer()
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
VStack {
self.content()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: collapsed ? 0 : .none)
.clipped()
.animation(.easeOut)
.transition(.slide)
}
}
}
Calling this View:
Collapsible {
Text("Collapsible")
} content: {
ForEach(1..<5) { index in
Text("\(index) test")
}
}
Syntax for 2 Views
struct PopOver<Content, PopView> : View where Content: View, PopView: View {
var isShowing: Bool
#ViewBuilder var content: () -> Content
#ViewBuilder var popover: () -> PopView
var body: some View {
ZStack(alignment: .center) {
self
.content()
.disabled(isShowing)
.blur(radius: isShowing ? 3 : 0)
ZStack {
self.popover()
}
.frame(width: 112, height: 112)
.opacity(isShowing ? 1 : 0)
.disabled(!isShowing)
}
}
}

How can I trigger an action when a swiftUI toggle() is toggled?

In my SwiftUI view I have to trigger an action when a Toggle() changes its state. The toggle itself only takes a Binding.
I therefore tried to trigger the action in the didSet of the #State variable. But the didSet never gets called.
Is there any (other) way to trigger an action? Or any way to observe the value change of a #State variable?
My code looks like this:
struct PWSDetailView : View {
#ObjectBinding var station: PWS
#State var isDisplayed: Bool = false {
didSet {
if isDisplayed != station.isDisplayed {
PWSStore.shared.toggleIsDisplayed(station)
}
}
}
var body: some View {
VStack {
ZStack(alignment: .leading) {
Rectangle()
.frame(width: UIScreen.main.bounds.width, height: 50)
.foregroundColor(Color.lokalZeroBlue)
Text(station.displayName)
.font(.title)
.foregroundColor(Color.white)
.padding(.leading)
}
MapView(latitude: station.latitude, longitude: station.longitude, span: 0.05)
.frame(height: UIScreen.main.bounds.height / 3)
.padding(.top, -8)
Form {
Toggle(isOn: $isDisplayed)
{ Text("Wetterstation anzeigen") }
}
Spacer()
}.colorScheme(.dark)
}
}
The desired behaviour would be that the action "PWSStore.shared.toggleIsDisplayed(station)" is triggered when the Toggle() changes its state.
iOS 14+
If you're using iOS 14 and higher you can use onChange:
struct ContentView: View {
#State private var isDisplayed = false
var body: some View {
Toggle("", isOn: $isDisplayed)
.onChange(of: isDisplayed) { value in
// action...
print(value)
}
}
}
Here is a version without using tapGesture.
#State private var isDisplayed = false
Toggle("", isOn: $isDisplayed)
.onReceive([self.isDisplayed].publisher.first()) { (value) in
print("New value is: \(value)")
}
iOS13+
Here is a more generic approach you can apply to any Binding for almost all built in Views like Pickers, Textfields, Toggle..
extension Binding {
func didSet(execute: #escaping (Value) -> Void) -> Binding {
return Binding(
get: { self.wrappedValue },
set: {
self.wrappedValue = $0
execute($0)
}
)
}
}
And usage is simply;
#State var isOn: Bool = false
Toggle("Title", isOn: $isOn.didSet { (state) in
print(state)
})
iOS14+
#State private var isOn = false
var body: some View {
Toggle("Title", isOn: $isOn)
.onChange(of: isOn) { _isOn in
/// use _isOn here..
}
}
The cleanest approach in my opinion is to use a custom binding.
With that you have full control when the toggle should actually switch
import SwiftUI
struct ToggleDemo: View {
#State private var isToggled = false
var body: some View {
let binding = Binding(
get: { self.isToggled },
set: {
potentialAsyncFunction($0)
}
)
func potentialAsyncFunction(_ newState: Bool) {
//something async
self.isToggled = newState
}
return Toggle("My state", isOn: binding)
}
}
I think it's ok
struct ToggleModel {
var isWifiOpen: Bool = true {
willSet {
print("wifi status will change")
}
}
}
struct ToggleDemo: View {
#State var model = ToggleModel()
var body: some View {
Toggle(isOn: $model.isWifiOpen) {
HStack {
Image(systemName: "wifi")
Text("wifi")
}
}.accentColor(.pink)
.padding()
}
}
I found a simpler solution, just use onTapGesture:D
Toggle(isOn: $stateChange) {
Text("...")
}
.onTapGesture {
// Any actions here.
}
This is how I code:
Toggle("Title", isOn: $isDisplayed)
.onReceive([self.isDisplayed].publisher.first()) { (value) in
//Action code here
}
Updated code (Xcode 12, iOS14):
Toggle("Enabled", isOn: $isDisplayed.didSet { val in
//Action here
})
The .init is the constructor of Binding
#State var isDisplayed: Bool
Toggle("some text", isOn: .init(
get: { isDisplayed },
set: {
isDisplayed = $0
print("changed")
}
))
Based on #Legolas Wang's answer.
When you hide the original label from the toggle you can attach the tapGesture only to the toggle itself
HStack {
Text("...")
Spacer()
Toggle("", isOn: $stateChange)
.labelsHidden()
.onTapGesture {
// Any actions here.
}
}
class PWSStore : ObservableObject {
...
var station: PWS
#Published var isDisplayed = true {
willSet {
PWSStore.shared.toggleIsDisplayed(self.station)
}
}
}
struct PWSDetailView : View {
#ObservedObject var station = PWSStore.shared
...
var body: some View {
...
Toggle(isOn: $isDisplayed) { Text("Wetterstation anzeigen") }
...
}
}
Demo here https://youtu.be/N8pL7uTjEFM
Here's my approach. I was facing the same issue, but instead decided to wrap UIKit's UISwitch into a new class conforming to UIViewRepresentable.
import SwiftUI
final class UIToggle: UIViewRepresentable {
#Binding var isOn: Bool
var changedAction: (Bool) -> Void
init(isOn: Binding<Bool>, changedAction: #escaping (Bool) -> Void) {
self._isOn = isOn
self.changedAction = changedAction
}
func makeUIView(context: Context) -> UISwitch {
let uiSwitch = UISwitch()
return uiSwitch
}
func updateUIView(_ uiView: UISwitch, context: Context) {
uiView.isOn = isOn
uiView.addTarget(self, action: #selector(switchHasChanged(_:)), for: .valueChanged)
}
#objc func switchHasChanged(_ sender: UISwitch) {
self.isOn = sender.isOn
changedAction(sender.isOn)
}
}
And then its used like this:
struct PWSDetailView : View {
#State var isDisplayed: Bool = false
#ObservedObject var station: PWS
...
var body: some View {
...
UIToggle(isOn: $isDisplayed) { isOn in
//Do something here with the bool if you want
//or use "_ in" instead, e.g.
if isOn != station.isDisplayed {
PWSStore.shared.toggleIsDisplayed(station)
}
}
...
}
}
First, do you actually know that the extra KVO notifications for station.isDisplayed are a problem? Are you experiencing performance problems? If not, then don't worry about it.
If you are experiencing performance problems and you've established that they're due to excessive station.isDisplayed KVO notifications, then the next thing to try is eliminating unneeded KVO notifications. You do that by switching to manual KVO notifications.
Add this method to station's class definition:
#objc class var automaticallyNotifiesObserversOfIsDisplayed: Bool { return false }
And use Swift's willSet and didSet observers to manually notify KVO observers, but only if the value is changing:
#objc dynamic var isDisplayed = false {
willSet {
if isDisplayed != newValue { willChangeValue(for: \.isDisplayed) }
}
didSet {
if isDisplayed != oldValue { didChangeValue(for: \.isDisplayed) }
}
}
You can try this(it's a workaround):
#State var isChecked: Bool = true
#State var index: Int = 0
Toggle(isOn: self.$isChecked) {
Text("This is a Switch")
if (self.isChecked) {
Text("\(self.toggleAction(state: "Checked", index: index))")
} else {
CustomAlertView()
Text("\(self.toggleAction(state: "Unchecked", index: index))")
}
}
And below it, create a function like this:
func toggleAction(state: String, index: Int) -> String {
print("The switch no. \(index) is \(state)")
return ""
}
Here is a handy extension I wrote to fire a callback whenever the toggle is pressed. Unlike a lot of the other solutions this truly only will fire when the toggle is switched and not on init which for my use case was important. This mimics similar SwiftUI initializers such as TextField for onCommit.
USAGE:
Toggle("My Toggle", isOn: $isOn, onToggled: { value in
print(value)
})
EXTENSIONS:
extension Binding {
func didSet(execute: #escaping (Value) -> Void) -> Binding {
Binding(
get: { self.wrappedValue },
set: {
self.wrappedValue = $0
execute($0)
}
)
}
}
extension Toggle where Label == Text {
/// Creates a toggle that generates its label from a localized string key.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// `Text` for more information about localizing strings.
///
/// To initialize a toggle with a string variable, use
/// ``Toggle/init(_:isOn:)-2qurm`` instead.
///
/// - Parameters:
/// - titleKey: The key for the toggle's localized title, that describes
/// the purpose of the toggle.
/// - isOn: A binding to a property that indicates whether the toggle is
/// on or off.
/// - onToggled: A closure that is called whenver the toggle is switched.
/// Will not be called on init.
public init(_ titleKey: LocalizedStringKey, isOn: Binding<Bool>, onToggled: #escaping (Bool) -> Void) {
self.init(titleKey, isOn: isOn.didSet(execute: { value in onToggled(value) }))
}
/// Creates a toggle that generates its label from a string.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// title similar to ``Text/init(_:)-9d1g4``. See `Text` for more
/// information about localizing strings.
///
/// To initialize a toggle with a localized string key, use
/// ``Toggle/init(_:isOn:)-8qx3l`` instead.
///
/// - Parameters:
/// - title: A string that describes the purpose of the toggle.
/// - isOn: A binding to a property that indicates whether the toggle is
/// on or off.
/// - onToggled: A closure that is called whenver the toggle is switched.
/// Will not be called on init.
public init<S>(_ title: S, isOn: Binding<Bool>, onToggled: #escaping (Bool) -> Void) where S: StringProtocol {
self.init(title, isOn: isOn.didSet(execute: { value in onToggled(value) }))
}
}
Available for Xcode 13.4
import SwiftUI
struct ToggleBootCamp: View {
#State var isOn: Bool = true
#State var status: String = "ON"
var body: some View {
NavigationView {
VStack {
Toggle("Switch", isOn: $isOn)
.onChange(of: isOn, perform: {
_isOn in
// Your code here...
status = _isOn ? "ON" : "OFF"
})
Spacer()
}.padding()
.navigationTitle("Toggle switch is: \(status)")
}
}
}
Just in case you don't want to use extra functions, mess the structure - use states and use it wherever you want. I know it's not a 100% answer for the event trigger, however, the state will be saved and used in the most simple way.
struct PWSDetailView : View {
#State private var isToggle1 = false
#State private var isToggle2 = false
var body: some View {
ZStack{
List {
Button(action: {
print("\(self.isToggle1)")
print("\(self.isToggle2)")
}){
Text("Settings")
.padding(10)
}
HStack {
Toggle(isOn: $isToggle1){
Text("Music")
}
}
HStack {
Toggle(isOn: $isToggle1){
Text("Music")
}
}
}
}
}
}
lower than iOS 14:
extension for Binding with Equatable check
public extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> where Value: Equatable {
Binding(
get: { self.wrappedValue },
set: { newValue in
if self.wrappedValue != newValue { // equal check
self.wrappedValue = newValue
handler(newValue)
}
}
)
}
}
Usage:
Toggle(isOn: $pin.onChange(pinChanged(_:))) {
Text("Equatable Value")
}
func pinChanged(_ pin: Bool) {
}
Add a transparent Rectangle on top, then:
ZStack{
Toggle(isOn: self.$isSelected, label: {})
Rectangle().fill(Color.white.opacity(0.1))
}
.contentShape(Rectangle())
.onTapGesture(perform: {
self.isSelected.toggle()
})
Available for XCode 12
import SwiftUI
struct ToggleView: View {
#State var isActive: Bool = false
var body: some View {
Toggle(isOn: $isActive) { Text(isActive ? "Active" : "InActive") }
.padding()
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}