macOS SwiftUI popover with TextField closes on first edit - macos

I'm building a macOS SwiftUI app. In it, I have a popover with a TextField. The TextField has a binding that updates an ObservableObject that then propagates its value back down a NavigationView to the Detail View.
On the first keyboard entry to the TextField, the popover closes. However, on all subsequent entries, the popover stays open.
I've also tried this with a DatePicker and have had the same results, so it's not limited to the TextField entry.
Note that this does not happen when running the same code on iOS (on iPhone where popovers are displayed as sheets or on iPad where they are popovers).
I've included a minimal example.
Anyone have any clue how to avoid the popover closing on first edit?
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(store: AppState())
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
}
class AppState : ObservableObject {
#Published var models : [String:Model] = ["1" : Model(id: "1", title: "June 14"),
"2" : Model(id: "2", title: "July 21"),
"3" : Model(id: "3", title: "Sept 5")]
func changeTitle(key: String, newTitle: String) {
guard var model = self.models[key] else {
return
}
model.title = newTitle
self.models[key] = model
}
}
struct Model {
var id : String
var title: String
}
struct ContentView: View {
#ObservedObject var store : AppState
var body: some View {
NavigationView {
VStack {
List {
ForEach(Array(store.models.values), id: \.id) { model in
NavigationLink(destination: DetailView(model: model,
changeTitle: { title in
self.store.changeTitle(key: model.id, newTitle: title)
})) {
VStack(alignment: .leading) {
Text(model.title).font(.headline)
}.padding(.vertical, 8)
}
}
}.frame(minWidth: 150, idealWidth: 200, maxWidth: 200, maxHeight: .infinity)
}
EmptyView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct DetailView : View {
var model : Model
var changeTitle: (String) -> Void
#State private var showPopover = false
var customBinding : Binding<String> {
Binding<String>(get: { self.model.title }, set: { newValue in
self.changeTitle(newValue)
})
}
var body: some View {
VStack {
Text(model.title)
Button("Edit") {
self.showPopover.toggle()
}.popover(isPresented: self.$showPopover) {
TextField("Text", text: self.customBinding)
.frame(minWidth: 300)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

Related

How can I hide statusBar of a View/Window in macOS in SwiftUI?

I was trying to hide statusBar of a view but I noticed it does not work in macOS, also Blur did not worked also opacity did not work! it look like I am trying programming new language, apple said code in place and apply every where! why I can not done those simple task in macOS?
struct ContentView: View {
var body: some View {
HStack {
Button { print("OK was clicked!") } label: { Text("OK").frame(width: 100) }
Button { print("Close was clicked!") } label: { Text("Close").frame(width: 100) }
}
.frame(width: 400, height: 200)
.background(Color.white.opacity(0.4).blur(radius: 0.5))
//.statusBar(hidden: true)
}
}
import SwiftUI
#main
struct macOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Hope it helps you.
Thank you.
class AppDelegate: NSObject, UIApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the content view, to display the contents
let contentView = ContentView()
//to display content view beyond status bar.
.edgesIgnoringSafeArea(.top)
// Now, just create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .texturedBackground, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
//So, this does the maigc
//Hides the titlebar.
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
}

SwiftUI: How to let ScrollView move one item once

My image list is a horizontal scroll list. And image in the list is presented as full screen width. By default, it is moving continuously, however, I want to let it move one item at a time, how could I do that.
I search it here, there is some similar questions related with Android but not iOS, specifically SwiftUI.
private struct PresentedImageList: View {
var images: [ImageViewModel]
var body: some View {
GeometryReader { gr in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 6) {
ForEach(self.images, id: \.self) { image in
KFImage(source: .network(image.fileURL))
.resizable()
.frame(width: gr.size.width)
.aspectRatio(1.77, contentMode: .fit)
}
}
}.background(Color.black)
}
}
}
If you have one ScrollView appearing, then you could just simply add
.onAppear {
UIScrollView.appearance().isPagingEnabled = true
}
this code at the end of the ScrollView Closure.
entire code would be
private struct PresentedImageList: View {
var images: [ImageViewModel]
var body: some View {
GeometryReader { gr in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 6) {
ForEach(self.images, id: \.self) { image in
KFImage(source: .network(image.fileURL))
.resizable()
.frame(width: gr.size.width)
.aspectRatio(1.77, contentMode: .fit)
}
}
}
.onAppear {
UIScrollView.appearance().isPagingEnabled = true
}
}
}
}
However if you have multiple ScrollView appearing, that added code will affect all of the ScrollViews appearing.
So in that case you could use TabView, instead of HStackView.
var body: some View {
GeometryReader { gr in
ScrollView(.horizontal, showsIndicators: false) {
TabView(alignment: .top, spacing: 6) {
ForEach(self.images, id: \.self) { image in
KFImage(source: .network(image.fileURL))
.resizable()
.frame(width: gr.size.width)
.aspectRatio(1.77, contentMode: .fit)
}
}
//.tabViewStyle(PageTabViewStyle.page(indexDisplayMode: .never))
// --> in case you don't want the indicators
}
}
}
in SwiftUI this is not available. so need to do using UIKit
struct ContentView: View {
var images: [ImageViewModel]
var body: some View {
GeometryReader { gr in
ScrollViewUI(hideScrollIndicators: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.images, id: \.self) { image in
VStack {
KFImage(source: .network(image.fileURL))
.resizable()
.frame(width: gr.size.width-6,alignment: .center)
.aspectRatio(1.77, contentMode: .fit)
}
.frame(width: gr.size.width,alignment: .center)
}
}
}
}
}
}
ScrollViewUI
struct ScrollViewUI<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
var hideScrollIndicators: Bool = false
init(hideScrollIndicators: Bool, #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.hideScrollIndicators = hideScrollIndicators
}
func makeUIViewController(context: Context) -> ScrollViewController<Content> {
let vc = ScrollViewController(rootView: self.content())
vc.hideScrollIndicators = hideScrollIndicators
return vc
}
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = self.content()
}
}
ScrollViewController
class ScrollViewController<Content: View>: UIViewController, UIScrollViewDelegate {
var hideScrollIndicators: Bool = false
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.delegate = self
view.showsVerticalScrollIndicator = !hideScrollIndicators
view.showsHorizontalScrollIndicator = !hideScrollIndicators
view.bounces = false
view.backgroundColor = .clear
view.isPagingEnabled = true
return view
}()
init(rootView: Content) {
self.hostingController = UIHostingController<Content>(rootView: rootView)
self.hostingController.view.backgroundColor = .clear
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var hostingController: UIHostingController<Content>! = nil
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
view.backgroundColor = .clear
self.makefullScreen(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.makefullScreen(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func makefullScreen(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
}

How to make modal non-dismissible in SwiftUI

I’m creating an app where I have create first screen (it will be short description of application) and on the screen I have a next Button if I click on next button it should be dismiss otherwise it must not be dismiss either pull down.
If user pull down a sheet, it should be again re-position.
The problem is, that the user can dismiss the modal by swiping it down and application dashboard screen show that should be prevented.
How can we prevent to dismiss the Model by pull down.
struct ModalView : View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Rectangle()
.fill(Color.orange)
.frame(width: 400, height: 650)
.overlay(
VStack{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "chevron.left")
Text("Dismiss")
}.padding(10.0)
.overlay(
RoundedRectangle(cornerRadius: 10.0)
.stroke(lineWidth: 2.0)
)
}.accentColor(.white)
})
.border(Color.blue)
.gesture( DragGesture())
}
}
ContentView
struct ContentView: View {
//MARK: Properties
//isPresented:- Present's a Welcome Screen in the form of cards.
#State private var isPresented = true
var body: some View {
VStack{
DashboardView()
.sheet(isPresented: $isPresented){
//IntroductionView(isPresentingSheet: self.$isPresented)
ModalView()
}
}
}
}
DashboardView
struct DashboardView: View {
var body: some View {
Text("Hello SwiftUI")
}
}
You can try this solution:
struct ModalWrapper: View {
var body: some View {
ModalView().highPriorityGesture(DragGesture())
}
}
struct ModalView : View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Rectangle()
.fill(Color.orange)
.frame(width: 400, height: 650)
.overlay(
VStack{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "chevron.left")
Text("Dismiss")
}.padding(10.0)
.overlay(
RoundedRectangle(cornerRadius: 10.0)
.stroke(lineWidth: 2.0)
)
}.accentColor(.white)
})
.border(Color.blue)
.highPriorityGesture(DragGesture())
}
}
struct ContentView: View {
//MARK: Properties
//isPresented:- Present's a Welcome Screen in the form of cards.
#State private var isPresented = true
var body: some View {
VStack{
DashboardView()
.sheet(isPresented: $isPresented){
//IntroductionView(isPresentingSheet: self.$isPresented)
ModalWrapper()
}
}
}
}
struct DashboardView: View {
var body: some View {
Text("Hello SwiftUI")
}
}
Here I have added ModalWrapper for wrap the modal view Or else you will have to add highPriorityGesture(DragGesture()) to all subviews of the ModalView So it is better to keep one wrapper view.
Hope this will help you.

Add Image to TextField/SecureField in SwiftUI, add padding to placeholder text

I made a textfield and a securetextfield in SwiftUI but I have no idea how to add in an image into my textfield/secure textfield in SwiftUI. There is not much documentation online for SwiftUI like there was for the older versions of Swift. I also want to shift over the (placeholder/typed in text) over by a designated amount say for example like 30 points to the right. I also was trying out to see if the background color would change from white to red, but as you can see, it is in my code with no effect on the UI.
Note:I have the GeometryReader called earlier in my code as well as the #state variables for the username and the password.
My goal is to have it look like this , right now it looks like this
VStack (spacing: deviceSize.size.height * (50/812)) {
TextField ("Username", text: self.$username)
.foregroundColor(.black)//text color when you type
.accentColor(.blue)//cursor color
.background(Color(.red))//????
.textFieldStyle(RoundedBorderTextFieldStyle())
.cornerRadius(50)
// .border(Color.white)
//.font(.title)
SecureField ("Password", text: self.$password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.cornerRadius(50)
}
.padding(.init(top: 0, leading: deviceSize.size.width * (38/375), bottom: 0, trailing: deviceSize.size.width * (38/375)))
The easiest way to achieve such a design would be to place the Image and TextField in a HStack and give it one Rounded background. It is slightly more complicated with the password field as it needs an extra Button, and when you hide/show the password you need to change between TextField and SecureField. Here is my take on it:
struct ContentView: View {
#State private var username = ""
#State private var password = ""
#State private var showPassword = false
var body: some View {
ZStack {
Color.blue
VStack {
HStack {
Image(systemName: "person")
.foregroundColor(.secondary)
TextField("Username",
text: $username)
} .padding()
.background(Capsule().fill(Color.white))
HStack {
Image(systemName: "lock")
.foregroundColor(.secondary)
if showPassword {
TextField("Password",
text: $password)
} else {
SecureField("Password",
text: $password)
}
Button(action: { self.showPassword.toggle()}) {
Image(systemName: "eye")
.foregroundColor(.secondary)
}
} .padding()
.background(Capsule().fill(Color.white))
} .padding()
}
}
}
I'm really new to SwiftUI, but I found a workaround for this that I hope doesn't cause any issues in the future or it will be a big lesson learned. If anyone has any suggestion I'd appreciate it too! =]
I embedded the TextField and the image in a ZStack and I put the image inside a View and gave the view a padding.
struct FormInputBox: View {
#State private var text: String = ""
#State private var textFieldState: TextFieldState = .empty
private var textFieldType: TextFieldType
private var textViewPlaceholder = ""
init(placeholder: String,
textFieldType: TextFieldType) {
self.textViewPlaceholder = placeholder
self.textFieldType = textFieldType
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .center), content: {
TextField(textViewPlaceholder, text: $text)
.textFieldStyle(MyTextFieldStyle(textFieldState: $textFieldState))
AnyView(
Image("tick")
.resizable()
.frame(width: 20, height: 20, alignment: .leading)
)
.padding(32)
})
}
I have created a reusable SwiftUI Textfield named ASTextField which works similar to the textField in UIKit, where you can add the leftView and rightView of the textField and can handle the events related them.
You can find the implementation of this at gist.
This the way you can consume it:-
struct ContentView : View , ASTextFieldDelegate {
let leftImage = UIImage(systemName: "calendar")
let rightImage = UIImage(systemName: "eye")
let rightImage1 = UIImage(systemName: "trash")
#State var text : String? = "with simple binding"
#State var text1 : String? = "with closure for right item"
#State var text2 : String? = "for secure entry"
var body: some View {
VStack {
Spacer()
ASTextField(text: $text)
Spacer()
ASTextField(rightItem: rightImage1, leftItem: leftImage, handleLeftTap: {
print("right icon tapped.....")
}, delegate: self, text: $text1)
Spacer()
ASTextField(rightItem: rightImage, leftItem: leftImage, isSecuredEntry: true, delegate: self, text: $text2)
Spacer()
}
}
}
"Introspect" will work for you
Textfield()
.introspectTextField { textfield in
textfield.rightViewMode = .unlessEditing
textfield.rightView = UIImageView(image: UIImage(named: ImageCatalog.error.content))
}
I am totally newborn toddle in iOS Dev. So i wrote just like this. My apologises in advance if someone will get blind from the ugliness of the written code.
struct ContentView: View {
#State private var nameSearch: String = ""
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.frame(width: 230, height: 30)
.border(.black, width: 0.2)
.foregroundColor(.white)
HStack {
ZStack {
Image(systemName: "magnifyingglass.circle")
.foregroundColor(.gray)
.frame(width: 10, height: 10, alignment: .leading)
.padding(.trailing, 200)
TextField( "Search", text: $nameSearch)
.frame(width: 180, height: 30)
.padding(.leading, 20 )
}
}
}

Change window size based on NavigationView in a SwiftUI macOS app

I'm using SwiftUI for a Mac app where the main window contains a NavigationView. This NavigationView contains a sidebar list. When an item in the sidebar is selected, it changes the view displayed in the detail view. The views presented in the detail view are different sizes which should cause the size of the window to change when they are displayed. However, when the detail view changes size the window does not change size to accommodate the new detail view.
How can I make the window size change according to the size of the NavigationView?
My example code for the app is below:
import SwiftUI
struct View200: View {
var body: some View {
Text("200").font(.title)
.frame(width: 200, height: 400)
.background(Color(.systemRed))
}
}
struct View500: View {
var body: some View {
Text("500").font(.title)
.frame(width: 500, height: 300)
.background(Color(.systemBlue))
}
}
struct ViewOther: View {
let item: Int
var body: some View {
Text("\(item)").font(.title)
.frame(width: 300, height: 200)
.background(Color(.systemGreen))
}
}
struct DetailView: View {
let item: Int
var body: some View {
switch item {
case 2:
return AnyView(View200())
case 5:
return AnyView(View500())
default:
return AnyView(ViewOther(item: item))
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(1...10, id: \.self) { index in
NavigationLink(destination: DetailView(item: index)) {
Text("Link \(index)")
}
}
}
.listStyle(SidebarListStyle())
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And here is what the example app looks like when the detail view changes size:
Here is demo of possible approach that works. I did it on one different view, because you will need to redesign your solution to adopt it.
Demo
1) The view requiring window animated resize
struct ResizingView: View {
public static let needsNewSize = Notification.Name("needsNewSize")
#State var resizing = false
var body: some View {
VStack {
Button(action: {
self.resizing.toggle()
NotificationCenter.default.post(name: Self.needsNewSize, object:
CGSize(width: self.resizing ? 800 : 400, height: self.resizing ? 350 : 200))
}, label: { Text("Resize") } )
}
.frame(minWidth: 400, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
}
}
2) Window's owner (in this case AppDelegate)
import Cocoa
import SwiftUI
import Combine
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var subscribers = Set<AnyCancellable>()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ResizingView()
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), // just default
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
NotificationCenter.default.publisher(for: ResizingView.needsNewSize)
.sink(receiveCompletion: {_ in}) { [unowned self] notificaiton in
if let size = notificaiton.object as? CGSize {
var frame = self.window.frame
let old = self.window.contentRect(forFrameRect: frame).size
let dX = size.width - old.width
let dY = size.height - old.height
frame.origin.y -= dY // origin in flipped coordinates
frame.size.width += dX
frame.size.height += dY
self.window.setFrame(frame, display: true, animate: true)
}
}
.store(in: &subscribers)
}
...
Asperi's answer works for me, but the animation is not working on Big Sur 11.0.1, Xcode 12.2. Thankfully, the animation works if you wrap it in an NSAnimationContext:
NSAnimationContext.runAnimationGroup({ context in
context.timingFunction = CAMediaTimingFunction(name: .easeIn)
window!.animator().setFrame(frame, display: true, animate: true)
}, completionHandler: {
})
Also it should be mentioned that ResizingView and window don't have to be initialized inside AppDelegate; you can continue using SwiftUI's App struct:
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ResizingView()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow?
var subscribers = Set<AnyCancellable>()
func applicationDidBecomeActive(_ notification: Notification) {
self.window = NSApp.mainWindow
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
setupResizeNotification()
}
private func setupResizeNotification() {
NotificationCenter.default.publisher(for: ResizingView.needsNewSize)
.sink(receiveCompletion: {_ in}) { [unowned self] notificaiton in
if let size = notificaiton.object as? CGSize, self.window != nil {
var frame = self.window!.frame
let old = self.window!.contentRect(forFrameRect: frame).size
let dX = size.width - old.width
let dY = size.height - old.height
frame.origin.y -= dY // origin in flipped coordinates
frame.size.width += dX
frame.size.height += dY
NSAnimationContext.runAnimationGroup({ context in
context.timingFunction = CAMediaTimingFunction(name: .easeIn)
window!.animator().setFrame(frame, display: true, animate: true)
}, completionHandler: {
})
}
}
.store(in: &subscribers)
}
}
the following will not solve your problem, but might (with some extra work), lead you to a solution.
I did not have much to investigate further, but it's possible to overwrite the setContentSize method in NSWindow (by subclassing of course). That way you can override the default behavior, which is setting the window frame without an animation.
The problem you will have to figure out is the fact that for complex views such as yours, the setContentSize method is called repeatedly, causing the animation not to work properly.
The following example works fine, but that's because we are dealing with a very simple view:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the window and set the content view.
window = AnimatableWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
class AnimatableWindow: NSWindow {
var lastContentSize: CGSize = .zero
override func setContentSize(_ size: NSSize) {
if lastContentSize == size { return } // prevent multiple calls with the same size
lastContentSize = size
self.animator().setFrame(NSRect(origin: self.frame.origin, size: size), display: true, animate: true)
}
}
struct ContentView: View {
#State private var flag = false
var body: some View {
VStack {
Button("Change") {
self.flag.toggle()
}
}.frame(width: self.flag ? 100 : 300 , height: 200)
}
}

Resources