Binding<Bool> to Binding<Bool?> in Swift - swiftui

NavigationLinks selected parameeter takes in Binding but my ObservedObject has Bool variable not an Optional value.
How to convert it so that selected would accept it?
Code:
class RegistrationFlowEnvironment: ObservableObject {
#Published var done = false
}
the view:
struct ContentView: View {
#ObservedObject var regFlowEnv = RegistrationFlowEnvironment()
var body: some View {
NavigationView {
VStack{
NavigationLink("Go to other place", destination: Dest(),
tag: true,
selection: $regFlowEnv.done)
}
}
}
}

Here is fixed variant (tested with Xcode 11.7 / iOS 13.7)
NavigationView {
VStack{
NavigationLink("Go to other place", destination: Text("Dest"),
tag: true,
selection: Binding($regFlowEnv.done))
}
}

Related

SwiftUI Tab Selection Not Working With Any Hashable Content

SwiftUI’s Tab selection is suppose to work with any hashable content however that doesn’t seem to work.
In the example provided, you can see that in “Working” Tab, eveything works correctly if you use an integer for the tab selection. When you switch over to the “Broken” tab, the selection is a ColorItem and the selection does not update the view.
I believe this is a SwiftUI bug and have filed a feedback(FB8879981).
Tested with Xcode 12.2 and iOS 14.2(RC).
struct ColorItem: Identifiable, Hashable{
let color: Color
let title: String
var id: String{
title
}
}
struct ContentView: View {
let items = [
ColorItem(color: .red, title: "Red"),
ColorItem(color: .blue, title: "Blue"),
ColorItem(color: .purple, title: "Purple")
]
var body: some View {
TabView{
TabViewWorking(items: items)
.tabItem {
Label("Working", systemImage: "hand.thumbsup")
}
TabViewBroken(items: items)
.tabItem {
Label("Broken", systemImage: "hand.thumbsdown")
}
}
}
}
struct TabViewWorking: View {
#State private var tabSelection = 0
let items: [ColorItem]
var body: some View {
ZStack{
TabView(selection: $tabSelection){
ForEach(0..<items.count){ i in
items[i].color.edgesIgnoringSafeArea(.all)
.tag(i)
}
}
.tabViewStyle(PageTabViewStyle())
VStack{
Text(tabSelection.description)
Text(items[tabSelection].title)
}
.font(.largeTitle)
}
}
}
struct TabViewBroken: View {
#State private var tabSelection = ColorItem(color: .red, title: "Red")
let items: [ColorItem]
var body: some View {
ZStack{
TabView(selection: $tabSelection){
ForEach(0..<items.count){ i in
items[i].color.edgesIgnoringSafeArea(.all)
.tag(i)
}
}
.tabViewStyle(PageTabViewStyle())
VStack{
Text(items.firstIndex(of: tabSelection)?.description ?? "N/A")
Text(tabSelection.title)
}
.font(.largeTitle)
}
}
}
No, it is not SwiftUI bug. Type of selection and type of tag must be same, so in your first scenario they are both integers, but in second one they are not same - selection is ColorItem, but tag is still integer - thus selection does not work.
Here is fixed variant:
struct TabViewBroken: View {
#State private var tabSelection = ColorItem(color: .red, title: "Red")
let items: [ColorItem]
var body: some View {
ZStack{
TabView(selection: $tabSelection){
ForEach(0..<items.count){ i in
items[i].color.edgesIgnoringSafeArea(.all)
.tag(items[i]) // << here !!
}
}
.tabViewStyle(PageTabViewStyle())
VStack{
Text(items.firstIndex(of: tabSelection)?.description ?? "N/A")
Text(tabSelection.title)
}
.font(.largeTitle)
}
}
}

The correct way to list CoreData object in nest NavigationView in NavigationLink

In xcode 12 (swift 5.3), I using the conditional navigationLink navigate to another navigationView to list coreData object with NavigationLink. But it seems the AnotherView's NavigationTitle can not be correctly show at the top of screen, instead it padding to the top. The list in another navigationView have a external white background color. The something.id which I want to pass to SomethingView report Argument passed to call that takes no arguments error, but I can get something.name in Text.
struct StartView: View {
#State var changeToAnotherView: String? = nil
var body: some View {
NavigationView {
VStack(spacing: 20) {
...
NavigationLink(destination: AnotherView(), tag: "AnotherView",
selection: $changeToAnotherView) { EmptyView() }
}
}
}
}
struct AnotherView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Something.entity(), sortDescriptors: []) var somethings: FetchedResults<Something>
...
var body: some View {
NavigationView {
List {
ForEach(self.somethings, id: \.id) { something in
NavigationLink(destination: SomethingView(somethingID: something.id)) {
Text(something.name ?? "unknown name")
}
}
}
.navigationBarTitle("SomethingList")
}
}
}
You don't need second NavigationView - it must be only one in view hierarchy, as well it is better to pass CoreData object by reference (view will be able to observe it), so
struct AnotherView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Something.entity(), sortDescriptors: []) var somethings: FetchedResults<Something>
...
var body: some View {
List {
ForEach(self.somethings, id: \.id) { something in
NavigationLink(destination: SomethingView(something: something)) {
Text(something.name ?? "unknown name")
}
}
}
.navigationBarTitle("SomethingList")
}
}
struct SomethingView: View {
#ObservedObject var something: Something
var body: some View {
// .. your next code

Can a published var in observed object be used directly in Picker selection as binding var?

I am new to xcode 11 and SwiftUI. I am working on a settings view for my app.
Created an ObservableObject with Published var dataType Int. In the settings view, i have a picker view where i pass in the settingsStore.dataType. It gives me below error.
Cannot convert value of type 'Int' to expected argument type
'Binding<SelectionValue>'
I know i can work around this by setting a #State var dataType in the view and passing in $dataTpye for the Picker and then assign the value to the settings object. Just wondering if there is a more straight forward method of doing it.
import SwiftUI
import Combine
struct SettingsView: View {
#ObservedObject var settingsStore: SettingsStore
var body: some View {
VStack {
Text("Settings").font(.title)
Text("Number of weeks in calendar view")
HStack (spacing: 28) {
Button (action: {
self.settingsStore.numberOfWeeks -= 1
}) {
Image(systemName: "minus.circle")
.resizable()
.frame(width:60, height:60)
}
Text("\(settingsStore.numberOfWeeks)")
.font(.system(size: 38.0))
Button (action: {
self.settingsStore.numberOfWeeks += 1
}) {
Image(systemName: "plus.circle")
.resizable()
.frame(width:60, height:60)
}
}
Text("Default data type")
Picker(selection: settingsStore.dataType, label: Text("Dafault Data Type")) {
Text("Blood Ketone Value (mmol/L)").tag(0)
Text("Ketostix").tag(1)
}.pickerStyle(SegmentedPickerStyle())
}
}
}
class SettingsStore: ObservableObject {
#Published var numberOfWeeks: Int {
didSet {
UserDefaults.standard.set(numberOfWeeks, forKey: "numberOfWeeks")
}
}
#Published var dataType: Int {
didSet {
UserDefaults.standard.set(dataType, forKey: "dataType")
}
}
init() {
self.numberOfWeeks = UserDefaults.standard.integer(forKey: "numberOfWeeks")
self.dataType = UserDefaults.standard.integer(forKey: "dataType")
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
return SettingsView(settingsStore: SettingsStore())
}
}
The same notation, ie $, for ObservedObject properties,
Picker(selection: $settingsStore.dataType, label: Text("Dafault Data Type")) {
Text("Blood Ketone Value (mmol/L)").tag(0)
Text("Ketostix").tag(1)
}.pickerStyle(SegmentedPickerStyle())

SwiftUI: How can I pop to the root view for watchOS?

I need to pop to the root view from a deep detail view. And while the following solution using isDetailLink and isActive works quite well for iOS, it does not work for watchOS. The isDetailLink command is unavailable in watchOS.
'isDetailLink' is unavailable in watchOS
import SwiftUI
class AppState : ObservableObject {
#Published var showState : Bool = false
}
struct MoreTests: View {
#EnvironmentObject var appState : AppState // injected from SceneDelegate
var body: some View {
NavigationView {
NavigationLink(
destination: MoreView1(),
isActive: $appState.showState, // required to work
label: { Text("Go to MoreView1") }
).isDetailLink(false) // required to work
}.navigationBarTitle("Root")
}
}
struct MoreView1: View {
var body: some View {
NavigationLink(
destination: MoreView2(),
label: { Text("Go to MoreView2") }
)
.navigationBarTitle("MoreView1")
}
}
struct MoreView2: View {
var body: some View {
NavigationLink(
destination: MoreView3(),
label: { Text("Go to MoreView3") }
)
.navigationBarTitle("MoreView2")
}
}
struct MoreView3: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var appState : AppState
var body: some View {
VStack {
Button(action: {
self.appState.showState = false // required
}) {
Text("Dismiss to root")
}
}.navigationBarTitle("MoreView3")
}
}
The iOS solution came from How can I pop to the Root view using SwiftUI?.
An alternative is to (re)set the id of the NavigationView
Replace AppState with
class AppState : ObservableObject {
#Published var appID = UUID()
}
In MoreTests remove the isActive parameter and add this modifier right before setting the navigationBarTitle
NavigationView {...
}
.id(appState.appID)
In MoreView3 assign a new UUID to the appID
Button(action: {
self.appState.appID = UUID()
}) {
Text("Dismiss to root")
}
Fixed with Xcode 13.4 / watchOS 8.5
Create Project from template.
Just copy-pasted above code
Commented .isDetailLink(false)
Use MoreTests as content of scene
Build & Run

Is it possible for a NavigationLink to perform an action in addition to navigating to another view?

I'm trying to create a button that not only navigates to another view, but also run a function at the same time. I tried embedding both a NavigationLink and a Button into a Stack, but I'm only able to click on the Button.
ZStack {
NavigationLink(destination: TradeView(trade: trade)) {
TradeButton()
}
Button(action: {
print("Hello world!") //this is the only thing that runs
}) {
TradeButton()
}
}
You can use .simultaneousGesture to do that. The NavigationLink will navigate and at the same time perform an action exactly like you want:
NavigationLink(destination: TradeView(trade: trade)) {
Text("Trade View Link")
}.simultaneousGesture(TapGesture().onEnded{
print("Hello world!")
})
You can use NavigationLink(destination:isActive:label:). Use the setter on the binding to know when the link is tapped. I've noticed that the NavigationLink could be tapped outside of the content area, and this approach captures those taps as well.
struct Sidebar: View {
#State var isTapped = false
var body: some View {
NavigationLink(destination: ViewToPresent(),
isActive: Binding<Bool>(get: { isTapped },
set: { isTapped = $0; print("Tapped") }),
label: { Text("Link") })
}
}
struct ViewToPresent: View {
var body: some View {
print("View Presented")
return Text("View Presented")
}
}
The only thing I notice is that setter fires three times, one of which is after it's presented. Here's the output:
Tapped
Tapped
View Presented
Tapped
NavigationLink + isActive + onChange(of:)
// part 1
#State private var isPushed = false
// part 2
NavigationLink(destination: EmptyView(), isActive: $isPushed, label: {
Text("")
})
// part 3
.onChange(of: isPushed) { (newValue) in
if newValue {
// do what you want
}
}
This works for me atm:
#State private var isActive = false
NavigationLink(destination: MyView(), isActive: $isActive) {
Button {
// run your code
// then set
isActive = true
} label: {
Text("My Link")
}
}
Use NavigationLink(_:destination:tag:selection:) initializer and pass your model's property as a selection parameter. Because it is a two-way binding, you can define didset observer for this property, and call your function there.
struct ContentView: View {
#EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationView {
List(0 ..< 10, id: \.self) { row in
NavigationLink(destination: DetailView(id: row),
tag: row,
selection: self.$navigationModel.linkSelection) {
Text("Link \(row)")
}
}
}
}
}
struct DetailView: View {
var id: Int;
var body: some View {
Text("DetailView\(id)")
}
}
class NavigationModel: ObservableObject {
#Published var linkSelection: Int? = nil {
didSet {
if let linkSelection = linkSelection {
// action
print("selected: \(String(describing: linkSelection))")
}
}
}
}
It this example you need to pass in your model to ContentView as an environment object:
ContentView().environmentObject(NavigationModel())
in the SceneDelegate and SwiftUI Previews.
The model conforms to ObservableObject protocol and the property must have a #Published attribute.
(it works within a List)
I also just used:
NavigationLink(destination: View()....) {
Text("Demo")
}.task { do your stuff here }
iOS 15.3 deployment target.