How to create tappable url/phone number in SwiftUI - swiftui

I would like to display a phone number in a SwiftUI Text (or any View), and then make it clickable so that it will open the 'Phone'.
Is there a way to do this with SwiftUI, or should I try to wrap a UITextView in SwiftUI and do it the old-fashioned way with NSAttributed string etc?
I've read the documentation for Text in SwiftUI, and couldn't find anything about how to do this. Currently trying to do this in Xcode 11 beta 5.
I've searched 'text' in the SwiftUI API in SwiftUI.h
I've also searched stackoverflow [swiftui] and google with queries like "make phone number/url tappable", "Tappable link/url swiftUI" etc..
Text("123-456-7890")
.onTapGesture {
// do something here
}
(text will be Japanese phone number)

Using iOS 14 / Xcode 12.0 beta 5
Use new link feature in SwiftUI for phone and email links.
// Link that will open Safari on iOS devices
Link("Apple", destination: URL(string: "https://www.apple.com")!)
// Clickable telphone number
Link("(800)555-1212", destination: URL(string: "tel:8005551212")!)
// Clickable Email Address
Link("apple#me.com", destination: URL(string: "mailto:apple#me.com")!)

Try this,
let strNumber = "123-456-7890"
Button(action: {
let tel = "tel://"
let formattedString = tel + strNumber
guard let url = URL(string: formattedString) else { return }
UIApplication.shared.open(url)
}) {
Text("123-456-7890")
}

Thanks to Ashish's answer, I found the necessary code I needed to solve this:
In the action inside of the Button - you need to call this method:
UIApplication.shared.open(url)
to actually make the phone call / open a link in a SwiftUI View.
Of course, I didn't understand how to format my phone number at first, which I found in these answers:
How to use openURL for making a phone call in Swift?
Don't forget to add the 'tel://' to the beginning of your string/format it as URL..
The full code of what worked is
Button(action: {
// validation of phone number not included
let dash = CharacterSet(charactersIn: "-")
let cleanString =
hotel.phoneNumber!.trimmingCharacters(in: dash)
let tel = "tel://"
var formattedString = tel + cleanString
let url: NSURL = URL(string: formattedString)! as NSURL
UIApplication.shared.open(url as URL)
}) {
Text(verbatim: hotel.phoneNumber!)
}

KISS answer:
Button("url") {UIApplication.shared.open(URL(string: "https://google.com")!)}

From iOS 14, Apple provides us a Link view by default. So, you can just use this,
Link("Anyone can learn Swift", destination: URL(string: "https://ohmyswift.com")!)
For the previous versions of iOS, like iOS 13.0, you still have to use
Button("Anyone can learn Swift") {
UIApplication.shared.open(URL(string: "https://ohmyswift.com")!)
}

iOS 15 (beta)
Take advantage of Markdown in SwiftUI, which supports links!
struct ContentView: View {
var body: some View {
Text("Call [123-456-7890](tel:1234567890)")
}
}
Result:

Make it a Button() with an action, not a Text() with a gesture.

Button {
var cleanNum = selectedItem.phoneNum
let charsToRemove: Set<Character> = [" ", "(", ")", "-"] // "+" can stay
cleanNum.removeAll(where: { charsToRemove.contains($0) })
guard let phoneURL = URL(string: "tel://\(cleanNum)") else { return }
UIApplication.shared.open(phoneURL, options: [:], completionHandler: nil)
} label: {
// ...
}

Using iOS 14 / Xcode 12.5
I add this in case you wants to use a view.
To make a call by tapping a View
Link(destination: URL(string: "tel:1234567890")!, label: {
SomeView()
})
To make a call by tapping a String
Link("1234567890", destination: URL(string: "tel:1234567890")!)

You can do this way as well if in case you need to log any event on tap of the link :
struct exampleView: View {
#SwiftUI.Environment(\.openURL) private var openURL
var body: some View {
Button {
if let url = URL(string: "https://www.google.com") {
openURL(url)
}
} label: {
Text("Link")
.foregroundColor(Color("EyrusDarkBlue"))
}
}
}

Related

How to read formatted text of json file in swiftui 14?

I have a json file that includes html styled text:
I need to maintain the included text formatting: bold, italic, underline, etc.
I am using SwiftUI and Xcode 14. I read the json into a SwiftUI list, then navigate to a detail page that shows the description text. I can't find any examples of how to maintain the formatted text that is in the json file. I've googled, looked at Apple Developer documentation and more, but to no avail. Can anyone help me out with this. My application depends on properly formatted text. It seems odd that Apple wouldn't include something. This is easy in html and javascript. What am I missing? I am not a pro programmer and just starting out with SwiftUI. TIA
[
{ "imageurl": "1.png",
"levelLongDesc":"A longer description",
"id": "1.",
"name": "A name)",
"page": "Details",
"description":"<p><b>E pluribus unum</b></p><b>Instructions.</b> Latin for “Out of many one”, is a motto requested by <i>Pierre Eugene du Simitiere</i> (originally Pierre-Eugène Ducimetière) and found in 1776 on the Seal of the United States, along with Annuit cœptis and Novus ordo seclorum, and adopted by an Act of Congress in 1782.</p><p>",
"videoDemo":"myvideo"
}...]
import SwiftUI
struct LandmarkDetail: View {
var landmark: Landmark
#State var Description = AttributedString("")
var body: some View {
NavigationView{
ScrollView {
RectImage(image: landmark.image)
.padding(.top, 30)
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("\(landmark.name)")
Text(landmark.description)
.font(.title3)
.padding(.horizontal, 20)
.onAppear {
(addStyling(landmark.description))
}
} .padding()
}.navigationTitle(landmark.name)
.navigationBarTitleDisplayMode(.inline)
}
private func addStyling(_ htmlString: String) -> NSAttributedString {
var resultString = NSAttributedString()
var data = Data()
// Add the html data/ var in which you stored it
data.append(Data(landmark.description.utf8))
// Convert the attributed String
do {
let attributedString = try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
resultString = attributedString
} catch {
print(error)
}
return resultString
}
}
Above is what my detail view looks like. I don't know where/how to add the attributes. I get errors with everything that I try. Again, thank you to the two of you who tried to help. -R
Assuming your "description" property of your decoded object model, is some simple html text,
you could use AttributedString and NSAttributedString as shown
in this example code:
struct ContentView: View {
#State var description = AttributedString("")
var body: some View {
Text(description)
.onAppear {
let txt = """
<p><b>E pluribus unum</b></p><b>Instructions.</b> Latin for “Out of many one”, is a motto requested by <i>Pierre Eugene du Simitiere</i> (originally Pierre-Eugène Ducimetière) and found in 1776 on the Seal of the United States, along with Annuit cœptis and Novus ordo seclorum, and adopted by an Act of Congress in 1782.</p><p>
"""
description = asAttribTxt(txt)
}
}
func asAttribTxt(_ txt: String) -> AttributedString {
let data = txt.data(using: .utf16)! // <-- adjust to your needs
do {
let nsString = try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
return AttributedString(nsString)
} catch {
print(error)
}
return AttributedString(txt)
}
}
private func addStyling(_ htmlString: String) -> NSAttributedString {
var resultString = NSAttributedString()
var data = Data()
// Add the html data/ var in which you stored it
data.append(Data(htmlString.utf8))
// Convert the attributed String
do {
let attributedString = try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
resultString = attributedString
} catch {
print(error)
}
return resultString
}

SwiftUI How to share a URL to Facebook using FBSDKShareKit and ShareDialog?

I tried looking for a solution in posts such as this and this where people asked this very same question: How to share a url to Facebook using SwiftUI?
I even tried this post where somebody asked how to export a file using SwiftUI, but my problem is specifically with Facebook since I have no problem sharing urls to apps such as Whatsapp.
However, I found no answers...
UIKit
By reading Facebook's Developer documentation, I found a way to share a post using UIKit.
I created a very simple sample project to make sure I understood the topic.
Here is the sample project in UIKit:
import UIKit
import FBSDKShareKit
class ProfileViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func share(_ sender: UIButton) {
shareLink(url: URL(string: "http://www.apple.com")!)
}
func shareLink(url: URL) {
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(
fromViewController: self,
content: content,
delegate: nil
)
dialog.show()
}
}
Luckily, this worked right away.
By making this project work, I confirmed that I wasn't forgetting to link my AppBundleID to Facebook, or forgetting to add my FacebookAppID or any other needed files inside my info.plist.
SwiftUI
I then tried to replicate the same project in SwiftUI to see if I could get it to work.
I used ViewControllerRepresentable to be able to include UIActivityViewController into SwiftUI.
The reason for using UIActivityViewController is because I want the user to choose where they want to share the URL (Whatsapp, Twitter, Facebook, etc).
Here is the code:
ContentView
struct ContentView: View {
#State var showSharingView = false
var body: some View {
Button("Share Link") {
showSharingView.toggle()
}
.sheet(isPresented: $showSharingView) {
ActivityViewController(activityItems: [URL(string: "https://www.apple.com/")!])
}
}
}
UIViewControllerRepresentable
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [URL]
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
if activityType == .postToFacebook {
shareLink(from: activityItems.first!)
}
}
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
func shareLink(from url: URL) {
// controller was created so I would have a UIViewControllerType to put as a parameter for fromViewController in ShareDialog, even though I don't think it fits
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(fromViewController: controller, content: content, delegate: nil)
dialog.show()
}
}
This code doesn't work because of what I assume is the controller variable used when initializing ShareDialog. (self doesn't work either because "XCode cannot convert value of type 'ActivityViewController' to expected argument type 'UIViewController?'")
Question
Facebook Developer's documentation tells me that I need to write the following code in order to share a link:
guard let url = URL(string: "https://developers.facebook.com") else {
// handle and return
}
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(
viewController: self, //<--this has been changed to 'fromViewController'
content: content,
delegate: self
)
dialog.show()
However, SwiftUI doesn't work with ViewControllers. How can I create a ViewController to use as a parameter in fromViewController in ShareDialog in order to successfully share my URL to Facebook?
Use like this,
guard let url = URL(string: "https://developers.facebook.com") else {
// handle and return
}
let content = ShareLinkContent()
content.contentURL = url
let dialog = ShareDialog(
viewController: UIApplication.shared.windows.first!.rootViewController,
content: content,
delegate: UIApplication.shared.windows.first!.rootViewController
)
dialog.show()

SwiftUI GKLeaderboard loadEntries

I would like to add leaderboards to my SwiftUI app.
I can't find any examples of using loadEntries to load leaderboard values.
I tried the following...
let leaderBoard: GKLeaderboard = GKLeaderboard()
leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
leaderBoard.timeScope = .allTime
leaderBoard.loadScores { (scores, error) in ...
This results in the following warnings:
'identifier' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'timeScope' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'loadScores(completionHandler:)' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler:.
using loadEntriesForPlayerScope results in the following warning:
'loadEntriesForPlayerScope(_:timeScope:range:completionHandler:)' has
been renamed to 'loadEntries(for:timeScope:range:completionHandler:)'
Using loadEntries I don't know how to specify the leaderboard identifier.
Here is simple demo of possible approach - put everything in view model and load scores on view appear.
import GameKit
class BoardModel: ObservableObject {
private var board: GKLeaderboard?
#Published var localPlayerScore: GKLeaderboard.Entry?
#Published var topScores: [GKLeaderboard.Entry]?
func load() {
if nil == board {
GKLeaderboard.loadLeaderboards(IDs: ["YOUR_LEADERBOARD_ID_HERE"]) { [weak self] (boards, error) in
self?.board = boards?.first
self?.updateScores()
}
} else {
self.updateScores()
}
}
func updateScores() {
board?.loadEntries(for: .global, timeScope: .allTime, range: NSRange(location: 1, length: 10),
completionHandler: { [weak self] (local, entries, count, error) in
DispatchQueue.main.async {
self?.localPlayerScore = local
self?.topScores = entries
}
})
}
}
struct DemoGameboardview: View {
#StateObject var vm = BoardModel()
var body: some View {
List {
ForEach(vm.topScores ?? [], id: \.self) { item in
HStack {
Text(item.player.displayName)
Spacer()
Text(item.formattedScore)
}
}
}
.onAppear {
vm.load()
}
}
}
I might be stating the obvious but have you looked at the WWDC20 videos?
Usually when there are big changes like this they cover it during WWDC that year.
Tap into Game Center: Leaderboards, Achievements, and Multiplayer
Tap into Game Center: Dashboard, Access Point, and Profile
I haven't looked at the videos but the documentation eludes that identifier might be replaced by var baseLeaderboardID: String

What is the best way to add ads to a SwiftUI Grid?

Hello I want to add ads to a swiftUI grid. The grid contains pictures that I get from a firebase backend and after every couple of pictures I would like to have an ad.
I am quite new to both SwiftUi and working with ads, so I'm not sure how correct my code is, but here is what I got so far.
// Code for the pictures Grid
struct PicturesGrid: View {
private let data: [Item]
var body: some View {
let gridItems = [GridItem(.fixed(UIScreen.screenWidth / 2),
alignment: .leading),
GridItem(.fixed(UIScreen.screenWidth / 2),
alignment: .leading)]
return ScrollView(showsIndicators: false) {
LazyVGrid(columns: gridItems) {
ForEach(0..<self.data.count, id: \.self) { index in
// Using this workaround for the ad to be on the whole width of the screen
// Also, after every six images I am adding and ad
if index != 0, index % 6 == 0 {
AdView()
.frame(width: UIScreen.screenWidth, height: 280)
.padding(.top, 20)
Spacer()
item
.frame(width: UIScreen.screenWidth / 2)
} else {
item
.frame(width: UIScreen.screenWidth / 2)
}
}
}
}
}
// this is for the picture
var item: some View {
NavigationLink(destination: DetailView(viewModel: DetailViewModel(item: itemAtIndexPath))) {
Cell(viewModel: CellViewModel(item: itemAtIndexPath))
}
.buttonStyle(PlainButtonStyle())
}
}
This is the code that I am currently using to load, create and display an ad
// Code for the ad that I am currently using
struct AdView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let adController = AdViewController(self)
return adController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
class AdViewController: UIViewController {
private var adView: AdView
/// The height constraint applied to the ad view, where necessary.
var heightConstraint: NSLayoutConstraint?
/// The ad loader. You must keep a strong reference to the GADAdLoader during the ad loading
/// process.
var adLoader: GADAdLoader!
/// The native ad view that is being presented.
var nativeAdView: GADUnifiedNativeAdView!
/// The ad unit ID.
let adUnitID = "ca-app-pub-3940256099942544/3986624511"
init(_ adView: AdView) {
self.adView = adView
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
var nibView: Any?
nibView = Bundle.main.loadNibNamed("ListAdView", owner: nil, options: nil)?.first
guard let nativeAdView = nibView as? GADUnifiedNativeAdView else {
return
}
setAdView(nativeAdView)
adLoader = GADAdLoader(adUnitID: adUnitID, rootViewController: self,
adTypes: [.unifiedNative], options: nil)
adLoader.delegate = self
DispatchQueue.global(qos: .background).async {
self.adLoader.load(GADRequest())
}
}
func setAdView(_ adView: GADUnifiedNativeAdView) {
// Remove the previous ad view.
DispatchQueue.main.async { [weak self] in
guard let weakSelf = self else {
return
}
weakSelf.nativeAdView = adView
weakSelf.view.addSubview(weakSelf.nativeAdView)
weakSelf.nativeAdView.translatesAutoresizingMaskIntoConstraints = false
// Layout constraints for positioning the native ad view to stretch the entire width and height
let viewDictionary = ["_nativeAdView": weakSelf.nativeAdView!]
weakSelf.view.addConstraints(
NSLayoutConstraint.constraints(
withVisualFormat: "H:|[_nativeAdView]|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
)
weakSelf.view.addConstraints(
NSLayoutConstraint.constraints(
withVisualFormat: "V:|[_nativeAdView]|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
)
}
}
}
extension AdViewController: GADUnifiedNativeAdLoaderDelegate {
func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error:
GADRequestError) {
print("didFailToReceiveAdWithError: \(error)")
}
func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADUnifiedNativeAd) {
print("Received unified native ad: \(nativeAd)")
// Deactivate the height constraint that was set when the previous video ad loaded.
heightConstraint?.isActive = false
// Populate the native ad view with the native ad assets.
// The headline and mediaContent are guaranteed to be present in every native ad.
(nativeAdView.headlineView as? UILabel)?.text = nativeAd.headline
nativeAdView.mediaView?.mediaContent = nativeAd.mediaContent
// This app uses a fixed width for the GADMediaView and changes its height to match the aspect
// ratio of the media it displays.
if let mediaView = nativeAdView.mediaView, nativeAd.mediaContent.aspectRatio > 0 {
heightConstraint = NSLayoutConstraint(
item: mediaView,
attribute: .height,
relatedBy: .equal,
toItem: mediaView,
attribute: .width,
multiplier: CGFloat(1 / nativeAd.mediaContent.aspectRatio),
constant: 0)
heightConstraint?.isActive = true
}
// This asset is not guaranteed to be present. Check that it is before
// showing or hiding it.
(nativeAdView.advertiserView as? UILabel)?.text = nativeAd.advertiser
nativeAdView.advertiserView?.isHidden = nativeAd.advertiser == nil
// In order for the SDK to process touch events properly, user interaction should be disabled.
nativeAdView.callToActionView?.isUserInteractionEnabled = false
// Associate the native ad view with the native ad object. This is
// required to make the ad clickable.
// Note: this should always be done after populating the ad views.
nativeAdView.nativeAd = nativeAd
}
}
I want to mention that this is working at the moment, but the problems that I want to fix and I don't know how are:
The grid with the pictures load, but when I scroll over an ad, it takes several seconds for the ad to load and display. How could I at least hide it while it loads or make it faster?
If I scroll over an ad, the ad loads and if I continue scrolling, when I scroll back up, the ad is not loaded anymore and I have to wait for it to load again. How can I fix this? Or what is the best practice for this kind of scenario?
Should I use multipleAds? To load them before showing? If yes, then how should I do this?
Does what I am doing here look even a little bit correct? Please...I need some help
The Best Way to show ads in SwiftUI Grids is implementing Native Ads in your app to provide personalized ad experience

Is it possible to set a character limit on a TextField using SwiftUI?

[RESOLVED]
I am using a codable struct which stores the object values retrieved from an API call so I have amended my TextField using Cenk Belgin's example, I've also removed extra bits I've added in so if anyone else is trying to do the same thing then they won't have pieces of code from my app that aren't required.
TextField("Product Code", text: $item.ProdCode)
.onReceive(item.ProdCode.publisher.collect()) {
self.item.ProdCode = String($0.prefix(5))
}
Here is one way, not sure if it was mentioned in the other examples you gave:
#State var text = ""
var body: some View {
TextField("text", text: $text)
.onReceive(text.publisher.collect()) {
self.text = String($0.prefix(5))
}
}
The text.publisher will publish each character as it is typed. Collect them into an array and then just take the prefix.
From iOS 14 you can add onChange modifier to the TextField and use it like so :
TextField("Some Placeholder", text: self.$someValue)
.onChange(of: self.someValue, perform: { value in
if value.count > 10 {
self.someValue = String(value.prefix(10))
}
})
Works fine for me.
You can also do it in the Textfield binding directly:
TextField("Text", text: Binding(get: {item.ProCode}, set: {item.ProCode = $0.prefix(5).trimmingCharacters(in: .whitespacesAndNewlines)}))