[iOS] #7 閱讀是一輩子的事之Google Books API

音樂和電影相關內容想應用在其他作業當中,因此這次的第三方API應用選用書籍資料。Google Books API除了提供書目資料之外,也有供應資訊、價格及試閱等資源,沒有申請Access Key也可以取用,只是會有一些限制,但不影響練習和測試。

Google Books API

呼叫API:

https://www.googleapis.com/books/v1/volumes?q=關鍵字

回傳JSON格式:

{
"kind": "books#volumes",
"totalItems": 694,
"items": [
{
"kind": "books#volume",
"id": "GmrQEAAAQBAJ",
"etag": "dTDu8i8ykS4",
"selfLink": "https://www.googleapis.com/books/v1/volumes/GmrQEAAAQBAJ",
"volumeInfo": {
"title": "大腦生病救命手冊(暢銷10萬本珍藏紀念版)",
"subtitle": "有些人不是真的壞、偏激、不用功、人格異常,而是大腦出問題了!",
"authors": [
"丹尼爾.亞曼(Daniel G. Amen)"
],
"publisher": "柿子文化",
"publishedDate": "2023-08-10",
"description": "修復你的大腦,搶救你的人生 解決憂鬱、焦慮、暴力、偏執、社交障礙、注意力不集中 背後的大腦問題 找回你應有的生活品質 ★全新封面設計 ★連續20年蟬聯Amazon生理心理學TOP10 ★《華盛頓郵報》讚譽:全美最受歡迎的精神病學家 ★網路醫療資訊平台Sharecare讚譽:網路最有影響力的心理健康專家和倡導者 ★逾11萬張腦部造影+逾30年臨床經驗,全世界111個國家病人因此重獲新生 全世界有1/8人口有精神障礙,而且大多數都沒有獲得治療! 憂鬱、妄想、焦躁、偏執、暴力……因為種種異常情緒和行為所釀成的悲劇,年年上演,除了是教育、心理、社會的問題,知名科神經科精神醫生,同時也是腦部造影、腦部與行為和注意力缺陷症的專家──丹尼爾.亞曼,提出另一種觀點: 「情緒問題和異常行為不一定是性格不好或人格有問題, 有時候,這可能與大腦某些部位的功能失調或受損有關。」 失控的大腦,失控的人生 重整5大腦區,危機救命,減少悲劇 ◆被撞就想斃了對方? ◆害怕與人衝突結果成了爛好人? ◆卡債高築還要買? ◆八百年前的小事讓你對人生很絕望? ◆被超車就飆三字經? ◆一定要在上學的路上數柏油路有多少裂痕? ◆不自覺的愛找碴? ◆最簡單的家事、功課、雜事或例行公事都搞不定? 如果你受夠了這一切,也不想再自責、自憐……,那你一定要知道,你之所以會一犯再犯,有時候,並不是毅力、決心、想不想得開的問題,而是跟大腦某個部位的功能有關…… ◆經前症候群、憂鬱症、躁鬱症、「分手」症候群、空巢症候群、產後憂鬱…… →大腦失控部位:親密關係和情緒的調味窗口──「深層邊緣系統」。 →救腦處方:寫下負面想法加以駁斥、多和正面思考的人在一起、建立美好記憶庫、擁抱、薰衣草和柑橘精油、適當的運動……。 ◆恐慌症、創傷後壓力症候群、衝突恐懼症、帕金森氏症、妥瑞氏症、頭痛、工作狂…… →大腦失控部位:調整身體和情緒的反應和節奏的整合單位──「基底核系統」。 →救腦處方:腹部呼吸、想像一個舒服的天堂、「十八/四十/六十法則」…… ◆考試型焦慮、社交型焦慮、注意力缺陷症、精神分裂症、被害妄想症…… →大腦失控部位:監督時間管理、判斷、規劃、組織等功能的人生主管──「前額葉皮質」。 →救腦處方:列出重要事項、找有樂趣的事做、避免單醣和單一碳水化合物…… ◆認知僵化、負面思考、開車火氣大、強迫症、成癮症(購物狂、賭博、酗酒、嗑藥)…… →大腦失控部位:調節認知彈性的換檔器──「扣帶系統」。 →救腦處方:祈禱、逆向心理學、提高血清素的食物、活用分散注意力清單…… ◆語言障礙、閱讀障礙、社交障礙、面孔失認症、幻覺、自殺、宗教狂熱、健忘、阿茲海默症…… →大腦失控部位:儲存記憶和影像的資料庫──「顳葉」。 →救腦處方:聽聽莫札特、每天哼唱開嗓5分鐘、每晚睡足6到8小時、舞蹈治療…… 《大腦生病救命手冊》是亞曼醫師第一本,也是最暢銷的作品。他深入淺出帶你瞭解大腦五大區塊彼此的連結和各種情緒精神問題的關係,也提供簡單又效果驚人的處方箋,包括心理調整、行為練習、芳療、飲食、運動、想像力療法、呼吸、冥想、自我催眠、藥物治療等,以及預防大腦問題產生的生活撇步,讓大腦良好運作,使身心健康、做事有效率,生命更幸福與成功。 本書特色 ◆男女老少都用得到的大腦保健建議:無論是求學讀書的學生、正在努力打拼工作賺錢的上班族、處理家裡大小事物帶小孩的家庭婦女,或是期待退休或老年生活過得更活躍且更具成就感的人,都可以把人生活得更精彩、更幸福、更心想事成。 ◆超過50則真實病例、50幅腦袋SPECT 3D造影圖:透視大腦的祕密,你是否也有同樣的問題? ◆5大腦區自我檢測表:搶先一步關心自己的健康! ◆100項預防大腦問題的生活小撇步:日常保健,大腦不生病、生活更美滿。 ◆超過50種無藥處方箋:不用看醫生也可輕鬆做到的預防、自癒處方箋。 ◆改變大腦的健康、幸福說話術:用語言的力量保健你的腦袋。 ◆立即見效的飲食保養:控制不良的情緒。 ◆大腦相關用藥一覽表:讓你與醫師溝通無障礙。 你的大腦創造了你的世界! 遺憾的是, 當一人出現情緒障礙、精神疾病、病態行為時, 我們很多人,包括專業醫師和心理師在內, 卻很少考慮腦部生理機能異常的可能性…… 好評推薦 掛名推薦 蘇東平,台北榮民總醫院精神部特約門診主治醫師、振興醫院精神醫學部部主任 專文推薦 亞曼醫師就像是一位腦部夜間的探險科學家,讓你看到大腦的活動,並進一步明瞭為何如此。例如,我們大腦前面有一個叫做前額葉的地方,亞曼醫師的SPECT影像科學可以清楚解釋為何有些人這地方出問題,會有注意力或衝動問題,為何容易暴力或口不擇言,又為何缺乏組織能力,甚至常常有憂鬱情緒控制問題……──林耿立,松德精神科診所醫師 亞曼醫師在本書中使用了許多實例,分享了他於各種精神疾病與個性特質的兒童及成人,進行一種腦影像檢查(SPECT)的臨床經驗,例如注意力不足過動症、焦慮症、強迫症、憂鬱症……等等。他除了簡潔地串聯大腦部位的功能與異常時的現象,闡明藥物治療過程的大腦機制與變化,也屢屢述及運用認知行為及養生來調適心態、觀念以及情緒。要提昇大腦功能、要救您的大腦,務須先了解您的大腦,此書能讓您輕輕鬆鬆地迅速掌握大腦的奧祕與乾坤。──陳映雪,台北榮總精神部特約門診教學主治醫師 本世紀藉由腦影像科技的研究,可以「看到」許多異常行為與情緒背後的腦內變化,知道不同腦區及迴路的功能,以及藥物與生活方式能如何改善這些症狀。本書在這方面作了很好的說明,實用性高,非常值得一讀。──黃宗正,台大醫院精神醫學部主任 亞曼醫師使用臨床最常用的腦血流量之神經造影,生動而活潑的闡明行為/情緒變化過程中腦部的相關活性,讓讀者們很容易了解深奧的心理活動之腦功能內涵。這是一本很值得一讀再讀的書,雖然有些神經解剖、藥理的知識對一般大眾而言不太易懂,但透過圖象,讀者可以更清楚的抓到腦活動與行為關係之精髓。神經影像與行為間的關係是我們探究人類心靈奧祕重要的工具之一,《大腦生病救命手冊》在這方面是本很值得推薦的書籍。──楊延光,成大醫學院精神學科特聘教授 我雖然是西醫,近年來追隨老師學習調息、放鬆、伸展、拉筋,不斷的和自己的細胞對話,得到非常多的回饋。亞曼醫師除了提出許多科學上的實例,更在書中介紹許多觀念和做法,這些觀念和做法竟然和許多中國古老的傳承不謀而合。我感謝自己有機會接觸這本書,經由寫序讓自己有更多的機會檢視自己並且重新思考,我更樂於將本書推薦給大家,尤其是和我一樣的女性朋友們。──閻紫宸,林口長庚醫院分子影像轉譯研究中心主任暨核子醫學部主治醫師 革命性的一本書。亞曼醫師讓你明白你的腦可以變成你最可怕的敵人,但也可以經由適當的治療而變成你最好的朋友。——馬丁‧史坦(Martin Stein)醫師,喬治華聖頓大學精神科臨床副教授 開啟一扇大門,藉由評估及對策來改變你的生活。——羅伯特‧杭特(Robert D. Hunt) 醫師,范德堡大學精神科臨床副教授 創新的技術、臨床的知識以及真誠的諮詢集合而成的一本增強腦力的使用手冊。亞曼醫生提供了能夠改變任何一個人的人生的簡單、直接而且立刻有效的處方箋。——艾密特‧米勒(Emmett E. Miller)醫師,《深層治療:身心醫學之精髓》作者 《大腦生病救命手冊》是一本具前瞻性的著作。亞曼醫生提供簡單又具說服力的證據證明許多過去被認為是心理疾病的行為異常其實是有生物學的理論基礎為根據的。透過新型腦部造影片技術的鏡頭,人們可以看到患有憂鬱症、焦慮症、暴躁、衝動與強迫症的腦部實際上看起來的樣子。他給了許多對抗這些問題的實際建議,也提供許多幫助我們充分利用頭腦並改善生活的工具。——羅伯‧孔恩(Rob Kohn, D.O.)醫師,伊利諾大學芝加哥校區精神科與神經科醫師 亞曼醫師這本突破性的著作永遠改變了精神病學與心理學這兩個領域。健康的頭腦是健康人生的先決條件,亞曼醫師提供了一個實用的指南。——厄爾‧亨斯林(Earl Henslin)博士,《你是爸爸的寶貝女兒》、《男人與男人之間:幫助父子建立親子關係》作者 這本書出色極了。亞曼醫師是位先驅。——強納生‧沃克(Jonathan Walker),精神科醫生 亞曼醫師對腦部功能的完善記錄對這個快速發展的專業領域有著特殊的貢獻。不過,他還有多個不同之處:裡面有許許多多幫助病患及父母該如何做的建議。這本書是目前最完整也最具體的建議手冊,病患以及父母可以學習並試著幫助自己療癒。——科里登‧克拉克(Corydon Clark),精神科醫生 在這個自己動手醫療保健風行的年代,《大腦生病救命手冊》完全符合這個需求。充滿在書中的「大腦處方」(透過認知鍛煉和營養忠告)適合所有經歷過焦慮、抑鬱、衝動、過度憤怒或擔心,以及有強迫行為的讀者來閱讀。──亞馬遜網書導讀推薦 這本書很顯然讓那些喜愛科學懸疑情節、趣聞的讀者心神嚮往!──出版人週刊",
"industryIdentifiers": [
{
"type": "ISBN_13",
"identifier": "9786267198766"
},
{
"type": "ISBN_10",
"identifier": "6267198766"
}
],
"readingModes": {
"text": true,
"image": true
},
"pageCount": 338,
"printType": "BOOK",
"categories": [
"Health & Fitness"
],
"maturityRating": "NOT_MATURE",
"allowAnonLogging": false,
"contentVersion": "1.1.1.0.preview.3",
"panelizationSummary": {
"containsEpubBubbles": false,
"containsImageBubbles": false
},
"imageLinks": {
"smallThumbnail": "http://books.google.com/books/content?id=GmrQEAAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api",
"thumbnail": "http://books.google.com/books/content?id=GmrQEAAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api"
},
"language": "zh-TW",
"previewLink": "http://books.google.com.tw/books?id=GmrQEAAAQBAJ&printsec=frontcover&dq=%E6%9A%A2%E9%8A%B7&hl=&cd=1&source=gbs_api",
"infoLink": "https://play.google.com/store/books/details?id=GmrQEAAAQBAJ&source=gbs_api",
"canonicalVolumeLink": "https://play.google.com/store/books/details?id=GmrQEAAAQBAJ"
},
"saleInfo": {
"country": "TW",
"saleability": "FOR_SALE",
"isEbook": true,
"listPrice": {
"amount": 483,
"currencyCode": "TWD"
},
"retailPrice": {
"amount": 328,
"currencyCode": "TWD"
},
"buyLink": "https://play.google.com/store/books/details?id=GmrQEAAAQBAJ&rdid=book-GmrQEAAAQBAJ&rdot=1&source=gbs_api",
"offers": [
{
"finskyOfferType": 1,
"listPrice": {
"amountInMicros": 483000000,
"currencyCode": "TWD"
},
"retailPrice": {
"amountInMicros": 328000000,
"currencyCode": "TWD"
}
}
]
},
"accessInfo": {
"country": "TW",
"viewability": "PARTIAL",
"embeddable": true,
"publicDomain": false,
"textToSpeechPermission": "ALLOWED",
"epub": {
"isAvailable": true,
"acsTokenLink": "http://books.google.com.tw/books/download/%E5%A4%A7%E8%85%A6%E7%94%9F%E7%97%85%E6%95%91%E5%91%BD%E6%89%8B%E5%86%8A_%E6%9A%A2%E9%8A%B710%E8%90%AC%E6%9C%AC-sample-epub.acsm?id=GmrQEAAAQBAJ&format=epub&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
},
"pdf": {
"isAvailable": true,
"acsTokenLink": "http://books.google.com.tw/books/download/%E5%A4%A7%E8%85%A6%E7%94%9F%E7%97%85%E6%95%91%E5%91%BD%E6%89%8B%E5%86%8A_%E6%9A%A2%E9%8A%B710%E8%90%AC%E6%9C%AC-sample-pdf.acsm?id=GmrQEAAAQBAJ&format=pdf&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
},
"webReaderLink": "http://play.google.com/books/reader?id=GmrQEAAAQBAJ&hl=&source=gbs_api",
"accessViewStatus": "SAMPLE",
"quoteSharingAllowed": false
},
"searchInfo": {
"textSnippet": "修復你的大腦,搶救你的人生 解決憂鬱、焦慮、暴力、偏執、社交障礙、注意力不集中 背後的大腦問題 找回你應有的生活品質 ★全新封面設計 ★連續20年蟬聯Amazon生理心理學TOP10 ..."
}
}
]
}

JSON轉成自訂型別

API提供的資料項目眾多,我只挑選稍後APP會使用到的幾種書目資料,宣告自訂型別。JSON結構有多層次,若子層次的項目眾多或有陣列,就再另訂子型別,讓型別比較扁平好閱讀。例如item之下的volumeInfo項目眾多,就再定義型別volumeInfo:

另外需注意的是,並不是每一本書籍都有完整的書目資料,有可能沒有出版社資料,或是沒有作者,因此在宣告自訂型別時,記得將可有可無的項目加上問號(?),不然若剛好缺了其中一項,將會在JSON轉換的時候出錯。完整的自訂型別:

struct ResultItem:Codable {
let kind:String
let totalItems:Int
let items:[Item]
}

struct Item:Codable {
let kind: String //"books#volume"
let id: String //"TjYpEAAAQBAJ"
let etag: String //"XY3QVSRDgSM"
let selfLink: String //"https://www.googleapis.com/books/v1/volumes/TjYpEAAAQBAJ"
let volumeInfo: volumeInfo
let saleInfo: saleInfo
let accessInfo: accessInfo
}

struct volumeInfo:Codable {
let title:String
let subtitle:String?
let authors:[String]?
let publisher:String?
let publishedDate:String?
let description:String?
var industryIdentifiers:[industryIdentify]? = [industryIdentify]()
let categories:[String]?
let imageLinks:imageLinks?
let language:String
let previewLink:String?
let infoLink:String
let canonicalVolumeLink:String
}

struct industryIdentify:Codable {
let type: String //"ISBN_13",
let identifier: String //"9789863487081"
}

struct imageLinks:Codable {
let smallThumbnail: String?
let thumbnail: String?
}

struct saleInfo:Codable {
let country:String //TW
let saleability:String //FOR_SALE
let isEbook:Bool //true
let listPrice:price?
let retailPrice:price?
let buyLink:String?
}

struct price:Codable {
let amount: Double
let currencyCode: String //TWD
}

struct accessInfo:Codable {
let country:String
let webReaderLink:String?
}

URLSession抓取資料

利用背景執行緒透過網路下載資料,並將JSON decode轉換為自訂型別Item存入變數,待稍後繪製畫面使用。資料下載轉換完成,再將繪製畫面的工作交由主執行緒執行。

    func fetchItems(q:String) {
let urlStr = "https://www.googleapis.com/books/v1/volumes?maxResults=30&orderBy=newest&q="+q.urlEncoded()
if let url = URL(string: urlStr) {
URLSession.shared.dataTask(with: url) { data, response , error in
if let data = data {
let decoder = JSONDecoder()
do {
let resultItem = try decoder.decode(ResultItem.self, from: data)
self.items = resultItem.items
self.itemCount = resultItem.totalItems
DispatchQueue.main.async {
self.tableView.reloadData()
}

} catch {
print(error)
}
} else {
print(error ?? "")
}
}.resume()
}
}

SearchBar搜尋

APP採用Navigation Controller切換頁面,搜尋的功能則直接在Navigation Item中加入Search Controller,讓搜尋方框定位在Navigation Title的位置。

    let searchController = UISearchController()
searchController.searchResultsUpdater = self
navigationItem.searchController = searchController

採用UISearchResultsUpdating觸發搜尋

負責更新搜尋結果的是目前畫面的BookTableViewController,因此要遵從UISearchResultsUpdating這個protocol,並在這protocol規定的函式updateSearchResults( ) 取得使用者輸入的字串,並呼叫API執行搜尋。

class BookTableViewController: UITableViewController, UISearchResultsUpdating {

override func viewDidLoad() {
super.viewDidLoad()

let searchController = UISearchController()
searchController.searchResultsUpdater = self
navigationItem.searchController = searchController

...
}

//搜尋
func updateSearchResults(for searchController: UISearchController) {

if let searchText = searchController.searchBar.text, searchText.isEmpty == false {
if searchController.searchBar.returnKeyType == UIReturnKeyType.search {
fetchItems(q: searchText)
}
}
tableView.reloadData()
}

//搜尋
func updateSearchResults(for searchController: UISearchController) {
//從search bar取得使用者輸入的字串
if let searchText = searchController.searchBar.text,
searchText.isEmpty == false {
//查詢並顯示結果
fetchItems(q: searchText)
}
tableView.reloadData()
}
}

UISearchResultsUpdating的機制,是當使用者在搜尋文字框輸入時,只要有字元顯示在輸入框,就會即時觸發搜尋,也就是每多一個字元,就會將文字框內的字串執行一次搜尋。我原本希望等使用者輸入完整字串之後再按enter或搜尋按鈕來觸發搜尋,但可能要改用其他UI元件,之後我再繼續研究^^|||。

*輸入的文字一有改變立即重新搜尋*

畫面繪製

圖書列表UI

TableView搭配固定高度的TableViewCell繪製圖書列表。

由於書目的Label和書封的ImageView都在TableViewCell底下,因此將上網搜尋下載功能寫在BookTableViewController.swift,圖書資訊相關UI的顯示獨立寫在BookTableViewCell.swift。

BookTableViewController

    //BookTableViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(BookTableViewCell.self)", for: indexPath) as? BookTableViewCell else {
fatalError("dequeueReusableCell LoverTableViewCellfailed")
}

let item = items[indexPath.row]
cell.update(item)

return cell
}

BookTableViewCell

class BookTableViewCell: UITableViewCell {

@IBOutlet weak var CoverImageView: UIImageView!
@IBOutlet weak var TitleLabel: UILabel!
@IBOutlet weak var AuthorLabel: UILabel!
@IBOutlet weak var PublishedDateLabel: UILabel!
@IBOutlet weak var ISBNLabel: UILabel!
@IBOutlet weak var PublisherLabel: UILabel!

//書目
func update(_ item:Item){
TitleLabel.text = item.volumeInfo.title
if let authors = item.volumeInfo.authors {
AuthorLabel.text = ""
for author in authors {
AuthorLabel.text?.append(author + " ")
}
}
PublishedDateLabel.text = item.volumeInfo.publishedDate
PublisherLabel.text = item.volumeInfo.publisher
if let inds = item.volumeInfo.industryIdentifiers {
for isbn in inds {
if isbn.type == "ISBN_13" {
ISBNLabel.text = isbn.identifier
}
}
}
updateCover(item)
}
}

以URL取得書封圖檔

Google Books API 回傳的JSON也包含圖書封面圖檔的網址,再次透過URLSession取回圖檔,並在繪製cell時一起更新顯示:

    //書封 
func updateCover(_ item:Item){
//從JSON轉換的資料取得圖檔網址
if let urlStr = item.volumeInfo.imageLinks?.thumbnail?.replacingOccurrences(of: "http://", with: "https://"), let url = URL(string: urlStr) {
URLSession.shared.dataTask(with: url ) { data, response , error in
if let data = data {
DispatchQueue.main.async {
self.CoverImageView.image = UIImage(data: data)
}
}
}.resume()
}else{
self.CoverImageView.image = UIImage(named: "nocover")
}
}

圖書詳細資料UI

使用者點選任一個cell,畫面轉入圖書詳細資料頁BookDetailViewController。列表頁和詳細頁透過segue傳遞資料:

資料傳遞:BookTableViewController -> BookDetailViewController

    // --- BookTableViewController --- //
@IBSegueAction func showBookDetail(_ coder: NSCoder) -> BookDetailViewController? {
//偵測使用者點選哪一個cell
if let row = tableView.indexPathForSelectedRow?.row {
//將對應的圖書item傳入詳細頁
return BookDetailViewController(coder: coder, item: items[row])
} else {
return nil
}
}

// --- BookDetailViewController --- //
class BookDetailViewController: UIViewController, SFSafariViewControllerDelegate {
...

let item: Item
init?(coder: NSCoder, item: Item) {
self.item = item
super.init(coder: coder)
}
...
}

以字數計算設定Label行數

書目及書封的顯示與列表頁相同,只有多加Scroll View顯示較多文字的簡介資料。由於有些文字很多超過千字,需要動態設定Label行數及Scroll View的高度,才能完整顯示簡介資料。

    //簡介
func adjustDescriptionView(){
//計算簡介字元數目
if let wordCount = item.volumeInfo.description?.count {
//設定Scroll View 高度
DescriptionScrollView.contentSize = CGSize(width: 0, height: wordCount + 70)
//設定Label行數
DescriptionLabel.numberOfLines = wordCount / 10 + 50
//讓Label依文字內容自動調整高度
DescriptionLabel.sizeToFit()
}
}

開啟試讀與購買網頁

Google Books API也有提供試閱網址及購買網址,利用按鈕的點擊,直接在詳細頁,透過 SFSafariViewController 開啟網頁。只要載入SafariServices套件,再讓詳細頁 BookDetailViewController 遵從 SFSafariViewControllerDelegate 即可:

import SafariServices

class BookDetailViewController: UIViewController, SFSafariViewControllerDelegate {

...

//試讀
@IBAction func previewBook(_ sender: Any) {
//取得試閱頁網址
if let urlStr = item.accessInfo.webReaderLink, let url = URL(string: urlStr) {
let safariVC = SFSafariViewController(url: url)
safariVC.preferredBarTintColor = .black //設定safari表頭背景色
safariVC.preferredControlTintColor = .white//設定safari表頭字顏色
safariVC.dismissButtonStyle = .close //設定關閉safari按鈕文字
safariVC.delegate = self
self.present(safariVC, animated: true, completion: nil)
}
}

//購買
@IBAction func buyBook(_ sender: Any) {
//取得購買頁網址
if let urlStr = item.saleInfo.buyLink, let url = URL(string: urlStr) {
let safariVC = SFSafariViewController(url: url)
safariVC.dismissButtonStyle = .close
safariVC.delegate = self
self.present(safariVC, animated: true, completion: nil)
}
}

...
}

GitHub

--

--