I have custom view as below which i am using in another view
import SwiftUI
public struct TopSheet<Content >: View where Content : View {
private var content: () -> Content
#State private var arrowOffset: Double = 0
public init(#ViewBuilder content: #escaping () -> Content) { self.content = content }
public func expandRatio() -> Double { return max((currentHeight - minHeight) / contentHeight, 0) }
public var body: some View {
HStack(alignment: .center) {
Arrow(offset: arrowOffset)
.stroke(Color.pink, style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
.frame(width: 30, height: 4)
Spacer()
}
}
}
I am using above view in below view
import SwiftUI
struct PassengerView: View {
#State private var passengers: [String] = ["Joe Black", "Eva Green", "Jared Leto"]
var body: some View {
TopSheet {
VStack {
ForEach($passengers, id: \.self) { passenger in
HStack {
Text(passenger.wrappedValue)
Spacer()
}
}
}
.padding(.horizontal, .afklPaddingL)
}
}
}
Here I want to give one condition Arrow() from Topsheet should visible only if passenger count is greater than 1.
I am not sure how should i give this condition as both are in diff view.
Try this:
Add a var to your TopSheet:
var count: Int
Change the constructor to:
public init(count: Int, #ViewBuilder content: #escaping () -> Content) {
self.count = count
self.content = content
}
and your body to:
public var body: some View {
HStack(alignment: .center) {
if count > 1 {
Arrow(offset: arrowOffset)
.stroke(Color.pink, style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
.frame(width: 30, height: 4)
}
Spacer()
}
}
call it like:
TopSheet(count: passengers.count) {
VStack {
........
As passengers is a #State variable it will reavaluate your views as it changes.
Related
I'm developing a fitness app for the Apple Watch that lets you choose an intensity before starting a workout. I want to present the intensity picker before the workout starts, so I tried presenting it as a sheet before navigating to the actual workout view. The problem is that when I try to dismiss the sheet, it is dismissed but it comes right back. I'm using Xcode 14.2 and watchOS 9.1.
This is the main view and also the view that presents the said sheet (the first one, controlled by showingZonePickerView):
import SwiftUI
#main
struct Wise_Watch_AppApp: App {
#StateObject private var workoutManager = WorkoutManager()
#SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
WorkoutView()
}
.sheet(isPresented: $workoutManager.showingZonePickerView, onDismiss: {
workoutManager.showingZonePickerView = false
}) {
WorkoutLevelPickerView(total: 5, completed: 1)
.toolbar(.hidden)
}
.sheet(isPresented: $workoutManager.showingSummaryView) {
SummaryView()
}
.environmentObject(workoutManager)
}
}
}
This is the view where the user can pick the preferred workout:
import SwiftUI
import HealthKit
struct WorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#State var linkActive: Bool = false
var workoutTypes: [HKWorkoutActivityType] = [.cycling, .running, .walking]
var workoutDictionary: Dictionary<String, HKWorkoutActivityType> = [
"figure.outdoor.cycle" : .cycling,
"figure.run" : .running,
"figure.walk" : .walking
]
var body: some View {
List(Array(workoutDictionary.keys), id: \.self) { workoutType in
NavigationLink(
destination: SessionPagingView(),
tag: workoutDictionary[workoutType]!,
selection: $workoutManager.selectedWorkout
) {
Label("\(workoutDictionary[workoutType]!.name)", systemImage: workoutType)
.font(.subheadline)
.fontWeight(.semibold)
.padding()
}
.padding(EdgeInsets(top: 15, leading: 5, bottom: 15, trailing: 5))
}
.listStyle(.carousel)
.navigationBarTitle("Workouts")
.onAppear {
workoutManager.requestAuthorization()
}
}
}
This is the view to which the app navigates when a NavigationLink is pressed:
import SwiftUI
import WatchKit
import ConfettiSwiftUI
struct SessionPagingView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#State private var selection: Tab = .metrics
#State private var counter: Int = 0
#State private var isViewHidden: Bool = true
enum Tab {
case metrics, nowPlaying, milestone
}
var body: some View {
if(isViewHidden) {
sessionView.hidden()
} else {
sessionView
}
}
var sessionView: some View {
TabView(selection: $selection) {
ForEach(workoutManager.tabItems) { item in
VStack {
Text("Congrats!")
.font(.title2)
.fontWeight(.bold)
.confettiCannon(counter: $counter, num: 40, radius: 200)
Text("You just reached")
.font(.subheadline)
.foregroundColor(.cyan)
.tag(Tab.milestone)
Text("\(item.value) km")
.font(.subheadline)
.foregroundColor(.cyan)
.tag(Tab.milestone)
}
}
MetricsView().tag(Tab.metrics)
NowPlayingView().tag(Tab.nowPlaying)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.onAppear {
workoutManager.showingZonePickerView = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isViewHidden = false
}
}
.onChange(of: workoutManager.running) { _ in
displayMetricsView()
}
.onChange(of: workoutManager.tabItems) { _ in
if(workoutManager.tabItems.count > 0) {
displayMilestoneView()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: {
displayMetricsView()
})
}
}
.ignoresSafeArea()
.animation(.easeInOut, value: self.selection)
}
private func displayMetricsView() {
withAnimation {
selection = .metrics
}
}
private func displayMilestoneView() {
withAnimation {
selection = .milestone
WKInterfaceDevice.current().play(.notification)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
counter += 1
})
}
}
}
This is the actual view that I'm presenting inside the sheet:
import SwiftUI
struct WorkoutLevelPickerView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#Environment(\.dismiss) private var dismiss
let total: Int
#State var lineWidth: CGFloat = 16
#State var color: Color = .green
#State var completed: Double = 1.0
#State var currentZone: HeartRateZone = zones[0]
#State var isScrolling: Bool = false
var body: some View {
VStack {
ZStack {
CircleLabelView(
radius: 30,
tracking: 0,
size: .init(width: 120, height: 120),
text: currentZone.intensity.uppercased()
)
.font(.headline)
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: Double(currentZone.labelRotationAngleModifier * currentZone.intensity.count / 2)))
.opacity(isScrolling ? 0 : 1)
.animation(.easeInOut, value: isScrolling)
WorkoutLevelPickerBackgroundView(total: total, lineWidth: lineWidth)
withAnimation(.spring()) {
WorkoutLevelPickerProgressView(total: total, completed: Int(completed), lineWidth: lineWidth, zone: currentZone)
}
VStack {
Button {
dismiss()
} label: {
Image(systemName: currentZone.iconName)
.padding()
.font(.title2)
}
.frame(width: 70, height: 70)
}
}
.frame(width: 150, height: 150)
.focusable()
.digitalCrownRotation(
detent: $completed.animation(.spring()),
from: 1.0,
through: 5.0,
by: 1.0,
sensitivity: .low,
isContinuous: false,
isHapticFeedbackEnabled: true,
onChange: { _ in
isScrolling = true
},
onIdle: {
isScrolling = false
}
)
.digitalCrownAccessory(.hidden)
.onChange(of: completed) {_ in
if(Int(completed) != currentZone.id) {
currentZone = zones[Int(completed) - 1]
print(currentZone.tint.description)
}
}
}
}
}
This is a video of the flow that generates my problem:
I tried presenting the sheet from other views. I also tried dismissing the sheet through a binding, not through the dismiss action. On all these changes, the outcome was the same as before.
Edit:
Forgot to mention that, when the sheet is presented, pressing the side button will dismiss it and the app will work as intended until the user tries to start a new workout.
As Paulw11 mentioned in his comment, the problem was that I was constantly setting showingZonePickerView on true when the sheet was dismissed and the view reappeared.
I created a circularprogress view to be able to show a progress bar according to the steps data. But for some reason I can not reach to the step.count inside my stepView file.
This is my StepView
struct StepView: View {
private var healthStore: HealthStore?
#State private var presentClipboardView = true
#State private var steps: [Step] = [Step]()
init() {
healthStore = HealthStore()
}
private func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
statisticsCollection.enumerateStatistics(from: startOfDay, to: now) { (statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
let step = Step(count: Int(count ?? 0), date: statistics.startDate, wc: Double(count ?? 0 / 1000 ))
steps.append(step)
}
}
var body: some View {
VStack {
ForEach(steps, id: \.id) { step in
VStack {
HStack{
Text("WC")
Text("\(step.wc)")
}
HStack {
Text("\(step.count ?? 0)")
Text("Total Steps")
}
Text(step.date, style: .date)
.opacity(0.5)
CircularProgress(steps: step.count) //ERROR
Spacer()
}
}
.navigationBarBackButtonHidden(true)
}
.onAppear() {
if let healthStore = healthStore {
healthStore.requestAuthorization { (success) in
if success {
healthStore.calculateSteps { (statisticsCollection) in
if let statisticsCollection = statisticsCollection {
updateUIFromStatistics(statisticsCollection)
}
}
}
}
}
}
.onDisappear() {
self.presentClipboardView.toggle()
}
}
}
and this is my circularprogress view
struct CircularProgress: View {
var steps: Binding<Int>
var body: some View {
ZStack {
Color.progressBarColor
.edgesIgnoringSafeArea(.all)
VStack {
ZStack {
Label()
Outline(steps: steps)
}
}
}
}
}
struct Label: View {
var percentage: CGFloat = 20
var body : some View {
ZStack {
Text(String(format: "%.0f", percentage))
.font(Font.custom("SFCompactDisplay-Bold", size: 56))
}
}
}
struct Outline: View {
var steps: Binding<Int>
var percentage: CGFloat = 20
var colors : [Color] = [Color.trackProgressBarColor]
var body: some View {
ZStack {
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle()
.trim(from: 0, to: percentage * 0.01)
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.fill(AngularGradient(gradient: .init(colors: colors), center: .center, startAngle: .zero, endAngle: .init(degrees: 360)))
).animation(.spring(response: 2.0, dampingFraction: 1.0, blendDuration: 1.0))
}
}
}
I am getting this error at stepview WHILE CALLING CIRCULARPROGRESS inside the stepview. I guess I am trying to get the data in the wrong way.
I don't see necessity of binding here, so just replace corresponding places with simple Int:
struct CircularProgress: View {
var steps: Int
and
struct Outline: View {
var steps: Int
So I have a circular progress bar that is declared like this
struct ProgressBar: View {
var progress: CGFloat
var body: some View {
let gradient = LinearGradient(...)
ZStack {
Circle()
.stroke(lineWidth: 25)
.opacity(0.3)
.foregroundColor(Color.secondary)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(gradient ,style: StrokeStyle(lineWidth: 25.0, lineCap: .round, lineJoin: .round))
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
}
}
}
The variable progress is passed into the view and is just simple division of value/total where I have two buttons to reduce or increase value and I use the animation to update the progress cleanly.
I call this view in another view called DetailView
However for some reason the second secondView floats in from the top when I navigate to DetailView from another view. What is happening?
Im not sure if this is a common issue but I can share a video if that might help you.
As requested here is an example.
import SwiftUI
struct ContentView: View {
#State var bookData: [Book] = load("list")
#State private var selectedBook: Book? = nil
#State var showOnboarding = false
var body: some View {
NavigationView {
Form {
Section{
ForEach(bookData){ bookDetail in
NavigationLink(
destination: PageDetail(bookData: $bookData, book: bookDetail),
label: {
BookView(book: bookDetail)
})
}
}
}
}
}
import SwiftUI
struct PageDetail: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#Binding var bookData: [Book]
#State var count = 0
var progress: CGFloat{
let page = value
let total = Int(book.total) ?? 1
let answer = CGFloat(page)/CGFloat(total)
return answer
}
var value: Int{
let page = Int(book.page) ?? 1
let cnt = count
let calc = page + cnt
return calc
}
var book: Book{
var body: some View {
ZStack{
LinearGradient(...)
VStack {
VStack(spacing: -25){
ProgressBar(page: value,total: book.total ,progress: progress)
.frame(width: 250, height: 300)
.padding(.horizontal, 20)
HStack {
Button(action: {
self.count -= 1
}, label: {
Image(systemName: "minus.circle")
})
Button(action: {
self.count += 1
}, label: {
Image(systemName: "plus.circle")
})
}
}
}
}
}
struct ProgressBar: View {
var page: Int
var total: String
var progress: CGFloat
var body: some View {
let gradient = LinearGradient(...)
ZStack {
Circle()
.stroke(lineWidth: 25)
.opacity(0.3)
.foregroundColor(Color.secondary)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(gradient ,style: StrokeStyle(lineWidth: 25.0, lineCap: .round, lineJoin: .round))
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
}
}
}
struct PageDetail_Previews: PreviewProvider {
#State static var previewed = testData
static var previews: some View {
PageDetail(bookData: $previewed, book: previewed[0])
}
}
Only idea - try to make animation value dependent, like below
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(gradient ,style: StrokeStyle(lineWidth: 25.0, lineCap: .round, lineJoin: .round))
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear, value: progress) // << here !!
Below is my code to create a standard segmented control.
struct ContentView: View {
#State private var favoriteColor = 0
var colors = ["Red", "Green", "Blue"]
var body: some View {
VStack {
Picker(selection: $favoriteColor, label: Text("What is your favorite color?")) {
ForEach(0..<colors.count) { index in
Text(self.colors[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(colors[favoriteColor])")
}
}
}
My question is how could I modify it to have a customized segmented control where I can have the boarder rounded along with my own colors, as it was somewhat easy to do with UIKit? Has any one done this yet.
I prefect example is the Uber eats app, when you select a restaurant you can scroll to the particular portion of the menu by selecting an option in the customized segmented control.
Included are the elements I'm looking to have customized:
* UPDATE *
Image of the final design
Is this what you are looking for?
import SwiftUI
struct CustomSegmentedPickerView: View {
#State private var selectedIndex = 0
private var titles = ["Round Trip", "One Way", "Multi-City"]
private var colors = [Color.red, Color.green, Color.blue]
#State private var frames = Array<CGRect>(repeating: .zero, count: 3)
var body: some View {
VStack {
ZStack {
HStack(spacing: 10) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
}.padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)).background(
GeometryReader { geo in
Color.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }
}
)
}
}
.background(
Capsule().fill(
self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
Picker(selection: self.$selectedIndex, label: Text("What is your favorite color?")) {
ForEach(0..<self.titles.count) { index in
Text(self.titles[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(self.titles[self.selectedIndex])")
Spacer()
}
}
func setFrame(index: Int, frame: CGRect) {
self.frames[index] = frame
}
}
struct CustomSegmentedPickerView_Previews: PreviewProvider {
static var previews: some View {
CustomSegmentedPickerView()
}
}
If I'm following the question aright the starting point might be something like the code below. The styling, clearly, needs a bit of attention. This has a hard-wired width for segments. To be more flexible you'd need to use a Geometry Reader to measure what was available and divide up the space.
struct ContentView: View {
#State var selection = 0
var body: some View {
let item1 = SegmentItem(title: "Some Way", color: Color.blue, selectionIndex: 0)
let item2 = SegmentItem(title: "Round Zip", color: Color.red, selectionIndex: 1)
let item3 = SegmentItem(title: "Multi-City", color: Color.green, selectionIndex: 2)
return VStack() {
Spacer()
Text("Selected Item: \(selection)")
SegmentControl(selection: $selection, items: [item1, item2, item3])
Spacer()
}
}
}
struct SegmentControl : View {
#Binding var selection : Int
var items : [SegmentItem]
var body : some View {
let width : CGFloat = 110.0
return HStack(spacing: 5) {
ForEach (items, id: \.self) { item in
SegmentButton(text: item.title, width: width, color: item.color, selectionIndex: item.selectionIndex, selection: self.$selection)
}
}.font(.body)
.padding(5)
.background(Color.gray)
.cornerRadius(10.0)
}
}
struct SegmentButton : View {
var text : String
var width : CGFloat
var color : Color
var selectionIndex = 0
#Binding var selection : Int
var body : some View {
let label = Text(text)
.padding(5)
.frame(width: width)
.background(color).opacity(selection == selectionIndex ? 1.0 : 0.5)
.cornerRadius(10.0)
.foregroundColor(Color.white)
.font(Font.body.weight(selection == selectionIndex ? .bold : .regular))
return Button(action: { self.selection = self.selectionIndex }) { label }
}
}
struct SegmentItem : Hashable {
var title : String = ""
var color : Color = Color.white
var selectionIndex = 0
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
None of the above solutions worked for me as the GeometryReader returns different values once placed in a Navigation View that throws off the positioning of the active indicator in the background. I found alternate solutions, but they only worked with fixed length menu strings. Perhaps there is a simple modification to make the above code contributions work, and if so, I would be eager to read it. If you're having the same issues I was, then this may work for you instead.
Thanks to inspiration from a Reddit user "End3r117" and this SwiftWithMajid article, https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/, I was able to craft a solution. This works either inside or outside of a NavigationView and accepts menu items of various lengths.
struct SegmentMenuPicker: View {
var titles: [String]
var color: Color
#State private var selectedIndex = 0
#State private var frames = Array<CGRect>(repeating: .zero, count: 5)
var body: some View {
VStack {
ZStack {
HStack(spacing: 10) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: {
print("button\(index) pressed")
self.selectedIndex = index
}) {
Text(self.titles[index])
.foregroundColor(color)
.font(.footnote)
.fontWeight(.semibold)
}
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
.modifier(FrameModifier())
.onPreferenceChange(FramePreferenceKey.self) { self.frames[index] = $0 }
}
}
.background(
Rectangle()
.fill(self.color.opacity(0.4))
.frame(
width: self.frames[self.selectedIndex].width,
height: 2,
alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX, y: self.frames[self.selectedIndex].height)
, alignment: .leading
)
}
.padding(.bottom, 15)
.animation(.easeIn(duration: 0.2))
Text("Value: \(self.titles[self.selectedIndex])")
Spacer()
}
}
}
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
struct FrameModifier: ViewModifier {
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: FramePreferenceKey.self, value: geometry.frame(in: .global))
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}
struct NewPicker_Previews: PreviewProvider {
static var previews: some View {
VStack {
SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.blue)
NavigationView {
SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.red)
}
}
}
}
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.
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.
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:
iOS:
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.
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: and , 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.
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:
Checked:
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() }
}
}
}
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()
}
}
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()
}
}
You should take a look to this post, it's awesome!
https://medium.com/better-programming/how-to-create-and-animate-checkboxes-in-swiftui-e428fe7cc9c1