How do I make a data within a view refresh when initiated? - swift

I have a capsule with the number of steps displayed inside. This capsule is in my Home View which is one of the tabs in the tab bar. This is what is meant by a step capsule:
When I use onAppear and initialise the view for the second time, the steps capsule view loads again but the previous one is still there (so there are two step capsule views).
I don't want a second capsule view. I want the data in the first step capsule to change.
To stop the second capsule view I switched onAppear to onLoad this (it was successful):
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
This is the onLoad function in use (replaced onAppear):
.onLoad {
if let healthStore = healthStore {
healthStore.requestAuthorization { success in
if success {
healthStore.calculateSteps { statisticsCollection in
if let statisticsCollection = statisticsCollection {
// update the UI
updateUIFromStatistics(statisticsCollection)
}
}
}
}
}
}
This fixed the a new step capsule appearing every time the view was initiated however the data doesn't change. When the user takes more steps and goes onto the home view, I want to run the function that fetches the step data again.
How do I get the step data in the capsule to refresh when the view is initiated?

Related

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() }
// }
// }
}
}

SwiftMessage: how to handle action when user tap on outside of message view?

I'm using SwiftMessage in my project. when the specific message is showing on the screen, I want when the user tap on outside of the message view (anywhere else on message view), some actions happening. how can I do this?
update
I forgot to say I'm using SwiftMessage with one button.
Unfortunately, there isn't a way to add an action on tap outside of the MessageView.
However, if your dimMode is interactive (has tap outside to dismiss enabled) you can easily catch .willHide and .didHide events using eventListeners of SwiftMessages.Config:
let view = MessageView.viewFromNib(layout: .cardView)
var config = SwiftMessages.defaultConfig
config.dimMode = .gray(interactive: false)
config.eventListeners.append { event in
switch event {
case .willHide:
// Handle will hide
case .didHide:
// Handle did hide
default:
break
}
}
SwiftMessages.show(config: config, view: view)
Those events will be triggered on tap outside of the MessageView.
Update: In your particular case where you have a button which have a different action from the tap outside action, you can use something like this:
func showMessageView(buttonHandler: #escaping () -> Void, dismiss: #escaping () -> Void) {
var buttonTapped = false
let view = MessageView.viewFromNib(layout: .cardView)
view.buttonTapHandler = { sender in
buttonHandler()
buttonTapped = true
}
var config = SwiftMessages.defaultConfig
config.dimMode = .gray(interactive: false)
config.eventListeners.append { event in
if !buttonTapped, event == .didHide {
dismiss()
}
}
SwiftMessages.show(config: config, view: view)
}
That way when the button is tapped the dismiss closure will never run.

How can I show/hide a button added to the title bar of an NSWindow?

I have created a method in an NSWindow extension that allows me to add a button next to the text in the title bar. This is similar to the "down chevron" button that appears in the title bar of Pages and Numbers. When the button is clicked, an arbitrary code, expressed as a closure, is run.
While I have that part working fine, I would also like the button to be invisible most of the time and only become visible when the mouse is scrolled into the title bar area. This would be mimicking the way that Pages and Numbers displays the button.
However, I'm having difficulties getting the show/hide to work properly. I believe I can do it if I make it completely custom in the application delegate, and possibly by subclassing NSWindow, but I would really like to keep it as a single method in an NSWindow extension. In this way the code would be easily reusable in multiple applications.
To accomplish this I believe I need to inject an additional handler/listener that will tell me when the mouse enters and leaves the appropriate area. I can define the necessary area using an NSTrackingArea, but I haven't figured out how to "inject" an event listener without the need of subclasses. Does anyone know how (or if) such a thing is possible?
The key to handling the show/hide based on the mouse position was to use an NSTrackingArea to signify the portion that we are interested in, and to handle the mouse enter and mouse exit events. But since this can't be done directly on the title bar view (since we have to subclass the view in order to add the event handlers) we need to create an additional NSView that is invisible but covers the area we want to track.
I'll post the full code below, but the key parts related to this question are the TrackingHelper class defined near the bottom of the file and the way it is added to the titleBarView with its constrains set to be equal to the size of the title bar. The class itself is designed to take three closures, one for the mouse enter event, one for the mouse exit, and one for the action to take when the button is pressed. (Technically the latter doesn't really need to be part of the TrackingHelper, but it is a convenient place to put it to ensure it does not go out of scope while the UI still exists. A more correct solution would be to subclass NSButton to keep the closure, but I have always found subclassing NSButton to be a royal pain.)
Here is the full text of the solution. Note that this has a couple of things that depend on another library of mine - but they are not necessary for the understanding of this problem and are used to deal with the button image. If you wish to use this code you will need to replace the getImage function with one that creates the image you want. (And if you want to see what KSSCocoa is adding, you can obtain it from https://github.com/klassen-software-solutions/KSSCore)
//
// NSWindowExtension.swift
//
// Created by Steven W. Klassen on 2020-02-24.
//
import os
import Cocoa
import KSSCocoa
public extension NSWindow {
/**
Add an action button to the title bar. This will add a "down chevron" icon, similar to the one used in
Numbers and Pages, just to the right of the title in the title bar. When clicked it will run the given
lambda.
*/
#available(OSX 10.14, *)
func addTitleActionButton(_ lambda: #escaping () -> Void) -> NSButton {
guard let titleBarView = getTitleBarView() else {
fatalError("You can only add a title action to an app that has a title bar")
}
guard let titleTextField = getTextFieldChild(of: titleBarView) else {
fatalError("You can only add a title action to an app that has a title field")
}
let trackingHelper = TrackingHelper()
let actionButton = NSButton(image: getImage(),
target: trackingHelper,
action: #selector(trackingHelper.action))
actionButton.setButtonType(.momentaryPushIn)
actionButton.translatesAutoresizingMaskIntoConstraints = false
actionButton.isBordered = false
actionButton.isEnabled = false
actionButton.alphaValue = 0
trackingHelper.translatesAutoresizingMaskIntoConstraints = false
trackingHelper.onButtonAction = lambda
trackingHelper.onMouseEntered = {
actionButton.isEnabled = true
actionButton.alphaValue = 1
}
trackingHelper.onMouseExited = {
actionButton.isEnabled = false
actionButton.alphaValue = 0
}
titleBarView.addSubview(trackingHelper)
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[trackingHelper]-0-|",
options: [], metrics: nil,
views: ["trackingHelper": trackingHelper]))
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[trackingHelper]-0-|",
options: [], metrics: nil,
views: ["trackingHelper": trackingHelper]))
titleBarView.addSubview(actionButton)
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[titleTextField]-[actionButton(==7)]",
options: [], metrics: nil,
views: ["actionButton": actionButton,
"titleTextField": titleTextField]))
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-1-[actionButton]-3-|",
options: [], metrics: nil,
views: ["actionButton": actionButton]))
DistributedNotificationCenter.default().addObserver(
actionButton,
selector: #selector(actionButton.onThemeChanged(notification:)),
name: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"),
object: nil
)
return actionButton
}
fileprivate func getTitleBarView() -> NSView? {
return standardWindowButton(.closeButton)?.superview
}
fileprivate func getTextFieldChild(of view: NSView) -> NSTextField? {
for subview in view.subviews {
if let textField = subview as? NSTextField {
return textField
}
}
return nil
}
}
fileprivate extension NSButton {
#available(OSX 10.14, *)
#objc func onThemeChanged(notification: NSNotification) {
image = image?.inverted()
}
}
#available(OSX 10.14, *)
fileprivate func getImage() -> NSImage {
var image = NSImage(sfSymbolName: "chevron.down")!
if NSApplication.shared.isDarkMode {
image = image.inverted()
}
return image
}
fileprivate final class TrackingHelper : NSView {
typealias Callback = ()->Void
var onMouseEntered: Callback? = nil
var onMouseExited: Callback? = nil
var onButtonAction: Callback? = nil
override func mouseEntered(with event: NSEvent) {
onMouseEntered?()
}
override func mouseExited(with event: NSEvent) {
onMouseExited?()
}
#objc func action() {
onButtonAction?()
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
for trackingArea in self.trackingAreas {
self.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways]
let trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil)
self.addTrackingArea(trackingArea)
}
}

Binding not updated immediately inside updateUIView in UIViewRepresentable

I am adapting a Swift Project to SwiftUI. The original project has Drag and Drop on UIImages, drawing and some other manipulations which aren't possible in SwiftUI.
The code below is for explaining the problem only. Working code on:
https://github.com/rolisanchez/TestableDragAndDrop
On the project, when a user clicks on a button, a #State setChooseImage is changed to true, opening a sheet in which he is presented with a series of images:
.sheet(isPresented: self.$setChooseImage, onDismiss: nil) {
VStack {
Button(action: {
self.setChooseImage = false
self.chosenAssetImage = UIImage(named: "testImage")
self.shouldAddImage = true
}) {
Image("testImage")
.renderingMode(Image.TemplateRenderingMode?.init(Image.TemplateRenderingMode.original))
Text("Image 1")
}
Button(action: {
self.setChooseImage = false
self.chosenAssetImage = UIImage(named: "testImage2")
self.shouldAddImage = true
}) {
Image("testImage2")
.renderingMode(Image.TemplateRenderingMode?.init(Image.TemplateRenderingMode.original))
Text("Image 2")
}
}
}
After selecting the image, setChooseImage is set back to false, closing the sheet. The Image self.chosenAssetImage is also a #State and is set to the chosen Image. The #State shouldAddImage is also set to true. This chosen UIImage and Bool are used inside a UIView I created to add the UIImages. It is called inside SwiftUI like this:
DragAndDropRepresentable(shouldAddImage: $shouldAddImage, chosenAssetImage: $chosenAssetImage)
The UIViewRepresentable to add the UIView is the following:
struct DragAndDropRepresentable: UIViewRepresentable {
#Binding var shouldAddImage: Bool
#Binding var chosenAssetImage: UIImage?
func makeUIView(context: Context) -> DragAndDropUIView {
let view = DragAndDropUIView(frame: CGRect.zero)
return view
}
func updateUIView(_ uiView: DragAndDropUIView, context: UIViewRepresentableContext< DragAndDropRepresentable >) {
if shouldAddImage {
shouldAddImage = false
guard let image = chosenAssetImage else { return }
uiView.addNewAssetImage(image: image)
}
}
}
I understand updateUIView is called whenever there are some
changes that could affect the views. In this case, shouldAddImage
became true, thus entering the if loop correctly and calling
addNewAssetImage, which inserts the image into the UIView.
The problem here is that updateUIView is called multiple times, and
enters the if shouldAddImage loop all those times, before updating the shouldAddImage Binding. This means that it will add the image multiple times to the UIView.
I put a breakpoint before and after the assignment of shouldAddImage = false, and even after passing that line, the value continues to be true.
The behavior I wanted is to change the shouldAddImage immediately to false, so that even if updateUIView is called multiple times, it would only enter the loop once, adding the image only once.

How to detect when a UIPageViewController's view has changed

I have an app with a PageViewController and I want to make my own custom PageViewController indicator. Everything is all set up. The only thing I need to do now is to figure out how to tell when the view controller's view has changed and what view it is currently on.
I have linked to this demo. In the first part I am clicking on the buttons at the top to change the page and the slider indicating what the current page is working, however after that I swipe my finger to change the controller, and I want to make the slider indicator to move with the page.
Here I had commented what to do for getting current Page index using PageViewController
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool)
{
if (completed)
{
//No need as we had Already managed This action in other Delegate
}
else
{
///Update Current Index
Manager.pageindex = Manager.lastIndex
///Post notification to hide the draggable Menu
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "updateLabelValue"), object: nil)
}
}
//MARK: Check Transition State
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController])
{
///Update Last Index
Manager.lastIndex = Manager.pageindex
///Update Index
Manager.pageindex = pages.index(of: pendingViewControllers[0])!
///Post notification to hide the draggable Menu
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "updateLabelValue"), object: nil)
}
I Had used Notification Observer to Notify the main controller about page Change and Performing some action
Manager is my struct , that hold the current page number , you can make hold the index in struct or in class wherever you want
My struct class
//Struct Class
struct Manager
{
///Current Page Index
static var pageindex :Int = 0
///Last Index
static var lastIndex :Int = 0
}
Main usage
switch Manager.pageindex
{
case 0:
self.mainHomeLabel.text = "VC1"
case 1:
self.mainHomeLabel.text = "VC2"
case 2:
self.mainHomeLabel.text = "VC3"
default:
self.mainHomeLabel.text = "VC1"
}
Note Any queries in the respective code Please ask
Update - Added Demo Project Link
Link - https://github.com/RockinGarg/PageViewController-Demo.git
If you have an array of the pages you're showing then you could use the UIPageViewControllerDelegate to detect page changes. More specifically this function https://developer.apple.com/documentation/uikit/uipageviewcontrollerdelegate/1614091-pageviewcontroller as it will tell you what you are about to transition to. You could then determine what page you are on based on the view controller that is about to be presented.