How to use form sections from separate file? - swiftui

Earlier this year I created an app using SwiftUI. One part of this app is broken when running in iOS 14. I have a creation and editing form, the goal is to reuse the sections that they have in common. Previously it worked to have these sections in a Group, since iOS 14 that fails and puts all sections into a single view.
It's currently implemented similar to this:
struct CreateForm: View {
var body: some View {
Form {
Section {
Text("First section")
}
OtherSections()
}
}
}
struct OtherSections: View {
var body: some View {
Group { // <--- This is causing the issue
Section {
Text("First Detail Section")
}
// In real implementation there are some conditional sections
if false {
Section {
Text("Second Detail Section")
}
}
}
}
}
Any suggestions on how to solve this issue?

Try using #ViewBuilder instead of Group:
struct OtherSections: View {
#ViewBuilder
var body: some View {
Section {
Text("First Detail Section")
}
// In real implementation there are some conditional sections
if false {
Section {
Text("Second Detail Section")
}
}
}
}
(Also make sure OtherSections conforms to View)

Related

Footer of section of list shown as a sidebar panel in NavigationSplitView is missing

I am dabbling with the NavigationSplitView introduced this year.
I have a funny behaviour when I use a list with several sections in the side panel, where the sections have footers: those footers are not displayed at all, but only when used in the side panel.
Here's my List in a separate view:
struct SampleListWithSections: View {
var body: some View {
List {
Section {
Text("Cell")
} header: {
Text("Header")
} footer: {
Text("Footer")
}
Section {
Text("Cell2")
} header: {
Text("Header2")
} footer: {
Text("Footer2")
}
}
}
}
When I use this view as the root view, everything works as expected …
#main
struct poc_Navigation2App: App {
#StateObject private var appModel = AppModel.mock
var body: some Scene {
WindowGroup {
SampleListWithSections()
}
}
}
… as in: the footer is shown.
But when I use my List view within NavigationSplitView like so …
#main
struct poc_Navigation2App: App {
#StateObject private var appModel = AppModel.mock
var body: some Scene {
WindowGroup {
NavigationSplitView {
SampleListWithSections()
} detail: {
Text("Unimplemented Detail View")
}
}
}
}
… the footer is omitted …
Am I "holding it wrong" or is this a bug? If the latter, is it a known bug?
When you use a list as a sidebar in a NavigationSplitView, it defaults to using the list style called, unsurprisingly, SidebarListStyle - the equivalent to explicitly adding .listStyle(.sidebar) as a modifier to your list.
This style formats sections with bold headers, makes the section collapsible, and (crucially) doesn't display footers.
You can choose your own list style for your sidebar, though. For example, you could set it to an inset grouped style:
List {
// ...
}
.listStyle(.insetGrouped)
This will display your footers, and for the most part your sections may look similar to those in the default styles. However, they won't be collapsible in the same way, and the header text will be much smaller.
You can partly compensate for the change in header style by also using the .headerProminence(.increased) modifier on some or all of your sections if you need to.

SwiftUI: How to update detail column from a distant child using iOS16/iPadOS16 NavigationSplitView and NavigationLink

I'm trying to update an older app to use the new NavigationSplitView and NavigationLink, but trying to wrap my head around the proper way to do it when the sidebar has a hierarchy of child objects and I want to update the detail view from a distant child object. For example, based on the WWDC2022 example project Navigation Cookbook I tried the following and it didn't work:
TestApp.swift
#main
struct TestApp: App {
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
NavigationSplitView {
ProjectListView()
} detail: {
if let chapter = appState.chapter {
ChapterDetailView(chapter: chapter)
} else {
Text("Pick a Chapter")
}
}
}
}
}
ChapterListView.swift < A distant (3 levels down) sibling of ProjectListView()
List(selection: $appState.chapter) {
ForEach(chapters) { chapter in
NavigationLink(chapter.title ?? "A Chapter", value: chapter)
}
}
appState.swift
class AppState: ObservableObject {
#Published var chapter: Chapter?
}
I'm sure I'm just not understanding the basics of how the new way of doing navigation works. Yes, I am targeting iOS16
This is a known bug in beta 1. To workaround, the logic in the detail section needs to be wrapped in a ZStack. From the release notes:
Conditional views in columns of NavigationSplitView fail to update on some state changes. (91311311)
Workaround: Wrap the contents of the column in a ZStack. TestApp works if changed to this:
#main
struct TestApp: App {
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
NavigationSplitView {
ProjectListView()
} detail: {
// Wrap in a ZStack to fix this bug
ZStack {
if let chapter = appState.chapter {
ChapterDetailView(chapter: chapter)
} else {
Text("Pick a Chapter")
}
}
}
}
}
}

How to use NavigationLink for List view swipe action

I'm wondering how to place NavigationLink into swipeActions section in code below. Code itself is compiled without any issue but when I tap "Edit" link nothing happens. My intention is to show another view by tapping "Edit".
Thanks
var body: some View {
List {
ForEach(processes, id: \.id) { process in
NavigationLink(process.name!, destination: MeasurementsView(procID: process.id!, procName: process.name!))
.swipeActions() {
Button("Delete") {
deleteProcess = true
}.tint(.red)
NavigationLink("Edit", destination: ProcessView(procID: process.id!, procName: process.name!)).tint(.blue)
}
}
}
}
It does not work because swipeActions context is out of NavigationView. Instead we can use same NavigationLink for conditional navigation, depending on action.
Here is a simplified demo of possible approach - make destination conditional and use programmatic activation of link.
Tested with Xcode 13.2 / iOS 15.2
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(0..<2, id: \.id) {
// separate into standalone view for better
// state management
ProcessRowView(process: $0)
}
}
}
}
}
struct ProcessRowView: View {
enum Action {
case view
case edit
}
#State private var isActive = false
#State private var action: Action?
let process: Int
var body: some View {
// by default navigate as-is
NavigationLink("Item: \(process)", destination: destination, isActive: $isActive)
.swipeActions() {
Button("Delete") {
}.tint(.red)
Button("Edit") {
action = .edit // specific action
isActive = true // activate link programmatically
}.tint(.blue)
}
.onChange(of: isActive) {
if !$0 {
action = nil // reset back
}
}
}
#ViewBuilder
private var destination: some View {
// construct destination depending on action
if case .edit = action {
Text("ProcessView")
} else {
// just to demo different type destinations
Color.yellow.overlay(Text("MeasurementsView"))
}
}
}

SwiftUI artefact after detail with list is presented

With the simple navigation-link structure below, I get a strange artefact after the detail view with a list is pushed on screen, as shown here https://youtu.be/LU9uluD5hEw.
If I include a section with a header, the view does not jerk up after the screen is loaded, but remains in its originally presented position. Anybody else experiencing this problem, or know how to fix it?
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: DetailView()) {
Text("Link")
}
}
.navigationBarTitle("Master")
.listStyle(GroupedListStyle())
}
}
}
struct DetailView: View {
var body: some View {
List {
Text("Detail")
}
.navigationBarTitle("Detail")
.listStyle(GroupedListStyle())
}
}
This is especially annoying for picker details where I cannot add an empty section header as a workaround.
Workaround: It looks like a bug in TitleDisplayMode.large mode, because in the .inline mode such effect is not observed. So, the following might be considered as workaround if it is allowed by app design:
struct DetailView: View {
var body: some View {
List {
Text("Detail")
}
.navigationBarTitle("Detail", displayMode: .inline)
.listStyle(GroupedListStyle())
}
}

Disable drag to dismiss in SwiftUI Modal

I've presented a modal view but I would like the user to go through some steps before it can be dismissed.
Currently the view can be dragged to dismiss.
Is there a way to stop this from being possible?
I've watched the WWDC Session videos and they mention it but I can't seem to put my finger on the exact code I'd need.
struct OnboardingView2 : View {
#Binding
var dismissFlag: Bool
var body: some View {
VStack {
Text("Onboarding here! 🙌🏼")
Button(action: {
self.dismissFlag.toggle()
}) {
Text("Dismiss")
}
}
}
}
I currently have some text and a button I'm going to use at a later date to dismiss the view.
iOS 15+
Starting from iOS 15 we can use interactiveDismissDisabled:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
We just need to attach it to the sheet. Here is an example from the documentation:
struct PresentingView: View {
#Binding var showTerms: Bool
var body: some View {
AppContents()
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
#State private var acceptedTerms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTerms = true
}
}
.interactiveDismissDisabled(!acceptedTerms)
}
}
It is easy if you use the 3rd party lib Introspect, which is very useful as it access the corresponding UIKit component easily. In this case, the property in UIViewController:
VStack { ... }
.introspectViewController {
$0.isModalInPresentation = true
}
Not sure this helps or even the method to show the modal you are using but when you present a SwiftUI view from a UIViewController using UIHostingController
let vc = UIHostingController(rootView: <#your swiftUI view#>(<#your parameters #>))
you can set a modalPresentationStyle. You may have to decide which of the styles suits your needs but .currentContext prevents the dragging to dismiss.
Side note:I don't know how to dismiss a view presented from a UIHostingController though which is why I've asked a Q myself on here to find out 😂
I had a similar question here
struct Start : View {
let destinationView = SetUp()
.navigationBarItem(title: Text("Set Up View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Set Up")
}
}
}
}
The main thing here is that it is hiding the back button. This turns off the back button and makes it so the user can't swipe back ether.
For the setup portion of your app you could create a new SwiftUI file and add a similar thing to get home, while also incorporating your own setup code.
struct SetUp : View {
let destinationView = Text("Your App Here")
.navigationBarItem(title: Text("Your all set up!"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Done")
}
}
}
}
There is an extension to make controlling the modal dismission effortless, at https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
A temporary solution before the official solution released by Apple.
/// Example:
struct ContentView: View {
#State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}