28

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

0

11 Answers 11

49

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!")
}
...
1
  • 1
    Are you telling that swift ui doesn't have a checkbox? Oo
    – Demigod
    Commented Feb 7, 2023 at 7:35
28

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.

14

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

8

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()

1
  • 1
    You can replace this self.checkState = !self.checkState to self.checkState.toggle() Commented Jan 19, 2023 at 21:17
7

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.

0
6

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.

0
4

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:

3

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() }
        }
    }
}
2

Selectable Circle, Customizable

struct SelectableCircle: View {
@Binding var isSelected: Bool
var selectionColor: Color = Color.green
var size: CGFloat = 20

var body: some View {
    ZStack {
        RoundedRectangle(cornerRadius: 10, style: .circular)
            .stroke(Color.gray, lineWidth: 2)
            .background(isSelected ? selectionColor : Color.clear)
            .frame(width: size, height: size, alignment: .center)
            .clipShape(Circle())
            .onTapGesture {
                withAnimation {
                    isSelected.toggle()
                }
            }
     }
  }
}

You can use like this:

struct CircleChooseView_Previews: PreviewProvider {
struct CircleChooseView: View {
    @State var checked = false

    var body: some View {
        HStack {
            SelectableCircle(isSelected: $checked)
            Text("Item that needs to be selected")
        }
        
    }
}

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

enter image description here

1

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()
    }
}
0

I adapted the solution by Chris for RadioButtons here How to create Radiobuttons in SwiftUI? and made it render a checkbox group:

import SwiftUI

struct ColorInvert: ViewModifier {

    @Environment(\.colorScheme) var colorScheme

    func body(content: Content) -> some View {
        Group {
            if colorScheme == .dark {
                content.colorInvert()
            } else {
                content
            }
        }
    }
}

struct Checkbox: View {

    @Environment(\.colorScheme) var colorScheme

    let id: String
    let callback: (String)->()
    let selectedId: String?
    let size: CGFloat
    let color: Color
    let textSize: CGFloat

    init(
        _ id: String,
        callback: @escaping (String)->(),
        selectedId: String?,
        size: CGFloat,
        color: Color,
        textSize: CGFloat
    ) {
        self.id = id
        self.size = size
        self.color = color
        self.textSize = textSize
        self.selectedId = selectedId
        self.callback = callback
    }

    var body: some View {
        Button(action:{
            self.callback(self.id)
        }) {
            HStack(alignment: .center, spacing: 10) {
                Image(systemName: self.selectedId == self.id ? "checkmark.rectangle" : "rectangle")
                    .renderingMode(.original)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: self.size, height: self.size)
                    .modifier(ColorInvert())
                Text(id)
                    .font(Font.system(size: textSize))
                Spacer()
            }.foregroundColor(self.color)
        }
        .foregroundColor(self.color)
    }
}

struct CheckboxGroup: View {

    let items : [String]
    @Binding var selectedIdList: [String]
    let size: CGFloat
    let color: Color
    let textSize: CGFloat
    let callback: ([String]) -> ()

    init(
        items: [String],
        selectedIdList: Binding<[String]>,
        size: CGFloat = 20,
        color: Color = Color.primary,
        textSize: CGFloat = 25,
        callback: @escaping ([String])->()
    ) {
        self.items = items
        self._selectedIdList = selectedIdList
        self.size = size
        self.color = color
        self.textSize = textSize
        self.callback = callback
    }

    var body: some View {
        VStack {
            ForEach(0..<items.count, id: \.self) { index in
                Checkbox(self.items[index], callback: self.checkboxCallback, selectedId: selectedIdList.contains(items[index]) ? items[index] : nil, size: size, color: color, textSize: textSize)
            }
        }
    }

    func checkboxCallback(id: String) {
        if let index = selectedIdList.firstIndex(of: id) {
            selectedIdList.remove(at: index)
        } else {
            selectedIdList.append(id)
        }
        callback(selectedIdList)
    }
}

Use it like this:

@State private var optionsSelected: [String] = []

CheckboxGroup(items: ["red", "blue", "green"], selectedIdList: $optionsSelected) { selected in
    print("Selected: \(selected)")
}

enter image description here

Not the answer you're looking for? Browse other questions tagged or ask your own question.