pull down to refresh data in swift UI - swift

I am trying to develop my first IOS app in SwiftUI and I want to have a pull down to refresh List. I already know that at the moment Apple hasn't implemented it but I found the following solution here on stackoverflow (Pull down to refresh data in SwiftUI) and I am implemented the solution from the first answer. And this works fine.
But now I want to have a SwiftUI View in this refreshable View. The solution says I have to:
wrapping them in a UIHostingController and dropping them in makeUIView
And here is my problem. What did I have to do exactly? I tried the following but it isn't working.
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.refreshControl = UIRefreshControl()
control.refreshControl?.addTarget(context.coordinator, action:
#selector(Coordinator.handleRefreshControl),
for: .valueChanged)
// Simply to give some content to see in the app
/*let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 30))
label.text = "Scroll View Content"
control.addSubview(label)*/
let parent = UIViewController()
let child = UIHostingController(rootView: RecipeList())
child.view.translatesAutoresizingMaskIntoConstraints = false
child.view.frame = parent.view.bounds
// First, add the view of the child to the view of the parent
parent.view.addSubview(child.view)
// Then, add the child to the parent
parent.addChild(child)
return control
}
Has anyone an idea what I am doing wrong and can tell me what I have to change?
Thanks and best regards
Henrik

iOS 15+
struct ContentView: View {
var body: some View {
NavigationView {
if #available(iOS 15.0, *) {
List(1..<100) { row in
Text("Row \(row)")
}
.refreshable {
print("write your pull to refresh logic here")
}
} else {
// Fallback on earlier versions
}
}
}
}

Related

How to change a macOS menubar icon?

I'm trying to change a macOS menubar icon at the click of a button based on this question.
However, when I run the app and click the button, I get a Could not cast value of type 'SwiftUI.AppDelegate' (0x20dfafd68) to 'MenuBarTest.AppDelegate' (0x10442c580). error on this line:
let appDelegate = NSApplication.shared.delegate as! AppDelegate
Main Swift File
#main
struct MenuBarTestApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem?
var popOver = NSPopover()
func applicationDidFinishLaunching(_ notification: Notification) {
let menuView = MenuBarView()
popOver.behavior = .transient
popOver.animates = true
popOver.contentViewController = NSViewController()
popOver.contentViewController?.view = NSHostingView(rootView: menuView)
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let MenuButton = statusItem?.button {
MenuButton.image = NSImage(named: "settings_bw")
MenuButton.image?.size = NSSize(width: 18.0, height: 18.0)
MenuButton.action = #selector(MenuButtonToggle)
}
}
func setIcon(colorType:String) {
var icon = NSImage(named: "settings_color")
if colorType != "color" {
icon = NSImage(named: "settings_bw")
}
statusItem?.button?.image = icon
}
#objc
func MenuButtonToggle(sender: AnyObject) {
if popOver.isShown {
popOver.performClose(sender)
} else {
if let MenuButton = statusItem?.button {
self.popOver.show(relativeTo: MenuButton.bounds, of: MenuButton, preferredEdge: NSRectEdge.minY)
popOver.contentViewController?.view.window?.makeKey()
}
}
}
}
MenuBar View
struct MenuBarView: View {
var body: some View {
VStack {
Button(action: {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
appDelegate.setIcon(colorType: "color")
}, label: {
Text("Change Icon")
})
}
.frame(width: 150, height: 100)
}
}
First, I'll answer the question as asked, but then I'll follow up with why it actually doesn't matter because it's solving the wrong problem.
Accessing the App Delegate in a SwiftUI lifecycle app
When you're using SwiftUI to manage your app lifecycle, you don't use an app delegate directly. SwiftUI will set the application's app delegate to an instance of its own class, SwiftUI.AppDelegate. This is what you saw when trying to cast it to MenuBarTest.AppDelegate. You can't cast it to your class, because it's not an instance of your class.
As a backwards compatibility feature, SwiftUI's app delegate lets you provide your own delegate instance to that it will forward to. This is wired up with the NSApplicationDelegateAdaptor property wrapper. If you want to access an instance, that delegate instance property is the way to get at it (not NSApplication.shared.delegate).
Don't even use the App Delegate
You can disregard everything I said so far, because this is just a bad design, and you shouldn't use it. The App Delegate should be strictly limited to code related to managing the life cycle of your app and how it interacts with the system. It should not be a kitchen-junk-drawer of any code that fits.
Having random code right in the App Delegate is the AppKit equivalent to putting all your code in the main() function of a C program. It's done for brevity, but rarely a good idea. This is what you'll often see in tutorials, which aim to keep file and line count at a minimum, to illustrate some other point (e.g. Related to menu bar configuration, in this case).
As you'll notice, none of the code in this app delegate does much that's specific to the App life cycle. It just calls out to NSStatusBar. This code can just be put in any other appropriate place, like any other stateful SwiftUI code.

Menu Bar Popover is missing from application's elements tree on macOS

I'm currently trying to write simple UI Tests for an App that comes with a popover in the macOS menu bar. One of the tests is supposed to open the menu bar popover and interact with its content. The problem is that the content seems to be completely absence from the application's element tree.
I'm creating the pop-up like so:
let view = MenuBarPopUp()
self.popover.animates = false
self.popover.behavior = .transient
self.popover.contentViewController = NSHostingController(rootView: view)
…and show/hide it on menu bar, click like this:
if let button = statusItem.button {
button.image = NSImage(named: NSImage.Name("MenuBarButtonImage"))
button.action = #selector(togglePopover(_:))
}
#objc func togglePopover(_ sender: AnyObject?) {
if self.popover.isShown {
popover.performClose(sender)
} else {
openToolbar()
}
}
func openToolbar() {
guard let button = menuBarItem.menuBarItem.button else { return }
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
NSApp.activate(ignoringOtherApps: true)
}
When I dump the element tree, the popover is not present:
[…]
MenuBar, 0x7fef747219d0, {{1089.0, 1.0}, {34.0, 22.0}}
StatusItem, 0x7fef74721b00, {{1089.0, 1.0}, {34.0, 22.0}}
[…]
Everything works when I compile the app and click around, I just can't make it work when it comes to automated UI testing. Any ideas?
Okay, after spending a lot of time with it, this solved my problem.
First, I had to add the Popover to the app's accessibility children like so:
var accessibilityChildren = NSApp.accessibilityChildren() ?? [Any]()
accessibilityChildren.append(popover.contentViewController?.view as Any)
NSApp.setAccessibilityChildren(accessibilityChildren)
However, this didn't seem to solve my problem at first. I'm using an App Delegate in a SwiftUI application. After tinkering with it for quite some time, I figured out that the commands I've added in my App.swift didn't go too well with my changes to the accessibility children in the App Delegate. After removing the commands from the Window Group, everything worked as expected.
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
// .commands {
// CommandGroup(replacing: CommandGroupPlacement.appSettings) {
// Button("Preferences...") { showPreferences() }
// }
// }
}
}

UIButton Not Clickable when added UITabbar

When added UIButton on UITabbar to middle as shown in figure.
The button action on above the UITabBar unable to click
func setupMiddleButton() {
plusButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64))
var menuButtonFrame = plusButton.frame
menuButtonFrame.origin.x = tabBar.bounds.width/2 - menuButtonFrame.size.width/2
let hasNotched :Bool? = UIDevice.current.hasNotch
if hasNotched != nil {
menuButtonFrame.origin.y = tabBar.bounds.height - menuButtonFrame.height - 15
} else {
menuButtonFrame.origin.y = tabBar.bounds.height - menuButtonFrame.height - 50
}
plusButton.frame = menuButtonFrame
plusButton.setTitle("+", for: .normal)
plusButton.titleLabel?.font = UIFont.helveticaNeue(ofSize: 40)
plusButton.backgroundColor = UIColor.init(hexString: "5E71FE")
plusButton.titleEdgeInsets = UIEdgeInsets(top: 0,left: 10,bottom: 10,right: 10)
tabBar.addSubview(plusButton)
plusButton.layer.cornerRadius = menuButtonFrame.height/2
plusButton.addTarget(self, action: #selector(plusButtonAction(sender:)), for: .touchUpInside)
}
You need to override the hitTest method in your custom tab bar class like this
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
for member in subviews.reversed()
{
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event)
else { continue }
return result
}
return nil
}
Basically the problem is that upper part is not clickable because it is outside of the bounds of main content view of tab bar.
This method will check if the tap is inside the bounds of the view, if it is it will return the view and the action for that button will get called.
Documentation by apple: Link
P.s I was facing the same issue recently and got this help which worked smooth.
I suspect that what you are trying to do is not possible, or at the least, not supported by Apple. (And thus not recommended since you might find a way to make it might work today but not in some future OS version.)
As a rule, Apple does not support you adding custom view objects to system components like tab bars, navigation bars, stack views, table/collection view controllers, etc except through a documented API.
I would suggest NOT doing what you are trying to do. instead, add a button in the content view of the tab bar controller. I don't know if you'll be able to make it partly cover the tab bar like you are trying to do however.
Add the button to the view of the UITabbarController instead of adding to the TabBar. And then reposition the button, it will work.

How can I put the Image in the background of a Form in SwiftUI?

I'm currently working on an app and I wonder how to insert an image as a background of a Form in SwiftUI.
I have already tried this:
.onAppear {
UITableView.appearance().backgroundView = UIImageView(image: UIImage(named: "Background"))
}
First, it seemed to work, but when I pressed one of the multiple NavigationLinks that has the Form, the App crashes.
Thanks!
You need to clear both the UITableView and the UITableViewCell appearance:
UITableView.appearance().backgroundColor = UIColor.clear
UITableViewCell.appearance().backgroundColor = UIColor.clear
Then, you can change the background as you wish:
struct ContentView: View {
init() {
UITableView.appearance().backgroundColor = UIColor.clear
UITableViewCell.appearance().backgroundColor = UIColor.clear
}
var body: some View {
Form {
Text("Item!")
.listRowBackground(Color.clear)
}
.background(
Image("Background")
)
}
}
(You also need .listRowBackground if you want to change the row background.)

How to create Status Bar icon and menu in macOS using SwiftUI

What is SwiftUI API for creating status bar menus?
Apple seems to use SwiftUI views in Battery & WiFi menus according to the accessibility inspector. Screenshot of a battery menu attached, also its view hierarchy.
EDIT:
Posted the solution as a separate answer.
Since this question received more attention lately, and the only reply doesn't fully solve the issue I would like to repeat the edited part of my question and mark it as resolved.
Edit2: Added an additional piece of code that allows using a SwiftUI view as the status bar icon. Might be handy for displaying dynamic badges.
Found a way to show this in swiftui without an annoying NSPopover. Even though I used AppDelegate's applicationDidFinishLaunching to execute the code, it can be called from any place of your app, even in a SwiftUI lifecycle app.
Here is the code:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// SwiftUI content view & a hosting view
// Don't forget to set the frame, otherwise it won't be shown.
//
let contentViewSwiftUI = VStack {
Color.blue
Text("Test Text")
Color.white
}
let contentView = NSHostingView(rootView: contentViewSwiftUI)
contentView.frame = NSRect(x: 0, y: 0, width: 200, height: 200)
// Status bar icon SwiftUI view & a hosting view.
//
let iconSwiftUI = ZStack(alignment:.center) {
Rectangle()
.fill(Color.green)
.cornerRadius(5)
.padding(2)
Text("3")
.background(
Circle()
.fill(Color.blue)
.frame(width: 15, height: 15)
)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
.padding(.trailing, 5)
}
let iconView = NSHostingView(rootView: iconSwiftUI)
iconView.frame = NSRect(x: 0, y: 0, width: 40, height: 22)
// Creating a menu item & the menu to add them later into the status bar
//
let menuItem = NSMenuItem()
menuItem.view = contentView
let menu = NSMenu()
menu.addItem(menuItem)
// Adding content view to the status bar
//
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.menu = menu
// Adding the status bar icon
//
statusItem.button?.addSubview(iconView)
statusItem.button?.frame = iconView.frame
// StatusItem is stored as a property.
self.statusItem = statusItem
}
Inside the AppDelegate add the following code:
// Create the status item in the Menu bar
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
// Add a menu and a menu item
let menu = NSMenu()
let editMenuItem = NSMenuItem()
editMenuItem.title = "Edit"
menu.addItem(editMenuItem)
//Set the menu
self.statusBarItem.menu = menu
//This is the button which appears in the Status bar
if let button = self.statusBarItem.button {
button.title = "Here"
}
This will add a Button with a custom Menu to your MenuBar.
Edit - How to use SwiftUI View
As you asked, here is the answer how to use a SwiftUI View.
First create a NSPopover and then wrap your SwiftUI view inside a NSHostingController.
var popover: NSPopover
let popover = NSPopover()
popover.contentSize = NSSize(width: 350, height: 350)
popover.behavior = .transient
popover.contentViewController = NSHostingController(rootView: contentView)
self.popover = popover
Then instead of showing a NSMenu, toggle the popover:
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
button.title = "Click"
button.action = #selector(showPopover(_:))
}
With following action:
#objc func showPopover(_ sender: AnyObject?) {
if let button = self.statusBarItem.button
{
if self.popover.isShown {
self.popover.performClose(sender)
} else {
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
}
MenuBarExtra (macOS Ventura)
In macOS 13.0+ and Xcode 14.0+, the MenuBarExtra struct allows you create a system menu bar, that is similar to NSStatusBar's icons and menus. Use a MenuBarExtra when you want to provide access to commonly used functionality, even when your app is not active.
import SwiftUI
#available(macOS 13.0, *) // macOS Ventura
#main struct StatusBarApp: App {
#State private var command: String = "a"
var body: some Scene {
MenuBarExtra(command, systemImage: "\(command).circle") {
Button("Uno") { command = "a" }
.keyboardShortcut("U")
Button("Dos") { command = "b" }
.keyboardShortcut("D")
Divider()
Button("Salir") { NSApplication.shared.terminate(nil) }
.keyboardShortcut("S")
}
}
}
Getting rid of the App's icon on the Dock
In Xcode's Info tab, choose Application is agent (UIElement) and set its value to YES.