How to add buttons to wkWebView? - swift3

Because we make wkWebView as the root view, the button I added on my layout doesn't appear on screen. Then I tried to add wkWebView as a subview of the root view, but the app crashes. Then I found about UIBarButtonItem, but it seems that you could only create 2 buttons.
I need to add 5 buttons anywhere on the screen. Is there a workaround for this? I'm a newbie on ios development and I'm not that good at using the interface builder.
After trying #Matic Oblak's code and placing it on loadView() where I do my wkwebview initialization, I'm getting an error saying "Warning: Attempt to present SomeViewController on WebViewController whose view is not in the window hierarchy!
This is my current code.
override func loadView() {
// let contentController: WKUserContentController = WKUserContentController()
// let config: WKWebViewConfiguration = WKWebViewConfiguration()
//
// contentController.add(self, name: "callbackHandler")
// config.userContentController = contentController
//
// webView = WKWebView(
// frame: UIScreen.main.bounds,
// configuration: config
// )
//webView.navigationDelegate = self
//webView.translatesAutoresizingMaskIntoConstraints = false
//webView.scrollView.isScrollEnabled = false
//view = webView
if let webViewContainer = webviewContainer {
// Initialize:
let contentController: WKUserContentController = WKUserContentController()
let config: WKWebViewConfiguration = WKWebViewConfiguration()
contentController.add(self, name: "callbackHandler")
config.userContentController = contentController
let webView = WKWebView(frame: webViewContainer.bounds, configuration: WKWebViewConfiguration()) // Create a new web view
webView.translatesAutoresizingMaskIntoConstraints = false // This needs to be called due to manually adding constraints
webView.navigationDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
webView.scrollView.isScrollEnabled = false
// Add as a subview:
webViewContainer.addSubview(webView)
// Add constraints to be the same as webViewContainer
webViewContainer.addConstraint(NSLayoutConstraint(item: webView, attribute: .leading, relatedBy: .equal, toItem: webViewContainer, attribute: .leading, multiplier: 1.0, constant: 0.0))
webViewContainer.addConstraint(NSLayoutConstraint(item: webView, attribute: .trailing, relatedBy: .equal, toItem: webViewContainer, attribute: .trailing, multiplier: 1.0, constant: 0.0))
webViewContainer.addConstraint(NSLayoutConstraint(item: webView, attribute: .top, relatedBy: .equal, toItem: webViewContainer, attribute: .top, multiplier: 1.0, constant: 0.0))
webViewContainer.addConstraint(NSLayoutConstraint(item: webView, attribute: .bottom, relatedBy: .equal, toItem: webViewContainer, attribute: .bottom, multiplier: 1.0, constant: 0.0))
// Assign web view for reference
self.webView = webView
}
}
override func viewDidLoad() {
if(someVar1.isEmpty && someVar2.isEmpty){
showSomeDialog()
}
}
The first commented lines on loadView() are my previous code for reference.
After placing #Matic Oblak's code on viewDidLoad(), I'm getting a black screen. Also, after placing the code that calls SomeViewController on viewDidAppear(), the error is gone but I'm still getting the black screen.

Actually WKWebView has a bit of a nasty bug that it crashes when added in storyboard. But you can do that in code. What I usually do is create a view in storyboard just as a web view panel (usually full screen but not if you have some tool bars) then just add a new web view as a subview. I still use constraints to add the web view:
import UIKit
import WebKit
class WebViewController: UIViewController {
#IBOutlet private var webViewContainer: UIView? // A view on which a web view will be added to
private var webView: WKWebView? // Web view manually added
override func viewDidLoad() {
super.viewDidLoad()
if let webViewContainer = webViewContainer {
// Initialize:
let webView = WKWebView(frame: webViewContainer.bounds, configuration: WKWebViewConfiguration()) // Create a new web view
webView.translatesAutoresizingMaskIntoConstraints = false // This needs to be called due to manually adding constraints
// Add as a subview:
webViewContainer.addSubview(webView)
// Add constraints to be the same as webViewContainer
webViewContainer.addConstraint(NSLayoutConstraint(item: webView, attribute: .leading, relatedBy: .equal, toItem: webViewContainer, attribute: .leading, multiplier: 1.0, constant: 0.0))
webViewContainer.addConstraint(NSLayoutConstraint(item: webView, attribute: .trailing, relatedBy: .equal, toItem: webViewContainer, attribute: .trailing, multiplier: 1.0, constant: 0.0))
webViewContainer.addConstraint(NSLayoutConstraint(item: webView, attribute: .top, relatedBy: .equal, toItem: webViewContainer, attribute: .top, multiplier: 1.0, constant: 0.0))
webViewContainer.addConstraint(NSLayoutConstraint(item: webView, attribute: .bottom, relatedBy: .equal, toItem: webViewContainer, attribute: .bottom, multiplier: 1.0, constant: 0.0))
// Assign web view for reference
self.webView = webView
}
}
}
So here webViewContainer is dragged from storyboard as any view you want. It can even be the main view of the view controller if you want to.
The rest should be explained in the comments.
So using this procedure you don't need to "hack" adding subviews on top of everything. You can add your button normally in your storyboard.
Note: The problem with using webViewContainer as main view is that web view will be added later than the rest of views so it may cover those you added in storyboard. You need to either send the web view to back or bring overlaying items to front. It is just adding webViewContainer.sendSubviewToBack(webView) but I still prefer to use a separate view just for web view.

You could add floating button on top of your viewcontroller by using some third party library. Let's say using Floaty or with any other similar.

Related

GeometryReader sometimes gives wrong .global frame origin for Map annotations

Test setup:
I have a View that displays modally a Map with annotations:
struct Annotation: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
struct MapViewSWUI: View {
#Binding var show_map_modal: Bool
#State var region: MKCoordinateRegion
var body: some View {
let _ = Self._printChanges()
VStack {
ZStack { // Top bar
Text("Oo")
.bold()
HStack {
Button(action: {
show_map_modal = false
}) {
HStack(spacing: 10) {
Image(systemName: "chevron.left")
Text(NSLocalizedString("BACK", comment: " "))
}
.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0))
}
Spacer()
}
}
let annotations = [Annotation(name: "0",
coordinate: CLLocationCoordinate2D(latitude: 49.450534, longitude: 8.6694930)),
Annotation(name: "1",
coordinate: CLLocationCoordinate2D(latitude: 49.450701, longitude: 8.6677427))
]
Map(coordinateRegion: $region,
interactionModes: .all,
showsUserLocation: false,
annotationItems: annotations) { annotation in
MapAnnotation(coordinate: annotation.coordinate, anchorPoint: CGPoint(x: 0.5, y: 1)) {
GeometryReader { geo in
Color(.green)
.onAppear {
print("Annotation coordinate: (\(annotation.coordinate.latitude),\(annotation.coordinate.longitude))")
print(".global origin: \(geo.frame(in: .global).origin)")
}
}
.frame(width: 30, height: 30)
}
}
} // VStack
}
}
When show_map_modal is set to true, the map is shown with the test annotations:
Problem:
When the map is shown the following is normally logged:
MapViewSWUI: #self, #identity, _region, _annotationDragged changed.
2023-01-07 09:19:17.254324+0100 ShopEasy![47736:11270478] Metal API Validation Enabled
MapViewSWUI: _region changed.
Annotation coordinate: (49.450534,8.669493)
.global origin: (271.94092664926427, 438.1697202246186)
Annotation coordinate: (49.450701,8.6677427)
.global origin: (91.00613205620596, 411.6026828329208)
However it may also happen that the following is logged:
MapViewSWUI: #self, #identity, _region, _annotationDragged changed.
MapViewSWUI: _region changed.
Annotation coordinate: (49.450701,8.6677427)
.global origin: (-15.0, 76.66666666666666)
Annotation coordinate: (49.450534,8.669493)
.global origin: (-15.0, 76.66666666666666)
When I dismiss the modally shown map using the back button ("Zurück"), and display the map again, the same happens (one of the logs above).
Workarounds tried:
When, within GeometryReader .onAppear, I embed the print statements in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // … }, the .global frame coordinates seem always to be correct. But this seems to be a hack.
When I replace .onAppear by .onTapGesture, the .global frame coordinates also seem always to be correct. Unfortunately, I need the coordinates when the annotations appear on the screen, without further user interaction.
Questions:
How is it possible that the annotations (green square) are correctly placed on the map, but the GeometryReader gives sometimes the correct .global screen coordinates, but sometimes another strange value (-15.0, 76.66666666666666) that is, if it is given, always the same? Is this a SwiftUI bug?
Is something wrong with my code? And if so, how to do it right?

Tap gesture not working after constraints added

My code works perfectly fine until constraints are added.Once constraints are added tap gesture doesn't seem to work.
My code is as below:
//this is in viewDidLoad()
containerView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(containerView)
self.view.addSubview(scrollView)
if #available(iOS 11.0, *) {
let top = NSLayoutConstraint.init(item: containerView, attribute: .top, relatedBy: .equal, toItem: self.view.safeAreaLayoutGuide, attribute: .top, multiplier: 1.0, constant: 0.0)
let leading = NSLayoutConstraint.init(item: containerView, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1.0, constant: 0.0)
let trailing = NSLayoutConstraint.init(item: containerView, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1.0, constant: 0.0)
let height = NSLayoutConstraint.init(item: containerView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 60.0)
NSLayoutConstraint.activate([top, leading, trailing, height])
} else {
// Fallback on earlier versions
}
self.automaticallyAdjustsScrollViewInsets = false
//This is inside a function where I create subviews
containerView.addSubview(DiaryList)
let tapGesture = AddMyTapGesture(target: self, action: #selector(handleTap))
tapGesture.title = date
tapGesture.titleFormat = dateFormat
DiaryList.addGestureRecognizer(tapGesture)
DiaryList.isUserInteractionEnabled = true
//this is the tap gesture
#objc func handleTap(sender: AddMyTapGesture) {
print("tapped")
}

Xcode ViewController elements is not interactive after animation

I have a project that need to have a top sheet, that when user scrolling down it will reveal. so I created 2 separate ViewController in storyboard like this:
in the main ViewController I add the sheet view controller as a subview like this:
let topSheet = self.storyboard?.instantiateViewController(withIdentifier: "top_sheet_vc") as! TopSheetVC
self.addChildViewController(topSheet)
self.view.addSubview(topSheet.view)
topSheet.didMove(toParentViewController: self)
topSheet.view.translatesAutoresizingMaskIntoConstraints = false
let height = NSLayoutConstraint(item: topSheet.view, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: Constant.TOP_SHEET_HEIGHT)
let leading = NSLayoutConstraint(item: topSheet.view, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1.0, constant: 0)
let trailing = NSLayoutConstraint(item: topSheet.view, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1.0, constant: 0)
let bottom = NSLayoutConstraint(item: topSheet.view, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1.0, constant: 0)
NSLayoutConstraint.activate([height, leading, trailing, bottom])
Then I have a UIPanGestureRecognizer that attached to the main view of main view controller, and in the handler method I can move the view so the top sheet can be revealed, like this:
func panGestureHappened(recognizer: UIPanGestureRecognizer) {
let y = self.view.frame.minY
let translation = recognizer.translation(in: self.view)
let min = fmin(y + translation.y, Constant.TOP_SHEET_HEIGHT)
self.view.transform = CGAffineTransform(translationX: 0, y: min)
// or I can change the frame, same result as CGAffineTransform
// self.view.frame = CGRect(x: CGFloat(0), y: min, width: self.view.frame.width, height: self.view.frame.height)
}
so far so good, the top sheet reveals and become visible and the main view is shifting down as I expected.
The Problem is that the top sheet is not interactive and clickable and I can't figure out what is wrong with this.
so any help or tip would be appreciated.
tnx in advance.
NOTE: If I animate the TopSheet not the base view, so the top sheet overlap the base view, and not shifting down the base view the problem will be gone and TopSheet is clickable. But I can't figure out why.
UPDATE: I eliminate all the custom constraints and added a ContainerView in the main controller inside storyboard, and the container has negative value for top to bring it outside, and when the main view get animated and top is revealed But it is still not clickable, BUT if I overlay the container it became clickable again, so there is something related to view height, any idea?

Set the center of custom MKAnnotationView

I am using the custom MKAnnotationView. I use the autolayouts which are working fine, but the position(frame) of annotation view is not appropriate.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)
{
if view.annotation is MKUserLocation
{
return
}
let starbucksAnnotation = view.annotation as! LiquorLocation
let views = Bundle.main.loadNibNamed("CustomCalloutView", owner: nil, options: nil)
let calloutView = views?[0] as! CustomCalloutView
calloutView.labelLiquorShopName.text = starbucksAnnotation.title
calloutView.labelLiquorShopAddress.text = starbucksAnnotation.address
calloutView.labelLiquorShopAwayDistance.text = starbucksAnnotation.locationKmAway
calloutView.imageViewLiquorShop.image = starbucksAnnotation.liquorShopIcon
calloutView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(calloutView)
mapView.setCenter((view.annotation?.coordinate)!, animated: true)
}
What I am doing wrong? I want to center the annotation view at the top of the pin.
Autolayouts again save my day. I add the layout constraint between my calloutview and view(MKAnnotationView). I write below lines underneath view.addSubview(calloutView) in didSelect method.
let horizontalConstraint = NSLayoutConstraint(item: calloutView, attribute: NSLayoutAttribute.centerX, relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.centerX, multiplier: 1, constant: 0)
let verticalConstraint = NSLayoutConstraint(item: calloutView, attribute: NSLayoutAttribute.bottom, relatedBy: NSLayoutRelation.equal, toItem: view, attribute: NSLayoutAttribute.top, multiplier: 1, constant: 0)
let widthConstraint = NSLayoutConstraint(item: calloutView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: calloutView.frame.size.width)
view.addConstraints([horizontalConstraint, verticalConstraint, widthConstraint])

how can I add auto layout constraints for a line programatically

I drew a line programatically in y UI. How can I add constraints for this line.
let line = UIView(frame: CGRect(x: 10, y: 350, width: 350, height: 1))
line.backgroundColor = UIColor.white;self.view.addSubview(line)
I created a red line: height = 1, top = 50 and the width will be flexible You can modify the position as you want.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let line = UIView()
line.backgroundColor = UIColor.red
view.addSubview(line)
line.translatesAutoresizingMaskIntoConstraints = false
let leadingConstraint = NSLayoutConstraint(item: line, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 20.0)
let topConstraint = NSLayoutConstraint(item: line, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 50.0)
let trailingConstraint = NSLayoutConstraint(item: view, attribute: .right, relatedBy: .equal, toItem: line, attribute: .right, multiplier: 1.0, constant: 20.0)
let heightConstraint = NSLayoutConstraint(item: line, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 1.0)
view.addConstraints([topConstraint, leadingConstraint, trailingConstraint, heightConstraint])
}
The result!
NSLayoutConstraint Style
NSLayoutConstraint(item: line, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
NSLayoutConstraint(item: line, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterY, multiplier: 1, constant: 0).active = true`