How to navigate in a Animated Custom Tab Bar / SwiftUI - swiftui

I have created with help from another tutorial a Custom tab Bar with animation for my Example App to test the Project etc..
and now I want to add the navigation for each Icon(symbol) so when the user presses TapBarButton 1(Symbol"house") they see the HomeView and when tap TapBarButton 2 the next view is visible with some Data, but I don't understand this topic in moment.
so here is the complete Code from this Section for the Custom Tab Bar.
I don't need a complete solution, I need only a start Point in which section of my code I must implement the Navigation and how is this the best way for Request?.
the code follow here.. ->
struct CustomTabBarNew: View {
#Binding var selectedTab: String
// Storing each Tab Midpoints to animate in future...
#State var tabPoints : [CGFloat] = []
var body: some View {
HStack(spacing: 10) {
// Tab Bar Buttons
TabBarButton(image: "house", selectedTab: $selectedTab, tabPoints: $tabPoints)
TabBarButton(image: "cloud.sun", selectedTab: $selectedTab, tabPoints: $tabPoints)
TabBarButton(image: "paperplane", selectedTab: $selectedTab, tabPoints: $tabPoints)
TabBarButton(image: "plus.app", selectedTab: $selectedTab, tabPoints: $tabPoints)
TabBarButton(image: "gearshape", selectedTab: $selectedTab, tabPoints: $tabPoints)
}
.padding()
.background(
Color.white
.clipShape(TabCurve(taboint: getCurvePoint() - 15))
)
.overlay(
Circle()
.fill(Color.white)
.frame(width: 10, height: 10)
.offset(x: getCurvePoint() - 20)
,alignment: .bottomLeading)
.cornerRadius(30.0)
.padding(.horizontal)
}
// extracting Points..
func getCurvePoint()->CGFloat {
// if tabpoint is empty...
if tabPoints.isEmpty {
return 10
}
else {
switch selectedTab {
case "house":
return tabPoints[0]
case "cloud.sun":
return tabPoints[1]
case "paperplane":
return tabPoints[2]
case "plus.app":
return tabPoints[3]
default:
return tabPoints[4]
}
}
}
}
struct CustomTabBarNew_Previews: PreviewProvider {
static var previews: some View {
HomeNewView()
}
}
struct TabBarButton: View {
var image: String
#Binding var selectedTab: String
#Binding var tabPoints: [CGFloat]
var body: some View{
// mid point for each animation of each button...
GeometryReader { reader -> AnyView in
let midX = reader.frame(in: .global).midX
DispatchQueue.main.async {
// avoiding junk data...
if tabPoints.count <= 5 {
tabPoints.append(midX)
}
}
// extracting Midpoint and Storing ...
return AnyView(
Button(action:
// changing Tab..
// Spring animation...
{
withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.5, blendDuration: 0.5)) {
selectedTab = image
}
}, label: {
// filling the color if its selcted..
Image(systemName: "\(image)\(selectedTab == image ? ".fill" : "")")
.font(.system(size: 25, weight: .semibold))
.foregroundColor(Color("TabSelected"))
// Lifting View..
// if its selected
.offset(y: selectedTab == image ? -10 : 0)
})
// MAX FRAME
.frame(maxWidth: .infinity, maxHeight: .infinity)
)
}
// MAX Height
.frame(height: 50)
}
}

Related

SwiftUI migrate NavigationView to NavigationStack or something

I'm trying to take my app and migrate my opening NavigationView which has been deprecated to the new NavigationStack.
My opening screen looks like:
and the code that presents it is:
import SwiftUI
struct MainAppView: View {
#State var listSelection: Int = 1
#Binding var isLoggedIn: Bool
var body: some View {
NavigationView {
if DBManager.shared.checkDatabaseFileExists()
{
SideListView(listSelection: $listSelection)
HStack {
if listSelection == 1
{
AccountsListView()
}
if listSelection == 2
{
SetAsideBalanceListView()
}
if listSelection == 3
{
BankBalanceListView()
}
if listSelection == 4
{
SetupMenuView()
}
if listSelection == 5
{
ReportsListView()
}
} // END HSTACK
} else {
Text("NO DATABASE FOUND!!!!")
.font(.system(size: 60))
} // END OUTTER IF/ELSE DATABASE FOUND
} // END NAVIGATION VIEW
} // END BODY VIEW
} // END STRUCT VIEW
I did the simple thing of changing NavigationView to NavigationStack and while it compiles, it looks wrong:
Chris... the solution that I implemented using your suggestion is working and would be acceptable, except that the navigation behavior seems to have changed with NavigationSplitView and NavigationStack. When a selection is made in the left pane the corresponding view appears in the right pane. The view in the right pane has a NavigationLink to a subview. This works and the subview has the back button. What I noticed is that with NavigationView if the user in a subview clicks on the selection in the left pane, the view immediately pops the appropriate selected view to the right pane clearing the subview of some other selection that is showing. But in this model, using NavigationStack on the selected view, if the subview is showing for a given selection, clicking on the left pane selection has no effect until the existing subview is back buttoned to the parent view at which time the selected view is presented.
Like this:
And then selected the sub view looks OK like this:
But when I select in the left pane another selection like this:
Using NavigationView the subview for SetAside would immediately pop but instead only shows after I use the back button on the sub view...
UPDATES:
Here is the code I've implemented for the MainAppView.swift
import SwiftUI
struct MainAppView: View {
#State var listSelection: Int? = 1
#Binding var isLoggedIn: Bool
var body: some View {
if DBManager.shared.checkDatabaseFileExists()
{
NavigationSplitView {
SideListView(listSelection: $listSelection)
} detail: {
NavigationStack {
switch listSelection {
case 1: AccountsListView()
case 2: SetAsideBalanceListView()
case 3: BankBalanceListView()
case 4: SetupMenuView()
case 5: ReportsListView()
default: Text("Select View")
}
} // END NAVIGATION STACK
} // END NAVIGATION SPLIT VIEW DETAIL
} else {
Text("NO DATABASE FOUND!!!!")
.font(.system(size: 60))
} // END OUTTER IF/ELSE DATABASE FOUND
} // END BODY VIEW'
} // END STRUCT VIEW
Here is the view code for the SetAsideBalanceListView you can comment out the DB function calls and get an idea of the code. They are all somewhat clones of each other
import SwiftUI
struct SetAsideBalanceListView: View {
var accounts: [Accounts.AccountRecord] {
var acctRec = [Accounts.AccountRecord]()
acctRec = Accounts.shared.selectAllAccounts()
return acctRec
}
var body: some View {
// NavigationStack {
VStack {
CustomDivider(horizontalpadding: 0, thickness: 1)
ForEach (accounts, id: \.self) { accountRec in
let result = Budget.shared.getBudgetMonthAndYearForView(withAccountCode: accountRec.account_code)
NavigationLink(destination: SetAsideBalanceView(account_code: accountRec.account_code, currentBudgetYear: result.viewYear)){
HStack {
Image(systemName: "dollarsign.circle")
.foregroundColor(.white)
.background(Color("iconBlue"))
.cornerRadius(25)
.padding(.leading, 15)
Text(accountRec.account_name)
Spacer()
}
}
.foregroundColor(.primary)
Divider().background(Color.primary)
}// END FOR EACH
} // END VSTACK
.font(.title2)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.top, 20)
.navigationTitle("Managing Your Money - Set Aside")
// } // END NAVIGATION STACK
} // END BODY VIEW
} // END STRUCT VIEW
It looks like this:
And here is the subview that the view above displays.. This is the one that needs a full "back button" to see the selected view.. If you clone these to make the other subviews you'll get the results (I hope:-))
import SwiftUI
struct SetAsideBalanceView: View {
#State var account_code: Int
#State var currentBudgetYear: Int
#State var totalBalances: Double = 0.00
// Array of SetAside Balance records for View List
var setAsideBalances: [SetAsideBalances.SetAsideBalancesRecord]
{
return SetAsideBalances.shared.selectSetAsideBalancesForAccount(withAccountCode: self.account_code)
}
var body: some View {
GeometryReader { gr in
let viewWidth = gr.size.width * 1
let columns = [
GridItem(.fixed(viewWidth * 0.60), alignment: .leading),
GridItem(.fixed(viewWidth * 0.30), alignment: .trailing)
]
VStack {
if setAsideBalances.count > 0 {
SetAsideBalanceHeader(accountName: setAsideBalances[0].account_name!, budgetYear: currentBudgetYear)
ScrollView {
ForEach (setAsideBalances, id: \.self) { setAsideRecord in
SetAsideBalancesRow(accountCode: account_code, setAsideBalanceCode: setAsideRecord.set_aside_code, description: setAsideRecord.description, set_aside_balance: setAsideRecord.set_aside_balance, currentBudgetYear: currentBudgetYear)
.frame(height: 45)
}
}
.frame(height: CGFloat((setAsideBalances.count + 1) * 45))
LazyVGrid(columns: columns, spacing: 0) {
Text("TOTAL BALANCES")
.padding(.leading, 20)
Text("\(NumberFormatter.formatAsCurrency(value: totalBalances))")
.foregroundColor((totalBalances < 0 ) ? Color("negative") : nil)
}
.frame(maxWidth: .infinity)
.frame(height: 55)
.border(Color.primary)
.foregroundColor(.black)
.background(Rectangle().fill(Color("lightBlue")))
.font(.title2)
Spacer()
}
} // END VSTACK
.font(.title2)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.top, 5)
.onAppear {
self.totalBalances = SetAsideBalances.shared.sumSetAsideBalancesForAccount(withAccountCode: self.account_code)
}
.font(.title2)
.navigationTitle("Managing Your Money - Set Aside")
.navigationBarTitleDisplayMode(.inline)
.ignoresSafeArea(edges: .bottom)
}
} // END BODY VIEW
} // END STRUCT VIEW
This is what the subview looks like:
I think I found it. We have to update the NavigationLink in the subview also to the new logic.
In SetAsideBalanceListView replace this:
ForEach (accounts, id: \.self) { accountRec in
let result = Budget.shared.getBudgetMonthAndYearForView(withAccountCode: accountRec.account_code)
NavigationLink(destination: SetAsideBalanceView(account_code: accountRec.account_code, currentBudgetYear: result.viewYear)){
HStack {
Image(systemName: "dollarsign.circle")
.foregroundColor(.white)
.background(Color("iconBlue"))
.cornerRadius(25)
.padding(.leading, 15)
Text(accountRec.account_name)
Spacer()
}
}
.foregroundColor(.primary)
Divider().background(Color.primary)
}// END FOR EACH
with this:
ForEach (accounts, id: \.self) { accountRec in
let result = Budget.shared.getBudgetMonthAndYearForView(withAccountCode: accountRec.account_code)
NavigationLink(value: accountRec) { // HERE
HStack {
Image(systemName: "dollarsign.circle")
.foregroundColor(.white)
.background(Color("iconBlue"))
.cornerRadius(25)
.padding(.leading, 15)
Text(accountRec.account_name)
Spacer()
}
}
.foregroundColor(.primary)
Divider().background(Color.primary)
}// END FOR EACH
// HERE
.navigationDestination(for: AccountRecord.self) { accountRec in
SetAsideBalanceView(account_code: accountRec.account_code, currentBudgetYear: result.viewYear)
}
You want to switch to NavigationSplitView(sidebar: content:). As the name implies it has two elements: 1 the sidebar, and 2 the content area.
I also would like to suggest to exchange the many if statements for the view selection with a switch statement on listSelection.
#State var listSelection: Int = 1
#Binding var isLoggedIn: Bool
var body: some View {
if DBManager.shared.checkDatabaseFileExists()
{
NavigationSplitView {
SideListView(listSelection: $listSelection)
} detail: {
switch listSelection {
case 1: AccountsListView()
case 2: SetAsideBalanceListView()
case 3: BankBalanceListView()
case 4: SetupMenuView()
case 5: ReportsListView()
default: Text("Select View")
}
}
} else {
Text("NO DATABASE FOUND!!!!")
.font(.system(size: 60))
} // END OUTTER IF/ELSE DATABASE FOUND
} // END BODY VIEW
extended version including detail links.
struct ContentView: View {
#State var listSelection: Int? = 1
// #Binding var isLoggedIn: Bool
var body: some View {
// if DBManager.shared.checkDatabaseFileExists()
// {
NavigationSplitView {
List(selection: $listSelection) {
Label("Accounts", systemImage: "dollarsign.circle").tag(1)
Label("Set Aside", systemImage: "folder").tag(2)
Label("Bank Bal.", systemImage: "line.2.horizontal.decrease.circle").tag(3)
Label("Setup", systemImage: "gear").tag(4)
Label("Reports", systemImage: "gear").tag(5)
}
} detail: {
switch listSelection {
case 1: SubView(title: "Accounts")
case 2: SubView(title: "Set Aside")
case 3: SubView(title: "Bank Balance")
case 4: SubView(title: "Setup")
case 5: SubView(title: "Reports")
default: Text("Select View")
}
}
// } else {
// Text("NO DATABASE FOUND!!!!")
// .font(.system(size: 60))
// } // END OUTTER IF/ELSE DATABASE FOUND
} // END BODY VIEW
} // END STRUCT VIEW
struct SubView: View {
let title: String
var body: some View {
NavigationStack {
List(0..<6, id: \.self) { nr in
NavigationLink("\(title) Subview \(nr)", value: nr) // select
}
.navigationDestination(for: Int.self) { nr in // define the destination (Int.self refers to type of selection)
DetailView(item: nr)
}
}
.navigationBarTitle(title)
}
}
struct DetailView: View {
let item: Int
var body: some View {
Text("This is the detail view for item \(item)")
}
}

Function holds System Image rather than image

#ViewBuilder
func TabButton (image: String)-> some View{
Button {
withAnimation{currentTab = image}
} label: {
Image(image)
.resizable()
.renderingMode(.original)
.aspectRatio(contentMode: .fit)
.frame(width: 23, height: 22)
.foregroundColor(currentTab == image ? .primary : .gray)
.frame(maxWidth: .infinity)
}.buttonStyle(GradientButtonStyle())
}
}
I want rather than image from assets as input, system image whom is variable is input here and can change each time and not fixed. thank you
I use it like this,
struct MainView: View {
#State var showMenu: Bool = false
// Hiding Native One.
init(){
UITabBar.appearance().isHidden = true
}
#State var currentTab = "Home"
//offset for both drag gesture and showing menu.
#State var offset: CGFloat = 0
#State var lastStoredOffset: CGFloat = 0
var body: some View {
//let sideBarWidth = getRect().width - 90
// whole navigation view....
NavigationView{
HStack(spacing : 0){
//Side Menu
//SideMenu(showMenu: $showMenu)
//Main Tab View
VStack(spacing: 0){
TabView(selection: $currentTab){
Home(showMenu: $showMenu)
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(true)
.tag("Home")
}
this is the tabview code up
VStack(spacing: 0)
{
Divider()
HStack(spacing:0){
// tab buttons
TabButton(image: "Home")
}
}
}
}
I want system image rather than image here , need help please!
I can only guess ... but I think you want something like this?
struct MainView: View {
// Hiding Native One.
init(){
UITabBar.appearance().isHidden = true
}
#State var currentTab = "house"
var body: some View {
//let sideBarWidth = getRect().width - 90
// whole navigation view....
NavigationView{
VStack(spacing : 0) {
//Side Menu
//SideMenu(showMenu: $showMenu)
//Main Tab View
TabView(selection: $currentTab) {
Text("Home Content") // Home(showMenu: $showMenu)
.tag("house")
Text("Star Content") // ...
.tag("star")
Text("Settings Content") // ...
.tag("gear")
}
.tabViewStyle(.page(indexDisplayMode: .never))
Divider()
Spacer(minLength: 10)
HStack {
// tab buttons
TabButton(image: "house")
TabButton(image: "star")
TabButton(image: "gear")
}
}
}
}
func TabButton (image: String)-> some View{
Button {
withAnimation{ currentTab = image }
} label: {
Image(systemName: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 23, height: 22)
.foregroundColor( currentTab == image ? .primary : .gray)
.frame(maxWidth: .infinity)
}
// .buttonStyle(GradientButtonStyle())
}
}

SwiftUI Crash when calling scrollTo method when view is disappearing

I try to make my ScrollView fixed in a specific place, but the scrollTo method will cause the application to crash.
How to make the ScrollView stay in a fixed place?
I want to control the switching of views by MagnificationGesture to switch from one view to another.
But when Scroll() disappdars, the app crashs.
struct ContentView: View {
#State var tabCount:Int = 1
#State var current:CGFloat = 1
#State var final:CGFloat = 1
var body: some View {
let magni = MagnificationGesture()
.onChanged(){ value in
current = value
}
.onEnded { value in
if current > 2 {
self.tabCount += 1
}
final = current + final
current = 0
}
VStack {
VStack {
Button("ChangeView"){
self.tabCount += 1
}
if tabCount%2 == 0 {
Text("some text")
}else {
Scroll(current: $current)
}
}
Spacer()
HStack {
Color.blue
}
.frame(width: 600, height: 100, alignment: .bottomLeading)
}
.frame(width: 600, height: 400)
.gesture(magni)
}
}
This is ScrollView, I want it can appear and disappear. When MagnificationGesture is changing, scrollview can keep somewhere.
struct Scroll:View {
#Binding var current:CGFloat
let intRandom = Int.random(in: 1..<18)
var body: some View {
ScrollViewReader { proxy in
HStack {
Button("Foreword"){
proxy.scrollTo(9, anchor: .center)
}
}
ScrollView(.horizontal) {
HStack {
ForEach(0..<20) { item in
RoundedRectangle(cornerRadius: 25.0)
.frame(width: 100, height: 40)
.overlay(Text("\(item)").foregroundColor(.white))
.id(item)
}
}
.onChange(of: current, perform: { value in
proxy.scrollTo(13, anchor: .center)
})
}
}
}
}

SwiftUI View Not Updating to Environment Object

I am having an issue getting the ContentBodyView to update properly when a button is pressed in the MenuView.
When I print out the #Published currentPage var, it changes on button press, but doesn't appear to get carried across into ContentBodyView. Am I missing something?
I wrote the code based off of this tutorial: https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
I've tried doing an .onReceive event on ContentBodyView, but the view still did not change.
App.swift:
#main
struct App: App {
#StateObject var viewRouter = ViewRouter()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(viewRouter)
}
}
}
ContentView:
struct ContentView: View {
// MARK: - PROPERTIES
// for future use...
#State var width = UIScreen.main.bounds.width - 90
// to hide view...
#State var x = -UIScreen.main.bounds.width + 90
#EnvironmentObject var viewRouter: ViewRouter
// MARK: - BODY
var body: some View {
VStack {
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) {
ContentBodyView(x: $x)
.environmentObject(ViewRouter())
MenuView(x: $x)
.shadow(color: Color.black.opacity(x != 0 ? 0.1 : 0), radius: 5, x: 5, y: 0)
.offset(x: x)
.background(Color.black.opacity(x == 0 ? 0.5 : 0).ignoresSafeArea(.all, edges: .vertical).onTapGesture {
// hiding the view when back is pressed...
withAnimation {
x = -width
}
}) //: background
.environmentObject(ViewRouter())
} //: ZSTACK
// adding gesture or drag feature...
.gesture(DragGesture().onChanged({ (value) in
withAnimation {
if value.translation.width > 0 {
// disabling over drag...
if x < 0 {
x = -width + value.translation.width
}
} else {
if x != -width {
x = value.translation.width
}
}
}
}).onEnded({ (value) in
withAnimation {
// checking if half the value of menu is dragged means setting x to 0...
if -x < width / 2 {
x = 0
} else {
x = -width
}
}
})) //: GESTURE
} //: VSTACK
}
}
MenuView:
struct MenuView: View {
// MARK: - PROPERTIES
var edges = UIApplication.shared.windows.first?.safeAreaInsets
// for future use...
#State var width = UIScreen.main.bounds.width - 90
// to hide view...
#Binding var x: CGFloat
#EnvironmentObject var viewRouter: ViewRouter
// MARK: - BODY
var body: some View {
HStack(spacing: 0) {
VStack(alignment: .leading) {
HStack{
Button(action: {
withAnimation {
x = -width
}
}) {
Image(systemName: "xmark")
.resizable()
.frame(width: 18, height: 18)
.padding()
.padding(.top, 25)
.foregroundColor(Color.black)
}
Spacer()
}
ForEach(menuData) { item in
Button(action: {
withAnimation {
if (item.router == "shareables") {
viewRouter.currentPage = .shareables
} else {
viewRouter.currentPage = .home
}
x = -width
}
}) {
Text("\(item.label)")
} //: BUTTON
} //: FOREACH
} //: VSTACK
.padding(.horizontal,20)
// since vertical edges are ignored....
.padding(.top,edges!.top == 0 ? 15 : edges?.top)
.padding(.bottom,edges!.bottom == 0 ? 15 : edges?.bottom)
// default width...
.frame(width: UIScreen.main.bounds.width - 90)
.background(Color.white)
.ignoresSafeArea(.all, edges: .vertical)
Spacer(minLength: 0)
} //: HSTACK
}
}
ContentBodyView:
struct ContentBodyView: View {
// MARK: - PROPERTIES
#EnvironmentObject var viewRouter: ViewRouter
#Binding var x : CGFloat
// MARK: - BODY
var body: some View{
VStack {
switch viewRouter.currentPage {
case .home:
NavigationBarView(x: $x, title: "Home")
Spacer()
HomeView()
.transition(.scale)
case .shareables:
NavigationBarView(x: $x, title: "Shareables")
Spacer()
ShareablesView()
.transition(.scale)
} //: SWITCH
} //: VSTACK
// for drag gesture...
.contentShape(Rectangle())
.background(Color.white)
}
}
ViewRouter:
final class ViewRouter: ObservableObject {
#Published var currentPage: Page = .home
}
enum Page {
case home
case shareables
}
Try using the same ViewRouter instance in all views.
The following code creates new instances of ViewRouter:
ContentBodyView(x: $x)
.environmentObject(ViewRouter())
MenuView(x: $x)
...
.environmentObject(ViewRouter())
Replace them with:
#EnvironmentObject var viewRouter: ViewRouter
...
.environmentObject(viewRouter)
But in reality you usually need to inject .environmentObject only once per environment, so all these calls may be unnecessary.
Injecting the ViewRouter once in ContentView should be enough (if you're not using sheet etc):
#StateObject var viewRouter = ViewRouter()
...
ContentView().environmentObject(viewRouter)

How to create Radiobuttons in SwiftUI?

I would like to react on a choice of a user. Something similar to this example:
In a 2nd stage would I like to show additional content below each radiobutton, e.g. moving the buttons 2 and 3 from each other in order to give a list of websites for allowing.
So far I haven't found how to do this in SwiftUI.
Many thanks in advance!
Picker(selection: $order.avocadoStyle, label: Text("Avocado:")) {
Text("Sliced").tag(AvocadoStyle.sliced)
Text("Mashed").tag(AvocadoStyle.mashed)
}.pickerStyle(RadioGroupPickerStyle())
This is the code from the 2019 swiftUI essentials keynote (SwiftUI Essentials - WWDC 2019. Around 43 minutes in the video they show this example.
It will look like this:
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()
}
}
I just edited #LizJ answer , by adding Binding instead of didTapActive & didTapInactive , so like that it will looks like other SwiftUI elements
import SwiftUI
struct RadioButton: View {
#Binding var checked: Bool //the variable that determines if its checked
var body: some View {
Group{
if checked {
ZStack{
Circle()
.fill(Color.blue)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.checked = false}
} else {
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.checked = true}
}
}
}
}
I'm using swift4, Catalina OS and Xcode 11.2 and was having the issue where RadioGroupPickerStyle was unavailable for iOS and .radiogroup just didn't work (it froze in build) so I made my own that's reusable for other occasions. (notice its only the button so you have to handle the logic yourself.) Hope it helps!
import SwiftUI
struct RadioButton: View {
let ifVariable: Bool //the variable that determines if its checked
let onTapToActive: ()-> Void//action when taped to activate
let onTapToInactive: ()-> Void //action when taped to inactivate
var body: some View {
Group{
if ifVariable {
ZStack{
Circle()
.fill(Color.blue)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.onTapToInactive()}
} else {
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.onTapToActive()}
}
}
}
}
TO USE: Put this in any file and you can use it as you would any other view anywhere else in the project. (we keep a global folder that has a buttons file in it)
I will use the previous answer of #LizJ and i will add a text after the radio button to resemble (RadioListTile in Flutter)
struct RadioButton: View {
let ifVariable: Bool //the variable that determines if its checked
let radioTitle: String
var onTapToActive: ()-> Void//action when taped to activate
let onTapToInactive: ()-> Void //action when taped to inactivate
var body: some View {
Group{
if ifVariable {
HStack(alignment: .center, spacing: 16) {
ZStack{
Circle()
.fill(AppColors.primaryColor)
.frame(width: 20, height: 20)
Circle()
.fill(Color.white)
.frame(width: 8, height: 8)
}.onTapGesture {self.onTapToInactive()}
Text(radioTitle)
.font(.headline)
}
} else {
HStack(alignment: .center, spacing: 16){
Circle()
.fill(Color.white)
.frame(width: 20, height: 20)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.onTapGesture {self.onTapToActive()}
Text(radioTitle)
.font(.headline)
}
}
}
}
I will also provide an example for the selection logic
we will create a enum for radio cases
enum PaymentMethod: Int {
case undefined = 0
case credit = 1
case cash = 2
}
then we will create #State variable to carry the selection, i will not recreate another SwiftUI view but only explain the basic concept without any boilerplate code
struct YourView: View {
#State private var paymentMethod: PaymentMethod
var body: some View {
RadioButton(ifVariable: paymentMethod == PaymentMethod.credit,radioTitle: "Pay in Credit", onTapToActive: {
paymentMethod = .credit
}, onTapToInactive: {})
RadioButton(ifVariable: paymentMethod == PaymentMethod.cash,radioTitle: "Pay in Cash", onTapToActive: {
paymentMethod = .cash
}, onTapToInactive: {})
}
}
with this previous code you can toggle between radio buttons in SwiftUI with a text after each selection to resemble (RadioListTile in Flutter)