By default the first list added to a view seems to be set to sidebar. Even if you don't specifically call .listStyle(SidebarListStyle()).
Is there any way that the list you set on the view (even if it's the first view) is not set to a sidebar? There is no indication on the Apple's documentation, and anything I have tried to style the list is not working.
So instead of the list looking like this:
It should look like:
Edit:
Yes, code. Any simple code will do.
struct ContentView: View {
var body: some View {
List {
NavigationLink(
destination: TextEView(),
) {
Text("One")
}
}
}
}
struct TextEView: View {
#State private var fullText: String = "This is some editable text..."
var body: some View {
TextEditor(text: $fullText)
}
}
For your case try
List {
NavigationLink(
destination: TextEView(),
) {
Text("One")
}
}
.listStyle(.plain) // << here !!
Related
I am having trouble getting the SwiftUI TextEditor to work when it is in a Child View.
This is a small example that demonstrates the issue for me:
import SwiftUI
struct ContentView: View {
#State private var someText: String = "Hello World"
var body: some View {
VStack {
HStack {
Button("Text 1", action: {someText = "hello"})
Button("Text 2", action: {someText = "world"})
}
ViewWithEditor(entry: $someText)
}
}
}
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
When I click the buttons I expected the Editor text to change, however it remains with its initial value.
The print statement show that the "localString" variable is being updated.
Is TextEditor broken or am I missing something fundamental ??
If you move the buttons into the same view as the TextEditor, directly changing local state var it works as expected.
This is being run under MacOS in case it makes a difference.
TIA Alan.
Ok. So a proxy Binding does the job for me. See updated editor view below:
struct ViewWithEditor: View {
#Binding var entry: String
var body: some View
{
let localString = Binding<String> (
get: {
entry
},
set: {
entry = $0
}
)
return VStack {
Text(entry)
TextEditor(text: localString)
}
}
A bit more ugly (proxy bindings just seem clutter), but in some ways simpler..
It allows for the result of the edit to be reviewed / rejected before being pushed into the bound var.
This is occurring because the binding entry var is not actually being used after initialization of ViewWithEditor. In order to make this work without using the proxy binding add onChange to the ViewWithEditor as below:
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
The problem here is that now entry is not updating if localString changes. One could just use the same approach as before:
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
.onChange(of: localString) {
entry = $0
}
}
but why not just use $entry as the binding string for TextEditor?
I'd like to hide/show the details split view of a NavigationSplitView on macOS.
However NavigationSplitViewVisibility does not seem to have such option. Changing .navigationSplitViewColumnWidth() or .frame() has no effect on the details view although it works well with the content and list view.
NavigationSplitView {
List(selection: $selection)
} content: {
content(for: selection)
} detail: {
Text("Detail")
}
Did Apple forget to implement such a feature? :/
Trying to figure out an answer to the same question for myself, I have come to this conclusion:
A NavigationSplitView is meant to display a hierarchy where each next level (sidebar, content, detail) is a sub-level of of the previous one. In such a structure you might always want to show a detail view, even it is empty.
In any case, even if that is not the logic, the way to make the "detail" part hidable would be by implementing a two-column navigation with NavigationSplitView and adding a DetailView, enclosing all of these in an HStack and making the DetailView visibility conditional:
struct MyView: View {
#State var showingDetail: Bool = true
var body: some View {
HStack {
NavigationSplitView {
SidebarView()
} detail: {
ContentView()
}
if showingDetail {
DetailView()
}
}
.toolbar {
Toggle(isOn: $showingDetail) {
Image(systemName: "sidebar.trailing")
}
.toggleStyle(.button)
}
}
}
I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.
I've come to SwiftUI from UIKit and I'm having trouble with a NavigationLink not animating when presenting a new View.
I've set up the view structure to include the NavigationLink when the following property is non-nil:
#State private var retrievedDeviceIdentity: Proteus.DeviceIdentity?
The Proteus.DeviceIdentity type is a basic data struct. This property is populated by a successful asynchronous closure, rather than a direct user interaction. Hence, the view structure is set up like so, using NavigationLink's destination:isActive:label: initialiser:
var body: some View {
NavigationView {
VStack {
Form {
// Form building
}
if let deviceIdentity = retrievedDeviceIdentity {
NavigationLink(
destination: AddDeviceLinkDeviceForm(deviceIdentity: deviceIdentity),
isActive: .constant(retrievedDeviceIdentity != nil),
label: {
EmptyView()
}
)
.onDisappear() {
updateSyncButtonEnabledState()
}
}
}
}
}
When retrievedDeviceIdentity is populated to be non-nil the new View is indeed presented. However, there is no slide transition to that View; it just changes immediately. When in that new view, tapping on the back button does do the slide transition back to this view.
Any ideas how to fix this? As I'm pretty new to SwiftUI if I've set the new structure up wrong then I'd welcome feedback on that too.
(I'm using Xcode 12.3 on macOS Big Sur 11.0.1.)
#Asperi got close, but moving the NavigationLink led to the view not presenting at all.
What did work was removing the if brace unwrapping retrievedDeviceIdentity:
var body: some View {
NavigationView {
VStack {
Form {
// Form building
}
NavigationLink(
destination: AddDeviceLinkDeviceForm(deviceIdentity: deviceIdentity),
isActive: .constant(retrievedDeviceIdentity != nil),
label: {
EmptyView()
}
)
.onDisappear() {
updateSyncButtonEnabledState()
}
}
}
This required AddDeviceLinkDeviceForm's deviceIdentity property to be made optional to accept the wrapped value.
I think it is due to conditional injection, try instead to have it persistently included in view hierarchy (and so be registered in NavigationView), like
VStack {
Form {
// Form building
}
}
.background(
NavigationLink(
destination: AddDeviceLinkDeviceForm(deviceIdentity: retrievedDeviceIdentity),
isActive: .constant(retrievedDeviceIdentity != nil),
label: {
EmptyView()
}
)
.onDisappear() {
updateSyncButtonEnabledState()
}
)
Note: I'm not sure about your expectation for .onDisappear and why do you need it, probably it will be needed to move in some other place or under different modifier.
I am currently developing an app for watchOS 6 (independent app) using Swift/SwiftUI in XCode 11.5 on macOS Catalina.
Before a user can use my app, a configuration process is required. As the configuration process consists of several different views which are shown one after each other, I implemented this by using navigation links.
After the configuration process has been finished, the user should click on a button to return to the "main" app (main view). For controlling views which are on the same hierarchical level, my plan was to use an EnvironmentObject (as far as I understood, an EnvironmentObject once injected is handed over to the subviews and subviews can use the EnvironmentObject) in combination with a "controlling view" which controls the display of the views. Therefore, I followed the tutorial: https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
This is my code:
ContentView.swift
struct ContentView: View {
var body: some View {
ContentViewManager().environmentObject(AppStateControl())
}
}
struct ContentViewManager: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
if(appStateControl.callView == "AppConfig") {
AppConfig()
}
if(appStateControl.callView == "AppMain") {
AppMain()
}
}
}
}
AppStateControl.swift
class AppStateControl: ObservableObject {
#Published var callView: String = "AppConfig"
}
AppConfig.swift
struct AppConfig: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Main")
NavigationLink(destination: DetailView1().environmentObject(appStateControl)) {
Text("Show Detail View 1")
}
}
}
}
struct DetailView1: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Detail View 1")
NavigationLink(destination: DetailView2().environmentObject(appStateControl)) {
Text("Show Detail View 2")
}
}
}
}
struct DetailView2: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Detail View 2")
Button(action: {
self.appStateControl.callView = "AppMain"
}) {
Text("Go to main App")
}
}
}
}
AppMain.swift
struct AppMain: View {
var body: some View {
Text("Main App")
}
}
In a previous version of my code (without the handing over of the EnvironmentObject all the time) I got a runtime error ("Thread 1: Fatal error: No ObservableObject of type AppStateControl found. A View.environmentObject(_:) for AppStateControl may be missing as an ancestor of this view.") caused by line 41 in AppConfig.swift. In the internet, I read that this is probably a bug of NavigationLink (see: https://www.hackingwithswift.com/forums/swiftui/environment-object-not-being-inherited-by-child-sometimes-and-app-crashes/269, https://twitter.com/twostraws/status/1146315336578469888). Thus, the recommendation was to explicitly pass the EnvironmentObject to the destination of the NavigationLink (above implementation). Unfortunately, this also does not work and instead a click on the button "Go to main App" in "DetailView2" leads to the view "DetailView1" instead of "AppMain".
Any ideas how to solve this problem? To me, it seems that a change of the EnvironmentObject in a view called via a navigation link does not refresh the views (correctly).
Thanks in advance.
One of the solutions is to create a variable controlling whether to display a navigation stack.
class AppStateControl: ObservableObject {
...
#Published var isDetailActive = false // <- add this
}
Then you can use this variable to control the first NavigationLink by setting isActive parameter. Also you need to add .isDetailLink(false) to all subsequent links.
First link in stack:
NavigationLink(destination: DetailView1().environmentObject(appStateControl), isActive: self.$appStateControl.isDetailActive) {
Text("Show Detail View 1")
}
.isDetailLink(false)
All other links:
NavigationLink(destination: DetailView2().environmentObject(appStateControl)) {
Text("Show Detail View 2")
}
.isDetailLink(false)
Then just set isDetailActive to false to pop all your NavigationLinks and return to the main view:
Button(action: {
self.appStateControl.callView = "AppMain"
self.appStateControl.isDetailActive = false // <- add this
}) {
Text("Go to main App")
}