I am using SwiftUI to build a feature where a user can click on a button to create rows and each row has multiple views (Textfields, Radio buttons...) [see image below]
I created a swift class DynamicSkills where I handle the creation of the items and the "Add" button, but when I click "Add" the App crashes completely, below is my code so far and an image showing how it should be, I just started with SwiftUI and I am not fully grasping the concept of Binding<>.
What should I do exactly to get it up and running?
//DynamicSkills.swift
struct SkillListEditor: View {
#Binding var evalList: [String]
#Binding var skillList: [String]
func getBindingS(forIndex index: Int) -> Binding<String> {
return Binding<String>(get: { skillList[index] },
set: { skillList[index] = $0 })
}
func getBindingE(forIndex index: Int) -> Binding<String> {
return Binding<String>(get: { evalList[index] },
set: { evalList[index] = $0 })
}
var body: some View {
ForEach(0..<skillList.count, id: \.self) { index in
ListItem(skillEvaluation: getBindingE(forIndex: index), text: getBindingS(forIndex: index)) { self.skillList.remove(at: index) }
}
AddButton{ self.skillList.append("") }
}
}
fileprivate struct ListItem: View {
#Binding var skillEvaluation: String
#Binding var text: String
var removeAction: () -> Void
var body: some View {
VStack(alignment: .leading){
Spacer().frame(height: 8)
HStack{
Button(action: removeAction) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.padding(.horizontal)
}
TextField("Skill", text: $text)
.padding(.all)
.background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0))
.cornerRadius(6)
}
HStack{
Text("Evaluation").font(.callout)
Spacer()
}
HStack{
RadioButtonField(id: "1", label: "1", isMarked: $skillEvaluation.wrappedValue == "1" ? true : false) { selected in
self.skillEvaluation = selected
}
Spacer()
RadioButtonField(id: "2", label: "2", isMarked: $skillEvaluation.wrappedValue == "2" ? true : false) { selected in
self.skillEvaluation = selected
}
Spacer()
RadioButtonField(id: "3", label: "3", isMarked: $skillEvaluation.wrappedValue == "3" ? true : false) { selected in
self.skillEvaluation = selected
}
Spacer()
RadioButtonField(id: "4", label: "4", isMarked: $skillEvaluation.wrappedValue == "4" ? true : false) { selected in
self.skillEvaluation = selected
}
Spacer()
RadioButtonField(id: "5", label: "5", isMarked: $skillEvaluation.wrappedValue == "5" ? true : false) { selected in
self.skillEvaluation = selected
}
}
}
}
}
fileprivate struct AddButton: View {
var addAction: () -> Void
var body: some View {
HStack{
Spacer()
Button(action: addAction) {
Text("add skill").padding(.horizontal, 12).padding(.vertical, 6).background(.white).foregroundColor(Color("PassioGreen")).overlay(RoundedRectangle(cornerRadius: 25).stroke(Color("PassioGreen"), lineWidth: 2))
}
}.padding(.top, 14)
}
}
And in my main screen in ContentView I am showing it this way:
//ContentView.swift
#State var skills = [String]()
#State var skillsEvals = [String]()
struct ContentView: View {
NavigationView{
VStack{
HStack{
Text("Skills").font(.headline)
Spacer()
}
HStack{
Text("Add up to 5 skills").font(.callout)
Spacer()
}
SkillListEditor(evalList: $skillsEvals, skillList: $skills)
}
}
}
Update: this is the custom radio button class CustomRadio.swift
//Custom Radio button used:
struct RadioButtonField: View {
let id: String
let label: String
let isMarked: Bool
let callback: (String)->()
init(
id: String,
label:String,
isMarked: Bool = false,
callback: #escaping (String)->()
) {
self.id = id
self.label = label
self.isMarked = isMarked
self.callback = callback
}
var body: some View {
Button(action:{
self.callback(self.id)
}) {
VStack(alignment: .center) {
Text(label).foregroundColor(self.isMarked ? .white : .black).bold().padding(.horizontal, 8).padding(.vertical, 8)
}
}
.padding()
.background(self.isMarked ? Color("PassioGreen") : Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0))
.cornerRadius(10)
}
}
Your AddButton action appends an item to skillList, but does not append an item to evalList. So when ListItem uses its skillEvaluation binding, the binding's getter tries to access an element of evalList that doesn't exist.
Try this:
AddButton {
self.evalList.append("")
self.skillList.append("")
}
Related
firstly I am really new to iOS development and Swift (2 weeks coming here from PHP :))
I am trying to build a iOS application that has a side menu. And my intention is when I click on a item in the menu the new view will appear on screen like the 'HomeViewController' and for each consequent item like example1, 2 etc (In place of the menu button, Note I will be adding the top nav bar soon to open the menu)
I am wondering how I can accomplish this feature?
Thanks
ContentView.swift
import SwiftUI
struct MenuItem: Identifiable {
var id = UUID()
let text: String
}
func controllView(clickedview:String) {
print(clickedview)
}
struct MenuContent: View{
let items: [MenuItem] = [
MenuItem(text: "Home"),
MenuItem(text: "Example1"),
MenuItem(text: "Example2"),
MenuItem(text: "Example3")
]
var body: some View {
ZStack {
Color(UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1))
VStack(alignment: .leading, spacing: 0) {
ForEach(items) {items in
HStack {
Text(items.text)
.bold()
.font(.system(size: 20))
.multilineTextAlignment(/*#START_MENU_TOKEN#*/.leading/*#END_MENU_TOKEN#*/)
.foregroundColor(Color.white)
Spacer()
}
.onTapGesture {
controllView(clickedview: items.text)
}
.padding()
Divider()
}
Spacer()
}
.padding(.top, 40)
}
}
}
struct SideMenu: View {
let width: CGFloat
let menuOpen: Bool
let toggleMenu: () -> Void
var body: some View {
ZStack {
//Dimmed backgroud
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.15))
.opacity(self.menuOpen ? 1 : 0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.toggleMenu()
}
//Menucontent
HStack {
MenuContent()
.frame(width: width)
.offset(x: menuOpen ? 0 : -width)
.animation(.default)
Spacer()
}
}
}
}
struct ContentView: View {
#State var menuOpen = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
if menuOpen {
withAnimation {
print("Left")
menuOpen.toggle()
}
}
}
if $0.translation.width > -100 {
if !menuOpen {
withAnimation {
print("Right")
menuOpen.toggle()
}
}
}
}
ZStack {
if !menuOpen {
Button(action: {
self.menuOpen.toggle()
}, label: {
Text("Open Menu")
.bold()
.foregroundColor(Color.white)
.frame(width: 200, height: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.background(Color(.systemBlue))
})
}
SideMenu(width: UIScreen.main.bounds.width/1.6, menuOpen: menuOpen, toggleMenu: toggleMenu)
}
.edgesIgnoringSafeArea(.all)
.gesture(drag)
}
func toggleMenu(){
menuOpen.toggle()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The on tap command from the above code:
.onTapGesture {
controllView(clickedview: items.text)
}
HomeViewController.swift
import UIKit
import WebKit
class HomeViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://developer.apple.com")!
webView.load(URLRequest(url: url))
}
}
You'll need a couple of ingredients:
A way to store the state of the currently active view
A way to communicate the state between your menu and main content view
For the first one, I made an enum that listed the different types of views (ViewType) and added it to your MenuItem model.
For the second, you can pass state via a #Binding from parent to child views and back up the chain.
struct MenuItem: Identifiable {
var id = UUID()
let text: String
let viewType : ViewType
}
enum ViewType {
case home, example1, example2, example3
}
struct MenuContent: View{
#Binding var activeView : ViewType
let items: [MenuItem] = [
MenuItem(text: "Home", viewType: .home),
MenuItem(text: "Example1", viewType: .example1),
MenuItem(text: "Example2", viewType: .example2),
MenuItem(text: "Example3", viewType: .example3)
]
var body: some View {
ZStack {
Color(UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1))
VStack(alignment: .leading, spacing: 0) {
ForEach(items) { item in
HStack {
Text(item.text)
.bold()
.font(.system(size: 20))
.multilineTextAlignment(.leading)
.foregroundColor(Color.white)
Spacer()
}
.onTapGesture {
activeView = item.viewType
}
.padding()
Divider()
}
Spacer()
}
.padding(.top, 40)
}
}
}
struct SideMenu: View {
let width: CGFloat
let menuOpen: Bool
let toggleMenu: () -> Void
#Binding var activeView : ViewType
var body: some View {
ZStack {
//Dimmed backgroud
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.15))
.opacity(self.menuOpen ? 1 : 0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.toggleMenu()
}
//Menucontent
HStack {
MenuContent(activeView: $activeView)
.frame(width: width)
.offset(x: menuOpen ? 0 : -width)
.animation(.default)
Spacer()
}
}
}
}
struct ContentView: View {
#State private var menuOpen = false
#State private var activeView : ViewType = .home
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
if menuOpen {
withAnimation {
print("Left")
menuOpen.toggle()
}
}
}
if $0.translation.width > -100 {
if !menuOpen {
withAnimation {
print("Right")
menuOpen.toggle()
}
}
}
}
ZStack {
VStack {
if !menuOpen {
Button(action: {
self.menuOpen.toggle()
}, label: {
Text("Open Menu")
.bold()
.foregroundColor(Color.white)
.frame(width: 200, height: 50, alignment: .center)
.background(Color(.systemBlue))
})
}
switch activeView {
case .home:
HomeViewControllerRepresented()
case .example1:
Text("Example1")
case .example2:
Text("Example2")
case .example3:
Text("Example3")
}
}
SideMenu(width: UIScreen.main.bounds.width/1.6,
menuOpen: menuOpen,
toggleMenu: toggleMenu,
activeView: $activeView)
.edgesIgnoringSafeArea(.all)
}
.gesture(drag)
}
func toggleMenu(){
menuOpen.toggle()
}
}
struct HomeViewControllerRepresented : UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> HomeViewController {
HomeViewController()
}
func updateUIViewController(_ uiViewController: HomeViewController, context: Context) {
}
}
Hi I'm learning SwiftUI and I encountered a problem while trying to build an app. My question is how can I make a tab bar that changes a systemImage while active (when the home button is active it shows "home.fill", and when the user presses the search button, the home button changes to "home").
If you see any improvement that I could make in the code below it's appreciated as well. Thank you, have a nice day, and stay safe.
import SwiftUI
struct ContentView: View {
#State private var fullScreenCover = false
init() {
UITabBar.appearance().isTranslucent = false
}
var body: some View {
VStack {
ForEach(0..<6) { num in
Spacer()
}
HStack {
Spacer()
Button(action: {}, label: {
Image(systemName: "message")
.font(.system(size: 20))
})
.padding(.horizontal, 15)
}
.foregroundColor(Color("Reverse"))
.padding(.vertical, 8)
TabView {
Main_Home()
.tabItem {
Image(systemName: "house")
.font(.system(size: 30))
}
Main_Search()
.tabItem {
Image(systemName: "magnifyingglass")
.font(.system(size: 30))
}
Main_AddContent()
.tabItem {
Image(systemName: "plus.viewfinder")
.font(.system(size: 30))
}
Main_Message()
.tabItem {
Image(systemName: "pencil.circle.fill")
.font(.system(size: 30))
}
Main_Profile()
.tabItem {
Image(systemName: "person.crop.circle")
.font(.system(size: 30))
}
}
.accentColor(Color("Reverse"))
}
.background(Color("Normal"))
.edgesIgnoringSafeArea(.all)
}
}
Here is a custom ViewModifier that you can use for adding tab items:
public struct TabViewItem<SelectionValue>: ViewModifier where SelectionValue: Hashable {
#Binding private var selectedIndex: SelectionValue
private let index: SelectionValue
private let text: String
private let imageName: String
public init(selectedIndex: Binding<SelectionValue>, index: SelectionValue, text: String, imageName: String) {
self._selectedIndex = selectedIndex
self.index = index
self.text = text
self.imageName = imageName
}
public func body(content: Content) -> some View {
content
.tabItem {
image
Text(text)
}
.tag(index)
}
private var image: some View {
guard selectedIndex == index else {
return Image(systemName: imageName)
}
let imageName = self.imageName + ".fill"
if let uiImage = UIImage(systemName: imageName) {
return Image(uiImage: uiImage)
}
return Image(systemName: self.imageName)
}
}
public extension View {
func tabItem<Selection>(
_ text: String,
imageName: String,
index: Selection,
selection: Binding<Selection>
) -> some View where Selection: Hashable {
modifier(TabViewItem(selectedIndex: selection, index: index, text: text, imageName: imageName))
}
}
Then, creating the TabView is more straightforward:
struct ContentView: View {
#State private var selectedTab = 3
var body: some View {
TabView(selection: $selectedTab) {
Text("Main_Home")
.tabItem("Home", imageName: "house", index: 1, selection: $selectedTab)
Text("Main_Search")
.tabItem("Search", imageName: "magnifyingglass", index: 2, selection: $selectedTab)
Text("Main_AddContent")
.tabItem("AddContent", imageName: "plus.viewfinder", index: 3, selection: $selectedTab)
Text("Main_Message")
.tabItem("Message", imageName: "pencil.circle", index: 4, selection: $selectedTab)
Text("Main_Profile")
.tabItem("Profile", imageName: "person.crop.circle", index: 5, selection: $selectedTab)
}
}
}
Note: The above implementation is for generic purposes - if you don't need some parts (like text in tabs) just remove it or make them optional.
I have a custom modal View which is part of the ZStack which overlays the other content when enabled.
When a button is pressed, I want the modal sheet to transition from the bottom edge of the device to the centre of the screen, which I have somewhat accomplished. However, the animation somewhat fails when dismissing the modal view, as seen in the provided video, and I'm having difficulties figuring out why this is.
The animation of the modal view I'm using is
.animation(Animation.spring().speed(1.5))
.transition(.move(edge: .bottom))
For the sake of completion, here is my modal view:
struct AddEventView: View {
#State var eventName: String = ""
#State var endDate = Date().addingTimeInterval(60)
#State var gradientIndex: Int = 0
#EnvironmentObject var model: Model
let existingEvent: Event?
let linearGradients: [LinearGradient] = Gradient.gradients.map {
LinearGradient(
gradient: $0,
startPoint: .topTrailing,
endPoint: .bottomLeading
)
}
/// This closure is invoked when the view is dimissed, with a newly created Event passed as its parameter.
/// If the user cancelled this action, `nil` is passed as the parameter
let onDismiss: (Event?) -> Void
var body: some View {
print("Redrawing AddEventView")
return VStack(spacing: 30.0) {
HStack {
Spacer().frame(width: 44)
Spacer()
Text(existingEvent == nil ? "Create Event" : "Edit Event")
.font(.title3)
.bold()
Spacer()
Button(action: {
onDismiss(nil)
}) {
Image(systemName: "xmark.circle.fill")
.imageScale(.large)
}
.frame(width: 44)
}
.padding(.bottom, 5)
.padding(.top, 8)
HStack {
Text("Name of Event").padding(.trailing, 20)
TextField("My Birthday", text: $eventName)
.frame(height: 35)
}
DatePicker(
"Date of Event".padding(toLength: 19, withPad: " ", startingAt: 0),
selection: $endDate,
in: Date()...
)
.frame(height: 35)
ColorChooser(
linearGradients,
selectedIndex: $gradientIndex
)
.frame(height: 75)
Button(action: {
let adjustedEnd = Calendar.current.date(bySetting: .second, value: 0, of: endDate)
let event = Event(
name: eventName,
start: existingEvent?.start ?? Date(),
end: adjustedEnd!,
gradientIndex: gradientIndex
)
onDismiss(event)
}) {
RoundedRectangle(cornerRadius: 13)
.frame(maxWidth: .infinity)
.frame(height: 42)
.overlay(
Text(existingEvent == nil ? "Add Event" : "Edit Event")
.foregroundColor(.white)
.bold()
)
.padding(.horizontal, 1)
}
.padding(.top, 8)
.disabled(self.eventName.isEmpty)
}
.padding(.all, 16)
.background(Color.white)
.cornerRadius(16)
.shadow(radius: 16)
.onAppear {
if let event = existingEvent {
self.eventName = event.name
self.endDate = event.end
self.gradientIndex = event.gradientIndex
}
}
}
}
and my ContentView:
struct ContentView: View {
#State var progress: Double = 0.0
#State var showModal: Bool = false
#State var showPopover: Bool = false
#State var modifiableEvent: Event?
#State var now: Date = Date()
#State var confettiView = ConfettiUIView()
#EnvironmentObject var model: Model
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
let columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
var alertButtons: [Alert.Button] {
return Model.SortableKeyPaths.map { key, _ in
.default(Text(key)) { model.sortedKey = key }
}
}
func onEventEnd() {
self.confettiView.emit(with: [.text("🎉")])
AudioManager.shared.play("Success 1.mp4")
let taptics = UINotificationFeedbackGenerator()
taptics.notificationOccurred(.success)
}
var grid: some View {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(model.events, id: \.self) { event in
SmallCardView(event: event)
.contextMenu {
Button(action: {
modifiableEvent = event
withAnimation {
self.showModal = true
}
}) {
Text("Edit")
Image(systemName: "slider.horizontal.3")
}
Button(action: {
model.removeEvent(event)
}) {
Text("Delete")
Image(systemName: "trash")
}
}
.animation(.linear)
}
if !showModal || modifiableEvent != nil {
AddEventButtonView() {
modifiableEvent = nil
self.showModal = true
}
} else {
Spacer().frame(height: 100)
}
}
.navigationBarTitle(Text("My Events"), displayMode: .large)
.navigationBarItems(
leading: Button(action: { }) {
Image(systemName: "ellipsis")
.imageScale(.large)
},
trailing: Button(action: { self.showPopover = true }) {
Image(systemName: "arrow.up.arrow.down").imageScale(.large)
}
.actionSheet(isPresented: $showPopover) {
ActionSheet(
title: Text("Sort Events"),
buttons: alertButtons + [.cancel()]
)
}
)
}
var body: some View {
return ZStack {
NavigationView {
ScrollView {
grid.padding(.horizontal, 16)
}
.padding(.top)
}
.brightness(self.showModal ? -0.1 : 0)
.blur(radius: self.showModal ? 16 : 0)
.scaleEffect(self.showModal ? 0.95 : 1)
if self.showModal {
AddEventView(existingEvent: modifiableEvent) { event in
if let event = event {
self.model.removeEvent(modifiableEvent)
self.model.addEvent(event)
}
withAnimation {
self.showModal = false
}
}
.padding(.horizontal, 16)
.zIndex(1.0)
.animation(Animation.spring().speed(1.5))
.transition(.move(edge: .bottom))
}
EmptyView().id("\(self.now.hashValue)")
}
.overlay(
UIViewWrapper(view: $confettiView)
.edgesIgnoringSafeArea(.all)
.allowsHitTesting(false)
)
.onReceive(timer) { _ in
if !showModal { self.now = Date() }
if model.events.contains(where: { -1...0 ~= $0.timeRemaining }) {
onEventEnd()
}
}
}
}
I following the link to create RadioButtons.
import SwiftUI
//MARK:- Single Radio Button Field
struct RadioButtonField: View {
let id: String
let label: String
let size: CGFloat
let color: Color
let textSize: CGFloat
let isMarked:Bool
let callback: (String)->()
init(
id: String,
label:String,
size: CGFloat = 20,
color: Color = Color.black,
textSize: CGFloat = 14,
isMarked: Bool = false,
callback: #escaping (String)->()
) {
self.id = id
self.label = label
self.size = size
self.color = color
self.textSize = textSize
self.isMarked = isMarked
self.callback = callback
}
var body: some View {
Button(action:{
self.callback(self.id)
}) {
HStack(alignment: .center, spacing: 10) {
Image(systemName: self.isMarked ? "largecircle.fill.circle" : "circle")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: self.size, height: self.size)
Text(label)
.font(Font.system(size: textSize))
Spacer()
}.foregroundColor(self.color)
}
.foregroundColor(Color.white)
}
}
//MARK:- Group of Radio Buttons
enum Gender: String {
case male = "Male"
case female = "Female"
}
struct RadioButtonGroups: View {
let callback: (String) -> ()
#State var selectedId: String = ""
var body: some View {
VStack {
radioMaleMajority
radioFemaleMajority
}
}
var radioMaleMajority: some View {
RadioButtonField(
id: Gender.male.rawValue,
label: Gender.male.rawValue,
isMarked: selectedId == Gender.male.rawValue ? true : false,
callback: radioGroupCallback
)
}
var radioFemaleMajority: some View {
RadioButtonField(
id: Gender.female.rawValue,
label: Gender.female.rawValue,
isMarked: selectedId == Gender.female.rawValue ? true : false,
callback: radioGroupCallback
)
}
func radioGroupCallback(id: String) {
selectedId = id
callback(id)
}
}
struct ContentView: View {
var body: some View {
HStack {
Text("Gender")
.font(Font.headline)
RadioButtonGroups { selected in
print("Selected Gender is: \(selected)")
}
}.padding()
}
}
The code above works well. But when I put the HStack of RadioButtons into Form, the RadioButtons does not work. I cannot select the RadioButtons in this situation. The code is as following:
Form {
HStack {
Text("Gender")
.font(Font.headline)
RadioButtonGroups { selected in
print("Selected Gender is: \(selected)")
}
}.padding()
}
Thanks for any help.
Use instead the following body for RadioButtonField (tested with Xcode 11.4)
var body: some View {
HStack(alignment: .center, spacing: 10) {
Image(systemName: self.isMarked ? "largecircle.fill.circle" : "circle")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: self.size, height: self.size)
Text(label)
.font(Font.system(size: textSize))
Spacer()
}.foregroundColor(self.color)
.onTapGesture {
self.callback(self.id)
}
}
I'm creating a simple form app. In that, I have checkboxes and Radio buttons, but I don't know how to do it.
I have done below code to dynamically change the selected option's colour. But it can be select multiple values. I need to select only one value among 5 values like a radio button.
E.g:
I'm taping on the second radio button. Now if I select the fourth radio button, the second one should deselect and the fourth one should get selected.
struct DCTableCell: View {
#Binding var dcValue: String
#State var isSelected: Bool = false
var body: some View {
Button(action: {
print("Tapped")
self.isSelected.toggle()
}){
ZStack {
RoundedRectangle(cornerRadius: 8)
.stroke(self.isSelected ? Color.init("borderSelected"): Color.init("border"))
.frame(height: 56)
.foregroundColor(.clear)
HStack {
Text(dcValue)
.font(.custom("Montserrat", size: 16))
.fontWeight(.medium)
.foregroundColor(self.isSelected ? Color.init("borderSelected") : .white)
.padding()
Spacer()
ZStack {
Circle()
.stroke(self.isSelected ? Color.init("borderSelected") : Color("circleBorder"))
.frame(width: 18, height: 18)
.padding()
Circle()
.frame(width: 10, height: 10)
.foregroundColor(self.isSelected ? Color.init("borderSelected"): Color.clear)
}
}
}
}
}
}
check this out...an easy to use SwiftUI RadiobuttonGroup for iOS
you can use it like this:
RadioButtonGroup(items: ["Rome", "London", "Paris", "Berlin", "New York"], selectedId: "London") { selected in
print("Selected is: \(selected)")
}
and here is the code:
struct ColorInvert: ViewModifier {
#Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
Group {
if colorScheme == .dark {
content.colorInvert()
} else {
content
}
}
}
}
struct RadioButton: 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 = 20,
color: Color = Color.primary,
textSize: CGFloat = 14
) {
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 ? "largecircle.fill.circle" : "circle")
.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 RadioButtonGroup: View {
let items : [String]
#State var selectedId: String = ""
let callback: (String) -> ()
var body: some View {
VStack {
ForEach(0..<items.count) { index in
RadioButton(self.items[index], callback: self.radioGroupCallback, selectedID: self.selectedId)
}
}
}
func radioGroupCallback(id: String) {
selectedId = id
callback(id)
}
}
struct ContentView: View {
var body: some View {
HStack {
Text("Example")
.font(Font.headline)
.padding()
RadioButtonGroup(items: ["Rome", "London", "Paris", "Berlin", "New York"], selectedId: "London") { selected in
print("Selected is: \(selected)")
}
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ContentViewDark_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.colorScheme, .dark)
.darkModeFix()
}
}
Ok this is not an ideal solution but it works and hopefully opens your eyes to improve what you have. I give every RadioButton an ID and when the selected ID changes it updates:
struct DCTableCell: View {
var id: Int
#Binding var dcValue: String
#Binding var selectedID: Int
var body: some View {
Button(action: {
print("Tapped")
self.selectedID = self.id
}){
ZStack {
RoundedRectangle(cornerRadius: 8)
.stroke(self.id == self.selectedID ? Color.blue : Color.white)
.frame(height: 56)
.foregroundColor(.clear)
HStack {
Text(dcValue)
.font(.custom("Montserrat", size: 16))
.fontWeight(.medium)
.foregroundColor(self.id == self.selectedID ? .blue : .white)
.padding()
Spacer()
ZStack {
Circle()
.stroke(self.id == self.selectedID ? Color.blue : .black)
.frame(width: 18, height: 18)
.padding()
Circle()
.frame(width: 10, height: 10)
.foregroundColor(self.id == self.selectedID ? Color.blue: Color.clear)
}
}
}
}
}
}
And here how to use it. maybe you should create an array with ids and the strings that you want to pass in.
struct ContentView: View {
#State var str = "lolz"
#State var selectedID = -1
var body: some View {
VStack {
ForEach((1...5), id: \.self) { index in
DCTableCell(id: index, dcValue: self.$str, selectedID: self.$selectedID)
}
}
}
}
I hope this helps!
I have a similar solution, use the labels as the hashable tag which makes it very straight. So you only need to setting the outer layer: CustomDCPicker, like a general picker.
UIHostingController(rootView: CustomDCPicker())
struct CustomDCPicker: View {
#State var dcValue: String = ""
var body: some View {
VStack{
Text(dcValue).bold()
DCTable.init(dcValue: $dcValue, Labels: ["sample1","sample2","sample3","sample4","sample5"])
}
}
}
struct DCTable: View {
#Binding var dcValue: String
var Labels: [String] = []
var body: some View {
ForEach(Labels, id:\.self){
DCTableCell(dcValue: self.$dcValue, myLabel: $0)
}
}
}
struct DCTableCell: View {
#Binding var dcValue: String
var isSelected: Bool {
get{ self.dcValue == self.myLabel}
}
var myLabel : String
var body: some View {
Button(action: {
print("Tapped")
self.dcValue = self.myLabel
}){
ZStack {
RoundedRectangle(cornerRadius: 8.0)
.stroke(self.isSelected ? Color.red: Color.yellow)
.frame(height: 56)
.foregroundColor(.clear)
HStack {
Text(myLabel)
//.font(.custom("Montserrat", size: 16))
.fontWeight(.medium)
.foregroundColor(self.isSelected ? Color.red : .black)
.padding()
Spacer()
ZStack {
Circle()
.stroke(self.isSelected ? Color.red : Color.black)
.frame(width: 18, height: 18)
.padding()
Circle()
.frame(width: 10, height: 10)
.foregroundColor(self.isSelected ? Color.red: Color.clear)
}
}
}
}
}
}