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
The task is simple, but I don't know where to start looking for any pointers on MacOs App development with two displays. I'm building a presenter app where the main app will be displaying on the primary display and with the push of a button I need to push a fullscreen window onto the second display or second monitor. How would I go about doing this? I am using SwiftUI, but am open to other suggestions.
After a bit of tinkering around I came across this solution which I modified to make it work for me. Add the following extension.
extension View {
private func newWindowInternal(with title: String) -> NSWindow {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 0, height: 0),
styleMask: [.closable, .borderless],
backing: .buffered,
defer: false)
guard let secondScreen = NSScreen.screens.last else {
print("Failed to find last display")
return window
}
window.setFrame(secondScreen.frame, display: true)
window.level = NSWindow.Level.screenSaver
window.isReleasedWhenClosed = false
window.title = title
window.orderFront(nil)
return window
}
func openNewWindow(with title: String = "new Window") {
self.newWindowInternal(with: title).contentView = NSHostingView(rootView: self)
}
}
In your ContentView, add a button which will trigger the activation of the new window. Here's an example ContentView
struct ContentView: View {
#State private var windowOpened = false
var body: some View {
VStack {
Button("Open window") {
if !windowOpened {
ProjectorView(isOpen: $windowOpened).openNewWindow(with: "Projector")
}
}
.keyboardShortcut("o", modifiers: [.option, .command])
Button("Close window") {
NSApplication.shared.windows.first(where: { $0.title == "Projector" })?.close()
}
.keyboardShortcut("w", modifiers: [.option, .command])
}
.frame(width: 300, height: 100)
}
}
Finally, here's what ProjectorView looks like.
struct ProjectorView: View {
#Binding var isOpen: Bool
var body: some View {
HStack {
Spacer()
VStack {
Spacer()
Text("Hello World!")
.font(.title2)
Spacer()
}
Spacer()
}
.padding()
.onDisappear {
isOpen = false
}
.onAppear {
isOpen = true
}
}
}
This solution works great. Pressing ⌥⌘O will open ProjectorView on the second screen above all other windows and pressing ⌥⌘W will close ProjectorView window. Window settings, and window level can be tweaked in the extension.
Tested on macOS Monterey, Xcode 13, Apple M1 MacBook Air.
My Custom ButtonStyle look like this.
struct DefaultButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.frame(minWidth: 0, maxWidth: .infinity)
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 4)
.fill(configuration.isPressed ? Color.black : Color.green)
)
}
}
Sample implementation of using custom button style.
Button(action: { self.viewModel.login() }) {
Text("Sign In")
.font(.headline)
}
.buttonStyle(DefaultButtonStyle())
.frame(minWidth: 0, maxWidth: 380)
.padding([.leading, .trailing], 27.5)
My preview code.
#if DEBUG
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView (viewModel: LoginViewModel()).environment(\.verticalSizeClass, .regular)
}
}
#endif
When I put the custom style inside the same file as the view. The preview is ok.
But when I moved the custom style in it's own file (in the purpose of reusing it in the whole app). The preview is throwing error.
Compiling failed: type 'Any' has no member 'leading'
Do I have to add something in the LoginView_Previews to make it load in preview? What am I doing wrong?
I suppose this is due to conflict with built-in DefaultButtonStyle, so name your somehow differently, like
struct MyDefaultButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
// .. other code
...
Button(action: { self.viewModel.login() }) {
Text("Sign In")
.font(.headline)
}
.buttonStyle(MyDefaultButtonStyle())
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)
I'm trying to add Touch Bar support for a SwiftUI View. There seems to be SwiftUI API for this using the .touchBar(content: () -> View) function on Views, but documentation is non existent and I can't get my Touch Bar to display anything.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.touchBar {
Button(action: {
}) {
Text("do something")
}
}
}
}
This code does compile and run, but the Touch Bar remains empty. How can I get my touch bar to display content using SwiftUI (not catalyst)?
Using .focusable doesn't work without "Use keyboard navigation to move focus between controls" checked in System Preferences -> Keyboard -> Shortcuts. To work around that, I did this:
/// Bit of a hack to enable touch bar support.
class FocusNSView: NSView {
override var acceptsFirstResponder: Bool {
return true
}
}
/// Gets the keyboard focus if nothing else is focused.
struct FocusView: NSViewRepresentable {
func makeNSView(context: NSViewRepresentableContext<FocusView>) -> FocusNSView {
return FocusNSView()
}
func updateNSView(_ nsView: FocusNSView, context: Context) {
// Delay making the view the first responder to avoid SwiftUI errors.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
if let window = nsView.window {
// Only set the focus if nothing else is focused.
if let _ = window.firstResponder as? NSWindow {
window.makeFirstResponder(nsView)
}
}
}
}
}
Help from this How to use a SwiftUI touchbar with a NSWindow - Apple Developer Forums:
Use the focusable() modifier
The touch bar shows the text when you add the .focusable() modifier just before the .touchBar(content:) modifier.
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.focusable()
.touchBar {
Button(action: {
print("Perform some action")
}) {
Text("do something")
}
}
}
}