How to add Tooltip on macOS 10.15 with SwiftUI - macos

According to Apple help modifier is only available in macOS11, so what is the workaround for adding a tooltip in macOS 10.15?
In SwiftUI on macOS 11, you can use the .help("Tooltip text") view modifier to add a tooltip. See the "What's new in SwiftUI" session for WWDC 2020.
REFERENCE
https://developer.apple.com/forums/thread/123243

The workaround is to use a overplayed old NSView
import SwiftUI
struct Tooltip: NSViewRepresentable {
let tooltip: String
func makeNSView(context: NSViewRepresentableContext<Tooltip>) -> NSView {
let view = NSView()
view.toolTip = tooltip
return view
}
func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<Tooltip>) {
}
}
public extension View {
func toolTip(_ toolTip: String) -> some View {
self.overlay(Tooltip(tooltip: toolTip))
}
}
To use the modifier
Image("pin")
.resizable()
.toolTip("TEST")
Also an open-source solution can be found on GitHub, https://github.com/quassummanus/SwiftUI-Tooltip

Related

SwiftUI 2.0 disable window's zoom button on macOS

I am writing a small App for the Mac..
I need to disable to (Green Button) for full screen.
I am using SwiftUI App not AppKit App Delegate
Cant find how to disable the Full Screen Button for my app.
Any thoughts?
Because no one answered with a cleaner SwiftUI only version:
struct ContentView: View {
var body: some View {
HostingWindowFinder { window in
window?.standardWindowButton(.zoomButton)?.isHidden = true //this removes the green zoom button
}
Text("Hello world")
}
}
struct HostingWindowFinder: NSViewRepresentable {
var callback: (NSWindow?) -> ()
func makeNSView(context: Self.Context) -> NSView {
let view = NSView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
HostingWindowFinder concept taken from https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/

WKWebView shows white bar until window moved/resized

I'm trying to show a WKWebView in SwiftUI on MacOS. When the app initially loads, the WKWebView has a large, white bar at the top. Moving or resizing the window causes this to immediately disappear and display correctly. Interestingly, the blue border around the view does not exhibit the bad behavior.
My guess is that I'm missing some action in updateNSView.
I'm on MacOS 11.1 Big Sur, Xcode 12.2, Intel. Another thing to note is that I need to enable the "Outgoing Connections" entitlement in the App Sandbox to get WKWebView to render anything at all, even tho the content is provided locally from a string.
import SwiftUI
import WebKit
#main
struct ProblemWKWebViewApp: App {
var body: some Scene {
WindowGroup {
SwiftUIWebView()
.border(Color.blue, width: 2)
}
}
}
struct SwiftUIWebView: NSViewRepresentable {
public typealias NSViewType = WKWebView
func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.loadHTMLString("<body style=\"background-color: red;\"><h1>Hello World!</h1></body>", baseURL: nil)
return webView
}
func updateNSView(_ nsView: WKWebView, context: Context) {
}
}
I had the same problem. I suppose that it is a bug. However I managed to fix it with adding ignoresSafeArea():
#main
struct ProblemWKWebViewApp: App {
var body: some Scene {
WindowGroup {
SwiftUIWebView()
.border(Color.blue, width: 2)
// Shows weird black bar on top without this on macOS
.ignoresSafeArea()
}
}
}

Approach to use a SwiftUI view together with a UIView

I'm using a 3rd party library developed with UIKit. It's API needs a reference to a UIView.
How can I use this library inside SwiftUI? And how can I convert a SwiftUI view to a UIView?
I've tried creating a UIViewRepresentable like this:
struct SomeView: UIViewRepresentable {
let contentViewController: UIViewController
init<Content: View>(contentView: Content) {
self.contentViewController = UIHostingController(rootView: contentView)
}
func makeUIView(context: Context) -> UIKitView {
// Whatever I do here doesn't work. Frame is always 0
contentViewController.loadViewIfNeeded()
contentViewController.view.setNeedsDisplay()
contentViewController.view.layoutIfNeeded()
print(contentViewController.view.frame)
let uikitView = UIKitView()
uikitView.show(contentViewController.view)
return popover
}
func updateUIView(_ uiView: UIKitView, context: Context) {
}
}

How do I make my SwiftUI UIViewRepresentable respect intrinsicContentSize in previews?

When I create a view in SwiftUI and render it in an Xcode preview, using PreviewLayout.sizeThatFits, the preview adjusts its size according to its content. When I import a UIKIt view using UIViewRepresentable, it appears with a full device-size frame.
Is there a way to make SwiftUI respect the intrinsicContentSize of UIView subclasses?
struct Label: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<Label>) -> UILabel {
return UILabel()
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<Label>) {
uiView.text = "Some text"
}
}
#if DEBUG
struct Label_Previews: PreviewProvider {
static var previews: some View {
Group {
Label().previewLayout(.sizeThatFits)
}
}
}
#endif
Add the following to your updateUIView function:
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
You can also limit the UIViewRepresentable size from the SwiftUI side.
For this you can use fixedSize:
struct Label_Previews: PreviewProvider {
static var previews: some View {
Label()
.fixedSize()
.previewLayout(.sizeThatFits)
}
}
You can also fix the view size in one dimension only:
.fixedSize(horizontal: false, vertical: true)

Is there any way to use storyboard and SwiftUI in same iOS Xcode project?

As Swift 5 introduces the SwiftUI framework for creating the views, but we are currently using the storyboard for UI design.
So I just wanted to know the procedure to use Storyboard and Swift UI in same iOS Single View Application.
I just started to look at the SwiftUI. Sharing a small example.
In the storyboard add Hosting View Controller
Subclass the UIHostingController with your own class (ChildHostingController)
ChildHostingController should look something like that:
import UIKit
import SwiftUI
struct SecondView: View {
var body: some View {
VStack {
Text("Second View").font(.system(size: 36))
Text("Loaded by SecondView").font(.system(size: 14))
}
}
}
class ChildHostingController: UIHostingController<SecondView> {
required init?(coder: NSCoder) {
super.init(coder: coder,rootView: SecondView());
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
For more details have a look at Custom UIHostingController
Apple Docs UIhostingController (Unfortunatelly it hasn't been documented yet)
Integrating SwiftUI Video
Mixing UIKit with SwiftUI
Both can be embedded and mixed SwiftUI in Storyboards and the other way around
UIViewControllerRepresentable used for embedding Storyboards in SwiftUI
Create the ViewController on a Storyboard and give it a Storyboard ID
import SwiftUI
struct ContentView: View {
var body: some View {
StoryboardViewController()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct StoryboardViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
let storyboard = UIStoryboard(name: "Storyboard", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier: "Main")
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Communication between UIKit and SwiftUI can be through #Binding variable. This will let SwiftUI view inject variable state and control the it, func updateUIViewController will be called to do the work when changed.
UIViewRepresentable used same way for views
import SwiftUI
struct ContentView: View {
#State var color = UIColor.green
var body: some View {
SampleView(color: $color).frame(width: 100, height: 100
, alignment: .center)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct SampleView: UIViewRepresentable {
#Binding var color: UIColor
func makeUIView(context: Context) -> some UIView {
return UIView()
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.backgroundColor = color
}
}
UIHostingController used for embedding SwiftUI in UIKit
First is to be created programmatically
let childViewController = UIHostingController(rootView: SwiftUIContentView())
addChild(childViewController)
childViewController.view.frame = frame
view.addSubview(childViewController.view)
childViewController.didMove(toParent: self)
Second with Xcode 11 by adding Hosting View Controller to the storyboard and create a segue to it and then create an #IBSegueAction by Control-drag from the segue to your ViewController and then create an instance of the SwiftUI view and pass it to HostingViewController initializer
Third is by adding Hosting View Controller to the storyboard and then subclassing it as mentioned in the previous answer above https://stackoverflow.com/a/58250271/3033056
Yes you can do that! Here are the steps you can take to do so:
Go to your current Xcode project -> Storyboard, click on the + sign (right upper corner) and search for Hosting Controller (just like you would for a button or label).
Drag Hosting Controller to your Storyboard. Create a Segue connection from your UI element (I'm using a button) to that Hosting Controller and select Push.
Create an outlet connection from that Segue to your View Controller (it's a new feature - just like you would create an outlet for a Label), and name it.
Declare your view inside of this outlet connection (you can do that, don't have to use PrepareForSegue method), and return it.
For example: I created a SwiftUI view in my current project (in Xcode: File -> New -> File -> SwiftUI View) and called it DetailsView. My outlet connection would look like this:
import UIKit
import SwiftUI
class ViewController: UIViewController {
#IBSegueAction func showDetails(_ coder: NSCoder) -> UIViewController? {
let detailsView = DetailsView()
return UIHostingController(coder: coder, rootView: detailsView)
}
override func viewDidLoad() {
super.viewDidLoad()
// some code
}
}
That's it! Now run it.
as Edgell Mentioned, there is a new ViewController named HostViewController that can host a SwiftUI page inside it.
there's a complete talk about integrating SwiftUI in existing project at WWDC that answers your question very well.
Integrating SwiftUI:
https://developer.apple.com/videos/play/wwdc2019/231/
WWDC19:
https://developer.apple.com/videos/
Add SwiftUI to a UIKit project:
1) Display a complete screen from SwiftUI:
If this is your case, you need just to follow this article or check one of the above answers.
2) Add SwiftUI view inside UIViewController:
In this case, follow the following steps:
Add this extension to your project:
This extension is copy pasted from a scale project in production:
import UIKit
import SwiftUI
extension UIViewController {
/// component: View created by SwiftUI
/// targetView: The UIView that will host the component
func host(component: AnyView, into targetView: UIView) {
let controller = UIHostingController(rootView: component)
self.addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
targetView.addSubview(controller.view)
controller.didMove(toParent: self)
NSLayoutConstraint.activate([
controller.view.widthAnchor.constraint(equalTo: targetView.widthAnchor, multiplier: 1),
controller.view.heightAnchor.constraint(equalTo: targetView.heightAnchor, multiplier: 1),
controller.view.centerXAnchor.constraint(equalTo: targetView.centerXAnchor),
controller.view.centerYAnchor.constraint(equalTo: targetView.centerYAnchor)
])
}
}
Use the extension inside your viewDidLoad:
import UIKit
import SwiftUI
class ViewController: UIViewController {
// MARK: - Outlets
// This view should have the needed constraints
// bc we are going to magically convert it to your `SwiftUI` view!
#IBOutlet weak var myUIView: UIView!
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Create instance from your `SwiftUI` view
let mySwiftUIView = SomeAmazingView()
// Use the extension
self.host(component: AnyView(mySwiftUIView), into: myUIView)
}
}
That's all, good luck!
You need to add a "Host View Controller" to the Storyboard. The SwiftUI form will be displayed in the Host View Controller and is callable from any Storyboard Form.
Be advised, the Host View Controller does not display in the Library for Xcode 11 on Mohave, you must be on Catalina. This is a bug, because once you have a project with a Host View Controller created on Catalina, that same project will run fine on Mohave, in fact, you can even copy that Host View Controller to other Storyboards and it will wok fine.

Resources