Rendering a View as an image in SwiftUI project - ios

I am currently handling an end-to-end SwiftUI project which involves a ClockKit complication. As one of complication's templates requires an image (UIImage) I need to render (snapshot) an existing SwiftUI View to an image. I have found a couple of possible solutions to this problem, including the following View extension:
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
The problem I have is that wherever I implement this extension I keep getting "not in scope" Xcode errors ("Cannot find 'UIHostingController' in scope", "Cannot find 'UIGraphicsImageRenderer' in scope", "Cannot infer contextual base in reference to member 'clear'").
SwiftUI and UIKit are imported in these Swift files (but I think the latter shouldn't be necessary). Xcode 12.5.
Please do share some tips that could help overcome this hurdle... I am a rather inexperienced developer and I could be missing something rather obvious, but at this point I'm pulling my hair out. Whenever I implement a reasonable solution that happened to be in other questions or websites it always comes to UIKit specific commands not being in scope.
Edit: So it turns out this extension works only if implemented in main ContentView for iOS target. Does not work if put in any other directory. However I need this extension to work in files of (watchOS) Extension files (as I said it's needed for CK Complication) and it's not possible to set target membership of the main iOS ContentView also for (watchOS) Extension, as then two ContentViews would apply to one watchOS version and error occurs. How then effectively place the above extension code so it could apply for where it's needed?

The problem for the errors pop up (like Cannot find UIHostingController in scope and others) is because UIHostingController and UIGraphicsImageRenderer are not available on watchOS, meaning the extension, as written, will not compile on that platform.
Refer to my other answered question.

Related

PDFView causes app to freeze on iOS 16, only when autoscales = true

I am using a PDFView to display images in my app (built using SwifUI), simply for quick and easy pinch-to-zoom functionality. This worked perfectly in iOS 15, but since updating to iOS 16, the app freezes when attempting to load the image viewer (PhotoDetailView below). The issue persists across both the simulator and a physical device.
Here is the code I'm using:
import SwiftUI
import PDFKit
struct PhotoDetailView: UIViewRepresentable {
let image: UIImage
func makeUIView(context: Context) -> PDFView {
let view = PDFView()
view.document = PDFDocument()
guard let page = PDFPage(image: image) else { return view }
view.document?.insert(page, at: 0)
view.autoScales = true
return view
}
func updateUIView(_ uiView: PDFView, context: Context) {
// empty
}
}
When I run the code on iOS 16.0 in the simulator, I get 2 errors in the console:
[Assert] -[UIScrollView _clampedZoomScale:allowRubberbanding:]: Must be called with non-zero scale
[Unknown process name] CGAffineTransformInvert: singular matrix.
I have been able to isolate the issue to view.autoscales = true. If I print view.scaleFactor, I can see that it is 1.0 before the autoscale and 0.0 afterward (which is what appears to be prompting the errors). These errors also show up in the console when using iOS 15 in the simulator, but the images load as expected.
When view.autoscales = true is commented out, the image loads, albeit at a size that is much larger than the device screen.
Does anyone have any idea what may be causing this? I'd really like to avoid having to build a custom viewer, since I'm just trying to let users quickly pinch to zoom on images.
I managed to resolve this issue. I'm posting my solution here, in case anyone else runs into the same type of problem.
The issue only occurred when using a NavigationLink to navigate to PhotoDetailView. This led me to look more closely at my navigation stack.
After some digging, I found that the problem was related to my use of .navigationViewStyle(.stack) on the NavigationView. I needed this modifier to get things to display correctly on iOS 15, but the same modifier was breaking my image viewer with iOS 16.
My solution was to create a custom container to replace NavigationView, which conditionally uses NavigationStack for iOS 16, or NavigationView + .navigationViewStyle(.stack) for iOS 15. That worked like a charm and my image viewer is back in action.
I found inspiration for my custom container here: https://developer.apple.com/forums/thread/710377

Swift Playground Protocol Crashing after Fixed Number of Iterations

For this year's WWDC scholarship, the format required is a Swift playground. I'm building my playground in an app, where I have sliders in UITableViewCells connected to a SCNScene and SCNNode and SCNParticleSystem via a custom delegate. It works perfectly fine running as an app, running on the Mac, but when it comes to the running it in swift playgrounds on the iPad, the delegate method crashes after a constant number of iterations. I have determined it is not the method in the node itself, as I made this print("Hello") and it still crashed with the exact same 96 iterations. The node has been in both Source files and the main playground file. Any ideas or help would be greatly appreciated.
Delegate
protocol ReturnParameterDelegate {
func parameter1(value: Float)
func parameter2(value: Float)
func defaultParameter(value: Float)
}
Table View Cell
#objc private func returnValue() {
guard let delegate = delegate else { return }
print(self.parameter.title)
switch self.parameter.title {
case ParameterNames.parameter1:
delegate.parameter1(value: self.slider.value)
...
}
}
Scene
public func parameter1(value: Float) {
node.changeValue(value: value)
}
...
After many hours of work and trying this with other methods, I have found a simple solution: move code from the main .playground to separate .swift files in the Sources folder. While this shouldn't change anything, adding it to the sources made all of my methods work perfectly. This does complicate protocols and I replaced them with creating a global object instead, however. It does remove the crash after a constant number of iterations and it makes certain other functions work that were not previously (for example, changing the diffuse of a SCNNode).
I move everything to source file. And it works a little bit better.

PlaygroundBook liveView display a SceneView error

I am currently playing with PlaygroundBook. From the Doc, it says that you can use LiveView.swift to present a always-on View in the liveView. So I create a SceneViewController in the Source Folder. In the SceneViewController, I create a Sphere which lightingModel is physicallyBased, and I using a 3D map as the background contents.
Then in the LiveView.swift
import PlaygroundSupport
let page = PlaygroundPage.current
page.current.liveView = SceneViewController()
But when I run it in the iPad Air Playground. I get no error, but the liveView didn't show anything!
I spend many years finding errors. And I have try to create a new iOS Project using the same SceneViewController, and it work very well. I got no idea.
In the last, I try to change the lightingModel to .phong, and change the background to the white color.
Then the magic thing happened! The liveView now show a black sphere and the white background.
Why?
From the WWDC Session, I learn that I can use proxy to communicate with main process in Contents.swift and LiveView process.
So next, I make a extension.
extension SceneViewController: PlaygroundLiveViewMessageHandler {
public func receive(_ message: PlaygroundValue) {
if case let .string(_) = message {
changeColor()
}
}
}
And inside the Contents.swift, I create a new function
func addSky() {
if let proxy = page.liveView as? PlaygroundRemoteLiveViewProxy {
proxy.send(.string("addSky"))
}
}
And everyTime I call it, The liveView changed, see the image below. It's the same as The first time I try to use a 3D map as the background.
Does anyone know what does it happened ?

Limited number of views in Swift Playgrounds

When creating a new Swift Playground / .playgroundbook intended to be used on the iPad App, I often received the error message:
"Problem running playground. There was a problem encountered while running this playground. Check your code for mistakes."
I could track this issue down to be caused when adding certain subviews to my live view. To be more precise, my goal is to split a UIImage into multiple parts and create new UIImageViews for them:
for x in 0..<parts {
for y in 0..<parts {
//Create UIImageView with cropped image
let pieceView = UIImageView.init(frame: CGRect.init(x: CGFloat(x)*singleSize.width, y:CGFloat(y)*singleSize.height, width: singleSize.width, height: singleSize.height))
let imageRef = image.cgImage!.cropping(to: CGRect.init(x:0, y:0, width: 100, height: 100));
pieceView.image = UIImage.init(cgImage: imageRef!)
//Add them to an array
self.viewArray.append(pieceView)
}
}
And that's where things become very tricky for me: Adding 7 of these UIImageViews now works without a problem. But as soon as I want to add 8 or more of them, the playground stops working and gives the error message "Problem running playground..." (see above)
What I tested so far:
Adding UIImageViews with the same image does not cause this problem
Cropping the UIImage in a background thread and adding the view on the main thread does not help either
Creating the UIImageViews without adding them to the live-view does not cause any problems
The code works well when being executed on a mac playground, no matter how man views to add
I experienced this kind of iPad Swift Playground run-time error while adding multiple UI elements.
The problem caused by the default setting of "Enable Results" in the playground's property which is set to be ON. The "Enable Results" previews all the in-line object results' viewer. It makes the swift playground crashed when you produce many UI elements.
Try to disable the "Enable Results". It works for me.
I saw a similar issue, and the problem was that it was just a nil incorrectly used.
In order to get a clear error message, just create a playground with the same files, and run it on the mac: in this way you can get more detailed information about what is going on and find easier to solve your issue.
Let me know if you find any difficulties :)
Probably is a late answer... but I was experiencing same issue during last week... finally today just figure it out how to solve:
I was running a piece of code where I add a background view as next:
func createView(){
// gray background
let marco = CGRect(x:0, y: 0, width: 603, height: 825)
vista = UIView(frame: marco)
vista.backgroundColor = UIColor.gray
vista.isUserInteractionEnabled = true
vista.tag = 5
PlaygroundPage.current.liveView = vista
PlaygroundPage.current.needsIndefiniteExecution = true
}
But I place this at the beginning of the code
What I have changed is :
PlaygroundPage.current.liveView = vista
PlaygroundPage.current.needsIndefiniteExecution = true
Placing it at the end of all the code, before you start to run your program... if you need more detail let me know and can share more information.

Swift iOS playground: Error sending deferral props

With an iOS playground set up as simply as this:
import UIKit
import SpriteKit
import XCPlayground
let s = CGSize(width: 300, height: 300)
let f = CGRect(origin: CGPointZero, size: s)
let view = SKView(frame: f)
let scene = SKScene(size: s)
scene.backgroundColor = SKColor.redColor()
view.presentScene(scene)
XCPShowView("main view", view)
I'm getting this in the console:
2014-09-04 17:02:13.358 SpriteKitBETA7[2009:20695] Error sending deferral props: 0x10000003
There IS a "main view" box in the Assistant Editor thing, but it doesn't display anything. This exact same code (with import Cocoa instead of import UIKit) works perfectly on an OSX playground.
I am aware I can just test stuff in an OSX playground (though it would be more convenient on an iOS one since I don't want to use Yosemite but I do have the iOS7 SDK) and copy-paste to my project, but I wanted to know if anyone understood what's happening here.
Per the Xcode 6 Release Notes, you will want to enable Run in Full Simulator in the File Inspector. Keep in mind this will run kind of slow since it is running through iOS Simulator. You'll have to wait a while for the iOS Simulator to launch before your XCPShowView starts displaying in the playground's Timeline.
In the Xcode menu bar, go to View > Utilities > Show File Inspector (⌘+⌥+1)
In the File Inspector on the right, under Playground Settings, make sure the Platform is set to iOS
Make sure Run in Full Simulator is checked.
If you mouse over the Run in Full Simulator option it explains that you should use this with views that animate or use OpenGL. Both of which apply to using either SceneKit or SpriteKit.
What did you see? I got as far as seeing a scene view with a color background I want. Try this:
import UIKit
import SceneKit
import QuartzCore
import XCPlayground
let view = SCNView(frame: CGRectMake(0, 0, 300, 300))
let myScene = SCNScene()
view.scene = myScene
view.backgroundColor = UIColor.blueColor()
view.backgroundColor = UIColor.yellowColor()
let root = view.scene?.rootNode
XCPShowView("The Scene View", view)
You can test it by diff color, and it rendered it. However, if i add a sphere to the root node, nothing happens. And I also see this in my Console Output:
2014-09-13 16:59:35.766 SceneKitPlayground[23682:13804922] Error sending deferral props: 0x10000003
Additionally, if I switch the thing to OS X, the same code that add the sphere will work.
I managed to get it running by opening the file inspector, changing the playground settings to OSX, then back to iOS and checking 'Run in Full Simulator'. This is in XCode 6.1.

Resources