Why the #State value is not updated - swift

I have three buttons, each button corresponds to a value in the data array. When user click a button, the corresponding value will be displayed in a sheet window. Here's my code:
struct TestView: View {
#State var data: [Int] = [100, 200, 300]
#State var presented: Bool = false
#State var index4edit: Int = 1
var body: some View {
HStack{
ForEach(1..<4){ index in
Button(action: {
index4edit = index
presented.toggle()
}){ Text(String(index)) }
.frame(width: 30, height: 30)
.padding()
}
}
.sheet(isPresented: $presented, content: {
Text("\(index4edit): \(data[index4edit-1])")
})
}
}
My problem is, the first time I click a button, no matter which button, it's always display the first data 100. The reason is that the data index4edit was not updated on the first click, it is always 1, but why? Thanks.

That's because sheet view is created on View initalization. When you change your State, your view won't be recreated because your State variable is not used in your view.
To solve it, just use your State variable somewhere in the code like this dummy example
HStack{
ForEach(1..<4){ index in
Button(action: {
index4edit = index
presented.toggle()
}){ Text(String(index) + (index4edit == 0 ? "" : "")) } //<<here is the State used
.frame(width: 30, height: 30)
.padding()
}
}
Now you view will rerender when the State changes, hence your sheet view will be rerendered.

You should set up index4edit to be the real (0-based) index, not index + 1. You should also use data.indices in the ForEach instead of manually inputting the range.
struct TestView: View {
#State private var data: [Int] = [100, 200, 300]
#State private var presented: Bool = false
#State private var index4edit: Int = 0
var body: some View {
HStack {
ForEach(data.indices) { index in
Button(action: {
index4edit = index
presented.toggle()
}){ Text(String(index)) }
.frame(width: 30, height: 30)
.padding()
}
}
.sheet(isPresented: $presented, content: {
Text("\(index4edit): \(data[index4edit])")
})
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}

Related

Tapping on a View to also change its sibling views

Within a VStack, I have 3 views. A view's selection and colour are toggled when tapping on them. I want the previously selected View to be deselected when selecting the next view.
The tapGesture is implemented in each view. I am not sure what is the best way to achieve this.
Thanks.
Here is the code sample:
struct ContentView: View {
#State var tile1 = Tile()
#State var tile2 = Tile()
#State var tile3 = Tile()
var body: some View {
VStack {
TileView(tile: tile1 )
TileView(tile: tile2 )
TileView(tile:tile3 )
}
.padding()
}
}
struct Tile: Identifiable, Equatable{
var id:UUID = UUID()
var isSelected:Bool = false
}
struct TileView: View {
#State var tile:Tile
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill( tile.isSelected ? Color.red : Color.yellow )
.frame(height: 100)
.padding()
.onTapGesture {
tile.isSelected.toggle()
}
}
}
You need to relate the 3 tiles somehow. An Array is an option. Then once they are related you can change the selection at that level.
extension Array where Element == Tile{
///Marks the passed `tile` as selected and deselects other tiles.
mutating func select(_ tile: Tile) {
for (idx, t) in self.enumerated(){
if t.id == tile.id{
self[idx].isSelected.toggle()
}else{
self[idx].isSelected = false
}
}
}
}
Then you can change your views to use the new function.
struct MyTileListView: View {
#State var tiles: [Tile] = [Tile(), Tile(), Tile()]
var body: some View {
VStack {
ForEach(tiles) { tile in
TileView(tile: tile, onSelect: {
//Use the array to select the tile
tiles.select(tile)
})
}
}
.padding()
}
}
struct TileView: View {
//#State just create a copy of the tile `#Binding` is a two-way connection if needed
let tile:Tile
///Called when the tile is selected
let onSelect: () -> Void
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill(tile.isSelected ? Color.red : Color.yellow)
.frame(height: 100)
.padding()
.onTapGesture {
onSelect()
}
}
}

Conditional view modifier sometimes doesn't want to update

As I am working on a study app, I'm tring to build a set of cards, that a user can swipe each individual card, in this case a view, in a foreach loop and when flipped through all of them, it resets the cards to normal stack. The program works but sometimes the stack of cards doesn't reset. Each individual card updates a variable in a viewModel which my conditional view modifier looks at, to reset the stack of cards using offset and when condition is satisfied, the card view updates, while using ".onChange" to look for the change in the viewModel to then update the variable back to original state.
I've printed each variable at each step of the way and every variable updates and I can only assume that the way I'm updating my view, using conditional view modifier, may not be the correct way to go about. Any suggestions will be appreciated.
Here is my code:
The view that houses the card views with the conditional view modifier
extension View {
#ViewBuilder func `resetCards`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition == true {
transform(self).offset(x: 0, y: 0)
} else {
self
}
}
}
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count: currentStudySet.allSets[index].studyItem.count)
.resetCards(studyCards.isDone) { view in
view
}
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
}
}
HomeViewModel
class HomeViewModel: ObservableObject {
#Published var studySet: StudyModel = StudyModel()
#Published var allSets: [StudyModel] = [StudyModel()]
}
I initialize allSets with one StudyModel() to see it in the preview
StudyListViewModel
class StudyListViewModel: ObservableObject {
#Published var cardsSortedThrough: Int = 0
#Published var isDone: Bool = false
}
StudyModel
import SwiftUI
struct StudyModel: Hashable{
var title: String = ""
var days = ["One day", "Two days", "Three days", "Four days", "Five days", "Six days", "Seven days"]
var studyGoals = "One day"
var studyItem: [StudyItemModel] = []
}
Lastly, StudyItemModel
struct StudyItemModel: Hashable, Identifiable{
let id = UUID()
var itemTitle: String = ""
var itemDescription: String = ""
}
Once again, any help would be appreciated, thanks in advance!
I just found a fix and I put .onChange at the end for StudyCardItemView. Basically, the onChange helps the view scan for a change in currentCard.isDone variable every time it was called in the foreach loop and updates offset individuality. This made my conditional view modifier obsolete and just use the onChange to check for the condition.
I still used onChange outside the view with the foreach loop, just to set currentCard.isDone variable false because the variable will be set after all array elements are iterator through.
The updated code:
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
.onChange(of: currentCard.isDone, perform: {_ in
offset = .zero
})
}
}
StudyListView
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count:
currentStudySet.allSets[index].studyItem.count)
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
Hope this helps anyone in the future!

SwiftUI: Why do I get the same pattern of random items in an array?

I'm working on my project and there's a section where you can test yourself on Japanese letters. You tap a button that plays a sound of a letter then you choose the right button out of three with the correct letter. there are 10 questions in total and it should always randomize, the letters but whenever you come back to the view, the first question is always the same. after the first one it randomizes the rest of the questions but the first question always has the same pattern of letters. What I want is getting a random pattern of letters for the first question every time you come back to the view. I'd appreciate any help.
here's the code of QuestionView:
import SwiftUI
struct HiraganaQuiz: View {
var hiraganas: [Japanese] = Bundle.main.decode("Hiragana.json")
#State private var correctAnswer = Int.random(in: 0...45)
#StateObject var soundplayer = Audio()
#State private var answer = ""
#State private var counter = 0
#State private var correctAnswerCounter = 0
#State private var showingAlert = false
#State private var alertMessage = ""
#State private var disabled = false
var body: some View {
ZStack {
Color.darkBackground
.ignoresSafeArea()
VStack {
Text("\(counter) / 10")
.padding(.top,40)
.foregroundColor(.white)
.font(.system(size:30).bold())
Text("Tap the speaker and choose the right letter")
Button {
soundplayer.playSounds(file: hiraganas[correctAnswer].voice1)
} label: {
Image(systemName: "speaker.wave.3.fill")
}
.font(.system(size:70))
height: 110)
HStack {
ForEach (0...2, id: \.self) { index in
Button {
letterTapped(index)
} label: {
Text(hiraganas[index].letter)
}
}
.disabled(disabe())
.foregroundColor(.white)
.font(.system(size: 35).bold())
Text("\(answer)")
.foregroundColor(.white)
.padding(.bottom,20)
.font(.system(size: 30))
Button {
resetTheGame()
} label: {
Text("Next")
}.buttonStyle(.plain)
.font(.system(size: 30).bold())
.frame(width: 200, height: 50)
}
}
.alert("⭐️ Well done ⭐️", isPresented: $showingAlert) {
Button("Retry", action: reset)
} message: {
Text(alertMessage)
}
} .onAppear { hiraganas = Bundle.main.decode("Hiragana.json").shuffled() }
}
I'm surprised this even compiles -- it should be giving you an error about trying to initialize state like this.
Instead, you can shuffle in the properties in the property initializer:
struct HiraganaQuiz: View {
#State private var hiraganas: [Japanese] = Bundle.main.decode("Hiragana.json").shuffled()
Update, to show the request for onAppear usage:
struct HiraganaQuiz: View {
#State private var hiraganas: [Japanese] = []
var body: some View {
ZStack {
// body content
}.onAppear {
hiraganas = Bundle.main.decode("Hiragana.json").shuffled()
}
}
}

SwiftUI picker scale to fit selection

I have a picker like the following that has a frame width of 150. I need the text to fit inside the picker, but the lineLimit(), scaledToFit() and scaledToFill() do not do anything on the picker object.
#State var Status: String = ""
Picker(selection: $Status, label:
Text("Status")) {
Text("yyyyyyyyyyyyyyyyyyyyyyyyyyyyy").tag(0)
.lineLimit(2).scaledToFit().scaledToFill()
Text("zzzzzzzzzzzzzzzzzzzzzzzzzzzzz").tag(1)
.lineLimit(2).scaledToFit().scaledToFill()
}.frame(maxWidth: 150, minHeight: 50, maxHeight: 50, alignment:.center)
.background(Color.gray)
.lineLimit(2)
.scaledToFit()
What is the correct way to scale and fit selected text inside the picker?
Thank you for any help!!
I was unable to figure out how to do it with the picker object in SwiftUI, so I ended up creating a custom picker view that resizes the selected value in the frame.
struct PickerView: View{
#State var picking: Bool = false
#State var Status: String = ""
#State var ValueList = ["yyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]
var body: some View{
CustomPicker(picking: $picking, Status: $Status, ValueList: $ValueList).onTapGesture{
picking = true
}
}
}
struct CustomPicker: View{
#Binding var picking: Bool
#Binding var Status: String
#Binding var ValueList: [String]
var body: some View{
HStack{
HStack{
if Status == "" {
Text("Pick Value")
} else {
Text(Status).lineLimit(2)
}
}.frame(maxWidth: 150, minHeight: 50, maxHeight: 50, alignment:.center)
.background(Color.gray)
}.sheet(isPresented: $picking, content: {
ForEach(ValueList, id: \.self){ item in
Text(item).onTapGesture {
Status = item
picking = false
}.foregroundColor(Color.blue).padding().border(Color.gray)
}
})
}
}
You tap to pick a value
You can then select a value
And it displays correctly within the box
Try rather using scaleToFill with a lineLimit of 1.
Check out Apple's Documentation
Another good StackOverflow answer
Example:
Text("yyyyyyyyyyyyyyyyyyyyyyyyyyyyy").tag(0).lineLimit(1).scaledToFill()

SwiftUI - How to add selection to TabView

I have a JSON API, and I want to show the content inside the PageTabView. It is successful, but the view becomes laggy every time I swipe the page and I got the solution here https://stackoverflow.com/a/66591530/15132219. But the problem is now the page dots doesn't want to change every time I swipe the page. I have tried to solve it for days but still can't find the solution, Here is my Code (I can't show the API link as it's not shareable)
struct HomeBanner: View{
#State var banners: [BannerJSON] = []
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
#State var index = 0
var body: some View{
let bannerPic = "API_LINK"
GeometryReader{ geometry in
VStack(spacing: 2){
if banners.isEmpty{
Spacer()
ProgressView()
Spacer()
}else{
VStack{
ZStack{
TabView(selection: $index){
ForEach(banners, id: \.id){banner in
VStack{
//MARK: Display Banner
NavigationLink(destination: DetailBanner(banner: banner
)){
WebImage(url: URL(string: bannerPic + banner.IMAGE))
.resizable()
.scaledToFit()
}
}
}
}.tabViewStyle(PageTabViewStyle())
.frame(width: geometry.size.width, height: geometry.size.height / 3.5)
}
}
}
}.onAppear{
getBannerData(url: "API_LINK"){
(banners) in
self.banners = banners
}
}
}
}
}
struct BannerJSON{
var RECORD_STATUS: String
var IMAGE: String
var URL_LINK: String
var TITLE: String
var DESCRIPTION: String
}
extension BannerJSON: Identifiable, Decodable{
var id: String {return TITLE}
}
struct SampleTabView: View {
#State var selectedIndex: Int = 0
#State var banners: [Int] = [0,1,2,3,4,5]
var body: some View {
TabView(selection: $selectedIndex){
ForEach(0..<banners.count, id: \.self){ idx in
Text(banners[idx].description)
.tag(idx)//This line is crutial for selection to work put in on the VStack
//You can also make the tag the banner in your loop but your selection variable must be a Banner too.
}
}
}
}