Related
I have used the new SwiftUI Charts to make a simple bar chart, with month names on the x axis and cost on the y axis. I have added a .chartOverlay view modifier which tracks where I tap on the chart, and shows a BarMark with a text of the cost of the bar tapped.
This was very straightforward to implement, but when starting tapping on the bars, I found that tapping on the first and second bar produced the wanted result, but starting with the third bar, the bar tapped and the second bar was swapped, but the BarMark text showed was correct.
I have used a few hours on this problem, but can't find out what is wrong here.
The complete code is shown here, if anyone can see what is wrong:
import SwiftUI
import Charts
final class ViewModel: ObservableObject {
let months = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr"]
let costs = [20.0, 40.0, 80.0, 60.0, 75.0, 30.0]
#Published var forecast: [Forecast] = []
#Published var selectedMonth: String?
#Published var selectedCost: Double?
init() {
forecast = months.indices.map { .init(id: UUID(), month: months[$0], cost: costs[$0]) }
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Chart(viewModel.forecast) { data in
BarMark(x: .value("Month", data.month), y: .value("Kr", data.cost))
.foregroundStyle(Color.blue)
if let selectedMonth = viewModel.selectedMonth, let selectedCost = viewModel.selectedCost {
RuleMark(x: .value("Selected month", selectedMonth))
.annotation(position: .top, alignment: .top) {
VStack {
Text(estimated(for: selectedMonth, and: selectedCost))
}
}
}
}
.chartYAxis {
AxisMarks(position: .leading)
}
.chartOverlay { proxy in
GeometryReader { geometry in
ZStack(alignment: .top) {
Rectangle().fill(.clear).contentShape(Rectangle())
.onTapGesture { location in
updateSelectedMonth(at: location, proxy: proxy, geometry: geometry)
}
}
}
}
}
func updateSelectedMonth(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) {
let xPosition = location.x - geometry[proxy.plotAreaFrame].origin.x
guard let month: String = proxy.value(atX: xPosition) else {
return
}
viewModel.selectedMonth = month
viewModel.selectedCost = viewModel.forecast.first(where: { $0.month == month })?.cost
}
func estimated(for month: String, and cost: Double) -> String {
let estimatedString = "estimated: \(cost)"
return estimatedString
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.padding()
}
}
struct Forecast: Identifiable {
let id: UUID
let month: String
let cost: Double
}
try this approach, using a ForEach loop and having the RuleMark after it:
var body: some View {
Chart { // <-- here
ForEach(viewModel.forecast) { data in // <-- here
BarMark(x: .value("Month", data.month), y: .value("Kr", data.cost))
.foregroundStyle(Color.blue)
} // <-- here
if let selectedMonth = viewModel.selectedMonth,
let selectedCost = viewModel.selectedCost {
RuleMark(x: .value("Selected month", selectedMonth))
.annotation(position: .top, alignment: .top) {
VStack {
Text(estimated(for: selectedMonth, and: selectedCost))
}
}
}
}
.chartYScale(domain: 0...100) // <-- here
.chartYAxis {
AxisMarks(position: .leading)
}
.chartOverlay { proxy in
GeometryReader { geometry in
ZStack(alignment: .top) {
Rectangle().fill(.clear).contentShape(Rectangle())
.onTapGesture { location in
updateSelectedMonth(at: location, proxy: proxy, geometry: geometry)
}
}
}
}
}
I'm having problem with my app. When I tap on a TextField, trying to fill data, in the sheet that just show up, my app freezes and four errors appear on console:
> [Assert] Animation styles expected to be empty on user driven
> presentation. Actually contains: (
> "<<_UIViewControllerKeyboardAnimationStyle: 0x281ee4d00>; animated = YES; duration = 0.35; force = NO>" )
>
> [KeyboardSceneDelegate] Animation styles were not empty on user driven
> presentation!
>
> Fit_Vein/ProfileView.swift:69: Fatal error: Unexpectedly found nil
> while unwrapping an Optional value
>
> Fit_Vein/ProfileView.swift:69: Fatal error: Unexpectedly found nil
> while unwrapping an Optional value
This situation takes place when I go from ProfileView to SettingsView, then try to do one of options: change email, password or delete account and input data in those sheets.
My code:
ProfileView:
struct ProfileView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#EnvironmentObject private var sessionStore: SessionStore
#Environment(\.colorScheme) var colorScheme
#State private var image = UIImage()
#State private var shouldPresentAddActionSheet = false
#State private var shouldPresentImagePicker = false
#State private var shouldPresentCamera = false
#State private var shouldPresentSettings = false
init(profileViewModel: ProfileViewModel) {
self.profileViewModel = profileViewModel
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
ScrollView(.vertical) {
HStack {
if profileViewModel.profilePicturePhotoURL != nil {
AsyncImage(url: profileViewModel.profilePicturePhotoURL!) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
.onTapGesture {
self.shouldPresentAddActionSheet = true
}
} placeholder: {
Image(uiImage: UIImage(named: "blank-profile-hi")!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
}
} else {
Image(uiImage: UIImage(named: "blank-profile-hi")!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
.onTapGesture {
self.shouldPresentAddActionSheet = true
}
}
Spacer(minLength: screenWidth * 0.05)
VStack {
HStack {
Text(profileViewModel.profile!.firstName)
.foregroundColor(.green)
.font(.system(size: screenHeight * 0.03))
.fontWeight(.bold)
Spacer()
NavigationLink(destination: SettingsView(profile: profileViewModel).environmentObject(sessionStore), isActive: $shouldPresentSettings) {
Button(action: {
shouldPresentSettings = true
}, label: {
Image(systemName: "slider.vertical.3")
.resizable()
.aspectRatio(contentMode: .fit)
})
.frame(width: screenWidth * 0.12, height: screenHeight * 0.04)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
}
.padding(.top, screenHeight * 0.02)
HStack {
Text(profileViewModel.profile!.username)
.foregroundColor(Color(uiColor: UIColor.lightGray))
Spacer()
}
Spacer()
}
}
.padding()
VStack {
Text("Level 1")
.font(.system(size: screenHeight * 0.03))
.fontWeight(.bold)
RoundedRectangle(cornerRadius: 25)
.frame(width: screenWidth * 0.9)
.padding()
.overlay(
HStack {
RoundedRectangle(cornerRadius: 25)
.foregroundColor(.green)
.padding()
.frame(width: screenWidth * 0.7)
Spacer()
}
)
.shadow(color: .gray, radius: 7)
Text("7 / 10 Workouts")
}
Spacer()
}
.sheet(isPresented: $shouldPresentImagePicker) {
ImagePicker(sourceType: self.shouldPresentCamera ? .camera : .photoLibrary, selectedImage: self.$image)
.onDisappear {
profileViewModel.uploadPhoto(image: image)
}
}
.actionSheet(isPresented: $shouldPresentAddActionSheet) {
ActionSheet(title: Text("Add a new photo"), message: nil, buttons: [
.default(Text("Take a new photo"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = true
}),
.default(Text("Upload a new photo"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = false
}),
ActionSheet.Button.cancel()
])
}
}
}
}
SettingsView:
import SwiftUI
struct SettingsView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#StateObject private var sheetManager = SheetManager()
#State private var shouldPresentActionSheet = false
#Environment(\.dismiss) var dismiss
private class SheetManager: ObservableObject {
enum Sheet {
case email
case password
case logout
case signout
}
#Published var showSheet = false
#Published var whichSheet: Sheet? = nil
}
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
Form {
Section(header: Text("Chats")) {
Toggle(isOn: .constant(false), label: {
Text("Hide my activity status")
})
}
Section(header: Text("Account")) {
Button(action: {
sheetManager.whichSheet = .email
sheetManager.showSheet.toggle()
}, label: {
Text("Change e-mail address")
})
Button(action: {
sheetManager.whichSheet = .password
sheetManager.showSheet.toggle()
}, label: {
Text("Change password")
})
Button(action: {
sheetManager.whichSheet = .logout
shouldPresentActionSheet = true
}, label: {
Text("Logout")
.foregroundColor(.red)
})
Button(action: {
sheetManager.whichSheet = .signout
shouldPresentActionSheet = true
}, label: {
Text("Delete account")
.foregroundColor(.red)
})
}
Section(header: Text("Additional")) {
Label("Follow me on GitHub:", systemImage: "link")
.font(.system(size: 17, weight: .semibold))
Link("#Vader20FF", destination: URL(string: "https://github.com/Vader20FF")!)
.font(.system(size: 17, weight: .semibold))
}
}
.navigationBarTitle("Settings")
.navigationBarTitleDisplayMode(.large)
.sheet(isPresented: $sheetManager.showSheet) {
switch sheetManager.whichSheet {
case .email:
ChangeEmailAddressSheetView(profile: profileViewModel)
case .password:
ChangePasswordSheetView(profile: profileViewModel)
case .signout:
DeleteAccountSheetView(profile: profileViewModel)
default:
Text("No view")
}
}
.confirmationDialog(sheetManager.whichSheet == .logout ? "Are you sure you want to logout?" : "Are you sure you want to delete your account? All data will be lost.", isPresented: $shouldPresentActionSheet, titleVisibility: .visible) {
if sheetManager.whichSheet == .logout {
Button("Logout", role: .destructive) {
profileViewModel.sessionStore!.signOut()
dismiss()
}
Button("Cancel", role: .cancel) {}
} else {
Button("Delete Account", role: .destructive) {
sheetManager.showSheet.toggle()
}
Button("Cancel", role: .cancel) {}
}
}
}
}
}
struct DeleteAccountSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var email = ""
#State private var password = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you delete your account please provide your login credentials to confirm it is really you.")) {
TextField("E-mail", text: $email)
SecureField("Password", text: $password)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.deleteUserData() {
profileViewModel.sessionStore!.deleteUser(email: email, password: password) {
print("Successfully deleted user.")
}
}
}
}, label: {
Text("Delete account permanently")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
struct ChangeEmailAddressSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var oldEmail = ""
#State private var password = ""
#State private var newEmail = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you change your e-mail address please provide your login credentials to confirm it is really you.")) {
TextField("Old e-mail address", text: $oldEmail)
SecureField("Password", text: $password)
TextField("New e-mail address", text: $newEmail)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.emailAddressChange(oldEmailAddress: oldEmail, password: password, newEmailAddress: newEmail) {}
}
}, label: {
Text("Change e-mail address")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
struct ChangePasswordSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var email = ""
#State private var oldPassword = ""
#State private var newPassword = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you change your password please provide your login credentials to confirm it is really you.")) {
TextField("E-mail", text: $email)
SecureField("Old password", text: $oldPassword)
SecureField("New password", text: $newPassword)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.passwordChange(emailAddress: email, oldPassword: oldPassword, newPassword: newPassword) {}
}
}, label: {
Text("Change password")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
ProfileViewModel:
import Foundation
import SwiftUI
#MainActor
class ProfileViewModel: ObservableObject {
#Published var sessionStore: SessionStore?
private let firestoreManager = FirestoreManager()
private let firebaseStorageManager = FirebaseStorageManager()
#Published var profile: Profile?
#Published var profilePicturePhotoURL: URL?
#Published var fetchingData = true
init(forPreviews: Bool) {
self.profile = Profile(id: "sessionStore!.currentUser!.uid", firstName: "firstname", username: "username", birthDate: Date(), age: 18, country: "country", city: "city", language: "language", gender: "gender", email: "email", profilePictureURL: nil)
}
init() {
Task {
try await fetchData()
}
}
func setup(sessionStore: SessionStore) {
self.sessionStore = sessionStore
}
func fetchData() async throws {
if sessionStore != nil {
if sessionStore!.currentUser != nil {
print("Fetching Data")
fetchingData = true
let (firstname, username, birthDate, age, country, city, language, gender, email, profilePictureURL) = try await self.firestoreManager.fetchDataForProfileViewModel(userID: sessionStore!.currentUser!.uid)
self.profile = Profile(id: sessionStore!.currentUser!.uid, firstName: firstname, username: username, birthDate: birthDate, age: age, country: country, city: city, language: language, gender: gender, email: email, profilePictureURL: profilePictureURL)
if profilePictureURL != nil {
self.firebaseStorageManager.getDownloadURLForImage(stringURL: profilePictureURL!, userID: sessionStore!.currentUser!.uid) { photoURL in
self.profilePicturePhotoURL = photoURL
}
}
Task {
fetchingData = false
}
}
} else {
fetchingData = false
}
}
func uploadPhoto(image: UIImage) {
if self.profile!.profilePictureURL != nil {
Task {
try await self.firebaseStorageManager.deleteImageFromStorage(userPhotoURL: self.profile!.profilePictureURL!, userID: self.profile!.id)
}
}
self.firebaseStorageManager.uploadImageToStorage(image: image, userID: self.profile!.id) { photoURL in
self.firestoreManager.addProfilePictureURLToUsersData(photoURL: photoURL) {
Task {
try await self.fetchData()
}
}
}
}
func emailAddressChange(oldEmailAddress: String, password: String, newEmailAddress: String, completion: #escaping (() -> ())) {
self.sessionStore!.changeEmailAddress(oldEmailAddress: oldEmailAddress, password: password, newEmailAddress: newEmailAddress) {
print("Successfully changed user's e-mail address")
}
}
func passwordChange(emailAddress: String, oldPassword: String, newPassword: String, completion: #escaping (() -> ())) {
self.sessionStore!.changePassword(emailAddress: emailAddress, oldPassword: oldPassword, newPassword: newPassword) {
print("Successfully changed user's password")
}
}
func deleteUserData(completion: #escaping (() -> ())) {
if self.profile!.profilePictureURL != nil {
self.firestoreManager.deleteUserData(userUID: sessionStore!.currentUser!.uid) {
print("Successfully deleted user data")
Task {
try await self.firebaseStorageManager.deleteImageFromStorage(userPhotoURL: self.profile!.profilePictureURL!, userID: self.sessionStore!.currentUser!.uid)
completion()
}
}
}
}
}
ProfileModel:
import Foundation
import SwiftUI
struct Profile: Codable, Identifiable {
var id: String
var firstName: String
var username: String
var birthDate: Date
var age: Int
var country: String
var city: String
var language: String
var gender: String
var email: String
var profilePictureURL: String?
}
When I select a cell, I want to deselect it from the other cell. After making the selection, I cannot remove the selection from the previous cell. What is the reason of this ? Sorry. I know little English.
Enum
enum Page {
case newest
case populer
case iPhone
case iPad
case mac
case watch
}
ViewModel
class MainViewModel: ObservableObject {
#Published var selectedTab = Page.newest
}
Category Model
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
Basic Category
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
]
Cell
struct TabView: View {
var title: String = ""
var icon: String = ""
var color: Color
var page: Page
#ObservedObject var macMainVM = MainViewModel()
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
self.macMainVM.selectedTab = self.page
}
}) {
VStack {
Image(systemName: icon)
.imageScale(.large)
.foregroundColor(color)
Text(title)
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
//I emphasize the choice here
.background(self.macMainVM.selectedTab == self.page ? RoundedRectangle(cornerRadius: 10).stroke(color) : nil)
}
.buttonStyle(PlainButtonStyle())
}
}
ForEach
VStack {
ForEach(basicCategory, id: \.id) { item in
TabView(title: item.title, icon: item.icon, color: item.color, page: item.page)
}
}
Updated my answer with the working answer.
The problem was that you were using #ObservedObject instead of a #StateObject
Always use #StateObject for the parent view and #ObservedObject for the children view.
import SwiftUI
struct stackT: View {
#StateObject var macMainVM = MainViewModel()
var body: some View {
VStack {
ForEach(basicCategory, id: \.id) { item in
TabView(macMainVM: macMainVM, title: item.title, icon: item.icon, color: item.color, page: item.page)
}
}
}
}
struct stackT_Previews: PreviewProvider {
static var previews: some View {
stackT()
}
}
struct TabView: View {
#ObservedObject var macMainVM: MainViewModel
var title: String = ""
var icon: String = ""
var color: Color
let page: Page
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
DispatchQueue.main.async {
self.macMainVM.globalTab = page
print(macMainVM.globalTab)
}
}
}) {
VStack {
Image(systemName: icon)
.imageScale(.large)
.foregroundColor(color)
Text(title)
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
//I emphasize the choice here
.background(self.macMainVM.globalTab == self.page ? RoundedRectangle(cornerRadius: 10).stroke(color) : RoundedRectangle(cornerRadius: 10).stroke(.clear))
}
.buttonStyle(PlainButtonStyle())
}
}
class MainViewModel: ObservableObject {
#Published var globalTab = Page.newest
}
enum Page {
case newest
case populer
case iPhone
case iPad
case mac
case watch
case none
}
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
Category(title: "Another", icon: "book", color: .blue, page: .mac)
]
Below are the approaches for single selection and multiple selection.
For single selection you can pass Page as Bindings to subView.
For multiple selection you can maintain a boolean state array, that can tell if current cell was already selected or not.
Single Selection
import SwiftUI
enum Page:String {
case newest
case populer
case iPhone
case iPad
case mac
case watch
}
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
class MainViewModel: ObservableObject {
#Published var selectedTab = Page.newest
}
struct TabView: View {
var title: String = ""
var icon: String = ""
var color: Color = .red
var page: Page
#Binding var macMainVM :Page
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
self.macMainVM = self.page
}
}) {
VStack {
Text("Tap")
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
//I emphasize the choice here
.background(self.macMainVM == self.page ? RoundedRectangle(cornerRadius: 10).stroke(color) : nil)
}
.buttonStyle(PlainButtonStyle())
}
}
struct mainView:View{
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
]
#ObservedObject var macMainVM = MainViewModel()
var body: some View {
VStack {
ForEach(basicCategory.indices) { index in
TabView(page: basicCategory[index].page,macMainVM: $macMainVM.selectedTab)
}
}
}
}
Multiple Selection
import SwiftUI
enum Page:String {
case newest
case populer
case iPhone
case iPad
case mac
case watch
}
class MainViewModel: ObservableObject {
#Published var selectedTab = Page.newest
var stateArray:[Bool] = []
}
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
struct TabView: View {
var title: String = ""
var icon: String = ""
var color: Color = Color.red
var page: Page
var count = 0
var onIndex:Int
#ObservedObject var macMainVM = MainViewModel()
init(totalPage:Int,page:Page,onIndex:Int) {
self.count = totalPage
self.page = page
self.onIndex = onIndex
macMainVM.stateArray = [Bool](repeating: false, count: totalPage)
}
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
if macMainVM.stateArray[onIndex] == false{
macMainVM.stateArray[onIndex] = true
}else{
macMainVM.stateArray[onIndex] = false
}
macMainVM.selectedTab = self.page
}
}) {
VStack {
Text("Tap")
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
//I emphasize the choice here
.background(macMainVM.stateArray[onIndex] ? RoundedRectangle(cornerRadius: 10).stroke(color) : nil)
}
.buttonStyle(PlainButtonStyle())
}
}
struct mainView:View{
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
]
//var model = MainViewModel()
var body: some View {
VStack {
ForEach(basicCategory.indices) { index in
TabView(totalPage: basicCategory.count, page: basicCategory[index].page, onIndex: index)
}
}
}
}
NOTE-: I haven’t explained what I did in deep, because it will be more understandable by looking into code.
The most Easy way: You should just use shared object, which you are creating new one every time! I did not edit your code! I just used a shared model! that was all I done! Or you can use environmentObject, and that would also solve the issue without shared one! both way is working!
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
ForEach(basicCategory, id: \.id) { item in
TabView(title: item.title, icon: item.icon, color: item.color, page: item.page)
}
}
.background(Color.gray)
}
}
enum Page {
case newest
case populer
case iPhone
case iPad
case mac
case watch
}
class MainViewModel: ObservableObject {
static let shared: MainViewModel = MainViewModel() // <<: Here
#Published var selectedTab = Page.newest
}
struct Category: Identifiable {
var id = UUID()
var title: String
var icon: String
var color: Color
var page: Page
}
let basicCategory = [
Category(title: "En Yeniler", icon: "flame", color: .red, page: .newest),
Category(title: "Popüler", icon: "star", color: .yellow, page: .populer),
Category(title: "iPhone", icon: "iphone.homebutton", color: .yellow, page: .iPhone),
Category(title: "ipad", icon: "ipad.homebutton", color: .yellow, page: .iPad),
]
struct TabView: View {
let title: String
let icon: String
let color: Color
let page: Page
#ObservedObject var macMainVM = MainViewModel.shared // <<: Here
var body: some View {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
macMainVM.selectedTab = page
}
}) {
VStack {
Image(systemName: icon)
.imageScale(.large)
.foregroundColor(color)
Text(title)
.font(.custom("Oswald-Light", size: 14))
.fixedSize()
}
.padding(5)
.background(macMainVM.selectedTab == page ? RoundedRectangle(cornerRadius: 10).stroke(color) : nil)
}
.buttonStyle(PlainButtonStyle())
}
}
I'm working on my project with the feature of select multiple blocks of thumbnails. Only selected thumbnail(s)/image will be highlighted.
For the ChildView, The binding activeBlock should be turned true/false if a use taps on the image.
However, when I select a thumbnail, all thumbnails will be highlighted.I have come up with some ideas like
#State var selectedBlocks:[Bool]
// which should contain wether or not a certain block is selected.
But I don't know how to implement it.
Here are my codes:
ChildView
#Binding var activeBlock:Bool
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color("orange"))
}
}
}
BlockBView
struct VideoData: Identifiable{
var id = UUID()
var thumbnails: String
}
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "test"), VideoData(thumbnails: "test2"), VideoData(thumbnails: "test1")
]
#State var activeBlock = false
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(0..<videos.count) { _ in
Button(action: {
self.activeBlock.toggle()
}, label: {
ChildView(activeBlock: $activeBlock, thumbnail: "test")
})
}
}
}
}
Thank you for your help!
Here is a demo of possible approach - we initialize array of Bool by videos count and pass activated flag by index into child view.
Tested with Xcode 12.1 / iOS 14.1 (with some replicated code)
struct BlockView: View {
var videos:[VideoData] = [
VideoData(thumbnails: "flag-1"), VideoData(thumbnails: "flag-2"), VideoData(thumbnails: "flag-3")
]
#State private var activeBlocks: [Bool] // << declare
init() {
// initialize state with needed count of bools
self._activeBlocks = State(initialValue: Array(repeating: false, count: videos.count))
}
var body: some View {
ScrollView(.horizontal){
HStack {
ForEach(videos.indices, id: \.self) { i in
Button(action: {
self.activeBlocks[i].toggle() // << here !!
}, label: {
ChildView(activeBlock: activeBlocks[i], // << here !!
thumbnail: videos[i].thumbnails)
})
}
}
}
}
}
struct ChildView: View {
var activeBlock:Bool // << value, no binding needed
var thumbnail: String
var body: some View {
VStack {
ZStack {
Image(thumbnail)
.resizable()
.frame(width: 80, height: 80)
.background(Color.black)
.cornerRadius(10)
if activeBlock {
RoundedRectangle(cornerRadius: 10)
.stroke(style: StrokeStyle(lineWidth: 2))
.frame(width: 80, height: 80)
.foregroundColor(Color.orange)
}
}
}
}
}
Final result
Build your element and it's model first. I'm using MVVM,
class RowModel : ObservableObject, Identifiable {
#Published var isSelected = false
#Published var thumnailIcon: String
#Published var name: String
var id : String
var cancellables = Set<AnyCancellable>()
init(id: String, name: String, icon: String) {
self.id = id
self.name = name
self.thumnailIcon = icon
}
}
//Equivalent to your BlockView
struct Row : View {
#ObservedObject var model: RowModel
var body: some View {
GroupBox(label:
Label(model.name, systemImage: model.thumnailIcon)
.foregroundColor(model.isSelected ? Color.orange : .gray)
) {
HStack {
Capsule()
.fill(model.isSelected ? Color.orange : .gray)
.onTapGesture {
model.isSelected = !model.isSelected
}
//Two way binding
Toggle("", isOn: $model.isSelected)
}
}.animation(.spring())
}
}
Prepare data and handle action in your parent view
struct ContentView: View {
private let layout = [GridItem(.flexible()),GridItem(.flexible())]
#ObservedObject var model = ContentModel()
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(model.rowModels) { model in
Row(model: model)
}
}
}
if model.selected.count > 0 {
HStack {
Text(model.selected.joined(separator: ", "))
Spacer()
Button(action: {
model.clearSelection()
}, label: {
Text("Clear")
})
}
}
}
.padding()
.onAppear(perform: prepare)
}
func prepare() {
model.prepare()
}
}
class ContentModel: ObservableObject {
#Published var rowModels = [RowModel]()
//I'm handling by ID for futher use
//But you can convert to your Array of Boolean
#Published var selected = Set<String>()
func prepare() {
for i in 0..<20 {
let row = RowModel(id: "\(i)", name: "Block \(i)", icon: "heart.fill")
row.$isSelected
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: { [weak self] selected in
guard let `self` = self else { return }
print(selected)
if selected {
self.selected.insert(row.name)
}else{
self.selected.remove(row.name)
}
}).store(in: &row.cancellables)
rowModels.append(row)
}
}
func clearSelection() {
for r in rowModels {
r.isSelected = false
}
}
}
Don't forget to import Combine framework.
I am trying to change a value inside a for in loop. The value Is a bool that I declared as a var and not a let but I get the error "Cannot use mutating member on immutable value: 'notat' is a 'let' constant"
So im trying to make it so that when I tap the image inside the button in my list it will change the completed (bool) value to true. And I want it so that when completed == true I get a filled checkmark
import SwiftUI
struct Notat : Identifiable
{
let id = UUID()
var cost: Int
var name: String
var completed: Bool
}
struct ContentView: View {
var modelData: [Notat] =
[Notat(cost: 50, name: "Klippe plenen", completed: false),
Notat(cost: 100, name: "Vaske speil", completed: true),
Notat(cost: 150, name: "Støvsuge huset", completed: false),
Notat(cost: 50, name: "Vaske bilen", completed: true)]
var body: some View {
List(modelData)
{
notat in HStack
{
Text("\(notat.cost)kr").frame(width: 50, height: 10, alignment: .leading)
Text(notat.name)
Button(action: /*#START_MENU_TOKEN#*/{}/*#END_MENU_TOKEN#*/)
{
Image(systemName: checkmarkSymbol(completed: notat.completed)).font(Font.system(size: 25, weight: .light))
.onTapGesture
{
test(notat: notat)
}
}.frame(maxWidth: .infinity, alignment: .trailing)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
func checkmarkSymbol(completed: Bool) -> String
{
if(completed)
{
return "checkmark.square.fill"
}else
{
return "checkmark.square"
}
}
func test(notat: Notat)
{
notat.completed.toggle() //Here is the error "Cannot use mutating member on immutable value: 'notat' is a 'let' constant"
}
You could do something like the following:
import SwiftUI
struct Notat : Identifiable, Equatable {
let id = UUID()
var cost: Int
var name: String
var completed: Bool
}
//You should change the name to something more descriptive than ViewModel...
class ViewModel: ObservableObject {
#Published var modelData = [
Notat(cost: 50, name: "Klippe plenen", completed: false),
Notat(cost: 100, name: "Vaske speil", completed: true),
Notat(cost: 150, name: "Støvsuge huset", completed: false),
Notat(cost: 50, name: "Vaske bilen", completed: true)
]
func setCompleted(for notat: Notat) {
guard let index = modelData.firstIndex(of: notat) else { return }
modelData[index].completed.toggle()
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
List(viewModel.modelData) { notat in HStack {
Text("\(notat.cost)kr").frame(width: 50, height: 10, alignment: .leading)
Text(notat.name)
Button(action: {})
{
Image(systemName: self.checkmarkSymbol(completed: notat.completed))
.font(Font.system(size: 25, weight: .light))
.onTapGesture
{
self.viewModel.setCompleted(for: notat)
}
}.frame(maxWidth: .infinity, alignment: .trailing)
}
}
}
func checkmarkSymbol(completed: Bool) -> String {
if (completed) {
return "checkmark.square.fill"
}
else {
return "checkmark.square"
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
So basically each time you are tapping on an image, the ViewModel will toggle the completed property and since modelData is marked with #Publish these changes will causing the View to reload itself.
Here it's descriped in more detail...