I’m trying to build a custom sidebar menu that animates out when a button in it is tapped. For debugging purposes the animation is deliberately slow at 2.0 seconds. As you can see the animation does not work properly:
I suspect there are two parts to this problem:
The background of the newly selected button is moving out faster than the menu. I think this is rooted in the default system animation of Button.
When I replace Button with Text and use an .onTapGesture, there is still the fading animation, so I assume there is something structurally wrong in the way I’m setting selected in FeatureButton.
Sorry the example code is a bit long, tried to simplify my app architecture as much as possible. The reason for using MenuState as an EnvironmentObject is to be able to the change its properties from various places throughout the app.
Here’s the code:
class MenuState: ObservableObject {
#Published var currentFeature: Feature = .featureA
#Published var menuOffset: CGFloat = 0
}
enum Feature: String, CaseIterable {
case featureA = "Feature A"
case featureB = "Feature B"
case featureC = "Feature C"
}
extension Feature: Identifiable {
var id: RawValue { rawValue }
}
struct ContentView: View {
#StateObject var menuState = MenuState()
var body: some View {
ZStack(alignment: .leading) {
content
menu
}
.environmentObject(menuState)
.animation(.easeOut(duration: 2.0), value: menuState.menuOffset)
}
var menu: some View {
VStack {
ForEach(Feature.allCases) { feature in
FeatureButton(feature: feature)
}
}
.frame(maxHeight: .infinity)
.frame(width: 200)
.background(.thinMaterial)
.offset(x: menuState.menuOffset)
}
var content: some View {
VStack {
Button("Show Menu") {
menuState.menuOffset = 0
}
Text(menuState.currentFeature.rawValue)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct FeatureButton: View {
#EnvironmentObject var menuState: MenuState
let feature: Feature
var selected: Bool {
return menuState.currentFeature == feature
}
var body: some View {
Button(feature.rawValue) {
menuState.currentFeature = feature
menuState.menuOffset = -200
}
.buttonStyle(FeatureButtonStyle(selected: selected))
}
}
struct FeatureButtonStyle: ButtonStyle {
#EnvironmentObject var menuState: MenuState
var selected: Bool
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity, minHeight: 44)
.foregroundColor(selected ? .blue : .primary)
.background(Color.gray.opacity(selected ? 0.4 : 0))
.contentShape(Rectangle())
}
}
EDIT:
For some reason making the animation explicit solves the issue, see answer below.
The problem can be solved by making the animation explicit instead of using the .animation modifier:
Button(feature.rawValue) {
menuState.currentFeature = feature
withAnimation(.easeOut) {
menuState.menuOffset = -200
}
}
.buttonStyle(FeatureButtonStyle(selected: selected))
I don't understand why it only works like this though.
I have created a ViewModifier that adds a icon to the right of a its content, the way I want the icon to appear is by animating the .clipShape() modifier from -50 to 0, the problem is that when appearing the first time, it just pops out with no animation and the same thing happens when disappearing for the last time. At the bottom you'll find a video demonstration
My ViewModifier so far
extension View {
func addRightIcon(icon: Image, show: Bool) -> some View {
return modifier(RightIconModifier(icon: icon, show: show))
}
}
struct RightIconModifier: ViewModifier {
var icon: Image
private var iconMask: Int = 0
init(icon: Image, show: Bool) {
self.icon = icon
withAnimation(Animation.interpolatingSpring(stiffness: 170, damping: 15).delay(2.5)) {
iconMask = show ? 0 : -50
}
}
func body(content: Content) -> some View {
ZStack {
content
.overlay(rightIcon)
}
}
var rightIcon: some View {
icon
.font(.system(size: 25))
.foregroundColor(.black)
.frame(maxWidth: .infinity,
maxHeight: .infinity,
alignment: .trailing)
.padding()
.clipShape(Rectangle().offset(x: CGFloat(iconMask)))
}
}
This would be a short version of how I'm using it, hopefully you get an idea to make it work
TextField(placeholder, text: $text).addRightIcon(icon: Image(systemName: "checkmark"), show: isTextValid)
var isTextValid: Bool {
if !text.isEmpty {
let validation = NSPredicate(format: "SELF MATCHES %#", "[’a-zA-Z]{3,20}")
let validated = validation.evaluate(with: text)
return validated
}
return false
}
This is a video demonstration
Animatable modifiers should be inside body (directly or called from within body), but not in init. Modifier is also a struct, so if its properties modified externally they are also animatable.
So here is fixed ViewModifier. Tested with Xcode 14 / iOS 16
Note: I simplified animation and filter for testing purpose
struct RightIconModifier: ViewModifier {
var icon: Image
var show: Bool // << injected changes
func body(content: Content) -> some View {
ZStack {
content
.overlay(rightIcon)
}
}
var rightIcon: some View {
icon
.font(.system(size: 25))
.foregroundColor(.black)
.frame(maxWidth: .infinity,
maxHeight: .infinity,
alignment: .trailing)
.padding()
.clipShape(Rectangle().offset(x: CGFloat(show ? 0 : -50))) // << switch is here !!
.animation(.easeIn(duration: 1), // << simplified for testing
value: show)
}
}
Test module on GitHub
I'm assuming I should probably file this as a feedback report with Apple, but posting here in case I am missing something - or if there is new guidance with latest SwiftUI.
This code works as expected in Xcode 13, but in Xcode 14 beta 2, the navigation bar and "Cancel" button are missing. Is this ProgressView with deferred content loading somehow a technique that doesn't work anymore?
import SwiftUI
struct ContentView: View {
#State private var isFlowDetermined = false
var body: some View {
NavigationView {
//NestedView()
if self.isFlowDetermined {
NestedView()
} else {
ProgressView()
.task {
await self.determineFlow()
}
}
}
}
private func determineFlow() async {
self.isFlowDetermined = true
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct NestedView: View {
var body: some View {
ScrollView {
Text("Where is the \"Cancel\" button?")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
#if !os(macOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
#if !os(macOS)
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
print("got here")
}
}
#endif
}
}
}
UPDATE: Xcode 14 beta 4 appears to resolve this issue. The suggested workaround below is no longer needed.
It seems they optimised toolbar construction (and don't assume it is changed). Anyway I see only one workaround for now:
NavigationView {
// .. content
}
.id(isFlowDetermined) // << here !!
Tested with Xcode 14b2 / iOS 16
*Note: NavigationView is deprecated since iOS 16
Im trying to make Augmented Reality app with RealityKit but ContentView.swift I have some problems
What is missing here ?` You can see errors on picture which I shared. I followed some tutorial so Im new on Xcode and Realitykit.
Failed to produce diagnostic for expression; please file a bug report
Cannot find 'PlacementButtonsView' in scope
import SwiftUI
import RealityKit
struct ContentView : View {
var models: [String] = {
let filemanager = FileManager.default
guard let path = Bundle.main.resourcePath, let files = try?
filemanager.contentsOfDirectory(atPath:path) else
{ return[]
}
var avaliableModels: [String] = []
for filename in files where filename.hasSuffix("usdz") {
let modelName = filename.replacingOccurrences(of: ".usdz", with: "")
avaliableModels.append(modelName)
}
return avaliableModels
}()
var body: some View {
ZStack(alignment: .bottom) {
ARViewContainer()
ModelPickerView(models: self.models)
PlacementButtonsView()
}
}
}
struct ARViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
struct ModelPickerView: View {
var models: [String]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(0 ..<
self.models.count) { index in
Button(action: {
print("DEBUG: selected model with name: \(self.models[index])")
}) {
Image(uiImage: UIImage(named: self.models[index])!)
.resizable()
.frame(height: 60)
.aspectRatio(1/1,contentMode: .fit)
.background(Color.white)
.cornerRadius(12)
}
.buttonStyle (PlainButtonStyle())
}
}
}
.padding(15)
.background(Color.black.opacity(0.5))
}
struct PlacementButtonsView: View {
var body: some View {
HStack {
//Cancel Button
Button(action: {
print("DEBUG: model placement canceled.")
}) {
Image(systemName: "xmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
//Confirm Button
Button(action: {
print("DEBUG: model placement confirmed.")
}) {
Image(systemName: "checkmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.65))
.cornerRadius(30)
.padding(20)
}
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
}
Check your braces. The PlacementButtonsView struct is actually nested within the ModelPickerView struct, and not globally available, hence why it is not available from your ContentView.
By the way, in Xcode, you can find this out by option clicking on the declaration of PlacementButtonsView:
ModelPickerView.PlacementButtonsView shows you what went wrong here; PlacementButtonsView is nested within ModelPickerView. This is why you seem to have a strange closing brace on the final line of your code sample - the same issue occurs with the preview, as it is also nested in ModelPickerView.
To make this issue more visible, and see similar issues like this in the future more easily, you can also have Xcode indent your code for you by selecting all (Cmd + A) and then pressing Control + I. You'll see the PlacementButtonsView struct indent, making it more clear that it is not globally available.
So the question is pretty simple and it's in the title. I want to remove the line separator in SwiftUI iOS 14. Previously, I was using
UITableView().appearance().separatorStyle = .none
and that used to do the job in iOS 13. Now however, it doesn't work. Any update or idea on how to make it work. Thanks:)
Here is a demo of possible solution. Tested with Xcode 12b.
List {
ForEach(0..<3) { _ in
VStack {
Text("Hello, World!").padding(.leading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.listRowInsets(EdgeInsets())
.background(Color(UIColor.systemBackground)))
}
}
Merged #asperi, #akmin and #zrfrank answer into one thing. In my experience List is more reliable and efficient than LazyVStack, so I use still use List for anything complex requiring reliability.
extension View {
func listRow() -> some View {
self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1))
.background(Color(.systemBackground))
}
}
List {
Color.red
.listRow()
Color.green
.listRow()
}
How I made a list that works on both iOS 14 and iOS 13, It shows no separators and extra margins
struct NoButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
}
}
struct ListWithoutSepatorsAndMargins<Content: View>: View {
let content: () -> Content
var body: some View {
if #available(iOS 14.0, *) {
ScrollView {
LazyVStack(spacing: 0) {
self.content()
}
.buttonStyle(NoButtonStyle())
}
} else {
List {
self.content()
}
.listStyle(PlainListStyle())
.buttonStyle(NoButtonStyle())
}
}
}
Sample Usage -
ListWithoutSepatorsAndMargins {
ForEach(0..<5) { _ in
Text("Content")
}
}
in case you've more components in list, wrap them in Group
ListWithoutSepatorsAndMargins {
Group {
self.groupSearchResults()
self.myGroups()
self.exploreGroups()
}
}
}
Hope this helps someone, I wasted a lot of time in such minor thing, Apple is trying to push us hard to use LazyVStack, it seems
iOS 15:
This year Apple introduced a new modifier .listRowSeparator that can be used to style the separators. you can pass .hidden to hide it:
List(items, id:\.self) {
Text("Row \($0)")
.listRowSeparator(.hidden)
}
🌈 Also you can set each separator to any color by settings listRowSeparatorTintColor as I mentioned here in this answer:
iOS 14
Follow the answer here
I found this simple solution on the Apple Developer forums. It's working for me on 14.4:
List {
...
}.listStyle(SidebarListStyle())
This seems to add a tiny bit of padding around the edges. If that's a problem for you, you could try some negative padding.
Based on average Joe's answer I ended up with the following modifier:
struct ListSeparatorNone: ViewModifier {
var backgroundColor: Color = Color(.systemBackground)
func body(content: Content) -> some View {
content
.listRowInsets(EdgeInsets(top: -1, leading: 0, bottom: 0, trailing: 0))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.background(backgroundColor)
}
}
The view extension:
extension View {
func listSeparatorNone(backgroundColor: Color = Color(.systemBackground)) -> some View {
self.modifier(ListSeparatorNone(backgroundColor: backgroundColor))
}
}
Usage example:
List {
ForEach(viewModel.countries, id: \.self) { country in
Text(country)
.padding(.leading, 10)
}
.listSeparatorNone()
}
If you don't have a lot of cells, and therefore don't need to rely on a LazyVStack for performance, you can fallback to a ScrollView + VStack:
ScrollView {
VStack {
Row1()
Row2()
Row3()
}
}
You can also call this function at the end of your VStack (that is inner the List).
It will be an overlay on List Seperator on iOS 14 :)
private func hideDefaultListSeperator() -> some View {
Rectangle()
.fill(colorScheme == .light ? Color.white : Color.black)
.frame(maxHeight: 1)
}
Update:
I figured out a solution that works on both iOS 13 and iOS 14 and gives a simple list and uses List on both iOS.
struct ListWithoutSepatorsAndMargins<Content>: View where Content: View {
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
List {
self.content()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.listRowInsets(EdgeInsets())
.background(Color.white)
}
.listStyle(PlainListStyle())
.buttonStyle(NoButtonStyle())
}
}
struct NoButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
}
and do the following in SceneDelegate.swift to remove default grey selection of cells
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
UITableView.appearance().separatorStyle = .none
UITableView.appearance().allowsSelection = false
.......
and we can use it the following way
ListWithoutSepatorsAndMargins {
ForEach(0..<5) { _ in
Text("Content")
}
}
ListWithoutSepatorsAndMargins {
Group {
self.groupSearchResults()
self.myGroups()
self.exploreGroups()
}
}
}
Here is my solution for iOS 14:
struct MyRowView: View {
var body: some View {
ZStack(alignment: .leading) {
// Background color of the Row. It will spread under the entire row.
Color(.systemBackground)
NavigationLink(destination: Text("Details")) {
EmptyView()
}
.opacity(0) // Hide the Disclosure Indicator
Text("Go to Details").padding(.leading)
}
// These 2 lines hide the row separators
.padding(.horizontal, -16) // Removes default horizontal padding
.padding(.vertical, -6) // Removes default vertical padding
}
}
The enclosing List should have this modifier
.listStyle(PlainListStyle())
The upside of this solution over using a LazyVStack is that you can still use the Edit capabilities of the List.
This solution relies unfortunately on hard-coded values to remove the system default paddings on each row. Hopefully SwiftUI 3.0 will provide simple .separatorStyle(.none) and .accessoryType(.none) modifiers.
The code to remove the Disclosure Indicators comes from: https://www.appcoda.com/hide-disclosure-indicator-swiftui-list/
Thank for #asperi, #akmin and #zrfrank and #averageJoe 's answers.
Here is another improved method works in iOS 14 and 15.
extension View {
func hideListRowSeperator() -> some View {
if #available(iOS 15, *) {
return AnyView(self.listRowSeparator(.hidden))
} else {
return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1))
.background(Color(.systemBackground)))
}
}
}
Use example
var body: some View {
List {
ForEach(0..<3) { _ in
Text("Hello, World!")
.padding(.leading)
.hideListRowSeperator()
}
}
.listStyle(.plain)
}
The above answer work for me, you have to set only below both function:
.listRowInsets(EdgeInsets())
.background(Color.white)