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.
Related
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 having an .onDrag on an Image. Whether I provide a preview myself or not, the preview always gets stuck to show the Image that appeared on the first drag. The NSItemProvider itself works well and I access the proper data, but not within the preview.
A simple example (code available here: https://github.com/godbout/DragPreview)
import SwiftUI
struct AppDetail {
let bundleIdentifier: String
let icon: NSImage
init(bundleIdentifier: String) {
self.bundleIdentifier = bundleIdentifier
if
let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier),
let representation = NSWorkspace.shared.icon(forFile: url.path).bestRepresentation(for: NSRect(x: 0, y: 0, width: 64, height: 64), context: nil, hints: nil)
{
self.icon = NSImage(size: representation.size)
self.icon.addRepresentation(representation)
} else {
self.icon = NSApp.applicationIconImage!
}
}
}
struct ContentView: View {
#State private var apps: [AppDetail] = [
AppDetail(bundleIdentifier: "com.apple.Mail"),
AppDetail(bundleIdentifier: "com.apple.MobileSMS"),
AppDetail(bundleIdentifier: "com.apple.Safari"),
]
var body: some View {
VStack(alignment: .center) {
Image(nsImage: apps.first == nil ? NSApp.applicationIconImage! : apps.first!.icon)
.onDrag {
guard
let app = apps.first,
let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: app.bundleIdentifier)
else {
return NSItemProvider()
}
return NSItemProvider(object: url as NSURL)
} preview: {
Text(apps.first == nil ? "no app" : apps.first!.bundleIdentifier)
Image(nsImage: apps.first == nil ? NSApp.applicationIconImage! : apps.first!.icon)
}
Button("next") {
apps.removeFirst()
}
.disabled(apps.isEmpty)
}
.frame(width: 200, height: 200)
.padding()
}
}
Anything I'm doing wrong? Could this be a(nother) SwiftUI bug? If the preview is actually static and only rendered at the first drag, how could I change the code so that I get the preview of my current Image?
Thanks.
I read somewhere in a comment by #Asperi that the drag preview is only created once at init. So if you add an .id to the image it forces a redraw and reinit of preview:
Image(nsImage: apps.first == nil ? NSApp.applicationIconImage! : apps.first!.icon)
.id(apps.first?.bundleIdentifier) // here
.onDrag { ...
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.
I have a TabView thats using the swiftUI 2.0 PageTabViewStyle. Is there any way to disable the swipe to change pages?
I have a search bar in my first tab view, but if a user is typing, I don't want to give the ability to change they are on, I basically want it to be locked on to that screen until said function is done.
Here's a gif showing the difference, I'm looking to disable tab changing when it's full screen in the gif.
https://imgur.com/GrqcGCI
Try something like the following (tested with some stub code). The idea is to block tab view drag gesture when some condition (in you case start editing) happens
#State var isSearching = false
// ... other code
TabView {
// ... your code here
Your_View()
.gesture(isSearching ? DragGesture() : nil) // blocks TabView gesture !!
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
I tried Asperis's solution, but I still couldn't disable the swiping, and adding disabled to true didn't work since I want the child views to be interactive. The solution that worked for me was using Majid's (https://swiftwithmajid.com/2019/12/25/building-pager-view-in-swiftui/) custom Pager View and adding a conditional like Asperi's solution.
Majid's PagerView with conditional:
import SwiftUI
struct PagerView<Content: View>: View {
let pageCount: Int
#Binding var canDrag: Bool
#Binding var currentIndex: Int
let content: Content
init(pageCount: Int, canDrag: Binding<Bool>, currentIndex: Binding<Int>, #ViewBuilder content: () -> Content) {
self.pageCount = pageCount
self._canDrag = canDrag
self._currentIndex = currentIndex
self.content = content()
}
#GestureState private var translation: CGFloat = 0
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
self.content.frame(width: geometry.size.width)
}
.frame(width: geometry.size.width, alignment: .leading)
.offset(x: -CGFloat(self.currentIndex) * geometry.size.width)
.offset(x: self.translation)
.animation(.interactiveSpring(), value: currentIndex)
.animation(.interactiveSpring(), value: translation)
.gesture(!canDrag ? nil : // <- here
DragGesture()
.updating(self.$translation) { value, state, _ in
state = value.translation.width
}
.onEnded { value in
let offset = value.translation.width / geometry.size.width
let newIndex = (CGFloat(self.currentIndex) - offset).rounded()
self.currentIndex = min(max(Int(newIndex), 0), self.pageCount - 1)
}
)
}
}
}
ContentView:
import SwiftUI
struct ContentView: View {
#State private var currentPage = 0
#State var canDrag: Bool = true
var body: some View {
PagerView(pageCount: 3, canDrag: $canDrag, currentIndex: $currentPage) {
VStack {
Color.blue
Button {
canDrag.toggle()
} label: {
Text("Toogle drag")
}
}
VStack {
Color.red
Button {
canDrag.toggle()
} label: {
Text("Toogle drag")
}
}
VStack {
Color.green
Button {
canDrag.toggle()
} label: {
Text("Toogle drag")
}
}
}
}
}
Ok I think it is possible to block at least 99% swipe gesture if not 100% by using this steps:
and 2.
Add .gesture(DragGesture()) to each page
Add .tabViewStyle(.page(indexDisplayMode: .never))
SwiftUI.TabView(selection: $viewModel.selection) {
ForEach(pages.indices, id: \.self) { index in
pages[index]
.tag(index)
.gesture(DragGesture())
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Add .highPriorityGesture(DragGesture()) to all remaining views images, buttons that still enable to drag and swipe pages
You can also in 1. use highPriorityGesture but it completely blocks drags on each pages, but I need them in some pages to rotate something
For anyone trying to figure this out, I managed to do this by setting the TabView state to disabled.
TabView(selection: $currentIndex.animation()) {
Items()
}.disabled(true)
Edit: as mentioned in the comments this will disable everything within the TabView as well