It happens a lot that code evaluation in Playgrounds (the labels that say you have compile bugs or warnings in your code) gets stuck. So in order to fix them I sometimes must force quit the whole Playgrounds app. Sometimes I even have to completely restart iPadOS.
I know triggering Build in Xcode usually fixes those problems in Xcode... so I wonder: is there, or, why isn’t there… a Playgrounds alternative to Xcode’s Build command?
(Using Playgrounds 4.0.2 on iPadOS 15.4.1 running on the M1 iPad Pro)
Post
Replies
Boosts
Views
Activity
Do I really have to use Bundle to use resources? Or can it be done easier?
I have this JSON file resource in my Playground and am wondering about the least amount of code necessary to load its data into a dictionary.
I’m currently using this Bundle extension:
extension Bundle {
func decode(_ file: String) -> [String: MyType] {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) form bundle.")
}
let decoder = JSONDecoder()
guard let loaded = try? decoder.decode([String: MyType].self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}
return loaded
}
}
This type:
struct MyType: Codable, Identifiable {
let id: String
let name: String
let description: String
}
Using this to load it into a struct:
let myData = Bundle.main.decode("MyData.json")
There must be an easier way to access resources in Playgrounds… like image assets for example, it’s just Image(“My Image File Name Here”) and done…
I've updated Xcode to 13.2 and suddenly the code completion that used to turn numberOfRowsInSection into
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//...
}
...inside a UITableViewController is no longer working.
Anyone else experiencing this?
In Playgrounds on iPadOS, what’s the difference between these 2 types of playgrounds?
For the life of me I cannot find any documentation about this.
In the following code, I'm loading HTML pages into a WKWebView, only to get the contents of their first H1 tag. Other than what *any type of page* renders in its first H1 tag, I don't need anything else...
Is there perhaps a more efficient/performant way besides initializing a WKWebView for this?
(the .frame(width: 0, height: 0) modifier annoys me a bit, as well)
swift
import SwiftUI
import WebKit
import WebView /* add this Swift Package Dependency https://github.com/kylehickinson/SwiftUI-WebView */
struct ContentView: View {
@StateObject private var viewModel: ContentViewModel
init() {
let viewModel = ContentViewModel()
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
VStack(spacing: 30) {
Button("load plain H1 tag") {
viewModel.webViewStore.webView.loadHTMLString("h1H1/h1", baseURL: nil)
}
Button("load body where h1 is appended by JavaScript") {
viewModel.webViewStore.webView.loadHTMLString("body/bodyscript(function(){ let h1 = document.createElement('h1'); h1.textContent = 'H1 JavaScript'; document.body.appendChild(h1); })()/script", baseURL: nil)
}
Button("load apple.com/iphone") {
viewModel.webViewStore.webView.load(URLRequest(url: URL(string: "https://apple.com/iphone")!))
}
Button("load hackingwithswift.com") {
viewModel.webViewStore.webView.load(URLRequest(url: URL(string: "https://hackingwithswift.com")!))
}
Group {
Text("the first H1 element on ") Text(viewModel.webViewStore.webView.url?.description ?? "...").foregroundColor(.accentColor)
+ Text(" is")
}
.font(.subheadline)
Text(viewModel.h1)
.font(.largeTitle)
.foregroundColor(.green)
WebView(webView: viewModel.webViewStore.webView)
.frame(width: 0, height: 0)
}
}
}
class ContentViewModel: NSObject, WKScriptMessageHandler, ObservableObject {
@Published var h1 = "..."
@Published var webViewStore: WebViewStore
override init() {
let userContentController = WKUserContentController()
let configuration = WKWebViewConfiguration()
let userScript = WKUserScript(
source: """
function elementReady(selector) {
return new Promise((resolve, reject) = {
let el = document.querySelector(selector)
if (el) {
resolve(el)
}
new MutationObserver((mutationRecords, observer) = {
Array.from(document.querySelectorAll(selector)).forEach((element) = {
resolve(element)
observer.disconnect()
})
}).observe(document.documentElement, {
childList: true,
subtree: true,
})
})
}
elementReady("h1").then((titleElement) = {
window.webkit.messageHandlers.h1.postMessage(titleElement.textContent)
})
""",
injectionTime: .atDocumentStart,
forMainFrameOnly: false,
in: .defaultClient
)
userContentController.addUserScript(userScript)
configuration.userContentController = userContentController
let webView = WKWebView(frame: .zero, configuration: configuration)
webViewStore = WebViewStore(webView: webView)
super.init()
userContentController.add(self, contentWorld: .defaultClient, name: "h1")
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "h1" {
h1 = message.body as! String
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
SwiftUI question: (How) Can the myClass instance in this View interact with a State variable?
Not working:
struct MyView: View {
@State private var hello = "world"
let myClass = MyClass()
class MyClass: NSObject, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "hello"{
hello = message.body
}
}
// ...
}
by the way, I’m adding myClass to a webview’s configuration.userContentController. The userContentController in the class gets called when javascript calls window.webkit.messageHandlers.hello.postMessage("new world")
Thank you kindly
I'm trying to observe an audio element on a webpage. How do I best keep it's "JS" data in sync with SwiftUI?
Here's the start of my view:
struct ContentView: View {
// @State private var webAudioElementFound = false
@State private var paused = true
@State private var currentTime = 0.0
@State private var duration = 0.0
private let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
@StateObject private var webViewStore: WebViewStore
private let userContentController = WKUserContentController()
private let configuration = WKWebViewConfiguration()
private let userScript = WKUserScript(
source: """
function elementReady(selector) {
return new Promise((resolve, reject) => {
let el = document.querySelector(selector)
if (el) {
resolve(el)
}
new MutationObserver((mutationRecords, observer) => {
Array.from(document.querySelectorAll(selector)).forEach((element) => {
resolve(element)
observer.disconnect()
})
}).observe(document.documentElement, {
childList: true,
subtree: true,
})
})
}
window.webAudioPromise = elementReady("audio").then((webAudioElement) => {
window.webAudioElement = webAudioElement
})
""",
injectionTime: .atDocumentStart,
forMainFrameOnly: false,
in: .defaultClient
)
init() {
userContentController.addUserScript(userScript)
configuration.userContentController = userContentController
configuration.mediaTypesRequiringUserActionForPlayback = []
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.customUserAgent = "Mozilla"
_webViewStore = StateObject(wrappedValue: WebViewStore(webView: webView))
}
the body:
WebView(webView: webViewStore.webView)
.onReceive(timer, perform: webViewTimer)
and a the webViewTimer method:
func webViewTimer(time: Date) {
checkPlayback()
if !webViewStore.isLoading && !paused {
webViewStore.webView.evaluateJavaScript("[webAudioElement.duration, webAudioElement.currentTime]",
in: nil, in: .defaultClient,
completionHandler: { result in
switch result {
case .success(let value):
guard let array = value as? [Double] else { return }
duration = array[0]
currentTime = array[1]
case .failure(let failure):
print("timer failure: \(failure.localizedDescription)")
break
}
})
}
}
As you can see, I'm using Timer, but surely there must be a better way no?
Is there such a setting?
Thank you
Why is self unresolved in Playgrounds?
import Foundation
func startLoad() {
let url = URL(string: "https://www.apple.com/")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
self.handleClientError(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
self.handleServerError(response)
return
}
if let mimeType = httpResponse.mimeType, mimeType == "text/html",
let data = data,
let string = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.webView.loadHTMLString(string, baseURL: url)
}
}
}
task.resume()
}
startLoad()