I wrote test code for NavigationStack. The behavior of the code is a two-step transition(ContentView -> SubsubTestView -> DetailView).
But I got an error when I have selected a name in SubsubTestView.
A NavigationLink is presenting a value of type βUserβ but there is no matching navigationDestination declaration visible from the location of the link. The link cannot be activated.
Is there anything wrong with the wrong part of the code?
ContentView.swift
import SwiftUI
class EnvObj: ObservableObject {
#Published var users = [User(name: "a"), User(name: "b"), User(name: "c")]
}
struct User: Hashable, Identifiable, Equatable {
var id = UUID()
var name = ""
static func == (lhs: User, rhs: User) -> Bool{
return lhs.id == rhs.id
}
}
struct ContentView: View {
#EnvironmentObject var envObj: EnvObj
#State var moveToSubsub = false
var body: some View {
NavigationStack {
Button("To subsub") {
moveToSubsub = true
}
.navigationDestination(isPresented: $moveToSubsub) {
SubsubTestView(vm: VM(envObj: envObj))
}
}
}
}
struct SubsubTestView: View {
#EnvironmentObject var envObj: EnvObj
#StateObject var vm: VM
var body: some View {
VStack {
List(self.vm.envObj.users) { user in
NavigationLink(value: user) {
Text(user.name)
}
}
.navigationDestination(for: User.self) { user in
DetailView(vm: VMD(envObj: envObj, selectedUser: user))
}
}
}
}
class VM: ObservableObject {
var envObj: EnvObj = .init()
init(envObj: EnvObj) {
self.envObj = envObj
}
}
struct DetailView: View {
#StateObject var vm: VMD
var body: some View {
VStack {
TextField("Name: ", text: (self.$vm.selectedUser ?? User()).name)
Text(self.vm.selectedUser?.name ?? User().name)
Button("submit", action: self.vm.submit)
}
}
}
class VMD: ObservableObject {
var envObj: EnvObj = .init()
#Published var selectedUser: User?
init(envObj: EnvObj, selectedUser: User? = nil) {
self.envObj = envObj
self.selectedUser = selectedUser
}
private(set) lazy var submit = {
if let user = self.selectedUser {
self.update(user: user)
}
}
func update(user: User) {
self.envObj.users = self.envObj.users.map {
return $0 == user ? user : $0
}
}
}
func ??<T>(binding: Binding<T?>, fallback: T) -> Binding<T> {
return Binding(get: {
binding.wrappedValue ?? fallback
}, set: {
binding.wrappedValue = $0
})
}
Thanks,
Related
I'm trying to build a simple SwiftUI view that displays a number of toggles. While I can get everything to display ok, I cannot get the toggles to flip. Here's is a simplified code example:
import SwiftUI
class StoreableParam: Identifiable {
let name: String
var id: String { name }
#State var isEnabled: Bool
let toggleAction: ((Bool) -> Void)?
init(name: String, isEnabled: Bool, toggleAction: ((Bool) -> Void)? = nil) {
self.name = name
self.isEnabled = isEnabled
self.toggleAction = toggleAction
}
}
class StoreableParamViewModel: ObservableObject {
#Published var storeParams: [StoreableParam] = []
init() {
let storedParam = StoreableParam(name: "Stored Param", isEnabled: false) { value in
print("Value changed")
}
storeParams.append(storedParam)
}
}
public struct UBIStoreDebugView: View {
#StateObject var viewModel: StoreableParamViewModel
public var body: some View {
VStack {
List {
ForEach(viewModel.storeParams, id: \.id) { storeParam in
Toggle(storeParam.name, isOn: storeParam.$isEnabled)
.onChange(of: storeParam.isEnabled) {
storeParam.toggleAction?($0)
}
}
}
}.navigationBarTitle("Toggle Example")
}
}
As mentioned in the comments, there are a couple of things going on:
#State is only for use in a View
Your model should be a struct
Then, you can get a Binding using the ForEach element binding syntax:
struct StoreableParam: Identifiable {
let name: String
var id: String { name }
var isEnabled: Bool
let toggleAction: ((Bool) -> Void)?
}
class StoreableParamViewModel: ObservableObject {
#Published var storeParams: [StoreableParam] = []
init() {
let storedParam = StoreableParam(name: "Stored Param", isEnabled: false) { value in
print("Value changed")
}
storeParams.append(storedParam)
}
}
public struct UBIStoreDebugView: View {
#StateObject var viewModel: StoreableParamViewModel
public var body: some View {
VStack {
List {
ForEach($viewModel.storeParams, id: \.id) { $storeParam in
Toggle(storeParam.name, isOn: $storeParam.isEnabled)
.onChange(of: storeParam.isEnabled) {
storeParam.toggleAction?($0)
}
}
}
}
}
}
I am trying to disable a button based on a computed property from the View Model, but is only disabled after the view is reloaded.
This is the View Model :
class VerifyFieldViewModel : ObservableObject {
#ObservedObject var coreDataViewModel = CoreDataViewModel()
func isValidFirstName() -> Bool {
guard coreDataViewModel.savedDetails.first?.firstName?.count ?? 0 > 0 else {
return false
}
return true
}
func isValidLastName() -> Bool {
guard coreDataViewModel.savedDetails.first?.lastName?.count ?? 0 > 0 else {
return false
}
return true
}
var isFirstNameValid : String {
if isValidFirstName() {
return ""
} else {
return "Name is empty"
}
}
var isLastNameValid : String {
if isValidLastName() {
return ""
} else {
return "Surname is empty"
}
}
var isSignUpComplete: Bool {
if !isValidFirstName() || !isValidLastName() {
return false
}
return true
}
}
This is how I am disabling the button .
struct CartsView: View {
#State var onboardingState: Int = 0
#StateObject var coreDataViewModel = CoreDataViewModel()
#ObservedObject var verifyFieldViewModel = VerifyFieldViewModel()
var body: some View {
ZStack {
switch onboardingState {
case 0 :
VStack {
detailOrder
.transition(transition)
Spacer()
bottomButton
.padding(30)
}
case 2 :
VStack {
detailOrder2
.transition(transition)
Spacer()
bottomButton
.padding(30)
.opacity(verifyFieldViewModel.isSignUpComplete ? 1 : 0.6)
.disabled(!verifyFieldViewModel.isSignUpComplete)
}
default:
EmptyView()
}
}
}
}
This is the Core Data View Model :
class CoreDataViewModel : ObservableObject {
let manager = CoreDataManager.instance
#Published var savedDetails : [Details] = []
init() {
fetchSavedDetails()
}
func fetchSavedDetails() {
let request = NSFetchRequest<Details>(entityName: "Details")
do {
savedDetails = try manager.context.fetch(request)
} catch let error {
print("Error fetching \(error)")
}
}
func saveContext() {
DispatchQueue.main.async {
self.manager.save()
self.fetchSavedDetails()
}
}
}
NOTE : It works, but only when the view is reloaded.
EDITED : I updated the question to make it easier to understand. Hope that you can help me now.
EDITED2: Added Core Data View Model .
As mentioned above you don't need a computed property in this case. I made a small example of Login procedure which demonstrates the same behavior.
class LoginViewModel: ObservableObject {
#Published var username: String = ""
#Published var password: String = ""
var isValid: Bool {
(username.isNotEmpty && password.isNotEmpty)
}
func login() {
// perform login
}
}
struct ContentView: View {
#StateObject private var vm: LoginViewModel = LoginViewModel()
var body: some View {
Form {
TextField("User name", text: $vm.username)
TextField("Password", text: $vm.password)
Button("Login") {
vm.login()
}.disabled(!vm.isValid)
}
}
}
When I modify the score using the stepper on View1 it successfully updates the score on View1.ViewModel. However when I dismiss the sheet it does not update the score on the players section.
I know that you can set #Binding var on Player1 but how do I pass this down to the View1.ViewModel so that when it makes changes it updates the parent view?
ContentView
import SwiftUI
struct ContentView: View {
#State var game: Game = Game(name: "Game #1")
#State var isPresented: Bool = true
var body: some View {
NavigationView {
List {
Section(header: Text("PLAYERS"), content: {
ForEach(game.players) { player in
HStack {
Text(player.name)
Spacer()
Text("\(player.score)")
}
}
})
}
.navigationTitle(game.name)
}
.sheet(isPresented: $isPresented, onDismiss: {}, content: {
TabView {
ForEach(game.players) { player in
View1(player: player)
}
}
.tabViewStyle(PageTabViewStyle())
})
.onAppear(perform: {
for p in 1...3 {
let player = Player(name: "Player #\(p)")
self.game.players.append(player)
}
})
}
}
struct View1: View {
#ObservedObject var vm: View1.ViewModel
init(player: Player) {
vm = ViewModel(player: player)
}
var body: some View {
VStack {
Spacer()
Text(vm.name)
Divider()
Group {
Stepper("Earned Points: \(vm.earnedPoints)", value: $vm.earnedPoints, in: 0...50)
}
.padding(.horizontal)
Spacer()
}
}
}
extension View1 {
class ViewModel: ObservableObject {
#Published private var player: Player
#Published private var round: Round
init(player: Player) {
self.player = player
self.round = player.rounds[0]
}
var name: String {
return player.name
}
var earnedPoints: Int {
get {return self.round.earnedPoints}
set {self.round.earnedPoints = newValue}
}
}
}
Game
import SwiftUI
struct Game: Identifiable {
init(name: String) {
self.name = name
}
var id: UUID = UUID()
var name:String
var players: Array<Player> = Array<Player>()
var isMastersEdition: Bool = false
}
Player
import SwiftUI
struct Player: Identifiable {
init(name: String) {
self.name = name
}
var id: UUID = UUID()
var name:String
var rounds: Array<Round> = [Round()]
var score: Int {
return self.rounds.map({$0.earnedPoints}).reduce(0, +)
}
}
Round
import SwiftUI
struct Round: Identifiable {
var id: UUID = UUID()
var earnedPoints: Int = 0
var completedPhase: Bool = false
var selectedPhase: Int = 0
}
Is there a way to create an extension for Optional value so that we don't need to create custom binding individually.
class ViewModel: ObservableObject {
#Published var text: String?
}
struct ContentView: View {
#ObservedObject var model: ViewModel
var body: some View {
let binding = Binding(
get: { self.model.text ?? "" },
set: { self.model.text = $0 }
)
return VStack {
TextField("Text", text: binding)
}
}
}
I'm thinking about doing something like this
extension Optional where Wrapped == String {
func optionalBinding() -> Binding<String> {
return Binding(get: {
self ?? ""
}, set: { newValue in
self = newValue
})
}
}
Any suggestions on this?
You may try extending Binding instead of Optional:
extension Binding where Value == String? {
var optionalBinding: Binding<String> {
.init(
get: {
self.wrappedValue ?? ""
}, set: {
self.wrappedValue = $0
}
)
}
}
And use it like this:
class ViewModel: ObservableObject {
#Published var text: String?
}
struct ContentView: View {
#ObservedObject var model = ViewModel()
var body: some View {
VStack {
TextField("Text", text: $model.text.optionalBinding)
Text(String(describing: model.text))
}
}
}
Nil-coalescing operator.
public extension Binding {
static func ?? <Wrapped>(optional: Self, defaultValue: Wrapped) -> Binding<Wrapped>
where Value == Wrapped? {
.init(
get: { optional.wrappedValue ?? defaultValue },
set: { optional.wrappedValue = $0 }
)
}
}
var wrappedValue: String? = "π¬"
#Binding(
get: { wrappedValue },
set: { wrappedValue = $0 }
) var string;
let stringOrDefault = $string ?? "π"
XCTAssertEqual(stringOrDefault.wrappedValue, "π¬")
string = nil
XCTAssertEqual(stringOrDefault.wrappedValue, "π")
stringOrDefault.wrappedValue = "πͺ"
XCTAssertEqual(string, "πͺ")
To build on #pawello2222's answer, you can use a more generic extension catching all values:
extension Binding {
func unwrapped<T>(_ defaultValue: T) -> Binding<T> where Value == Optional<T> {
let binding = Binding<T>(get: { self.wrappedValue ?? defaultValue }, set: { self.wrappedValue = $0 })
return binding
}
}
And it is used like:
class ViewModel: ObservableObject {
#Published var text: String?
}
struct ContentView: View {
#ObservedObject var model = ViewModel()
var body: some View {
VStack {
TextField("Text", text: $model.text.unwrapped(String()))
Text(String(describing: model.text))
}
}
}
I'm an early bird in programming so I know this question can be ridiculous from the point of view of an expert but I'm stuck in this situation from several days.
I would like to update a row by using a button "Edit" (pencil) after having used another button to store the item with a TextField.
Here's the code:
class Food: Hashable, Codable, Equatable {
var id : UUID = UUID()
var name : String
init(name: String) {
self.name = name
}
static func == (lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var shoppingChart: [Food] = []
init() {
let milk = Food(name: "Milk")
let coffee = Food(name: "Coffee")
shoppingChart.append(milk)
shoppingChart.append(coffee)
}
func newFood(name: String) {
let food = Food(name: name)
shoppingChart.insert(food, at: 0)
}
}
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
var body: some View {
VStack {
List {
ForEach(self.dm.shoppingChart, id:\.self) { food in
HStack {
Text(food.name)
Image(systemName: "pencil")
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}
struct Add: View {
#ObservedObject var dm : Manager
#State var fieldtext : String = ""
#Binding var isAddFoodOpened : Bool
var body: some View {
VStack {
TextField("Write a food", text: $fieldtext)
buttonSave
}
}
var buttonSave : some View {
Button(action: {
self.dm.newFood(name: self.fieldtext)
self.isAddFoodOpened = false
}) {
Text("Save")
}
}
}
The #ObservedObject var dm : Manager object is never initialized.
Try initialized dm in ContentView like this:
#ObservedObject var dm = Manager()
Ok, so if I understand correctly you want to update/edit a row by using a button "Edit".
This will do it:
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
#State var isEditOpened = false
#State var fieldtext : String = ""
var body: some View {
VStack {
List {
ForEach(0..<self.dm.shoppingChart.count, id:\.self) { i in
HStack {
Text(self.dm.shoppingChart[i].name)
Button(action: { self.isEditOpened.toggle() }) {
Image(systemName: "pencil")
}.sheet(isPresented: self.$isEditOpened) {
TextField(self.dm.shoppingChart[i].name, text: self.$fieldtext, onEditingChanged: { _ in
self.dm.shoppingChart[i].name = self.fieldtext
})
}
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}