How to remove highlight on tap of List with SwiftUI? - ios

How to remove highlight on tap of List with SwiftUI?
List {
}.whatModifierToAddHere?
The selection manager documentation doesnt say anything about it.

I know I'm a bit late, but it's for those of you who are searching (like me πŸ˜‡)
What I found
I guess you should take a look at the short article How to disable the overlay color for images inside Button and NavigationLink from #TwoStraws
Just add the .buttonStyle(PlainButtonStyle()) modifier to your item in the List and you'll have what you wanted. It also makes the Buttons work again in the List, which is another problem I encountered.
A working example for Swift 5.1 :
import Combine
import SwiftUI
struct YourItem: Identifiable {
let id = UUID()
let text: String
}
class YourDataSource: ObservableObject {
let willChange = PassthroughSubject<Void, Never>()
var items = [YourItem]()
init() {
items = [
YourItem(text: "Some text"),
YourItem(text: "Some other text")
]
}
}
struct YourItemView: View {
var item: YourItem
var body: some View {
VStack(alignment: .leading) {
Text(item.text)
HStack {
Button(action: {
print("Like")
}) {
Image(systemName: "heart.fill")
}
Button(action: {
print("Star")
}) {
Image(systemName: "star.fill")
}
}
}
.buttonStyle(PlainButtonStyle())
}
}
struct YourListView: View {
#ObservedObject var dataSource = YourDataSource()
var body: some View {
List(dataSource.items) { item in
YourItemView(item: item)
}
.navigationBarTitle("List example", displayMode: .inline)
.edgesIgnoringSafeArea(.bottom)
}
}
#if DEBUG
struct YourListView_Previews: PreviewProvider {
static var previews: some View {
YourListView()
}
}
#endif
As said in the article, it also works with NavigationLinks. I hope it helped some of you 🀞🏻

Simple answer to you question. Any cell that you don't want to highlight when tapped just add this modifier
.buttonStyle(PlainButtonStyle())
Therefore the modifier is not for the whole List is for each cell inside
var body: some View{
List{
ForEach(self.getElementsForList()){ element in
ElementCell(element: element)
.buttonStyle(PlainButtonStyle())
}
}
}

I know I'm a bit late but hope this will solve your problem.
You need to use UIKit modifiers to remove this. I recommend you to place them on SceneDelegate.swift.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = TabController()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
//MARK: Disable selection.
UITableView.appearance().allowsSelection = false
UITableViewCell.appearance().selectionStyle = .none
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
}
EDIT: This will disable all table view selections in your app. If instead, you want to disable the selection on a specific table view, you can disable it inside init().
struct SomeViewWithTableView: View {
init() {
//MARK: Disable selection.
UITableView.appearance().allowsSelection = false
UITableViewCell.appearance().selectionStyle = .none
}
var body: some View {
//your view code here
}
}

.listRowBackground worked for me
List {
ListItem()
.listRowBackground(Color.clear)
}

Just adding the .buttonStyle(PlainButtonStyle()) modifier to the item in the List, unfortunately didn't work for my case.
Instead I was able to obtain the desired effect -- not seeing any highlighting effect when tapping on a row -- by using the .onAppear() modifier on the List and the Appearance API for UITableViewCell.
As in the following example:
List {
ForEach(items) { item in
NavigationLink(destination: DetailView(item)) {
RowView(item)
}
}
}
.onAppear {
// this will disable highlighting the cell when is selected
UITableViewCell.appearance().selectionStyle = .none
// you can also remove the row separators
UITableView.appearance().separatorStyle = .none
}

This needs different approaches depending on the iOS.
For iOS 14:
.onAppear {
UITableViewCell.appearance().selectionStyle = .none
}
For iOS 15:
List {
ListItem()
.listRowBackground(Color.clear)
}

Edit:
The following only works if the SwiftUI view is embedded in a UIKit parent.
For anyone still wondering, you can use .onAppear modifier to achieve this.
.onAppear {
UITableViewCell.appearance().selectionStyle = .none
}
This is similar to viewWillAppear in UIKit.
Source: HackingWithSwift

A possible solution is to use a ZStack. And adjust insets to .zero. Change Color.red to your color.
Your List:
ForEach(items) { item in
ZStack {
NavigationLink(destination: DetailView()) {
EmptyView()
}
ItemRowWrap(item: item)
.background(Color.red)
}
}
.listStyle(InsetListStyle())
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Your ItemRowView:
struct ItemRowWrap: View {
let item: ListItem
var body: some View {
ItemRow(item: item)
.padding(EdgeInsets(top: 5, leading: 8, bottom: 5, trailing: 8))
}
}
You can adjust paddings as you need.

Setting the global AccentColor in Assets.xcassets as descirbed in

I met the same question, and I think it's a bug.
If I put a Text top the List, the navigation link item will be highlight;
If I remove it or put it below List, the navigation link item won't be highlight.
VStack {
Text("Some word") // Remove it or put it below List
List {
NavigationLink(destination: RedirectionFromList(data: data.first!)) {
Text("test")
}
}
}
If put the Text top the List, I can't find a way to dismiss highlight, very annoyed.

.onDisappear {
UITableViewCell.appearance().isSelected = false
}
You can set selected to false when the interface disappears or appears.

If I understand the question right, you want to select an item by clicking/tapping on it, and deselecting it when you click again.
struct SimpleSelectionView: View {
#State var items = ["First", "Second", "Third"]
#State var selectedItem: String?
var body: some View{
List{
ForEach(items, id: \.self) { item in
Text(item)
.onTapGesture {
if selectedItem == nil || selectedItem != item {
selectedItem = item
} else {
selectedItem = nil
}
}
.listRowBackground(self.selectedItem == item ? Color(red: 0.275, green: 0.420, blue: 0.153, opacity: 0.3) : .white)
}
}
}
}
The downside is that on macOS, you have to click the content (Text) rather than the row as such, but it's a nice lightweight solution, and if you want to do something with the selection, you can do it in didSet.

Related

How to despawn a Button and spawn a scrollView xcode swiftui [duplicate]

How do I toggle the presence of a button to be hidden or not?
We have the non-conditional .hidden() property; but I need the conditional version.
Note: we do have the .disabled(bool) property available, but not the .hidden(bool).
struct ContentView: View {
var body: some View {
ZStack {
Color("SkyBlue")
VStack {
Button("Detect") {
self.imageDetectionVM.detect(self.selectedImage)
}
.padding()
.background(Color.orange)
.foreggroundColor(Color.white)
.cornerRadius(10)
.hidden() // ...I want this to be toggled.
}
}
}
}
I hope hidden modifier gets argument later, but since then, Set the alpha instead:
#State var shouldHide = false
var body: some View {
Button("Button") { self.shouldHide = true }
.opacity(shouldHide ? 0 : 1)
}
For me it worked perfectly to set the frame's height to zero when you do not want to see it. When you want to have the calculated size, just set it to nil:
SomeView
.frame(height: isVisible ? nil : 0)
If you want to disable it in addition to hiding it, you could set .disabled with the toggled boolean.
SomeView
.frame(height: isVisible ? nil : 0)
.disabled(!isVisible)
You can utilize SwiftUI's new two-way bindings and add an if-statement as:
struct ContentView: View {
#State var shouldHide = false
var body: some View {
ZStack {
Color("SkyBlue")
VStack {
if !self.$shouldHide.wrappedValue {
Button("Detect") {
self.imageDetectionVM.detect(self.selectedImage)
}
.padding()
.background(Color.orange)
.foregroundColor(Color.white)
.cornerRadius(10)
}
}
}
}
}
The benefit of doing this over setting the opacity to 0 is that it will remove the weird spacing/padding from your UI caused from the button still being in the view, just not visible (if the button is between other view components, that is).
all the answers here works specifically for a button to be hidden conditionally.
What i think might help is making a modifier itself conditionally e.g:
.hidden for button/view, or maybe .italic for text, etc..
Using extensions.
For text to be conditionally italic it is easy since .italic modifier returns Text:
extension Text {
func italicConditionally(isItalic: Bool) -> Text {
isItalic ? self.italic() : self
}
}
then applying conditional italic like this:
#State private var toggle = false
Text("My Text")
.italicConditionally(isItalic: toggle)
However for Button it is tricky, since the .hidden modifier returns "some view":
extension View {
func hiddenConditionally(isHidden: Bool) -> some View {
isHidden ? AnyView(self.hidden()) : AnyView(self)
}
}
then applying conditional hidden like this:
#State private var toggle = false
Button("myButton", action: myAction)
.hiddenConditionally(isHidden: toggle)
You can easily hide a view in SwiftUI using a conditional statement.
struct TestView: View{
#State private var isVisible = false
var body: some View{
if !isVisible {
HStack{
Button(action: {
isVisible.toggle()
// after click you'r view will be hidden
}){
Text("any view")
}
}
}
}
}
It isn't always going to be a pretty solution, but in some cases, adding it conditionally may also work:
if shouldShowMyButton {
Button(action: {
self.imageDetectionVM.detect(self.selectedImage)
}) {
Text("Button")
}
}
There will be an issue of the empty space in the case when it isn't being shown, which may be more or less of an issue depending on the specific layout. That might be addressed by adding an else statement that alternatively adds an equivalently sized blank space.
#State private var isHidden = true
VStack / HStack
if isHidden {
Button {
if !loadVideo(),
let urlStr = drill?.videoURL as? String,
let url = URL(string: urlStr) {
player = VideoPlayerView(player: AVPlayer(), videoUrl: url)
playVideo.toggle()
}
} label: {
Image(playVideo ? "ic_close_blue" : "ic_video_attached")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50)
}
.buttonStyle(BorderlessButtonStyle())
}
.onAppear {
if shouldShowButton {
isHidden = false
} else {
isVideoButtonHidden = true
}
}

How do I change the background colour of a list in swiftui? [duplicate]

I'm trying to recreate an UI I built with UIKit in SwiftUI but I'm running into some minor issues.
I want the change the color of the List here, but no property seems to work as I expects. Sample code below:
struct ListView: View {
#EnvironmentObject var listData: ListData
var body: some View {
NavigationView {
List(listData.items) { item in
ListItemCell(item: item)
}
.content.background(Color.yellow) // not sure what content is defined as here
.background(Image("paper-3")) // this is the entire screen
}
}
}
struct ListItemCell: View {
let item: ListItem
var body: some View {
NavigationButton(destination: Text(item.name)) {
Text("\(item.name) ........................................................................................................................................................................................................")
.background(Color.red) // not the area I'm looking for
}.background(Color.blue) // also not the area I'm looking for
}
}
Ok, I found the solution for coloring the list rows:
struct TestRow: View {
var body: some View {
Text("This is a row!")
.listRowBackground(Color.green)
}
}
and then in body:
List {
TestRow()
TestRow()
TestRow()
}
This works as I expect, but I have yet to find out how to then remove the dividing lines between the rows...
This will set the background of the whole list to green:
init() {
UITableView.appearance().separatorStyle = .none
UITableViewCell.appearance().backgroundColor = .green
UITableView.appearance().backgroundColor = .green
}
struct ContentView: View {
var strings = ["a", "b"]
var body: some View {
List {
ForEach(strings, id: \.self) { string in
Text(string)
}.listRowBackground(Color.green)
}
}
}
You can do it by changing UITableView's appearance.
UITableView.appearance().backgroundColor = UIColor.clear
just put this line in Appdelegate's didFinishLaunchingWithOptions method.
In replace of UIColor.clear set whatever color you want to add in background color of list.
Changing Background Color
As other have mentioned, changing the UITableView background will affect all other lists in your app.
However if you want different background colors you can set the default to clear, and set the background color in swiftui views like so:
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
// Ignore safe area to take up whole screen
.background(Color.purple.ignoresSafeArea())
.onAppear {
// Set the default to clear
UITableView.appearance().backgroundColor = .clear
}
You probably want to set the tableview appearance earlier, such as in the SceneDelegate or root view like so:
// SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else {
print("Returning because screne does not exist")
return
}
// Set here
UITableView.appearance().backgroundColor = .clear
let contentView = ContentView()
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
// Root App View
#main
struct ListBackgroundApp: App {
init() {
UITableView.appearance().backgroundColor = .clear
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
2022
MacOS Solution
The following code makes ALL OF Lists background color transparent:
// Removes background from List in SwiftUI
extension NSTableView {
open override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
backgroundColor = NSColor.clear
if let esv = enclosingScrollView {
esv.drawsBackground = false
}
}
}
..........
..........
..........
the following code makes ALL OF TextEditors background color transparent:
extension NSTextView {
open override var frame: CGRect {
didSet {
backgroundColor = .clear
drawsBackground = true
}
}
}
There is an argument: listRowBackground() in SwiftUI, but if you use List directly to iterate the data collection, it doesn't work.
Here is my workaround:
List {
// To make the background transparent, we have we use a ForEach as a wrapper
ForEach(files) {file in
Label(
title: { Text(file.name ?? fileOptionalFiller).lineLimit(listRowTextLineLimit) },
icon: { AppIcon.doc.foregroundColor(.primary) }
)
}
.listRowBackground(Color.primary.colorInvert())
}
Basically, listRowBackground() works if you use a ForEach inside List.
I was able to get the whole list to change color by using colorMultiply(Color:). Just add this modifier to the end of the list view, and then the padding will push the table to the device edges. For example:
List {...}.colorMultiply(Color.green).padding(.top)
https://www.hackingwithswift.com/quick-start/swiftui/how-to-adjust-views-by-tinting-and-desaturating-and-more
I do not know what is the connection but if you wrap the list with Form it is working.
Form {
List(viewModel.currencyList, id: \.self) { currency in
ItemView(item: currency)
}
.listRowBackground(Color("Primary"))
.background(Color("Primary"))
}
iOS 16 provides a modifier to control the background visibility of List (and other scrollable views): scrollContentBackground(_:)
You can hide the standard system background via .hidden. If you provide a background as well, that will become visible.
List {
Text("One")
Text("Two")
}
.background(Image("MyImage"))
.scrollContentBackground(.hidden)
You may also want to customize the background of list rows - the individual cells - and separators. This can be done like so:
List {
Section("Header") {
Text("One")
Text("Two")
.listRowBackground(Color.red)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
.scrollContentBackground(.hidden)
struct Details: View {
var body: some View {
Spacer().overlay(
List {
Text("Hello World!").font(.title2)
.listRowBackground(Color.clear)
Text("Hello World again").font(.title2)
.listRowBackground(Color.clear)
}.onAppear() {
UITableView.appearance().backgroundColor = UIColor.green
UITableViewCell.appearance().backgroundColor = UIColor.green
}
)
}
}
The answer by Islom Alimov https://stackoverflow.com/a/59970379/9439097 seems to be the best implementation so far in my opinion.
Only drawback: this also changes the background color of all other list views in your app, so you need to manually change them back unless you want the same color everywhere.
Here is an example view:
import SwiftUI
struct TestView1: View {
init(){
UITableView.appearance().backgroundColor = UIColor(Color.clear)
}
#State var data = ["abc", "def"]
var body: some View {
VStack {
List {
ForEach(data, id: \.self) {element in
Text("\(String(describing: element))")
}
.background(Color.green)
.listRowBackground(Color.blue)
}
.background(Color.yellow)
Spacer()
Color.red
}
}
}
struct TestView1_Previews: PreviewProvider {
static var previews: some View {
TestView1()
}
}
produces:
Someone may find this useful if attempting to create a floating type cell with SwiftUI using .listRowBackground and applying .padding
var body: some View {
NavigationView {
List {
ForEach (site) { item in
HStack {
Text(String(item.id))
VStack(alignment: .leading) {
Text(item.name)
Text(item.crop[0])
}
}.listRowBackground(Color.yellow)
.padding(.trailing, 5)
.padding(.leading, 5)
.padding(.top, 2)
.padding(.bottom, 2))
}
}
.navigationBarTitle(Text("Locations"))
}
}
I assume the listRowPlatterColor modifier should do this, but isn't as of Xcode 11 Beta 11M336w
var body: some View {
List(pokemon) { pokemon in
PokemonCell(pokemon: pokemon)
.listRowPlatterColor(.green)
}
}
.colorMultiply(...)
As an option you can .colorMultiply(Color.yourColor) modifier.
Warning: this does not change the color! This only applies the Multiply modifier to the current color. Please read the question before any action, because you are probably looking for: "How to CHANGE the background color of a List in SwiftUI" and this will not work for you. ❄️
Example:
List (elements, id:\.self ) { element in
Text(element)
}
.colorMultiply(Color.red) <--------- replace with your color
For me, a perfect solution to change the background of List in SwiftUI is:
struct SomeView: View {
init(){
UITableView.appearance().backgroundColor = UIColor(named: "backgroundLight")
}
...
}
List is not perfect yet.
An option would be to use it like this -> List { ForEach(elements) { }} instead of List($elements)
On my end this is what worked best up to now.
Like #FontFamily said, it shouldn't break any List default behaviors like swiping.
Simply Add UITableView appearance background color in init() method and add list style (.listStyle(SidebarListStyle()). Don't forget to import UIKit module
struct HomeScreen: View {
init() {
UITableView.appearance().backgroundColor = .clear
}
let tempData:[TempData] = [TempData( name: "abc"),
TempData( name: "abc"),
TempData( name: "abc"),
TempData( name: "abc")]
var body: some View {
ZStack {
Image("loginBackgound")
.resizable()
.scaledToFill()
List{
ForEach(tempData){ data in
Text(data.name)
}
}
.listStyle(SidebarListStyle())
}
.ignoresSafeArea(edges: .all)
}
}
Using UITableView.appearance().backgroundColor is not a good idea as it changes the backgroundColor of all tables. I found a working solution for color changing at the exact table you selected in iOS 14, 15.
We will change the color using a modifier that needs to be applied inside the List
extension View {
func backgroundTableModifier(_ color: UIColor? = nil) -> some View {
self.modifier(BackgroundTableModifier(color: color))
}
}
Our task is to find the UITableView and after that change the color.
private struct BackgroundTableModifier: ViewModifier {
private let color: UIColor?
#State private var tableView: UITableView?
init(color: UIColor?) {
self.color = color
}
public func body(content: Content) -> some View {
if tableView?.backgroundColor != color {
content
.overlay(BackgroundTableViewRepresentable(tableBlock: { tableView in
tableView.backgroundColor = color
self.tableView = tableView
}))
} else {
content
}
}
}
private struct BackgroundTableViewRepresentable: UIViewRepresentable {
var tableBlock: (UITableView) -> ()
func makeUIView(context: Context) -> BackgroundTableView {
let view = BackgroundTableView(tableBlock: tableBlock)
return view
}
func updateUIView(_ uiView: BackgroundTableView, context: Context) {}
}
class BackgroundTableView: UIView {
var tableBlock: (UITableView) -> ()
init(tableBlock: #escaping (UITableView) -> ()) {
self.tableBlock = tableBlock
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let tableView = findTableView(in: self) {
tableBlock(tableView)
}
}
private func findTableView(in view: UIView) -> UITableView? {
if let tableView = view as? UITableView {
return tableView
}
if let superView = view.superview {
return findTableView(in: superView)
}
return nil
}
}
In order to find UITableView, the modifier must be inside the List. Naturally, you need to ensure that the modifier is called only once, you do not need to apply it to each row. Here is an example of usage
List {
rows()
.backgroundTableModifier(.clear)
}
func rows() -> some View {
ForEach(0..<10, id: \.self) { index in
Row()
}
}
In iOS 16, we got a native way to do this via scrollcontentbackground modifier.
You can either change the color by setting a color (ShapeStyle) to scrollcontentbackground.
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.scrollContentBackground(Color.pink)
Or you can hide the background .scrollContentBackground(.hidden) and set a custom one with .backgroud modifier.
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.background {
Image("ventura")
}
.scrollContentBackground(.hidden)
I've inspired some of the configurator used to config per page NavigationView nav bar style and write some simple UITableView per page configurator not use UITableView.appearance() global approach
import SwiftUI
struct TableViewConfigurator: UIViewControllerRepresentable {
var configure: (UITableView) -> Void = { _ in }
func makeUIViewController(context: UIViewControllerRepresentableContext<TableViewConfigurator>) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<TableViewConfigurator>) {
let tableViews = uiViewController.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()
for tableView in tableViews {
self.configure(tableView)
}
}
}
Then there is UIView extension needed to find all UITableViews
extension UIView {
func subviews<T:UIView>(ofType WhatType:T.Type) -> [T] {
var result = self.subviews.compactMap {$0 as? T}
for sub in self.subviews {
result.append(contentsOf: sub.subviews(ofType:WhatType))
}
return result
}
}
And usage at the end is:
List {
}.background(TableViewConfigurator {
$0.backgroundColor = .red
})
Maybe one thing should be improved that is usage of navigationController?.topViewController to make it work even without navigationController in view controllers hierarchy
If anyone came here looking for solutions for background in landscape not full width on iPhone X/11 try:
.listRowBackground(Color("backgroundColour").edgesIgnoringSafeArea(.all))
If you want to avoid setting the appearance for all table views globally, you can combine UITableView.appearance(whenContainedInInstancesOf:) with UIHostingController. Thanks DanSkeel for the comment you left above pointing this out. This is how I used it:
public class ClearTableViewHostingController<Content>: UIHostingController<Content> where Content: View {
public override func viewDidLoad() {
UITableView.appearance(whenContainedInInstancesOf: [ClearTableViewHostingController<Content>.self]).backgroundColor = .clear
}
}
You can use ClearTableViewHostingController like this:
let view = MyListView()
let viewController = ClearTableViewHostingController(coder: coder, rootView: view)
Then in your view you can set the list background color like so:
List {
Text("Hello World")
}
.background(Color.gray)
Make extension List like:
extension List{
#available(iOS 14, *)
func backgroundList(_ color: Color = .clear) -> some View{
UITableView.appearance().backgroundColor = UIColor(color)
return self
}
}
you can use introspect library from Github to set the background color for the underlying table view like this:
List { ... } .introspectTableView { tableView in
tableView.backgroundColor = .yellow
}
For some reason color change is not working, you can try the .listStyle to .plain
Code:
struct ContentView: View {
var body: some View {
VStack {
Text("Test")
List {
ForEach(1 ..< 4) { items in
Text(String(items))
}
}
.listStyle(.plain)
}
}
Changing background did not work for me, because of the system background. I needed to hide it.
List(examples) { example in
ExampleRow(example: example)
}.background(Color.white.edgesIgnoringSafeArea(.all))
.scrollContentBackground(.hidden)
Xcode Version 12.4
The Background property worked for me, but with the mandatory use of Opacity.
Without opacity it is not work.
List {
ForEach(data, id: \.id) { (item) in
ListRow(item)
.environmentObject(self.data)
}
}
.background(Color.black)
.opacity(0.5)

SwiftUI - NavigationLink cell in a Form stays highlighted after detail pop

In iOS 14, it appears that NavigationLinks do not become deselected after returning in a Form context.
This is also true for Form Pickers and anything else that causes the presentation of another View from a list (giving a highlight context to the presenting cell).
I didn't notice this behaviour in iOS 13.
Is there a way to 'deselect' the highlighted row once the other view is dismissed?
Example code:
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("Detail")) {
Text("Link")
}
}
}
}
(Different) Example visual:
In my case this behaviour appeared when using any Viewcontent (e.g. Text(), Image(), ...) between my NavigationView and List/Form.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
Putting the Text() beneath the List does not make any problems:
var body: some View {
NavigationView {
VStack {
List {
NavigationLink(destination: Text("Does work correct")) {
Text("Does work correct")
}
}
Text("This text doesn't make problems.")
}
}
}
This is definitely a XCode 12 bug. As more people report this, as earlier it gets resolved.
I have also run into this issue and believed I found the root cause in my case.
In my case I had.a structure like the following:
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
}
}
struct Page2View: View {
var body: some View {
List {
NavigationLink("Page 3", destination: Text("Page 3"))
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 2")
}
}
This issue would occur on the NavigationLink to Page 3. In the console output this error was showing when that link was used:
2021-02-13 16:41:00.599844+0000 App[59157:254215] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.
I discovered that I needed to apply .navigationViewStyle(StackNavigationViewStyle()) to the NavigationView and this solved the problem.
I.e.
struct Page1View: View {
var body: some View {
NavigationView {
List {
NavigationLink("Page 2", destination: Page2View())
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Page 1")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Been fighting this issue half day today and came to this post that helped me to understand that issue appears if Text, Button or something else placed between NavigationView and in my case List. And I found solution that worked for me. Just add .zIndex() for the item. .zIndex() must be higher than for List Tried with Xcode 12.5.
var body: some View {
NavigationView {
VStack {
Text("This text DOES make problems.")
.zIndex(1.0)
List {
NavigationLink(destination: Text("Doesn't work correct")) {
Text("Doesn't work correct")
}
}
}
}
}
I did a bit more tinkering, it turns out this was caused due by having the UIHostingController being nested in a UINavigationController and using that navigation controller. Changing the navigation stack to use a SwiftUI NavigationView instead resolved this issue.
Similar to what #pawello2222 says in the question comments, I think the underlying cause is something to do with SwiftUI not understanding the proper navigation hierarchy when the external UINavigationController is used.
This is just one instance where this is fixed though, I'm still experiencing the issue in various other contexts depending on how my view is structured.
I've submitted an issue report FB8705430 to Apple, so hopefully this is fixed sometime soon.
Before (broken):
struct ContentView: View {
var body: some View {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
let nav = UINavigationController(rootViewController: host)
present(nav, animated: true, completion: nil)
After (working):
struct ContentView: View {
var body: some View {
NavigationView {
Form {
NavigationLink(destination: Text("test")) {
Text("test")
}
}
}
}
}
// (UIKit presentation context)
let view = ContentView()
let host = UIHostingController(rootView: view)
present(host, animated: true, completion: nil)
This is definitely a bug in List, for now, my work-around is refreshing the List by changing the id, like this:
struct YourView: View {
#State private var selectedItem: String?
#State private var listViewId = UUID()
var body: some View {
List(items, id: \.id) {
NavigationLink(destination: Text($0.id),
tag: $0.id,
selection: $selectedItem) {
Text("Row \($0.id)")
}
}
.id(listViewId)
.onAppear {
if selectedItem != nil {
selectedItem = nil
listViewId = UUID()
}
}
}
}
I made a modifier based on this that you can use:
struct RefreshOnAppearModifier<Tag: Hashable>: ViewModifier {
#State private var viewId = UUID()
#Binding var selection: Tag?
func body(content: Content) -> some View {
content
.id(viewId)
.onAppear {
if selection != nil {
viewId = UUID()
selection = nil
}
}
}
}
extension View {
func refreshOnAppear<Tag: Hashable>(selection: Binding<Tag?>? = nil) -> some View {
modifier(RefreshOnAppearModifier(selection: selection ?? .constant(nil)))
}
}
use it like this:
List { ... }
.refreshOnAppear(selection: $selectedItem)
I managed to solve it by adding ids to the different components of the list, using binding and resetting the binding on .onDisappear
struct ContentView: View {
#State var selection: String? = nil
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
.padding()
List {
Section {
NavigationLink( destination: Text("Subscreen1"), tag: "link1", selection: $selection ) {
Text("Subscreen1")
}.onDisappear {
self.selection = nil
}
NavigationLink( destination: Text("Subscreen2"), tag: "link2", selection: $selection ) {
Text("Subscreen2")
}.onDisappear {
self.selection = nil
}
}.id("idSection1")
}
.id("idList")
}
}
}
}
I've also run into this issue and it seemed related to sheets as mentioned here.
My solution was to swizzle UITableView catch selections, and deselect the cell. The code for doing so is here. Hopefully this will be fixed in future iOS.
Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView fixed it for me.
Suggested in this thread: https://developer.apple.com/forums/thread/660468
This is my solution to this issue.
// This in a stack in front of list, disables large navigation title from collapsing by disallowing list from scrolling on top of navigation title
public struct PreventCollapseView: View {
#State public var viewColor: Color?
public init(color: Color? = nil) {
self.viewColor = color
}
public var body: some View {
Rectangle()
.fill(viewColor ?? Color(UIColor(white: 0.0, alpha: 0.0005)))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 1)
}
}
// handy modifier..
extension List {
public func uncollapsing(_ viewUpdater: Bool) -> some View {
VStack(spacing: 0.0) {
Group {
PreventCollapseView()
self
}.id(viewUpdater)
}
}
}
struct TestView: View {
#State var updater: Bool = false
var body: some View {
List {
Text("Item one")
Text("Item two")
Text("Manually refresh")
.onTapGesture { DispatchQueue.main.async { updater.toggle() } }
.onAppear { print("List was refreshed") }
}
.uncollapsing(updater)
.clipped()
.onAppear { DispatchQueue.main.async { updater.toggle() }} // Manually refreshes list always when re-appearing/appearing
}
}
Add a NavigationView, configure for largeTitle, and embed TestView and it's all set. Toggle updater to refresh.
Having the same Problem. The weird thing is, that the exact same code worked in iOS13.
I'm having this issue with a simple list:
struct TestList: View {
let someArray = ["one", "two", "three", "four", "five"]
var body: some View {
List(someArray, id: \.self) { item in
NavigationLink(
destination: Text(item)) {
Text(item)
}.buttonStyle(PlainButtonStyle())
}.navigationBarTitle("testlist")
}
}
This is embedded in:
struct ListControllerView: View {
#State private var listPicker = 0
var body: some View {
NavigationView{
Group{
VStack{
Picker(selection: $listPicker, label: Text("Detailoverview")) {
Text("foo").tag(0)
Text("bar").tag(1)
Text("TestList").tag(2)
}
This is inside a Tabbar.
This is the workaround I've been using until this List issue gets fixed. Using the Introspect library, I save the List's UITableView.reloadData method and call it when it appears again.
import SwiftUI
import Introspect
struct MyView: View {
#State var reload: (() -> Void)? = nil
var body: some View {
NavigationView {
List {
NavigationLink("Next", destination: Text("Hello"))
}.introspectTableView { tv in
self.reload = tv.reloadData
}.onAppear {
self.reload?()
}
}
}
}

Remove back button text from navigationbar in SwiftUI

I've recently started working in SwiftUI, came to the conclusion that working with navigation isn't really great yet. What I'm trying to achieve is the following. I finally managed to get rid of the translucent background without making the application crash, but now I ran into the next issue. How can I get rid of the "back" text inside the navbaritem?
I achieved the view above by setting the default appearance in the SceneDelegate.swift file like this.
let newNavAppearance = UINavigationBarAppearance()
newNavAppearance.configureWithTransparentBackground()
newNavAppearance.setBackIndicatorImage(UIImage(named: "backButton"), transitionMaskImage: UIImage(named: "backButton"))
newNavAppearance.titleTextAttributes = [
.font: UIFont(name: GTWalsheim.bold.name, size: 18)!,
.backgroundColor: UIColor.white
]
UINavigationBar.appearance().standardAppearance = newNavAppearance
One possible way that I could achieve this is by overriding the navigation bar items, however this has one downside (SwiftUI Custom Back Button Text for NavigationView) as the creator of this issue already said, the back gesture stops working after you override the navigation bar items. With that I'm also wondering how I could set the foregroundColor of the back button. It now has the default blue color, however I'd like to overwrite this with another color.
Piggy-backing on the solution #Pitchbloas offered, this method just involves setting the backButtonDisplayMode property to .minimal:
extension UINavigationController {
open override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}
It's actually really easy. The following solution is the fastest and cleanest i made.
Put this at the bottom of your SceneDelegate for example.
extension UINavigationController {
// Remove back button text
open override func viewWillLayoutSubviews() {
navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
This will remove the back button text from every NavigationView (UINavigationController) in your app.
I have found a straightforward approach to remove the back button text using SwiftUI only, and keeping the original chevron.
A drag gesture is added to mimic the classic navigation back button
when user wants to go back by swiping right. Following this, an extension of View is created to create a SwiftUI like modifier.
This is how to use it in code:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
// Your main view code here with a ZStack to have the
// gesture on all the view.
}
.navigationBarBackButtonTitleHidden()
}
}
This is how to create the navigationBarBackButtonTitleHidden() modifier:
import SwiftUI
extension View {
func navigationBarBackButtonTitleHidden() -> some View {
self.modifier(NavigationBarBackButtonTitleHiddenModifier())
}
}
struct NavigationBarBackButtonTitleHiddenModifier: ViewModifier {
#Environment(\.dismiss) var dismiss
#ViewBuilder #MainActor func body(content: Content) -> some View {
content
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading: Button(action: { dismiss() }) {
Image(systemName: "chevron.left")
.foregroundColor(.blue)
.imageScale(.large) })
.contentShape(Rectangle()) // Start of the gesture to dismiss the navigation
.gesture(
DragGesture(coordinateSpace: .local)
.onEnded { value in
if value.translation.width > .zero
&& value.translation.height > -30
&& value.translation.height < 30 {
dismiss()
}
}
)
}
}
Standard Back button title is taken from navigation bar title of previous screen.
It is possible the following approach to get needed effect:
struct TestBackButtonTitle: View {
#State private var hasTitle = true
var body: some View {
NavigationView {
NavigationLink("Go", destination:
Text("Details")
.onAppear {
self.hasTitle = false
}
.onDisappear {
self.hasTitle = true
}
)
.navigationBarTitle(self.hasTitle ? "Master" : "")
}
}
}
So I actually ended up with the following solution that actually works. I am overwriting the navigation bar items like so
.navigationBarItems(leading:
Image("backButton")
.foregroundColor(.blue)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
)
The only issue with this was that the back gesture wasn't working so that was solved by actually extending the UINavigationController
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
Now it's looking exactly the way I want it, the solution is kinda hacky... but it works for now, hopefully SwiftUI will mature a little bit so this can be done easier.
Using the Introspect framework, you can easily gain access to the underlying navigation item and set the backButtonDisplayMode to minimal.
Here’s how you might use that in the view that was pushed
var body: some View {
Text("Your body here")
.introspectNavigationController { navController in
navController.navigationBar.topItem?.backButtonDisplayMode = .minimal
}
}
If you want to:
Do it globally
Keep the standard back button (along with custom behaviours like an ability to navigate a few screens back on a long press)
Avoid introducing any third party frameworks
You can do it by setting the back button text color to Clear Color via appearance:
let navigationBarAppearance = UINavigationBarAppearance()
let backButtonAppearance = UIBarButtonItemAppearance(style: .plain)
backButtonAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.highlighted.titleTextAttributes = [.foregroundColor: UIColor.clear]
backButtonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear]
navigationBarAppearance.backButtonAppearance = backButtonAppearance
//Not sure you'll need both of these, but feel free to adjust to your needs.
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
You can do it once when the app starts and forget about it.
A potential downside (depending on your preferences) is that the transition to the clear color is animated as the title of the current window slides to the left as you move to a different one.
You can also experiment with different text attributes.
Works on iOS 16
Solutions above didn't work for me. I wanted to make changes specific to view without any global (appearance or extension) and with minimal boilerplate code.
Since you can update NavigationItem inside the init of the View. You can solve this in 2 steps:
Get visible View Controller.
// Get Visible ViewController
extension UIApplication {
static var visibleVC: UIViewController? {
var currentVC = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController
while let presentedVC = currentVC?.presentedViewController {
if let navVC = (presentedVC as? UINavigationController)?.viewControllers.last {
currentVC = navVC
} else if let tabVC = (presentedVC as? UITabBarController)?.selectedViewController {
currentVC = tabVC
} else {
currentVC = presentedVC
}
}
return currentVC
}
}
Update NavigationItem inside init of the View.
struct YourView: View {
init(hideBackLabel: Bool = true) {
if hideBackLabel {
// iOS 14+
UIApplication.visibleVC?.navigationItem.backButtonDisplayMode = .minimal
// iOS 13-
let button = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
UIApplication.visibleVC?.navigationItem.backBarButtonItem = button
}
}
}
custom navigationBarItems and self.presentationMode.wrappedValue.dismiss() worked but you are not allow to perform swiping back
You can either add the following code to make the swipe back again
//perform gesture go back
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
but the problem is, sometimes it will make your app crashed when you swipe half the screen and then cancel.
I would suggest the other way to remove the "Back" text.
Adding the isActive state to monitor whether the current screen is active or not. :)
struct ContentView: View {
#State var isActive = false
var body: some View {
NavigationView() {
NavigationLink(
"Next",
destination: Text("Second Page").navigationBarTitle("Second"),
isActive: $isActive
)
.navigationBarTitle(!isActive ? "Title" : "", displayMode: .inline)
}
}
}
I am accomplishing this by changing the title of the master screen before pushing the detail screen and then setting it back when it re-appears. The only caveat is when you go back to the master screen the title's re-appearance is a little noticeable.
Summary:
on master view add state var (e.g. isDetailShowing) to store if detail screen is showing or not
on master view use the navigationTitle modifier to set the title based on the current value of isDetailShowing
on master view use onAppear modifier to set the value of isDetailShowing to false
on the NavigationLink in master screen use the simultaneousGesture modifier to set the isDetailShowing to true
struct MasterView: View {
#State var isDetailShowing = false
var body: some View {
VStack {
Spacer()
.frame(height: 20)
Text("Master Screen")
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
.frame(height: 20)
NavigationLink(destination: DetailView()) {
Text("Go to detail screen")
}
.simultaneousGesture(TapGesture().onEnded() {
isDetailShowing = true
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(isDetailShowing ? "" : "Master Screen Title")
.onAppear() {
isDetailShowing = false
}
}
}
struct DetailView: View {
var body: some View {
Text("This is the detail screen")
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Detail Screen Title")
}
}
you can use .toolbarRole(.editor)
Why not use Custom BackButton with Default Back Button Hidden
You could use Any Design You Prefer !
Works on iOS 16
First View
struct ContentView: View {
var body: some View {
NavigationView {
VStack(){
Spacer()
NavigationLink(destination: View2()) {
Text("Navigate")
.font(.title)
}
Spacer()
}
}
}
}
Second View
struct View2: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ZStack{
VStack{
HStack(alignment:.center){
//Any Design You Like
Image(systemName: "chevron.left")
.font(.title)
.foregroundColor(.blue)
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
.padding()
Spacer()
}
Spacer()
}
}
}
.navigationBarBackButtonHidden(true)
}
}

How to change the background color for a Form in SwiftUI?

I wanna change that "light gray" background color for a form, but .foregroundColor(Color.blue) and .background(Color.blue) does not seem to work
struct ContentView : View {
#State var value = ""
var body: some View {
Form {
Section(header: Text("First Name")) {
TextField($value)
}
Section(header: Text("Last Name")) {
TextField($value)
}
}.foregroundColor(Color.blue)
}
}
iOS 16
You can hide the default background to show the underling view by appealing the following modifier to the Form:
.scrollContentBackground(.hidden)
iOS 13 and 15
All SwiftUI's Lists are backed by a UITableViewin iOS. so you need to change the background color of the tableView. But since Color and UIColor values are slightly different, you can get rid of the UIColor.
struct ContentView: View {
init(){
UITableView.appearance().backgroundColor = .clear
}
#State var value = ""
var body: some View {
Form {
Section(header: Text("First Name")) {
TextField("First Name", text: $value)
}
Section(header: Text("Last Name")) {
TextField("Last Name", text: $value)
}
}
/*.scrollContentBackground(.hidden)*/ // πŸ‘ˆ this line will work only on iOS 16 and above
.foregroundColor(Color.blue)
.background(Color.yellow)
}
}
Now you can use Any background (including all Colors) you want
Note that those top and bottom white areas are safe are and you can use .edgesIgnoringSafeArea() modifier to get rid of them.
Restore
Since UITableView.appearance().backgroundColor applies globally, you can use .onAppear modifier to change it in different views (since it is a global change). So you can use another onAppear or onDisappear to reset it back to what you want.
And the default colors are:
UIColor.systemGroupedBackground for the grouped style. And
UIColor.systemBackground for the plain style.
And they both have automatic support for both dark mode and light mode.
try this
.onAppear {
UITableView.appearance().backgroundColor = .blue
}
The accepted answer by Mojtaba Hosseini, above, works but the init() statement is not a good place for the UITableView statement. This is because it "hard codes" the ContentView's init parameters. In this case it has none so everything works but if an #ObservedObject was added to the view then this would break the init function.
Much simpler just to add the UITable statement to the body, explicitly return the Form and delete the init().
var body: some View {
UITableView.appearance().backgroundColor = .clear
return Form {...}
}
However, setting the background colour on the Form usually works as expected. The fact that it is not working on the ContentView screen may be a bug.
Don't change the global appearance().
You can use UITableView.appearance().backgroundColor = .red for example to set the Form's background color to red. This can go in the init of the view, however this affects every List and Form.
Alternatively, you could use SwiftUI-Introspect to customise a single one by doing something like:
struct ContentView: View {
#State private var value = ""
var body: some View {
Form {
Section(header: Text("First Name")) {
TextField("First", text: $value)
}
Section(header: Text("Last Name")) {
TextField("Last", text: $value)
}
}
.introspectTableView { $0.backgroundColor = .systemBlue }
.foregroundColor(Color.blue)
}
}
You can also add the following to each section to make the sections blue too:
.listRowBackground(Color.blue)
If you don't want to modify safe area of the Form, you can use ZStackas well:
struct ContentView: View {
init(){
UITableView.appearance().backgroundColor = .clear
}
#State var value = ""
var body: some View {
ZStack {
Color(UIColor.systemYellow)
.edgesIgnoringSafeArea(.all)
Form {
Section(header: Text("First Name")) {
TextField("First Name", text: $value)
}
Section(header: Text("Last Name")) {
TextField("Last Name", text: $value)
}
}
}
}
}
Copy these codes below each of your Form view:
Form {
// Your form view
}
.onAppear { // ADD THESE AFTER YOUR FORM VIEW
UITableView.appearance().backgroundColor = .clear
}
.onDisappear { // CHANGE BACK TO SYSTEM's DEFAULT
UITableView.appearance().backgroundColor = .systemGroupedBackground
}
.background(.yellow) // Add your background color
The solutions above didn't really work for what I was trying to achieve. I wanted an initial screen with a clear background for the form, and subsequent screens to have the default iOS systemGroupedBackground color. Using appear() and disappear() didn't work for me as switching between various tabs was leading to bugs in the appearance.
I came up with the following solution. It borrows from the solutions above.
For my ContentView screen, I inserted this code just inside the Struct.
init(){
UITableView.appearance().backgroundColor = .clear
}
This is a global change that affects all forms.
For all forms where I wanted the default color to work, I inserted this code just outside the Form {}.
.background(Color(.systemGroupedBackground))
For all versions
After seeing these solutions I created a modifier for you use when in iOS 16 and below.
struct FormHiddenBackground: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content.scrollContentBackground(.hidden)
} else {
content.onAppear {
UITableView.appearance().backgroundColor = .clear
}
.onDisappear {
UITableView.appearance().backgroundColor = .systemGroupedBackground
}
}
}
}
To use it you just need to update after a form.
Form {
Text("Test text")
}
.frame(height: 400)
.modifier(FormHiddenBackground())

Resources