SwiftUI NSViewRepresentable can't read data from #Publisher - swift

I'm writing a ChartView using NSView with data obtained from a rest api using Combine. The struct PlotView is the SwiftUI View that displays the chart, ChartViewRepresentable is the bridge between the NSView with the chart and the SwiftUI world and ChartView is the view that I actually draw on.
RestRequest gets the data from the network correctly and PlotView has access to it with no issues. When the data is received a ChartViewRepresentable is created and it contains the data, and ChartViewRepresentable creates a ChartView with the data and the data is stored in its data property correctly.
There are two problems: 1) the view's draw method never gets called when the data is loaded, and 2) if the view is redrawn a new ChartViewRepresentable (with a new ChartView) is created by SwiftUI but with no data.
I have connected the RestRequest #StateObject in every possible way imaginable, using #Binding, using #State, with no luck so far, so I'm discounting it as the problem, but with SwiftUI who really knows. It doesn't matter how I load the data, even loading the data manually into ChartView, it never calls the draw method on its own when receiving the data, and then when I for example resize the window to force a draw call it does call the draw method but on a new ChartViewRepresentable struct with no data in it.
What am I doing wrong? This is all the code besides the RestRequest() struct which I know works because I have been using it reliably on other views until now. Any clue or even a hint would be greatly appreciated.
struct PlotView: View {
#StateObject var request = RestRequest()
var body: some View {
Group {
ChartViewRepresentable(data: ChartData(array: ChartData.createArray(from: request.response.data)))
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
}
.onAppear{
let params: [String: String] = [
"limit": "10",
]
request.perform(endPoint: "http://localhost:4000/api/testdata", parameters: params)
}
}
}
struct ChartViewRepresentable: NSViewRepresentable {
typealias NSViewType = ChartView
var chart: ChartView
init(data: ChartData) {
chart = ChartView(data: data)
}
func makeNSView(context: Context) -> ChartView {
return chart
}
func updateNSView(_ nsView: ChartView, context: Context) {
}
}
class ChartView: NSView {
private var data: ChartData
init(data: ChartData) {
self.data = data
print("\(data)")
super.init(frame: .zero)
wantsLayer = true
layer?.backgroundColor = .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
print("draw call - Frame: \(self.frame), Data: \(data.array.count)")
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current else { return }
context.saveGraphicsState()
if data.array.count > 0 {
//detect data present on ChartView
let ctx = context.cgContext
ctx.setFillColor(NSColor.green.cgColor)
ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 10, height: 10))
}
context.restoreGraphicsState()
}
}

Right now, in your ChartViewRepresentable, you set the data in init, and then never touch it again.
This means that your ChartView will have its data set before your onAppear API call ever runs and returns data.
To fix this, you'll need to make use of the updateNSView function.
struct ChartViewRepresentable: NSViewRepresentable {
typealias NSViewType = ChartView
var data: ChartData //store the data -- not the chart view
func makeNSView(context: Context) -> ChartView {
return ChartView(data: data)
}
func updateNSView(_ chart: ChartView, context: Context) {
chart.data = data //update the chart view's data
}
}
And, you'll need to respond to the update of that data and force a redraw:
class ChartView: NSView {
var data: ChartData {
didSet {
self.needsDisplay = true //<-- Here
}
}
init(data: ChartData) {
self.data = data
print("\(data)")
super.init(frame: .zero)
wantsLayer = true
layer?.backgroundColor = .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
print("draw call - Frame: \(self.frame), Data: \(data.array.count)")
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current else { return }
context.saveGraphicsState()
if data.array.count > 0 {
//detect data present on ChartView
let ctx = context.cgContext
ctx.setFillColor(NSColor.green.cgColor)
ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 10, height: 10))
}
context.restoreGraphicsState()
}
}
Full working example with the API call mocked:
struct ChartData {
var array : [Int]
}
struct ContentView: View {
#State var chartData : ChartData = ChartData(array: [])
var body: some View {
Group {
ChartViewRepresentable(data: chartData)
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
}
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("New data!")
chartData = ChartData(array: [1,2,3,4])
}
}
}
}
struct ChartViewRepresentable: NSViewRepresentable {
typealias NSViewType = ChartView
var data: ChartData
func makeNSView(context: Context) -> ChartView {
return ChartView(data: data)
}
func updateNSView(_ chart: ChartView, context: Context) {
chart.data = data
}
}
class ChartView: NSView {
var data: ChartData {
didSet {
self.needsDisplay = true
}
}
init(data: ChartData) {
self.data = data
print("\(data)")
super.init(frame: .zero)
wantsLayer = true
layer?.backgroundColor = .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
print("draw call - Frame: \(self.frame), Data: \(data.array.count)")
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current else { return }
context.saveGraphicsState()
if data.array.count > 0 {
//detect data present on ChartView
let ctx = context.cgContext
ctx.setFillColor(NSColor.green.cgColor)
ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 10, height: 10))
}
context.restoreGraphicsState()
}
}

Related

NSButton's #objc action not being triggered in Swift for MacOS app

I have an NSButton in my ViewController class that is not triggering its click function (closeButtonPressed) when clicked.
I would test with a Button but UIKit isn't available for my project, as it's a non-Catalyst app for Mac.
As this has been a challenging and time-consuming issue, I am grateful for your time and expertise in helping me resolve it.
Minimal reproducible example
import SwiftUI
#main
struct MinimalReproducibleExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import Cocoa
import SwiftUI
struct ViewControllerWrapper: NSViewRepresentable {
func makeNSView(context: Context) -> NSView {
return ViewController().view
}
func updateNSView(_ nsView: NSView, context: Context) {
// You can add any logic you need here to update the view as needed.
}
}
class ViewController: NSViewController {
let closeButton = NSButton(title: "X", target: nil, action: nil)
override func loadView() {
self.view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 300))
self.view.wantsLayer = true
self.view.layer?.backgroundColor = NSColor.black.cgColor
}
override func viewDidLoad() {
super.viewDidLoad()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.cyan.cgColor
closeButton.target = self
closeButton.action = #selector(closeButtonPressed)
closeButton.frame = CGRect(x: 100, y: 100, width: 50, height: 30)
closeButton.bezelColor = NSColor.gray
view.addSubview(closeButton)
}
#objc func closeButtonPressed() {
print("Close button pressed")
}
}
struct ContentView: View {
private var viewControllerWrapper: ViewControllerWrapper
init(){
self.viewControllerWrapper = ViewControllerWrapper()
}
var body: some View {
VStack {
viewControllerWrapper
}
}
}
(MacOS 12.6.3, Xcode 14.2)
As you are using a NSViewController you should use NSViewControllerRepresentable instead of using NSViewRepresentable.
NSViewControllerRepresentable is explicitly for use with NSViewController while NSViewRepresentable is for use with NView.
If you can change your ViewControllerWrapper to the following:
struct ViewControllerWrapper: NSViewControllerRepresentable {
func makeNSViewController(context: Context) -> ViewController {
ViewController()
}
func updateNSViewController(_ nsViewController: ViewController, context: Context) {
}
}
This should allow the button to work.

Rasterising an NSView to NSImage in Swift

I'm trying to convert an NSView to a NSImage. The most popular solution suggested here is using bitmapImageRepForCachingDisplay which works fine most of the time. But in my particular case it fails to capture part of the view.
Here is the result I'm getting.
Input NSView:
Output Image:
As you can the code snippet highlighting is ignored in the output image. I'm using CodeEditor as the editor here. Is there an alternative way to capture the view?
import SwiftUI
import CodeEditor // from https://github.com/ZeeZide/CodeEditor
extension NSView {
func bitmapImage() -> NSImage? {
guard let rep = bitmapImageRepForCachingDisplay(in: bounds) else {
return nil
}
cacheDisplay(in: bounds, to: rep)
guard let cgImage = rep.cgImage else {
return nil
}
return NSImage(cgImage: cgImage, size: bounds.size)
}
}
extension View {
func renderAsImage() -> NSImage? {
let view = NSHostingView(rootView: self)
view.setFrameSize(view.fittingSize)
return view.bitmapImage()
}
}
struct CustomCodeEditor: View {
#State var source = """
struct ContentView: View {
var body: some View {
Text("Hello SwiftUI!")
.padding()
}
}
"""
var codeEditorView : some View {
return CodeEditor(source: $source, language: .swift, theme: .default).frame(width: 500, height: 500)
}
var body: some View{
Image(nsImage: codeEditorView.renderAsImage()!)
}
}

Create pdf with WKWebView.pdf(configuration:)

I want to create a pdf on macOS with the new WKWebView.pdf(configuration:) which was introduced in macOS 12/iOS 15. It tries to make use of the new async/await functionality (which I most likely have not grasped entirely I am afraid...).
Right now, I am getting an error that I have no idea how to handle:
Error Domain=WKErrorDomain Code=1 "An unknown error occurred"
UserInfo={NSLocalizedDescription=An unknown error occurred}
I try to load a html string into a web view, which I then want to generate the pdf from. The function I use to generate my PDFDocument looks like this:
func generatePdf() async {
let webView = WKWebView()
await webView.loadHTMLString(html, baseURL: nil)
let config = WKPDFConfiguration()
config.rect = .init(origin: .zero, size: .init(width: 595.28, height: 841.89))
do {
//this is where the error is happening
let pdfData = try await webView.pdf(configuration: config)
self.pdf = PDFDocument(data: pdfData)
} catch {
print(error) //this error gets printed
}
}
My best guess as it currently stands is that WKWebView's loadHTMLString has not finished loading the html–I did allow for outgoing connection in the app sandbox that's not it...
For the sake of completeness, here's the entire code:
import SwiftUI
import PDFKit
import WebKit
#main
struct AFPdfApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject private var vm = ViewModel()
var body: some View {
VStack {
TextEditor(text: $vm.html)
.frame(width: 300.0, height: 200.0)
.border(Color.accentColor, width: 1.0)
.padding()
PdfViewWrapper(pdfDocument: $vm.pdf)
}
.toolbar {
Button("Create PDF") {
Task {
await vm.generatePdf()
}
}
}
}
static let initHtml = """
<h1>Some fancy html</h1>
<h2>…and now how do I create a pdf from this?</h2>
"""
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class ViewModel: ObservableObject {
#Published var html = """
<h1>Some fancy html</h1>
<h2>…and now let's create some pdf…</h2>
"""
#Published var pdf: PDFDocument? = nil
func generatePdf() async {
let webView = WKWebView()
await webView.loadHTMLString(html, baseURL: nil)
let config = WKPDFConfiguration()
config.rect = .init(origin: .zero, size: .init(width: 595.28, height: 841.89))
do {
let pdfData = try await webView.pdf(configuration: config)
self.pdf = PDFDocument(data: pdfData)
} catch {
print(error)
}
}
}
struct PdfViewWrapper: NSViewRepresentable {
#Binding var pdfDocument: PDFDocument?
func makeNSView(context: Context) -> PDFView {
return PDFView()
}
func updateNSView(_ nsView: PDFView, context: Context) {
nsView.document = pdfDocument
}
}
After Chris made me take another look at it (thanks for that :-) ) I am now a step closer to a working solution.
It really seems as though I really have to wait for the webView to load the html prior to creating the pdf. While I was not able to make it work with WKWebView.pdf(configuration:), I now have a (kind of…) working solution by using WKWebView.createPDF(configuration:completionHandler:):
func generatePdf() {
webView.loadHTMLString(htmlString, baseURL: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let config = WKPDFConfiguration()
config.rect = .init(origin: .zero, size: .init(width: 595.28, height: 841.89))
self.webView.createPDF(configuration: config){ result in
switch result {
case .success(let data):
self.pdf = PDFDocument(data: data)
case .failure(let error):
print(error)
}
}
}
}
I said "kind of works" above, because the resulting pdf seems to introduce a new line after each word, which is weird–but I will scope that issue to another research/question on SO.
Again, for the sake of completeness, here's the whole "app":
import SwiftUI
import PDFKit
import WebKit
#main
struct AFPdfApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
//MARK: View
struct ContentView: View {
#StateObject private var vm = ViewModel()
var body: some View {
VStack {
TextEditor(text: $vm.htmlString)
.frame(width: 300.0, height: 200.0)
.border(Color.accentColor, width: 1.0)
.padding()
WebViewWrapper(htmlString: $vm.htmlString)
PdfViewRepresentable(pdfDocument: $vm.pdf)
}
.toolbar {
Button("Create PDF") {
Task {
vm.generatePdf()
}
}
}
}
}
//MARK: ViewModel
class ViewModel: ObservableObject {
#Published var htmlString = """
<h1>Some fancy html</h1>
<h2>…and now let's create some pdf…</h2>
"""
#Published var webView = WKWebView()
#Published var pdf: PDFDocument? = nil
func generatePdf() {
webView.loadHTMLString(htmlString, baseURL: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let config = WKPDFConfiguration()
config.rect = .init(origin: .zero, size: .init(width: 595.28, height: 841.89))
self.webView.createPDF(configuration: config){ result in
switch result {
case .success(let data):
self.pdf = PDFDocument(data: data)
case .failure(let error):
print(error)
}
}
}
}
}
//MARK: ViewRepresentables
struct WebViewWrapper: NSViewRepresentable {
#Binding var htmlString: String
public func makeNSView(context: Context) -> WKWebView {
return WKWebView()
}
public func updateNSView(_ nsView: WKWebView, context: Context) {
nsView.loadHTMLString(htmlString, baseURL: nil)
}
}
struct PdfViewRepresentable: NSViewRepresentable {
#Binding var pdfDocument: PDFDocument?
func makeNSView(context: Context) -> PDFView {
return PDFView()
}
func updateNSView(_ nsView: PDFView, context: Context) {
nsView.document = pdfDocument
}
}
Here is a way that I got this working. It uses a completely hidden WKWebView to render the content, using a delegate callback and javascript execution to determine when the page has fully loaded.
When ready, the new iOS 15 WKWebView method .pdf() is called, which generates the PDF data.
Bindings are used to update the parent view, which generates a PDFDocument from the data and navigates to it.
Caveat: The native WKWebView PDF methods do not produce paginated PDF files, so it will be one long page!
In the main view it starts with a button tap. This view has a pdfData variable that is of type Data and will be used to make the PDF document.
Button("Generate PDF") {
// Prevent triggering again while already processing
guard !isWorking else { return }
// Reset flags
isReadyToRenderPDF = false
isWorking = true
// Initialise webView with frame equal to A4 paper size in points
// (I also tried UIScreen.main.bounds)
self.wkWebView = WKWebView(frame: CGRect(origin: .zero, size: CGSize(width: 595, height: 842))) // A4 size
// Set web view navigation delegate.
// You must keep a strong reference to this, so I made it an #State property on this view
// This is a class that takes bindings so it can update the data and signal completion
self.navigationDelegate = WKWebViewDelegate(wkWebView!, pdfData: $data, isReadyToRenderPDF: $isReadyToRenderPDF)
wkWebView!.navigationDelegate = self.navigationDelegate
// Generate HTML. You could also use a simple HTML string.
// This is just my implementation.
let htmlContent = htmlComposer.makeHTMLString()
// Load HTML into web view
wkWebView!.loadHTMLString(htmlContent, baseURL: nil)
// Now the navigation delegate responds when data is updated.
}
The WKWebView delegate class has this callback method.
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// This ensures following code only runs once, as delegate method is called multiple times.
guard !readyOnce else { return }
// Use javascript to check document ready state (all images and resources loaded)
webView.evaluateJavaScript("document.readyState == \"complete\"") { result, error in
if (error != nil) {
print(error!.localizedDescription)
return
} else if result as? Int == 1 {
self.readyOnce = true
do {
// Create PDF using WKWebView method and assign it to the binding (updates data variable in main view)
self.pdfData = try await webView.pdf()
// Signal to parent view via binding
self.isReadyToRenderPDF = true
} catch {
print(error.localizedDescription)
}
} else {
return
}
}
}
Back in parent view, respond to the change of Boolean value.
.onChange(of: isReadyToRenderPDF) { _ in
isWorking = false
DispatchQueue.main.async {
// This toggle navigation link or sheet displaying PDF View using pdfData variable
isShowingPDFView = true
}
}
Finally, here is a PDFKit PDFView wrapped in UIViewRepresentable.
import SwiftUI
import PDFKit
struct PDFKitRepresentedView: UIViewRepresentable {
let data: Data
init(_ data: Data) {
self.data = data
}
func makeUIView(context: UIViewRepresentableContext<PDFKitRepresentedView>) -> PDFKitRepresentedView.UIViewType {
// Create PDFKit view and document
let pdfView = PDFView()
pdfView.document = PDFDocument(data: data)
pdfView.autoScales = true
pdfView.displaysPageBreaks = true
pdfView.usePageViewController(true, withViewOptions: nil)
pdfView.displayDirection = .horizontal
pdfView.displayMode = .singlePage
return pdfView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PDFKitRepresentedView>) {
// Not implemented
}
}

SwiftUI: Is it possible to let the user scale an image chosen with PHpicker?

I have an image picker created with PHPicker, and I was wondering if it is possible to let the user scale the chosen image?
This is not the entire code, but just the code for the makeUIViewController which I think is what is needed to solve this problem. I can of course provide the rest of the code if necessary.
This is what I'm looking for
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
config.filter = .images
config.selectionLimit = 1
let controller = PHPickerViewController(configuration: config)
controller.delegate = context.coordinator
return controller
}
can use this one line after choose the image to fixed height and width of your image
Image(room.thumbnailImage)
.resizable()
.frame(width: 32.0, height: 32.0)
or here i am sharing my running work with you checkout function didFinishPicking and var body: some View
import SwiftUI
import PhotosUI
struct PhotoPickerDemo: View {
#State private var isPresented: Bool = false
#State var pickerResult: [UIImage] = []
var config: PHPickerConfiguration {
var config = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
config.filter = .images //videos, livePhotos...
config.selectionLimit = 0 //0 => any, set 1-2-3 for har limit
return config
}
var body: some View {
ScrollView {
LazyVStack {
Button("Present Picker") {
isPresented.toggle()
}.sheet(isPresented: $isPresented) {
PhotoPicker(configuration: self.config,
pickerResult: $pickerResult,
isPresented: $isPresented)
}
ForEach(pickerResult, id: \.self) { image in
Image.init(uiImage: image)
.resizable()
.frame(width: UIScreen.main.bounds.width, height: 250, alignment: .center)
.aspectRatio(contentMode: .fit)
}
}
}
}
}
struct PhotoPicker: UIViewControllerRepresentable {
let configuration: PHPickerConfiguration
#Binding var pickerResult: [UIImage]
#Binding var isPresented: Bool
func makeUIViewController(context: Context) -> PHPickerViewController {
let controller = PHPickerViewController(configuration: configuration)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
/// PHPickerViewControllerDelegate => Coordinator
class Coordinator: PHPickerViewControllerDelegate {
private let parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
for image in results {
if image.itemProvider.canLoadObject(ofClass: UIImage.self) {
image.itemProvider.loadObject(ofClass: UIImage.self) { (newImage, error) in
if let error = error {
print(error.localizedDescription)
} else {
self.parent.pickerResult.append(newImage as! UIImage)
}
}
} else {
print("Loaded Assest is not a Image")
}
}
// dissmiss the picker
parent.isPresented = false
}
}
}
struct photoPickerDemo_Previews: PreviewProvider {
static var previews: some View {
PhotoPickerDemo()
}
}
or if you want to crop via user interface like attach picture
Step 1
Using Xcode 12, go to File -> Swift Packages -> Add Package Dependency and enter https://github.com/marshallino16/ImageCropper
Step 2
in your didFinishPicking method where you are receiving selected image pass it in this package using these lines
let ratio = CropperRatio(width: 1, height: 1)//square ratio for crop
ImageCropperView(image: Image(yourSelectedImageHere),cropRect: nil,ratio: ratio).onCropChanged { (newCrop) in
print(newCrop)//here you will receive cropped image
}
edited use of ImageCropperView
struct PhotoPicker: UIViewControllerRepresentable {
let configuration: PHPickerConfiguration
#Binding var pickerResult: [UIImage]
#Binding var isPresented: Bool
func makeUIViewController(context: Context) -> PHPickerViewController {
let controller = PHPickerViewController(configuration: configuration)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
/// PHPickerViewControllerDelegate => Coordinator
class Coordinator: PHPickerViewControllerDelegate {
private let parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
for image in results {
if image.itemProvider.canLoadObject(ofClass: UIImage.self) {
image.itemProvider.loadObject(ofClass: UIImage.self) { (newImage, error) in
if let error = error {
print(error.localizedDescription)
} else {
let ratio = CropperRatio(width: 1, height: 1)//square ratio for crop
ImageCropperView(image: Image(newImage),cropRect: nil,ratio: ratio).onCropChanged { (newCrop) in
print(newCrop)//here you will receive cropped image
}
}
}
} else {
print("Loaded Assest is not a Image")
}
}
// dissmiss the picker
parent.isPresented = false
}
}
}

Update image in UIImageView

I try to make UIKit element with replaceable image in it, that I can use like swiftUI element.
I stuck at the moment, when image in UIImageView should be refreshed with imageInBlackBox from ObservableObject. I try'd to set new imageInBlackBox from updateUIView and from imagePickerController after image has been selected. But both of this methods don't update UIImageView, it works only for swiftUI Image element, that I use for test.
How I should make imageView.image be refreshed after imageInBlackBox change?
//Data model
struct imageAdderDataHolder: Identifiable, Hashable {
var id: UUID = UUID()
var isShowingImagePicker:Bool = false
var imageInBlackBox:UIImage = UIImage(systemName: "photo")! // image for replace
var height: CGFloat = 160
var width: CGFloat = 160
}
//Data storage
class imageAdderData: ObservableObject{
init() {}
static let shared = imageAdderData()
#Published var img1: imageAdderDataHolder = imageAdderDataHolder()
}
struct simpleadder: UIViewRepresentable{
#ObservedObject var imageData: imageAdderData = .shared
let mainView: UIView = UIView()
var imageView: UIImageView = UIImageView()
func makeUIView(context: Context) -> UIView {
imageView.image = imageData.img1.imageInBlackBox
imageView.frame.size.width = imageData.img1.width
imageView.frame.size.height = imageData.img1.height
imageView.contentMode = .scaleAspectFit
mainView.addSubview(imageView)
return mainView
}
func updateUIView(_ uiView: UIView, context: Context) {
imageView.image = imageData.img1.imageInBlackBox // try to replace image
}
}
// swiftui view for test
struct photoadder: View {
#ObservedObject var imageData: imageAdderData = .shared
var body: some View {
VStack{
HStack{
simpleadder()
.frame(width: imageData.img1.width, height: imageData.img1.height)
.border(Color.black, width:1)
.sheet(isPresented: $imageData.img1.isShowingImagePicker, content: {
imagePickerUIView(isPresented: $imageData.img1.isShowingImagePicker)
})
Image(uiImage: imageData.img1.imageInBlackBox) // working element for test
.resizable()
.aspectRatio(contentMode: ContentMode.fit)
.frame(width: imageData.img1.width, height: imageData.img1.height)
.border(Color.black, width: 1)
}
Button("change image") {
imageData.img1.isShowingImagePicker = true
}
}
}
}
struct imagePickerUIView: UIViewControllerRepresentable {
#ObservedObject var imageData: imageAdderData = .shared
#Binding var isPresented: Bool
func makeUIViewController(context:
UIViewControllerRepresentableContext<imagePickerUIView>) ->
UIViewController {
let controller = UIImagePickerController()
controller.delegate = context.coordinator
return controller
}
func makeCoordinator() -> imagePickerUIView.Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
let parent: imagePickerUIView
init(parent: imagePickerUIView) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info:
[UIImagePickerController.InfoKey : Any]) {
if let selectedImageFromPicker = info[.originalImage] as? UIImage {
// try to replace image
parent.imageData.img1.imageInBlackBox = selectedImageFromPicker
}
self.parent.isPresented = false
}
}
func updateUIViewController(_ uiViewController:
imagePickerUIView.UIViewControllerType, context:
UIViewControllerRepresentableContext<imagePickerUIView>) {
}
}
Here is a solution - in representable all views should be created inside makeUIView, because struct can be (and usually does) recreated on parent update (so internal instances will be recreated as well, while made UIKit views life-cycle persisted).
Tested with Xcode 12.4 / iOS 14.4.
Fixed code:
struct simpleadder: UIViewRepresentable{
#ObservedObject var imageData: imageAdderData = .shared
func makeUIView(context: Context) -> UIView {
let mainView: UIView = UIView() // << create here !!
let imageView: UIImageView = UIImageView() // << create here !!
imageView.image = imageData.img1.imageInBlackBox
imageView.frame.size.width = imageData.img1.width
imageView.frame.size.height = imageData.img1.height
imageView.contentMode = .scaleAspectFit
mainView.addSubview(imageView)
return mainView
}
func updateUIView(_ uiView: UIView, context: Context) {
// find needed view in run-time
if let imageView = uiView.subviews.first as? UIImageView {
imageView.image = imageData.img1.imageInBlackBox
}
}
}