I'm having a hard time to understand how NSViewRepresentable interacts with a SwiftUI view.
For example, in the example below, how can I load a different URL when the button in the view is clicked?
import Cocoa
import SwiftUI
import WebKit
import PlaygroundSupport
struct InternalBrowser: NSViewRepresentable {
func makeNSView(context: Context) -> WKWebView {
let browser = WKWebView()
browser.load(URLRequest(url: URL(string: "https://www.google.com")!))
return browser
}
func updateNSView(_ nsView: WKWebView, context: Context) {
}
}
struct Browser: View {
var body: some View {
GeometryReader { g in
VStack {
InternalBrowser().frame(width: 400, height: 400)
Button(action: {
// how to tell InternalBrowser to load an URL here?
}) {
Text("Load Different URL")
}
}
}
}
}
PlaygroundPage.current.setLiveView(Browser().frame(width: 500, height: 500))
how can I load a different URL when the button in the view is clicked?
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4
struct InternalBrowser: NSViewRepresentable {
#Binding var url: URL?
func makeNSView(context: Context) -> WKWebView {
return WKWebView()
}
func updateNSView(_ browser: WKWebView, context: Context) {
if let url = self.url, url != browser.url {
browser.load(URLRequest(url: url))
}
}
}
struct Browser: View {
#State var resourceURL = URL(string: "https://www.google.com")
var body: some View {
VStack {
Button(action: {
self.resourceURL = URL(string: "https://www.stackoverflow.com")
}) {
Text("Load Different URL")
}
Divider()
InternalBrowser(url: self.$resourceURL).frame(width: 400, height: 400)
}
}
}
Related
I have tried to get the NSContactPicker to display a picker window in SwiftUI on macOS. Here is my code. If you click on the button nothing happens. What am I missing?
import SwiftUI
import Contacts
import ContactsUI
let d = MyContactPicker()
class MyContactPicker: NSObject, CNContactPickerDelegate
{
var contactName: String = "No user selected"
func pickContact()
{
let contactPicker = CNContactPicker()
contactPicker.delegate = self
}
func contactPicker(_ picker: CNContactPicker, didSelect contact: CNContact)
{
contactName = contact.givenName
}
}
struct ContentView: View
{
#State var contact: CNContact?
var picker = MyContactPicker()
var body: some View
{
VStack
{
Text(picker.contactName)
Button("Select Contact")
{
picker.pickContact()
}
}
}
}
Here's a possible starting point using NSViewRepresentable and an NSView subclass
class NSContactPickerView: NSView, CNContactPickerDelegate {
let didSelectContact: (CNContact) -> Void
init(didSelectContact: #escaping (CNContact) -> Void) {
self.didSelectContact = didSelectContact
super.init(frame: .zero)
Task {
showPicker()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showPicker() {
let picker = CNContactPicker()
picker.delegate = self
picker.showRelative(to: .zero, of: self, preferredEdge: .maxY)
}
func contactPicker(_ picker: CNContactPicker, didSelect contact: CNContact) {
didSelectContact(contact)
}
}
struct ContactPicker: NSViewRepresentable {
let didSelectContact: (CNContact) -> Void
func makeNSView(context: Context) -> NSContactPickerView {
NSContactPickerView(didSelectContact: didSelectContact)
}
func updateNSView(_ nsView: NSContactPickerView, context: Context) {
}
}
struct ContentView: View {
#State var contact: CNContact?
#State private var showPicker = false
var body: some View {
VStack {
Text(contact?.givenName ?? "")
Button("Select Contact") {
showPicker = true
}
}
.sheet(isPresented: $showPicker) {
ContactPicker { contact in
self.contact = contact
}
.frame(width: 1, height: 1)
}
}
}
It works, but it's not very elegant. Maybe someone else can improve on this.
I'm writing a Webapp in swiftUI with xcode13.1 I've a question "How can I add a progress bar that shows me the loading of page when user click on back button?
import SwiftUI
import WebKit
struct WebViewForCommunityTab : UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
init(web: WKWebView?, req: URLRequest) {
self.webview = WKWebView()
self.request = req
}
func makeUIView(context: Context) -> WKWebView {
return webview!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func goBack(){
webview?.goBack()
}
func goForward(){
webview?.goForward()
}
func goHomeScreen(){
webview?.load(URLRequest(url: (URL(string: DefaultUrlString.CommunityWebLink)!)))
}
}
struct WebContentView: View {
let webview = WebViewForHomeTab(web: nil, req: URLRequest(url: URL(string: DefaultUrlString)!))
var body: some View {
ZStack {
VStack {
VStack (spacing: 80) {
webview
}
HStack (alignment: .center) {
Group {
Spacer()
CustomButtonForWebView(btnImageTitle: ArrowLeftCircleImg, action: {
self.webview.goBack()
})
CustomButtonForWebView(btnImageTitle: ArrowRightCircleImg, action: {
self.webview.goForward()
})
Spacer()
}.padding(.bottom,10)
}.frame(width: UIScreen.screenWidth, height: 40, alignment: .center)
}.hiddenNavigationBarStyle().background(Color.white)
}
}
}
Question: How to get Progress in the WKWebView when user click on back button?
Can someone please explain to me How to get Progress?
Any help would be greatly appreciated.
Thanks in advance.
I have this little sample App which creates multiple Windows of my SwiftUI MacOS app.
Is it possible:
to have a list of all open windows in MainView?
to close a single window from MainView?
to send Message to a single window from MainView?
#main
struct MultiWindowApp: App {
#State var gvm = GlobalViewModel()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(gvm)
}
WindowGroup("Secondary") {
SecondaryView(bgColor: .blue)
.environmentObject(gvm)
}
.handlesExternalEvents(matching: Set(arrayLiteral: "*"))
}
}
struct MainView: View {
#Environment(\.openURL) var openURL
#EnvironmentObject var vm : GlobalViewModel
var body: some View {
VStack {
Text("MainView")
Button("Open Secondary") {
if let url = URL(string: "OpenNewWindowApp://bla") {
openURL(url)
}
//List of all open Windows
// Button to close a single window
// Button to set color of a single window to red
}
}
.padding()
}
}
struct SecondaryView: View {
var bgColor : Color
#EnvironmentObject var vm : GlobalViewModel
var body: some View {
VStack{
Spacer()
Text("Viewer")
Text("ViewModel: \(vm.name)")
Button("Set VM"){
vm.name = "Tom"
}
Spacer()
}
.background(bgColor)
.frame(minWidth: 300, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center )
}
}
class GlobalViewModel :ObservableObject {
#Published var name = "Frank"
}
It is possible that there's a more SwiftUI-centric way to do this. If there's not yet, I certainly hope Apple adds some better window management stuff for the Mac side of things -- right now, everything seems like a bit of a hack.
Here's what I came up with:
#main
struct MultiWindowApp: App {
#State var gvm = GlobalViewModel()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(gvm)
}
WindowGroup("Secondary") {
SecondaryView(bgColor: .blue)
.environmentObject(gvm)
}
.handlesExternalEvents(matching: Set(arrayLiteral: "*"))
}
}
struct MainView: View {
#Environment(\.openURL) var openURL
#EnvironmentObject var vm : GlobalViewModel
var body: some View {
VStack {
Text("MainView")
List {
ForEach(Array(vm.windows), id: \.windowNumber) { window in
HStack {
Text("Window: \(window.windowNumber)")
Button("Red") {
vm.setColor(.red, forWindowNumber: window.windowNumber)
}
Button("Close") {
window.close()
}
}
}
}
Button("Open Secondary") {
if let url = URL(string: "OpenNewWindowApp://bla") {
openURL(url)
}
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct SecondaryView: View {
var bgColor : Color
#EnvironmentObject var vm : GlobalViewModel
#State private var windowNumber = -1
var body: some View {
VStack{
HostingWindowFinder { window in
if let window = window {
vm.addWindow(window: window)
self.windowNumber = window.windowNumber
}
}
Spacer()
Text("Viewer")
Text("ViewModel: \(vm.name)")
Button("Set VM"){
vm.name = "Tom"
}
Spacer()
}
.background(vm.backgroundColors[windowNumber] ?? bgColor)
.frame(minWidth: 300, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center )
}
}
class GlobalViewModel : NSObject, ObservableObject {
#Published var name = "Frank"
#Published var windows = Set<NSWindow>()
#Published var backgroundColors : [Int:Color] = [:]
func addWindow(window: NSWindow) {
window.delegate = self
windows.insert(window)
}
func setColor(_ color: Color, forWindowNumber windowNumber: Int) {
backgroundColors[windowNumber] = color
}
}
extension GlobalViewModel : NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
if let window = notification.object as? NSWindow {
windows = windows.filter { $0.windowNumber != window.windowNumber }
}
}
}
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) {}
}
I'm using a trick from https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/ to get a reference to the NSWindow. That gets stored in the view model in a set. Later, to access things like closing the windows, etc. I reference the windows by windowNumber.
When a window appears, it adds itself to the view model's window list. Then, when the view model gets a windowWillClose call as the delegate, it removes it from the list.
Setting the background color is done via the backgroundColors property on the view model. If there's not one set, it uses the passed-in background color property. There are tons of different ways you could choose to architect this bit.
I am trying to add the back and forward button in the swiftui but the buttons do not work. The URL comes from a firebase.
I followed this question How to access goBack and goForward via UIViewRepresentable. If anyone help me, I am very thankful to you.
Here is the source code
import SwiftUI
import WebKit
import Firebase
struct WebView: View {
#State var websiteURL: String
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
ZStack{
Color.init(red: 172/255, green: 198/255, blue: 224/255).edgesIgnoringSafeArea(.all)
VStack{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image("back").renderingMode(.original).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 60, alignment: .leading).padding()
}
Webview(web: nil, req: URLRequest(url: URL(string: websiteURL)!))
HStack{
Button(action: {
//go to back
Webview(web: nil, req: URLRequest(url: URL(string: websiteURL)!)).goBack()
}) {
Image(systemName: "arrow.left").foregroundColor(.black)
}
Spacer()
Button(action: {
// go forwared
Webview(web: nil, req: URLRequest(url: URL(string: websiteURL)!)).goForward()
}) {
Image(systemName: "arrow.right").foregroundColor(.black)
}
}.padding([.leading,.trailing],20)
}.navigationBarTitle("")
.navigationBarHidden(true)
}.onAppear{
self.loadURL()
}
}
func loadURL(){
//For Admin
let rootRef = Database.database().reference().child("Admin").child(websiteURL)
rootRef.observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let getURL:String = value?["value"] as? String ?? ""
self.websiteURL=getURL
// ...
}) { (error) in
print(error.localizedDescription)
}
}
}
struct Webview : UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
init(web: WKWebView?, req: URLRequest) {
self.webview = WKWebView()
self.request = req
}
func makeUIView(context: Context) -> WKWebView {
return webview!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func goBack(){
webview?.goBack()
}
func goForward(){
webview?.goForward()
}
}
struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebView(websiteURL: "")
}
}
I stand by my comment, you create 3 different Webview. This is one way to make it work.
Copy and paste this code and let me know if that works for you.
import SwiftUI
import WebKit
import Firebase
struct WebView: View {
#State var websiteURL: String
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
let websiteView = Webview()
var body: some View {
ZStack{
Color.init(red: 172/255, green: 198/255, blue: 224/255).edgesIgnoringSafeArea(.all)
VStack{
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image("back").renderingMode(.original).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 60, alignment: .leading).padding()
}
self.websiteView
HStack{
Button(action: {
// go to back
self.websiteView.webview.goBack()
}) {
Image(systemName: "arrow.left").foregroundColor(.black)
}
Spacer()
Button(action: {
// go forwared
self.websiteView.webview.goForward()
}) {
Image(systemName: "arrow.right").foregroundColor(.black)
}
}.padding([.leading,.trailing],20)
}.navigationBarTitle("")
.navigationBarHidden(true)
}.onAppear{
self.loadURL()
}
}
func loadURL() {
//For Admin
let rootRef = Database.database().reference().child("Admin").child(websiteURL)
rootRef.observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let getURL:String = value?["value"] as? String ?? ""
self.websiteURL = getURL
// new code <-----
self.websiteView.loadRequest(request: URLRequest(url: URL(string: self.websiteURL)!))
// ...
}) { (error) in
print(error.localizedDescription)
}
}
}
struct Webview : UIViewRepresentable {
#State var request: URLRequest?
let webview = WKWebView()
func makeUIView(context: Context) -> WKWebView {
return webview
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if request != nil { uiView.load(request!) }
}
func loadRequest(request: URLRequest) {
self.request = request
webview.load(request)
}
func goBack(){
webview.goBack()
}
func goForward(){
webview.goForward()
}
}
Because the ScrollView does not provide a function to set the contentOffset, I'm trying to use the UIScrollView as UIViewRepresentable. The attached code shows both, the caller and the definition of the view and the view controller.
When running the code in simulator or previews, just a blue area is shown. When debugging the display, the Text is shown, as expected.
If have no idea about the reason - is it because I'm doing something wrong, or because there's a bug in Xcode or SwiftUI?
Here the custom scroll view:
struct PositionableScrollView<Content>: UIViewRepresentable where Content: View {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: UIViewRepresentableContext<PositionableScrollView<Content>>) -> UIScrollView {
let scrollViewVC = PositionableScrollViewVC<Content>(nibName: nil, bundle: nil)
scrollViewVC.add(content: content)
let control = scrollViewVC.scrollView
return control
}
func updateUIView(_ uiView: UIScrollView, context: UIViewRepresentableContext<PositionableScrollView<Content>>) {
// Do nothing at the moment.
}
}
The view controller:
final class PositionableScrollViewVC<Content>: UIViewController where Content: View {
var scrollView: UIScrollView = UIScrollView()
var contentView: UIView!
var contentVC: UIViewController!
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup() {
self.view.addSubview(self.scrollView)
self.scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
self.scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
self.scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
self.scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
}
override func viewDidLoad() {
super.viewDidLoad()
debugPrint("self:", self.frame())
debugPrint("self.view:", self.view!.frame)
debugPrint("self.view.subviews:", self.view.subviews)
// debugPrint("self.view.subviews[0]:", self.view.subviews[0])
// debugPrint("self.view.subviews[0].subviews:", self.view.subviews[0].subviews)
}
func add(#ViewBuilder content: #escaping () -> Content) {
self.contentVC = UIHostingController(rootView: content())
self.contentView = self.contentVC.view!
self.scrollView.addSubview(contentView)
self.contentView.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor).isActive = true
self.contentView.trailingAnchor.constraint(equalTo: self.scrollView.trailingAnchor).isActive = true
self.contentView.topAnchor.constraint(equalTo: self.scrollView.topAnchor).isActive = true
self.contentView.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor).isActive = true
self.contentView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.widthAnchor.constraint(greaterThanOrEqualTo: self.scrollView.widthAnchor).isActive = true
}
}
extension PositionableScrollViewVC: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<PositionableScrollViewVC>) -> PositionableScrollViewVC {
let vc = PositionableScrollViewVC()
return vc
}
func updateUIViewController(_ uiViewController: PositionableScrollViewVC, context: UIViewControllerRepresentableContext<PositionableScrollViewVC>) {
// Do nothing at the moment.
}
}
The callers:
struct TimelineView: View {
#State private var posX: CGFloat = 0
var body: some View {
GeometryReader { geo in
VStack {
Text("\(self.posX) || \(geo.frame(in: .global).width)")
PositionableScrollView() {
VStack {
Spacer()
Text("Hallo")
.background(Color.yellow)
Spacer()
}
.frame(width: 1000, height: 200, alignment: .bottomLeading)
}
.background(Color.blue)
}
}
}
}
struct TimelineView_Previews: PreviewProvider {
static var previews: some View {
TimelineView()
}
}
The display, when run, and in debugger: