Can I disable Navigationlink for a specified set of cells in my SwiftUI list? - swiftui-navigationlink

I am working on a contacts app in SwiftUI for work which will display hundreds of phone numbers. Some contacts have a detail view with address and alternate numbers, while some contacts do not.
For example, Harry Smith will have basic phone extension listed in row while his cell number and address are listed in detail view.
But the number for the Second Floor Bathroom only needs the basic phone extension in the row, a detail view containing cell number and address for the bathroom is unnecessary.
How do I disable navigation link for those contacts who do not have pertinent detail info?

I prefer to add NavigationLink on condition.
I will have a "Cell" with some contents. (image/file detail)
A - build a "content" View:
struct MediumFileContentRow: View {
var file: MediumFile
var body: some View {
VStack (alignment: .leading) {
let (name, attribs) = file.getInfo() // get from model..
Text(name)
Spacer()
Text(attribs)
}// VStack
}
}
B - Your cell (MediumFileRow) will conditionally add link (NavigationLink) "around" our content view, and will give back different Views using AnyView:
struct MediumFileRow: View {
var file: MediumFile
var body: some View {
let enabled = NavigationLink(destination: Detail(t: file.name!)) {
MediumFileContentRow(file: file)
}
let disabled = MediumFileContentRow(file: file)
return file.isEmpty ? AnyView(disabled) : AnyView(enabled)
}
All (almoast..) The code:
struct MediumFileContentRow: View {
var file: MediumFile
var body: some View {
VStack (alignment: .leading) {
let (s,sd) = file.displayed()
Text(s)
Spacer()
Text(sd).font(.system(size: 9.0))
}// VStack
}
}
struct MediumFileRow: View {
var file: MediumFile
var body: some View {
let enabled = NavigationLink(destination: Detail(t: file.name!)) {
MediumFileContentRow(file: file)
}
let disabled = MediumFileContentRow(file: file)
return file.isEmpty ? AnyView(disabled) : AnyView(enabled)
}
...
..
In list You will have:
var body: some View {
NavigationView{
....
List {
ForEach(mediumFiles, id: \.self) { file in
MediumFileRow(file: file)
}
}
......
advantage: You can easily customise "disabled" cells.

Related

.isFavorite results in errors

I'm trying to place an inFavorite on a Detailed View but get the following errors:
-Cannot convert value of type 'Binding' to expected condition type 'Bool'
-Value of type 'ObservedObject.Wrapper' has no dynamic member 'isFavorite' using key path from root type 'TitleModel'
import SwiftUI
struct DetailView: View {
#EnvironmentObject var model: TitleModel
var detail: Title
var body: some View {
ScrollView {
VStack (alignment: .leading) {
//MARK: Detail Image
Image(detail.image1)
.resizable()
.scaledToFill()
//MARK: Divider
Divider()
if $model.isFavorite {
Image(systemName: "star.fill")
.imageScale(.medium)
.foregroundColor(.yellow)
}
Divider()
//MARK: Remark
VStack(alignment: .leading) {
Text("Remark:")
.font(.headline)
.padding([.bottom, .top], 5)
ForEach(detail.remark, id:\.self) {item in
Text("• " + item)
}
}
.padding(.horizontal)
//MARK: Reference(s)
VStack(alignment: .leading) {
Text("Reference(s):")
.font(.headline)
.padding([.bottom, .top], 5)
}
.padding(.horizontal)
}
}
.navigationBarTitle(detail.title)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
//create a dummy title and pass it into the detail view so we can see a preview
let model = TitleModel()
DetailView(detail: model.titles[0])
}
}
The view works perfect until I throw in the .isFavorite code. 'var model' references my View Model for my JSON data 'titles'. I am attempting to allow the person to tap the .star to highlight the view. I will then have a Scroll View with only the favorites. Thanks for helping me out.

SwiftUI NavigationLink not displaying view until physical rotation then all is ok?

The opening view of App is blank with a Back Button. However, the intended view will appear after complete rotation of physical device (before touching anything) or by tapping the Back Button on the displayed empty view.
This is a weird bug. After that rotation (to landscape and back to protrait) everything is peachy-keen-o.
I am aware that adding: .navigationViewStyle(StackNavigationViewStyle()) to the NavigationView will sorta resolve the problem. But this solution is a poor fix for my App's needs.
Is there a command to intentionally rotate device in code to resolve this? Or some other fix to get that first view to appear without counting on the user to rotate the device?
I've tried various solutions with no luck.
Below is a quick setup of the problem.
struct FirstView: View {
#Binding var viewNum: Int?
var body: some View {
VStack {
Text("View One")
.padding(20)
Button {
viewNum = 2
} label: {
Text("Go to 2nd view")
}
}
.onAppear {
viewNum = 1
}
}
}
struct MainView: View {
#State var selection: Int? = 1
var body: some View {
// VStack {
NavigationView {
List {
NavigationLink("First View", tag: 1, selection: $selection){ FirstView(viewNum: $selection)}
NavigationLink("Second View", tag: 2, selection: $selection){
Text("View 2").padding(20)
Button {
selection = 1
} label: {
Text("Go to first view")
}
}
}
}
// .navigationViewStyle(StackNavigationViewStyle())
//}
}
}

How can I present an overlay above the keyboard in SwiftUI?

I have a screen in my app that automatically displays a keyboard when the screen is presented. The screen allows users to enter their mobile number. In order to select a dialling code, the user needs to tap a button which will then trigger the presentation of an overlay.
Problem
The overlay is being presented, but it's showing up behind the currently present keyboard.
Question
How can I make this overlay be the very top view?
There is no way for me to use the zIndex modifier on the keyboard for obvious reasons. I'm wondering if there is a way to make the overlay the top view when it's about to be presented, or if the overlay can be added to the window.
Thanks in advance
You should probably only have one source of input at any given time — either the keyboard should be presented to enter a number, or the overlay should be presented to pick the dialing code, not both. Here's an example which hides the keyboard when overlay appears, and vice versa. (Keyboard dismissal code from this answer.)
struct ContentView: View {
#State private var number = ""
#State private var showingOverlay = false
var body: some View {
GeometryReader { proxy in
ZStack(alignment: .top) {
// Just here to force ZStack to use the whole screen
Rectangle()
.fill(Color.clear)
VStack(alignment: .leading) {
Button("Select Dialing Code") {
UIApplication.shared.endEditing()
self.showingOverlay = true
}
TextField("Enter your number", text: self.$number, onEditingChanged: {
if $0 { self.showingOverlay = false }
})
.keyboardType(.phonePad)
}
Overlay(showing: self.$showingOverlay)
.frame(height: 400)
.offset(x: 0, y: proxy.size.height + (self.showingOverlay ? -300 : 100))
.animation(.easeInOut)
}
}
}
}
struct Overlay: View {
#Binding var showing: Bool
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(Color(.systemGray4))
Button("Dismiss") {
self.showing = false
}
}
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}

SwiftUI with ObservableObject - List having NavigationLink to details downloading it on scroll NOT on appear

I have list of cities
struct CityListView: View {
#ObservedObject private(set) var citiesViewModel: CitiesViewModel
var body: some View {
LoadingView(isShowing: .constant(citiesViewModel.cities?.isEmpty ?? false)) {
NavigationView {
List(self.citiesViewModel.cities ?? []) { city in
NavigationLink(destination: DetailView(cityName: city.name,
detailCityModel: DetailCityModel(cityId: city.id))) {
Text(city.name)
}
}
.navigationBarTitle(Text("Cities"), displayMode: .large)
}
}
}
}
and when I'm scrolling the list, the DetailCityModel inits and download data from API. How to downloading (or init DetailCityModel) on DetailView's appearance, not for showing item with NAvigationLink to DetailView?
You have to kick off the API call in onAppear() not in the initialiser of DetailView.

Load more functionality using SwiftUI

i have used ScrollView with HStack, now i need to load more data when user reached scrolling at last.
var items: [Landmark]
i have used array of items which i am appeding in HStack using ForEach
ScrollView(showsHorizontalIndicator: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items) { landmark in
CategoryItem(landmark: landmark)
}
}
}
What is the best possible solution to manage load more in SwiftUI without using custom action like loadmore button.
It's better to use ForEach and List for this purpose
struct ContentView : View {
#State var textfieldText: String = "String "
private let chunkSize = 10
#State var range: Range<Int> = 0..<1
var body: some View {
List {
ForEach(range) { number in
Text("\(self.textfieldText) \(number)")
}
Button(action: loadMore) {
Text("Load more")
}
}
}
func loadMore() {
print("Load more...")
self.range = 0..<self.range.upperBound + self.chunkSize
}
}
In this example each time you press load more it increases range of State property. The same you can do for BindableObject.
If you want to do it automatically probably you should read about PullDownButton(I'm not sure if it works for PullUp)
UPD:
As an option you can download new items by using onAppear modifier on the last cell(it is a static button in this example)
var body: some View {
List {
ForEach(range) { number in
Text("\(self.textfieldText) \(number)")
}
Button(action: loadMore) {
Text("")
}
.onAppear {
DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 10)) {
self.loadMore()
}
}
}
}
Keep in mind, that dispatch is necessary, because without it you will have an error saying "Updating table view while table view is updating). Possible you may using another async way to update the data
If you want to keep using List with Data instead of Range, you could implement the next script:
struct ContentView: View {
#State var items: [Landmark]
var body: some View {
List {
ForEach(self.items) { landmark in
CategoryItem(landmark: landmark)
.onAppear {
checkForMore(landmark)
}
}
}
}
func checkForMore(_ item: LandMark) {
guard let item = item else { return }
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
if items.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
// function to request more data
getMoreLandMarks()
}
}
}
Probably you should work in a ViewModel and separate the logic from the UI.
Credits to Donny Wals: Complete example