Inactive navigation buttons - swift

I'm trying to make a simple Master-Detail-FileViewer app. In the last FileViewer view I want to have a button, which has an option to make the file favourite (every file has an "id" string, which is appended to an Environment object). When you favour them, this object is shown at the master view for quick access for the user, linking to the FileViewer view. However, when the user taps and goes there, the button is inactive - you cannot tap it and it gets black from blue. If you want to remove them from favourites, you can't.
I'd really appreciate to tell me what is wrong and how to make the button active. No error is shown and the app doesn't crash. It just doesn't work.
Thanks in advance!
The files are either "judgement" and "secondary", both have id and title properties. The second picture is the problematic one.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
NavigationView {
List {
NavigationLink(destination: JudgementsView()) {
Text("Judgements")
}
NavigationLink(destination: SecondaryView()) {
Text("Secondary acts")
}
ScrollView(.horizontal, showsIndicators: false) {
VStack {
if favouriteList.items.isEmpty {
Text("Nothing favoured")
} else {
ForEach(favouriteList.items, id: \.self) { id in
VStack {
HStack {
ForEach(judgementsTAXraw.filter {
$0.id == id
}) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title).padding()
}
}
}
HStack {
ForEach(secondaryTAXraw.filter {
$0.id == id
}) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title).padding()
}
}
}
}
}
}
}
}
}
.navigationBarTitle(Text("Test"))
}
}
}
struct JudgementsView: View {
var body: some View {
List(judgementsTAXraw, id: \.id) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title)
}
}
}
}
struct SecondaryView: View {
var body: some View {
List(secondaryTAXraw, id: \.id) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title)
}
}
}
}
struct FileViewer: View {
var file: String
#State private var showCopySheet = false
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
Text(file)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.showCopySheet = true
}) {
Image(systemName: "doc.on.doc").frame(minWidth: 40)
}.actionSheet(isPresented: $showCopySheet) {
ActionSheet(title: Text("What do you want to do?"), buttons: [
.destructive(Text("favText"), action: {
if let index = self.favouriteList.items.firstIndex(of: self.file) {
self.favouriteList.items.remove(at: index)
} else {
self.favouriteList.items.append(self.file)
}
}),
.cancel()
])
}
)
}
}
Aaaand in a separate file is the object:
import Foundation
class FavouritesList: ObservableObject {
#Published var items = [String]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "FavouredItems")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "FavouredItems") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([String].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
}

Related

SwiftUI Modifying state during view update, this will cause undefined behavior - Proper use explanation

Im learning swift and this error/warning is driving me crazy because I cant see what call Im making that causing it... The Xcode warning only shows up in my #main struct
Modifying state during view update, this will cause undefined behavior.
I thought it might be in the ListView, but I realized the warning only shows after the "Submit Post" button is it.
Im looking for a fix, but more importantly and explanation as to why this is happening and the proper usage moving forward.
import SwiftUI
import Firebase
#main
struct SocialcademyApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
PostsList()
}
}
}
struct PostsList: View {
#StateObject var viewModel = PostsViewModel()
#State private var searchText = ""
#State private var showNewPostForm = false
var body: some View {
NavigationView {
List(viewModel.posts) { post in
if searchText.isEmpty || post.contains(searchText) {
PostRow(post: post)
}
}
.searchable(text: $searchText)
.navigationTitle("Posts")
.toolbar {
Button {
showNewPostForm = true
} label: {
Label("New Post", systemImage: "square.and.pencil")
}
}
.sheet(isPresented: $showNewPostForm) {
NewPostView(creationAction: viewModel.makeCreationAction())
}
}
}
}
struct NewPostView: View {
typealias CreationAction = (Post) async throws -> Void
let creationAction: CreationAction
#State private var post = Post(title: "", content: "", authorName: "")
#State private var state = FormState.idle
#Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
Form {
Section {
TextField("Title", text: $post.title)
TextField("Author Name", text: $post.authorName)
}
Section {
TextField("Content", text: $post.content)
.multilineTextAlignment(.leading)
}
Button(action: createPost, label: {
if state == .working {
ProgressView() } else {
Text("Submit Post")
}
})
.font(.headline)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.padding()
.listRowBackground(Color.accentColor)
}
}
.navigationTitle("New Post")
.disabled(state == .working)
.alert("Cannot Create Post", isPresented: $state.isError, actions: {}) {
Text("Sorry, something went wrong")
}
.onSubmit {
createPost()
}
}
private func createPost() {
print("[NewPostForm] creating a new post")
Task {
state = .working
do {
try await creationAction(post)
dismiss()
} catch {
state = .error
print("[NewPostForm] Cannot create post: \(error)")
}
}
}
}
private extension NewPostView {
enum FormState {
case idle, working, error
var isError: Bool {
get {
self == .error
}
set {
guard !newValue else { return }
self = .idle
}
}
}
}
#MainActor
class PostsViewModel: ObservableObject {
#Published var posts = [Post.testPost]
func makeCreationAction() -> NewPostView.CreationAction {
return { [weak self] post in
try await PostsRepository.create(post)
self?.posts.insert(post, at: 0)
}
}
}

Binding change not detected within List NavigationView

I'm building a picker component and am passing a Binding two layers deep. The issue is that I need to detect it's .onChange on the PickerListView, however, it never gets triggered.
import SwiftUI
public struct SectionItems<T: Hashable>: Hashable {
let header: String
public var items: [T]
public init(header: String, items: [T]) {
self.header = header
self.items = items
}
}
struct PickerListView<T: Hashable>: View {
#Environment(\.dismiss) var dismiss
#Binding var selected: T
#Binding var sections: [SectionItems<T>]
init(selected: Binding<T>, sections: Binding<[SectionItems<T>]>) {
self._selected = selected
self._sections = sections
}
var body: some View {
List {
ForEach(sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in
Button(action: {
selected = item
dismiss()
}) {
HStack {
Text(String(describing: item))
Spacer()
if selected == item {
Image(systemName: "checkmark")
}
}
}
}
}
}
}
.onChange(of: sections) { _ in
print("PickerListView: \(sections.count)") // Doesn't run
}
}
}
import SwiftUI
public struct PickerView<T: Hashable>: View {
#Binding var selected: T
#Binding var sections: [SectionItems<T>]
public var body: some View {
NavigationView {
NavigationLink(
String(describing: selected),
destination: PickerListView(
selected: $selected,
sections: $sections
)
)
}
.onChange(of: sections) { _ in
print("PickerView: \(sections.count)") // Runs
}
}
}
struct ContentView: View {
#State private var selected = "33"
#State private var sections = [
SectionItems(header: "Itemr", items: [
"33",
"73",
"38"
])
]
var body: some View {
VStack {
PickerView(selected: $selected, sections: $sections)
Button("Add Value", action: {
sections[0].items.append("\(Int.random(in: 1...100))")
})
}
.onChange(of: sections) { _ in
print("ContentView: \(sections.count)") // Runs
}
}
}
You can try a different approach, instead of passing multiple items across many Views you can combine all the variables in a single ObservableObject
Then you can use didSet to make any changes you would make with onChange
///Keeps all the code needed for several views in one spot
class SectionViewModel<T: Hashable>: ObservableObject{
#Published var selected : T
#Published var sections : [SectionItems<T>]{
didSet{
//Use didSet instead of onChange
print("\(type(of: self)) :: \(#function) :: \(sections.count)")
print("\(type(of: self)) :: \(#function) :: section[0]items count: \(sections[0].items.count)")
}
}
init(selected : T, sections : [SectionItems<T>]){
self.selected = selected
self.sections = sections
}
}
Your Views would look something like
struct PickerListView<T: Hashable>: View {
#ObservedObject var vm: SectionViewModel<T>
#Environment(\.dismiss) var dismiss
var body: some View {
List {
ForEach(vm.sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in
Button(action: {
vm.selected = item
dismiss()
}) {
HStack {
Text(String(describing: item))
Spacer()
if vm.selected == item {
Image(systemName: "checkmark")
}
}
}
}
}
}
}
//Use didset for any changes
// .onChange(of: vm.sections) { _ in
// print("PickerListView: \(vm.sections.count)") // Doesn't run
// }
}
}
public struct PickerView<T: Hashable>: View {
#ObservedObject var vm: SectionViewModel<T>
public var body: some View {
//Navigation View should be at the top
//NavigationView {
NavigationLink(
String(describing: vm.selected),
destination: PickerListView<T>(vm: vm)
)
// }
//Use did set for any actions
// .onChange(of: vm.sections) { _ in
// print("PickerView: \(vm.sections.count)") // Runs
// print("PickerView section[0]items count: \(vm.sections[0].items.count)") // Runs
// }
}
}
struct SampleDeepView: View {
#StateObject var vm: SectionViewModel = .init(selected: "33", sections: [
SectionItems(header: "Itemr", items: [
"33",
"73",
"38"
])
])
var body: some View {
NavigationView{
VStack {
PickerView(vm: vm)
//Adding an item not a section
Button("Add Value", action: {
vm.sections[0].items.append("\(Int.random(in: 1...100))")
})
//Adding a section
Button("Add Section", action: {
vm.sections.append(
SectionItems(header: "Itemr", items: [
"33",
"73",
"38"
])
)
})
}
}
//Use didset for any actions
// .onChange(of: vm.sections) { _ in
// print("ContentView section[0]items count: \(vm.sections[0].items.count)") // Runs
// }
}
}

SwiftUI conditional doesn't work at top level in view

I have a view model that looks like this:
class SegmentViewModel: ObservableObject {
#Published private(set) var itemIds = [Int]()
private var cancellables: Set<AnyCancellable> = []
init() {
fetchIds()
}
private func fetchIds() {
let request = URLRequest(path: "/ids")
network
.send(request)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error): print(error.localizedDescription)
case .finished: break
}
} receiveValue: { response in
self.itemIds = response
}
.store(in: &cancellables)
}
}
That all works fine and then my View is as follows:
struct SegmentView: View {
#ObservedObject var viewModel: SegmentViewModel
var body: some View {
if viewModel.itemIds.isEmpty {
ProgressView()
} else {
ScrollView {
LazyVStack {
ForEach(viewModel.itemIds, id: \.self) { id in
ItemView(viewModel: ItemViewModel(itemId: id))
.cornerRadius(15)
.padding()
.shadow(radius: 3)
}
}
}
}
}
}
But this doesn't work. The ProgressView is never hidden and replaced with the ScrollView. However, if I change my view to:
struct SegmentView: View {
#ObservedObject var viewModel: SegmentViewModel
var body: some View {
ScrollView {
if viewModel.itemIds.isEmpty {
ProgressView()
} else {
LazyVStack {
ForEach(viewModel.itemIds, id: \.self) { id in
ItemView(viewModel: ItemViewModel(itemId: id))
.cornerRadius(15)
.padding()
.shadow(radius: 3)
}
}
}
}
}
}
Then it works as expected and hides the ProgressView once the itemIds aren't empty.
Why is this?
It looks like ViewBuidler consumed the condition and so it is not observed. I would wrap top at Group
struct SegmentView: View {
#ObservedObject var viewModel: SegmentViewModel
var body: some View {
Group { // << here !!
if viewModel.itemIds.isEmpty {
ProgressView()
} else {
ScrollView {
LazyVStack {
ForEach(viewModel.itemIds, id: \.self) { id in
ItemView(viewModel: ItemViewModel(itemId: id))
.cornerRadius(15)
.padding()
.shadow(radius: 3)
}
}
}
}
}
}
}

SwiftUI - How to toogle all the booleans in a CoreData

I have a database with several objects with booleans as attribute. I'm looking for a function to invert all boolean objects when I press a button. I tried this function but several errors are displayed like (Value of type 'Bool' has no member 'indices') :
struct ViewList: View {
#Environment(\.managedObjectContext) var context
#State var newName: String = ""
#FetchRequest(
entity: Product.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Product.name, ascending: true)]
) var list: FetchedResults<Product>
var body: some View {
VStack {
HStack {
TextField("I insert the name of the product", text: $newName)
Button(action: { self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
List {
ForEach(list, id: \.self) {
product in ViewItem(product: product)
}
}
}
}
public func add() {
let newProduct = Product(context: context)
newProduct.name = newName
do {
try context.save()
} catch {
print(error)
}
}
}
struct ViewItem: View {
#State var product: Product
#State var refresh: Bool = false
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack(alignment: .top) {
Button( action: {
self.clean()
self.product.isSelected.toggle()
}) {
if self.product.isSelected == true {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
VStack() {
Text(product.name)
if product.password != "" {
Text("Password : " + product.password)
}
Text(String(refresh)).hidden()
}
}
}
.onAppear {
self.refresh = false
}
}
}
I've been thinking about it, but I don't know how to go about it...
func clean() {
for( index ) in self.product.isSelected.indices {
self.product[index]isSelected = false
}
}
You need to create a query to flip the state of the isSelected flag. This logic is best kept out of the view system so you can use it anywhere.
You create a SelectionHandler
import CoreData
class SelectionHandler {
func clearSelection(in context: NSManagedObjectContext) {
for item in currentSelected(in: context) {
item.isSelected = false
}
}
func selectProduct(_ product: Product) {
guard let context = product.managedObjectContext else {
assertionFailure("broken !")
return
}
clearSelection(in: context)
product.isSelected = true
}
func currentSelected(in context: NSManagedObjectContext) -> [Product] {
let request = NSFetchRequest<Product>(entityName: Product.entity().name!)
let predicate = NSPredicate(format: "isSelected == YES")
request.predicate = predicate
do {
let result = try context.fetch(request)
return result
} catch {
print("fetch error =",error)
return []
}
}
}
which you can then use to select your desired product.
SelectionHandler().selectProduct(product)
As it stands your NavigationLink will do nothing because the parent list is not held in a NavigationView so you'll need to change the body of ViewList to look like this.
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Create product with name", text: $newName)
Button(action: {
self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
.padding()
List {
ForEach(list, id: \.self) { product in
ViewItem(product: product)
}
}
}
}
}
and in ViewItem , Product should be an ObservedObject so that changes are detected in the managedObject.
struct ViewItem: View {
#ObservedObject var product: Product
#State var refresh: Bool = false
var checkmarkImage: some View {
return Group {
if self.product.isSelected {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
}
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack {
checkmarkImage
Text(product.name ?? "wat")
}
}
}
}
The original Button won't play with the NavigationLink but you can simply apply the selection to onAppear in ViewDetail
struct ViewDetail: View {
#ObservedObject var product: Product
#Binding var refresh: Bool
var body: some View {
VStack {
Text("Hello, World!")
Text("Product is \(product.name ?? "wat")")
}
.onAppear {
SelectionHandler().selectProduct(self.product)
}
}
}

SwiftUI Change View with Button

I understand there is PresentationButton and NavigationButton in order to change views in the latest SwiftUI. However I want to do a simple operation like below. When user clicks on SignIn button if credentials are correct it will sign them in but also do a segue (in this case change the view). However I could not check if they are correct in PresentationButton and I could not change the view in a normal button.
Is there another way to do that?
#IBAction func signInClicked(_ sender: Any) {
if emailText.text != "" && passwordText.text != "" {
Auth.auth().signIn(withEmail: emailText.text!, password: passwordText.text!) { (userdata, error) in
if error != nil {
//error
} else {
performSegue(withIdentifier: "toFeedActivity", sender: nil)
}
}
} else {
//error
}
}
Here's one way.
struct AppContentView: View {
#State var signInSuccess = false
var body: some View {
return Group {
if signInSuccess {
AppHome()
}
else {
LoginFormView(signInSuccess: $signInSuccess)
}
}
}
}
struct LoginFormView : View {
#State private var userName: String = ""
#State private var password: String = ""
#State private var showError = false
#Binding var signInSuccess: Bool
var body: some View {
VStack {
HStack {
Text("User name")
TextField("type here", text: $userName)
}.padding()
HStack {
Text(" Password")
TextField("type here", text: $password)
.textContentType(.password)
}.padding()
Button(action: {
// Your auth logic
if(self.userName == self.password) {
self.signInSuccess = true
}
else {
self.showError = true
}
}) {
Text("Sign in")
}
if showError {
Text("Incorrect username/password").foregroundColor(Color.red)
}
}
}
}
struct AppHome: View {
var body: some View {
VStack {
Text("Hello freaky world!")
Text("You are signed in.")
}
}
}
I had the same need in one of my app and I've found a solution...
Basically you need to insert your main view in a NavigationView, then add an invisible NavigationLink in you view, create a #state var that controls when you want to push the view and change it's value on your login callback...
That's the code:
struct ContentView: View {
#State var showView = false
var body: some View {
NavigationView {
VStack {
Button(action: {
print("*** Login in progress... ***")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.showView = true
}
}) {
Text("Push me and go on")
}
//MARK: - NAVIGATION LINKS
NavigationLink(destination: PushedView(), isActive: $showView) {
EmptyView()
}
}
}
}
}
struct PushedView: View {
var body: some View {
Text("This is your pushed view...")
.font(.largeTitle)
.fontWeight(.heavy)
}
}
Try with state & .sheet
struct ContentView: View {
#State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
DetailView()
}
}
}
You can use navigation link with tags so,
Here is the code:
first of all, declare tag var
#State var tag : Int? = nil
then create your button view:
Button("Log In", action: {
Auth.auth().signIn(withEmail: self.email, password: self.password, completion: { (user, error) in
if error == nil {
self.tag = 1
print("success")
}else{
print(error!.localizedDescription)
}
})
So when log in success tag will become 1 and when tag will become 1 your navigation link will get executed
Navigation Link code:
NavigationLink(destination: HomeView(), tag: 1, selection: $tag) {
EmptyView()
}.disabled(true)
if you are using Form use .disabled because here the empty view will be visible on form and you don't want your user to click on it and go to the homeView.