Related
Summary:
I have a list loaded from an API. Each list item have a button. On click of button, a unique ID associated with the list item is sent to server which in response provides a pdf directly there is no other response just a pdf file, the api is like :
http://myhost/api/DownloadPDF/uniqueID=67198287_239878092_8089
I have created the list and also able to download the pdf in documentDirectory by calling download task. However, I am unable to open the pdf automatically in app itself after downloading. I have created DisplayPDF struct which uses PDFKit to display as follows:
struct DisplayPDF: View {
var url:URL
var body:some View
{
PDFKitRepresentedView(url)
}
}
struct PDFKitRepresentedView: UIViewRepresentable{
func updateUIView(_ uiView: UIView, context:
UIViewRepresentableContext<PDFKitRepresentedView>) {
}
let url: URL
init(_ url:URL)
{
self.url = url
}
func makeUIView(context:
UIViewRepresentableContext<PDFKitRepresentedView>) ->
PDFKitRepresentedView.UIViewType {
let pdfView = PDFView()
pdfView.document = PDFDocument(url: self.url)
pdfView.autoScales = true
return pdfView
}}
I need to pass the url into the above struct. The url can be the saved location or API directly. However, the url is not passed when the DisplayPDF view is called.
What I have tried so far
1> Pass the DisplayPDF into navigationlink in ReportList(where list is loaded) struct and than either call getFile func in onAppear in DisplayPDF struct or ReportRow struct.
2> Call getFile() on ReportRow in onAppear and pass the url in DisplayPDF() there.
3> Call getFile() on DisplayPDF() onAppear and pass the url there
4> Also tried, sheet method blank sheet pops up
All failed, no value is sent to DisplayPDF(url) the moment it is called from any of the listed method.
ReportList struct
import SwiftUI
struct ReportList: View {
#ObservedObject var reportLink : ReportViewModel
var body: some View {
List{
ForEach(reportLink.trackReport)
{report in
VStack {
ReportRow(report: report)
}
if(reportLink.trackReport.isEmpty)
{
Text("No Report Found")
.foregroundColor(.accentColor)
.fontWeight(.semibold)
}
}
}
}
}
ReportRow struct:
struct ReportRow: View {
var report : ReportResponse
#StateObject var pdfDownload = PDFDownload()
var body: some View {
VStack{
HStack{
Text(report.name)
.font(.system(size: 16))
.foregroundColor(.black)
.padding(.bottom,1)
.padding(.top,1)
}.frame(maxWidth: .infinity, alignment: .leading)
HStack{
Text("P.Id:")
.foregroundColor(.black)
.font(.system(size: 14))
Text(report.patientID)
.foregroundColor(.purple)
.font(.system(size: 14))
Spacer()
Button(action: {
pdfDownload.uniqueReportId = report.uniqueID
pdfDownload.patientName = report.name
pdfDownload.getFile()
}, label:
{
Text("\(report.status)")
.foregroundColor(.blue)
.font(.system(size: 14))
.padding(.trailing,2)
}).frame(maxWidth: .infinity, alignment: .trailing)
}
}
}}
I have made this PDFDownload model in which openURL is declared a published var which should provide updated url to a view(like DisplayPDF() view):
class PDFDownload : UIViewController, ObservableObject
{
#Published var uniqueReportId:String = String()
#Published var patientName:String = String()
#Published var isNavigate:Bool = false
#Published var openURL:URL = URL(fileURLWithPath: "")
func getFile()
{
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "myHost"
urlComponents.port = 80
urlComponents.path = "/api/Reports/DownloadReport"
urlComponents.queryItems = [URLQueryItem(name: "uniquePackageId",
value: uniqueReportId)]
let url = urlComponents.url
print(url?.absoluteString)
let downloadTask = URLSession.shared.downloadTask(with: url!)
{
urlOrNil, responseOrNil, errorOrNil in
guard let fileURL = urlOrNil else {return}
do{
let documentURL = try FileManager.default.url(for:
.documentDirectory, in: .userDomainMask, appropriateFor:
nil, create: false)
let savedURL = documentURL.appendingPathComponent("\
(self.patientName)_\(UUID().uuidString).pdf")
print(savedURL)
try FileManager.default.moveItem(at: fileURL, to:
savedURL)
DispatchQueue.main.async {
self.openURL = savedURL
}
}
catch{
print("Error while writting")
}
}
downloadTask.resume()
}}
So what is the correct way of solving this problem that the correct URL can be passed to DisplayPDF() view.
Extra: ReportResponse model:
struct DownReport : Codable, Identifiable {
let id = UUID()
let success : Bool
let message : String
let reportResponse : [ReportResponse]
enum CodingKeys: String, CodingKey{
case success = "IsSuccess"
case message = "Message"
case reportResponse = "ResponseData"
}}
struct ReportResponse : Codable, Identifiable {
var id:String {uniqueID}
let patientID : String
let name : String
let status : String
let crmNo : String?
let recordDate : String
let uniqueID : String
let testCount : Int
enum CodingKeys: String, CodingKey {
case patientID = "PatientId"
case name = "Name"
case status = "Status"
case crmNo = "CrmNo"
case recordDate = "RecordDate"
case uniqueID = "UniquePackageId"
case testCount = "NoOfTests"
}
}
The above response is from POST request which is sent to generate list. To get pdf only unique id as Query is sent as I have posted on top.
The above structure successfully downloads the file but fail to open the file automatically. How to do that?
Here is some sample code that shows how to download a pdf document (wikipedia),
copy it to a local file, and display it on the screen by passing the savedURL to the View. You should be able to adapt the sample code for your purpose.
import Foundation
import SwiftUI
import PDFKit
struct ContentView: View {
#StateObject var downloader = PDFDownload()
var body: some View {
VStack (spacing: 30) {
Button("download1", action: {
downloader.patientName = "patient-1"
downloader.uniqueReportId = "astwiki-Homo_heidelbergensis-20200728.pdf/astwiki-Homo_heidelbergensis-20200728.pdf"
downloader.getFile()
}).buttonStyle(.bordered)
Button("download2", action: {
downloader.patientName = "patient-2"
downloader.uniqueReportId = "rowiki-Biban_european-20200728.pdf/rowiki-Biban_european-20200728.pdf"
downloader.getFile()
}).buttonStyle(.bordered)
if downloader.isDownloading { ProgressView("downloading ...") }
}
.fullScreenCover(item: $downloader.openURL) { siteUrl in
DisplayPDF(url: siteUrl.url)
}
}
}
struct DisplayPDF: View {
#Environment(\.dismiss) var dismiss
let url: URL
var body:some View {
VStack {
#if targetEnvironment(macCatalyst)
Button("Done", action: {dismiss()})
#endif
PDFViewer(url: url)
}
}
}
struct PDFViewer: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> PDFView {
let pdfView = PDFView()
pdfView.document = PDFDocument(url: url)
pdfView.autoScales = true
return pdfView
}
func updateUIView(_ uiView: PDFView, context: Context) { }
}
class PDFDownload : ObservableObject {
#Published var uniqueReportId = ""
#Published var patientName = ""
#Published var isNavigate = false
#Published var openURL: SiteURL?
#Published var isDownloading = false
func getFile() {
isDownloading = true
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "ia803207.us.archive.org"
urlComponents.path = "/0/items/\(uniqueReportId)" // <-- just for testing
// urlComponents.port = 80
// urlComponents.queryItems = [URLQueryItem(name: "uniquePackageId", value: uniqueReportId)]
guard let url = urlComponents.url else {return}
let downloadTask = URLSession.shared.downloadTask(with: url) { urlOrNil, responseOrNil, errorOrNil in
guard let fileURL = urlOrNil else { return }
do {
let documentURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let savedURL = documentURL.appendingPathComponent("\(self.patientName)_\(UUID().uuidString).pdf")
try FileManager.default.moveItem(at: fileURL, to: savedURL)
DispatchQueue.main.async {
self.openURL = SiteURL(url: savedURL)
self.isDownloading = false
}
}
catch {
print("Error \(error)")
}
}
downloadTask.resume()
}
}
struct SiteURL: Identifiable {
let id = UUID()
var url: URL
}
Updated Answer with your update question: the row will update when the file is downloaded, it will then be a navigation link to display pdf
struct DownReport : Codable, Identifiable {
let id = UUID()
let success : Bool
let message : String
let reportResponse : [ReportResponse]
enum CodingKeys: String, CodingKey{
case success = "IsSuccess"
case message = "Message"
case reportResponse = "ResponseData"
}
}
struct ReportResponse : Codable, Identifiable {
var id:String {uniqueID}
let patientID : String
let name : String
let status : String
let crmNo : String?
let recordDate : String
let uniqueID : String
let testCount : Int
// This URL is only set when report has been downloaded and it does not need to be part of the response
var localFileUrl: URL?
enum CodingKeys: String, CodingKey {
case patientID = "PatientId"
case name = "Name"
case status = "Status"
case crmNo = "CrmNo"
case recordDate = "RecordDate"
case uniqueID = "UniquePackageId"
case testCount = "NoOfTests"
}
}
class ReportViewModel: ObservableObject {
// some dummy value
#Published var trackReport: [ReportResponse] = [ReportResponse(patientID: "0001", name: "patient-1", status: "status", crmNo: nil, recordDate: "today", uniqueID: "010001", testCount: 1),ReportResponse(patientID: "0002", name: "patient-2", status: "status", crmNo: nil, recordDate: "today", uniqueID: "010002", testCount: 3)]
// Update the report in the array ussing report unique ID
func updateReport(withId reportId: String, url: URL) {
guard let index = trackReport.firstIndex(where: {$0.uniqueID == reportId}) else {return}
var report = trackReport[index]
report.localFileUrl = url
trackReport[index] = report
}
}
// no need for any observation on pdfDownload object as the completion will do the jobs
class PDFDownload {
var uniqueReportId: String
var patientName: String
init(uniqueReportId:String, patientName:String) {
self.uniqueReportId = uniqueReportId
self.patientName = patientName
}
func getFile(completion: #escaping (URL) -> ())
{
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "myHost"
urlComponents.port = 80
urlComponents.path = "/api/Reports/DownloadReport"
urlComponents.queryItems = [URLQueryItem(name: "uniquePackageId",value: uniqueReportId)]
let url = urlComponents.url
// print(url?.absoluteString)
let downloadTask = URLSession.shared.downloadTask(with: url!)
{
urlOrNil, responseOrNil, errorOrNil in
// Simulation of downloading
sleep(3)
DispatchQueue.main.async {
completion(URL(fileURLWithPath: "report\(self.patientName).pdf"))
}
guard let fileURL = urlOrNil else {return}
do{
let documentURL = try FileManager.default.url(for:
.documentDirectory, in: .userDomainMask, appropriateFor:
nil, create: false)
let savedURL = documentURL.appendingPathComponent("\(self.patientName)_\(UUID().uuidString).pdf")
print(savedURL)
try FileManager.default.moveItem(at: fileURL, to:
savedURL)
// Update the report url
DispatchQueue.main.async {
completion(savedURL)
}
}
catch{
print("Error while writting")
}
}
downloadTask.resume()
}
}
struct ReportList: View {
#ObservedObject var reportLink : ReportViewModel
var body: some View {
NavigationView{
List{
ForEach(reportLink.trackReport) { report in
if let url = report.localFileUrl {
NavigationLink {
DisplayPDF(url: url)
} label: {
Text(report.name)
}
} else {
ReportRow(report: report, updateReport: updateReport)
}
}
// Moved out of ForEach
if(reportLink.trackReport.isEmpty)
{
Text("No Report Found")
.foregroundColor(.accentColor)
.fontWeight(.semibold)
}
}
}
}
func updateReport(withId reportId: String, url: URL) {
reportLink.updateReport(withId: reportId, url: url)
}
}
struct ReportRow: View {
var report: ReportResponse
var updateReport: (String, URL) -> ()
var body: some View {
VStack{
HStack{
Text(report.name)
.font(.system(size: 16))
.foregroundColor(.black)
.padding(.bottom,1)
.padding(.top,1)
}.frame(maxWidth: .infinity, alignment: .leading)
HStack{
Text("P.Id:")
.foregroundColor(.black)
.font(.system(size: 14))
Text(report.patientID)
.foregroundColor(.purple)
.font(.system(size: 14))
Spacer()
Button(action: {
let pdfDownload = PDFDownload(uniqueReportId: report.uniqueID, patientName: report.name)
pdfDownload.getFile(completion: updateReportUrl)
}, label:
{
Text("\(report.status)")
.foregroundColor(.blue)
.font(.system(size: 14))
.padding(.trailing,2)
}).frame(maxWidth: .infinity, alignment: .trailing)
}
}
}
func updateReportUrl(url: URL) {
updateReport(report.uniqueID, url)
}
}
struct DisplayPDF: View {
var url:URL
var body:some View
{
// Stub as I can not download
Text(url.absoluteString)
// PDFKitRepresentedView(url)
}
}
struct PDFKitRepresentedView: UIViewRepresentable{
func updateUIView(_ uiView: UIView, context:
UIViewRepresentableContext<PDFKitRepresentedView>) {
}
let url: URL
init(_ url:URL)
{
self.url = url
}
func makeUIView(context:
UIViewRepresentableContext<PDFKitRepresentedView>) ->
PDFKitRepresentedView.UIViewType {
let pdfView = PDFView()
pdfView.document = PDFDocument(url: self.url)
pdfView.autoScales = true
return pdfView
}
}
I am trying to use swiftUI to be able to create a custom EPUB reader. Ive looked around at some but none fit my needs. I want to be able to customize it. The issue I have ran into is being able to Highlight text while reading either Orange, blue, green, etc. When highlight a text and then the menu bar pops up and I click on my custom MenuBar color the app crashes. I found this article on highlighting text but uses the UIkit and not SwiftUI. Ive been trying to "translate"(not sure what the correct term is)it to use it with SwiftUI but crashes due to unrecognized selector.Im thinking I am not setting up the things correct. Not sure if it is worth using SwiftUI anymore and just switching my app to UIKit at this point since I have not been able to find many resources using swiftUI. Here is article to highlight text : https://dailong.medium.com/highlight-text-in-wkwebview-1659a19715e6
Just started learning swiftUI so not sure if the way the WebView is setup is correct.
Here is the gitHub link to all of the code https://github.com/longvudai/demo/tree/master/highlight-webview/highlight-webview With the SwiftUI all i did was copy and paste the files. Only difference is with SwiftUI was wrapping the WebView other than that everything else is the same.
SWIFTUI
`struct WebView: UIViewRepresentable {
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
var webView: CustomView?
var serializedObject: SerializedObject?
private var dataStack = Stack<Highlights>()
func webView(_ webView: WKWebView?, didFinish navigation: WKNavigation!) {
self.webView = webView as? CustomView
}
// receive message from wkwebview
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
if let markerHandler = MarkerScript.Handler(message) {
guard
let dataString = message.body as? String,
let data = dataString.data(using: .utf8)
else { return }
let decoder = JSONDecoder()
guard let serialized = try? decoder.decode(
SerializedObject.self,
from: data
) else { return }
receiveMarkerMessage(markerHandler, data: serialized)
}
}
func receiveMarkerMessage(_ handler: MarkerScript.Handler, data: SerializedObject) {
switch handler {
case .serialize:
serializedObject = data
// your callback here
let script = MarkerScript.Evaluate.clearSelection()
self.webView?.evaluateJavaScript(script)
case .erase:
serializedObject = data
let highlights = data.highlights
let listId = highlights.map { $0.id }
guard let top = dataStack.top else { return }
let newData = top.filter { listId.contains($0.id) }
if newData != top {
dataStack.push(newData)
}
}
}
func highlight(_ color: MarkerColor) {
let script =
MarkerScript.Evaluate.highlightSelectedTextWithColor(color)
webView?.evaluateJavaScript(script)
print("highlightfunction")
}
func removeAll() {
let script = MarkerScript.Evaluate.removeAllHighlights()
self.webView?.evaluateJavaScript(script)
dataStack.push([])
}
func erase() {
let script = MarkerScript.Evaluate.erase()
self.webView?.evaluateJavaScript(script)
}
#objc func highlightthiscolor() {
highlight(MarkerColor.orange)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> CustomView {
let coordinator = makeCoordinator()
let configuration = WKWebViewConfiguration()
let uc = configuration.userContentController
uc.addUserScript(WKUserScript.injectViewPort())
// Jquery
uc.addUserScript(JQueryScript.core())
// Rangy
uc.addUserScript(RangyScript.core())
uc.addUserScript(RangyScript.classapplier())
uc.addUserScript(RangyScript.highlighter())
uc.addUserScript(RangyScript.selectionsaverestore())
uc.addUserScript(RangyScript.textrange())
// Marker
uc.addUserScript(MarkerScript.css())
uc.addUserScript(MarkerScript.jsScript())
uc.add(coordinator, name: MarkerScript.Handler.serialize.rawValue)
uc.add(coordinator, name: MarkerScript.Handler.erase.rawValue)
let _wkwebview = CustomView(frame: .zero, configuration: configuration)
_wkwebview.navigationDelegate = coordinator
return _wkwebview
}
func updateUIView(_ webView: CustomView, context: Context) {
guard let path: String = Bundle.main.path(forResource: "sample", ofType: "html") else { return }
let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
addCustomContextMenu()
}
func addCustomContextMenu(){
//Has to be type of WKWebView
let colorOrange:UIMenuItem = UIMenuItem(title: "Orange", action: #selector(Coordinator.highlightthiscolor))
UIMenuController.shared.menuItems = [colorOrange]
}
}`
UIKit
protocol MarkerLogic {
func erase()
func highlight(_ color: MarkerColor)
func removeAll()
}
class Marker: NSObject {
weak var webView: WKWebView?
var serializedObject: SerializedObject?
private var dataStack = Stack<Highlights>()
}
extension Marker: MarkerLogic {
func highlight(_ color: MarkerColor) {
let script =
MarkerScript.Evaluate.highlightSelectedTextWithColor(color)
webView?.evaluateJavaScript(script)
}
func removeAll() {
let script = MarkerScript.Evaluate.removeAllHighlights()
webView?.evaluateJavaScript(script)
dataStack.push([])
}
func erase() {
let script = MarkerScript.Evaluate.erase()
webView?.evaluateJavaScript(script)
}
}
// MARK: - WKScriptMessageHandler
extension Marker: WKScriptMessageHandler {
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
if let markerHandler = MarkerScript.Handler(message) {
guard
let dataString = message.body as? String,
let data = dataString.data(using: .utf8)
else { return }
let decoder = JSONDecoder()
guard let serialized = try? decoder.decode(
SerializedObject.self,
from: data
) else { return }
receiveMarkerMessage(markerHandler, data: serialized)
}
}
func receiveMarkerMessage(_ handler: MarkerScript.Handler, data: SerializedObject) {
switch handler {
case .serialize:
serializedObject = data
// your callback here
let script = MarkerScript.Evaluate.clearSelection()
webView?.evaluateJavaScript(script)
case .erase:
serializedObject = data
let highlights = data.highlights
let listId = highlights.map { $0.id }
guard let top = dataStack.top else { return }
let newData = top.filter { listId.contains($0.id) }
if newData != top {
dataStack.push(newData)
}
}
}
}
--- ViewDidLoad
class ViewController: UIViewController, WKScriptMessageHandler {
let marker: Marker = Marker()
let orangeButton: UIButton = {
let v = UIButton()
v.tag = 0
v.backgroundColor = MarkerColor.orange.value
v.layer.cornerRadius = 10
v.addTarget(self, action: #selector(highlight(_:)), for: .touchUpInside)
return v
}()
let cyanButton: UIButton = {
let v = UIButton()
v.tag = 1
v.backgroundColor = MarkerColor.cyan.value
v.layer.cornerRadius = 10
v.addTarget(self, action: #selector(highlight(_:)), for: .touchUpInside)
return v
}()
let pinkButton: UIButton = {
let v = UIButton()
v.tag = 2
v.backgroundColor = MarkerColor.pink.value
v.layer.cornerRadius = 10
v.addTarget(self, action: #selector(highlight(_:)), for: .touchUpInside)
return v
}()
let eraseButton: UIButton = {
let v = UIButton()
v.setTitle("Erase", for: .normal)
v.setTitleColor(.systemBlue, for: .normal)
v.addTarget(self, action: #selector(erase), for: .touchUpInside)
return v
}()
let eraseAllButton: UIButton = {
let v = UIButton(type: .close)
v.addTarget(self, action: #selector(eraseAll), for: .touchUpInside)
return v
}()
lazy var toolBars: UIStackView = {
let v = UIStackView(arrangedSubviews: [orangeButton, cyanButton, pinkButton, eraseButton, eraseAllButton])
v.axis = .horizontal
v.distribution = .fillEqually
v.spacing = 20
return v
}()
// This is to make the makeUIView
lazy var webView: WKWebView = {
let config = WKWebViewConfiguration()
let uc = config.userContentController
uc.addUserScript(WKUserScript.injectViewPort())
// Jquery
uc.addUserScript(JQueryScript.core())
// Rangy
uc.addUserScript(RangyScript.core())
uc.addUserScript(RangyScript.classapplier())
uc.addUserScript(RangyScript.highlighter())
uc.addUserScript(RangyScript.selectionsaverestore())
uc.addUserScript(RangyScript.textrange())
// Marker
uc.addUserScript(MarkerScript.css())
uc.addUserScript(MarkerScript.jsScript())
uc.add(self.marker, name: MarkerScript.Handler.serialize.rawValue)
uc.add(self.marker, name: MarkerScript.Handler.erase.rawValue)
let v = WKWebView(frame: .zero, configuration: config)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
marker.webView = webView
let path = Bundle.main.path(forResource: "sample", ofType: "html")!
let url = URL(fileURLWithPath: path)
webView.loadFileURL(url, allowingReadAccessTo: url)
let views = [webView, toolBars]
views.forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
toolBars.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
toolBars.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
toolBars.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
toolBars.heightAnchor.constraint(equalToConstant: 40)
])
}
// MARK: - Selector
#objc func highlight(_ sender: UIButton) {
switch sender.tag {
case 0:
marker.highlight(MarkerColor.orange)
case 1:
marker.highlight(MarkerColor.cyan)
case 2:
marker.highlight(MarkerColor.pink)
default:
break
}
}
#objc func erase() {
marker.erase()
}
#objc func eraseAll() {
marker.removeAll()
}
// MARK: - WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
}
}
I was finally able to get the code working by looking at the post: Call evaluateJavascript from a SwiftUI button. The problem I was running into was the fact that I was not able to run the javascript func in order to highlight. Using Combine i was able to make a button in the View and when that button is clicked being able to run the javascript code. Will post the code below for anyone who is interested.
import WebKit
import SwiftUI
import Combine
class WebViewData: ObservableObject {
#Published var parsedText: NSAttributedString? = nil
var functionCaller = PassthroughSubject<Void,Never>()
var isInit = false
var shouldUpdateView = true
}
struct ContentView: View {
#StateObject var webViewData = WebViewData()
var body: some View {
VStack {
Button(action: {
webViewData.functionCaller.send()
}) {
Text("Orange")
}
WebView(data: webViewData)
}
}
}
struct WebView: UIViewRepresentable {
#StateObject var data: WebViewData
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
//var webView: WKWebView?
var serializedObject: SerializedObject?
private var dataStack = Stack<Highlights>()
var parent: WebView
var webView: WKWebView? = nil
private var cancellable : AnyCancellable?
init(view: WebView) {
self.parent = view
super.init()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.webView = webView
}
// receive message from wkwebview
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
if let markerHandler = MarkerScript.Handler(message) {
guard
let dataString = message.body as? String,
let data = dataString.data(using: .utf8)
else { return }
let decoder = JSONDecoder()
guard let serialized = try? decoder.decode(
SerializedObject.self,
from: data
) else { return }
receiveMarkerMessage(markerHandler, data: serialized)
}
}
func receiveMarkerMessage(_ handler: MarkerScript.Handler, data: SerializedObject) {
switch handler {
case .serialize:
serializedObject = data
// your callback here
let script = MarkerScript.Evaluate.clearSelection()
self.webView?.evaluateJavaScript(script)
case .erase:
serializedObject = data
let highlights = data.highlights
let listId = highlights.map { $0.id }
guard let top = dataStack.top else { return }
let newData = top.filter { listId.contains($0.id) }
if newData != top {
dataStack.push(newData)
}
}
}
func tieFunctionCaller(data: WebViewData) {
cancellable = data.functionCaller.sink(receiveValue: { _ in
self.webView?.evaluateJavaScript("highlightSelectedTextWithColor('orange')")
})
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(view: self)
}
func makeUIView(context: Context) -> WKWebView {
let coordinator = makeCoordinator()
let configuration = WKWebViewConfiguration()
let uc = configuration.userContentController
uc.addUserScript(WKUserScript.injectViewPort())
// Jquery
uc.addUserScript(JQueryScript.core())
// Rangy
uc.addUserScript(RangyScript.core())
uc.addUserScript(RangyScript.classapplier())
uc.addUserScript(RangyScript.highlighter())
uc.addUserScript(RangyScript.selectionsaverestore())
uc.addUserScript(RangyScript.textrange())
// Marker
uc.addUserScript(MarkerScript.css())
uc.addUserScript(MarkerScript.jsScript())
uc.add(coordinator, name: MarkerScript.Handler.serialize.rawValue)
uc.add(coordinator, name: MarkerScript.Handler.erase.rawValue)
let _wkwebview = WKWebView(frame: .zero, configuration: configuration)
_wkwebview.navigationDelegate = coordinator
return _wkwebview
}
func updateUIView(_ webView: WKWebView, context: Context) {
guard data.shouldUpdateView else {
data.shouldUpdateView = false
return
}
context.coordinator.tieFunctionCaller(data: data)
context.coordinator.webView = webView
guard let path: String = Bundle.main.path(forResource: "sample", ofType: "html") else { return }
let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
}
}
I found why the cache prompt will not be shown.
If I use the ImageView directly on the ContentView, the cache prompt will not show.
If I wrap the ImageView with a View, then use the wrapper view on the ContentView, the cache prompt will show.
Here is the working code in the ContentView.swift
struct ContentView: View {
var links =
[NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-0.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-1.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-2.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-3.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-4.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-5.jpg"),
NewsItem(urlString: "https://www.jobyme88.com/wp-content/uploads/2020/11/50d0-kj-classroom-6.jpg")]
var body: some View {
List(links) { news in
// working
NewsListItemView(item: news)
// not working
//NewsImageView(urlString: news.urlString)
}
}
}
This is the NewsListItemView which is just a wrapper
struct NewsListItemView: View {
var item: NewsItem
var body: some View {
NewsImageView(urlString: item.urlString)
}
}
This is my cache prompt location.
NewsImageViewModel.swift
class NewsImageViewModel: ObservableObject {
static var placeholder = UIImage(named: "NewsIcon.png")
#Published var image: UIImage?
var urlString: String?
init(urlString: String) {
self.urlString = urlString
loadImage()
}
func loadImage() {
if loadImageFromCache() {
return
}
loadImageFromURL()
}
func loadImageFromCache() -> Bool {
guard let cacheIamge = TemporaryImageCache.getShared()[urlString!] else {
return false
}
print("load from cache")
self.image = cacheIamge
return true
}
func loadImageFromURL() {
print("load from url")
guard let urlString = urlString else {
return
}
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url, completionHandler: getResponseFromURL(data:response:error:))
task.resume()
}
func getResponseFromURL(data: Data?, response: URLResponse?, error: Error?) {
guard error == nil else {
print("Error \(error!)")
return
}
guard data != nil else {
print("No founded data")
return
}
DispatchQueue.main.async {
guard let loadedImage = UIImage(data: data!) else {
print("Not supported data ")
return
}
self.image = loadedImage
TemporaryImageCache.getShared().cache.setObject(loadedImage, forKey: self.urlString! as NSString)
}
}
}
NewsImageView.swift
import SwiftUI
struct NewsImageView: View {
#ObservedObject var model: NewsImageViewModel
init(urlString: String) {
model = NewsImageViewModel(urlString: urlString)
}
var body: some View {
Image(uiImage: model.image ?? NewsImageViewModel.placeholder!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100, alignment: .center)
}
}
NewsListItemView.swift
struct NewsListItemView: View {
var item: NewsItem
var body: some View {
NewsImageView(urlString: item.urlString)
}
}
This is ImageCache.swift
protocol ImageCache {
subscript(_ urlString: String) -> UIImage? {get set }
}
struct TemporaryImageCache: ImageCache {
subscript(urlString: String) -> UIImage? {
get {
cache.object(forKey: urlString as NSString)
}
set {
newValue == nil ? cache.removeObject(forKey: urlString as NSString) : cache.setObject(newValue!, forKey: urlString as NSString)
}
}
var cache = NSCache<NSString, UIImage>()
}
extension TemporaryImageCache {
private static var shared = TemporaryImageCache()
static func getShared() -> TemporaryImageCache {
return shared
}
}
This is NewsItem.swift
struct NewsItem: Identifiable {
var id = UUID()
var urlString: String
}
I want to make a login code screen. This consists of 4 separate UITextField elements, each accepting one character. What I did is implement a system whereby every time one of the UITextField's changes it will verify if all the values are filled out, and if they are update a boolean binding to tell the parent object that the code is correct.
In order to do this I wrap the #State variables inside a custom binding that does a callback on the setter, like this:
#State private var chars:[String] = ["","","",""]
...
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
and those bindings are passed to the components. Every time my text value changes validateCode is called. This works perfectly.
However now I want to add an extra behavior: If the user types 4 characters and the code is wrong I want to move the first responder back to the first textfield and clear its contents. The first responder part works fine (I also manage that using #State variables, but I do not use a binding wrapper for those), however I can't change the text inside my code. I think it's because my components use that wrapped binding, and not the variable containing the text.
This is what my validateCode looks like:
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.chars = ["","","",""]
}
}
hasFocus does its thing correctly and the cursor is being moved to the first UITextField. The text however remains in the text fields. I tried creating those bindings in the init so I could also use them in my validateCode function but that gives all kinds of compile errors because I am using self inside the getter and the setter.
Any idea how to solve this? Should I work with Observables? I'm just starting out with SwiftUI so it's possible I am missing some tools that I can use for this.
For completeness, here is the code of the entire file:
import SwiftUI
struct CWCodeView: View {
var value:String
#Binding var isValid:Bool
#State private var chars:[String] = ["","","",""]
#State private var hasFocus = [true,false,false,false]
#State private var nothingHasFocus:Bool = false
init(value:String,isValid:Binding<Bool>) {
self.value = value
self._isValid = isValid
}
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.nothingHasFocus = false
self.chars = ["","","",""]
}
}
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
return GeometryReader { geometry in
ScrollView (.vertical){
VStack{
HStack {
CWNumberField(letter: bindings[0],hasFocus: self.$hasFocus[0], previousHasFocus: self.$nothingHasFocus, nextHasFocus: self.$hasFocus[1])
CWNumberField(letter: bindings[1],hasFocus: self.$hasFocus[1], previousHasFocus: self.$hasFocus[0], nextHasFocus: self.$hasFocus[2])
CWNumberField(letter: bindings[2],hasFocus: self.$hasFocus[2], previousHasFocus: self.$hasFocus[1], nextHasFocus: self.$hasFocus[3])
CWNumberField(letter: bindings[3],hasFocus: self.$hasFocus[3], previousHasFocus: self.$hasFocus[2], nextHasFocus: self.$nothingHasFocus)
}
}
.frame(width: geometry.size.width)
.frame(height: geometry.size.height)
.modifier(AdaptsToSoftwareKeyboard())
}
}
}
}
struct CWCodeView_Previews: PreviewProvider {
static var previews: some View {
CWCodeView(value: "1000", isValid: .constant(false))
}
}
struct CWNumberField : View {
#Binding var letter:String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
var body: some View {
CWSingleCharacterTextField(character:$letter,hasFocus: $hasFocus, previousHasFocus: $previousHasFocus, nextHasFocus: $nextHasFocus)
.frame(width: 46,height:56)
.keyboardType(.numberPad)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.init("codeBorder"), lineWidth: 1)
)
}
}
struct CWSingleCharacterTextField : UIViewRepresentable {
#Binding var character: String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
func makeUIView(context: Context) -> UITextField {
let textField = UITextField.init()
//textField.isSecureTextEntry = true
textField.keyboardType = .numberPad
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 16)
textField.tintColor = .black
textField.text = character
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if hasFocus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UITextFieldDelegate {
var parent:CWSingleCharacterTextField
init(_ parent:CWSingleCharacterTextField) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let result = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if result.count > 0 {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.nextHasFocus = true
}
} else {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.previousHasFocus = true
}
}
if result.count <= 1 {
parent.character = string
return true
}
return false
}
}
}
Thanks!
you just make a little mistake, but i cannot believe you just "started" SwiftUI ;)
1.) just build textfield one time, so i took it as a member variable instead of building always a new one
2.) update the text in updateuiview -> that's it
3.) ...nearly: there is still a focus/update problem...the last of the four textfields won't update correctly ...i assume this is a focus problem....
try this:
struct CWSingleCharacterTextField : UIViewRepresentable {
#Binding var character: String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
let textField = UITextField.init()
func makeUIView(context: Context) -> UITextField {
//textField.isSecureTextEntry = true
textField.keyboardType = .numberPad
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 16)
textField.tintColor = .black
textField.text = character
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = character
if hasFocus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UITextFieldDelegate {
var parent:CWSingleCharacterTextField
init(_ parent:CWSingleCharacterTextField) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let result = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if result.count > 0 {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.nextHasFocus = true
}
} else {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.previousHasFocus = true
}
}
if result.count <= 1 {
parent.character = string
return true
}
return false
}
}
}
I consider how to create SwitUI List that has as its row custom UIViews.
I create List:
List {
RowView()
}
RowView is UIViewRepresentable of UIRowView
struct RowView : UIViewRepresentable {
func makeUIView() -> UIRowView { ... }
}
UIRowView is custom view
UIRowView: UIView { ... }
Currently first rows are displayed but they are usually not layout properly and while scrolling this views disappear instead of being recycled
UPDATE
Example 1
struct NoteView: UIViewRepresentable {
// MARK: - Properties
let note: Note
let date = Date()
func makeUIView(context: Context) -> UINoteView {
let view = UINoteView()
view.note = note
return view
}
func updateUIView(_ uiView: UINoteView, context: Context) {
uiView.note = note
print("View bounds: \(uiView.bounds)")
}
}
var body: some View {
List {
ForEach(Array(notes.enumerated()), id: \.1) { (i, note) in
NoteView(note: note)
.background(Color.green)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}.background(Color.red)
}
Example 2 - Simplified
struct TestView : UIViewRepresentable {
let text : String
func makeUIView(context: Context) -> UILabel {
UILabel()
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
}
}
var body: some View {
List {
ForEach(0..<30, id: \.self) { i in
TestView(text: "\(i)")
}
}
}
Both seems to work incorrectly, as rows dissapears
I had also issue with views not keeping padding and going outside of the screen if there was more content. Only several first rows (visible initially on screen layouts correctly) other disappears or jump somewhere.
UPDATE 2
Here is Autosizable UINoteView
class UINoteView: UIView {
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupViews()
}
// MARK: - Properties
var note: Note? {
didSet {
textView.attributedText = note?.content?.parsedHtmlAttributedString(textStyle: .html)
noteFooterViewModel.note = note
}
}
// MARK: - Views
lazy var textView: UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.backgroundColor = UIColor.yellow
textView.textContainer.lineBreakMode = .byWordWrapping
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.isScrollEnabled = false
textView.isSelectable = true
textView.isUserInteractionEnabled = true
textView.isEditable = false
textView.textContainer.maximumNumberOfLines = 0
return textView
}()
lazy var label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "TEST ROW \(note?.id ?? "")"
return label
}()
lazy var vStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [
textView,
noteFooter
])
stack.axis = .vertical
stack.alignment = .fill
stack.distribution = .fill
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
var noteFooterViewModel = NoteFooterViewModel()
var noteFooter: UIView {
let footer = NoteFooter(viewModel: noteFooterViewModel)
let hosting = UIHostingController(rootView: footer)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
return hosting.view
}
private func setupViews() {
self.backgroundColor = UIColor.green
self.addSubview(vStack)
NSLayoutConstraint.activate([
vStack.leadingAnchor.constraint(equalTo: self.leadingAnchor),
vStack.trailingAnchor.constraint(equalTo: self.trailingAnchor),
vStack.topAnchor.constraint(equalTo: self.topAnchor),
vStack.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}
}
UPDATED ANSWER
try this:
struct TestView : UIViewRepresentable {
let text : String
var label : UILabel = UILabel()
func makeUIView(context: Context) -> UILabel {
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
}
}
struct ContentView : View {
var body: some View {
List (0..<30, id: \.self) { i in
TestView(text: "\(i)").id(i)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I did like below. You can get the frame size even without UIViewRepresentable by getTextFrame(for note)
var body: some View {
List {
ForEach(Array(notes.enumerated()), id: \.1) { (i, note) in
NoteView(note: note)
// add this
.frame(width: getTextFrame(for: note).width, height: getTextFrame(for: note).height)
.background(Color.green)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}.background(Color.red)
}
func getTextFrame(for text: String, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> CGSize {
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.preferredFont(forTextStyle: .body)
]
let attributedText = NSAttributedString(string: text, attributes: attributes)
let width = maxWidth != nil ? min(maxWidth!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
let height = maxHeight != nil ? min(maxHeight!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
let constraintBox = CGSize(width: width, height: height)
let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral
print(rect.size)
return rect.size
}
struct NoteView: UIViewRepresentable {
let note: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = UIFont.preferredFont(forTextStyle: .body)
textView.isEditable = false
textView.isSelectable = true
textView.isUserInteractionEnabled = true
textView.isScrollEnabled = false
textView.backgroundColor = .clear
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.textContainer.lineBreakMode = .byWordWrapping
textView.text = note
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}