How can I change button status in SwiftUI? - swiftui

I am trying to change the color programmatically of some buttons in SwiftUI.
The buttons are stored in a LazyVGrid. Each button is built via another view (ButtonCell).
I'm using a #State in the ButtonCell view to check the button state.
If I click on the single button, his own state changes correctly, just modifying the #State var of the ButtonCell view. If I try to do the same from the ContentView nothing is happening.
This is my whole ContentView (and ButtonCell) view struct:
struct ContentView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<10) { number in
ButtonCell(value: number + 1)
}
}
}
Button(action: {
ButtonCell(value: 0, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
struct ButtonCell: View {
var value: Int
#State var isEnabled:Bool = false
var body: some View {
Button(action: {
print (value)
print (isEnabled)
isEnabled = true
}) {
Rectangle()
.foregroundColor(isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(value)").foregroundColor(.white)
)
}
}
}
}
How I may change the color of a button in the LazyVGrid by clicking the "Change isEnabled state" button?

You need a different approach here. Currently you try to change the State of ButtonCell from the outside. State variables should always be private and therefore should not be changed from outside. You should swap the state and parameters of ButtonCell into a ViewModel. The ViewModels then are stored in the parent View (ContentView) and then you can change the ViewModels and the child views automatically update. Here is a example for a ViewModel:
final class ButtonCellViewModel: ObservableObject {
#Published var isEnabled: Bool = false
let value: Int
init(value: Int) {
self.value = value
}
}
Then store the ViewModels in the ContentView:
struct ContentView: View {
let buttonViewModels = [ButtonCellViewModel(value: 0), ButtonCellViewModel(value: 1), ButtonCellViewModel(value: 2)]
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<3) { index in
ButtonCell(viewModel: buttonViewModels[index])
}
}
}
Button(action: {
buttonViewModels[0].isEnabled.toggle()
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
}
And implement the ObservedObject approach in ButtonCell.
struct ButtonCell: View {
#ObservedObject var viewModel: ButtonCellViewModel
var body: some View {
Button(action: {
print (viewModel.value)
print (viewModel.isEnabled)
viewModel.isEnabled = true
}) {
Rectangle()
.foregroundColor(viewModel.isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(viewModel.value)").foregroundColor(.white)
)
}
}
}

Related

TabView in sheet: "Simultaneous accesses to ..., but modification requires exclusive access"

I have a CreateNewCard View that is opened in a sheet. In this view I have the KeyboardHelperView that should be displayed if showHelp is true. This view contains a TabView. But when I run this code I get an Simultaneous accesses to XX, but modification requires exclusive access. The weird part now is: When I change the TabView() to List it works without any problem. Is this a SwiftUI bug? Does anyone know how to fix it?
struct CreateNewCard: View {
#State var showHelp = false
var body: some View {
ZStack {
Color(.blue).edgesIgnoringSafeArea(.all)
Text("open")
.onTapGesture {
withAnimation(.spring()){
self.showHelp = true
}
}
KeyboardHelpView(show: $showHelp)
.frame(width: 300, height: 500)
.opacity(showHelp ? 1 : 0)
}
}
}
struct KeyboardHelpView: View {
#Binding var show: Bool
var steps = ["A", "B", "C", "D","E"]
var images = ["1", "2", "3", "4", "5"]
var body: some View {
ZStack(alignment: .topTrailing){
Color(.red)
TabView() {
ForEach(steps.indices) { index in
VStack(alignment: .center, spacing: 10){
Text(steps[index])
Text(images[index])
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
Image(systemName: "xmark.circle.fill")
.padding()
.foregroundColor(Color(.red))
.font(.system(size: 30, weight: .semibold))
.onTapGesture {
withAnimation(Animation.spring()){
self.show = false
}
}
}
.cornerRadius(20)
.background(
RoundedRectangle(cornerRadius: 20)
.shadow(color: Color.black.opacity(0.3), radius: 5, x: 5, y: 5)
)
}
}
EDIT: Okay, the next weird observations: It works on a real device without any problem. And when I comment out .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always)) it works on the simulator as well. I'm not sure whether this is now a Xcode/simulator bug or is this just a random behavior?!

SwiftUI DragGesture blocking List vertical scroll?

i am trying to add swipe inside list cell , swipe to show more options such as Delete, archive etc .
The swipe is working just fine , but the List ( vertical scroll ) is no longer scrolling up down .
Cell Bite :
import SwiftUI
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
//Swipe to custom options ,by "Jack" this option not yet available in SwiftUI
let drag = DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged {
if (self.offset.width > 0 ){ return }
self.offset.width = $0.translation.width
}.onEnded {
if $0.translation.width < -100 {
self.offset = .init(width: -100, height: 0)
} else {
self.offset = .zero
}
}
ZStack{
Rectangle().foregroundColor(.blue).offset(x: offset.width, y: offset.height)
.gesture(drag)
.animation(.easeIn, value: offset)
Text("test").foregroundColor(.white)
}.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 100,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
struct cl_task_Previews: PreviewProvider {
static var previews: some View {
cl_task().previewLayout(.sizeThatFits)
}
}
List main view :
struct Item {
let uuid = UUID()
let value: String
}
struct w_tasks: View {
#State private var items = [Item]()
var body: some View {
ZStack {
List(self.items, id: \.uuid) {item in
cl_task()
}
.simultaneousGesture(DragGesture().onChanged({ value in
//Scrolled
}))
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
self.items.append(Item(value: "Item"))
}, label: {
Text("+")
.font(.system(size: 50))
.frame(width: 77, height: 70)
.foregroundColor(Color.white)
.padding(.bottom, 7)
})
.background(Color(hex : "#216D94"))
.cornerRadius(38.5)
.padding()
.shadow(color: Color.black.opacity(0.3),
radius: 3,
x: 3,
y: 3)
}
}
}
}
}
struct w_tasks_Previews: PreviewProvider {
static var previews: some View {
w_tasks()
}
}
I've posted my question after spending hours solving this issue as i am new to SwiftUI , any advice how to solve it ?
The solution is to give different distance for swipe, like below
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
// give 25 distance makes vertical scroll enabled !!
let drag = DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onChanged {
Tested with Xcode 12.4 / iOS 14.4

How to drag across Views in a LazyVGrid with GeometryReader?

I want to drag across rectangles in a grid and change their color. This code is almost working, but this is not always the right rectangle that reacts: it behaves rather randomly. Any hints?
import SwiftUI
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color] = Array(repeating: Color.gray, count: 4)
#State private var rect = [CGRect]()
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect.insert(geo.frame(in: .global), at: rect.endIndex)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}
If I correctly understood your goal, here is fixed variant (with small modifications to avoid repeated hardcoding).
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color]
#State private var rect: [CGRect]
init() {
_colors = State(initialValue: Array(repeating: .gray, count: data.count))
_rect = State(initialValue: Array(repeating: .zero, count: data.count))
}
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect[item] = geo.frame(in: .global)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}

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)

'Modifying state during view update, this will cause undefined behavior.' error when typing on a textfield (SwiftUI)

I have two textfields, assigned to:
#State private var emailAddress: String = ""
#State private var password: String = ""
Now whenever I am typing on it, the app seems to get stuck and gives me this error:
'Modifying state during view update, this will cause undefined behavior.'
I have a StartView():
class UserSettings: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
#Published var loggedIn : Bool = false {
didSet {
didChange.send(())
}
}
}
struct StartView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
if settings.loggedIn {
return AnyView(TabbarView())
}else {
return AnyView(ContentView())
}
}
}
I have created a ObservableObject class of UserSettings that has loggedIn bool value. When the user taps on 'Log In' button in LogInView(), this bool value becomes true and a new view appears (TabbarView())
This is LogInView():
struct LogInView: View {
#EnvironmentObject var settings: UserSettings
#State private var emailAddress: String = ""
#State private var password: String = ""
var body: some View {
GeometryReader { geometry in
VStack (alignment: .center){
HStack {
Image("2")
.resizable()
.frame(width: 20, height: 20)
Text("Social App")
.font(.system(size: 12))
}.padding(.top, 30)
.padding(.bottom, 10)
Text("Log In to Your Account")
.font(.title)
.font(.system(size: 14, weight: .bold, design: Font.Design.default))
.padding(.bottom, 50)
TextField("Email", text: self.$emailAddress)
.frame(width: geometry.size.width - 45, height: 50)
.textContentType(.emailAddress)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.accentColor(.red)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.cornerRadius(5)
TextField("Password", text: self.$password)
.frame(width: geometry.size.width - 45, height: 50)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.foregroundColor(.gray)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.textContentType(.password)
.cornerRadius(5)
Button(action: {
self.settings.loggedIn = true
}) {
HStack {
Text("Log In")
}
.padding()
.frame(width: geometry.size.width - 40, height: 40)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}
.padding(.bottom, 40)
Divider()
Button(action: {
print("Take to forget password VC")
}) {
Text("Forgot your password?")
}
Spacer()
}
.padding(.bottom, 90)
}
}
}
I know this error appears if I am updating the view while state is being modified (when typing in textfield). But I am not updating the view anywhere in the Log In screen. Then why this error occurs. Help will be appreciated!
This works for me, you don't even need to import Combine! When you use #Published, SwiftUI will automatically synthesize the objectWillChange subject, and will call send whenever the property is mutated. You can still call .send() manually if you need to, but in most cases you won't.
class UserSettings: ObservableObject {
#Published var loggedIn : Bool = false
}
Excerpt from beta 5 release notes:
You can manually conform to ObservableObject by defining an
objectWillChange publisher that emits before the object changes.
However, by default, ObservableObject automatically synthesizes
objectWillChange and emits before any #Published properties change.
This is the full code that is working fine for me (both iPhone Xr and real device, iPad 6th Gen):
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserSettings()))
import SwiftUI
struct ContentView: View {
var body: some View {
StartView()
}
}
class UserSettings: ObservableObject {
#Published var loggedIn : Bool = false
}
struct StartView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
if settings.loggedIn {
return AnyView(Text("LOGGED IN"))
} else {
return AnyView(LogInView())
}
}
}
struct LogInView: View {
#EnvironmentObject var settings: UserSettings
#State private var emailAddress: String = ""
#State private var password: String = ""
var body: some View {
GeometryReader { geometry in
VStack (alignment: .center){
HStack {
Image(systemName: "2.circle.fill")
.resizable()
.frame(width: 20, height: 20)
Text("Social App")
.font(.system(size: 12))
}.padding(.top, 30)
.padding(.bottom, 10)
Text("Log In to Your Account")
.font(.title)
.font(.system(size: 14, weight: .bold, design: Font.Design.default))
.padding(.bottom, 50)
TextField("Email", text: self.$emailAddress)
.frame(width: geometry.size.width - 45, height: 50)
.textContentType(.emailAddress)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.accentColor(.red)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.cornerRadius(5)
TextField("Password", text: self.$password)
.frame(width: geometry.size.width - 45, height: 50)
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
.foregroundColor(.gray)
.background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
.textContentType(.password)
.cornerRadius(5)
Button(action: {
self.settings.loggedIn = true
}) {
HStack {
Text("Log In")
}
.padding()
.frame(width: geometry.size.width - 40, height: 40)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(5)
}
.padding(.bottom, 40)
Divider()
Button(action: {
print("Take to forget password VC")
}) {
Text("Forgot your password?")
}
Spacer()
}
.padding(.bottom, 90)
}
}
}
I guess this is a bug. This message you got is also happening on this simple view which filters out list entries by user input. Just typing fast in the text field causes this issue. If you enter the first character into the text field, the UI stuck for some time.
struct ContentView: View {
#State private var list: [String] = (0..<500).map { "Text \($0)" }
#State private var searchText: String = ""
var filteredList: [String] {
guard !searchText.isEmpty else { return list }
return list.filter({ $0.contains(self.searchText) })
}
var body: some View {
VStack {
TextField("Search", text: $searchText)
List(filteredList, id: \String.self) { t in Text(t) }
}
.padding()
}
}
A workaround is to move the #State variables into a model. So this seems to be an issue with #State:
class Model: ObservableObject {
#Published var list: [String] = (0..<500).map { "Text \($0)" }
#Published var searchText: String = ""
var filteredList: [String] {
guard !searchText.isEmpty else { return list }
return list.filter({ $0.contains(self.searchText) })
}
}
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
VStack {
TextField("Search", text: $model.searchText)
List(model.filteredList, id: \String.self) { t in Text(t) }
}
.padding()
}
}
This may not be related to your issue, but in Xcode 11 Beta 4, Apple changed "didset" to "willset" and "didChange" to "willChange"
In Xcode 11 Beta 5, apple changed "willChange" to "objectWillChange".
Thus the StartView() should be:
class UserSettings: ObservableObject {
var objectWillChange = PassthroughSubject<Void, Never>()
#Published var loggedIn : Bool = false {
willSet {
objectWillChange.send(())
}
}
}
Don't branch with if, use .opacity(_:)
#ViewBuilder
var body: some View {
// if settings.loggedIn {
TabbarView().opacity(settings.loggedIn ? 1 : 0)
// } else {
ContentView().opacity(settings.loggedIn ? 0 : 1)
// }
}