How can I create a text with Checkbox in SwiftUI?

Viewed 21729

I have a requirement of Checkbox (✅ as in to-do list) with textfield. Currently I have created button object like below :

    Button(action: {
            // do when checked / unchecked
            //...
    }) {
        HStack(alignment: .top, spacing: 10) {

            Rectangle()
                .fill(Color.white)
                .frame(width:20, height:20, alignment: .center)
                .cornerRadius(5)
            Text("Todo  item 1")
        }
    }

I need to preserve checked and unchecked state in SwiftUI.

checkbox sample image

10 Answers

Here is a simple, re-usable checkmark component I created that follows a color scheme similar to other checkmarks on iOS (e.g. selecting messages in the Messages app):

import SwiftUI

struct CheckBoxView: View {
    @Binding var checked: Bool

    var body: some View {
        Image(systemName: checked ? "checkmark.square.fill" : "square")
            .foregroundColor(checked ? Color(UIColor.systemBlue) : Color.secondary)
            .onTapGesture {
                self.checked.toggle()
            }
    }
}

struct CheckBoxView_Previews: PreviewProvider {
    struct CheckBoxViewHolder: View {
        @State var checked = false

        var body: some View {
            CheckBoxView(checked: $checked)
        }
    }

    static var previews: some View {
        CheckBoxViewHolder()
    }
}

You can use it in other views like this:

...
@State private var checked = true
...

HStack {
    CheckBoxView(checked: $checked)
    Spacer()
    Text("Element that requires checkmark!")
}
...

The best way for iOS devices is to create a CheckboxStyle struct and conform to the ToggleStyle protocol. That allows you to then use the built-in Toggle component provided by Apple.

// CheckboxStyle.swift
import SwiftUI

struct CheckboxStyle: ToggleStyle {

    func makeBody(configuration: Self.Configuration) -> some View {

        return HStack {
            Image(systemName: configuration.isOn ? "checkmark.square" : "square")
                .resizable()
                .frame(width: 24, height: 24)
                .foregroundColor(configuration.isOn ? .blue : .gray)
                .font(.system(size: 20, weight: .regular, design: .default))
                configuration.label
        }
        .onTapGesture { configuration.isOn.toggle() }

    }
}

// Example usage in a SwiftUI view
Toggle(isOn: $checked) {
    Text("The label")
}
.toggleStyle(CheckboxStyle())

On macOS, Apple already has created a CheckboxToggleStyle() that you can use for macOS 10.15+. But it isn't available for iOS - yet.

We can take help of the @State from Apple, which persists value of a given type, through which a view reads and monitors the value.

Working example :

struct CheckboxFieldView: View {
    
    @State var checkState: Bool = false
    
    var body: some View {
        
         Button(action:
            {
                //1. Save state
                self.checkState = !self.checkState
                print("State : \(self.checkState)")
                
                
        }) {
            HStack(alignment: .top, spacing: 10) {
                 
                        //2. Will update according to state
                   Rectangle()
                            .fill(self.checkState ? Color.green : Color.red)
                            .frame(width:20, height:20, alignment: .center)
                            .cornerRadius(5)
                     
                   Text("Todo item ")
                 
            }
        }
        .foregroundColor(Color.white)
  
    }
    
}

Now, you can add CheckboxFieldView()

You'll want something like this:

struct TodoCell: View {
    var todoCellViewModel: TodoCellViewModel
    var updateTodo: ((_ id: Int) -> Void)

    var body: some View {
        HStack {
            Image(systemName: (self.todoCellViewModel.isCompleted() ? "checkmark.square" : "square")).tapAction {
                self.updateTodo(self.todoCellViewModel.getId())
            }

            Text(self.todoCellViewModel.getTitle())
        }
        .padding()
    }
}

Your list could look something like this:

struct TodoList: View {
    var todos: Todos
    var updateTodo: ((_ id: Int) -> Void)

    var body: some View {
        List(self.todos) { todo in
            TodoCell(todoCellViewModel: TodoCellViewModel(todo: todo), updateTodo: { (id) in
                self.updateTodo(id)
            })
        }
    }
}

Your model might look something like this:

public class TodoCellViewModel {

    private var todo: Todo

    public init(todo: Todo) {
        self.todo = todo
    }

    public func isCompleted() -> Bool {
        return self.todo.completed
    }

    public func getTitle() -> String {
        return self.todo.title
    }

    public func getId() -> Int {
        return self.todo.id
    }
}

And finally a Todo class:

public class Todo: Codable, Identifiable {    
    public let id: Int
    public var title: String
    public var completed: Bool
}

None of this has actually been tested and not all of the code has been implemented but this should get you on the right track.

Toggle seems to work for both macOS and iOS, using the native control on each.

https://developer.apple.com/documentation/swiftui/toggle

A control that toggles between on and off states.

@State var isOn: Bool = true

var body: some View {
   Toggle("My Checkbox Title", isOn: $isOn)
       .padding()
}

macOS:

macOS checkbox

iOS:

iOS checkbox

Here’s my take on it. I’m actually doing this for MacOS, but the process should be the same.

First, I had to fake the checkbox by creating two png images: enter image description here and enter image description here, calling them checkbox-on.png and checkbox-off.png respectively. These I put into Assets.xcassets.

I believe that for iOS, the images are already available.

Second, the view includes a state variable:

@State var checked = false

The rest is to implement a Button with an action, an image, some text, and some modifiers:

Button(action: {
    checked.toggle()
}) {
    Image(checked ? "checkbox-on" :  "checkbox-off")
        .renderingMode(.original)
        .resizable()
        .padding(0)
        .frame(width: 14.0, height: 14.0)
        .background(Color(NSColor.controlBackgroundColor))
    Text("Choose me … !").padding(0)
}
.buttonStyle(PlainButtonStyle())
.background(Color(red: 0, green: 0, blue: 0, opacity: 0.02))
.cornerRadius(0)
  • checked is the boolean variable you want to toggle
  • The image depends on the value of the boolean, using the condition operator to choose between the two
  • renderingMode() ensures that the image appears correctly and resizable() is used to enable frame().
  • The rest of the modifiers are there to tweak the appearance.

Obviously, if you are going to make a habit of this, you can create a struct:

struct Checkbox: View {
    @Binding var toggle: Bool
    var text: String
    var body: some View {
        Button(action: {
            self.toggle.toggle()
        }) {
            Image(self.toggle ? "checkbox-on" :  "checkbox-off")
                .renderingMode(.original)
                .resizable()
                .padding(0)
                .frame(width: 14.0, height: 14.0)
                .background(Color(NSColor.controlBackgroundColor))
            Text(text).padding(0)
        }
        .buttonStyle(PlainButtonStyle())
        .background(Color(red: 0, green: 0, blue: 0, opacity: 0.02))
        .cornerRadius(0)
    }
}

and then use:

Checkbox(toggle: self.$checked, text: "Choose me … !")

(Note that you need to use self.$checked on this one).

Finally, if you prefer to use a common alternative appearance, that of a filled in square for the check box, you can replace Image with:

Rectangle()
    .fill(self.autoSave ? Color(NSColor.controlAccentColor) : Color(NSColor.controlColor))
    .padding(4)
    .border(Color(NSColor.controlAccentColor), width: 2)
    .frame(width: 14, height: 14)

I learned a lot doing this, and hopefully, this will help.

I found this solution here to be much better than using a completely custom made View:

https://swiftwithmajid.com/2020/03/04/customizing-toggle-in-swiftui/

He uses the ToggleStyle protocol to simply change the look of the toggle, instead of rebuilding it:

struct CheckboxToggleStyle: ToggleStyle {
    func makeBody(configuration: Configuration) -> some View {
        return HStack {
            configuration.label
            Spacer()
            Image(systemName: configuration.isOn ? "checkmark.square" : "square")
                .resizable()
                .frame(width: 22, height: 22)
                .onTapGesture { configuration.isOn.toggle() }
        }
    }
}

Here is my way:

import SwiftUI

extension ToggleStyle where Self == CheckBoxToggleStyle {

    static var checkbox: CheckBoxToggleStyle {
        return CheckBoxToggleStyle()
    }
}

// Custom Toggle Style
struct CheckBoxToggleStyle: ToggleStyle {

    func makeBody(configuration: Configuration) -> some View {
        Button {
            configuration.isOn.toggle()
        } label: {
            Label {
                configuration.label
            } icon: {
                Image(systemName: configuration.isOn ? "checkmark.square.fill" : "square")
                    .foregroundColor(configuration.isOn ? .accentColor : .secondary)
                    .accessibility(label: Text(configuration.isOn ? "Checked" : "Unchecked"))
                    .imageScale(.large)
            }
        }
        .buttonStyle(PlainButtonStyle())
    }
}

struct ContentView: View {

    @State var isOn = false

    var body: some View {
    
        Toggle("Checkmark", isOn: $isOn).toggleStyle(.checkbox)

    }
}

Unchecked:

Unchecked:

Checked:

Checked:

You can use the following code and change the color etc. This is an individual component and I used a callback method to get informed when the checkbox is selected or not.

Step 1: Create a customizable and reusable checkbox view Step 2: Let use the component in the main view Use the checkboxSelected() callback function to know which checkbox is selected or not.

import SwiftUI

//MARK:- Checkbox Field
struct CheckboxField: View {
    let id: String
    let label: String
    let size: CGFloat
    let color: Color
    let textSize: Int
    let callback: (String, Bool)->()

    init(
        id: String,
        label:String,
        size: CGFloat = 10,
        color: Color = Color.black,
        textSize: Int = 14,
        callback: @escaping (String, Bool)->()
        ) {
        self.id = id
        self.label = label
        self.size = size
        self.color = color
        self.textSize = textSize
        self.callback = callback
    }

    @State var isMarked:Bool = false

    var body: some View {
        Button(action:{
            self.isMarked.toggle()
            self.callback(self.id, self.isMarked)
        }) {
            HStack(alignment: .center, spacing: 10) {
                Image(systemName: self.isMarked ? "checkmark.square" : "square")
                    .renderingMode(.original)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: self.size, height: self.size)
                Text(label)
                    .font(Font.system(size: size))
                Spacer()
            }.foregroundColor(self.color)
        }
        .foregroundColor(Color.white)
    }
}

enum Gender: String {
    case male
    case female
}

struct ContentView: View {
    var body: some View {
        HStack{
            Text("Gender")
                .font(Font.headline)
            VStack {
                CheckboxField(
                    id: Gender.male.rawValue,
                    label: Gender.male.rawValue,
                    size: 14,
                    textSize: 14,
                    callback: checkboxSelected
                )
                CheckboxField(
                    id: Gender.female.rawValue,
                    label: Gender.female.rawValue,
                    size: 14,
                    textSize: 14,
                    callback: checkboxSelected
                )
            }
        }
        .padding()
    }

    func checkboxSelected(id: String, isMarked: Bool) {
        print("\(id) is marked: \(isMarked)")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Related