SwiftUI: Reinitialize after popover.show - swift

I am writing a MacOS (10.15 Catalina) application using a popover. The main ContentView includes a custom view with a simple toggle:
class AppDelegate: NSObject, NSApplicationDelegate {
var popover=NSPopover()
func applicationDidFinishLaunching(_ aNotification: Notification) {
self.popover.contentViewController = NSHostingController(rootView: contentView)
self.statusBarItem = NSStatusBar.system.statusItem(withLength: 18)
if let statusBarButton = self.statusBarItem.button {
statusBarButton.title = "☰"
statusBarButton.action = #selector(togglePopover(_:))
}
func show() {
let statusBarButton=self.statusBarItem.button!
self.popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
func hide() {
popover.performClose(nil)
}
#objc func togglePopover(_ sender: AnyObject?) {
self.popover.isShown ? hide() : show()
}
}
struct ContentView: View {
var body: some View {
Test("Hello")
// more stuff
}
}
struct Test: View {
var message: String
#State private var clicked: Bool = false
init(message: String) {
self.message = message
_clicked = State(initialValue: false)
print("init")
}
var body: some View {
return HStack {
Text(message)
Button("Click") {
self.clicked = true
}
if !self.clicked {
Text("Before")
}
else {
Text("After")
}
}
}
}
I would like to reinitialize some data in custom view whenever the popup reappears. So, in this example, clicked should reset to false. I have tried every combination of #Binding and #State variables I could find in my many searches, but nothing appears to work. It appears that .onAppear() only fires the first time.
The init() function is there because in my application I also need to include additional content and code. In this example, I have tried to use it to initialize the clicked state variable, but, though the print() function does print, the variable doesn’t seem to get reset.
How can I reinitialize the #State variable?

To initialize state in init you should not initialize it as property (because properties are initialised before init and state is initialised only once), so
struct Test: View {
var message: String
#State private var clicked: Bool // << here, only declare
init(message: String) {
self.message = message
_clicked = State(initialValue: false) // << then this works
print("init")
}
// ... other code
}

Related

SwiftUI: How to update textfield with live changes

In my content view i have function that detects whenever a user copies a website address
ContentView
#State private var detectedurl = ""
.................
.onAppear {
urlclipboardwatcher()
}
func urlclipboardwatcher() {
let pasteboard = NSPasteboard.general
var changeCount = NSPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { _ in
if let copiedString = pasteboard.string(forType: .string) {
...............
if copiedString.starts(with: "https://") {
detectedurl = copiedString
}
}
}
}
I want to pass this value to the textfield in my NewBookmark View. How do i update the textfield with any changes that happen with the pasteboard?
struct NewBookmark: View {
#Binding var detectedurl: String
#ObservedObject private var vm: AddNewBookmarkViewModel
init(vm: AddNewBookmarkViewModel, detectedurl: Binding<String>) {
self.vm = vm
self._detectedurl = detectedurl
}
TextField("Enter a URL", text: $vm.url)
// i want the detected url to automatically populate this textfield
Button("Save") {
vm.save()
}.disabled(vm.url.isEmpty)
AddBookMarkViewModel
class AddNewBookmarkViewModel: ObservableObject {
#Published var url: String = ""
.............
func save() {
do {
let myBM = MyBookmark(context: context)
myBM.url = url
try myBM.save()
} catch {
print(error)
}
}
}
Tbh, I am not really sure how the code which you posted works. But I did something similar in the past. Maybe it helps.
What I basically did is, one viewModel with two views. Both views hold on to the viewModel PasteboardViewModel. PasteboardViewModel is a StateObject which is passed on two the second view via. environmentObject. And url variable in the viewModel is bound to the PasteboardView. So every time this Publisher changes the TextField does it too.
struct ContentView: View {
#StateObject var viewModel: PasteboardViewModel = .init()
var body: some View {
VStack {
.....
PasteboardView()
.environmentObject(viewModel)
}
.onAppear {
viewModel.watchPasteboard()
}
.padding()
}
}
struct PasteboardView: View {
#EnvironmentObject var viewModel: PasteboardViewModel
var body: some View {
TextField(text: $viewModel.url) {
Text("Test")
}
}
}
class PasteboardViewModel: ObservableObject {
#Published var url: String = ""
func watchPasteboard() {
let pasteboard = UIPasteboard.general
var changeCount = UIPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if let copiedString = pasteboard.string {
if pasteboard.changeCount != changeCount {
self.url = copiedString
changeCount = pasteboard.changeCount
}
}
}
}
}

SwiftUI: #Environment(\.presentationMode)'s dismiss not working in iOS14

I have a view that shows a sheet for filtering the items in a list. The view has this var:
struct JobsTab: View {
#State private var jobFilter: JobFilter = JobFilter()
var filter: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView($jobFilter, categoriesViewModel, jobsViewModel)
}
}
However, in the sheet, I'm trying the following and I can't make the view dismissed when clicking on the DONE button, only on the CANCEL button:
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var categoriesViewModel: CategoriesViewModel
#ObservedObject var jobsViewModel: JobsViewModel
let filterViewModel: FilterViewModel
#Binding var jobFilter: JobFilter
#State private var draft: JobFilter
#State var searchText = ""
init(_ jobFilter: Binding<JobFilter>, _ categoriesViewModel: CategoriesViewModel, _ jobsViewModel: JobsViewModel) {
_jobFilter = jobFilter
_draft = State(wrappedValue: jobFilter.wrappedValue)
self.categoriesViewModel = categoriesViewModel
self.jobsViewModel = jobsViewModel
self.filterViewModel = FilterViewModel()
}
...
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("FilterView.Button.Cancel.Text".capitalizedLocalization) {
presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("FilterView.Button.Done.Text".capitalizedLocalization) {
let request = Job.defaultRequest()
request.predicate = filterViewModel.buildPredicate(withJobFilterDraft: self.draft)
request.sortDescriptors = [NSSortDescriptor(key: #keyPath(Job.publicationDate), ascending: false)]
jobsViewModel.filteredJobsFetchRequest = request
self.jobFilter = self.draft
presentationMode.wrappedValue.dismiss()
}
}
}
I have also tried with a #Binding like Paul says here but there's no luck.
Is there any workaround, or am I doing something wrong?
Thanks in advance!
EDIT: I've posted the properties of both views, because I think the problem comes from the the line in FilterView self.jobFilter = self.draft.
What I'm trying to do here is create a filter view, and the aforementioned line will be executed when the user presses the DONE button: I want to assign my binding jobFilter in the JobsTab the value of the FilterView source of truth (which is a #State) and probably, since I'm updating the binding jobFilter the FilterView is being shown again even though the $showFilter is false? I don't know to be honest.
EDIT2: I have also tried
``
if #available(iOS 15.0, *) {
let _ = Self._printChanges()
} else {
// Fallback on earlier versions
}
in both `FilterView` and its called `JobTabs` and in both, I get the same result: unchanged
According to your code, I assumed your FilterView() is not a sub view, but an independent view by its own.
Therefore, to make sure "presentationMode.wrappedValue.dismiss()" works, you don't need to create #Binding or #State variables outside the FilerView() for passing the data back and forth between different views. Just create one variable inside your FilterView() to make it works.
I don't have your full code, but I created a similar situation to your problem as below code:
import SwiftUI
struct Main: View {
#State private var showFilter = false
var body: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView()
}
}
}
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
NavigationView {
VStack {
Text("Filter View")
}.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("cancel")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("okay")
}
}
}
}
}
}
the parameter onDismiss is missing:
.sheet(isPresented: $showFilter, onDismiss: { isPresented = false }) { ... }
View model objects are usually a source of bugs because we don't use those in SwiftUI instead we use structs for speed and consistency. I recommend making an #State struct containing a bool property isSheetPresented and also the data the sheet needs. Add a mutating func and call it on the struct from the button event. Pass a binding to the struct into the sheet's view where you can call another mutating function to set the bool to false. Something like this:
struct SheetConfig {
var isPresented = false
var data: [String] = []
mutating func show(initialData: [String] ) {
isPresented = true
data = initialData
}
mutating func hide() {
isPresented = false
}
}
struct ContentView: View {
#State var config = SheetConfig()
var body: some View {
Button {
config.show(intialData: ...)
} label: {
}
.sheet(isPresented: $config.isPresented) {
FilterView($config)
}
Here is what I did to handle this issue.
Create a new protocol to avoid repeatations.
public protocol UIViewControllerHostedView where Self: View {
/// A method which should be triggered whenever dismiss is needed.
/// - Note: Since `presentationMode & isPresented` are not working for presented UIHostingControllers on lower iOS versions than 15. You must call, this method whenever you want to dismiss the presented SwiftUI.
func dismissHostedView(presentationMode: Binding<PresentationMode>)
}
public extension UIViewControllerHostedView {
func dismissHostedView(presentationMode: Binding<PresentationMode>) {
// Note: presentationMode & isPresented are not working for presented UIHostingControllers on lower iOS versions than 15.
if #available(iOS 15, *) {
presentationMode.wrappedValue.dismiss()
} else {
self.topViewController?.dismisOrPopBack()
}
}
}
Extend UIWindow and UIApplication
import UIKit
public extension UIWindow {
// Credits: - https://gist.github.com/matteodanelli/b8dcdfef39e3417ec7116a2830ff67cf
func visibleViewController() -> UIViewController? {
if let rootViewController: UIViewController = self.rootViewController {
return UIWindow.getVisibleViewControllerFrom(vc: rootViewController)
}
return nil
}
class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
switch(vc){
case is UINavigationController:
let navigationController = vc as! UINavigationController
return UIWindow.getVisibleViewControllerFrom( vc: navigationController.visibleViewController!)
case is UITabBarController:
let tabBarController = vc as! UITabBarController
return UIWindow.getVisibleViewControllerFrom(vc: tabBarController.selectedViewController!)
default:
if let presentedViewController = vc.presentedViewController {
if let presentedViewController2 = presentedViewController.presentedViewController {
return UIWindow.getVisibleViewControllerFrom(vc: presentedViewController2)
}
else{
return vc;
}
}
else{
return vc;
}
}
}
}
#objc public extension UIApplication {
/// LCUIComponents: Returns the current visible top most window of the app.
var topWindow: UIWindow? {
return windows.first(where: { $0.isKeyWindow })
}
var topViewController: UIViewController? {
return topWindow?.visibleViewController()
}
}
Extend View to handle UIHostingController presented View`
public extension View {
weak var topViewController: UIViewController? {
UIApplication.shared.topViewController
}
}
Finally, use the helper method in your SwiftUI view as follows: -
struct YourView: View, UIViewControllerHostedView {
// MARK: - Properties
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body some View {
Button {
// Note: this part trigger a method in a created protocol above.
dismissHostedView(presentationMode: presentationMode)
} label: {
Text("Tap to dismiss")
}
}
In the sheet try adding this instead.
#Environment(\.dismiss) var dismiss // EnvironmentValue
Button("Some text") {
// Code
dismiss()
}

Is there a way to know if a view has already been loaded in SwiftUI

I'm trying to focus the isUsernameFocused textField as soon as it loads on the screen, I tried doing it directly in the onAppear method but it looks like it needs a delay in order for it to focus. My concern is that for some reason the focus only occurs with a delay greater than 0.6 fractions of a second. Setting it at 0.7 fractions of a second seems to work fine but I'm afraid that eventually, this will stop working if the view gets bigger since it will need more time to load.
Is there a way to know when the VStack is fully loaded so I can trigger the isUsernameFocused? Something like, viewDidLoad in UIKit.
struct ContentView: View {
#FocusState private var isUsernameFocused: Bool
#State private var username = ""
var body: some View {
VStack {
TextField("Username", text: $username)
.focused($isUsernameFocused)
}
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7){
self.isUsernameFocused = true
}
}
}
}
If you are on macOS or tvOS you can use prefersDefaultFocus for that. It should come to iOS in June.
In the meantime, I just created this example that works around the issue. If your form appears multiple times you might want to check other values before setting the focus.
import SwiftUI
import UIKit
struct FocusTestView : View {
#State var presented = false
var body: some View {
Button("Click Me") {
presented = true
}
.sheet(isPresented: $presented) {
LoginForm()
}
}
}
struct LoginForm : View {
enum Field: Hashable {
case usernameField
case passwordField
}
#State private var username = ""
#State private var password = ""
#FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .usernameField)
SecureField("Password", text: $password)
.focused($focusedField, equals: .passwordField)
Button("Sign In") {
if username.isEmpty {
focusedField = .usernameField
} else if password.isEmpty {
focusedField = .passwordField
} else {
// handleLogin(username, password)
}
}
}
.uiKitOnAppear {
focusedField = .usernameField
}
}
}
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.action = action
return vc
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {
}
}
class UIAppearViewController: UIViewController {
var action: () -> Void = {}
override func viewDidLoad() {
view.addSubview(UILabel())
}
override func viewDidAppear(_ animated: Bool) {
DispatchQueue.main.asyncAfter(deadline:.now()) { [weak self] in
self?.action()
}
}
}
public extension View {
func uiKitOnAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}
UIKitAppear was taken from dev forum post and I added the dispatch async to call the action. LoginForm is from the docs on FocusState.

Is there a way to show the system keyboard and take inputs from it without a SwiftUI TextField?

I want to be able to display the system keyboard and my app take inputs from the keyboard without using a TextField or the like. My simple example app is as follows:
struct TypingGameView: View {
let text = “Some example text”
#State var displayedText: String = ""
var body: some View {
Text(displayedText)
}
}
I'm making a memorization app, so when I user types an input on the keyboard, it should take the next word from text and add it to displayedText to display onscreen. The keyboard should automatically pop up when the view is displayed.
If there is, a native SwiftUI solution would be great, something maybe as follows:
struct TypingGameView: View {
let text = “Some example text”
#State var displayedText: String = ""
var body: some View {
Text(displayedText)
.onAppear {
showKeyboard()
}
.onKeyboardInput { keyPress in
displayedText += keyPress
}
}
}
A TextField could work if there is some way to 1. Make it so that whatever is typed does not display in the TextField, 2. Disable tapping the text (e.g. moving the cursor or selecting), 3. Disable deleting text.
Here's a possible solution using UIViewRepresentable:
Create a subclass of UIView that implements UIKeyInput but doesn't draw anything
Wrap it inside a struct implementing UIViewRepresentable, use a Coordinator as a delegate to your custom UIView to carry the edited text "upstream"
Wrap it again in a ViewModifier that shows the content, pass a binding to the wrapper and triggers the first responder of your custom UIView when tapped
I'm sure there's a more synthetic solution to find, three classes for such a simple problem seems a bit much.
protocol InvisibleTextViewDelegate {
func valueChanged(text: String?)
}
class InvisibleTextView: UIView, UIKeyInput {
var text: String?
var delegate: InvisibleTextViewDelegate?
override var canBecomeFirstResponder: Bool { true }
// MARK: UIKeyInput
var keyboardType: UIKeyboardType = .decimalPad
var hasText: Bool { text != nil }
func insertText(_ text: String) {
self.text = (self.text ?? "") + text
setNeedsDisplay()
delegate?.valueChanged(text: self.text)
}
func deleteBackward() {
if var text = text {
_ = text.popLast()
self.text = text
}
setNeedsDisplay()
delegate?.valueChanged(text: self.text)
}
}
struct InvisibleTextViewWrapper: UIViewRepresentable {
typealias UIViewType = InvisibleTextView
#Binding var text: String?
#Binding var isFirstResponder: Bool
class Coordinator: InvisibleTextViewDelegate {
var parent: InvisibleTextViewWrapper
init(_ parent: InvisibleTextViewWrapper) {
self.parent = parent
}
func valueChanged(text: String?) {
parent.text = text
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> InvisibleTextView {
let view = InvisibleTextView()
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: InvisibleTextView, context: Context) {
if isFirstResponder {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
}
}
struct EditableText: ViewModifier {
#Binding var text: String?
#State var editing: Bool = false
func body(content: Content) -> some View {
content
.background(InvisibleTextViewWrapper(text: $text, isFirstResponder: $editing))
.onTapGesture {
editing.toggle()
}
.background(editing ? Color.gray : Color.clear)
}
}
extension View {
func editableText(_ text: Binding<String?>) -> some View {
modifier(EditableText(text: text))
}
}
struct CustomTextField_Previews: PreviewProvider {
struct Container: View {
#State private var value: String? = nil
var body: some View {
HStack {
if let value = value {
Text(value)
Text("meters")
.font(.subheadline)
} else {
Text("Enter a value...")
}
}
.editableText($value)
}
}
static var previews: some View {
Group {
Container()
}
}
}
you can have the textfield on screen but set opacity to 0 if you don't want it shown. Though this would not solve for preventing of deleting the text
You can then programmatically force it to become first responder using something like this https://stackoverflow.com/a/56508132/3919220

SwiftUI using NSSharingServicePicker in MacOS

I am trying to use a Share function inside my MacOS app in SwiftUI. I am having a URL to a file, which I want to share. It can be images/ documents and much more.
I found NSSharingServicePicker for MacOS and would like to use it. However, I am struggeling to use it in SwiftUI.
Following the documentation, I am creating it like this:
let shareItems = [...]
let sharingPicker : NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems as [Any])
sharingPicker.show(relativeTo: NSZeroRect, of:shareView, preferredEdge: .minY)
My problem is in that show() method. I need to set a NSRect, where I can use NSZeroRect.. but I am struggeling with of: parameter. It requires a NSView. How can I convert my current view as NSView and use it that way. Or can I use my Button as NSView(). I am struggling with that approach.
Another option would be to use a NSViewRepresentable. But should I just create a NSView and use it for that method.
Here is minimal working demo example
struct SharingsPicker: NSViewRepresentable {
#Binding var isPresented: Bool
var sharingItems: [Any] = []
func makeNSView(context: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if isPresented {
let picker = NSSharingServicePicker(items: sharingItems)
picker.delegate = context.coordinator
// !! MUST BE CALLED IN ASYNC, otherwise blocks update
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
class Coordinator: NSObject, NSSharingServicePickerDelegate {
let owner: SharingsPicker
init(owner: SharingsPicker) {
self.owner = owner
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) {
// do here whatever more needed here with selected service
sharingServicePicker.delegate = nil // << cleanup
self.owner.isPresented = false // << dismiss
}
}
}
Demo of usage:
struct TestSharingService: View {
#State private var showPicker = false
var body: some View {
Button("Share") {
self.showPicker = true
}
.background(SharingsPicker(isPresented: $showPicker, sharingItems: ["Message"]))
}
}
Another option without using NSViewRepresentable is:
extension NSSharingService {
static func submenu(text: String) -> some View {
return Menu(
content: {
ForEach(items, id: \.title) { item in
Button(action: { item.perform(withItems: [text]) }) {
Image(nsImage: item.image)
Text(item.title)
}
}
},
label: {
Image(systemName: "square.and.arrow.up")
}
)
}
}
You lose things like the "more" menu item or recent recipients. But in my opinion it's more than enough, simple and pure SwiftUI.