Programmatically adding buttons in a list on Swift 5 - swift

So I'm relatively new to swift and coding in general, I'm currently trying to develop an app that essentially operates like a to do list. My goal for the home page is pretty basic. The logo centered at the top and a button to create "New Lists". I've gone back and forth on how to manage the various lists (using buttons or using listviews). But ideally I want the New List button to create a new button (which accesses the newly created list) and also take the user to the newly created list to name it and add contents etc.
I'm currently exploring the use of NavigationLinks and Navigation View (Code Below)
import SwiftUI
struct ContentView: View {
#State private var isShowingNewList = false
var body: some View {
NavigationView{
VStack(spacing: 30) {
NavigationLink(destination: Text("This is gonna be a new list"), isActive: $isShowingNewList) { EmptyView()}
Button("New List!!") {
self.isShowingNewList = true
}
.buttonStyle(.bordered)
.offset(y: -100)
}
.navigationBarTitle("Nudge")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm thinking navigation view is not the way to go because even when creating a button to control the link it controls the entire screen. Any pointers?
This is a complex question so any sort of guidance or suggestions of strategies that could be worth looking into would be great!

Related

SwiftUI NavigationView Stuck after a Few Steps

I am walking my first steps with SwiftUI as I'm thinking about migrating my existing UIKit-based iOS app. It makes extensive use of split views. Ideally, there will be something like the iOS Mail app with a master/detail view as well as an additional (leftmost) column for selecting content, account, etc.
For now, I am stuck on some basic issues such as this one: I created a short list of navigation links and a default content to be shown when the app initially opens the detail view. This works as expected when navigating back and forth in "stacked" mode, i.e. on iPhone in portrait orientation. Changing to multiple-column view, e.g. iPhone max in landscape, the primary column is stuck after a few steps. In landscape view, I have to manually hide it by clicking on the detail, and in portrait view the detail view does no longer show at all.
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
NavigationView {
List {
NavigationLink(destination: destination1) {
Text("Hello, World 1!")
}
NavigationLink(destination: destination2) {
Text("Hello, World 2!")
}
.navigationTitle("Primary")
}
Text("This is the default content shown when the navigation view is created.")
}
}
}
var destination1: some View {
Text("Destination 1 text.")
.navigationTitle("secondary 1")
}
var destination2: some View {
Text("Destination 2 text.")
.navigationTitle("secondary 2")
}
}

Navigation between SwiftUI Views

I don't know how to navigate between views with buttons.
The only thing I've found online is detail view, but I don't want a back button in the top left corner. I want two independent views connected via two buttons one on the first and one on the second.
In addition, if I were to delete the button on the second view, I should be stuck there, with the only option to going back to the first view being crashing the app.
In storyboard I would just create a button with the action TouchUpInSide() and point to the preferred view controller.
Also do you think getting into SwiftUI is worth it when you are used to storyboard?
One of the solutions is to have a #Statevariable in the main view. This view will display one of the child views depending on the value of the #Statevariable:
struct ContentView: View {
#State var showView1 = false
var body: some View {
VStack {
if showView1 {
SomeView(showView: $showView1)
.background(Color.red)
} else {
SomeView(showView: $showView1)
.background(Color.green)
}
}
}
}
And you pass this variable to its child views where you can modify it:
struct SomeView: View {
#Binding var showView: Bool
var body: some View {
Button(action: {
self.showView.toggle()
}) {
Text("Switch View")
}
}
}
If you want to have more than two views you can make #State var showView1 to be an enum instead of a Bool.

SwiftUI - Presenting a View on top of the current View from AppDelegate

I am working on a project that at some point it receives a notification. When that happens, I need to show a View. I am not able to catch notification from any View so I am looking for a way to change to control it from outside of View structs. After the View's purpose is done, I need to dismiss it where the app left off. Think like the native behaviour when there is an active call.
I thought I could use sheet however I could not find any way to trigger it for every View that could be active when the notifications come. Or maybe trying to extend native View class would work but again, no luck finding a tutorial.
Any help will be appreciated.
Just update your model based on notification. There is not necessary to define .sheet (modal view) everywhere in your view hierarchy. Doing it in root view should be enough.
To demonstrate that (copy - paste - run) I create small project where I mimic notification with SwiftUI Toggle.
import SwiftUI
class Model: ObservableObject {
#Published var show = false
}
struct SubView: View {
#EnvironmentObject var model: Model
var tag: Int
var body: some View {
VStack {
NavigationLink(destination: SubView(tag: tag + 1).environmentObject(model)) {
Text("subview \(tag)")
}
if tag == 2 {
Toggle(isOn: $model.show) {
Text("toggle")
}.padding()
}
}.navigationBarTitle("subview \(tag)")
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
SubView(tag: 0).environmentObject(model)
}.sheet(isPresented: $model.show) {
Text("sheet")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the result

SwiftUI Navigation on iPad - How to show master list

My app has simple navigation needs
List View (parent objects)
List View (child objects)
Detail View (child object)
I have this setup and working on iPhone, but when I run the app on iPad in portrait mode the master list is always hidden.
I'm using .isDetailLink(false) on the navigation link from the first list to the second, so both lists always stay in the master column. In iPad landscape everything works as expected but in portrait the detail view fills the screen. I can swipe in from the left side of the screen to show the list but I'd like to provide more clarity to the user.
I'd like to show or add the back button to show the master/list side (sort of like the Apple Notes app). On the iPhone I get the back button by default but on iPad in portrait mode there is nothing in its place.
This is what I see on iPhone
But this is what I see on iPad
Parent list
struct ParentList: View {
let firstList = ["Sample data 01", "Sample data 02", "Sample data 03", "Sample data 04", "Sample data 05"]
var body: some View {
NavigationView {
List{
ForEach(firstList, id: \.self) { item in
NavigationLink(destination: ChildList()){
Text(item)
}
.isDetailLink(false)
}
}
}
}
}
Child list
struct ChildList: View {
let secondList = ["More Sample data 01", "More Sample data 02", "More Sample data 03", "More Sample data 04", "More Sample data 05"]
var body: some View {
List{
ForEach(secondList, id: \.self) { item in
NavigationLink(destination: ChildDetail()){
Text(item)
}
}
}
}
}
Child detail
struct ChildDetail: View {
var body: some View {
Text("Child detail view")
}
}
Update: As of Oct 17, 2019 I have not found a way to get this to work. I decided to use .navigationViewStyle(StackNavigationViewStyle()) for the time being. Interestingly, this needs to go outside of the navigation view like a normal modifier, not inside it with the navigation title.
In portrait the default split view does not work. This may be fixed in future but it appears the current options are:
(a) change the navigation view style of your first list to .navigationViewStyle(StackNavigationViewStyle()) so the navigation will work like on iPhone and push each view.
(b) leave the style to default and only support landscape for iPad
(c) implement a UIKit split view controller
There also is a quite hacky workaround (see https://stackoverflow.com/a/57215664/3187762)
By adding .padding() to the NavigationView it seems to achieve the behaviour of always display the Master.
NavigationView {
MyMasterView()
DetailsView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
.padding()
Not sure if it is intended though. Might break in the future (works using Xcode 11.0, in simulator on iOS 13.0 and device with 13.1.2).
You should not rely on it. This comment seems to be the better answer: https://stackoverflow.com/a/57919024/3187762
in iOS 13.4, a "back to master view" button has been added to the iPad layout. From the release notes:
When using a NavigationView with multiple columns, the navigation bar now shows a control to toggle the columns. (49074511)
For example:
struct MyNavView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: Text("navigated"),
label: {Text("Go somwhere")}
)
.navigationBarTitle("Pick Item")
}
}
}
Has the following result:
Look this solution here
I hope this help
For my project I'm using this extension.
They will always use StackNavigationViewStyle for iPhone, iPad in a vertical orientation, and if you provide forceStackedStyle: true.
Otherwise DoubleColumnNavigationViewStyle will be used.
var body: some View {
NavigationView {
Text("Hello world")
}
.resolveNavigationViewStyle(forceStackedStyle: false)
}
extension View {
func resolveNavigationViewStyle(forceStackedStyle: Bool) -> some View {
if forceStackedStyle || UIDevice.current.userInterfaceIdiom == .phone {
return self.navigationViewStyle(StackNavigationViewStyle())
.eraseToAnyView()
} else {
return GeometryReader { p in
if p.size.height > p.size.width { self.navigationViewStyle(StackNavigationViewStyle())
} else {
self.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
.eraseToAnyView()
}
}
}

EnvironmentObject in SwiftUI

To my knowledge, I should be able to use EnvironmentObject to observe & access model data from any view in the hierarchy. I have a view like this, where I display a list from an array that's in LinkListStore. When I open AddListView and add an item, it correctly refreshes the ListsView with the added item. However, if I use a PresentationButton to present, I have to do AddListView().environmentObject(listStore), otherwise there will be a crash when showing AddListView. Is my basic assumption correct (and this is behavior is most likely a bug) or am I misunderstanding the use of EnvironmentObject?
Basically: #State to bind a variable to a view in the same View (e.g. $text to TextField), #ObjectBinding/BindableObject to bind variables to other Views, and EnvironmentObject to do the same as #ObjectBinding but without passing the store object every time. With this I should be able to add new items to an array from multiple views and still refresh the Lists View correctly? Otherwise I don't get the difference between ObjectBinding and EnvironmentObject.
struct ListsView : View {
#EnvironmentObject var listStore: LinkListStore
var body: some View {
NavigationView {
List {
NavigationButton(destination: AddListView()) {
HStack {
Image(systemName: "plus.circle.fill")
.imageScale(.large)
Text("New list")
}
}
ForEach(listStore.lists) { list in
HStack {
Image(systemName: "heart.circle.fill")
.imageScale(.large)
.foregroundColor(.yellow)
Text(list.title)
Spacer()
Text("\(list.linkCount)")
}
}
}.listStyle(.grouped)
}
}
}
#if DEBUG
struct ListsView_Previews : PreviewProvider {
static var previews: some View {
ListsView()
.environmentObject(LinkListStore())
}
}
#endif
From Apple docs EnvironmentObject:
EnvironmentObject
A dynamic view property that uses a bindable object supplied by an ancestor view to invalidate the current view whenever the bindable object changes.
It translates as the binding affects the current view hierarchy. My guess is that when you are presenting a new view via PresentationButton, you are creating a new hierarchy, which is not rooted in your view -- the one you have supplied the object to. I'd guess the workaround here is to add the object to the "global" environment by implementing a struct that confirms to the EnvironmentKey protocol.